migrate.go
⎇
Raw
1package db
2
3import "context"
4
5// addColumn describes one ALTER TABLE ADD COLUMN migration step.
6type 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.
15var 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.
25func (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
47func (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