Move schema_migrations table creation into 000.sql with INTEGER version column #58
@@ -2,6 +2,7 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -100,3 +101,139 @@ func TestDatabaseConcurrentAccess(t *testing.T) {
|
|||||||
t.Errorf("expected 10 chunks, got %d", count)
|
t.Errorf("expected 10 chunks, got %d", count)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseMigrationVersion(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
filename string
|
||||||
|
wantVer int
|
||||||
|
wantError bool
|
||||||
|
}{
|
||||||
|
{name: "valid 000.sql", filename: "000.sql", wantVer: 0, wantError: false},
|
||||||
|
{name: "valid 001.sql", filename: "001.sql", wantVer: 1, wantError: false},
|
||||||
|
{name: "valid 099.sql", filename: "099.sql", wantVer: 99, wantError: false},
|
||||||
|
{name: "valid with description", filename: "001_initial_schema.sql", wantVer: 1, wantError: false},
|
||||||
|
{name: "valid large version", filename: "123_big_migration.sql", wantVer: 123, wantError: false},
|
||||||
|
{name: "invalid alpha version", filename: "abc.sql", wantVer: 0, wantError: true},
|
||||||
|
{name: "invalid mixed chars", filename: "12a.sql", wantVer: 0, wantError: true},
|
||||||
|
{name: "invalid no extension", filename: "schema.sql", wantVer: 0, wantError: true},
|
||||||
|
{name: "empty string", filename: "", wantVer: 0, wantError: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got, err := ParseMigrationVersion(tc.filename)
|
||||||
|
if tc.wantError {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("ParseMigrationVersion(%q) = %d, nil; want error", tc.filename, got)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("ParseMigrationVersion(%q) unexpected error: %v", tc.filename, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tc.wantVer {
|
||||||
|
t.Errorf("ParseMigrationVersion(%q) = %d; want %d", tc.filename, got, tc.wantVer)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyMigrations_Idempotent(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
conn, err := sql.Open("sqlite", ":memory:?_foreign_keys=ON")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to open database: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := conn.Close(); err != nil {
|
||||||
|
t.Errorf("failed to close database: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
conn.SetMaxOpenConns(1)
|
||||||
|
conn.SetMaxIdleConns(1)
|
||||||
|
|
||||||
|
// First run: apply all migrations.
|
||||||
|
if err := applyMigrations(ctx, conn); err != nil {
|
||||||
|
t.Fatalf("first applyMigrations failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count rows in schema_migrations after first run.
|
||||||
|
var countBefore int
|
||||||
|
if err := conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM schema_migrations").Scan(&countBefore); err != nil {
|
||||||
|
t.Fatalf("failed to count schema_migrations after first run: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second run: must be a no-op.
|
||||||
|
if err := applyMigrations(ctx, conn); err != nil {
|
||||||
|
t.Fatalf("second applyMigrations failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count rows in schema_migrations after second run — must be unchanged.
|
||||||
|
var countAfter int
|
||||||
|
if err := conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM schema_migrations").Scan(&countAfter); err != nil {
|
||||||
|
t.Fatalf("failed to count schema_migrations after second run: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if countBefore != countAfter {
|
||||||
|
t.Errorf("schema_migrations row count changed: before=%d, after=%d", countBefore, countAfter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBootstrapMigrationsTable_FreshDatabase(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
conn, err := sql.Open("sqlite", ":memory:?_foreign_keys=ON")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to open database: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := conn.Close(); err != nil {
|
||||||
|
t.Errorf("failed to close database: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
conn.SetMaxOpenConns(1)
|
||||||
|
conn.SetMaxIdleConns(1)
|
||||||
|
|
||||||
|
// Verify schema_migrations does NOT exist yet.
|
||||||
|
var tableBefore int
|
||||||
|
if err := conn.QueryRowContext(ctx,
|
||||||
|
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='schema_migrations'",
|
||||||
|
).Scan(&tableBefore); err != nil {
|
||||||
|
t.Fatalf("failed to check for table before bootstrap: %v", err)
|
||||||
|
}
|
||||||
|
if tableBefore != 0 {
|
||||||
|
t.Fatal("schema_migrations table should not exist before bootstrap")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run bootstrap.
|
||||||
|
if err := bootstrapMigrationsTable(ctx, conn); err != nil {
|
||||||
|
t.Fatalf("bootstrapMigrationsTable failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify schema_migrations now exists.
|
||||||
|
var tableAfter int
|
||||||
|
if err := conn.QueryRowContext(ctx,
|
||||||
|
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='schema_migrations'",
|
||||||
|
).Scan(&tableAfter); err != nil {
|
||||||
|
t.Fatalf("failed to check for table after bootstrap: %v", err)
|
||||||
|
}
|
||||||
|
if tableAfter != 1 {
|
||||||
|
t.Fatalf("schema_migrations table should exist after bootstrap, got count=%d", tableAfter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify version 0 row exists.
|
||||||
|
var version int
|
||||||
|
if err := conn.QueryRowContext(ctx,
|
||||||
|
"SELECT version FROM schema_migrations WHERE version = 0",
|
||||||
|
).Scan(&version); err != nil {
|
||||||
|
t.Fatalf("version 0 row not found in schema_migrations: %v", err)
|
||||||
|
}
|
||||||
|
if version != 0 {
|
||||||
|
t.Errorf("expected version 0, got %d", version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user