183 lines
6.9 KiB
PL/PgSQL
183 lines
6.9 KiB
PL/PgSQL
-- 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;
|