package database import ( "database/sql" "log" "strings" _ "github.com/mattn/go-sqlite3" ) type Migrator func(*sql.DB) (*sql.DB, error) func MigrateUsers(dbConn *sql.DB) (*sql.DB, error) { log.Println("migrating users table") _, err := dbConn.Exec(`CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, username TEXT NOT NULL, primary_mac TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP );`) if err != nil { return dbConn, err } _, err = dbConn.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username ON users (username);`) return dbConn, err } // MigrateUserPrimaryMac adds primary_mac to pre-existing users tables. It's // idempotent: SQLite has no "ADD COLUMN IF NOT EXISTS", so the duplicate-column // error (column already present, e.g. on a fresh DB) is swallowed. func MigrateUserPrimaryMac(dbConn *sql.DB) (*sql.DB, error) { log.Println("migrating users.primary_mac column") _, err := dbConn.Exec(`ALTER TABLE users ADD COLUMN primary_mac TEXT;`) if err != nil && !strings.Contains(err.Error(), "duplicate column name") { return dbConn, err } return dbConn, nil } // MigrateDNSRecords holds the user-editable DNS records served for the LAN zone. func MigrateDNSRecords(dbConn *sql.DB) (*sql.DB, error) { log.Println("migrating dns_records table") _, err := dbConn.Exec(`CREATE TABLE IF NOT EXISTS dns_records ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, name TEXT NOT NULL, type TEXT NOT NULL, content TEXT NOT NULL, ttl INTEGER NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE );`) if err != nil { return dbConn, err } _, err = dbConn.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_dns_records_name_content_type ON dns_records (name, type, content);`) if err != nil { return dbConn, err } _, err = dbConn.Exec(`CREATE INDEX IF NOT EXISTS idx_dns_records_name_type ON dns_records (name, type);`) return dbConn, err } // MigrateMacs maps hardware addresses to users. One user can own many MACs — // that's how a privacy-rotated device "logs back in" to an existing account. func MigrateMacs(dbConn *sql.DB) (*sql.DB, error) { log.Println("migrating macs table") _, err := dbConn.Exec(`CREATE TABLE IF NOT EXISTS macs ( mac TEXT PRIMARY KEY, user_id TEXT NOT NULL, first_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE );`) return dbConn, err } func MigrateSessions(dbConn *sql.DB) (*sql.DB, error) { log.Println("migrating sessions table") _, err := dbConn.Exec(`CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expire_at TIMESTAMP NOT NULL, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE );`) return dbConn, err } func MigrateChat(dbConn *sql.DB) (*sql.DB, error) { log.Println("migrating chat table") _, err := dbConn.Exec(`CREATE TABLE IF NOT EXISTS chat ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, body TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE );`) if err != nil { return dbConn, err } _, err = dbConn.Exec(`CREATE INDEX IF NOT EXISTS idx_chat_created_at ON chat (created_at);`) return dbConn, err } func MigrateConnections(dbConn *sql.DB) (*sql.DB, error) { log.Println("migrating connections table") _, err := dbConn.Exec(`CREATE TABLE IF NOT EXISTS connections ( ip TEXT PRIMARY KEY, mac TEXT NOT NULL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );`) return dbConn, err } func MigrateMinecraft(dbConn *sql.DB) (*sql.DB, error) { log.Println("migrating minecraft_status table") _, err := dbConn.Exec(`CREATE TABLE IF NOT EXISTS minecraft_status ( id INTEGER PRIMARY KEY CHECK (id = 1), status TEXT NOT NULL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );`) return dbConn, err } func MigrateCanvas(dbConn *sql.DB) (*sql.DB, error) { log.Println("migrating canvas tables") _, err := dbConn.Exec(`CREATE TABLE IF NOT EXISTS canvas_pixels ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT, x INTEGER NOT NULL, y INTEGER NOT NULL, color INTEGER NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL );`) if err != nil { return dbConn, err } _, err = dbConn.Exec(`CREATE TABLE IF NOT EXISTS canvas_snapshots ( id INTEGER PRIMARY KEY AUTOINCREMENT, data BLOB NOT NULL, through_id INTEGER NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );`) return dbConn, err } func Migrate(dbConn *sql.DB) (*sql.DB, error) { log.Println("migrating database") migrations := []Migrator{ MigrateUsers, MigrateUserPrimaryMac, MigrateMacs, MigrateSessions, MigrateChat, MigrateConnections, MigrateMinecraft, MigrateCanvas, MigrateDNSRecords, } for _, migration := range migrations { var err error if dbConn, err = migration(dbConn); err != nil { return dbConn, err } } return dbConn, nil }