-- Read-only candidate window for personal reports. Direct table grants on -- agentic_rectification_cases stay revoked; this RPC is the only service_role -- path. Failure to find a row returns null. The function never returns any -- column except start_time / end_time. begin; do $migration$ begin if current_user <> 'schema_owner' then raise exception 'read_report_candidate_range_requires_schema_owner' using errcode = '42501'; end if; end $migration$; create or replace function public.read_report_candidate_range( p_user_id uuid, p_rectification_case_id uuid default null ) returns jsonb language plpgsql stable security definer set search_path = '' as $$ declare v_start text; v_end text; v_range jsonb; begin if p_user_id is null then return null; end if; if p_rectification_case_id is not null then select to_char(c.candidate_start, 'HH24:MI'), to_char(c.candidate_end, 'HH24:MI') into v_start, v_end from public.birth_time_rectification_cases as c where c.id = p_rectification_case_id and c.user_id = p_user_id and c.status in ('confirmed', 'completed') and c.candidate_start is not null and c.candidate_end is not null; if found then return jsonb_build_object('start_time', v_start, 'end_time', v_end); end if; end if; select c.candidate_range into v_range from public.agentic_rectification_cases as c where c.user_id = p_user_id and c.status = 'candidate_accepted' order by c.updated_at desc limit 1; if v_range is null then return null; end if; v_start := nullif(btrim(coalesce(v_range->>'start_time', '')), ''); v_end := nullif(btrim(coalesce(v_range->>'end_time', '')), ''); if v_start is null or v_end is null then return null; end if; return jsonb_build_object('start_time', v_start, 'end_time', v_end); end; $$; revoke all on function public.read_report_candidate_range(uuid, uuid) from public, anon, authenticated; grant execute on function public.read_report_candidate_range(uuid, uuid) to service_role; commit;