store: add users table and user_id scoping to migrations
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.
This commit is contained in:
6
backend/internal/store/migrations/0021_users_table.sql
Normal file
6
backend/internal/store/migrations/0021_users_table.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
oidc_sub TEXT NOT NULL UNIQUE,
|
||||
display_name TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
@@ -0,0 +1,15 @@
|
||||
-- user_id is nullable here even though every row will eventually need one:
|
||||
-- migrations can't take runtime parameters, so the actual owner isn't known
|
||||
-- yet. A one-time Go bootstrap step (store.ClaimLegacyOwner, run at
|
||||
-- geniusrund startup) backfills every existing row to one user once given
|
||||
-- that user's OIDC subject; from then on every store method requires a
|
||||
-- non-nil userID and this column is never NULL again in practice.
|
||||
ALTER TABLE activities ADD COLUMN user_id INTEGER REFERENCES users(id);
|
||||
ALTER TABLE sync_runs ADD COLUMN user_id INTEGER REFERENCES users(id);
|
||||
|
||||
-- Used by UpsertActivity's ON CONFLICT target going forward. The original
|
||||
-- UNIQUE(garmin_activity_id) constraint (migration 0001) stays in place too
|
||||
-- -- Garmin's own activity ids are already globally unique in practice, so
|
||||
-- the stricter constraint is harmless, and SQLite can't drop a column-level
|
||||
-- constraint without a full table rebuild, which isn't worth the risk here.
|
||||
CREATE UNIQUE INDEX ux_activities_user_garmin_id ON activities(user_id, garmin_activity_id);
|
||||
@@ -0,0 +1,76 @@
|
||||
-- profile.id's CHECK(id = 1) singleton constraint (migration 0003) can't be
|
||||
-- dropped via ALTER TABLE -- SQLite has no DROP CONSTRAINT -- so this
|
||||
-- rebuilds the table via SQLite's documented rename/recreate/copy/drop
|
||||
-- pattern instead. Always create the replacement under a different name and
|
||||
-- RENAME it into place at the end (never rename the live table away first)
|
||||
-- -- verified directly against SQLite that this ordering is what keeps
|
||||
-- other tables' foreign keys intact when they exist (not the case for
|
||||
-- profile, but kept consistent with migrations 0024/0025 for the same
|
||||
-- pattern). user_id is nullable for the same not-yet-known-owner reason as
|
||||
-- migration 0022 -- see its comment.
|
||||
CREATE TABLE profile_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER REFERENCES users(id),
|
||||
name TEXT NOT NULL DEFAULT 'Default',
|
||||
garmin_email TEXT NOT NULL DEFAULT '',
|
||||
garmin_password TEXT NOT NULL DEFAULT '',
|
||||
rolling_window_days INTEGER NOT NULL DEFAULT 90,
|
||||
backfill_horizon_days INTEGER NOT NULL DEFAULT 1095,
|
||||
max_heart_rate REAL,
|
||||
resting_heart_rate REAL,
|
||||
hr_zone1_min_pct REAL NOT NULL DEFAULT 50,
|
||||
hr_zone1_max_pct REAL NOT NULL DEFAULT 60,
|
||||
hr_zone2_min_pct REAL NOT NULL DEFAULT 60,
|
||||
hr_zone2_max_pct REAL NOT NULL DEFAULT 70,
|
||||
hr_zone3_min_pct REAL NOT NULL DEFAULT 70,
|
||||
hr_zone3_max_pct REAL NOT NULL DEFAULT 80,
|
||||
hr_zone4_min_pct REAL NOT NULL DEFAULT 80,
|
||||
hr_zone4_max_pct REAL NOT NULL DEFAULT 90,
|
||||
hr_zone5_min_pct REAL NOT NULL DEFAULT 90,
|
||||
hr_zone5_max_pct REAL NOT NULL DEFAULT 100,
|
||||
warmup_minutes REAL NOT NULL DEFAULT 10,
|
||||
cooldown_minutes REAL NOT NULL DEFAULT 5,
|
||||
min_representative_pace_sec_per_km REAL NOT NULL DEFAULT 720,
|
||||
min_representative_time_seconds REAL NOT NULL DEFAULT 3,
|
||||
pace_color TEXT NOT NULL DEFAULT '#3b82f6',
|
||||
heart_rate_color TEXT NOT NULL DEFAULT '#ef4444',
|
||||
warmup_color TEXT NOT NULL DEFAULT '#c2410c',
|
||||
effort_color TEXT NOT NULL DEFAULT '#7c3aed',
|
||||
recovery_color TEXT NOT NULL DEFAULT '#15803d',
|
||||
cooldown_color TEXT NOT NULL DEFAULT '#fb923c',
|
||||
main_line_tint_pct REAL NOT NULL DEFAULT 20,
|
||||
background_darken_pct REAL NOT NULL DEFAULT 35,
|
||||
target_brighten_pct REAL NOT NULL DEFAULT 20,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE(user_id)
|
||||
);
|
||||
|
||||
INSERT INTO profile_new (
|
||||
id, user_id, name, garmin_email, garmin_password, rolling_window_days, backfill_horizon_days,
|
||||
max_heart_rate, resting_heart_rate,
|
||||
hr_zone1_min_pct, hr_zone1_max_pct, hr_zone2_min_pct, hr_zone2_max_pct,
|
||||
hr_zone3_min_pct, hr_zone3_max_pct, hr_zone4_min_pct, hr_zone4_max_pct,
|
||||
hr_zone5_min_pct, hr_zone5_max_pct,
|
||||
warmup_minutes, cooldown_minutes,
|
||||
min_representative_pace_sec_per_km, min_representative_time_seconds,
|
||||
pace_color, heart_rate_color, warmup_color, effort_color, recovery_color, cooldown_color,
|
||||
main_line_tint_pct, background_darken_pct, target_brighten_pct,
|
||||
created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
id, NULL, name, garmin_email, garmin_password, rolling_window_days, backfill_horizon_days,
|
||||
max_heart_rate, resting_heart_rate,
|
||||
hr_zone1_min_pct, hr_zone1_max_pct, hr_zone2_min_pct, hr_zone2_max_pct,
|
||||
hr_zone3_min_pct, hr_zone3_max_pct, hr_zone4_min_pct, hr_zone4_max_pct,
|
||||
hr_zone5_min_pct, hr_zone5_max_pct,
|
||||
warmup_minutes, cooldown_minutes,
|
||||
min_representative_pace_sec_per_km, min_representative_time_seconds,
|
||||
pace_color, heart_rate_color, warmup_color, effort_color, recovery_color, cooldown_color,
|
||||
main_line_tint_pct, background_darken_pct, target_brighten_pct,
|
||||
created_at, updated_at
|
||||
FROM profile;
|
||||
|
||||
DROP TABLE profile;
|
||||
|
||||
ALTER TABLE profile_new RENAME TO profile;
|
||||
@@ -0,0 +1,31 @@
|
||||
-- workout_kinds.name's UNIQUE(name) constraint (migration 0001) must become
|
||||
-- per-user (UNIQUE(user_id, name)) now that every user gets their own copy
|
||||
-- of the same 8-kind taxonomy -- otherwise a second user could never be
|
||||
-- provisioned (inserting the same seeded name would collide). kind_assignments
|
||||
-- and workout_type_paces hold foreign keys into this table
|
||||
-- (workout_kind_id REFERENCES workout_kinds(id)) -- the create-new-name/
|
||||
-- copy/drop-old/rename-into-place order below (verified against a real
|
||||
-- SQLite database) leaves those foreign keys' schema text untouched
|
||||
-- throughout, so they resolve correctly again the instant the final
|
||||
-- `ALTER TABLE workout_kinds_new RENAME TO workout_kinds` completes.
|
||||
CREATE TABLE workout_kinds_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER REFERENCES users(id),
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
color TEXT NOT NULL DEFAULT '',
|
||||
rule_json TEXT NOT NULL,
|
||||
priority INTEGER NOT NULL DEFAULT 0,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE(user_id, name)
|
||||
);
|
||||
|
||||
INSERT INTO workout_kinds_new (id, user_id, name, description, color, rule_json, priority, is_active, created_at, updated_at)
|
||||
SELECT id, NULL, name, description, color, rule_json, priority, is_active, created_at, updated_at
|
||||
FROM workout_kinds;
|
||||
|
||||
DROP TABLE workout_kinds;
|
||||
|
||||
ALTER TABLE workout_kinds_new RENAME TO workout_kinds;
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Same CHECK(id = 1) rebuild reasoning as migration 0023's profile rebuild.
|
||||
CREATE TABLE sync_state_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER REFERENCES users(id),
|
||||
earliest_synced_date TEXT,
|
||||
backfill_complete INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE(user_id)
|
||||
);
|
||||
|
||||
INSERT INTO sync_state_new (id, user_id, earliest_synced_date, backfill_complete)
|
||||
SELECT id, NULL, earliest_synced_date, backfill_complete
|
||||
FROM sync_state;
|
||||
|
||||
DROP TABLE sync_state;
|
||||
|
||||
ALTER TABLE sync_state_new RENAME TO sync_state;
|
||||
Reference in New Issue
Block a user