-- 20260502115405_share_votes.sql -- -- Per-user vote tracking for the /said share gallery. -- -- Replaces the broken counter-bump RPC (vote_card) with a proper relational -- design: one row per (share_card, user) pair, RLS-enforced ownership, -- aggregate columns on share_cards kept in sync via trigger. Voters can -- change direction or undo at any time. One vote per user per card. -- -- This migration is idempotent: re-running it is a no-op. -- --------------------------------------------------------------------------- -- Table -- --------------------------------------------------------------------------- create table if not exists public.share_votes ( share_id text not null references public.share_cards(id) on delete cascade, user_id uuid not null references auth.users(id) on delete cascade, direction text not null check (direction in ('up','down')), created_at timestamptz not null default now(), updated_at timestamptz not null default now(), primary key (share_id, user_id) ); create index if not exists share_votes_share_id_idx on public.share_votes (share_id); create index if not exists share_votes_user_id_idx on public.share_votes (user_id); -- --------------------------------------------------------------------------- -- Row Level Security -- --------------------------------------------------------------------------- alter table public.share_votes enable row level security; drop policy if exists "users can read all votes" on public.share_votes; drop policy if exists "users can insert their own votes" on public.share_votes; drop policy if exists "users can update their own votes" on public.share_votes; drop policy if exists "users can delete their own votes" on public.share_votes; create policy "users can read all votes" on public.share_votes for select using (true); create policy "users can insert their own votes" on public.share_votes for insert with check (auth.uid() = user_id); create policy "users can update their own votes" on public.share_votes for update using (auth.uid() = user_id); create policy "users can delete their own votes" on public.share_votes for delete using (auth.uid() = user_id); -- --------------------------------------------------------------------------- -- Aggregate sync trigger -- -- After every insert / update / delete on share_votes, recompute the three -- aggregate columns on the parent share_cards row. SECURITY DEFINER is -- required because the calling user only has RLS access to their own vote -- but the trigger needs to update share_cards.upvotes/downvotes/score. -- --------------------------------------------------------------------------- create or replace function public.recalc_share_card_score() returns trigger language plpgsql security definer set search_path = public as $$ declare s_id text; begin s_id := coalesce(NEW.share_id, OLD.share_id); update public.share_cards set upvotes = (select count(*) from public.share_votes where share_id = s_id and direction = 'up'), downvotes = (select count(*) from public.share_votes where share_id = s_id and direction = 'down'), score = (select count(*) filter (where direction = 'up') - count(*) filter (where direction = 'down') from public.share_votes where share_id = s_id) where id = s_id; return null; end; $$; drop trigger if exists share_votes_recalc on public.share_votes; create trigger share_votes_recalc after insert or update or delete on public.share_votes for each row execute function public.recalc_share_card_score(); -- --------------------------------------------------------------------------- -- updated_at touch trigger (only on UPDATE) -- --------------------------------------------------------------------------- create or replace function public.touch_share_votes_updated_at() returns trigger language plpgsql as $$ begin NEW.updated_at := now(); return NEW; end; $$; drop trigger if exists share_votes_touch on public.share_votes; create trigger share_votes_touch before update on public.share_votes for each row execute function public.touch_share_votes_updated_at();