store: fix migration 0026's silent-cascade-delete FK toggle bug

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>
This commit is contained in:
2026-07-25 14:28:16 +02:00
parent d8b722820d
commit b08f0152ef
3 changed files with 109 additions and 31 deletions

View File

@@ -79,16 +79,19 @@ func (db *DB) migrate() error {
return fmt.Errorf("read migration %s: %w", name, err)
}
// Migrations 0023, 0024, and 0025 rebuild tables that have incoming
// foreign keys (kind_assignments/workout_type_paces reference
// workout_kinds; sync_state is referenced by activities/sync_runs).
// These require temporary FK disable in autocommit mode (before the
// transaction begins) so the DROP TABLE succeeds; a mid-transaction
// PRAGMA is a no-op with modernc.org/sqlite.
// Migrations 0023, 0024, 0025, and 0026 rebuild tables that have
// incoming foreign keys (kind_assignments/workout_type_paces
// reference workout_kinds; sync_state is referenced by
// activities/sync_runs; laps/activity_samples/kind_assignments
// reference activities). These require temporary FK disable in
// autocommit mode (before the transaction begins) so the DROP
// TABLE succeeds; a mid-transaction PRAGMA is a no-op with
// modernc.org/sqlite.
tableRebuildMigrations := map[string]bool{
"0023_profile_user_scoped.sql": true,
"0024_workout_kinds_user_scoped.sql": true,
"0025_sync_state_user_scoped.sql": true,
"0023_profile_user_scoped.sql": true,
"0024_workout_kinds_user_scoped.sql": true,
"0025_sync_state_user_scoped.sql": true,
"0026_activities_unique_constraint.sql": true,
}
needsFKToggle := tableRebuildMigrations[name]