mirror of
https://github.com/jcreek/LivingDexTracker.git
synced 2026-07-12 18:43:45 +00:00
529 lines
16 KiB
PL/PgSQL
529 lines
16 KiB
PL/PgSQL
-- Baseline schema for reference data + user data.
|
|
-- Replaces legacy migrations; safe to reapply on a fresh database.
|
|
|
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
|
|
|
-- Function to automatically update updatedAt timestamp
|
|
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW."updatedAt" = NOW();
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Reference data: regions
|
|
CREATE TABLE regions (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
name TEXT NOT NULL UNIQUE,
|
|
"releaseOrder" INTEGER NOT NULL,
|
|
"createdAt" TIMESTAMPTZ DEFAULT NOW(),
|
|
"updatedAt" TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Reference data: games
|
|
CREATE TABLE games (
|
|
id TEXT PRIMARY KEY,
|
|
"displayName" TEXT NOT NULL UNIQUE,
|
|
"regionId" BIGINT NOT NULL REFERENCES regions(id) ON DELETE RESTRICT,
|
|
region TEXT NOT NULL,
|
|
"releaseYear" INTEGER NOT NULL,
|
|
"createdAt" TIMESTAMPTZ DEFAULT NOW(),
|
|
"updatedAt" TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_games_region_id ON games("regionId");
|
|
CREATE INDEX idx_games_release_year ON games("releaseYear");
|
|
|
|
-- Reference data: game dexes (base, DLCs, islands, etc.)
|
|
CREATE TABLE game_dexes (
|
|
id TEXT PRIMARY KEY,
|
|
"gameId" TEXT NOT NULL REFERENCES games(id) ON DELETE CASCADE,
|
|
"displayName" TEXT NOT NULL,
|
|
"sortOrder" INTEGER NOT NULL,
|
|
"isDlc" BOOLEAN DEFAULT FALSE,
|
|
"parentDexId" TEXT REFERENCES game_dexes(id) ON DELETE SET NULL,
|
|
"createdAt" TIMESTAMPTZ DEFAULT NOW(),
|
|
"updatedAt" TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE ("gameId", "displayName")
|
|
);
|
|
|
|
CREATE INDEX idx_game_dexes_game_id ON game_dexes("gameId");
|
|
CREATE INDEX idx_game_dexes_sort_order ON game_dexes("sortOrder");
|
|
|
|
-- Reference data: pokemon (one row per pokedexNumber + form)
|
|
CREATE TABLE pokemon (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
"pokedexNumber" INTEGER NOT NULL,
|
|
pokemon TEXT NOT NULL,
|
|
form TEXT,
|
|
"spriteKey" TEXT,
|
|
"originRegionId" BIGINT NOT NULL REFERENCES regions(id) ON DELETE RESTRICT,
|
|
"canGigantamax" BOOLEAN DEFAULT FALSE,
|
|
"regionToEvolveIn" TEXT,
|
|
"evolutionInformation" TEXT,
|
|
"catchInformation" TEXT[],
|
|
notes TEXT,
|
|
"createdAt" TIMESTAMPTZ DEFAULT NOW(),
|
|
"updatedAt" TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
CREATE UNIQUE INDEX uniq_pokemon_base_form
|
|
ON pokemon("pokedexNumber")
|
|
WHERE form IS NULL;
|
|
|
|
CREATE UNIQUE INDEX uniq_pokemon_number_form
|
|
ON pokemon("pokedexNumber", form)
|
|
WHERE form IS NOT NULL;
|
|
|
|
CREATE INDEX idx_pokemon_pokedex_number ON pokemon("pokedexNumber");
|
|
CREATE INDEX idx_pokemon_name ON pokemon(pokemon);
|
|
CREATE INDEX idx_pokemon_origin_region_id ON pokemon("originRegionId");
|
|
|
|
-- Reference data: pokemon origin games (many-to-many)
|
|
CREATE TABLE 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")
|
|
);
|
|
|
|
CREATE INDEX idx_pokemon_origin_games_game_id ON pokemon_origin_games("gameId");
|
|
|
|
-- Reference data: per-game pokedex entries (seeded from per-game CSVs)
|
|
CREATE TABLE game_pokedex_entries (
|
|
"dexId" TEXT NOT NULL REFERENCES game_dexes(id) ON DELETE CASCADE,
|
|
"pokemonId" BIGINT NOT NULL REFERENCES pokemon(id) ON DELETE CASCADE,
|
|
"dexNumber" INTEGER,
|
|
notes TEXT,
|
|
PRIMARY KEY ("dexId", "pokemonId")
|
|
);
|
|
|
|
CREATE INDEX idx_game_pokedex_entries_dex_id ON game_pokedex_entries("dexId");
|
|
CREATE INDEX idx_game_pokedex_entries_pokemon_id ON game_pokedex_entries("pokemonId");
|
|
|
|
-- User-owned pokedexes
|
|
CREATE TABLE pokedexes (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
"userId" UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
|
|
-- Type flags (combinable)
|
|
"isLivingDex" BOOLEAN DEFAULT FALSE,
|
|
"isShinyDex" BOOLEAN DEFAULT FALSE,
|
|
"isOriginDex" BOOLEAN DEFAULT FALSE,
|
|
"isFormDex" BOOLEAN DEFAULT FALSE,
|
|
|
|
-- Scope (NULL = all games, or specific game displayName)
|
|
"gameScope" TEXT,
|
|
|
|
"createdAt" TIMESTAMPTZ DEFAULT NOW(),
|
|
"updatedAt" TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
UNIQUE("userId", name)
|
|
);
|
|
|
|
CREATE INDEX idx_pokedexes_user_id ON pokedexes("userId");
|
|
|
|
-- Selected dex scopes per user pokedex
|
|
CREATE TABLE pokedex_dex_scopes (
|
|
"pokedexId" UUID NOT NULL REFERENCES pokedexes(id) ON DELETE CASCADE,
|
|
"dexId" TEXT NOT NULL REFERENCES game_dexes(id) ON DELETE CASCADE,
|
|
PRIMARY KEY ("pokedexId", "dexId")
|
|
);
|
|
|
|
CREATE INDEX idx_pokedex_dex_scopes_pokedex_id ON pokedex_dex_scopes("pokedexId");
|
|
CREATE INDEX idx_pokedex_dex_scopes_dex_id ON pokedex_dex_scopes("dexId");
|
|
|
|
-- User-owned expected entries per pokedex
|
|
CREATE TABLE 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 idx_pokedex_pokemon_mapping_pokedex_id
|
|
ON pokedex_pokemon_mapping("pokedexId");
|
|
CREATE INDEX idx_pokedex_pokemon_mapping_pokemon_id
|
|
ON pokedex_pokemon_mapping("pokemonId");
|
|
|
|
-- User-owned catch records
|
|
CREATE TABLE catch_records (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
"userId" UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
"pokemonId" BIGINT NOT NULL REFERENCES pokemon(id) ON DELETE CASCADE,
|
|
"pokedexId" UUID NOT NULL REFERENCES pokedexes(id) ON DELETE CASCADE,
|
|
|
|
-- Catch status fields
|
|
"haveToEvolve" BOOLEAN DEFAULT FALSE,
|
|
caught BOOLEAN DEFAULT FALSE,
|
|
"inHome" BOOLEAN DEFAULT FALSE,
|
|
"hasGigantamaxed" BOOLEAN DEFAULT FALSE,
|
|
"personalNotes" TEXT,
|
|
|
|
"createdAt" TIMESTAMPTZ DEFAULT NOW(),
|
|
"updatedAt" TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
UNIQUE("userId", "pokemonId", "pokedexId")
|
|
);
|
|
|
|
CREATE INDEX idx_catch_records_user_id ON catch_records("userId");
|
|
CREATE INDEX idx_catch_records_pokemon_id ON catch_records("pokemonId");
|
|
CREATE INDEX idx_catch_records_pokedex_id ON catch_records("pokedexId");
|
|
CREATE INDEX idx_catch_records_caught ON catch_records(caught);
|
|
CREATE INDEX idx_catch_records_user_caught ON catch_records("userId", caught);
|
|
|
|
-- Cached public stats
|
|
CREATE TABLE stats_cache (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
pokemon_caught BIGINT NOT NULL DEFAULT 0,
|
|
total_users BIGINT NOT NULL DEFAULT 0,
|
|
completed_pokedexes BIGINT NOT NULL DEFAULT 0,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_stats_cache_updated_at ON stats_cache(updated_at DESC);
|
|
|
|
-- Triggers for updatedAt
|
|
CREATE TRIGGER update_regions_updated_at
|
|
BEFORE UPDATE ON regions
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
CREATE TRIGGER update_games_updated_at
|
|
BEFORE UPDATE ON games
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
CREATE TRIGGER update_game_dexes_updated_at
|
|
BEFORE UPDATE ON game_dexes
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
CREATE TRIGGER update_pokemon_updated_at
|
|
BEFORE UPDATE ON pokemon
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
CREATE TRIGGER update_pokedexes_updated_at
|
|
BEFORE UPDATE ON pokedexes
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
CREATE TRIGGER update_catch_records_updated_at
|
|
BEFORE UPDATE ON catch_records
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
-- RLS policies
|
|
ALTER TABLE regions ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "Regions are publicly readable" ON regions
|
|
FOR SELECT TO public USING (true);
|
|
|
|
ALTER TABLE games ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "Games are publicly readable" ON games
|
|
FOR SELECT TO public USING (true);
|
|
|
|
ALTER TABLE game_dexes ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "Game dexes are publicly readable" ON game_dexes
|
|
FOR SELECT TO public USING (true);
|
|
|
|
ALTER TABLE pokemon ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "Pokemon are publicly readable" ON pokemon
|
|
FOR SELECT TO public USING (true);
|
|
|
|
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);
|
|
|
|
ALTER TABLE game_pokedex_entries ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "Game pokedex entries are publicly readable" ON game_pokedex_entries
|
|
FOR SELECT TO public USING (true);
|
|
|
|
ALTER TABLE pokedexes ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "Users can view own pokedexes" ON pokedexes
|
|
FOR SELECT USING (auth.uid() = "userId");
|
|
CREATE POLICY "Users can create own pokedexes" ON pokedexes
|
|
FOR INSERT WITH CHECK (auth.uid() = "userId");
|
|
CREATE POLICY "Users can update own pokedexes" ON pokedexes
|
|
FOR UPDATE USING (auth.uid() = "userId");
|
|
CREATE POLICY "Users can delete own pokedexes" ON pokedexes
|
|
FOR DELETE USING (auth.uid() = "userId");
|
|
|
|
ALTER TABLE pokedex_dex_scopes ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "Users can view own dex scopes" ON pokedex_dex_scopes
|
|
FOR SELECT USING (
|
|
EXISTS (
|
|
SELECT 1 FROM pokedexes
|
|
WHERE pokedexes.id = pokedex_dex_scopes."pokedexId"
|
|
AND pokedexes."userId" = auth.uid()
|
|
)
|
|
);
|
|
CREATE POLICY "Users can manage own dex scopes" ON pokedex_dex_scopes
|
|
FOR ALL USING (
|
|
EXISTS (
|
|
SELECT 1 FROM pokedexes
|
|
WHERE pokedexes.id = pokedex_dex_scopes."pokedexId"
|
|
AND pokedexes."userId" = auth.uid()
|
|
)
|
|
);
|
|
|
|
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()
|
|
)
|
|
);
|
|
|
|
ALTER TABLE catch_records ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "Users can view own catch records" ON catch_records
|
|
FOR SELECT USING (
|
|
auth.uid() = "userId" AND
|
|
EXISTS (
|
|
SELECT 1 FROM pokedexes
|
|
WHERE pokedexes.id = catch_records."pokedexId"
|
|
AND pokedexes."userId" = auth.uid()
|
|
)
|
|
);
|
|
CREATE POLICY "Users can insert own catch records" ON catch_records
|
|
FOR INSERT WITH CHECK (
|
|
auth.uid() = "userId" AND
|
|
EXISTS (
|
|
SELECT 1 FROM pokedexes
|
|
WHERE pokedexes.id = catch_records."pokedexId"
|
|
AND pokedexes."userId" = auth.uid()
|
|
)
|
|
);
|
|
CREATE POLICY "Users can update own catch records" ON catch_records
|
|
FOR UPDATE USING (
|
|
auth.uid() = "userId" AND
|
|
EXISTS (
|
|
SELECT 1 FROM pokedexes
|
|
WHERE pokedexes.id = catch_records."pokedexId"
|
|
AND pokedexes."userId" = auth.uid()
|
|
)
|
|
);
|
|
CREATE POLICY "Users can delete own catch records" ON catch_records
|
|
FOR DELETE USING (
|
|
auth.uid() = "userId" AND
|
|
EXISTS (
|
|
SELECT 1 FROM pokedexes
|
|
WHERE pokedexes.id = catch_records."pokedexId"
|
|
AND pokedexes."userId" = auth.uid()
|
|
)
|
|
);
|
|
|
|
ALTER TABLE stats_cache ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "Anyone can view stats cache" ON stats_cache
|
|
FOR SELECT USING (true);
|
|
|
|
-- Compatibility view for the app (pokedex_entries)
|
|
CREATE OR REPLACE VIEW pokedex_entries AS
|
|
SELECT
|
|
p.id,
|
|
p."pokedexNumber",
|
|
p.pokemon,
|
|
p.form,
|
|
p."spriteKey",
|
|
p."canGigantamax",
|
|
r.name AS "regionToCatchIn",
|
|
r."releaseOrder" AS "regionReleaseOrder",
|
|
COALESCE(
|
|
ARRAY_AGG(g."displayName" ORDER BY g."displayName") FILTER (WHERE g.id IS NOT NULL),
|
|
ARRAY[]::TEXT[]
|
|
) AS "gamesToCatchIn",
|
|
p."regionToEvolveIn",
|
|
p."evolutionInformation",
|
|
p."catchInformation",
|
|
p."createdAt",
|
|
p."updatedAt",
|
|
CASE
|
|
WHEN p.form IS NULL OR lower(p.form) = 'male' THEN 0
|
|
WHEN lower(p.form) = 'female' THEN 1
|
|
WHEN lower(p.form) LIKE '%alolan%'
|
|
OR lower(p.form) LIKE '%galarian%'
|
|
OR lower(p.form) LIKE '%hisuian%'
|
|
OR lower(p.form) LIKE '%paldean%'
|
|
THEN 2
|
|
ELSE 3
|
|
END AS "formSortBucket",
|
|
CASE
|
|
WHEN lower(p.form) LIKE '%alolan%'
|
|
OR lower(p.form) LIKE '%galarian%'
|
|
OR lower(p.form) LIKE '%hisuian%'
|
|
OR lower(p.form) LIKE '%paldean%'
|
|
THEN r."releaseOrder"
|
|
ELSE 0
|
|
END AS "formSortRegionOrder",
|
|
CASE
|
|
WHEN lower(p.form) LIKE 'female-%' THEN 1
|
|
ELSE 0
|
|
END AS "formSortRegionalSub",
|
|
COALESCE(lower(p.form), '') AS "formSortLabel",
|
|
CASE
|
|
WHEN p.pokemon = 'Unown' THEN
|
|
CASE
|
|
WHEN p.form = '?' THEN 26
|
|
WHEN p.form = '!' THEN 27
|
|
WHEN length(p.form) = 1 AND ascii(upper(p.form)) BETWEEN 65 AND 90 THEN ascii(upper(p.form)) - 65
|
|
ELSE 28
|
|
END
|
|
ELSE 0
|
|
END AS "unownSortOrder"
|
|
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, r."releaseOrder";
|
|
|
|
GRANT SELECT ON pokedex_entries TO anon, authenticated;
|
|
|
|
-- Dex-scoped view for ordering and filtering by native dexes
|
|
CREATE OR REPLACE VIEW game_pokedex_entry_details AS
|
|
SELECT
|
|
gpe."dexId",
|
|
gd."displayName" AS "dexDisplayName",
|
|
gd."sortOrder" AS "dexSortOrder",
|
|
gpe."dexNumber",
|
|
pe.*
|
|
FROM game_pokedex_entries gpe
|
|
JOIN game_dexes gd ON gd.id = gpe."dexId"
|
|
JOIN pokedex_entries pe ON pe.id = gpe."pokemonId";
|
|
|
|
GRANT SELECT ON game_pokedex_entry_details TO anon, authenticated;
|
|
|
|
-- RPC: recalculate expected mappings for a pokedex
|
|
CREATE OR REPLACE FUNCTION recalculate_pokedex_mappings(
|
|
p_pokedex_id UUID,
|
|
p_entry_ids BIGINT[]
|
|
)
|
|
RETURNS VOID
|
|
SECURITY DEFINER
|
|
SET search_path = public, pg_temp
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
DECLARE
|
|
v_user_id UUID;
|
|
BEGIN
|
|
IF p_entry_ids IS NULL OR array_length(p_entry_ids, 1) IS NULL THEN
|
|
RAISE EXCEPTION 'entry_ids cannot be null or empty';
|
|
END IF;
|
|
|
|
SELECT "userId" INTO v_user_id
|
|
FROM pokedexes
|
|
WHERE id = p_pokedex_id;
|
|
|
|
IF v_user_id IS NULL THEN
|
|
RAISE EXCEPTION 'pokedex not found';
|
|
END IF;
|
|
|
|
IF auth.uid() IS NULL OR v_user_id IS DISTINCT FROM auth.uid() THEN
|
|
RAISE EXCEPTION 'not authorized: you do not own this pokedex';
|
|
END IF;
|
|
|
|
DELETE FROM pokedex_pokemon_mapping
|
|
WHERE "pokedexId" = p_pokedex_id;
|
|
|
|
INSERT INTO pokedex_pokemon_mapping ("pokedexId", "pokemonId")
|
|
SELECT p_pokedex_id, unnest(p_entry_ids)
|
|
ON CONFLICT ("pokedexId", "pokemonId") DO NOTHING;
|
|
END;
|
|
$$;
|
|
|
|
-- RPC: stats cache
|
|
CREATE OR REPLACE FUNCTION update_and_get_stats()
|
|
RETURNS TABLE (
|
|
pokemon_caught BIGINT,
|
|
total_users BIGINT,
|
|
completed_pokedexes BIGINT,
|
|
updated_at TIMESTAMPTZ
|
|
)
|
|
SECURITY DEFINER
|
|
SET search_path = public, pg_temp
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
DECLARE
|
|
v_pokemon_caught BIGINT;
|
|
v_total_users BIGINT;
|
|
v_completed_pokedexes BIGINT;
|
|
v_updated_at TIMESTAMPTZ := NOW();
|
|
BEGIN
|
|
SELECT COUNT(*) INTO v_pokemon_caught
|
|
FROM catch_records
|
|
WHERE caught = true;
|
|
|
|
SELECT COUNT(*) INTO v_total_users
|
|
FROM auth.users;
|
|
|
|
SELECT COUNT(*) INTO v_completed_pokedexes
|
|
FROM (
|
|
SELECT cr."pokedexId"
|
|
FROM catch_records cr
|
|
INNER JOIN pokedex_pokemon_mapping ppm
|
|
ON cr."pokedexId" = ppm."pokedexId"
|
|
AND cr."pokemonId" = ppm."pokemonId"
|
|
GROUP BY cr."pokedexId"
|
|
HAVING COUNT(*) = COUNT(*) FILTER (WHERE cr.caught = true)
|
|
AND COUNT(*) = (
|
|
SELECT COUNT(*)
|
|
FROM pokedex_pokemon_mapping
|
|
WHERE "pokedexId" = cr."pokedexId"
|
|
)
|
|
) completed;
|
|
|
|
INSERT INTO stats_cache (id, pokemon_caught, total_users, completed_pokedexes, updated_at)
|
|
VALUES (1, v_pokemon_caught, v_total_users, v_completed_pokedexes, v_updated_at)
|
|
ON CONFLICT (id) DO UPDATE
|
|
SET pokemon_caught = EXCLUDED.pokemon_caught,
|
|
total_users = EXCLUDED.total_users,
|
|
completed_pokedexes = EXCLUDED.completed_pokedexes,
|
|
updated_at = v_updated_at;
|
|
|
|
RETURN QUERY
|
|
SELECT v_pokemon_caught, v_total_users, v_completed_pokedexes, v_updated_at AS updated_at;
|
|
END;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION get_public_stats()
|
|
RETURNS TABLE (
|
|
pokemon_caught BIGINT,
|
|
total_users BIGINT,
|
|
completed_pokedexes BIGINT,
|
|
updated_at TIMESTAMPTZ
|
|
)
|
|
SECURITY DEFINER
|
|
SET search_path = public, pg_temp
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
DECLARE
|
|
v_cache_exists BOOLEAN;
|
|
BEGIN
|
|
SELECT EXISTS(
|
|
SELECT 1 FROM stats_cache
|
|
WHERE stats_cache.updated_at > NOW() - INTERVAL '24 hours'
|
|
) INTO v_cache_exists;
|
|
|
|
IF v_cache_exists THEN
|
|
RETURN QUERY
|
|
SELECT sc.pokemon_caught, sc.total_users, sc.completed_pokedexes, sc.updated_at
|
|
FROM stats_cache sc
|
|
ORDER BY sc.updated_at DESC
|
|
LIMIT 1;
|
|
ELSE
|
|
RETURN QUERY
|
|
SELECT * FROM update_and_get_stats();
|
|
END IF;
|
|
END;
|
|
$$;
|