Drop schema migrations

The index is a cache. Everything in it is re-derived from the statements and
rules.toml, and has been since the last hand-set tags went, so keeping
machinery to nurse an old index through a schema change was paying for a
guarantee nothing needs. The ALTER TABLE lists, the dropped-column list and the
pragma_table_info reader are gone; Open applies the schema and returns.

What replaces it is a line in the release note: delete index.db and import
again. The two directions are not symmetric, which is worth knowing before
assuming something is broken. Removing a column leaves an older index working,
carrying the dead column and its data unread, because every statement here
names its columns -- that is why the drop half was never really load-bearing.
Adding a column the code reads breaks every command against an older index with
`no such column` until the file is deleted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 23:39:07 +02:00
co-authored by Claude Opus 5
parent 02814d09e5
commit 93491016cd
3 changed files with 29 additions and 71 deletions
+5 -71
View File
@@ -58,74 +58,12 @@ CREATE INDEX IF NOT EXISTS idx_txn_date ON transactions(date);
CREATE INDEX IF NOT EXISTS idx_txn_account ON transactions(account_id);
`
// migrations bring an index created by an older build up to date. SQLite
// errors on a duplicate column, which is how we detect "already applied".
var migrations = []string{
`ALTER TABLE transactions ADD COLUMN type TEXT NOT NULL DEFAULT ''`,
`ALTER TABLE transactions ADD COLUMN balance_minor INTEGER`,
}
// dropped are columns an older build created that this one no longer reads.
// Nothing indexes or constrains them, so they can simply go; leaving them
// would keep a NOT NULL column alive that no INSERT here ever names.
var dropped = []string{"counterparty", "rule_transfer", "manual_transfer", "manual_tag"}
// migrate brings an index created by an older build up to date, adding the
// columns it lacks and removing the ones it should no longer have.
func migrate(db *sql.DB) error {
have, err := columns(db, "transactions")
if err != nil {
return err
}
for _, stmt := range migrations {
name := addedColumn(stmt)
if have[name] {
continue
}
if _, err := db.Exec(stmt); err != nil {
return fmt.Errorf("migrate (%s): %w", stmt, err)
}
}
for _, name := range dropped {
if !have[name] {
continue
}
stmt := fmt.Sprintf(`ALTER TABLE transactions DROP COLUMN %s`, name)
if _, err := db.Exec(stmt); err != nil {
return fmt.Errorf("migrate (%s): %w", stmt, err)
}
}
return nil
}
func columns(db *sql.DB, table string) (map[string]bool, error) {
rows, err := db.Query(`SELECT name FROM pragma_table_info(?)`, table)
if err != nil {
return nil, fmt.Errorf("inspect %s: %w", table, err)
}
defer rows.Close()
out := map[string]bool{}
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return nil, err
}
out[name] = true
}
return out, rows.Err()
}
// addedColumn pulls the column name out of an ADD COLUMN statement.
func addedColumn(stmt string) string {
_, rest, ok := strings.Cut(stmt, "ADD COLUMN ")
if !ok {
return ""
}
name, _, _ := strings.Cut(rest, " ")
return name
}
// Open opens (creating if needed) the index at path.
//
// There is no schema migration: the index caches what the statements and
// rules.toml already say, so a build that changes the schema is answered by
// deleting index.db and importing again, not by patching the old one in place.
// Until it is deleted, queries against it fail on the columns it lacks.
func Open(path string) (*DB, error) {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return nil, fmt.Errorf("create index dir: %w", err)
@@ -138,10 +76,6 @@ func Open(path string) (*DB, error) {
sqlDB.Close()
return nil, fmt.Errorf("apply schema: %w", err)
}
if err := migrate(sqlDB); err != nil {
sqlDB.Close()
return nil, err
}
return &DB{sql: sqlDB}, nil
}