210 lines
11 KiB
MySQL
210 lines
11 KiB
MySQL
|
|
-- =====================================================================
|
||
|
|
-- TeeCup — frittstående rundeføring med detaljert statistikk
|
||
|
|
-- (migrasjon 020, ADR-033)
|
||
|
|
-- =====================================================================
|
||
|
|
-- Frittstående runder er eid av en BRUKER (`app_user.id`), ikke en
|
||
|
|
-- organisasjon (Beslutning A) -- INGEN RLS på disse tabellene. Samme
|
||
|
|
-- allerede etablerte mønster som personlig profil (015)/HCP-historikk
|
||
|
|
-- (018)/sekundær e-post (017): `plain_connection()` (ingen
|
||
|
|
-- `app.current_org`), autorisasjon håndheves eksplisitt i app-laget med
|
||
|
|
-- `WHERE owner_user_id = $1` i hver spørring.
|
||
|
|
--
|
||
|
|
-- Banedata (Beslutning C, bekreftet med bruker): offisielle teeoff-baner
|
||
|
|
-- slås opp LIVE ved behov (ingen lokal kopi/import, kun en referanse +
|
||
|
|
-- et navne-snapshot for visning). Egendefinerte baner som ikke finnes i
|
||
|
|
-- teeoff havner i en NY, GLOBAL banekatalog (`personal_course` +
|
||
|
|
-- tilhørende hull/utslag/rating-tabeller) -- delt på tvers av ALLE
|
||
|
|
-- TeeCup-brukere (ikke org-scopet som den eksisterende `course`-tabellen),
|
||
|
|
-- med søk-før-opprett tenkt håndtert i API-/frontend-laget for å begrense
|
||
|
|
-- duplikater (ikke håndhevet i skjemaet).
|
||
|
|
--
|
||
|
|
-- `personal_course_tee_rating` har BEVISST ingen `scope`-kolonne (ulikt
|
||
|
|
-- den org-scopede `tee_rating`) -- Beslutning G sin Net-Par-tilnærming
|
||
|
|
-- for uspilte hull gjør at frittstående runder alltid regnes som en
|
||
|
|
-- 18-hulls-ekvivalent gjennom ÉN formel, uansett om 9 eller 18 hull
|
||
|
|
-- faktisk ble spilt. Ingen egen front_9/back_9-rating trengs derfor her.
|
||
|
|
--
|
||
|
|
-- Hver runde snapshotter par/stroke-index PER SPILT HULL på selve
|
||
|
|
-- `round_hole`-raden (samme reproduserbarhets-prinsipp som ADR-007 sin
|
||
|
|
-- handicap-snapshot) -- en fremtidig endring i teeoff sin banedata, eller
|
||
|
|
-- en redigering av en `personal_course`, skal ALDRI endre en allerede
|
||
|
|
-- spilt rundes tall retroaktivt.
|
||
|
|
--
|
||
|
|
-- Statistikk-modellen (Beslutning B): et fast sett navngitte felt per
|
||
|
|
-- hull, ikke fri slag-for-slag-logging. GIR er bevisst IKKE en egen
|
||
|
|
-- lagret kolonne -- den er alltid DERIVERBAR ved lesing
|
||
|
|
-- (`approach_result = 'hit' AND (score - putts) <= par - 2`), se
|
||
|
|
-- ADR-033.
|
||
|
|
--
|
||
|
|
-- Bevisst v1-avgrensning: en `round` er kun synlig/redigerbar for sin
|
||
|
|
-- `owner_user_id` -- en lenket deltaker (`round_participant.user_id`
|
||
|
|
-- satt) får IKKE egen tilgang til runden i denne runden (notert som
|
||
|
|
-- åpent punkt i ADR-033, ikke løst her).
|
||
|
|
-- =====================================================================
|
||
|
|
|
||
|
|
\set ON_ERROR_STOP on
|
||
|
|
|
||
|
|
-- ---------------------------------------------------------------------
|
||
|
|
-- Global banekatalog for egendefinerte (ikke-teeoff) baner
|
||
|
|
-- ---------------------------------------------------------------------
|
||
|
|
|
||
|
|
CREATE TABLE personal_course (
|
||
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
name text NOT NULL,
|
||
|
|
created_by_user_id uuid NOT NULL REFERENCES app_user(id) ON DELETE CASCADE,
|
||
|
|
created_at timestamptz NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE TABLE personal_course_hole (
|
||
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
personal_course_id uuid NOT NULL REFERENCES personal_course(id) ON DELETE CASCADE,
|
||
|
|
hole_number smallint NOT NULL CHECK (hole_number BETWEEN 1 AND 18),
|
||
|
|
par smallint NOT NULL CHECK (par BETWEEN 3 AND 6),
|
||
|
|
stroke_index smallint NOT NULL CHECK (stroke_index BETWEEN 1 AND 18),
|
||
|
|
UNIQUE (personal_course_id, hole_number),
|
||
|
|
UNIQUE (personal_course_id, stroke_index)
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE TABLE personal_course_tee (
|
||
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
personal_course_id uuid NOT NULL REFERENCES personal_course(id) ON DELETE CASCADE,
|
||
|
|
name text NOT NULL,
|
||
|
|
UNIQUE (personal_course_id, name)
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE TABLE personal_course_tee_rating (
|
||
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
personal_course_tee_id uuid NOT NULL REFERENCES personal_course_tee(id) ON DELETE CASCADE,
|
||
|
|
-- Kun m/f, ikke 'x' -- en WHS-rating er alltid for ett bestemt kjønn
|
||
|
|
-- (samme presedens som org-scopet tee_rating, ADR-029).
|
||
|
|
gender text NOT NULL CHECK (gender IN ('m', 'f')),
|
||
|
|
course_rating numeric(4,1) NOT NULL,
|
||
|
|
slope_rating smallint NOT NULL CHECK (slope_rating BETWEEN 55 AND 155),
|
||
|
|
par smallint NOT NULL,
|
||
|
|
UNIQUE (personal_course_tee_id, gender)
|
||
|
|
);
|
||
|
|
|
||
|
|
-- ---------------------------------------------------------------------
|
||
|
|
-- Selve runden
|
||
|
|
-- ---------------------------------------------------------------------
|
||
|
|
|
||
|
|
CREATE TABLE round (
|
||
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
owner_user_id uuid NOT NULL REFERENCES app_user(id) ON DELETE CASCADE,
|
||
|
|
|
||
|
|
course_source text NOT NULL CHECK (course_source IN ('teeoff', 'custom')),
|
||
|
|
-- 'teeoff': live oppslag, kun referanse + navn-snapshot lagres.
|
||
|
|
teeoff_facility_slug text,
|
||
|
|
teeoff_course_id text,
|
||
|
|
-- 'custom': ekte FK til den globale katalogen over.
|
||
|
|
personal_course_id uuid REFERENCES personal_course(id),
|
||
|
|
CHECK (
|
||
|
|
(course_source = 'teeoff'
|
||
|
|
AND teeoff_facility_slug IS NOT NULL AND teeoff_course_id IS NOT NULL
|
||
|
|
AND personal_course_id IS NULL)
|
||
|
|
OR
|
||
|
|
(course_source = 'custom'
|
||
|
|
AND personal_course_id IS NOT NULL
|
||
|
|
AND teeoff_facility_slug IS NULL AND teeoff_course_id IS NULL)
|
||
|
|
),
|
||
|
|
-- Snapshot for visning uten et nytt live-oppslag hver gang runden åpnes.
|
||
|
|
course_name_snapshot text NOT NULL,
|
||
|
|
tee_name_snapshot text NOT NULL,
|
||
|
|
|
||
|
|
played_at date NOT NULL,
|
||
|
|
-- Fritt starthull (Beslutning E) -- 9/18 under er kun et UI-standardvalg,
|
||
|
|
-- IKKE håndhevet (spilleren kan avslutte etter et vilkårlig antall hull).
|
||
|
|
start_hole smallint NOT NULL DEFAULT 1 CHECK (start_hole BETWEEN 1 AND 18),
|
||
|
|
holes_planned smallint NOT NULL DEFAULT 18 CHECK (holes_planned IN (9, 18)),
|
||
|
|
|
||
|
|
completed_at timestamptz,
|
||
|
|
created_at timestamptz NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE INDEX ON round (owner_user_id);
|
||
|
|
|
||
|
|
-- ---------------------------------------------------------------------
|
||
|
|
-- Deltakere i runden -- eieren selv ELLER andre i flighten (Beslutning D)
|
||
|
|
-- ---------------------------------------------------------------------
|
||
|
|
|
||
|
|
CREATE TABLE round_participant (
|
||
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
round_id uuid NOT NULL REFERENCES round(id) ON DELETE CASCADE,
|
||
|
|
|
||
|
|
-- Nøyaktig én av user_id/guest_name -- en ekte TeeCup-bruker som
|
||
|
|
-- spiller med, ELLER et rent navn uten konto (samme prinsipp som
|
||
|
|
-- org-scopet `player` uten `user_id`).
|
||
|
|
user_id uuid REFERENCES app_user(id),
|
||
|
|
guest_name text,
|
||
|
|
CHECK (
|
||
|
|
((user_id IS NOT NULL)::int + (guest_name IS NOT NULL)::int) = 1
|
||
|
|
),
|
||
|
|
|
||
|
|
is_owner boolean NOT NULL DEFAULT false,
|
||
|
|
|
||
|
|
-- Snapshot ved tilføyelse (samme reproduserbarhets-prinsipp som ADR-007
|
||
|
|
-- sin handicap-snapshot) -- en senere profilendring skal ikke endre en
|
||
|
|
-- allerede spilt rundes tall retroaktivt.
|
||
|
|
gender text NOT NULL CHECK (gender IN ('m', 'f', 'x')),
|
||
|
|
handicap_index_snapshot numeric(4,1), -- NULL = ingen HCP-sporing for denne deltakeren
|
||
|
|
course_handicap_snapshot smallint,
|
||
|
|
|
||
|
|
-- Fylles ved fullføring (ADR-033 Beslutning F/G) -- kun sant/satt når
|
||
|
|
-- minimums-/gyldighetskravene er oppfylt.
|
||
|
|
counts_for_handicap boolean NOT NULL DEFAULT false,
|
||
|
|
score_differential numeric(4,1),
|
||
|
|
|
||
|
|
created_at timestamptz NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE INDEX ON round_participant (round_id);
|
||
|
|
CREATE INDEX ON round_participant (user_id);
|
||
|
|
-- Maks én markert eier per runde.
|
||
|
|
CREATE UNIQUE INDEX round_participant_one_owner ON round_participant (round_id) WHERE is_owner;
|
||
|
|
|
||
|
|
-- ---------------------------------------------------------------------
|
||
|
|
-- Hull-for-hull -- rating-snapshot OG statistikk, per deltaker
|
||
|
|
-- ---------------------------------------------------------------------
|
||
|
|
|
||
|
|
CREATE TABLE round_hole (
|
||
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
round_participant_id uuid NOT NULL REFERENCES round_participant(id) ON DELETE CASCADE,
|
||
|
|
hole_number smallint NOT NULL CHECK (hole_number BETWEEN 1 AND 18),
|
||
|
|
|
||
|
|
-- Rating-snapshot (par/stroke-index på TIDSPUNKTET runden ble spilt) --
|
||
|
|
-- se moduldoc-kommentaren øverst om hvorfor dette IKKE er en live-referanse.
|
||
|
|
par smallint NOT NULL CHECK (par BETWEEN 3 AND 6),
|
||
|
|
stroke_index smallint NOT NULL CHECK (stroke_index BETWEEN 1 AND 18),
|
||
|
|
|
||
|
|
played boolean NOT NULL DEFAULT false,
|
||
|
|
score smallint CHECK (score IS NULL OR score BETWEEN 1 AND 20),
|
||
|
|
putts smallint CHECK (putts IS NULL OR putts BETWEEN 0 AND 10),
|
||
|
|
club_off_tee text,
|
||
|
|
-- Kun meningsfullt på par 4/5 (håndheves i app-laget, ikke her).
|
||
|
|
tee_shot_result text CHECK (tee_shot_result IS NULL OR tee_shot_result IN ('fairway', 'left', 'right')),
|
||
|
|
-- "Innspillsslaget" = siste slag før første putt, uansett hullets par
|
||
|
|
-- (ADR-033 Beslutning B) -- GIR er DERIVERT herfra ved lesing, ikke en
|
||
|
|
-- egen lagret kolonne: approach_result='hit' AND (score-putts) <= par-2.
|
||
|
|
approach_result text CHECK (approach_result IS NULL OR approach_result IN ('hit', 'long', 'short', 'left', 'right')),
|
||
|
|
chip_count smallint CHECK (chip_count IS NULL OR chip_count >= 0),
|
||
|
|
bunker_shot_count smallint CHECK (bunker_shot_count IS NULL OR bunker_shot_count >= 0),
|
||
|
|
penalty_strokes smallint CHECK (penalty_strokes IS NULL OR penalty_strokes >= 0),
|
||
|
|
first_putt_distance_m numeric(4,1) CHECK (first_putt_distance_m IS NULL OR first_putt_distance_m >= 0),
|
||
|
|
|
||
|
|
UNIQUE (round_participant_id, hole_number)
|
||
|
|
);
|
||
|
|
|
||
|
|
-- ---------------------------------------------------------------------
|
||
|
|
-- Rettigheter -- INGEN RLS her (Beslutning A). teecup_app filtrerer selv
|
||
|
|
-- med WHERE owner_user_id = $1 (round) / gjennom round_id (de to andre) i
|
||
|
|
-- hver spørring, samme mønster som handicap_history/user_secondary_email.
|
||
|
|
-- ---------------------------------------------------------------------
|
||
|
|
|
||
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON personal_course TO teecup_app;
|
||
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON personal_course_hole TO teecup_app;
|
||
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON personal_course_tee TO teecup_app;
|
||
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON personal_course_tee_rating TO teecup_app;
|
||
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON round TO teecup_app;
|
||
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON round_participant TO teecup_app;
|
||
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON round_hole TO teecup_app;
|