// Package masterdb provides the SQLite database for the mcp-master daemon. // It stores the cluster-wide node registry, service placements, and edge routes. // This is separate from the agent's registry (internal/registry/) because the // master and agent have fundamentally different schemas. package masterdb import ( "database/sql" "fmt" _ "modernc.org/sqlite" ) // Open opens the master 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: cluster state ` CREATE TABLE IF NOT EXISTS nodes ( name TEXT PRIMARY KEY, address TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'worker', arch TEXT NOT NULL DEFAULT 'amd64', status TEXT NOT NULL DEFAULT 'unknown', containers INTEGER NOT NULL DEFAULT 0, last_heartbeat TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS placements ( service_name TEXT PRIMARY KEY, node TEXT NOT NULL REFERENCES nodes(name), tier TEXT NOT NULL DEFAULT 'worker', deployed_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS edge_routes ( hostname TEXT PRIMARY KEY, service_name TEXT NOT NULL, edge_node TEXT NOT NULL REFERENCES nodes(name), backend_hostname TEXT NOT NULL, backend_port INTEGER NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_edge_routes_service ON edge_routes(service_name); `, }