Commit 8d21b26f by luoqi

fix(sync): cohort 模式下患者主档不注入 cursor —— 被别的表带进来的患者主档拉不到

测试服实测(第二轮增量):cohort 列出 15,107 人,其中「复杂病例变了但人没来诊」的
19 人确实被 UNION 分支正确带进了 cohort,但他们的 host_follow_up_active 始终没更新。

根因在 loadTablesForCohort:主档 query 同时被注入 cursor 和 cohort 过滤 →
  WHERE last_visit_time > '2026-07-30 09:36' AND (patient_id,brand) IN (<本批 tuples>)
这些人末次就诊在几个月前 → 被 cursor 挡掉 → 主档整轮拉不到 → 不 upsert。
batch 日志早有征兆:cohort 726 人,fact_client_out 只回 675 行。

cohort 已经是比 cursor 更强的限定(它就是"本轮要处理哪些患者"的答案),
主档再叠 cursor 是多余且有害的。非 cohort 模式靠 reversePullPatientMaster 兜这个场景,
cohort 模式此前无兜底 —— 与其再补一次反向拉,不如从源头去掉这个条件。

️ 这是**既有缺陷**,不是本次接入引入的:任何"事实变了但 last_visit_time 没变"的患者
(EMR 补写、预约改期),其主档在 cohort 模式下本来就整轮不更新。此前只表现为主档字段
偶尔陈旧、被 stub 兜住不易察觉;到了派生列(主档不拉 = 列不重算)才致命。

cursorAdvances 不受影响:bundle 水位写的是 run_start baseline,不取 max。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parent 4cfecec8
......@@ -615,7 +615,17 @@ export class ClickHouseSourceService {
await Promise.all(
Object.entries(source.queries).map(async ([tableName, sql]) => {
const incCfg = incremental?.perQuery[tableName];
const sqlWithCursor = incCfg
// ⭐ 患者主档表**不注入 cursor** —— cohort 已经限定了"要处理哪些患者",主档只负责
// 把这些患者的属性拉全。再叠一层 `last_visit_time > cursor` 会把**被别的表带进
// cohort 的患者**(病例变了/EMR 被编辑,但人没来诊)的主档挡掉:
// cohort 列出了他,主档却拉不到 → 该患者主档字段整轮不更新。
// 2026-08-01 测试服实测:cohort batch 726 人,主档只拉回 675 人,差的就是这批;
// 宿主跟进闸(派生列 has_active_complex_case)因此始终停在旧值。
// 非 cohort 模式有 reversePullPatientMaster 兜这个,cohort 模式此前没有兜底 ——
// 与其再补一次反向拉,不如从源头不加这个多余条件(cohort 本就是更强的限定)。
const isPatientMaster = tableName === this.tableKeyOf(cohort.patient_list_from);
const sqlWithCursor =
incCfg && !isPatientMaster
? this.injectIncrementalCursor(sql, incCfg.cursorColumn, incCfg.cursorValue)
: sql;
const sqlWithCohort = this.injectCohortFilter(sqlWithCursor, cohortClause);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment