store: introduce versioned migrations with ordered runner + atomic apply
The old migrate() helper only knew how to re-run a fixed slab of CREATE
TABLE IF NOT EXISTS plus per-column ensureColumnExists calls. That worked
while every schema change was a benign additive column; it falls apart
as soon as we need a data backfill, an index, a rename, or anything that
has to happen exactly once in a known order.
Replaces it with a schema_migrations table + ordered []migration slice.
Each migration has a unique id, a human-readable name, and a func(*Tx)
body; the runner opens a transaction per migration so DDL and any data
changes either both land and get recorded or both roll back together,
leaving the DB in a state where retrying on next Open() reapplies from
the same point.
Migration 1 ("baseline") collapses the current schema into one entry:
fresh databases apply it in one shot; existing dev databases see
idempotent `CREATE TABLE IF NOT EXISTS` + `ALTER TABLE … ADD COLUMN`
statements that succeed as no-ops, and the only net effect is the
schema_migrations row that brings them into the versioned system.
Tests cover fresh apply, idempotent re-open, skipping already-applied
ids, rollback on body error (the transient table the migration created
must not survive), and duplicate-id rejection.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b930c51990
commit
34dd7644d8
3 changed files with 354 additions and 78 deletions
|
|
@ -31,7 +31,7 @@ func Open(path string) (*Store, error) {
|
|||
return nil, err
|
||||
}
|
||||
store := &Store{db: db}
|
||||
if err := store.migrate(); err != nil {
|
||||
if err := runMigrations(db); err != nil {
|
||||
_ = db.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -66,54 +66,6 @@ func sqliteDSN(path string) (string, error) {
|
|||
}).String(), nil
|
||||
}
|
||||
|
||||
func (s *Store) migrate() error {
|
||||
stmts := []string{
|
||||
`CREATE TABLE IF NOT EXISTS images (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
managed INTEGER NOT NULL DEFAULT 0,
|
||||
artifact_dir TEXT,
|
||||
rootfs_path TEXT NOT NULL,
|
||||
work_seed_path TEXT,
|
||||
kernel_path TEXT NOT NULL,
|
||||
initrd_path TEXT,
|
||||
modules_dir TEXT,
|
||||
packages_path TEXT,
|
||||
build_size TEXT,
|
||||
seeded_ssh_public_key_fingerprint TEXT,
|
||||
docker INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);`,
|
||||
`CREATE TABLE IF NOT EXISTS vms (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
image_id TEXT NOT NULL,
|
||||
guest_ip TEXT NOT NULL UNIQUE,
|
||||
state TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
last_touched_at TEXT NOT NULL,
|
||||
spec_json TEXT NOT NULL,
|
||||
runtime_json TEXT NOT NULL,
|
||||
stats_json TEXT NOT NULL DEFAULT '{}',
|
||||
FOREIGN KEY(image_id) REFERENCES images(id) ON DELETE RESTRICT
|
||||
);`,
|
||||
}
|
||||
for _, stmt := range stmts {
|
||||
if _, err := s.db.Exec(stmt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := ensureColumnExists(s.db, "images", "work_seed_path", "TEXT"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureColumnExists(s.db, "images", "seeded_ssh_public_key_fingerprint", "TEXT"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) UpsertImage(ctx context.Context, image model.Image) error {
|
||||
s.writeMu.Lock()
|
||||
defer s.writeMu.Unlock()
|
||||
|
|
@ -432,35 +384,6 @@ func scanVMInto(row scanner) (model.VMRecord, error) {
|
|||
return vm, nil
|
||||
}
|
||||
|
||||
func ensureColumnExists(db *sql.DB, table, column, columnType string) error {
|
||||
rows, err := db.Query(fmt.Sprintf("PRAGMA table_info(%s)", table))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var (
|
||||
cid int
|
||||
name string
|
||||
valueType string
|
||||
notNull int
|
||||
defaultV sql.NullString
|
||||
pk int
|
||||
)
|
||||
if err := rows.Scan(&cid, &name, &valueType, ¬Null, &defaultV, &pk); err != nil {
|
||||
return err
|
||||
}
|
||||
if name == column {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = db.Exec(fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s", table, column, columnType))
|
||||
return err
|
||||
}
|
||||
|
||||
func boolToInt(value bool) int {
|
||||
if value {
|
||||
return 1
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue