Foreign keys were only enforced on one pooled connection, orphaning rows and breaking the 0.9.0 upgrade #163
Labels
No labels
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
qwc-open/asiakirjat#163
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Found while recovering a production instance that could not start after the upgrade to 0.9.0.
Symptom
The instance failed at startup with:
Migration
011_project_access_source_uniqueis the only one in the 010 → 017 range that rebuilds a table (the SQLite repair from #133). The rebuild inserts into a new table with live FK references, so it enforces constraints that the old table had not been enforcing. Production had 11 rows that violated them, the INSERT failed, golang-migrate rolled the transaction back and leftschema_migrationsdirty at 11.The data itself was intact — the driver wraps each migration in a transaction and SQLite has transactional DDL — but the instance was down.
The 11 orphans
All eleven were children of deleted projects, across four tables, every one of them under an
ON DELETE CASCADEthat never fired:project_accessproject_id→projectsversionsproject_id→projectsupload_logsproject_id→projectsapi_tokensproject_id→projectsuploaded_byviolations: none.Cause
internal/database/database.godid this after opening:dbis a*sqlx.DB— a pool, not a connection. The pragma reached whichever connection happened to serve it; every connection the pool opened afterwards kept SQLite's default of foreign keys OFF.foreign_keysis a per-connection setting, so enforcement was effectively a coin toss.ProjectStore.Delete(internal/store/sql/project.go:151) is a bareDELETE FROM projects WHERE id = ?that relies entirely on cascade for its children. Served by a connection with foreign keys off, it deleted the project and silently left everything that pointed at it behind.Two details worth recording:
009_uploaded_by_nullableperforms the same kind of table rebuild onversionsandupload_logs, and it succeeded on this database. So the orphans were created after 009 — during 0.8.x — which dates the rot and confirms the mechanism.Recovery applied to production
PRAGMA foreign_key_checkis clean afterwards and the instance starts.Fix
Move both pragmas into the DSN so the driver applies them in its connection-open path (
applyQueryParams, called per connection), and assert at startup that foreign keys are actually enabled rather than trusting that they are.Note for other installations
Any SQLite database that ran 0.8.x may carry the same orphans and will fail the same way on the 011 rebuild.
PRAGMA foreign_key_check;reports them; the SQL above clears them. A new migration cannot help here, since a database stuck dirty at 11 never reaches one.Assisted-by: Claude Opus 5