Author SHA1 Message Date
sneak d111b3c665 Trim the TODO.md Completed Steps entry to three lines
check / check (push) Failing after 1s
Drop the raw-SQLite rationale so the entry gives what landed, the date,
and the issue URL within the three-line cap.

Model: opus-4-8
2026-09-21 07:44:39 +00:00
sneak b2a4f748b7 Refuse an unversioned database that already has a files table (closes #11)
check / check (push) Successful in 2m22s
scan opened any database whose user_version was 0 as empty and ran an
unconditional CREATE TABLE files, so a foreign or partially initialized
file with a files table failed with the raw SQLite "table files already
exists", which explains nothing. createSchema now checks for an existing
files table first and, when one is present at version 0, reports the
schema-version error telling the operator to remove the file and rescan,
matching the README rule that an unrecognized database is fatal.

Model: opus-4-8
2026-09-21 07:23:09 +00:00
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, branch `next`, closes
https://git.eeqj.de/sneak/sfdupes/issues/11)
- fix the lint-image pin comments and `FROM` form in `Dockerfile` and - fix the lint-image pin comments and `FROM` form in `Dockerfile` and
`Dockerfile.lint` (2026-08-10, branch `next`, closes `Dockerfile.lint` (2026-08-10, branch `next`, closes
https://git.eeqj.de/sneak/sfdupes/issues/25): dropped the false https://git.eeqj.de/sneak/sfdupes/issues/25): dropped the false
+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()