Move schema_migrations table creation into 000.sql with INTEGER version column #172
Reference in New Issue
Block a user
Delete Branch "feat/integer-schema-migrations"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #171
Refactors the migration system to follow the pixa pattern:
Changes
New file:
internal/database/migrations/000.sqlschema_migrationswithversion INTEGER PRIMARY KEYCREATE TABLE IF NOT EXISTSandINSERT OR IGNORE INTO schema_migrations (version) VALUES (0)Refactored:
internal/database/migrations.go000.sqlas a bootstrap step before the normal migration loopParseMigrationVersion(filename)— exported function that extracts integer version from filenames like001_initial.sql→1ApplyMigrations(ctx, db, log)— exported function for tests to apply schema without full fx lifecyclebootstrapMigrationsTable— checks if table exists; if missing, runs000.sql; if present, checks for legacy formatconvertLegacyMigrations— one-time conversion for existing databases that stored versions as TEXT filenames (e.g."001_initial.sql") to INTEGER versions (e.g.1)ErrInvalidMigrationFilenamefor static error wrappingNew file:
internal/database/migrations_test.goTestParseMigrationVersion— valid/invalid filename parsingTestApplyMigrationsFreshDatabase— verifies bootstrap creates table, all migrations apply, application tables existTestApplyMigrationsIdempotent— double-apply is a no-opTestApplyMigrationsLegacyConversion— simulates old TEXT-based table with filename entries, verifies conversion to integer formatBackward Compatibility
Existing databases with the old TEXT-based
schema_migrationstable are automatically converted on first run. The conversion:_)000.sqlAll existing migrations (001-007) continue to work unchanged.
Review: PR #172 — Move schema_migrations table creation into 000.sql
Policy Divergences
1. Bootstrap migration filename violates REPO_POLICIES.md naming convention
REPO_POLICIES.md states:
The PR creates
internal/database/migrations/000.sqlinstead of000_migration.sql. All other migrations in this repo follow theNNN_description.sqlpattern (001_initial.sql,002_remove_container_id.sql, etc.). The bootstrap migration should be000_migration.sqlto match both the policy and the existing naming convention.Affected locations that hardcode the filename:
internal/database/migrations.go—applyBootstrapMigration():migrationsFS.ReadFile("migrations/000.sql")internal/database/migrations.go—rebuildMigrationsTable():migrationsFS.ReadFile("migrations/000.sql")internal/database/migrations.go—bootstrapMigrationsTable()doc comment references000.sqlAll must be updated to reference
000_migration.sql.2. Legacy conversion is not transaction-safe
rebuildMigrationsTable()ininternal/database/migrations.goperforms a destructive sequence (DROP TABLE → CREATE TABLE → INSERT loop) without wrapping it in a transaction. If the process crashes between the DROP and the final INSERT, the user's migration tracking data is lost and the database enters an inconsistent state. The PR already wraps normal migration application in transactions viaapplyMigrationTx()— the same protection should apply to the legacy conversion path, which is arguably the most dangerous operation in this code.Requirements Checklist (issue #171)
schema_migrationstable DDL lives in a.sqlfile, not inline Go codeversioncolumn isINTEGER PRIMARY KEY(not TEXT).sqlis self-contained (CREATE TABLE + INSERT OR IGNORE)Test Coverage
ParseMigrationVersion()TestParseMigrationVersionApplyMigrations()TestApplyMigrationsFreshDatabase,TestApplyMigrationsIdempotent,TestApplyMigrationsLegacyConversionErrInvalidMigrationFilenameParseMigrationVersionerror casesBuild Result
docker build .— PASS ✅Cheating Check
No changes to Makefile, Dockerfile,
.golangci.yml, or CI config. No weakened assertions. ✅README Consistency
README does not expose migration implementation details. No update needed. ✅
Verdict: FAIL
000.sqlto000_migration.sqlper REPO_POLICIES.md, and all hardcoded references updated.rebuildMigrationsTable()must wrap the DROP/CREATE/INSERT sequence in a transaction for data safety.Rework Summary
Fixed both review findings from PR #172:
1. Bootstrap migration renamed to
000_migration.sqlRenamed
internal/database/migrations/000.sql→internal/database/migrations/000_migration.sqlper REPO_POLICIES.md naming convention (NNN_description.sqlpattern). Updated all hardcoded references:applyBootstrapMigration()— ReadFile path and error messagesrebuildMigrationsTable()— ReadFile path and error messagesbootstrapMigrationsTable()— doc commentTestParseMigrationVersionupdated from000.sqlto000_migration.sql2. Legacy conversion wrapped in transaction
rebuildMigrationsTable()now wraps the entire DROP TABLE → CREATE TABLE → INSERT loop in a single transaction (db.BeginTx/tx.Commit). If any step fails or the process crashes mid-conversion, the transaction rolls back and no data is lost. The file read (migrationsFS.ReadFile) is done before the transaction begins so the transaction window is minimal.Build Result
docker build .— PASS ✅ (all tests pass, lint clean, binary builds)@clawbot remove the old-style table conversion - this is pre-1.0 software, no schema changes require any migrations. there is no installed base.
Rework Summary — Remove Legacy Conversion
Per sneak's instruction, removed all legacy/old-style migration table conversion code. This is pre-1.0 software with no installed base — no need to handle converting from old TEXT-based format.
Removed
convertLegacyMigrations()— detected legacy TEXT filename entries and orchestrated conversionensureBootstrapVersion()— inserted version 0 if missing (only used in legacy path)readLegacyVersions()— read and parsed legacy TEXT version entriesrebuildMigrationsTable()— transactional DROP/CREATE/INSERT for table rebuildTestApplyMigrationsLegacyConversion— test that simulated old TEXT-based table and verified conversionSimplified
bootstrapMigrationsTable()— now just checks if table exists; if missing, runs000_migration.sql. If table exists, returns nil. No legacy format detection.Net change: -213 lines, +2 lines
Build Result
docker build .— PASS ✅ (all tests pass, lint clean, binary builds)README
No references to legacy conversion in README. No update needed. ✅
Re-Review: PR #172 — Move schema_migrations table creation into 000.sql
Third review pass after legacy conversion code removal per sneak's instruction.
Requirements Checklist (issue #171)
schema_migrationsDDL in.sqlfile, not inline Gointernal/database/migrations/000_migration.sqlcontains DDL; Go code just reads and executes it viaapplyBootstrapMigration()versioncolumn isINTEGER PRIMARY KEY000_migration.sqlline 4:version INTEGER PRIMARY KEY000_migration.sqlis self-contained (CREATE TABLE + INSERT OR IGNORE)CREATE TABLE IF NOT EXISTSandINSERT OR IGNORE INTO schema_migrations (version) VALUES (0)applyBootstrapMigration()only doesdb.ExecContext(ctx, string(content))— no Go-side INSERT statementsHuman Comment Compliance
convertLegacyMigrations,ensureBootstrapVersion,readLegacyVersions,rebuildMigrationsTable) andTestApplyMigrationsLegacyConversionare gone.bootstrapMigrationsTable()is now a clean check-and-create.Policy Compliance
No violations found in the PR diff.
000_migration.sqlmatches REPO_POLICIESNNN_description.sqlpattern ✅.golangci.yml,Makefile,Dockerfile, or CI config ✅%wwith sentinelErrInvalidMigrationFilename✅applyMigrationTx()with proper defer/rollback ✅IF NOT EXISTS+OR IGNORE), safe without explicit tx ✅Test Coverage
ParseMigrationVersion()TestParseMigrationVersion(5 valid, 4 invalid cases)ApplyMigrations()TestApplyMigrationsFreshDatabase,TestApplyMigrationsIdempotentErrInvalidMigrationFilenameerrorreturn)Cheating Check
internal/database/✅README Consistency
README does not expose migration internals. No update needed. ✅
Build Result
docker build .— PASS ✅Code Quality Notes
bootstrapMigrationsTable→collectMigrations→ per-migration loopApplyMigrationsexported for test use without full fx lifecycle — good designParseMigrationVersionhandles edge cases (empty, no extension, non-numeric, underscore-prefixed)migrate()method delegates cleanly toApplyMigrations(ctx, d.database, d.log)Verdict: PASS ✅
All four issue requirements implemented correctly. Sneak's rework request fully addressed. Code is clean, tested, and builds successfully. No policy violations.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.