-- Sync migration: Make idempotent for production -- Extensions create extension if not exists "hypopg" with schema "extensions"; create extension if not exists "index_advisor" with schema "extensions"; -- Drop policy if exists (will be recreated at the end) drop policy if exists "Anyone can view questions in visible packs" on "public"."questions"; -- Drop and recreate constraint (idempotent) alter table "public"."question_packs" drop constraint if exists "question_packs_category_id_fkey"; -- Note: pack_question_stats view is NOT recreated here. -- The original migration (20260111100000_add_inverse_of_to_questions.sql) creates it correctly. -- Recreating it causes persistent diff noise due to pg_dump formatting differences. -- Create tables if not exists create table if not exists "public"."pack_topics" ( "pack_id" uuid not null, "topic_id" uuid not null ); alter table "public"."pack_topics" enable row level security; create table if not exists "public"."topics" ( "id" uuid not null default gen_random_uuid(), "name" text not null, "description" text, "icon" text, "sort_order" integer default 0, "created_at" timestamp with time zone default now() ); alter table "public"."topics" enable row level security; -- Add columns if not exists alter table "public"."admin_users" add column if not exists "permissions" jsonb default '[]'::jsonb; alter table "public"."admin_users" disable row level security; alter table "public"."ai_config" add column if not exists "cherry_pick_ensure_intensity_distribution" boolean default true; alter table "public"."ai_config" add column if not exists "council_selection_mode" text default 'whole_set'::text; alter table "public"."ai_config" add column if not exists "heuristic_keyword_triggers" text; alter table "public"."ai_config" add column if not exists "heuristic_min_text_length" integer not null default 12; alter table "public"."ai_config" add column if not exists "heuristic_record_reason" boolean not null default false; alter table "public"."ai_config" add column if not exists "heuristic_skip_if_no_alnum" boolean not null default true; alter table "public"."ai_config" add column if not exists "heuristic_skip_media_without_text" boolean not null default false; alter table "public"."ai_config" add column if not exists "heuristic_use_default_keywords" boolean not null default true; alter table "public"."ai_config" add column if not exists "heuristic_use_default_whitelist" boolean not null default true; alter table "public"."ai_config" add column if not exists "heuristic_whitelist" text; alter table "public"."ai_config" add column if not exists "heuristic_whitelist_max_length" integer not null default 30; alter table "public"."ai_config" add column if not exists "heuristics_enabled" boolean not null default false; -- Create indexes if not exists CREATE INDEX IF NOT EXISTS idx_pack_topics_pack_id ON public.pack_topics USING btree (pack_id); CREATE UNIQUE INDEX IF NOT EXISTS pack_topics_pkey ON public.pack_topics USING btree (pack_id, topic_id); CREATE UNIQUE INDEX IF NOT EXISTS topics_name_key ON public.topics USING btree (name); CREATE UNIQUE INDEX IF NOT EXISTS topics_pkey ON public.topics USING btree (id); -- Add constraints if not exists (check existence first) DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'pack_topics_pkey') THEN ALTER TABLE "public"."pack_topics" ADD CONSTRAINT "pack_topics_pkey" PRIMARY KEY USING INDEX "pack_topics_pkey"; END IF; END $$; DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'topics_pkey') THEN ALTER TABLE "public"."topics" ADD CONSTRAINT "topics_pkey" PRIMARY KEY USING INDEX "topics_pkey"; END IF; END $$; DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'ai_config_council_selection_mode_check') THEN ALTER TABLE "public"."ai_config" ADD CONSTRAINT "ai_config_council_selection_mode_check" CHECK ((council_selection_mode = ANY (ARRAY['whole_set'::text, 'cherry_pick'::text]))); END IF; END $$; DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'pack_topics_pack_id_fkey') THEN ALTER TABLE "public"."pack_topics" ADD CONSTRAINT "pack_topics_pack_id_fkey" FOREIGN KEY (pack_id) REFERENCES public.question_packs(id) ON DELETE CASCADE; END IF; END $$; DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'pack_topics_topic_id_fkey') THEN ALTER TABLE "public"."pack_topics" ADD CONSTRAINT "pack_topics_topic_id_fkey" FOREIGN KEY (topic_id) REFERENCES public.topics(id) ON DELETE CASCADE; END IF; END $$; DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'topics_name_key') THEN ALTER TABLE "public"."topics" ADD CONSTRAINT "topics_name_key" UNIQUE USING INDEX "topics_name_key"; END IF; END $$; DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'question_packs_category_id_fkey') THEN ALTER TABLE "public"."question_packs" ADD CONSTRAINT "question_packs_category_id_fkey" FOREIGN KEY (category_id) REFERENCES public.categories(id) ON DELETE CASCADE; END IF; END $$; set check_function_bodies = off; CREATE OR REPLACE FUNCTION public.get_profiles_with_auth_info() RETURNS TABLE(id uuid, name text, email text, avatar_url text, is_premium boolean, couple_id uuid, created_at timestamp with time zone, last_sign_in_at timestamp with time zone, email_confirmed_at timestamp with time zone) LANGUAGE sql SECURITY DEFINER SET search_path TO 'public' AS $function$ SELECT p.id, COALESCE(p.name, au.raw_user_meta_data->>'name') as name, COALESCE(p.email, au.email) as email, p.avatar_url, p.is_premium, p.couple_id, p.created_at, au.last_sign_in_at, au.email_confirmed_at FROM profiles p LEFT JOIN auth.users au ON au.id = p.id ORDER BY p.created_at DESC; $function$ ; CREATE OR REPLACE FUNCTION public.get_user_media_files(user_id uuid) RETURNS TABLE(id uuid, name text, bucket_id text, created_at timestamp with time zone, metadata jsonb) LANGUAGE sql SECURITY DEFINER AS $function$ SELECT id, name, bucket_id, created_at, metadata FROM storage.objects WHERE bucket_id = 'chat-media' AND owner = user_id ORDER BY created_at DESC; $function$ ; CREATE OR REPLACE FUNCTION public.get_user_storage_usage() RETURNS TABLE(owner uuid, total_bytes bigint) LANGUAGE sql SECURITY DEFINER AS $function$ SELECT owner, SUM((metadata->>'size')::bigint) as total_bytes FROM storage.objects WHERE bucket_id = 'chat-media' AND owner IS NOT NULL GROUP BY owner; $function$ ; CREATE OR REPLACE FUNCTION public.cleanup_expired_videos() RETURNS TABLE(deleted_count integer, deleted_paths text[]) LANGUAGE plpgsql SECURITY DEFINER AS $function$ DECLARE expired_paths text[]; count int; BEGIN SELECT array_agg(media_path) INTO expired_paths FROM messages WHERE media_type = 'video' AND media_expires_at IS NOT NULL AND media_expires_at < NOW() AND media_expired = false AND media_path IS NOT NULL; WITH updated AS ( UPDATE messages SET media_expired = true, media_path = NULL WHERE media_type = 'video' AND media_expires_at IS NOT NULL AND media_expires_at < NOW() AND media_expired = false AND media_path IS NOT NULL RETURNING 1 ) SELECT count(*) INTO count FROM updated; RETURN QUERY SELECT count, COALESCE(expired_paths, ARRAY[]::text[]); END; $function$ ; CREATE OR REPLACE FUNCTION public.get_pack_unique_question_count(pack_uuid uuid) RETURNS integer LANGUAGE sql STABLE AS $function$ SELECT COUNT(*)::INTEGER FROM questions WHERE pack_id = pack_uuid AND inverse_of IS NULL AND deleted_at IS NULL; $function$ ; CREATE OR REPLACE FUNCTION public.handle_new_user() RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER AS $function$ BEGIN INSERT INTO public.profiles (id, created_at, updated_at, onboarding_completed) VALUES (NEW.id, NOW(), NOW(), false); RETURN NEW; END; $function$ ; CREATE OR REPLACE FUNCTION public.notify_new_message() RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $function$ BEGIN PERFORM net.http_post( url := 'https://ckjcrkjpmhqhiucifukx.supabase.co/functions/v1/send-message-notification', headers := jsonb_build_object( 'Content-Type', 'application/json' ), body := jsonb_build_object( 'match_id', NEW.match_id, 'sender_id', NEW.user_id ) ); RETURN NEW; END; $function$ ; -- pack_question_stats view creation removed - see comment at top of file grant delete on table "public"."pack_topics" to "anon"; grant insert on table "public"."pack_topics" to "anon"; grant references on table "public"."pack_topics" to "anon"; grant select on table "public"."pack_topics" to "anon"; grant trigger on table "public"."pack_topics" to "anon"; grant truncate on table "public"."pack_topics" to "anon"; grant update on table "public"."pack_topics" to "anon"; grant delete on table "public"."pack_topics" to "authenticated"; grant insert on table "public"."pack_topics" to "authenticated"; grant references on table "public"."pack_topics" to "authenticated"; grant select on table "public"."pack_topics" to "authenticated"; grant trigger on table "public"."pack_topics" to "authenticated"; grant truncate on table "public"."pack_topics" to "authenticated"; grant update on table "public"."pack_topics" to "authenticated"; grant delete on table "public"."pack_topics" to "service_role"; grant insert on table "public"."pack_topics" to "service_role"; grant references on table "public"."pack_topics" to "service_role"; grant select on table "public"."pack_topics" to "service_role"; grant trigger on table "public"."pack_topics" to "service_role"; grant truncate on table "public"."pack_topics" to "service_role"; grant update on table "public"."pack_topics" to "service_role"; grant delete on table "public"."topics" to "anon"; grant insert on table "public"."topics" to "anon"; grant references on table "public"."topics" to "anon"; grant select on table "public"."topics" to "anon"; grant trigger on table "public"."topics" to "anon"; grant truncate on table "public"."topics" to "anon"; grant update on table "public"."topics" to "anon"; grant delete on table "public"."topics" to "authenticated"; grant insert on table "public"."topics" to "authenticated"; grant references on table "public"."topics" to "authenticated"; grant select on table "public"."topics" to "authenticated"; grant trigger on table "public"."topics" to "authenticated"; grant truncate on table "public"."topics" to "authenticated"; grant update on table "public"."topics" to "authenticated"; grant delete on table "public"."topics" to "service_role"; grant insert on table "public"."topics" to "service_role"; grant references on table "public"."topics" to "service_role"; grant select on table "public"."topics" to "service_role"; grant trigger on table "public"."topics" to "service_role"; grant truncate on table "public"."topics" to "service_role"; grant update on table "public"."topics" to "service_role"; -- Policies: drop if exists then create drop policy if exists "Admins can view couple pack settings" on "public"."couple_packs"; create policy "Admins can view couple pack settings" on "public"."couple_packs" as permissive for select to public using ((public.is_super_admin() OR public.has_permission('view_users'::text))); drop policy if exists "Admins can delete pack_topics" on "public"."pack_topics"; create policy "Admins can delete pack_topics" on "public"."pack_topics" as permissive for delete to authenticated using ((EXISTS ( SELECT 1 FROM public.admin_users WHERE (admin_users.user_id = auth.uid())))); drop policy if exists "Admins can insert pack_topics" on "public"."pack_topics"; create policy "Admins can insert pack_topics" on "public"."pack_topics" as permissive for insert to authenticated with check ((EXISTS ( SELECT 1 FROM public.admin_users WHERE (admin_users.user_id = auth.uid())))); drop policy if exists "Admins can update pack_topics" on "public"."pack_topics"; create policy "Admins can update pack_topics" on "public"."pack_topics" as permissive for update to authenticated using ((EXISTS ( SELECT 1 FROM public.admin_users WHERE (admin_users.user_id = auth.uid())))); drop policy if exists "Pack topics are viewable by authenticated users" on "public"."pack_topics"; create policy "Pack topics are viewable by authenticated users" on "public"."pack_topics" as permissive for select to authenticated using (true); drop policy if exists "Admins can delete topics" on "public"."topics"; create policy "Admins can delete topics" on "public"."topics" as permissive for delete to authenticated using ((EXISTS ( SELECT 1 FROM public.admin_users WHERE (admin_users.user_id = auth.uid())))); drop policy if exists "Admins can insert topics" on "public"."topics"; create policy "Admins can insert topics" on "public"."topics" as permissive for insert to authenticated with check ((EXISTS ( SELECT 1 FROM public.admin_users WHERE (admin_users.user_id = auth.uid())))); drop policy if exists "Admins can update topics" on "public"."topics"; create policy "Admins can update topics" on "public"."topics" as permissive for update to authenticated using ((EXISTS ( SELECT 1 FROM public.admin_users WHERE (admin_users.user_id = auth.uid())))); drop policy if exists "Topics are viewable by authenticated users" on "public"."topics"; create policy "Topics are viewable by authenticated users" on "public"."topics" as permissive for select to authenticated using (true); -- This policy was dropped at the start, recreate it create policy "Anyone can view questions in visible packs" on "public"."questions" as permissive for select to public using (((deleted_at IS NULL) AND (pack_id IN ( SELECT question_packs.id FROM public.question_packs WHERE (((question_packs.is_public = true) OR ((question_packs.is_premium = true) AND (EXISTS ( SELECT 1 FROM public.profiles WHERE ((profiles.id = auth.uid()) AND (profiles.is_premium = true)))))) AND ((question_packs.category_id IS NULL) OR (EXISTS ( SELECT 1 FROM public.categories c WHERE ((c.id = question_packs.category_id) AND (c.is_public = true)))))))))); -- Storage policies: drop if exists then create drop policy if exists "Anyone can view avatars" on "storage"."objects"; create policy "Anyone can view avatars" on "storage"."objects" as permissive for select to public using ((bucket_id = 'avatars'::text)); drop policy if exists "Users can delete own avatar" on "storage"."objects"; create policy "Users can delete own avatar" on "storage"."objects" as permissive for delete to public using (((bucket_id = 'avatars'::text) AND ((auth.uid())::text = (storage.foldername(name))[1]))); drop policy if exists "Users can update own avatar" on "storage"."objects"; create policy "Users can update own avatar" on "storage"."objects" as permissive for update to public using (((bucket_id = 'avatars'::text) AND ((auth.uid())::text = (storage.foldername(name))[1]))); drop policy if exists "Users can upload own avatar" on "storage"."objects"; create policy "Users can upload own avatar" on "storage"."objects" as permissive for insert to public with check (((bucket_id = 'avatars'::text) AND ((auth.uid())::text = (storage.foldername(name))[1])));