upaas does not start on a database from before PR 172 (migration versions stored as file names) #237

Closed
opened 2026-09-29 11:16:04 +02:00 by clawbot · 3 comments
Collaborator

sneak, 2026-09-29, redeploying upaas main on fsn1app1 over the database of the upaas that ran there before (verbatim excerpt of what he pasted):

[Fx] HOOK OnStart               sneak.berlin/go/upaas/internal/database.New.func1() called by sneak.berlin/go/upaas/internal/database.New failed in 7.271684ms: failed to run migrations: failed to record migration 000_migration.sql: UNIQUE constraint failed: schema_migrations.version
[Fx] ERROR              Start failed, rolling back: failed to run migrations: failed to record migration 000_migration.sql: UNIQUE constraint failed: schema_migrations.version
[Fx] ERROR              Failed to start: failed to run migrations: failed to record migration 000_migration.sql: UNIQUE constraint failed: schema_migrations.version

upaas does not start. Cause: before PR 172 (d5c3d4d5, 2026-03-30) upaas created schema_migrations with version TEXT PRIMARY KEY and recorded each migration by its file name (001_initial.sql). The current runner only bootstraps the table when it is missing, then looks each migration up by its number, finds none, and runs 000_migration.sql; that file's own INSERT OR IGNORE ... VALUES (0) inserts version 0, and the loop's INSERT INTO schema_migrations (version) VALUES (0) then collides. Had it got past that, it would have re-run 001_initial.sql onto live tables. Every upaas upgraded from before PR 172 hits this. A fresh database is unaffected.

Definition of done:

  • upaas starts on a database written by upaas from before PR 172: it recognises the old table (file-name versions), converts it to numeric versions in one transaction, and then applies only the migrations that were never applied. No other table or row changes.
  • A test builds the old table exactly as the pre-172 code did (file-name versions, some but not all migrations applied), starts the migration step, and checks the converted versions, that the missing migrations ran once, and that the data is intact.
  • Version 0 is recorded once: the bootstrap file and the loop no longer both insert it.
  • A fresh database still works as today (existing tests stay green).
  • The fix goes to main promptly: sneak's upaas on fsn1app1 is down until it lands or he rolls back.

Model: opus-5-5

sneak, 2026-09-29, redeploying upaas `main` on fsn1app1 over the database of the upaas that ran there before (verbatim excerpt of what he pasted): ``` [Fx] HOOK OnStart sneak.berlin/go/upaas/internal/database.New.func1() called by sneak.berlin/go/upaas/internal/database.New failed in 7.271684ms: failed to run migrations: failed to record migration 000_migration.sql: UNIQUE constraint failed: schema_migrations.version [Fx] ERROR Start failed, rolling back: failed to run migrations: failed to record migration 000_migration.sql: UNIQUE constraint failed: schema_migrations.version [Fx] ERROR Failed to start: failed to run migrations: failed to record migration 000_migration.sql: UNIQUE constraint failed: schema_migrations.version ``` upaas does not start. Cause: before PR 172 (`d5c3d4d5`, 2026-03-30) upaas created `schema_migrations` with `version TEXT PRIMARY KEY` and recorded each migration by its file name (`001_initial.sql`). The current runner only bootstraps the table when it is missing, then looks each migration up by its number, finds none, and runs `000_migration.sql`; that file's own `INSERT OR IGNORE ... VALUES (0)` inserts version 0, and the loop's `INSERT INTO schema_migrations (version) VALUES (0)` then collides. Had it got past that, it would have re-run `001_initial.sql` onto live tables. Every upaas upgraded from before PR 172 hits this. A fresh database is unaffected. Definition of done: - upaas starts on a database written by upaas from before PR 172: it recognises the old table (file-name versions), converts it to numeric versions in one transaction, and then applies only the migrations that were never applied. No other table or row changes. - A test builds the old table exactly as the pre-172 code did (file-name versions, some but not all migrations applied), starts the migration step, and checks the converted versions, that the missing migrations ran once, and that the data is intact. - Version 0 is recorded once: the bootstrap file and the loop no longer both insert it. - A fresh database still works as today (existing tests stay green). - The fix goes to `main` promptly: sneak's upaas on fsn1app1 is down until it lands or he rolls back. Model: opus-5-5
clawbot self-assigned this 2026-09-29 11:16:04 +02:00
Author
Collaborator

Plan (the cause and definition of done are above; this is the implementer's brief).

In internal/database/migrations.go, before the migration loop:

  1. Recognise the old table: schema_migrations exists and its version column is declared TEXT (PRAGMA table_info). Only that case is converted. A table that is missing gets the normal bootstrap, and one with INTEGER versions is left alone. The owner's hand-repaired database on fsn1app1 must also start.
  2. Convert it in one transaction. Create the table as 000_migration.sql defines it, then copy each old row with its version turned into a number by ParseMigrationVersion, keeping applied_at. Drop the old table and put the new one in its place, and record version 0. A row that ParseMigrationVersion rejects makes the transaction roll back with an error that names the row. Nothing is guessed. No other table is touched.
  3. Record version 0 once: the loop skips the bootstrap migration, which only the bootstrap step applies. Do not remove the INSERT OR IGNORE from 000_migration.sql; a fresh database needs it.
  4. Tests in internal/database/migrations_test.go:
    • Build the table exactly as the pre-172 runner did (version TEXT PRIMARY KEY, file-name versions). Apply the real 001 to 004 files and record them the old way, and put a row in apps.
    • Run ApplyMigrations and check that versions 0 to 7 are numeric and each recorded once, that 005 to 007 ran once, that applied_at of the old rows is kept, and that the row survived.
    • Add the rejected-row case.
    • Keep the fresh-database and idempotence tests green.

Small and plain. One conversion function with a comment naming the old format. No migration framework and no new table.

Model: opus-5-5

Plan (the cause and definition of done are above; this is the implementer's brief). In `internal/database/migrations.go`, before the migration loop: 1. Recognise the old table: `schema_migrations` exists and its `version` column is declared `TEXT` (`PRAGMA table_info`). Only that case is converted. A table that is missing gets the normal bootstrap, and one with `INTEGER` versions is left alone. The owner's hand-repaired database on fsn1app1 must also start. 2. Convert it in one transaction. Create the table as `000_migration.sql` defines it, then copy each old row with its version turned into a number by `ParseMigrationVersion`, keeping `applied_at`. Drop the old table and put the new one in its place, and record version 0. A row that `ParseMigrationVersion` rejects makes the transaction roll back with an error that names the row. Nothing is guessed. No other table is touched. 3. Record version 0 once: the loop skips the bootstrap migration, which only the bootstrap step applies. Do not remove the `INSERT OR IGNORE` from `000_migration.sql`; a fresh database needs it. 4. Tests in `internal/database/migrations_test.go`: - Build the table exactly as the pre-172 runner did (`version TEXT PRIMARY KEY`, file-name versions). Apply the real `001` to `004` files and record them the old way, and put a row in `apps`. - Run `ApplyMigrations` and check that versions 0 to 7 are numeric and each recorded once, that `005` to `007` ran once, that `applied_at` of the old rows is kept, and that the row survived. - Add the rejected-row case. - Keep the fresh-database and idempotence tests green. Small and plain. One conversion function with a comment naming the old format. No migration framework and no new table. Model: opus-5-5
Author
Collaborator

sneak's upaas on fsn1app1 now starts: he backed up the database, converted schema_migrations by hand (UPDATE schema_migrations SET version = CAST(substr(version,1,3) AS INTEGER); INSERT OR IGNORE INTO schema_migrations (version) VALUES (0);, leaving versions 0-6), and on start upaas applied 007_add_resource_limits. His recorded names were 001_initial.sql through 006_add_previous_image_id.sql. The fix here is still needed for any other install from before PR 172; that list is a real example of the old table's contents for the test.

Model: opus-5-5

sneak's upaas on fsn1app1 now starts: he backed up the database, converted `schema_migrations` by hand (`UPDATE schema_migrations SET version = CAST(substr(version,1,3) AS INTEGER); INSERT OR IGNORE INTO schema_migrations (version) VALUES (0);`, leaving versions 0-6), and on start upaas applied `007_add_resource_limits`. His recorded names were `001_initial.sql` through `006_add_previous_image_id.sql`. The fix here is still needed for any other install from before PR 172; that list is a real example of the old table's contents for the test. Model: opus-5-5
Author
Collaborator

Closed on sneak's word, 2026-09-29 in chat (verbatim):

there is no other installed base, only me.

His install on fsn1app1 was converted by hand and starts (#237 (comment)), so no upgrade path for older databases is needed.

Model: opus-5-5

Closed on sneak's word, 2026-09-29 in chat (verbatim): > there is no other installed base, only me. His install on fsn1app1 was converted by hand and starts (https://git.eeqj.de/sneak/upaas/issues/237#issuecomment-106833), so no upgrade path for older databases is needed. Model: opus-5-5
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/upaas#237