Refuse an unversioned database that already has a files table (closes #11)
check / check (push) Failing after 2s
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 (rebase)
This commit was merged in pull request #58.
This commit is contained in:
@@ -219,6 +219,12 @@ func openReportDatabase(ctx context.Context,
|
||||
}
|
||||
|
||||
v, err := userVersion(ctx, db)
|
||||
if err == nil && v == 0 {
|
||||
// An empty database passes this check and fails the version
|
||||
// check below.
|
||||
err = checkUnversioned(ctx, db)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
_ = db.Close()
|
||||
|
||||
@@ -245,6 +251,11 @@ func initSchema(ctx context.Context, db *sql.DB) error {
|
||||
|
||||
switch v {
|
||||
case 0:
|
||||
err = checkUnversioned(ctx, db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return createSchema(ctx, db)
|
||||
case schemaVersion:
|
||||
return nil
|
||||
@@ -254,25 +265,65 @@ 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. 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
|
||||
|
||||
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):
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("check for files table: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user