Files
LivingDexTracker/supabase/migrations/20260116210000_normalize_csv_schema.sql
T
2026-01-16 21:11:21 +00:00

213 lines
7.2 KiB
SQL

-- Normalize reference data schema to match data/csvs/*.csv
--
-- Target tables (reference data):
-- - regions
-- - games (add regionId FK, keep existing columns)
-- - pokemon (1 row per pokedexNumber + form, matching data/csvs/pokemon.csv)
-- - pokemon_origin_games (join table)
-- - region_game_mappings (join table)
--
-- Also migrates user data table catch_records to reference pokemon instead of pokedex_entries.
-- 1) Drop tables that are being replaced
--
-- NOTE: We intentionally keep auth.users and user-owned pokedexes.
-- Any existing catch_records will be wiped because the foreign key target changes.
TRUNCATE TABLE catch_records;
-- Drop old mapping table (depends on pokedex_entries)
DROP TABLE IF EXISTS pokedex_entries_mapping;
-- Drop old regional dex table (per approved plan)
DROP TABLE IF EXISTS regional_dex_numbers;
-- Drop old region-game mappings (string-based)
DROP TABLE IF EXISTS region_game_mappings;
-- Drop old pokedex entries table (legacy, replaced by pokemon)
DROP TABLE IF EXISTS pokedex_entries;
-- 2) Regions
CREATE TABLE IF NOT EXISTS regions (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
"createdAt" TIMESTAMPTZ DEFAULT NOW(),
"updatedAt" TIMESTAMPTZ DEFAULT NOW()
);
ALTER TABLE regions ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Regions are publicly readable" ON regions
FOR SELECT TO public USING (true);
CREATE TRIGGER update_regions_updated_at
BEFORE UPDATE ON regions
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- 3) Games: keep existing id + displayName, add regionId FK
ALTER TABLE games
ADD COLUMN IF NOT EXISTS "regionId" BIGINT;
ALTER TABLE games
ADD CONSTRAINT games_region_id_fkey
FOREIGN KEY ("regionId") REFERENCES regions(id)
ON DELETE RESTRICT;
CREATE INDEX IF NOT EXISTS idx_games_region_id ON games("regionId");
-- 4) Region ↔ Game mapping (normalized)
CREATE TABLE IF NOT EXISTS region_game_mappings (
"regionId" BIGINT NOT NULL REFERENCES regions(id) ON DELETE CASCADE,
"gameId" TEXT NOT NULL REFERENCES games(id) ON DELETE CASCADE,
PRIMARY KEY ("regionId", "gameId")
);
ALTER TABLE region_game_mappings ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Region-game mappings are publicly readable" ON region_game_mappings
FOR SELECT TO public USING (true);
CREATE INDEX IF NOT EXISTS idx_region_game_mappings_game_id ON region_game_mappings("gameId");
-- 5) Pokemon (1 row per pokedexNumber + form)
CREATE TABLE IF NOT EXISTS pokemon (
id BIGSERIAL PRIMARY KEY,
"pokedexNumber" INTEGER NOT NULL,
pokemon TEXT NOT NULL,
form TEXT,
"originRegionId" BIGINT NOT NULL REFERENCES regions(id) ON DELETE RESTRICT,
"canGigantamax" BOOLEAN DEFAULT FALSE,
"createdAt" TIMESTAMPTZ DEFAULT NOW(),
"updatedAt" TIMESTAMPTZ DEFAULT NOW()
);
-- Uniqueness: exactly one "base" (form IS NULL) per pokedex number
CREATE UNIQUE INDEX IF NOT EXISTS uniq_pokemon_base_form
ON pokemon("pokedexNumber")
WHERE form IS NULL;
-- Uniqueness: for non-null forms, unique per (pokedexNumber, form)
CREATE UNIQUE INDEX IF NOT EXISTS uniq_pokemon_number_form
ON pokemon("pokedexNumber", form)
WHERE form IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_pokemon_pokedex_number ON pokemon("pokedexNumber");
CREATE INDEX IF NOT EXISTS idx_pokemon_name ON pokemon(pokemon);
CREATE INDEX IF NOT EXISTS idx_pokemon_origin_region_id ON pokemon("originRegionId");
ALTER TABLE pokemon ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Pokemon are publicly readable" ON pokemon
FOR SELECT TO public USING (true);
CREATE TRIGGER update_pokemon_updated_at
BEFORE UPDATE ON pokemon
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- 6) Pokemon ↔ origin games (normalized)
CREATE TABLE IF NOT EXISTS pokemon_origin_games (
"pokemonId" BIGINT NOT NULL REFERENCES pokemon(id) ON DELETE CASCADE,
"gameId" TEXT NOT NULL REFERENCES games(id) ON DELETE CASCADE,
PRIMARY KEY ("pokemonId", "gameId")
);
ALTER TABLE pokemon_origin_games ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Pokemon origin games are publicly readable" ON pokemon_origin_games
FOR SELECT TO public USING (true);
CREATE INDEX IF NOT EXISTS idx_pokemon_origin_games_game_id ON pokemon_origin_games("gameId");
-- 6.1) Compatibility view: keep the app working while refactoring
--
-- This view matches the old pokedex_entries shape used by the app:
-- - id (BIGINT)
-- - "pokedexNumber"
-- - pokemon
-- - form
-- - "canGigantamax"
-- - "regionToCatchIn" (region name)
-- - "gamesToCatchIn" (TEXT[] of game display names)
-- - "createdAt" / "updatedAt"
--
-- Note: read-only. Inserts/updates should target the new tables.
CREATE OR REPLACE VIEW pokedex_entries AS
SELECT
p.id,
p."pokedexNumber",
p.pokemon,
p.form,
p."canGigantamax",
r.name AS "regionToCatchIn",
COALESCE(
ARRAY_AGG(g."displayName" ORDER BY g."displayName") FILTER (WHERE g.id IS NOT NULL),
ARRAY[]::TEXT[]
) AS "gamesToCatchIn",
p."createdAt",
p."updatedAt"
FROM pokemon p
JOIN regions r ON r.id = p."originRegionId"
LEFT JOIN pokemon_origin_games pog ON pog."pokemonId" = p.id
LEFT JOIN games g ON g.id = pog."gameId"
GROUP BY p.id, r.name;
-- RLS: views respect underlying table policies; keep it explicitly public readable
GRANT SELECT ON pokedex_entries TO anon, authenticated;
-- 7) Recreate pokedex mapping table for expected entries
-- (same intent as old pokedex_entries_mapping, but points at pokemon.id)
CREATE TABLE IF NOT EXISTS pokedex_pokemon_mapping (
id BIGSERIAL PRIMARY KEY,
"pokedexId" UUID NOT NULL REFERENCES pokedexes(id) ON DELETE CASCADE,
"pokemonId" BIGINT NOT NULL REFERENCES pokemon(id) ON DELETE CASCADE,
UNIQUE("pokedexId", "pokemonId")
);
CREATE INDEX IF NOT EXISTS idx_pokedex_pokemon_mapping_pokedex_id
ON pokedex_pokemon_mapping("pokedexId");
CREATE INDEX IF NOT EXISTS idx_pokedex_pokemon_mapping_pokemon_id
ON pokedex_pokemon_mapping("pokemonId");
ALTER TABLE pokedex_pokemon_mapping ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Anyone can view pokedex pokemon mapping"
ON pokedex_pokemon_mapping
FOR SELECT USING (true);
CREATE POLICY "Users can insert own pokedex pokemon mappings"
ON pokedex_pokemon_mapping
FOR INSERT WITH CHECK (
EXISTS (
SELECT 1 FROM pokedexes
WHERE pokedexes.id = pokedex_pokemon_mapping."pokedexId"
AND pokedexes."userId" = auth.uid()
)
);
CREATE POLICY "Users can delete own pokedex pokemon mappings"
ON pokedex_pokemon_mapping
FOR DELETE USING (
EXISTS (
SELECT 1 FROM pokedexes
WHERE pokedexes.id = pokedex_pokemon_mapping."pokedexId"
AND pokedexes."userId" = auth.uid()
)
);
-- 8) Update catch_records to reference pokemon instead of pokedex_entries
ALTER TABLE catch_records
DROP COLUMN IF EXISTS "pokedexEntryId";
ALTER TABLE catch_records
ADD COLUMN IF NOT EXISTS "pokemonId" BIGINT REFERENCES pokemon(id) ON DELETE CASCADE;
ALTER TABLE catch_records
ALTER COLUMN "pokemonId" SET NOT NULL;
-- Drop old unique constraint (name may vary across environments)
ALTER TABLE catch_records
DROP CONSTRAINT IF EXISTS catch_records_user_pokemon_pokedex_unique;
ALTER TABLE catch_records
ADD CONSTRAINT catch_records_user_pokemon_pokedex_unique
UNIQUE("userId", "pokemonId", "pokedexId");
CREATE INDEX IF NOT EXISTS idx_catch_records_pokemon_id ON catch_records("pokemonId");