-- Durable per-section personal report state. -- Mirrors the Supabase migration with the same schema, constraints, RLS, -- grants and security-definer transitions. begin; -- Existing job rows use the same progress field; extend its check without changing lifecycle ownership. alter table if exists public.personal_report_jobs drop constraint if exists personal_report_jobs_progress_phase_check; alter table if exists public.personal_report_jobs add constraint personal_report_jobs_progress_phase_check check (progress_phase ~ '^(?:[a-z][a-z0-9_]{0,63}|section:[a-z][a-z0-9_-]{0,95})$'); create table if not exists public.personal_report_sections ( user_id uuid not null references auth.users(id) on delete cascade, request_id uuid not null, section_id text not null check (section_id ~ '^[a-z][a-z0-9_-]{0,95}$'), payload jsonb, status text not null default 'pending' check (status in ('pending', 'ready', 'blocked')), attempt_count integer not null default 0 check (attempt_count >= 0), max_attempts integer not null default 2 check (max_attempts between 1 and 10), last_error_code text check (last_error_code is null or last_error_code ~ '^[a-z][a-z0-9_]{0,63}$'), created_at timestamptz not null default now(), updated_at timestamptz not null default now(), primary key (user_id, request_id, section_id), constraint personal_report_sections_report_fk foreign key (user_id, request_id) references public.personal_reports (user_id, request_id) on delete cascade, constraint personal_report_sections_payload_status_check check ((status = 'ready') = (payload is not null)), constraint personal_report_sections_attempt_budget_check check (attempt_count <= max_attempts) ); create index if not exists personal_report_sections_request_idx on public.personal_report_sections (user_id, request_id, status, section_id); alter table public.personal_report_sections enable row level security; revoke all on table public.personal_report_sections from public, anon, authenticated, service_role; revoke all on table public.personal_report_sections from app_runtime, admin_runtime, migration_runner, backup_reader; drop policy if exists personal_report_sections_select_own on public.personal_report_sections; create policy personal_report_sections_select_own on public.personal_report_sections for select to authenticated using (auth.uid() = user_id); grant select on table public.personal_report_sections to authenticated; grant select, insert, update, delete on table public.personal_report_sections to service_role; create or replace function public.ensure_personal_report_section( p_user_id uuid, p_request_id uuid, p_section_id text, p_max_attempts integer default 2 ) returns setof public.personal_report_sections language plpgsql security definer set search_path = pg_catalog, public as $$ begin if p_user_id is null or p_request_id is null or p_section_id !~ '^[a-z][a-z0-9_-]{0,95}$' or p_max_attempts not between 1 and 10 then raise exception using errcode = '22023', message = 'personal_report_section_payload_invalid'; end if; insert into public.personal_report_sections (user_id, request_id, section_id, max_attempts) values (p_user_id, p_request_id, p_section_id, p_max_attempts) on conflict (user_id, request_id, section_id) do nothing; return query select section.* from public.personal_report_sections as section where section.user_id = p_user_id and section.request_id = p_request_id and section.section_id = p_section_id; end; $$; create or replace function public.start_personal_report_section( p_user_id uuid, p_request_id uuid, p_section_id text ) returns setof public.personal_report_sections language plpgsql security definer set search_path = pg_catalog, public as $$ begin if p_user_id is null or p_request_id is null or p_section_id !~ '^[a-z][a-z0-9_-]{0,95}$' then raise exception using errcode = '22023', message = 'personal_report_section_identity_invalid'; end if; return query update public.personal_report_sections as section set attempt_count = section.attempt_count + 1, updated_at = clock_timestamp() where section.user_id = p_user_id and section.request_id = p_request_id and section.section_id = p_section_id and section.status = 'pending' and section.attempt_count < section.max_attempts returning section.*; end; $$; create or replace function public.complete_personal_report_section( p_user_id uuid, p_request_id uuid, p_section_id text, p_payload jsonb ) returns setof public.personal_report_sections language plpgsql security definer set search_path = pg_catalog, public as $$ begin if p_user_id is null or p_request_id is null or p_section_id !~ '^[a-z][a-z0-9_-]{0,95}$' or p_payload is null then raise exception using errcode = '22023', message = 'personal_report_section_completion_invalid'; end if; return query update public.personal_report_sections as section set status = 'ready', payload = p_payload, last_error_code = null, updated_at = clock_timestamp() where section.user_id = p_user_id and section.request_id = p_request_id and section.section_id = p_section_id and section.status = 'pending' returning section.*; end; $$; create or replace function public.block_personal_report_section( p_user_id uuid, p_request_id uuid, p_section_id text, p_error_code text ) returns setof public.personal_report_sections language plpgsql security definer set search_path = pg_catalog, public as $$ begin if p_user_id is null or p_request_id is null or p_section_id !~ '^[a-z][a-z0-9_-]{0,95}$' or p_error_code !~ '^[a-z][a-z0-9_]{0,63}$' then raise exception using errcode = '22023', message = 'personal_report_section_block_invalid'; end if; return query update public.personal_report_sections as section set status = 'blocked', payload = null, last_error_code = p_error_code, updated_at = clock_timestamp() where section.user_id = p_user_id and section.request_id = p_request_id and section.section_id = p_section_id and section.status = 'pending' returning section.*; end; $$; revoke all on function public.ensure_personal_report_section(uuid, uuid, text, integer) from public, anon, authenticated; revoke all on function public.start_personal_report_section(uuid, uuid, text) from public, anon, authenticated; revoke all on function public.complete_personal_report_section(uuid, uuid, text, jsonb) from public, anon, authenticated; revoke all on function public.block_personal_report_section(uuid, uuid, text, text) from public, anon, authenticated; grant execute on function public.ensure_personal_report_section(uuid, uuid, text, integer) to service_role; grant execute on function public.start_personal_report_section(uuid, uuid, text) to service_role; grant execute on function public.complete_personal_report_section(uuid, uuid, text, jsonb) to service_role; grant execute on function public.block_personal_report_section(uuid, uuid, text, text) to service_role; commit;