-- BUG-1001: rectification titles carry the result, not a date. -- One formula and one automatic-title predicate. The trigger and the backfill -- both call them. Do not assign chat_sessions.updated_at (BUG-988 / BUG-704). -- Open / GET / metadata PATCH still must not retitle (BUG-699): this trigger -- listens only to accepted_time, confirmed_time, and candidate_range. begin; do $migration$ begin if current_user <> 'schema_owner' then raise exception 'rectification_session_title_result_requires_schema_owner' using errcode = '42501'; end if; end $migration$; create or replace function public.rectification_session_title( p_accepted time, p_confirmed time, p_range jsonb ) returns text language sql immutable parallel safe set search_path = '' as $$ select case when p_confirmed is not null then '生时校正 · ' || to_char(p_confirmed, 'HH24:MI') when p_accepted is not null then '生时校正 · ' || to_char(p_accepted, 'HH24:MI') when public.agentic_rectification_is_clock(p_range ->> 'start_time') and public.agentic_rectification_is_clock(p_range ->> 'end_time') then '生时校正 · ' || (p_range ->> 'start_time') || '–' || (p_range ->> 'end_time') else '生时校正' end; $$; -- The dash in the range alternative is U+2013 EN DASH, not a hyphen. -- frontend/src/lib/session-title.ts DATED_ENTRY_TITLE must agree with this -- predicate. The parity test reads this pattern from this file. create or replace function public.rectification_session_title_is_automatic( p_title text ) returns boolean language sql immutable parallel safe set search_path = '' as $$ select p_title is not null and p_title ~ '^(?:(?:生时校正|今日节奏)\s*·\s*[0-9]{1,2}月[0-9]{1,2}日|[0-9]{1,2}月[0-9]{1,2}日\s*·\s*(?:今日节奏|生时校正))(?:\s+[0-9]{2}:[0-9]{2})?$|^生时校正$|^生时校正\s*·\s*[0-9]{2}:[0-9]{2}$|^生时校正\s*·\s*[0-9]{2}:[0-9]{2}–[0-9]{2}:[0-9]{2}$'; $$; create or replace function public.sync_rectification_session_title_from_case() returns trigger language plpgsql security definer set search_path = '' as $$ begin update public.chat_sessions set title = public.rectification_session_title(new.accepted_time, new.confirmed_time, new.candidate_range) where id = new.session_id and user_id = new.user_id and session_type = 'birth_time_rectification' and title is distinct from public.rectification_session_title(new.accepted_time, new.confirmed_time, new.candidate_range) and public.rectification_session_title_is_automatic(title); return new; end; $$; revoke all on function public.rectification_session_title(time, time, jsonb) from public, anon, authenticated; revoke all on function public.rectification_session_title_is_automatic(text) from public, anon, authenticated; revoke all on function public.sync_rectification_session_title_from_case() from public, anon, authenticated; grant execute on function public.rectification_session_title(time, time, jsonb) to schema_owner; grant execute on function public.rectification_session_title_is_automatic(text) to schema_owner, authenticated, service_role; grant execute on function public.sync_rectification_session_title_from_case() to schema_owner; drop trigger if exists agentic_rectification_cases_title_from_result on public.agentic_rectification_cases; create trigger agentic_rectification_cases_title_from_result after insert or update of accepted_time, confirmed_time, candidate_range on public.agentic_rectification_cases for each row execute function public.sync_rectification_session_title_from_case(); -- Client PATCH runs as authenticated and would otherwise write the minted -- title "生时校正" back over the result the insert trigger just stored. -- This function stays security invoker so current_user is the updating role. -- The result trigger is security definer, owned by schema_owner, so its -- update still sees current_user = schema_owner and is allowed. create or replace function public.keep_rectification_result_title() returns trigger language plpgsql set search_path = '' as $$ begin if current_user <> 'schema_owner' and new.session_type = 'birth_time_rectification' and public.rectification_session_title_is_automatic(new.title) and new.title is distinct from old.title then new.title := old.title; end if; return new; end; $$; revoke all on function public.keep_rectification_result_title() from public, anon, authenticated; grant execute on function public.keep_rectification_result_title() to schema_owner; drop trigger if exists chat_sessions_keep_rectification_result_title on public.chat_sessions; create trigger chat_sessions_keep_rectification_result_title before update of title on public.chat_sessions for each row execute function public.keep_rectification_result_title(); do $backfill$ declare retitled integer; begin -- BEGIN rectification_session_title_backfill update public.chat_sessions as session set title = public.rectification_session_title(case_row.accepted_time, case_row.confirmed_time, case_row.candidate_range) from public.agentic_rectification_cases as case_row where case_row.session_id = session.id and case_row.user_id = session.user_id and session.session_type = 'birth_time_rectification' and session.title is distinct from public.rectification_session_title(case_row.accepted_time, case_row.confirmed_time, case_row.candidate_range) and public.rectification_session_title_is_automatic(session.title); -- END rectification_session_title_backfill get diagnostics retitled = row_count; raise notice 'rectification_session_title_result retitled=%', retitled; end $backfill$; commit;