Files
Jyotisha/frontend/supabase/migrations/20260721010000_conversational_legacy_import_projection.sql
T
2026-07-21 14:16:54 +08:00

447 lines
18 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
begin;
create unique index if not exists birth_time_rectification_cases_one_v3_import_per_legacy
on public.birth_time_rectification_cases (imported_from_case_id)
where imported_from_case_id is not null
and journey_protocol = 'conversational-evidence-v3';
create or replace function public.conversational_rectification_project_legacy_event_evidence(
p_life_events jsonb,
p_birth_date date,
p_as_of_date date
)
returns jsonb
language plpgsql
immutable
strict
set search_path = ''
as $$
declare
v_item jsonb;
v_id uuid;
v_domain text;
v_imported_domain text;
v_label text;
v_precision text;
v_date text;
v_day date;
v_seen uuid[] := '{}'::uuid[];
v_result jsonb := '[]'::jsonb;
begin
if pg_catalog.jsonb_typeof(p_life_events) is distinct from 'array' then
return '[]'::jsonb;
end if;
for v_item in
select item.value
from pg_catalog.jsonb_array_elements(p_life_events) with ordinality item(value, ordinality)
order by item.ordinality
loop
begin
if pg_catalog.jsonb_typeof(v_item) is distinct from 'object'
or not public.conversational_rectification_has_only_keys(
v_item, array['id', 'domain', 'precision', 'date']
)
or public.conversational_rectification_valid_uuid_text(v_item ->> 'id') is not true then
continue;
end if;
v_id := (v_item ->> 'id')::uuid;
if v_id = any(v_seen) then continue; end if;
v_domain := v_item ->> 'domain';
v_precision := v_item ->> 'precision';
v_date := v_item ->> 'date';
if v_domain not in (
'career', 'education', 'relocation', 'relationship', 'finance', 'health_pressure'
) or v_precision not in ('year', 'month', 'day') then
continue;
end if;
if v_precision = 'year' then
if v_date !~ '^(19|20)[0-9]{2}$'
or v_date < pg_catalog.to_char(p_birth_date, 'YYYY')
or v_date > pg_catalog.to_char(p_as_of_date, 'YYYY') then
continue;
end if;
elsif v_precision = 'month' then
if v_date !~ '^(19|20)[0-9]{2}-(0[1-9]|1[0-2])$'
or v_date < pg_catalog.to_char(p_birth_date, 'YYYY-MM')
or v_date > pg_catalog.to_char(p_as_of_date, 'YYYY-MM') then
continue;
end if;
else
if v_date !~ '^(19|20)[0-9]{2}-(0[1-9]|1[0-2])-([0-2][0-9]|3[01])$' then
continue;
end if;
v_day := v_date::date;
if pg_catalog.to_char(v_day, 'YYYY-MM-DD') is distinct from v_date
or v_day < p_birth_date or v_day > p_as_of_date then
continue;
end if;
end if;
v_imported_domain := case
when v_domain in ('finance', 'health_pressure') then 'other'
else v_domain
end;
v_label := case v_imported_domain
when 'career' then '事业'
when 'education' then '学业'
when 'relocation' then '迁居'
when 'relationship' then '关系'
when 'family' then '家庭'
else '其他'
end;
v_result := v_result || pg_catalog.jsonb_build_array(
pg_catalog.jsonb_build_object(
'id', v_id,
'rawText', '旧校时记录中的' || v_label || '事件(' || v_date || '',
'domain', v_imported_domain,
'eventSummary', '旧校时记录中的' || v_label || '事件',
'dateValue', v_date,
'datePrecision', v_precision,
'extractionStatus', 'clear',
'scoreable', true,
'correctsEvidenceIds', '[]'::jsonb
)
);
v_seen := pg_catalog.array_append(v_seen, v_id);
if pg_catalog.jsonb_array_length(v_result) > 20 then
v_result := v_result - 0;
end if;
exception when others then
-- Old rows predate the strict v3 evidence contract. An invalid fragment
-- stays in the immutable source row instead of aborting or becoming fact.
continue;
end;
end loop;
return v_result;
end;
$$;
drop function if exists public.import_legacy_conversational_rectification_case(
uuid, uuid, uuid, bigint, uuid, integer, text, jsonb, jsonb, jsonb
);
create or replace function public.import_legacy_conversational_rectification_case(
p_user_id uuid,
p_case_id uuid,
p_legacy_case_id uuid,
p_expected_version bigint,
p_action_id uuid,
p_price integer,
p_pending_consultation_question text,
p_declared_birth_input jsonb,
p_evidence jsonb,
p_first_turn jsonb,
p_validation_receipt jsonb,
p_private_candidate jsonb
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_case public.birth_time_rectification_cases%rowtype;
v_legacy public.birth_time_rectification_cases%rowtype;
v_profile public.profiles%rowtype;
v_billing public.birth_time_rectification_billing%rowtype;
v_receipt public.birth_time_rectification_action_receipts%rowtype;
v_expected_declared jsonb;
v_expected_evidence jsonb;
v_expected_recap jsonb;
v_expected_range_start time without time zone;
v_expected_range_end time without time zone;
v_first_turn_id uuid;
v_response jsonb;
v_fingerprint text := public.conversational_rectification_fingerprint(
pg_catalog.jsonb_build_object(
'kind', 'import_legacy', 'userId', p_user_id, 'caseId', p_case_id,
'legacyCaseId', p_legacy_case_id, 'expectedVersion', p_expected_version,
'actionId', p_action_id, 'price', p_price,
'pendingConsultationQuestion', p_pending_consultation_question,
'declaredBirthInput', p_declared_birth_input, 'evidence', p_evidence,
'firstTurn', p_first_turn, 'validationReceipt', p_validation_receipt,
'privateCandidate', p_private_candidate
)
);
begin
if p_user_id is null or p_case_id is null or p_legacy_case_id is null
or p_action_id is null or p_case_id is distinct from p_action_id
or p_expected_version is null or p_expected_version < 0
or p_price is null or not (p_price between 1 and 1000000) then
raise exception 'conversational_action_conflict' using errcode = 'P0001';
end if;
perform pg_catalog.pg_advisory_xact_lock(
pg_catalog.hashtextextended(
p_user_id::text || ':conversational-rectification-case', 0
)
);
perform pg_catalog.pg_advisory_xact_lock(
pg_catalog.hashtextextended(p_user_id::text || ':' || p_action_id::text, 0)
);
select c.* into v_case
from public.birth_time_rectification_cases c
where c.id = p_case_id
for update;
if found then
if v_case.user_id is distinct from p_user_id
or v_case.journey_protocol is distinct from 'conversational-evidence-v3'
or v_case.imported_from_case_id is distinct from p_legacy_case_id then
raise exception 'conversational_case_not_found' using errcode = 'P0001';
end if;
select r.* into v_receipt
from public.birth_time_rectification_action_receipts r
where r.case_id = p_case_id and r.action_id = p_action_id
for update;
if found
and v_receipt.user_id is not distinct from p_user_id
and v_receipt.action_kind is not distinct from 'import_legacy'
and v_receipt.expected_turn_version is not distinct from p_expected_version
and v_receipt.request_fingerprint is not distinct from v_fingerprint then
return v_receipt.response;
end if;
raise exception 'conversational_action_conflict' using errcode = 'P0001';
end if;
select legacy.* into v_legacy
from public.birth_time_rectification_cases legacy
where legacy.id = p_legacy_case_id and legacy.user_id = p_user_id
for update;
if not found
or v_legacy.journey_protocol not in ('legacy-guided-v1', 'dynamic-choice-v2')
or v_legacy.status not in ('assessing', 'rectifying', 'candidate', 'confirming') then
raise exception 'conversational_case_not_found' using errcode = 'P0001';
end if;
if v_legacy.turn_version is distinct from p_expected_version then
raise exception 'conversational_stale_turn' using errcode = 'P0001';
end if;
if exists (
select 1
from public.birth_time_rectification_cases imported
where imported.imported_from_case_id = p_legacy_case_id
and imported.journey_protocol = 'conversational-evidence-v3'
) then
raise exception 'conversational_action_conflict' using errcode = 'P0001';
end if;
if exists (
select 1
from public.birth_time_rectification_cases active_case
where active_case.user_id = p_user_id
and active_case.id <> p_case_id
and active_case.id <> p_legacy_case_id
and active_case.journey_protocol = 'conversational-evidence-v3'
and active_case.status in ('starting', 'active', 'paused', 'confirming')
) then
raise exception 'conversational_action_conflict' using errcode = 'P0001';
end if;
select profile.* into v_profile
from public.profiles profile
where profile.id = p_user_id
for update;
if not found then
raise exception 'conversational_case_not_found' using errcode = 'P0001';
end if;
if v_profile.rectification_case_id is distinct from p_legacy_case_id then
raise exception 'conversational_action_conflict' using errcode = 'P0001';
end if;
v_profile.credits := public.recover_conversational_rectification_orphan_reservations(
p_user_id, null::uuid
);
v_expected_declared := pg_catalog.jsonb_build_object(
'birthDate', pg_catalog.to_char(v_legacy.reported_date, 'YYYY-MM-DD'),
'source', v_legacy.source,
'birthTimeClue', v_profile.birth_time_clue,
'birthplace', pg_catalog.jsonb_strip_nulls(pg_catalog.jsonb_build_object(
'countryCode', v_profile.country_code,
'provinceCode', v_profile.province_code,
'cityCode', v_profile.city_code,
'districtCode', v_profile.district_code,
'latitude', v_profile.latitude,
'longitude', v_profile.longitude,
'timezoneOffset', v_profile.timezone_offset
))
);
if v_legacy.reported_time is not null then
v_expected_declared := v_expected_declared || pg_catalog.jsonb_build_object(
'reportedTime', pg_catalog.to_char(v_legacy.reported_time, 'HH24:MI')
);
end if;
if v_legacy.reported_period is not null then
v_expected_declared := v_expected_declared || pg_catalog.jsonb_build_object(
'reportedPeriod', v_legacy.reported_period
);
end if;
if v_legacy.uncertainty_before_minutes is not null
or v_legacy.uncertainty_after_minutes is not null then
v_expected_declared := v_expected_declared || pg_catalog.jsonb_build_object(
'uncertaintyBeforeMinutes', v_legacy.uncertainty_before_minutes,
'uncertaintyAfterMinutes', v_legacy.uncertainty_after_minutes
);
end if;
v_expected_evidence := public.conversational_rectification_project_legacy_event_evidence(
v_legacy.life_events, v_legacy.reported_date, current_date
);
if v_legacy.journey_protocol = 'dynamic-choice-v2'
and v_legacy.turn_state #>> '{progress,currentRange,startTime}'
~ '^([01][0-9]|2[0-3]):[0-5][0-9]$'
and v_legacy.turn_state #>> '{progress,currentRange,endTime}'
~ '^([01][0-9]|2[0-3]):[0-5][0-9]$' then
v_expected_range_start := (
v_legacy.turn_state #>> '{progress,currentRange,startTime}'
)::time;
v_expected_range_end := (
v_legacy.turn_state #>> '{progress,currentRange,endTime}'
)::time;
elsif v_legacy.candidate_start is not null and v_legacy.candidate_end is not null then
v_expected_range_start := v_legacy.candidate_start;
v_expected_range_end := v_legacy.candidate_end;
end if;
select coalesce(pg_catalog.jsonb_agg(
pg_catalog.jsonb_build_object(
'id', item.value ->> 'id',
'summary', item.value ->> 'eventSummary',
'dateLabel', item.value ->> 'dateValue'
) order by item.ordinality
), '[]'::jsonb) into v_expected_recap
from pg_catalog.jsonb_array_elements(v_expected_evidence)
with ordinality item(value, ordinality);
if p_declared_birth_input is distinct from v_expected_declared
or p_evidence is distinct from v_expected_evidence
or public.conversational_rectification_valid_declared_birth_input(
p_declared_birth_input
) is not true
or public.conversational_rectification_valid_life_event_evidence_array(
p_evidence
) is not true
or public.conversational_rectification_valid_public_turn(p_first_turn) is not true
or public.conversational_rectification_valid_validation_receipt(
p_validation_receipt
) is not true
or public.conversational_rectification_valid_private_candidate(
p_private_candidate
) is not true
or p_first_turn ->> 'caseId' is distinct from p_case_id::text
or p_first_turn ->> 'journeyProtocol' is distinct from 'conversational-evidence-v3'
or (p_first_turn ->> 'turnVersion')::bigint is distinct from 0
or p_first_turn ->> 'status' not in ('active', 'confirming')
or p_first_turn -> 'evidenceRecap' is distinct from v_expected_recap
or p_first_turn -> 'candidate' ->> 'rangeStart'
is distinct from p_private_candidate ->> 'rangeStart'
or p_first_turn -> 'candidate' ->> 'rangeEnd'
is distinct from p_private_candidate ->> 'rangeEnd'
or (v_expected_range_start is not null and (
nullif(p_private_candidate ->> 'rangeStart', '')::time
is distinct from v_expected_range_start
or nullif(p_private_candidate ->> 'rangeEnd', '')::time
is distinct from v_expected_range_end
))
or nullif(p_first_turn ->> 'pendingConsultationQuestion', '')
is distinct from nullif(p_pending_consultation_question, '')
or pg_catalog.jsonb_path_exists(p_first_turn, '$.**.candidateWeights')
or pg_catalog.jsonb_path_exists(p_first_turn, '$.**.candidateScores')
or pg_catalog.jsonb_path_exists(p_first_turn, '$.**.partitionId')
or pg_catalog.jsonb_path_exists(p_first_turn, '$.**.rawModelOutput')
or pg_catalog.jsonb_path_exists(p_first_turn, '$.**.systemPrompt') then
raise exception 'conversational_action_conflict' using errcode = 'P0001';
end if;
if exists (
select 1 from public.birth_time_rectification_billing b
where b.user_id = p_user_id and b.state = 'reserved'
for update
) then
raise exception 'conversational_action_conflict' using errcode = 'P0001';
end if;
insert into public.birth_time_rectification_cases (
id, user_id, journey_protocol, status, reported_date, reported_time,
reported_period, source, uncertainty_before_minutes,
uncertainty_after_minutes, declared_birth_input,
questionnaire, answers, life_events, candidate_scan,
journey_snapshot, turn_version, turn_state,
candidate_result, candidate_result_id, candidate_start, candidate_end,
event_scoring_version, imported_from_case_id, baseline_active_time,
pending_consultation_question, updated_at
) values (
p_case_id, p_user_id, 'conversational-evidence-v3',
p_first_turn ->> 'status', v_legacy.reported_date, v_legacy.reported_time,
v_legacy.reported_period, v_legacy.source, v_legacy.uncertainty_before_minutes,
v_legacy.uncertainty_after_minutes, p_declared_birth_input,
'{}'::jsonb, '{}'::jsonb, '[]'::jsonb, '{}'::jsonb,
p_first_turn, 0, p_first_turn, p_private_candidate,
nullif(p_private_candidate ->> 'resultId', '')::uuid,
nullif(p_private_candidate ->> 'rangeStart', '')::time,
nullif(p_private_candidate ->> 'rangeEnd', '')::time,
nullif(p_private_candidate ->> 'calculationVersion', ''),
p_legacy_case_id, v_profile.active_birth_time,
nullif(p_pending_consultation_question, ''), pg_catalog.now()
);
update public.profiles
set rectification_case_id = p_case_id,
updated_at = pg_catalog.now()
where id = p_user_id
and rectification_case_id = p_legacy_case_id;
if not found then
raise exception 'conversational_action_conflict' using errcode = 'P0001';
end if;
insert into public.birth_time_rectification_turns (
case_id, turn_version, narrative, candidate, technical_receipt,
evidence_request, evidence_recap, actions, output_validation_receipt
) values (
p_case_id, 0, p_first_turn ->> 'narrative', p_first_turn -> 'candidate',
p_first_turn -> 'technicalReceipt',
nullif(p_first_turn -> 'evidenceRequest', 'null'::jsonb),
p_first_turn -> 'evidenceRecap', p_first_turn -> 'actions', p_validation_receipt
) returning id into v_first_turn_id;
insert into public.birth_time_rectification_event_evidence (
id, case_id, source_turn_id, raw_text, domain, event_summary,
date_value, date_precision, extraction_status, scoreable,
corrects_evidence_ids
)
select (item ->> 'id')::uuid, p_case_id, v_first_turn_id,
item ->> 'rawText', item ->> 'domain', item ->> 'eventSummary',
nullif(item ->> 'dateValue', ''), item ->> 'datePrecision',
item ->> 'extractionStatus', (item ->> 'scoreable')::boolean,
array(select value::uuid from pg_catalog.jsonb_array_elements_text(
coalesce(item -> 'correctsEvidenceIds', '[]'::jsonb)
) correction(value))
from pg_catalog.jsonb_array_elements(p_evidence) evidence(item);
insert into public.birth_time_rectification_billing (
case_id, user_id, price, state, billing_receipt_id,
complete_action_id, balance_after
) values (
p_case_id, p_user_id, p_price, 'migration_waived', pg_catalog.gen_random_uuid(),
p_action_id, v_profile.credits
);
v_response := public.conversational_rectification_case_projection(p_user_id, p_case_id);
insert into public.birth_time_rectification_action_receipts (
case_id, action_id, user_id, action_kind, expected_turn_version,
result_turn_version, request_fingerprint, request, response
) values (
p_case_id, p_action_id, p_user_id, 'import_legacy', p_expected_version,
0, v_fingerprint,
public.conversational_rectification_action_request(
'import_legacy', p_user_id, p_case_id, p_expected_version,
p_action_id, v_fingerprint
),
v_response
);
return v_response;
end;
$$;
revoke all on function public.conversational_rectification_project_legacy_event_evidence(
jsonb, date, date
) from public, anon, authenticated, service_role;
revoke all on function public.import_legacy_conversational_rectification_case(
uuid, uuid, uuid, bigint, uuid, integer, text, jsonb, jsonb, jsonb, jsonb, jsonb
) from public, anon, authenticated;
grant execute on function public.import_legacy_conversational_rectification_case(
uuid, uuid, uuid, bigint, uuid, integer, text, jsonb, jsonb, jsonb, jsonb, jsonb
) to service_role;
commit;