up
This commit is contained in:
+58
-6
@@ -81,6 +81,34 @@ func PingMySQL(c MySQLConfig) error {
|
||||
//go:embed migrations/*.sql
|
||||
var migrationsFS embed.FS
|
||||
|
||||
// Dialect names the backend opened at startup. Repos consult it for the few
|
||||
// places where SQLite and MySQL SQL genuinely differ (upserts, JSON extraction).
|
||||
// Timestamps are generated in Go (NowISO) for both, so most queries are shared.
|
||||
var Dialect = "sqlite"
|
||||
|
||||
// IsMySQL reports whether the active LOGBOOK backend is the shared MySQL
|
||||
// server. Config tables always live in local SQLite, so this only governs
|
||||
// qso-table SQL (see columnExpr).
|
||||
func IsMySQL() bool { return Dialect == "mysql" }
|
||||
|
||||
// SetDialect pins the logbook dialect ("mysql" | "sqlite"). Called once at
|
||||
// startup after the logbook backend is chosen, so it doesn't depend on the
|
||||
// order in which the local and logbook connections were opened.
|
||||
func SetDialect(d string) {
|
||||
if d == "mysql" {
|
||||
Dialect = "mysql"
|
||||
} else {
|
||||
Dialect = "sqlite"
|
||||
}
|
||||
}
|
||||
|
||||
// NowISO returns the current UTC time as the ISO-8601 string OpsLog stores in
|
||||
// *_at / date columns — millisecond precision, matching the old SQLite
|
||||
// strftime('%Y-%m-%dT%H:%M:%fZ','now') default. Bind this as a parameter so the
|
||||
// same INSERT/UPDATE works on both backends.
|
||||
func NowISO() string { return time.Now().UTC().Format("2006-01-02T15:04:05.000Z") }
|
||||
|
||||
|
||||
// Open opens (and creates if needed) the SQLite database at the given path,
|
||||
// enables performance PRAGMAs, and applies embedded migrations.
|
||||
func Open(path string) (*sql.DB, error) {
|
||||
@@ -97,7 +125,8 @@ func Open(path string) (*sql.DB, error) {
|
||||
_ = conn.Close()
|
||||
return nil, fmt.Errorf("ping sqlite: %w", err)
|
||||
}
|
||||
if err := migrate(conn); err != nil {
|
||||
Dialect = "sqlite"
|
||||
if err := migrate(conn, nil); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
@@ -106,12 +135,16 @@ func Open(path string) (*sql.DB, error) {
|
||||
|
||||
// migrate applies all embedded *.sql migrations in alphabetical order,
|
||||
// skipping those already applied. Intentionally minimal in-house system
|
||||
// (no external dependency).
|
||||
func migrate(conn *sql.DB) error {
|
||||
if _, err := conn.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
// (no external dependency). translate, when non-nil, rewrites each statement
|
||||
// for a non-SQLite backend (see mysqlDDL); nil means run the SQLite DDL as-is.
|
||||
func migrate(conn *sql.DB, translate func(string) string) error {
|
||||
if translate == nil {
|
||||
translate = func(s string) string { return s }
|
||||
}
|
||||
if _, err := conn.Exec(translate(`CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
name TEXT PRIMARY KEY,
|
||||
applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
)`); err != nil {
|
||||
)`)); err != nil {
|
||||
return fmt.Errorf("create schema_migrations: %w", err)
|
||||
}
|
||||
|
||||
@@ -141,11 +174,30 @@ func migrate(conn *sql.DB) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("read migration %s: %w", name, err)
|
||||
}
|
||||
sqlText := translate(string(content))
|
||||
|
||||
// MySQL implicitly commits each DDL statement, so a wrapping transaction
|
||||
// gives no atomicity — a mid-file failure would leave columns/tables
|
||||
// behind, unrecorded, and every restart would re-run and choke on
|
||||
// "duplicate column". Instead apply statement-by-statement and tolerate
|
||||
// "already exists" errors, making migrations idempotent (and self-healing
|
||||
// for a previously half-applied database).
|
||||
if IsMySQL() {
|
||||
if err := applyMySQLMigration(conn, sqlText); err != nil {
|
||||
return fmt.Errorf("apply migration %s: %w", name, err)
|
||||
}
|
||||
if _, err := conn.Exec(`INSERT INTO schema_migrations(name) VALUES(?)`, name); err != nil {
|
||||
return fmt.Errorf("record migration %s: %w", name, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// SQLite supports DDL inside a transaction, so keep it atomic there.
|
||||
tx, err := conn.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin tx for %s: %w", name, err)
|
||||
}
|
||||
if _, err := tx.Exec(string(content)); err != nil {
|
||||
if _, err := tx.Exec(sqlText); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return fmt.Errorf("apply migration %s: %w", name, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user