package registry import ( "database/sql" "fmt" _ "modernc.org/sqlite" ) // Open opens the registry database at the given path and runs migrations. func Open(path string) (*sql.DB, error) { db, err := sql.Open("sqlite", path) if err != nil { return nil, fmt.Errorf("open database: %w", err) } for _, pragma := range []string{ "PRAGMA journal_mode = WAL", "PRAGMA foreign_keys = ON", "PRAGMA busy_timeout = 5000", } { if _, err := db.Exec(pragma); err != nil { _ = db.Close() return nil, fmt.Errorf("exec %q: %w", pragma, err) } } if err := migrate(db); err != nil { _ = db.Close() return nil, fmt.Errorf("migrate: %w", err) } return db, nil } func migrate(db *sql.DB) error { _, err := db.Exec(` CREATE TABLE IF NOT EXISTS schema_migrations ( version INTEGER PRIMARY KEY, applied_at TEXT NOT NULL DEFAULT (datetime('now')) ); `) if err != nil { return fmt.Errorf("create migrations table: %w", err) } for i, m := range migrations { version := i + 1 var count int if err := db.QueryRow("SELECT COUNT(*) FROM schema_migrations WHERE version = ?", version).Scan(&count); err != nil { return fmt.Errorf("check migration %d: %w", version, err) } if count > 0 { continue } if _, err := db.Exec(m); err != nil { return fmt.Errorf("run migration %d: %w", version, err) } if _, err := db.Exec("INSERT INTO schema_migrations (version) VALUES (?)", version); err != nil { return fmt.Errorf("record migration %d: %w", version, err) } } return nil } var migrations = []string{ // Migration 1: initial schema ` CREATE TABLE IF NOT EXISTS services ( name TEXT PRIMARY KEY, active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS components ( name TEXT NOT NULL, service TEXT NOT NULL REFERENCES services(name) ON DELETE CASCADE, image TEXT NOT NULL, network TEXT NOT NULL DEFAULT 'bridge', user_spec TEXT NOT NULL DEFAULT '', restart TEXT NOT NULL DEFAULT 'unless-stopped', desired_state TEXT NOT NULL DEFAULT 'running', observed_state TEXT NOT NULL DEFAULT 'unknown', version TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')), PRIMARY KEY (service, name) ); CREATE TABLE IF NOT EXISTS component_ports ( service TEXT NOT NULL, component TEXT NOT NULL, mapping TEXT NOT NULL, PRIMARY KEY (service, component, mapping), FOREIGN KEY (service, component) REFERENCES components(service, name) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS component_volumes ( service TEXT NOT NULL, component TEXT NOT NULL, mapping TEXT NOT NULL, PRIMARY KEY (service, component, mapping), FOREIGN KEY (service, component) REFERENCES components(service, name) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS component_cmd ( service TEXT NOT NULL, component TEXT NOT NULL, position INTEGER NOT NULL, arg TEXT NOT NULL, PRIMARY KEY (service, component, position), FOREIGN KEY (service, component) REFERENCES components(service, name) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS events ( id INTEGER PRIMARY KEY AUTOINCREMENT, service TEXT NOT NULL, component TEXT NOT NULL, prev_state TEXT NOT NULL, new_state TEXT NOT NULL, timestamp TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_events_component_time ON events(service, component, timestamp); `, // Migration 2: component routes ` CREATE TABLE IF NOT EXISTS component_routes ( service TEXT NOT NULL, component TEXT NOT NULL, name TEXT NOT NULL, port INTEGER NOT NULL, mode TEXT NOT NULL DEFAULT 'l4', hostname TEXT NOT NULL DEFAULT '', host_port INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (service, component, name), FOREIGN KEY (service, component) REFERENCES components(service, name) ON DELETE CASCADE ); `, }