gastownhall/beads · error
legacy SQLite schema drift in %s
Error message
legacy SQLite schema drift in %s
What it means
verifyTable builds a canonical description of each table's columns — name|type|notNull|default|pk joined per row — and compares the joined string to an exact expected string from the package's schema map. Any difference in column names, order, types, nullability, defaults, or primary-key flags produces 'legacy SQLite schema drift in <table>'. This enforces the package's promise to migrate only exact, audited layouts.
Source
Thrown at internal/migration/legacysqlite/reader.go:355
var notNull, pk int
var defaultValue sql.NullString
if err := rows.Scan(&cid, &name, &typ, ¬Null, &defaultValue, &pk, &hidden); err != nil {
return err
}
if hidden != 0 {
return fmt.Errorf("legacy SQLite schema drift in %s hidden column", table)
}
defaultText := "-"
if defaultValue.Valid {
defaultText = defaultValue.String
}
got = append(got, fmt.Sprintf("%s|%s|%d|%s|%d", name, typ, notNull, defaultText, pk))
}
if err := rows.Err(); err != nil {
return err
}
if strings.Join(got, " ") != want {
return fmt.Errorf("legacy SQLite schema drift in %s", table)
}
return nil
}
func verifyFKs(ctx context.Context, db *sql.Tx, table string) error {
rows, err := db.QueryContext(ctx, "PRAGMA foreign_key_list("+table+")")
if err != nil {
return err
}
defer rows.Close()
count := 0
for rows.Next() {
var id, seq int
var target, from, to, update, deleteAction, match string
if err := rows.Scan(&id, &seq, &target, &from, &to, &update, &deleteAction, &match); err != nil {
return err
}
if table == "issues" || table == "metadata" || target != "issues" || from != "issue_id" || to != "id" || update != "NO ACTION" || deleteAction != "CASCADE" || match != "NONE" {View on GitHub (pinned to 71377f2769)
Solutions
- Diff your schema against the expected one: run PRAGMA table_xinfo(<table>) for each table and compare column-by-column (name, type, notnull, dflt_value, pk, order)
- Restore the database from a backup created by the supported bd release
- Rebuild the drifted table with the exact legacy schema and copy the rows across, then retry
- Use a bd build that accepts your database's actual legacy version (see the bd_version check) instead of hand-adjusting the schema
Example fix
-- find the drift sqlite3 beads.db "PRAGMA table_xinfo(dependencies);" -- e.g. shows an extra column 'thread_id TEXT' added by a tool -- fix: rebuild without it sqlite3 beads.db "CREATE TABLE dependencies_new (issue_id TEXT NOT NULL, depends_on_id TEXT NOT NULL, type TEXT NOT NULL DEFAULT 'blocks', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_by TEXT NOT NULL, metadata TEXT, thread_id TEXT, PRIMARY KEY(issue_id, depends_on_id, type)); INSERT INTO dependencies_new SELECT issue_id,depends_on_id,type,created_at,created_by,metadata,thread_id FROM dependencies; DROP TABLE dependencies; ALTER TABLE dependencies_new RENAME TO dependencies;"
Defensive patterns
Strategy: validation
Validate before calling
// pre-flight: compare each table's shape to the expected legacy contract
func checkSchemaShape(dbPath string) error {
db, err := sql.Open("sqlite3", dbPath+"?mode=ro"); if err != nil { return err }
defer db.Close()
for _, t := range []string{"metadata","issues","dependencies","labels","comments"} {
rows, err := db.Query("PRAGMA table_xinfo(" + t + ")"); if err != nil { return err }
var cols []string
for rows.Next() {
var cid, hidden, nn, pk int; var name, typ string; var d sql.NullString
if err := rows.Scan(&cid, &name, &typ, &nn, &d, &pk, &hidden); err != nil { rows.Close(); return err }
cols = append(cols, name+"|"+typ)
}
rows.Close()
fmt.Printf("%s: %s\n", t, strings.Join(cols, " ")) // diff this against the documented schema
}
return nil
} Try / catch
if err := legacysqlite.Export(ctx, src, out, os.Stdout); err != nil {
if strings.Contains(err.Error(), "schema drift in") && !strings.Contains(err.Error(), "hidden column") {
return fmt.Errorf("table layout differs from the supported legacy contract; restore from a backup or rebuild the table with the canonical schema")
}
return err
} Prevention
- Don't ALTER TABLE legacy databases that will be migrated; migrate first, change later
- Create databases only with the supported bd release so the schema matches the audited contract
- Keep backups taken by the original release for restoration if drift appears
- Compare PRAGMA table_info output against the documented schema before scheduling migration
When it happens
Trigger: Export -> read -> verify -> verifyTable: strings.Join(got, " ") != want for one of metadata, issues, dependencies, labels, comments. Any added/dropped/reordered column, changed type or NOT NULL/default/PK attribute triggers it.
Common situations: Migrating a database created by a different bd release whose schema differed; manual ALTER TABLE / column additions; ORM or migration tools that touched the legacy database; reproducing the schema by hand with slightly different types or defaults.
Related errors
- legacy SQLite schema drift in %s hidden column
- legacy SQLite release marker: %w
- legacy SQLite issue %s uses unsupported removed fields
- clone from %s succeeded, but the database needs %d schema %s
- sealed legacy SQLite database does not match source fingerpr
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/79413c5d8f5fe758.
Report an issue: GitHub.