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, &notNull, &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

  1. 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)
  2. Restore the database from a backup created by the supported bd release
  3. Rebuild the drifted table with the exact legacy schema and copy the rows across, then retry
  4. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/79413c5d8f5fe758. Report an issue: GitHub.