-- Migration 006: item_frequency — per-collective item usage tracking. -- -- Write path: SECURITY DEFINER trigger only. The app role has SELECT access only. -- Names are normalized (lower + trim) before upsert so "Milk" and "milk" are the same entry. -- ── item_frequency ──────────────────────────────────────────────────────────── CREATE TABLE public.item_frequency ( collective_id uuid NOT NULL REFERENCES public.collectives(id) ON DELETE CASCADE, name text NOT NULL, -- normalized: lower(trim(original name)) use_count integer NOT NULL DEFAULT 1, last_used_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (collective_id, name) ); ALTER TABLE public.item_frequency ENABLE ROW LEVEL SECURITY; -- Index for the suggestion query: ordered by use_count descending per collective CREATE INDEX item_frequency_collective_count_idx ON public.item_frequency (collective_id, use_count DESC); -- ── RLS ─────────────────────────────────────────────────────────────────────── -- Members (any role) can read frequency data for collectives they belong to. CREATE POLICY item_frequency_select ON public.item_frequency FOR SELECT USING (public.is_member(collective_id)); -- All direct writes are blocked; the SECURITY DEFINER trigger below is the only -- write path. This prevents clients from inflating suggestion scores artificially. CREATE POLICY item_frequency_insert_deny ON public.item_frequency FOR INSERT WITH CHECK (false); CREATE POLICY item_frequency_update_deny ON public.item_frequency FOR UPDATE USING (false); CREATE POLICY item_frequency_delete_deny ON public.item_frequency FOR DELETE USING (false); -- ── Trigger: upsert frequency on shopping_items insert ─────────────────────── CREATE OR REPLACE FUNCTION public.fn_record_item_frequency() RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE v_collective_id uuid; BEGIN SELECT collective_id INTO v_collective_id FROM public.shopping_lists WHERE id = NEW.list_id; INSERT INTO public.item_frequency (collective_id, name, use_count, last_used_at) VALUES (v_collective_id, lower(trim(NEW.name)), 1, now()) ON CONFLICT (collective_id, name) DO UPDATE SET use_count = item_frequency.use_count + 1, last_used_at = now(); RETURN NEW; END; $$; CREATE TRIGGER trg_item_frequency AFTER INSERT ON public.shopping_items FOR EACH ROW EXECUTE FUNCTION public.fn_record_item_frequency();