check / check (push) Waiting to run
Replaces #559, which conflicted. Model: opus-5-5
136 lines
3.2 KiB
Go
136 lines
3.2 KiB
Go
// Package migrations holds the schema of each kind of SQLite file
|
|
// webhooker writes, as numbered SQL files embedded in the binary, and
|
|
// applies it when a file is opened.
|
|
//
|
|
// Each kind of file has its own directory. In each, 000_migration.sql
|
|
// creates only the schema_migrations table, and 001_schema.sql holds
|
|
// the whole schema. Before 1.0 a schema change is an edit to
|
|
// 001_schema.sql, never a new file.
|
|
package migrations
|
|
|
|
import (
|
|
"embed"
|
|
"fmt"
|
|
"io/fs"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// The directories, one for each kind of database file.
|
|
const (
|
|
// Main is the main database, webhooker.db.
|
|
Main = "main"
|
|
|
|
// Events is a webhook's event database, events-{uuid}.db.
|
|
Events = "events"
|
|
|
|
// Archive is a database target's archive file.
|
|
Archive = "archive"
|
|
)
|
|
|
|
//go:embed main/*.sql events/*.sql archive/*.sql
|
|
var files embed.FS
|
|
|
|
// Apply runs each SQL file in dir, which is Main, Events or Archive,
|
|
// that the database has not had yet, in the order of the number its
|
|
// name starts with, and records that number in schema_migrations.
|
|
// 000_migration.sql creates that table and records itself, so it runs
|
|
// first, and only when the table is missing.
|
|
//
|
|
// It all runs in one transaction, so a failure leaves the file as it
|
|
// was.
|
|
func Apply(db *gorm.DB, dir string) error {
|
|
return db.Transaction(func(tx *gorm.DB) error {
|
|
var tables int
|
|
|
|
// Row().Scan, not (*gorm.DB).Scan: see internal/gormlog.
|
|
err := tx.Raw(
|
|
"SELECT count(*) FROM sqlite_master "+
|
|
"WHERE type = 'table' AND name = ?",
|
|
"schema_migrations",
|
|
).Row().Scan(&tables)
|
|
if err != nil {
|
|
return fmt.Errorf("looking for schema_migrations: %w", err)
|
|
}
|
|
|
|
if tables == 0 {
|
|
err = run(tx, path.Join(dir, "000_migration.sql"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// fs.Glob lists the files in name order, which is number order.
|
|
names, err := fs.Glob(files, path.Join(dir, "*.sql"))
|
|
if err != nil {
|
|
return fmt.Errorf("listing migrations: %w", err)
|
|
}
|
|
|
|
for _, name := range names {
|
|
err = applyOnce(tx, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// applyOnce runs the migration file name and records its number,
|
|
// unless schema_migrations already has that number.
|
|
func applyOnce(tx *gorm.DB, name string) error {
|
|
number, _, _ := strings.Cut(path.Base(name), "_")
|
|
|
|
version, err := strconv.Atoi(number)
|
|
if err != nil {
|
|
return fmt.Errorf("numbering migration %s: %w", name, err)
|
|
}
|
|
|
|
var applied int
|
|
|
|
err = tx.Raw(
|
|
"SELECT count(*) FROM schema_migrations WHERE version = ?",
|
|
version,
|
|
).Row().Scan(&applied)
|
|
if err != nil {
|
|
return fmt.Errorf("checking migration %s: %w", name, err)
|
|
}
|
|
|
|
if applied > 0 {
|
|
return nil
|
|
}
|
|
|
|
err = run(tx, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = tx.Exec(
|
|
"INSERT INTO schema_migrations (version) VALUES (?)", version,
|
|
).Error
|
|
if err != nil {
|
|
return fmt.Errorf("recording migration %s: %w", name, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// run executes every statement in the migration file name.
|
|
func run(tx *gorm.DB, name string) error {
|
|
script, err := files.ReadFile(name)
|
|
if err != nil {
|
|
return fmt.Errorf("reading migration %s: %w", name, err)
|
|
}
|
|
|
|
err = tx.Exec(string(script)).Error
|
|
if err != nil {
|
|
return fmt.Errorf("applying migration %s: %w", name, err)
|
|
}
|
|
|
|
return nil
|
|
}
|