Files
Jyotisha/frontend/supabase/migrations/20260830020000_personal_report_sections.sql
T
Jesse_Chen 0ae3e2d796
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
feat(reports): generate personal reports by section
2026-08-30 22:59:17 +08:00

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;