1 Commits
Author SHA1 Message Date
sneak 9be8731dc8 Refuse an unversioned database that already has a files table (closes #11)
check / check (push) Failing after 2s
A database at user_version 0 that already has a files table was made
by something else: scan used to run its CREATE TABLE on it and fail
with a raw SQLite error, and report and trees gave only a bare version
mismatch. All three now refuse such a database with the schema-version
error telling the operator to remove the file and rescan. scan creates
the table and index and sets the version in one transaction, so a first
scan stopped partway leaves an empty database the next scan sets up,
never a files table at version 0. A genuinely empty database is
unchanged.

Model: opus-4-8 (implementation); opus-5-5 (rework)
2026-10-04 09:04:55 +00:00
4 changed files with 65 additions and 14 deletions
+6 -4
View File
@@ -209,10 +209,12 @@ All three subcommands operate on a single SQLite database file:
`-shm` files beside it, which SQLite creates with the database
file's permissions.
- Schema (`PRAGMA user_version` is the schema version, currently 1; a
database with any other version is a fatal error. A database at
version 0 that already has a `files` table was not made by sfdupes;
every subcommand refuses it with an error telling the user to remove
the file and rescan):
database with any other version is a fatal error. `scan` creates
the schema and sets the version in one transaction, so a first scan
stopped while doing so leaves an empty database the next scan sets
up. A database at version 0 that already has a `files` table was
therefore not made by sfdupes; every subcommand refuses it with an
error telling the user to remove the file and rescan):
```sql
CREATE TABLE files (
+2 -2
View File
@@ -29,8 +29,8 @@
# Completed Steps
- refuse an unversioned database that already has a `files` table with
a clear schema-version error (2026-10-04,
- `scan` creates the schema in one transaction; a version-0 database with a
`files` table is refused with a clear schema-version error (2026-10-04,
https://git.eeqj.de/sneak/sfdupes/issues/11)
- the `Dockerfile` build stage keeps the Go module cache out of `builder`'s
+23 -8
View File
@@ -266,10 +266,10 @@ func initSchema(ctx context.Context, db *sql.DB) error {
}
// checkUnversioned checks a database at user_version 0 before it is
// taken for an empty one. A database that already has a files table
// was not created by this build: a foreign or partially initialized
// file. Adopting it could corrupt unrelated data, so that is a
// schema-version error telling the operator to remove the file and
// taken for an empty one. createSchema creates the files table and
// sets the version together, so a files table at version 0 was made by
// something else. Adopting it could corrupt unrelated data, so that is
// a schema-version error telling the operator to remove the file and
// rescan.
func checkUnversioned(ctx context.Context, db *sql.DB) error {
var name string
@@ -291,24 +291,39 @@ func checkUnversioned(ctx context.Context, db *sql.DB) error {
}
// createSchema applies the schema to a fresh database and stamps the
// schema version.
// schema version in one transaction, so a creation stopped partway, by
// an interrupt or an error, leaves an empty database the next scan
// sets up, never a files table at version 0, which checkUnversioned
// refuses.
func createSchema(ctx context.Context, db *sql.DB) error {
_, err := db.ExecContext(ctx, createTableSQL)
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("create schema: %w", err)
}
_, err = db.ExecContext(ctx, createIndexSQL)
defer func() { _ = tx.Rollback() }()
_, err = tx.ExecContext(ctx, createTableSQL)
if err != nil {
return fmt.Errorf("create schema: %w", err)
}
_, err = db.ExecContext(ctx,
_, err = tx.ExecContext(ctx, createIndexSQL)
if err != nil {
return fmt.Errorf("create schema: %w", err)
}
_, err = tx.ExecContext(ctx,
"PRAGMA user_version = "+strconv.Itoa(schemaVersion))
if err != nil {
return fmt.Errorf("set schema version: %w", err)
}
err = tx.Commit()
if err != nil {
return fmt.Errorf("create schema: %w", err)
}
return nil
}
+34
View File
@@ -114,6 +114,40 @@ func TestOpenDatabaseUnversionedForeign(t *testing.T) {
}
}
func TestSchemaCreationStoppedPartway(t *testing.T) {
t.Parallel()
// A first scan stopped while creating the schema must leave a
// database the next scan accepts. max_page_count(2) leaves room for
// the files table but not its index, so schema creation fails right
// after CREATE TABLE, a point an interrupt could also stop it at.
path := testDBPath(t)
db, err := openDB(path, scanParams+"&_pragma=max_page_count(2)")
if err != nil {
t.Fatal(err)
}
err = initSchema(t.Context(), db)
_ = db.Close()
if err == nil {
t.Fatal("initSchema with no room for the index succeeded")
}
db, err = openScanDatabase(t.Context(), path)
if err != nil {
t.Fatalf("next scan: %v", err)
}
defer func() { _ = db.Close() }()
v, err := userVersion(t.Context(), db)
if err != nil || v != schemaVersion {
t.Fatalf("userVersion = %d, %v; want %d, nil", v, err, schemaVersion)
}
}
func TestOpenReportDatabaseMissing(t *testing.T) {
t.Parallel()