Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9be8731dc8 |
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user