package model import ( "database/sql" "fmt" "log" _ "github.com/mattn/go-sqlite3" "xengineering.eu/ceres/model/migrations" ) type DB sql.DB func OpenDB(path string) *DB { db, err := sql.Open("sqlite3", path) if err != nil { log.Fatal(err) } err = db.Ping() if err != nil { log.Fatal(err) } return (*DB)(db) } func (db *DB) Transaction(f func(*sql.Tx) error) error { tx, err := (*sql.DB)(db).Begin() if err != nil { log.Printf("Failed to start database transaction: %v", err) return err } defer func() { if tx.Rollback() == nil { log.Println("Rolled back transaction") } }() err = f(tx) if err != nil { log.Printf("Failed transaction: %v", err) return err } return tx.Commit() } func (db *DB) IsEmpty(tx *sql.Tx) (bool, error) { var number int cmd := `SELECT COUNT(*) FROM sqlite_master WHERE type='table'` rows, err := tx.Query(cmd) if err != nil { return false, fmt.Errorf("Select call failed: %w", err) } defer rows.Close() if !rows.Next() { return false, fmt.Errorf("Result set is empty") } err = rows.Scan(&number) if err != nil { return false, fmt.Errorf("Failed to scan numerical value: %w", err) } return number == 0, nil } func (db *DB) setupMinimal(tx *sql.Tx, execVersion string) error { cmd := ` CREATE TABLE metadata ( key TEXT PRIMARY KEY, value TEXT ); INSERT INTO metadata (key, value) VALUES ('version', ?); ` _, err := tx.Exec(cmd, execVersion) return err } func (db *DB) SchemaVersion(tx *sql.Tx) (int, error) { empty, err := db.IsEmpty(tx) if err != nil { return 0, fmt.Errorf("Failed to check if DB is empty: %w", err) } if empty { return 0, nil } rows, err := tx.Query(`SELECT value FROM metadata WHERE key='version';`) if err != nil { return 0, fmt.Errorf("Select call failed: %w", err) } defer rows.Close() if rows.Next() { return 1, nil // version field was only present in one schema version } return 0, fmt.Errorf("Unknown schema version") } func (db *DB) Migrate(execVersion string) error { return db.Transaction(func(tx *sql.Tx) error { for { schema, err := db.SchemaVersion(tx) if err != nil { return fmt.Errorf("Failed to get DB schema version: %w", err) } switch schema { case 0: log.Println("Starting with empty database") err := db.setupMinimal(tx, execVersion) if err != nil { return fmt.Errorf("Failed to setup minimal database schema: %w", err) } log.Println("Executing initial migration") err = migrations.Migration001(tx) if err != nil { return err } case 1: return nil default: return fmt.Errorf("Cannot migrate database to a matching schema version") } } }) } func (db *DB) CreateExamples() { err := db.Transaction(func(tx *sql.Tx) error { recipes := RecipeTestData() for _, recipe := range recipes { err := recipe.Create(tx) if err != nil { return err } } return nil }) if err != nil { log.Fatalf("Failed to inject example recipes: %v", err) } } func (db *DB) Close() { err := (*sql.DB)(db).Close() if err != nil { log.Printf("Failed to close database: %v\n", err) } else { log.Println("Closed database") } }