Migration 0026 (activities table rebuild for the per-user unique
constraint) issued its own PRAGMA foreign_keys = OFF/ON inside the
migration's SQL content, but the whole migration file runs inside one
db.go tx.Begin()/tx.Exec()/tx.Commit() transaction, and SQLite documents
PRAGMA foreign_keys as a no-op once a transaction is open. As a result FK
enforcement never actually got disabled, so DROP TABLE activities
triggered SQLite's implicit DELETE FROM semantics, firing ON DELETE
CASCADE on every row in laps, activity_samples, and kind_assignments for
every activity -- silently, with no error. On a real upgrade with synced
data this would have permanently destroyed all lap/sample/classification
history. It was masked because every existing migration test runs
migrations back-to-back on an empty temp database with no pre-existing
child rows.
Fix: add "0026_activities_unique_constraint.sql" to db.go's
tableRebuildMigrations map so it gets the same autocommit-mode FK
disable/enable toggle (before/after the transaction) already used for
migrations 0023/0024/0025, and remove the now-redundant/misleading
mid-transaction PRAGMA lines from the migration file itself, matching
the established pattern.
Also restore the DEFAULT '' on event_type_key in migration 0026's
rebuilt activities table -- it was dropped from migration 0007's
original column definition during the rebuild, which broke every insert
that omits event_type_key and relies on that default
(TestClaimLegacyOwner_BindsExistingSingletonRowsToOneNewUser and others).
Extend TestRebuildMigrationsPreserveForeignKeyReferences to cover 0026:
seed a laps row and an activity_samples row (in addition to the existing
kind_assignments row) against a pre-existing activities row before the
rebuild migrations run, then verify after 0023-0026 complete that all
three child rows still exist and still reference the same activity, and
that FK enforcement rejects bogus activity_id/workout_kind_id afterward.
This is the regression guard that would have caught the original bug.
Note: TestClaimLegacyOwner_NoOpOnGenuinelyFreshInstall still fails on
this branch; verified it fails identically at the prior commit
(d8b7228), so it's a pre-existing, unrelated bug in ClaimLegacyOwner
(not migration 0026 or this FK-toggle bug) and out of scope for this fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The previous implementation issued PRAGMA foreign_keys toggles inside
transactions (after tx.Begin()), but SQLite ignores pragmas once a
transaction is open with modernc.org/sqlite, making those calls no-ops.
This fix relocates the toggle to autocommit mode (db.Exec, not tx.Exec)
and limits it to only the three migrations that need it (0023/0024/0025),
which rebuild tables with incoming foreign keys (kind_assignments/
workout_type_paces reference workout_kinds; activities/sync_runs
reference sync_state).
The connection string now correctly restores ?_pragma=foreign_keys(1),
ensuring FK enforcement is ON by default for normal runtime operation
and during migrations that don't need the special handling.
Each affected migration now:
1. Disables FK in autocommit mode before starting its transaction
2. Runs the migration's CREATE/INSERT/DROP/RENAME sequence
3. Re-enables FK in autocommit mode after the transaction commits
All other migrations run normally with FK enforcement active throughout,
protecting against cascading deletions silently failing if a future
connection change causes FK enforcement to inadvertently remain off.
Added TestForeignKeyEnforcementPostMigration to verify FK is correctly
enforced after all migrations complete: valid FK references are accepted,
and invalid ones are rejected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Schema-only step toward per-user profile isolation: profile/workout_kinds/
sync_state are rebuilt to drop their singleton constraints, activities/
sync_runs gain a nullable user_id column. Store methods are scoped in
later tasks.