Files
Completed/db/migrations/20261003000000_init.up.sql
T
SansanandClaude Sonnet 5 7fbd403d30 Make the platform list shared across users; keep games private
Platforms no longer have a user_id: there's one global list, unique by
name, that every authenticated user reads from and writes to. Games are
unchanged - still private per user, still referencing platform_id.

- Migration: dropped platform.user_id and its FK, UNIQUE(user_id, name) ->
  UNIQUE(name), index on change_seq alone instead of (user_id, change_seq).
  Amended the one init migration in place rather than stacking a second one,
  since nothing is deployed yet.
- db/query.sql: platform queries no longer take a user_id parameter.
  DeletePlatform's NOT EXISTS guard already checked games from any user (it
  never filtered the subquery by user_id), so deleting a shared platform is
  correctly blocked by any user's games on it, not just the deleter's.
- routes.go: platform handlers still require auth (the list isn't public to
  anonymous requests) but no longer scope by user.
- Tests: updated the mocks for the new signatures, added
  TestListPlatformsIsSharedAcrossUsers.

Verified against a real Postgres: user A creates "PC", user B sees it
immediately, both can add games against it, each only sees their own
games, deleting the platform is blocked while either user has a game on
it, and the name stays globally unique.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-03 11:34:47 +02:00

76 lines
2.7 KiB
PL/PgSQL

-- Requires pgcrypto for gen_random_uuid(); UUIDs are v7 when the app generates
-- them (see internal/model), but the DB default falls back to v4 for rows
-- inserted outside the app (e.g. manual SQL).
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS citext;
-- change_seq is a single global sequence shared by platform and game so a
-- client can ask "what changed since N?" across both tables in one pass.
CREATE SEQUENCE change_seq_seq;
CREATE TABLE users (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
email citext NOT NULL UNIQUE,
password_hash text NOT NULL,
name text NOT NULL,
bio text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE sessions (
token_hash bytea PRIMARY KEY,
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT now(),
expires_at timestamptz NOT NULL,
last_seen_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX idx_sessions_user_id ON sessions(user_id);
-- Platforms are shared: every user picks from and adds to the same list,
-- unlike games, which are private per user_id.
CREATE TABLE platform (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz,
change_seq bigint NOT NULL DEFAULT nextval('change_seq_seq'),
UNIQUE (name)
);
CREATE INDEX idx_platform_change_seq ON platform(change_seq);
CREATE TABLE game (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
platform_id uuid NOT NULL REFERENCES platform(id),
name text NOT NULL,
score int NOT NULL CHECK (score BETWEEN 0 AND 100),
release_date date NOT NULL,
finished date,
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz,
change_seq bigint NOT NULL DEFAULT nextval('change_seq_seq')
);
CREATE INDEX idx_game_user_change_seq ON game(user_id, change_seq);
-- Keep updated_at/change_seq current on every UPDATE so sync can detect
-- changes without clients having to set them manually.
CREATE OR REPLACE FUNCTION touch_change_seq() RETURNS trigger AS $$
BEGIN
NEW.updated_at := now();
NEW.change_seq := nextval('change_seq_seq');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER platform_touch_change_seq
BEFORE UPDATE ON platform
FOR EACH ROW EXECUTE FUNCTION touch_change_seq();
CREATE TRIGGER game_touch_change_seq
BEFORE UPDATE ON game
FOR EACH ROW EXECUTE FUNCTION touch_change_seq();