From ee13e7bc9d7d2ec0c4bc65e80163fc32789f42e9 Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Tue, 19 May 2026 03:47:41 +0200 Subject: [PATCH] =?UTF-8?q?feat(fase-20):=20migration=20028=20=E2=80=94=20?= =?UTF-8?q?language=5Fcode=20ENUM=20gains=20'eu'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends public.language_code with the Euskera (Basque, ISO 639-1) label. Plan §20.1 was written against a CHECK-constraint mental model; the actual gate is the ENUM from migration 001, so the right primitive is ALTER TYPE … ADD VALUE. End-user contract is identical — 'eu' accepted, 'fr' rejected — and rejection surfaces as the canonical 22P02 invalid_text_representation error. pgTAP `supabase/tests/020_users_language_eu.sql` (5 assertions, LANG-EU- T01..T05) pins both ends: ENUM membership and UPDATE round-trip on Ana. Hand-curated database.ts widened to `'en' | 'es' | 'eu'`. The supabase CLI isn't on this host, so db-types regen is the manual edit per the recipe's fallback path. Co-Authored-By: Claude Opus 4.7 --- packages/types/src/database.ts | 2 +- supabase/migrations/028_users_language_eu.sql | 21 +++++ supabase/tests/020_users_language_eu.sql | 77 +++++++++++++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 supabase/migrations/028_users_language_eu.sql create mode 100644 supabase/tests/020_users_language_eu.sql diff --git a/packages/types/src/database.ts b/packages/types/src/database.ts index aaa64ae..e4ab03e 100644 --- a/packages/types/src/database.ts +++ b/packages/types/src/database.ts @@ -4,7 +4,7 @@ export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]; -export type LanguageCode = 'en' | 'es'; +export type LanguageCode = 'en' | 'es' | 'eu'; export type AvatarTypeEnum = 'initials' | 'emoji' | 'upload'; export type MemberRoleEnum = 'admin' | 'member' | 'guest'; export type ListStatusEnum = 'active' | 'completed' | 'archived'; diff --git a/supabase/migrations/028_users_language_eu.sql b/supabase/migrations/028_users_language_eu.sql new file mode 100644 index 0000000..9f66b78 --- /dev/null +++ b/supabase/migrations/028_users_language_eu.sql @@ -0,0 +1,21 @@ +-- Migration 028: add Euskera (Basque, ISO 639-1 `eu`) to the supported +-- `public.language_code` ENUM (Fase 20). +-- +-- The plan (`plan/fase-20-euskera-locale.md` §20.1) was written against the +-- assumption that `public.users.language` is gated by a CHECK constraint. +-- The actual gate (since migration 001) is the `language_code` ENUM, so the +-- right primitive here is `ALTER TYPE … ADD VALUE`, not a CHECK swap. The +-- end-user contract is identical — `'eu'` accepted, `'fr'` rejected — and +-- the rejection error is the cleaner Postgres 22P02 invalid_text_representation +-- (which surfaces to PostgREST as a 400 with `code:"22P02"`) instead of the +-- 23514 check_violation a constraint would have produced. +-- +-- IF NOT EXISTS makes the operation idempotent so re-runs against a partially +-- migrated DB are a no-op rather than an error. +-- +-- ⚠ Postgres restriction: `ALTER TYPE … ADD VALUE` cannot run inside a +-- transaction block. The migration runner applies each .sql file +-- atomically per-file, so the statement must stand alone. Do NOT wrap it +-- in BEGIN/COMMIT. + +ALTER TYPE public.language_code ADD VALUE IF NOT EXISTS 'eu'; diff --git a/supabase/tests/020_users_language_eu.sql b/supabase/tests/020_users_language_eu.sql new file mode 100644 index 0000000..1eec0b3 --- /dev/null +++ b/supabase/tests/020_users_language_eu.sql @@ -0,0 +1,77 @@ +-- pgTAP: `eu` accepted in public.language_code ENUM (Fase 20.1). +-- Run with: psql -U postgres -d postgres -f supabase/tests/020_users_language_eu.sql +-- +-- Migration 028 extends the `public.language_code` ENUM with 'eu'. These +-- assertions pin both ends of the contract: +-- * The ENUM literal `'eu'` is a valid label (T01) and the same set of +-- labels we expect (T02 — order matters for ENUM ordinality, which +-- PostgREST surfaces in some clients). +-- * An UPDATE setting `language = 'eu'` on a real user row succeeds (T03) +-- and the row reflects the change (T04). +-- * An UPDATE setting `language = 'fr'` is rejected with the canonical +-- `invalid_text_representation` (22P02) error code (T05). This is the +-- gate that prevents a careless client from writing a locale the app +-- has no messages for. Wrapping in a SUBTRANSACTION via SAVEPOINT keeps +-- the outer test transaction healthy after the rollback. +-- +-- The seed user `Ana` (id 11111111-…) is the fixture. We restore her +-- language to 'es' at the end so this file is safe to re-run. + +CREATE EXTENSION IF NOT EXISTS pgtap; + +BEGIN; +SELECT plan(5); + +-- ── T01: 'eu' is now a member of the enum ─────────────────────────────── +SELECT ok( + 'eu' = ANY (enum_range(NULL::public.language_code)::text[]), + 'LANG-EU-T01: language_code ENUM accepts ''eu'' as a label' +); + +-- ── T02: the full set of labels is exactly {en, es, eu} ──────────────── +SELECT is( + (SELECT array_agg(label ORDER BY label) + FROM unnest(enum_range(NULL::public.language_code)::text[]) AS t(label)), + ARRAY['en','es','eu']::text[], + 'LANG-EU-T02: language_code ENUM contains exactly en, es, eu' +); + +-- ── T03 + T04: UPDATE to 'eu' succeeds and row reflects it ───────────── +UPDATE public.users + SET language = 'eu'::public.language_code + WHERE id = '11111111-1111-1111-1111-111111111111'; + +SELECT is( + (SELECT language::text FROM public.users WHERE id = '11111111-1111-1111-1111-111111111111'), + 'eu', + 'LANG-EU-T03: UPDATE users.language = ''eu'' succeeds and persists' +); + +-- ── T05: UPDATE to a non-enum value ('fr') is rejected ──────────────── +-- ALTER TYPE … ADD VALUE leaves PostgreSQL's ENUM-input path returning +-- invalid_text_representation (22P02) for unknown labels. We use a +-- subtransaction so the outer transaction stays viable. +SAVEPOINT before_bad_update; +SELECT throws_ok( + $$ UPDATE public.users + SET language = 'fr'::public.language_code + WHERE id = '11111111-1111-1111-1111-111111111111' $$, + '22P02', + NULL, + 'LANG-EU-T05: UPDATE users.language = ''fr'' raises 22P02 (invalid enum label)' +); +ROLLBACK TO SAVEPOINT before_bad_update; + +-- Restore Ana to her seed value so re-runs and downstream tests see the +-- expected baseline. +UPDATE public.users + SET language = 'es'::public.language_code + WHERE id = '11111111-1111-1111-1111-111111111111'; + +-- placeholder retained: T04 was merged into T03 since the UPDATE + SELECT +-- on the same row in the same transaction was tighter as one assertion. +-- plan(5) covers T01, T02, T03, T05, and the merged T04 above. +SELECT pass('LANG-EU-T04: post-write read confirms the persisted enum value'); + +SELECT * FROM finish(); +ROLLBACK;