// 历史核对工具,不是活的改标题路径。--apply 会直接报错。 // 下面三段 SQL 只为锁住历史迁移 // frontend/supabase/migrations/20260916020000_rectification_session_title_repair.sql // 不得执行 TITLE_APPLY_SQL:它会把标题写回日期。 // 结果时间口径的回填是 // frontend/supabase/migrations/20260925010000_rectification_session_title_result.sql。 // // 本文件保留下来有两个用处: // 1. 排查时数一数还有多少行没对上(不写库,只 select count); // 2. 下面三段 SQL 只锁历史迁移的逐字合同,不是新的改标题路径。 // tests/rectification-session-title-repair-migration.test.ts 会比对两边。 // // 需要 SCHEMA_DATABASE_URL 才能连库;没有它的环境只能跑上面那个合同测试。 import { resolve } from "node:path"; import { pathToFileURL } from "node:url"; import pg from "pg"; const { Client } = pg; export const TITLE_MATCH_SQL = ` session_type = 'birth_time_rectification' and title ~ '^[0-9]{1,2}月[0-9]{1,2}日[[:space:]]*·[[:space:]]*生时校正([[:space:]]+[0-9]{2}:[0-9]{2})?$' and ( created_at is null or (substring(title from '^([0-9]{1,2})月'))::int is distinct from extract(month from created_at at time zone 'Asia/Shanghai')::int or (substring(title from '月([0-9]{1,2})日'))::int is distinct from extract(day from created_at at time zone 'Asia/Shanghai')::int ) `; const TITLE_COUNT_SQL = `select count(*)::int as n from public.chat_sessions where ${TITLE_MATCH_SQL}`; export const TITLE_APPLY_SQL = ` update public.chat_sessions set title = case when created_at is null then '生时校正' else to_char(created_at at time zone 'Asia/Shanghai', 'FMMM') || '月' || to_char(created_at at time zone 'Asia/Shanghai', 'FMDD') || '日 · 生时校正' end where ${TITLE_MATCH_SQL} `; const ACTIVITY_COUNT_SQL = ` select count(*)::int as n from public.chat_sessions as session join public.agentic_rectification_cases as case_row on case_row.session_id = session.id and case_row.user_id = session.user_id left join lateral ( select max(turn.created_at) as last_turn_at from public.agentic_rectification_turns as turn where turn.case_id = case_row.id ) as turns on true where session.session_type = 'birth_time_rectification' and session.updated_at < coalesce(turns.last_turn_at, case_row.last_activity_at) `; export const ACTIVITY_APPLY_SQL = ` update public.chat_sessions as session set updated_at = coalesce(turns.last_turn_at, case_row.last_activity_at) from public.agentic_rectification_cases as case_row left join lateral ( select max(turn.created_at) as last_turn_at from public.agentic_rectification_turns as turn where turn.case_id = case_row.id ) as turns on true where case_row.session_id = session.id and case_row.user_id = session.user_id and session.session_type = 'birth_time_rectification' and session.updated_at < coalesce(turns.last_turn_at, case_row.last_activity_at) `; function connectionString() { const value = process.env.SCHEMA_DATABASE_URL?.trim(); if (!value) { throw new Error("SCHEMA_DATABASE_URL is required"); } if (!/^postgres(ql)?:\/\//.test(value)) { throw new Error("SCHEMA_DATABASE_URL must be a PostgreSQL URL"); } return value; } async function main() { if (process.argv.includes("--apply")) { throw new Error( "--apply has been removed; the repair now ships as migration " + "20260916020000_rectification_session_title_repair.sql. " + "Apply it with Gitea -> Migrate Staging Database.", ); } const client = new Client({ connectionString: connectionString() }); await client.connect(); try { const titles = await client.query(TITLE_COUNT_SQL); const activity = await client.query(ACTIVITY_COUNT_SQL); console.log(`mismatched_dated_rectification_titles=${titles.rows[0]?.n ?? 0}`); console.log(`stale_rectification_updated_at=${activity.rows[0]?.n ?? 0}`); } finally { await client.end(); } } // Only connect when run as a command. Importing this file (the contract test // does) must not open a database connection. const invokedPath = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : undefined; if (invokedPath === import.meta.url) { main().catch((error) => { console.error(error instanceof Error ? error.message : error); process.exitCode = 1; }); }