Commit ebc90fb7 by luoqi

merge: reparse 源表回溯修复 → test

parents 06ae44f0 7ad3aa93
Pipeline #3602 failed in 0 seconds
...@@ -2476,16 +2476,24 @@ export function traceRawSourceTable(primaryTable: string, transforms: ReadonlyAr ...@@ -2476,16 +2476,24 @@ export function traceRawSourceTable(primaryTable: string, transforms: ReadonlyAr
input?: string; input?: string;
output?: string; output?: string;
inputs?: string[]; inputs?: string[];
outputs?: Array<{ output?: string }>; /// ⚠️ route_by_pattern 的字段名是 `routes`(见 transforms.schema.ts RouteByPatternOpSchema),
/// 不是 `outputs` —— 早先这里写成 outputs,恒为 undefined,导致**凡链路经过 route 的资源
/// 回溯都停在中间表**(_treatment_actual_raw_emr 等)。后果:reparse 把 rawPayload 灌进中间表,
/// 随即被 transform 链用空结果覆盖 → 治疗类 reparse 恒 0 变更**且报成功**(exit 0)。
/// diagnosis 没踩到只因它的链 split→derive→derive 不过 route。
routes?: Array<{ output?: string }>;
}; };
if (t.kind === 'union' && t.output && Array.isArray(t.inputs)) { if (t.kind === 'union' && t.output && Array.isArray(t.inputs)) {
unionInputs.set(t.output, t.inputs); unionInputs.set(t.output, t.inputs);
continue; continue;
} }
if (t.output && t.input) byOutput.set(t.output, t.input); // ⚠️ 跳过**原地 derive**(output === input,如 `_treat_plan_raw → _treat_plan_raw` 补 treat_name):
// 它不改变这张表的来源。登记进去会让 byOutput 指向自己 → resolve 撞环保护当场返回,
// 回溯同样停在中间表(与 routes 那条是**两个独立 bug**,只修一个仍然失效)。
if (t.output && t.input && t.output !== t.input) byOutput.set(t.output, t.input);
// route_by_pattern 多 output:每个 output 都回到同一 input // route_by_pattern 多 output:每个 output 都回到同一 input
if (Array.isArray(t.outputs) && t.input) { if (Array.isArray(t.routes) && t.input) {
for (const o of t.outputs) if (o?.output) byOutput.set(o.output, t.input); for (const o of t.routes) if (o?.output) byOutput.set(o.output, t.input);
} }
} }
const resolve = (tbl: string, seen: Set<string>): string => { const resolve = (tbl: string, seen: Set<string>): string => {
......
/**
* traceRawSourceTable —— reparse 的源表回溯。
*
* 回溯错了不会报错:rawPayload 被灌进**中间表**,随即被 transform 链用空结果覆盖 →
* reparse 恒 0 变更、exit 0、日志写"重衍完成"。2026-08-27 实测治疗类三个资源全中,
* 意味着任何治疗字典修补都无法回填存量,而且没人会发现。
*
* 跑:
* pnpm test -- trace-raw-source-table
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as yaml from 'js-yaml';
import { traceRawSourceTable } from '../src/modules/sync/cold-import/cold-import.service';
const MANIFEST = path.join(__dirname, '../data/jvs-dw/manifest.yaml');
const manifest = yaml.load(fs.readFileSync(MANIFEST, 'utf8')) as { transforms?: unknown[] };
const TF = manifest.transforms ?? [];
const trace = (t: string) => traceRawSourceTable(t, TF);
describe('traceRawSourceTable · 真实 manifest', () => {
// ⛔ 这四个必须都回溯到真正的 DW 原始表,任何一个停在中间表 = reparse 静默失效
it.each([
'treatment_actual_rows',
'treatment_planned_rows',
'treatment_review_rows',
'diagnosis_rows',
])('%s → fact_emr_treatment_out', (tbl) => {
expect(trace(tbl)).toBe('fact_emr_treatment_out');
});
it('⛔ 不能停在 route 的输出表上(本次 bug 的指纹)', () => {
for (const tbl of ['treatment_actual_rows', 'treatment_planned_rows', 'treatment_review_rows']) {
expect(trace(tbl)).not.toMatch(/^_/); // 下划线开头 = manifest 里的中间表
}
});
it('原始表回溯到自身(reparse 据此判定"非 transform 产出"并跳过)', () => {
expect(trace('fact_emr_treatment_out')).toBe('fact_emr_treatment_out');
});
});
describe('traceRawSourceTable · 合成用例', () => {
it('route_by_pattern 用的是 routes 字段,不是 outputs', () => {
const tf = [
{ kind: 'split_json_array', input: 'raw_src', output: '_mid' },
{ kind: 'route_by_pattern', input: '_mid', field: 'x', routes: [{ output: '_a' }, { output: '_b' }] },
{ kind: 'derive', input: '_a', output: 'final_rows' },
];
expect(traceRawSourceTable('final_rows', tf)).toBe('raw_src');
});
it('union 各分支回溯不一致 → 停在 union 输出(既有语义不变)', () => {
const tf = [
{ kind: 'derive', input: 'raw_a', output: 't1' },
{ kind: 'union', inputs: ['t1', 'raw_b'], output: 'merged' },
];
expect(traceRawSourceTable('merged', tf)).toBe('merged');
});
});
...@@ -17,7 +17,10 @@ const transforms = [ ...@@ -17,7 +17,10 @@ const transforms = [
{ kind: 'filter', input: 'patient_settlement_spec', output: '_refund_item_raw', where: {} }, { kind: 'filter', input: 'patient_settlement_spec', output: '_refund_item_raw', where: {} },
{ kind: 'lookup', input: '_refund_item_raw', output: 'refund_item_rows', from: 'patient_settlement', select: {} }, { kind: 'lookup', input: '_refund_item_raw', output: 'refund_item_rows', from: 'patient_settlement', select: {} },
// route_by_pattern 多输出回同一 input // route_by_pattern 多输出回同一 input
{ kind: 'route_by_pattern', input: '_treat_raw', outputs: [{ output: '_actual_raw' }, { output: '_rec_raw' }] }, // ⚠️ 2026-08-27 修正:字段名是 `routes`(见 transforms.schema.ts RouteByPatternOpSchema),
// 本 fixture 原先写 `outputs` —— 跟当时的实现犯了同一个错,于是测试一直绿、生产一直坏
// (治疗类三个资源 reparse 恒 0 变更且报成功)。fixture 必须照**契约**写,不能照实现写。
{ kind: 'route_by_pattern', input: '_treat_raw', routes: [{ output: '_actual_raw' }, { output: '_rec_raw' }] },
]; ];
describe('traceRawSourceTable', () => { describe('traceRawSourceTable', () => {
......
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