mirror of
https://github.com/jcreek/LivingDexTracker.git
synced 2026-07-12 18:43:45 +00:00
213 lines
7.2 KiB
SQL
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");
|