-- BUG-732: split append_consultation_question into two caps. -- Conversation quota (200,000) counts only user-visible `text`. -- thinkingText / thinkingSections stay stored but must not enter that sum. -- Physical cap counts the whole message JSON, including receipts. -- Arithmetic: 50 rounds × (~4,000 body + ~4,000 thinkingText + ~3,000 -- thinkingSections + ~3,000 receipts) ≈ 700,000. Headroom → 1,000,000. -- Signature, return columns, error_code values, advisory lock, request_id -- idempotency, 200-message cap, and 16,000-char question check are unchanged -- (BUG-464). CREATE OR REPLACE is backward compatible with deployed callers. begin; do $migration$ begin if current_user <> 'schema_owner' then raise exception 'consultation_session_capacity_requires_schema_owner' using errcode = '42501'; end if; end $migration$; create or replace function public.append_consultation_question( p_user_id uuid, p_request_id text, p_session_id uuid, p_question_message jsonb ) returns table(success boolean, error_code text) language plpgsql security definer set search_path = '' as $$ declare v_session public.chat_sessions%rowtype; v_message jsonb; v_text text; v_request_id text; v_count integer; v_chars integer; v_new_chars integer; v_physical_chars bigint; v_new_physical bigint; begin v_request_id := btrim(coalesce(p_request_id, '')); if p_user_id is null or p_session_id is null or v_request_id = '' then return query select false, 'invalid_request'::text; return; end if; if jsonb_typeof(p_question_message) <> 'object' or p_question_message->>'role' <> 'user' then return query select false, 'invalid_question_message'::text; return; end if; v_text := btrim(coalesce(p_question_message->>'text', '')); if v_text = '' or char_length(v_text) > 16000 then return query select false, 'invalid_question_message'::text; return; end if; perform pg_advisory_xact_lock(hashtextextended(p_user_id::text || ':' || v_request_id, 0)); select session.* into v_session from public.chat_sessions as session where session.id = p_session_id and session.user_id = p_user_id and session.session_type = 'consultation' for update; if not found then return query select false, 'session_missing'::text; return; end if; if exists ( select 1 from jsonb_array_elements(coalesce(v_session.messages, '[]'::jsonb)) as elem where elem->>'requestId' = v_request_id ) then return query select true, null::text; return; end if; v_message := p_question_message || jsonb_build_object('requestId', v_request_id); v_count := jsonb_array_length(coalesce(v_session.messages, '[]'::jsonb)); select coalesce(sum(length(coalesce(elem->>'text', ''))), 0), coalesce(sum(length(elem::text)), 0) into v_chars, v_physical_chars from jsonb_array_elements(coalesce(v_session.messages, '[]'::jsonb)) as elem; v_new_chars := length(v_text); v_new_physical := length(v_message::text); if v_count >= 200 or (v_chars + v_new_chars) > 200000 or (v_physical_chars + v_new_physical) > 1000000 then return query select false, 'session_full'::text; return; end if; update public.chat_sessions as session set messages = coalesce(session.messages, '[]'::jsonb) || jsonb_build_array(v_message), title = case when coalesce(btrim(session.title), '') in ('', '新对话') then case when char_length(v_text) > 14 then left(v_text, 14) || '…' else v_text end else session.title end, updated_at = clock_timestamp() where session.id = p_session_id and session.user_id = p_user_id and session.session_type = 'consultation'; if not found then return query select false, 'session_missing'::text; return; end if; return query select true, null::text; end; $$; revoke all on function public.append_consultation_question(uuid, text, uuid, jsonb) from public, anon, authenticated; grant execute on function public.append_consultation_question(uuid, text, uuid, jsonb) to service_role; do $$ begin if exists(select 1 from pg_roles where rolname = 'admin_runtime') then grant execute on function public.append_consultation_question(uuid, text, uuid, jsonb) to admin_runtime; end if; end $$; commit;