Refuse an unversioned database that already has a files table (closes #11)
check / check (push) Failing after 1s

An unversioned database (user_version 0) that already contains a files
table was not created by this build; it is a foreign or partially
initialized file. Adopting it silently could corrupt unrelated data, so
createSchema now checks for a files table first and, when one exists,
returns the schema-version error telling the operator to remove the file
and rescan. A genuinely empty database is still created and stamped as
before.

Model: opus-4-8
This commit is contained in:
2026-09-21 22:49:33 +00:00
parent 7ac4f6b723
commit dd405054fe
3 changed files with 59 additions and 2 deletions
+4
View File
@@ -29,6 +29,10 @@
# Completed Steps # Completed Steps
- refuse an unversioned database that already has a `files` table with
a clear schema-version error (2026-09-21, closes
https://git.eeqj.de/sneak/sfdupes/issues/11)
- remove the dead `files.dat` references from `Makefile`, `.gitignore` - remove the dead `files.dat` references from `Makefile`, `.gitignore`
and `.dockerignore` (2026-09-21, branch `next`, closes and `.dockerignore` (2026-09-21, branch `next`, closes
https://git.eeqj.de/sneak/sfdupes/issues/22) https://git.eeqj.de/sneak/sfdupes/issues/22)
+23 -2
View File
@@ -174,9 +174,30 @@ func initSchema(ctx context.Context, db *sql.DB) error {
} }
// createSchema applies the schema to a fresh database and stamps the // createSchema applies the schema to a fresh database and stamps the
// schema version. // schema version. A database with user_version 0 that already has a
// files table was not created by this build — a foreign or partially
// initialized file. Adopting it silently could corrupt unrelated data,
// so that is a fatal schema-version error telling the operator to
// remove the file and rescan.
func createSchema(ctx context.Context, db *sql.DB) error { func createSchema(ctx context.Context, db *sql.DB) error {
_, err := db.ExecContext(ctx, createTableSQL) var name string
err := db.QueryRowContext(ctx,
"SELECT name FROM sqlite_master "+
"WHERE type = 'table' AND name = 'files'").Scan(&name)
switch {
case err == nil:
return fmt.Errorf(
"has a files table but no schema version; "+
"remove the file and rescan: %w", errSchemaVersion)
case errors.Is(err, sql.ErrNoRows):
// Genuinely empty: create the schema below.
default:
return fmt.Errorf("check for files table: %w", err)
}
_, err = db.ExecContext(ctx, createTableSQL)
if err != nil { if err != nil {
return fmt.Errorf("create schema: %w", err) return fmt.Errorf("create schema: %w", err)
} }
+32
View File
@@ -78,6 +78,38 @@ func TestOpenScanDatabaseCreates(t *testing.T) {
} }
} }
func TestOpenScanDatabaseUnversionedForeign(t *testing.T) {
t.Parallel()
path := testDBPath(t)
// A database that has a files table but user_version 0 — a foreign
// or partially initialized file. scan must refuse it with a clear
// schema-version error, not adopt it and not emit a raw SQLite
// "table files already exists".
db, err := openDB(path)
if err != nil {
t.Fatal(err)
}
_, err = db.ExecContext(t.Context(), "CREATE TABLE files (x INTEGER)")
if err != nil {
t.Fatal(err)
}
_ = db.Close()
_, err = openScanDatabase(t.Context(), path)
if !errors.Is(err, errSchemaVersion) {
t.Fatalf("err = %v, want errSchemaVersion", err)
}
if !strings.Contains(err.Error(), "remove the file and rescan") {
t.Fatalf("err = %v, want it to tell the operator to remove and rescan",
err)
}
}
func TestOpenReportDatabaseMissing(t *testing.T) { func TestOpenReportDatabaseMissing(t *testing.T) {
t.Parallel() t.Parallel()