replace manual SQL migration with goose (v3); embed migrations in binary; remove schema.sql / SCHEMA_PATH

This commit is contained in:
2026-06-24 19:20:48 +03:30
parent c959cb7a87
commit 4edff71bfd
8 changed files with 140 additions and 76 deletions

View File

@@ -3,23 +3,18 @@ package db
import (
"database/sql"
"embed"
"log/slog"
"os"
"time"
"github.com/pressly/goose/v3"
_ "modernc.org/sqlite"
)
const driverName = "sqlite"
var schemaPath string
func init() {
schemaPath = os.Getenv("SCHEMA_PATH")
if schemaPath == "" {
schemaPath = "/app/db/schema.sql"
}
}
//go:embed migrations/*.sql
var migrationFS embed.FS
// Store wraps a SQLite database connection and provides methods for all data access.
type Store struct {
@@ -83,10 +78,7 @@ func NewStore(path string) (*Store, error) {
return nil, err
}
db.Exec("PRAGMA journal_mode=WAL")
if err := migrate(db); err != nil {
return nil, err
}
if err := execSchema(db, schemaPath); err != nil {
if err := runMigrations(db); err != nil {
return nil, err
}
return &Store{db: db}, nil
@@ -102,22 +94,49 @@ func (s *Store) DB() *sql.DB {
return s.db
}
func execSchema(db *sql.DB, path string) error {
data, err := os.ReadFile(path)
if err != nil {
return err
}
_, err = db.Exec(string(data))
return err
}
func hasTable(db *sql.DB, name string) bool {
var exists int
db.QueryRow("SELECT COUNT(1) FROM sqlite_master WHERE type='table' AND name=?", name).Scan(&exists)
return exists > 0
}
func hasColumn(db *sql.DB, table, column string) bool {
// runMigrations handles all database migrations using goose.
func runMigrations(db *sql.DB) error {
// Step 1: Handle the ancient single-table schema (pre-goose era).
// Rename old tables so goose can create the current schema cleanly.
needsOldMigrate := hasTable(db, "events_old") || (hasTable(db, "events") && !hasCol(db, "events", "user_id"))
if needsOldMigrate {
slog.Info("migrating from old schema to new schema")
for _, t := range []string{"events", "days_off", "settings"} {
if hasTable(db, t) {
if _, err := db.Exec("ALTER TABLE " + t + " RENAME TO " + t + "_old"); err != nil {
return err
}
}
}
}
// Step 2: Run goose (creates goose_db_version table if needed,
// applies pending migrations).
goose.SetBaseFS(migrationFS)
if err := goose.SetDialect("sqlite3"); err != nil {
return err
}
if err := goose.Up(db, "migrations"); err != nil {
return err
}
// Step 3: Copy old data into the freshly created tables.
if needsOldMigrate {
if err := migrateFromOldSchemaData(db); err != nil {
return err
}
}
return nil
}
func hasCol(db *sql.DB, table, column string) bool {
rows, err := db.Query("PRAGMA table_info(" + table + ")")
if err != nil {
return false
@@ -138,37 +157,6 @@ func hasColumn(db *sql.DB, table, column string) bool {
return false
}
func migrate(db *sql.DB) error {
if hasTable(db, "users") {
if !hasColumn(db, "users", "calendar") {
slog.Info("migrating: adding calendar column to users")
db.Exec("ALTER TABLE users ADD COLUMN calendar TEXT NOT NULL DEFAULT 'gregorian' CHECK (calendar IN ('gregorian', 'jalali', 'hijri'))")
}
}
if hasTable(db, "events_old") || hasTable(db, "events") && !hasColumn(db, "events", "user_id") {
slog.Info("migrating from old schema to new schema")
if err := migrateFromOldSchema(db); err != nil {
return err
}
}
return nil
}
func migrateFromOldSchema(db *sql.DB) error {
// Rename old tables so CREATE TABLE IF NOT EXISTS can create new ones
for _, t := range []string{"events", "days_off", "settings"} {
if hasTable(db, t) {
db.Exec("ALTER TABLE " + t + " RENAME TO " + t + "_old")
}
}
if err := execSchema(db, schemaPath); err != nil {
return err
}
return migrateFromOldSchemaData(db)
}
func migrateFromOldSchemaData(db *sql.DB) error {
migrateUsers(db)
migrateDaysOff(db)