From 49398c1f0f99dd0292b452334149c28b38c7ea5a Mon Sep 17 00:00:00 2001 From: clawbot Date: Thu, 19 Mar 2026 23:07:03 -0700 Subject: [PATCH] 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);