From b641e8a9ac15cdf14fb1a08f7107b91e0423df95 Mon Sep 17 00:00:00 2001 From: Cenk Date: Sat, 22 Aug 2026 16:11:52 +0300 Subject: [PATCH 1/2] data: enforce user_places app contract in the database (#170) --- .../20260822_harden_user_places.sql | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 supabase/migrations/20260822_harden_user_places.sql diff --git a/supabase/migrations/20260822_harden_user_places.sql b/supabase/migrations/20260822_harden_user_places.sql new file mode 100644 index 0000000..a65d67b --- /dev/null +++ b/supabase/migrations/20260822_harden_user_places.sql @@ -0,0 +1,45 @@ +-- Harden user_places / user_home to match the app contract (#170). +-- +-- Follows 20260701_create_user_places.sql. Ships as a separate migration so +-- the checksum of the original file stays stable for databases where it was +-- already applied. +-- +-- Notes on the UPDATE policy: `using (auth.uid() = user_id)` with no explicit +-- WITH CHECK already blocks reassigning a row to another user, because +-- Postgres reuses the USING expression as the check and evaluates it against +-- the NEW row. The explicit WITH CHECK below is documentation + insurance: +-- it keeps that invariant true even if someone later adds a second update +-- policy or edits this one without re-deriving the semantics. + +alter table user_places + add constraint user_places_type_check + check (type in ( + 'library', + 'other_places', + 'airport', + 'sat_centre', + 'foreign_lang_exam_centre', + 'gov_offices' + )); + +alter table user_places + add constraint user_places_lat_check check (lat between -90 and 90); +alter table user_places + add constraint user_places_lng_check check (lng between -180 and 180); + +alter table user_home + add constraint user_home_lat_check check (lat between -90 and 90); +alter table user_home + add constraint user_home_lng_check check (lng between -180 and 180); + +drop policy if exists "Users can update their own places" on user_places; +create policy "Users can update their own places" + on user_places for update + using (auth.uid() = user_id) + with check (auth.uid() = user_id); + +drop policy if exists "Users can update their own home" on user_home; +create policy "Users can update their own home" + on user_home for update + using (auth.uid() = user_id) + with check (auth.uid() = user_id); From f8455124d052777744482b882c70b9a523d95407 Mon Sep 17 00:00:00 2001 From: Cenk Date: Sat, 22 Aug 2026 16:40:31 +0300 Subject: [PATCH 2/2] data: non-blocking constraint rollout + type-drift checklist (review) --- data/CONTRIBUTING.md | 12 ++++++ .../20260822_harden_user_places.sql | 43 ++++++++++++++++--- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/data/CONTRIBUTING.md b/data/CONTRIBUTING.md index d7e3585..d7363e1 100644 --- a/data/CONTRIBUTING.md +++ b/data/CONTRIBUTING.md @@ -89,3 +89,15 @@ One place per commit is fine. Multiple places of the same type in one commit is - Run `npm run dev` and verify the pin lands on the correct spot on the map before opening a PR. - If adding many places at once, batch by type (one commit per file). + +## Adding a new place type (maintainers) + +The list of valid place types lives in more than one place. Adding a type #7 +(e.g. a new `.json`) also requires: + +1. `src/lib/types.ts` — extend `PLACE_TYPES` (labels/colors derive from it) +2. `data/places.schema.json` — extend the `type` enum +3. `.github/ISSUE_TEMPLATE/add-place.yml` — extend the dropdown +4. A DB migration for the `user_places.type` CHECK constraint + (`supabase/migrations/20260822_harden_user_places.sql`) so saved places of + the new type pass validation diff --git a/supabase/migrations/20260822_harden_user_places.sql b/supabase/migrations/20260822_harden_user_places.sql index a65d67b..e5e7115 100644 --- a/supabase/migrations/20260822_harden_user_places.sql +++ b/supabase/migrations/20260822_harden_user_places.sql @@ -10,6 +10,29 @@ -- the NEW row. The explicit WITH CHECK below is documentation + insurance: -- it keeps that invariant true even if someone later adds a second update -- policy or edits this one without re-deriving the semantics. +-- +-- Constraint deployment (review feedback): ADD CONSTRAINT ... CHECK validates +-- every existing row under an ACCESS EXCLUSIVE lock, so a single legacy row +-- with a retired `type` (the imp_locations -> other_places rename predates +-- this) or an out-of-range coordinate would fail mid-deploy and block +-- everything behind it. Two mitigations: +-- 1. unknown types are first normalized to 'other_places' (the bucket the +-- app already uses for anything unmapped); +-- 2. constraints ship as NOT VALID + a separate VALIDATE step: the table is +-- only briefly locked for catalog changes, and any surviving bad row +-- surfaces as a clean validation failure instead of a locked-table +-- timeout. + +update user_places + set type = 'other_places' + where type not in ( + 'library', + 'other_places', + 'airport', + 'sat_centre', + 'foreign_lang_exam_centre', + 'gov_offices' + ); alter table user_places add constraint user_places_type_check @@ -20,17 +43,27 @@ alter table user_places 'sat_centre', 'foreign_lang_exam_centre', 'gov_offices' - )); + )) not valid; + +alter table user_places validate constraint user_places_type_check; alter table user_places - add constraint user_places_lat_check check (lat between -90 and 90); + add constraint user_places_lat_check + check (lat between -90 and 90) not valid; +alter table user_places validate constraint user_places_lat_check; alter table user_places - add constraint user_places_lng_check check (lng between -180 and 180); + add constraint user_places_lng_check + check (lng between -180 and 180) not valid; +alter table user_places validate constraint user_places_lng_check; alter table user_home - add constraint user_home_lat_check check (lat between -90 and 90); + add constraint user_home_lat_check + check (lat between -90 and 90) not valid; +alter table user_home validate constraint user_home_lat_check; alter table user_home - add constraint user_home_lng_check check (lng between -180 and 180); + add constraint user_home_lng_check + check (lng between -180 and 180) not valid; +alter table user_home validate constraint user_home_lng_check; drop policy if exists "Users can update their own places" on user_places; create policy "Users can update their own places"