From 48926747a0a59f3bacc332ee5105adefee9b4e94 Mon Sep 17 00:00:00 2001 From: user Date: Tue, 17 Mar 2026 01:56:57 -0700 Subject: [PATCH 1/4] Move schema_migrations table creation from Go code into 000.sql The schema_migrations table definition now lives in internal/database/schema/000.sql instead of being hardcoded as an inline SQL string in database.go. A bootstrap step checks sqlite_master for the table and applies 000.sql when it is missing. Existing databases that already have the table (created by older inline code) get version 000 back-filled so the normal migration loop skips the file. Also deduplicates the migration logic: both the Database.runMigrations method and the exported ApplyMigrations helper now delegate to a single applyMigrations function. Adds database_test.go with tests for fresh migration, idempotency, bootstrap on a fresh DB, and backwards compatibility with legacy DBs. --- internal/database/database.go | 95 ++++++++++++-- internal/database/database_test.go | 200 +++++++++++++++++++++++------ internal/database/schema/000.sql | 9 ++ 3 files changed, 255 insertions(+), 49 deletions(-) create mode 100644 internal/database/schema/000.sql diff --git a/internal/database/database.go b/internal/database/database.go index ddc01ed..b724b89 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -21,6 +21,10 @@ import ( //go:embed schema/*.sql var schemaFS embed.FS +// bootstrapVersion is the migration that creates the schema_migrations +// table itself. It is applied before the normal migration loop. +const bootstrapVersion = "000" + // Params defines dependencies for Database. type Params struct { fx.In @@ -143,17 +147,86 @@ func collectMigrations() ([]string, error) { return migrations, nil } -// ensureMigrationsTable creates the schema_migrations tracking table if -// it does not already exist. -func ensureMigrationsTable(ctx context.Context, db *sql.DB) error { - _, err := db.ExecContext(ctx, ` - CREATE TABLE IF NOT EXISTS schema_migrations ( - version TEXT PRIMARY KEY, - applied_at DATETIME DEFAULT CURRENT_TIMESTAMP - ) - `) +// bootstrapMigrationsTable ensures the schema_migrations table exists +// by applying 000.sql directly. For databases that already have the +// table (created by older code), it records version "000" for +// consistency. +func bootstrapMigrationsTable(ctx context.Context, db *sql.DB, log *slog.Logger) error { + var tableExists int + + err := db.QueryRowContext(ctx, + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='schema_migrations'", + ).Scan(&tableExists) if err != nil { - return fmt.Errorf("failed to create migrations table: %w", err) + return fmt.Errorf("failed to check for migrations table: %w", err) + } + + if tableExists > 0 { + return ensureBootstrapVersionRecorded(ctx, db, log) + } + + return applyBootstrapMigration(ctx, db, log) +} + +// ensureBootstrapVersionRecorded checks whether version "000" is already +// recorded in an existing schema_migrations table and inserts it if not. +func ensureBootstrapVersionRecorded(ctx context.Context, db *sql.DB, log *slog.Logger) error { + var recorded int + + err := db.QueryRowContext(ctx, + "SELECT COUNT(*) FROM schema_migrations WHERE version = ?", + bootstrapVersion, + ).Scan(&recorded) + if err != nil { + return fmt.Errorf("failed to check bootstrap migration status: %w", err) + } + + if recorded > 0 { + return nil + } + + _, err = db.ExecContext(ctx, + "INSERT INTO schema_migrations (version) VALUES (?)", + bootstrapVersion, + ) + if err != nil { + return fmt.Errorf("failed to record bootstrap migration: %w", err) + } + + if log != nil { + log.Info("recorded bootstrap migration for existing table", "version", bootstrapVersion) + } + + return nil +} + +// applyBootstrapMigration reads and executes 000.sql to create the +// schema_migrations table on a fresh database. +func applyBootstrapMigration(ctx context.Context, db *sql.DB, log *slog.Logger) error { + content, err := schemaFS.ReadFile("schema/000.sql") + if err != nil { + return fmt.Errorf("failed to read bootstrap migration 000.sql: %w", err) + } + + if log != nil { + log.Info("applying bootstrap migration", "version", bootstrapVersion) + } + + _, err = db.ExecContext(ctx, string(content)) + if err != nil { + return fmt.Errorf("failed to apply bootstrap migration: %w", err) + } + + _, err = db.ExecContext(ctx, + "INSERT INTO schema_migrations (version) VALUES (?)", + bootstrapVersion, + ) + if err != nil { + return fmt.Errorf("failed to record bootstrap migration: %w", err) + } + + if log != nil { + log.Info("bootstrap migration applied successfully", "version", bootstrapVersion) } return nil @@ -164,7 +237,7 @@ func ensureMigrationsTable(ctx context.Context, db *sql.DB) error { // This is exported so tests can apply the real schema without the full fx // lifecycle. func ApplyMigrations(ctx context.Context, db *sql.DB, log *slog.Logger) error { - if err := ensureMigrationsTable(ctx, db); err != nil { + if err := bootstrapMigrationsTable(ctx, db, log); err != nil { return err } diff --git a/internal/database/database_test.go b/internal/database/database_test.go index 9c2fc2c..ede0d1c 100644 --- a/internal/database/database_test.go +++ b/internal/database/database_test.go @@ -8,6 +8,20 @@ import ( _ "modernc.org/sqlite" // SQLite driver registration ) +// openTestDB returns a fresh in-memory SQLite database. +func openTestDB(t *testing.T) *sql.DB { + t.Helper() + + db, err := sql.Open("sqlite", ":memory:") + if err != nil { + t.Fatalf("failed to open test db: %v", err) + } + + t.Cleanup(func() { db.Close() }) + + return db +} + func TestParseMigrationVersion(t *testing.T) { tests := []struct { name string @@ -86,70 +100,180 @@ func TestParseMigrationVersion(t *testing.T) { } } -func TestApplyMigrations(t *testing.T) { - db, err := sql.Open("sqlite", ":memory:") - if err != nil { - t.Fatalf("failed to open in-memory database: %v", err) - } - defer db.Close() +func TestApplyMigrations_CreatesSchemaAndTables(t *testing.T) { + db := openTestDB(t) + ctx := context.Background() - // Apply migrations should succeed. - if err := ApplyMigrations(context.Background(), db, nil); err != nil { + if err := ApplyMigrations(ctx, db, nil); err != nil { t.Fatalf("ApplyMigrations failed: %v", err) } - // Verify the schema_migrations table recorded the version. - var version string - - err = db.QueryRowContext(context.Background(), - "SELECT version FROM schema_migrations LIMIT 1", - ).Scan(&version) + // The schema_migrations table must exist and contain at least + // version "000" (the bootstrap) and "001" (the initial schema). + rows, err := db.Query("SELECT version FROM schema_migrations ORDER BY version") if err != nil { t.Fatalf("failed to query schema_migrations: %v", err) } + defer rows.Close() - if version != "001" { - t.Errorf("expected version %q, got %q", "001", version) + var versions []string + for rows.Next() { + var v string + if err := rows.Scan(&v); err != nil { + t.Fatalf("failed to scan version: %v", err) + } + + versions = append(versions, v) } - // Verify a table from the migration exists (source_content). - var tableName string + if err := rows.Err(); err != nil { + t.Fatalf("row iteration error: %v", err) + } - err = db.QueryRowContext(context.Background(), - "SELECT name FROM sqlite_master WHERE type='table' AND name='source_content'", - ).Scan(&tableName) - if err != nil { - t.Fatalf("expected source_content table to exist: %v", err) + if len(versions) < 2 { + t.Fatalf("expected at least 2 migrations recorded, got %d: %v", len(versions), versions) + } + + if versions[0] != "000" { + t.Errorf("first recorded migration = %q, want %q", versions[0], "000") + } + + if versions[1] != "001" { + t.Errorf("second recorded migration = %q, want %q", versions[1], "001") + } + + // Verify that the application tables created by 001.sql exist. + for _, table := range []string{"source_content", "source_metadata", "output_content", "request_cache", "negative_cache", "cache_stats"} { + var count int + + err := db.QueryRow( + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?", + table, + ).Scan(&count) + if err != nil { + t.Fatalf("failed to check for table %s: %v", table, err) + } + + if count != 1 { + t.Errorf("table %s does not exist after migrations", table) + } } } -func TestApplyMigrationsIdempotent(t *testing.T) { - db, err := sql.Open("sqlite", ":memory:") - if err != nil { - t.Fatalf("failed to open in-memory database: %v", err) - } - defer db.Close() +func TestApplyMigrations_Idempotent(t *testing.T) { + db := openTestDB(t) + ctx := context.Background() - // Apply twice should succeed (idempotent). - if err := ApplyMigrations(context.Background(), db, nil); err != nil { + if err := ApplyMigrations(ctx, db, nil); err != nil { t.Fatalf("first ApplyMigrations failed: %v", err) } - if err := ApplyMigrations(context.Background(), db, nil); err != nil { + // Running a second time must succeed without errors. + if err := ApplyMigrations(ctx, db, nil); err != nil { t.Fatalf("second ApplyMigrations failed: %v", err) } - // Should still have exactly one migration recorded. + // Verify no duplicate rows in schema_migrations. var count int - err = db.QueryRowContext(context.Background(), - "SELECT COUNT(*) FROM schema_migrations", - ).Scan(&count) + err := db.QueryRow("SELECT COUNT(*) FROM schema_migrations WHERE version = '000'").Scan(&count) if err != nil { - t.Fatalf("failed to count schema_migrations: %v", err) + t.Fatalf("failed to count 000 rows: %v", err) } if count != 1 { - t.Errorf("expected 1 migration record, got %d", count) + t.Errorf("expected exactly 1 row for version 000, got %d", count) + } +} + +func TestBootstrapMigrationsTable_FreshDatabase(t *testing.T) { + db := openTestDB(t) + ctx := context.Background() + + if err := bootstrapMigrationsTable(ctx, db, nil); err != nil { + t.Fatalf("bootstrapMigrationsTable failed: %v", err) + } + + // schema_migrations table must exist. + var tableCount int + + err := db.QueryRow( + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='schema_migrations'", + ).Scan(&tableCount) + if err != nil { + t.Fatalf("failed to check for table: %v", err) + } + + if tableCount != 1 { + t.Fatalf("schema_migrations table not created") + } + + // Version "000" must be recorded. + var recorded int + + err = db.QueryRow( + "SELECT COUNT(*) FROM schema_migrations WHERE version = '000'", + ).Scan(&recorded) + if err != nil { + t.Fatalf("failed to check version: %v", err) + } + + if recorded != 1 { + t.Errorf("expected version 000 to be recorded, got count %d", recorded) + } +} + +func TestBootstrapMigrationsTable_ExistingTableBackwardsCompat(t *testing.T) { + db := openTestDB(t) + ctx := context.Background() + + // Simulate an older database that created the table via inline SQL + // (without recording version "000"). + _, err := db.Exec(` + CREATE TABLE schema_migrations ( + version TEXT PRIMARY KEY, + applied_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `) + if err != nil { + t.Fatalf("failed to create legacy table: %v", err) + } + + // Insert a fake migration to prove the table already existed. + _, err = db.Exec("INSERT INTO schema_migrations (version) VALUES ('001')") + if err != nil { + t.Fatalf("failed to insert legacy version: %v", err) + } + + if err := bootstrapMigrationsTable(ctx, db, nil); err != nil { + t.Fatalf("bootstrapMigrationsTable failed: %v", err) + } + + // Version "000" must now be recorded. + var recorded int + + err = db.QueryRow( + "SELECT COUNT(*) FROM schema_migrations WHERE version = '000'", + ).Scan(&recorded) + if err != nil { + t.Fatalf("failed to check version: %v", err) + } + + if recorded != 1 { + t.Errorf("expected version 000 to be recorded for legacy DB, got count %d", recorded) + } + + // The existing "001" row must still be there. + var legacyCount int + + err = db.QueryRow( + "SELECT COUNT(*) FROM schema_migrations WHERE version = '001'", + ).Scan(&legacyCount) + if err != nil { + t.Fatalf("failed to check legacy version: %v", err) + } + + if legacyCount != 1 { + t.Errorf("legacy version 001 row missing after bootstrap") } } diff --git a/internal/database/schema/000.sql b/internal/database/schema/000.sql new file mode 100644 index 0000000..c05b915 --- /dev/null +++ b/internal/database/schema/000.sql @@ -0,0 +1,9 @@ +-- Migration 000: Schema migrations tracking table +-- This must be the first migration applied. The bootstrap logic in +-- database.go applies it directly (bypassing the normal migration +-- loop) when the schema_migrations table does not yet exist. + +CREATE TABLE IF NOT EXISTS schema_migrations ( + version TEXT PRIMARY KEY, + applied_at DATETIME DEFAULT CURRENT_TIMESTAMP +); -- 2.49.1 From 2cabd48a8a8ca40825c06ad4b749a0057f7caa3c Mon Sep 17 00:00:00 2001 From: clawbot Date: Tue, 17 Mar 2026 19:47:59 -0700 Subject: [PATCH 2/4] Remove backwards compat code: no installed base pre-1.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Simplify bootstrapMigrationsTable to just check table existence and apply 000.sql if missing — no legacy DB detection - Remove ensureBootstrapVersionRecorded (legacy backfill path) - Remove applyBootstrapMigration (separate helper, now inlined) - Remove TestBootstrapMigrationsTable_ExistingTableBackwardsCompat - Simplify 000.sql header comment --- internal/database/database.go | 46 +------------------------ internal/database/database_test.go | 55 ------------------------------ internal/database/schema/000.sql | 4 +-- 3 files changed, 2 insertions(+), 103 deletions(-) diff --git a/internal/database/database.go b/internal/database/database.go index b724b89..0b38b64 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -148,9 +148,7 @@ func collectMigrations() ([]string, error) { } // bootstrapMigrationsTable ensures the schema_migrations table exists -// by applying 000.sql directly. For databases that already have the -// table (created by older code), it records version "000" for -// consistency. +// by applying 000.sql if the table is missing. func bootstrapMigrationsTable(ctx context.Context, db *sql.DB, log *slog.Logger) error { var tableExists int @@ -162,47 +160,9 @@ func bootstrapMigrationsTable(ctx context.Context, db *sql.DB, log *slog.Logger) } if tableExists > 0 { - return ensureBootstrapVersionRecorded(ctx, db, log) - } - - return applyBootstrapMigration(ctx, db, log) -} - -// ensureBootstrapVersionRecorded checks whether version "000" is already -// recorded in an existing schema_migrations table and inserts it if not. -func ensureBootstrapVersionRecorded(ctx context.Context, db *sql.DB, log *slog.Logger) error { - var recorded int - - err := db.QueryRowContext(ctx, - "SELECT COUNT(*) FROM schema_migrations WHERE version = ?", - bootstrapVersion, - ).Scan(&recorded) - if err != nil { - return fmt.Errorf("failed to check bootstrap migration status: %w", err) - } - - if recorded > 0 { return nil } - _, err = db.ExecContext(ctx, - "INSERT INTO schema_migrations (version) VALUES (?)", - bootstrapVersion, - ) - if err != nil { - return fmt.Errorf("failed to record bootstrap migration: %w", err) - } - - if log != nil { - log.Info("recorded bootstrap migration for existing table", "version", bootstrapVersion) - } - - return nil -} - -// applyBootstrapMigration reads and executes 000.sql to create the -// schema_migrations table on a fresh database. -func applyBootstrapMigration(ctx context.Context, db *sql.DB, log *slog.Logger) error { content, err := schemaFS.ReadFile("schema/000.sql") if err != nil { return fmt.Errorf("failed to read bootstrap migration 000.sql: %w", err) @@ -225,10 +185,6 @@ func applyBootstrapMigration(ctx context.Context, db *sql.DB, log *slog.Logger) return fmt.Errorf("failed to record bootstrap migration: %w", err) } - if log != nil { - log.Info("bootstrap migration applied successfully", "version", bootstrapVersion) - } - return nil } diff --git a/internal/database/database_test.go b/internal/database/database_test.go index ede0d1c..46983c3 100644 --- a/internal/database/database_test.go +++ b/internal/database/database_test.go @@ -222,58 +222,3 @@ func TestBootstrapMigrationsTable_FreshDatabase(t *testing.T) { t.Errorf("expected version 000 to be recorded, got count %d", recorded) } } - -func TestBootstrapMigrationsTable_ExistingTableBackwardsCompat(t *testing.T) { - db := openTestDB(t) - ctx := context.Background() - - // Simulate an older database that created the table via inline SQL - // (without recording version "000"). - _, err := db.Exec(` - CREATE TABLE schema_migrations ( - version TEXT PRIMARY KEY, - applied_at DATETIME DEFAULT CURRENT_TIMESTAMP - ) - `) - if err != nil { - t.Fatalf("failed to create legacy table: %v", err) - } - - // Insert a fake migration to prove the table already existed. - _, err = db.Exec("INSERT INTO schema_migrations (version) VALUES ('001')") - if err != nil { - t.Fatalf("failed to insert legacy version: %v", err) - } - - if err := bootstrapMigrationsTable(ctx, db, nil); err != nil { - t.Fatalf("bootstrapMigrationsTable failed: %v", err) - } - - // Version "000" must now be recorded. - var recorded int - - err = db.QueryRow( - "SELECT COUNT(*) FROM schema_migrations WHERE version = '000'", - ).Scan(&recorded) - if err != nil { - t.Fatalf("failed to check version: %v", err) - } - - if recorded != 1 { - t.Errorf("expected version 000 to be recorded for legacy DB, got count %d", recorded) - } - - // The existing "001" row must still be there. - var legacyCount int - - err = db.QueryRow( - "SELECT COUNT(*) FROM schema_migrations WHERE version = '001'", - ).Scan(&legacyCount) - if err != nil { - t.Fatalf("failed to check legacy version: %v", err) - } - - if legacyCount != 1 { - t.Errorf("legacy version 001 row missing after bootstrap") - } -} diff --git a/internal/database/schema/000.sql b/internal/database/schema/000.sql index c05b915..5999fad 100644 --- a/internal/database/schema/000.sql +++ b/internal/database/schema/000.sql @@ -1,7 +1,5 @@ -- Migration 000: Schema migrations tracking table --- This must be the first migration applied. The bootstrap logic in --- database.go applies it directly (bypassing the normal migration --- loop) when the schema_migrations table does not yet exist. +-- Applied as a bootstrap step before the normal migration loop. CREATE TABLE IF NOT EXISTS schema_migrations ( version TEXT PRIMARY KEY, -- 2.49.1 From 2074571a87562c8441ceb8a94e0393a2ad9b490f Mon Sep 17 00:00:00 2001 From: clawbot Date: Tue, 17 Mar 2026 20:21:32 -0700 Subject: [PATCH 3/4] Move bootstrap migration INSERT into 000.sql Per review: the SQL file should be self-contained. 000.sql now includes both the CREATE TABLE and the INSERT OR IGNORE for recording its own version. Removed the separate INSERT from Go code in bootstrapMigrationsTable(). --- internal/database/database.go | 8 -------- internal/database/schema/000.sql | 2 ++ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/internal/database/database.go b/internal/database/database.go index 0b38b64..7477d88 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -177,14 +177,6 @@ func bootstrapMigrationsTable(ctx context.Context, db *sql.DB, log *slog.Logger) return fmt.Errorf("failed to apply bootstrap migration: %w", err) } - _, err = db.ExecContext(ctx, - "INSERT INTO schema_migrations (version) VALUES (?)", - bootstrapVersion, - ) - if err != nil { - return fmt.Errorf("failed to record bootstrap migration: %w", err) - } - return nil } diff --git a/internal/database/schema/000.sql b/internal/database/schema/000.sql index 5999fad..aef25a8 100644 --- a/internal/database/schema/000.sql +++ b/internal/database/schema/000.sql @@ -5,3 +5,5 @@ CREATE TABLE IF NOT EXISTS schema_migrations ( version TEXT PRIMARY KEY, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP ); + +INSERT OR IGNORE INTO schema_migrations (version) VALUES ('000'); -- 2.49.1 From 49398c1f0f99dd0292b452334149c28b38c7ea5a Mon Sep 17 00:00:00 2001 From: clawbot Date: Thu, 19 Mar 2026 23:07:03 -0700 Subject: [PATCH 4/4] Change schema_migrations version from TEXT to INTEGER --- internal/database/database.go | 30 ++++++++++++--------- internal/database/database_test.go | 42 +++++++++++++++--------------- internal/database/schema/000.sql | 4 +-- 3 files changed, 41 insertions(+), 35 deletions(-) diff --git a/internal/database/database.go b/internal/database/database.go index 7477d88..c4af7e2 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -9,6 +9,7 @@ import ( "log/slog" "path/filepath" "sort" + "strconv" "strings" "go.uber.org/fx" @@ -23,7 +24,7 @@ var schemaFS embed.FS // bootstrapVersion is the migration that creates the schema_migrations // table itself. It is applied before the normal migration loop. -const bootstrapVersion = "000" +const bootstrapVersion = 0 // Params defines dependencies for Database. type Params struct { @@ -42,35 +43,40 @@ type Database struct { // ParseMigrationVersion extracts the numeric version prefix from a migration // filename. Filenames must follow the pattern ".sql" or // "_.sql", where version is a zero-padded numeric -// string (e.g. "001", "002"). Returns the version string and an error if -// the filename does not match the expected pattern. -func ParseMigrationVersion(filename string) (string, error) { +// string (e.g. "001", "002"). Returns the version as an integer and an +// error if the filename does not match the expected pattern. +func ParseMigrationVersion(filename string) (int, error) { name := strings.TrimSuffix(filename, filepath.Ext(filename)) if name == "" { - return "", fmt.Errorf("invalid migration filename %q: empty name", filename) + return 0, fmt.Errorf("invalid migration filename %q: empty name", filename) } // Split on underscore to separate version from description. // If there's no underscore, the entire stem is the version. - version := name + versionStr := name if idx := strings.IndexByte(name, '_'); idx >= 0 { - version = name[:idx] + versionStr = name[:idx] } - if version == "" { - return "", fmt.Errorf("invalid migration filename %q: empty version prefix", filename) + if versionStr == "" { + return 0, fmt.Errorf("invalid migration filename %q: empty version prefix", filename) } // Validate the version is purely numeric. - for _, ch := range version { + for _, ch := range versionStr { if ch < '0' || ch > '9' { - return "", fmt.Errorf( + return 0, fmt.Errorf( "invalid migration filename %q: version %q contains non-numeric character %q", - filename, version, string(ch), + filename, versionStr, string(ch), ) } } + version, err := strconv.Atoi(versionStr) + if err != nil { + return 0, fmt.Errorf("invalid migration filename %q: %w", filename, err) + } + return version, nil } diff --git a/internal/database/database_test.go b/internal/database/database_test.go index 46983c3..015ae22 100644 --- a/internal/database/database_test.go +++ b/internal/database/database_test.go @@ -26,33 +26,33 @@ func TestParseMigrationVersion(t *testing.T) { tests := []struct { name string filename string - want string + want int wantErr bool }{ { name: "version only", filename: "001.sql", - want: "001", + want: 1, }, { name: "version with description", filename: "001_initial_schema.sql", - want: "001", + want: 1, }, { name: "multi-digit version", filename: "042_add_indexes.sql", - want: "042", + want: 42, }, { name: "long version number", filename: "00001_long_prefix.sql", - want: "00001", + want: 1, }, { name: "description with multiple underscores", filename: "003_add_user_auth_tables.sql", - want: "003", + want: 3, }, { name: "empty filename", @@ -81,7 +81,7 @@ func TestParseMigrationVersion(t *testing.T) { got, err := ParseMigrationVersion(tt.filename) if tt.wantErr { if err == nil { - t.Errorf("ParseMigrationVersion(%q) expected error, got %q", tt.filename, got) + t.Errorf("ParseMigrationVersion(%q) expected error, got %d", tt.filename, got) } return @@ -94,7 +94,7 @@ func TestParseMigrationVersion(t *testing.T) { } if got != tt.want { - t.Errorf("ParseMigrationVersion(%q) = %q, want %q", tt.filename, got, tt.want) + t.Errorf("ParseMigrationVersion(%q) = %d, want %d", tt.filename, got, tt.want) } }) } @@ -109,16 +109,16 @@ func TestApplyMigrations_CreatesSchemaAndTables(t *testing.T) { } // The schema_migrations table must exist and contain at least - // version "000" (the bootstrap) and "001" (the initial schema). + // version 0 (the bootstrap) and 1 (the initial schema). rows, err := db.Query("SELECT version FROM schema_migrations ORDER BY version") if err != nil { t.Fatalf("failed to query schema_migrations: %v", err) } defer rows.Close() - var versions []string + var versions []int for rows.Next() { - var v string + var v int if err := rows.Scan(&v); err != nil { t.Fatalf("failed to scan version: %v", err) } @@ -134,12 +134,12 @@ func TestApplyMigrations_CreatesSchemaAndTables(t *testing.T) { t.Fatalf("expected at least 2 migrations recorded, got %d: %v", len(versions), versions) } - if versions[0] != "000" { - t.Errorf("first recorded migration = %q, want %q", versions[0], "000") + if versions[0] != 0 { + t.Errorf("first recorded migration = %d, want %d", versions[0], 0) } - if versions[1] != "001" { - t.Errorf("second recorded migration = %q, want %q", versions[1], "001") + if versions[1] != 1 { + t.Errorf("second recorded migration = %d, want %d", versions[1], 1) } // Verify that the application tables created by 001.sql exist. @@ -176,13 +176,13 @@ func TestApplyMigrations_Idempotent(t *testing.T) { // Verify no duplicate rows in schema_migrations. var count int - err := db.QueryRow("SELECT COUNT(*) FROM schema_migrations WHERE version = '000'").Scan(&count) + err := db.QueryRow("SELECT COUNT(*) FROM schema_migrations WHERE version = 0").Scan(&count) if err != nil { - t.Fatalf("failed to count 000 rows: %v", err) + t.Fatalf("failed to count version 0 rows: %v", err) } if count != 1 { - t.Errorf("expected exactly 1 row for version 000, got %d", count) + t.Errorf("expected exactly 1 row for version 0, got %d", count) } } @@ -208,17 +208,17 @@ func TestBootstrapMigrationsTable_FreshDatabase(t *testing.T) { t.Fatalf("schema_migrations table not created") } - // Version "000" must be recorded. + // Version 0 must be recorded. var recorded int err = db.QueryRow( - "SELECT COUNT(*) FROM schema_migrations WHERE version = '000'", + "SELECT COUNT(*) FROM schema_migrations WHERE version = 0", ).Scan(&recorded) if err != nil { t.Fatalf("failed to check version: %v", err) } if recorded != 1 { - t.Errorf("expected version 000 to be recorded, got count %d", recorded) + t.Errorf("expected version 0 to be recorded, got count %d", recorded) } } diff --git a/internal/database/schema/000.sql b/internal/database/schema/000.sql index aef25a8..e06a2da 100644 --- a/internal/database/schema/000.sql +++ b/internal/database/schema/000.sql @@ -2,8 +2,8 @@ -- Applied as a bootstrap step before the normal migration loop. CREATE TABLE IF NOT EXISTS schema_migrations ( - version TEXT PRIMARY KEY, + version INTEGER PRIMARY KEY, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -INSERT OR IGNORE INTO schema_migrations (version) VALUES ('000'); +INSERT OR IGNORE INTO schema_migrations (version) VALUES (0); -- 2.49.1