feat: provide foreign-key-free fresh install baseline

This commit is contained in:
2026-09-20 01:25:59 +08:00
parent a722d19cd9
commit 88ec4545b4
14 changed files with 1081 additions and 27 deletions

View File

@@ -19,6 +19,15 @@ import (
//go:embed migrations/*.up.sql
var migrationFiles embed.FS
// freshInstallBaseline is the current schema without the historical foreign
// key DDL. Existing databases keep using the immutable forward migration
// chain; only an empty database takes this path.
//
//go:embed baseline.sql
var freshInstallBaseline string
const freshInstallBaselineVersion uint64 = 28
type Migration struct {
Version uint64
Name string
@@ -95,6 +104,16 @@ func Migrate(ctx context.Context, db *sql.DB) error {
_ = conn.Raw(func(any) error { return driver.ErrBadConn })
}
}()
var ledgerExists, businessTables int
if err := conn.QueryRowContext(ctx, `SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='orbit_schema_migrations'`).Scan(&ledgerExists); err != nil {
return err
}
if err := conn.QueryRowContext(ctx, `SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND LEFT(TABLE_NAME,6) IN ('orbit_','pulse_','weave_') AND TABLE_NAME<>'orbit_schema_migrations'`).Scan(&businessTables); err != nil {
return err
}
if ledgerExists == 0 && businessTables > 0 {
return errors.New("Orbit tables exist without migration ledger; refuse to guess schema history")
}
if _, err := conn.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS orbit_schema_migrations (
version BIGINT UNSIGNED PRIMARY KEY,
name VARCHAR(191) NOT NULL,
@@ -122,6 +141,12 @@ func Migrate(ctx context.Context, db *sql.DB) error {
if err != nil {
return err
}
if len(applied) == 0 && businessTables > 0 {
return errors.New("Orbit tables exist with an empty migration ledger; reconcile schema history before proceeding")
}
if len(applied) == 0 && businessTables == 0 {
return applyFreshInstallBaseline(ctx, conn, migrations)
}
known := map[uint64]Migration{}
for _, m := range migrations {
known[m.Version] = m
@@ -166,6 +191,41 @@ func Migrate(ctx context.Context, db *sql.DB) error {
return nil
}
// applyFreshInstallBaseline creates a new database directly at the current
// schema version. It records every historical migration as dirty before DDL so
// an interrupted baseline remains blocked for operator reconciliation, just
// like an interrupted ordinary migration. The baseline is intentionally
// limited to an empty database; it never rewrites or reinterprets an upgrade.
func applyFreshInstallBaseline(ctx context.Context, conn *sql.Conn, migrations []Migration) error {
if len(migrations) == 0 || migrations[len(migrations)-1].Version != freshInstallBaselineVersion {
return errors.New("fresh-install baseline is out of date; regenerate it before release")
}
statements, err := splitSQL(freshInstallBaseline)
if err != nil {
return fmt.Errorf("fresh-install baseline: %w", err)
}
for _, statement := range statements {
upper := strings.ToUpper(statement)
if strings.Contains(upper, "FOREIGN KEY") || strings.Contains(upper, " REFERENCES ") {
return errors.New("fresh-install baseline contains forbidden foreign-key DDL")
}
}
for _, migration := range migrations {
if _, err := conn.ExecContext(ctx, "INSERT INTO orbit_schema_migrations (version,name,checksum,dirty) VALUES (?,?,?,TRUE)", migration.Version, migration.Name, migration.Checksum); err != nil {
return err
}
}
for i, statement := range statements {
if _, err := conn.ExecContext(ctx, statement); err != nil {
return fmt.Errorf("fresh-install baseline statement %d failed (migration ledger remains dirty): %w", i+1, err)
}
}
if _, err := conn.ExecContext(ctx, "UPDATE orbit_schema_migrations SET dirty=FALSE, applied_at=UTC_TIMESTAMP(6)"); err != nil {
return err
}
return nil
}
func Status(ctx context.Context, db *sql.DB) ([]MigrationStatus, error) {
rows, err := db.QueryContext(ctx, "SELECT version,name,checksum,dirty FROM orbit_schema_migrations ORDER BY version")
if err != nil {