migrate.go
| 1 | package db |
| 2 | |
| 3 | import "context" |
| 4 | |
| 5 | // addColumn describes one ALTER TABLE ADD COLUMN migration step. |
| 6 | type addColumn struct { |
| 7 | table string |
| 8 | column string |
| 9 | ddl string |
| 10 | } |
| 11 | |
| 12 | // schema.sql is the source of truth and runs first, so every table already |
| 13 | // exists here. Column-level changes cannot be expressed with IF NOT EXISTS, |
| 14 | // so they are applied explicitly and guarded by a pragma check. |
| 15 | var columnMigrations = []addColumn{ |
| 16 | {"users", "is_pending", "ALTER TABLE users ADD COLUMN is_pending INTEGER NOT NULL DEFAULT 0"}, |
| 17 | {"users", "register_application", "ALTER TABLE users ADD COLUMN register_application TEXT"}, |
| 18 | {"users", "passkey_setup_started_at", "ALTER TABLE users ADD COLUMN passkey_setup_started_at TEXT"}, |
| 19 | {"patches", "version", "ALTER TABLE patches ADD COLUMN version TEXT NOT NULL DEFAULT ''"}, |
| 20 | {"repositories", "allow_user_labels", "ALTER TABLE repositories ADD COLUMN allow_user_labels INTEGER NOT NULL DEFAULT 0"}, |
| 21 | {"ci_runs", "repo_run_id", "ALTER TABLE ci_runs ADD COLUMN repo_run_id INTEGER"}, |
| 22 | } |
| 23 | |
| 24 | // migrate brings an existing database file up to date. |
| 25 | func (d *DB) migrate(ctx context.Context) error { |
| 26 | for _, m := range columnMigrations { |
| 27 | has, err := d.hasColumn(ctx, m.table, m.column) |
| 28 | if err != nil { |
| 29 | return err |
| 30 | } |
| 31 | if has { |
| 32 | continue |
| 33 | } |
| 34 | if _, err := d.ExecContext(ctx, m.ddl); err != nil { |
| 35 | return err |
| 36 | } |
| 37 | } |
| 38 | // Backfill patch versions left empty by the column migration above. |
| 39 | if _, err := d.ExecContext(ctx, |
| 40 | "UPDATE patches SET version = lower(hex(randomblob(16))) WHERE version = ''"); err != nil { |
| 41 | return err |
| 42 | } |
| 43 | // Expired sessions serve no purpose after a restart. |
| 44 | return d.DeleteExpiredSessions(ctx) |
| 45 | } |
| 46 | |
| 47 | func (d *DB) hasColumn(ctx context.Context, table, column string) (bool, error) { |
| 48 | var n int |
| 49 | err := d.QueryRowContext(ctx, |
| 50 | `SELECT COUNT(*) FROM pragma_table_info(?) WHERE name = ?`, table, column).Scan(&n) |
| 51 | return n > 0, err |
| 52 | } |
| 53 |