package database import ( "context" "database/sql" "fmt" "time" ) type migration struct { version int apply func(context.Context, *sql.Tx) error } var migrations = []migration{ {version: 1, apply: migrationOne}, } func migrate(ctx context.Context, db *sql.DB) error { if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS schema_migrations ( version INTEGER PRIMARY KEY, applied_at TEXT NOT NULL )`); err != nil { return fmt.Errorf("create migrations table: %w", err) } for _, m := range migrations { var exists int if err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM schema_migrations WHERE version = ?", m.version).Scan(&exists); err != nil { return fmt.Errorf("read migration %d: %w", m.version, err) } if exists != 0 { continue } tx, err := db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("begin migration %d: %w", m.version, err) } if err := m.apply(ctx, tx); err != nil { _ = tx.Rollback() return fmt.Errorf("apply migration %d: %w", m.version, err) } if _, err := tx.ExecContext(ctx, "INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)", m.version, time.Now().UTC().Format(time.RFC3339Nano)); err != nil { _ = tx.Rollback() return fmt.Errorf("record migration %d: %w", m.version, err) } if err := tx.Commit(); err != nil { return fmt.Errorf("commit migration %d: %w", m.version, err) } } return nil } func migrationOne(ctx context.Context, tx *sql.Tx) error { statements := []string{ `CREATE TABLE lists ( id INTEGER PRIMARY KEY, name TEXT NOT NULL CHECK (length(trim(name)) BETWEEN 1 AND 120), position REAL NOT NULL )`, `CREATE TABLE cards ( id INTEGER PRIMARY KEY, list_id INTEGER NOT NULL, title TEXT NOT NULL CHECK (length(trim(title)) BETWEEN 1 AND 240), description TEXT NOT NULL DEFAULT '', position REAL NOT NULL, done INTEGER NOT NULL DEFAULT 0 CHECK (done IN (0, 1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, FOREIGN KEY (list_id) REFERENCES lists(id) ON DELETE CASCADE )`, `CREATE INDEX idx_lists_position ON lists(position, id)`, `CREATE INDEX idx_cards_list_position ON cards(list_id, position, id)`, } for _, statement := range statements { if _, err := tx.ExecContext(ctx, statement); err != nil { return err } } for i, name := range []string{"To do", "In progress", "Done"} { if _, err := tx.ExecContext(ctx, "INSERT INTO lists (name, position) VALUES (?, ?)", name, float64(i+1)*1024); err != nil { return err } } return nil }