Refuse an unversioned database that already has a files table (closes #11) #58
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user