Lightdash Composer 可视化架构 Step 1:用自描述的结果列(ResultColumn)让查询结果脱离 ItemsMap 直接渲染
【免费下载链接】lightdashAgentic BI. Analytics at the speed of code ⚡️项目地址: https://gitcode.com/GitHub_Trending/li/lightdash
本文以 Lightdash 仓库中的设计文档 docs/composer-viz-plan/01-design.md 为主体,深入讲解"Enriched Result Columns(富化结果列)"设计的完整方案:自描述的ResultColumn类型设计、格式表达式的保真度审计与 11 项转换器缺口(G1–G11)、按查询来源划分的列填充规则、消费者改造顺序、兼容性论证、PR 切分策略,以及实施过程中对原设计假设的修正与 M1 收敛缺口。读完本文,你可以理解 Lightdash 如何让泛型 v2 结果接口(ReadyQueryResultsPage:rows + columns + pivotDetails)在不依赖ItemsMap、Explore或MetricQuery的前提下驱动正确格式化、正确标注的可视化输出,并掌握"行存原始值、列携带渲染配方"这一单一格式化路径的架构决策。
1. 设计背景与目标
该设计是 docs/composer-viz-plan 五步计划的 Step 1,其上游计划文档 01-enrich-result-columns.md 定义了目标:让泛型结果形状自身携带足够的元数据(label、format、provenance),使得仅持有ReadyQueryResultsPage的消费者就能渲染出正确格式化、正确标注的输出,无需ItemsMap、Explore或MetricQuery。这是让可视化栈与查询栈解耦的契约。
设计文档的状态是"designed(研究完成,可拆分为 PR)",由四轮研究支撑:列构建清单盘点(column-construction inventory)、格式表达式保真度审计(format expression fidelity audit)、provenance 设计对比、消费者改造清单盘点。其研究动机直接来自仓库中的真实痛点:
ResultColumn原本只有{ reference, type: DimensionType },位于 packages/common/src/types/results.ts。类型定义处的注释早已预见到更丰富的列类型。- 格式化是服务端按结果页施加的:S3 存储原始 JSONL,
AsyncQueryService.getAsyncQueryResults用查询持久化的fields: ItemsMap构建格式化闭包。指标路径的结果已被格式化,但元数据没有以泛型形式出现在线上传输层。 - 后端代码中留有这样一条 TODO(当前仓库中位于 AsyncQueryService.ts):"We should use the columns data instead of fields. We need to: add format expression to columns type and refactor csv service, etc to use columns instead of fields"——随后是对 SQL 查询导出合成仅含 label 的
Dimension的临时方案。 - 泛型前端路径把格式化了:AI artifact 表格把每个单元格解包为
raw;SQL runner 直接从/query/{uuid}/results流式传输原始 JSONL,完全绕过格式化页面端点;Agent 路径同样如此。 - DuckDB/compose 节点通过
LIMIT 1探针派生列,即使每一列都原样来自某个引用了完整query_history.fields的语义层节点,也拿不到任何字段元数据。
2. 核心类型:ResultColumn 与 ResultColumnProvenance
设计锁定的类型定义(设计文档 §1)如下,其中format字段是关键创新:
// packages/common/src/types/results.ts export type ResultColumnProvenance = { /** Key into the query's fields map (query_history.fields). */ fieldId: string; /** * Which query in a multi-source pipeline the field belongs to. Omitted * for single-query results. Two composer nodes can both expose * `orders_status`, so a bare fieldId is ambiguous — the MergeFieldOrigin * lesson (mergeQuery.ts:620-635). */ sourceQueryUuid?: string; }; export type ResultColumn = { reference: string; type: DimensionType; /** Display label. Absent ⇒ consumers fall back to the reference. */ label?: string; /** * Lightdash format expression: ECMA-376 with in-repo extensions (IEC * bytes, tz-shift for date expressions). MUST be rendered with * formatValueWithExpression, never raw numfmt. */ format?: string; /** The expression cannot encode locale — carried beside it, mirroring * Field.separator / getFieldFormatOverrideProps (formatting.ts:1213). */ separator?: NumberSeparator; /** Escape hatch for the two non-expressible formats: Compact.AUTO and * negative round (magnitude rounding). */ formatOptions?: CustomFormat; /** Temporal grain. Required for QUARTER (no ECMA-376 token) and for * export paths (GSheets) that branch on grain. */ timeInterval?: TimeFrames; /** Resolved output of getFormatterTimezone: whether values shift into the * display timezone. */ shiftsTimezone?: boolean; /** Absent ⇒ no semantic field behind this column (computed DuckDB column, * raw SQL column, table calc, join key). Absence gates interaction * capabilities (drill, underlying data, URLs) off — by design. */ provenance?: ResultColumnProvenance; };当前仓库中 results.ts 的类型已包含这些字段(另有一个numericKind字段标注 NUMBER 列在数据源处的表示形式),说明设计已部分落地到代码库。
研究锁定的四条关键设计决策:
- Provenance 内联在列上、以列为主键,而非以字段为主键的 sidecar。Pivot 扇出(一个字段 → N 个
{field}_{agg}_{groupValue}列)在Record<FieldId, …>中不可表达;代码库中已有两处列主键模式的先例(pivotValuesColumns[col].referenceField、MergeTypedColumn.origin)。 - 不为泛型结果合成完整
ItemsMap。merge 路径是警示性证据:合成的Field能通过所有类型守卫(isField/isDimension),然后静默地出错——URL 菜单解析错误的行键,richText/image/colors/showUnderlyingValues未被察觉地丢失,无效字段检测永远无法触发。返回undefined的句柄是诚实的降级;伪造的字段是等着被发现的那个 bug。 format是 Lightdash 表达式,不是可移植的 ECMA-376。formatValueWithExpression(formatting.ts)在 numfmt 之上叠加了 IEC 字节处理、时区重标注和已注册的 separator locale。column.format的每个消费者都必须调用它,绝不裸调 numfmt。- 参数在列构建时服务端插值。参数值在一次查询执行的生命周期内是固定的(修改参数会重新执行),所以
${ld.parameters.*}占位符在存入column.format之前,已通过evaluateConditionalFormatExpression(item.format, usedParametersValues)解析。这使列自包含,并删除了前端重新格式化 hack(formatCellContent)。未插值的模板保留在 chart config 中(它本来就在)。
页面级增补:ReadyQueryResultsPage(api.ts)需要resolvedTimezone(execute 响应已有,页面原本缺失——没有时间区,时间表达式就是错的;当前仓库中该字段已出现在页面类型定义里)以及fields投影(provenance 解析映射,来自query_history.fields)。设计强调fields应以投影形式下发(label、tableLabel、fieldType、type、urls、richText、image、colors、showUnderlyingValues、filters)而非完整Field:Field.sql是原始 dbt SQL,目前并未在 SQL/composer 结果页暴露。这一增补是 Step 2(交互能力)的挂载点——句柄先落地,没有fields时它是惰性的。
formatValueWithExpression 的底层实现印证
format之所以"必须"经由formatValueWithExpression渲染,可以从源码印证。该函数(formatting.ts)依次处理:
- BigInt 安全区间检查:超出
Number.MAX_SAFE_INTEGER的 bigint 直接抛错降级; - IEC 字节单位:匹配表达式中的
"KiB"、"MiB"等二进制后缀,调用compactConfig.convertFn做数值换算后再格式化,并处理悬挂小数分隔符(stripDanglingDecimalSeparator); - 日期格式:带时区时
moment.utc(value).tz(timezone).utc(true)把墙钟时间重标注为 UTC,让 numfmt 的ignoreTimezone原样渲染;无时区分支也必须按 UTC 解析——注释明确说明,否则无 offset 的值会按观看者本地时区偏移,在跨月边界时出错; - 文本格式:裸
@(ID/文本格式)直接原样输出,绕过 numfmt 对大数字的 Excel 式科学计数法; - 兜底:任何异常都
return \${value}``,保证渲染永不中断。
3. 转换器缺口:G1–G11 保真度审计
getFormatExpression→convertCustomFormatToFormatExpression(formatting.ts)是列填充的主力转换器,审计出 11 项缺口。先修缺口,再填充列,否则列会与今天服务端格式化的值不一致:
| 缺口 | 修复方案 |
|---|---|
G1DEFAULT→ null | 输出#,##0.### |
G2ID→ null | 输出@(文本格式;顺带修复 Excel 科学计数法 ID) |
G3DATE/TIMESTAMP→ null,timeInterval未读取 | 按粒度输出yyyy/yyyy-mm/yyyy-mm-dd/yyyy-mm-dd, hh:mm:ss;QUARTER 和(Z)后缀仍由渲染端经timeInterval处理 |
| G4 round 默认分歧(PERCENT/BYTES:表达式说 2 位小数,结构化说 ≤3 尾零截断) | 对齐,并扩展目前覆盖不到它的 round-trip 测试(formatting.test.ts — 无 bytes 夹具,percent 值恰好落在 2 位小数) |
G5 IEC 字节伪表达式("KiB"字符串匹配 hack) | 可接受,但把format文档化为 Lightdash 方言;用户 CUSTOM 后缀含KiB时有误报风险 |
| G6 负数 round 丢失 | 不可表达 →formatOptions逃生舱 |
G7Compact.AUTO→ null(NUMBER/CURRENCY)且被静默丢弃(PERCENT/BYTES) | 依赖数值 →formatOptions逃生舱;修复静默丢弃分支 |
| G8 未转义的 prefix/suffix 引号 | 转义" |
G9YEAR_NUM对转换器不可见(会输出#,##0.###→2,021) | 填充时特判 |
G10 货币符号位置在PERIOD_COMMA下的行为;host-localeDEFAULT分隔符 | 对齐或接受并文档化分歧 |
G11 Excel COUNT 覆盖(#,##0) | 保留在导出路径——一个format装不下 UI 与 Excel 两种变体 |
渲染端约定(不放在表达式里):null → '∅'、undefined → '-'、布尔经formatBoolean(以type === BOOLEAN为键)、坏时间戳为'NaT'、抛错时回退String(value)。现有的两个实现(formatting.ts 与formatRowValueFromWarehouse)已经一致;提炼为一个共享 helper。
测试种子:把 round-trip 夹具扩展为对所有CustomFormatType×{round: undefined, 0, 2, -2}× 所有 separator ×{negative, zero, fractional}值断言applyCustomFormat(v, f) === formatValueWithExpression(convert(f), v, locale(f.separator))。G1–G10 会自然作为失败项浮出。
缺口修复的爆炸半径
转换器不只是一个未来的填充主力——它今天就有活的消费者,G1–G3 落地瞬间就会翻转它们的行为。全局修转换器仍然是对的(只修列的变体反而会重新制造出 Step 1 要消灭的那种分歧),但 PR 1 必须带着 golden/snapshot 覆盖故意地做出这些变更:
getFieldFormatOverrideProps(formatting.ts)——DEFAULT/ID/DATE/TIMESTAMP 的格式覆盖目前走结构化formatOptions分支(因为转换器返回 null);G1–G3 之后它们改走表达式分支,改变展开到查询结果字段上的内容。- 经
getExcelFormatExpression的 Excel 导出——DEFAULT 格式字段将获得显式#,##0.###numFmt 而非 General。COUNT#,##0守卫(G11)已预见到 count 场景;普通数字单元格会变化。在 ExcelService 测试中断言新 numFmt。 convertCustomMetricsToYaml——writeback 开始在之前省略format键的位置输出format: '#,##0.###':YAML diff 抖动,且与 §5 的VirtualViewCoder隐忧同类地构成幂等性隐患。用 snapshot 钉住新输出,并验证 writeback 往返(emit → parse → emit 稳定)。fields.ts的自定义指标格式比较两侧都做转换,构造上安全——无需动作。
4. 按查询来源划分的填充规则
列在写入时持久化进query_history.columns,读取时原样读回——富化发生在写路径。各来源的规则:
| 来源 | 规则 | 位置 |
|---|---|---|
| 指标查询 | itemsMap已是runQueryAndTransformRows的参数,且未透视的列键就是字段 id。getUnpivotedColumns增加一个itemsMap参数:当itemsMap[key]存在时 →label = getItemLabel(item),format = getFormatExpression(item)(缺口修复后、参数插值后),separator/formatOptions/timeInterval/shiftsTimezone取自 item,provenance = { fieldId: key }。这一条规则就是指标路径的全部算法。 | getUnpivotedColumns.ts + 调用点 |
| 透视值列 | 把valuesColumnData.values()(不是.keys())+itemsMap传入getPivotedColumns。每个{field}_{agg}_{groupValue}列:provenance.fieldId = referenceField,format取自源指标,label由指标 label +pivotValues[].formatted组合(已经过完整格式化计算)。索引/直通列自动继承(按引用拷贝)。硬编码的type: NUMBER对 MAX-of-timestamp / boolean ANY 是错的——经convertItemTypeToDimensionType修复属于行为变更,单独分期。 | getPivotedColumns.ts |
| 原始 SQL / SQL 图表 | 虚拟视图 item map 到达与fieldsMap相同的接缝,所以label = friendlyName(reference)免费获得。无 provenance——虚拟视图维度不是语义字段;标记它们会复活 fake-field 失败模式。format保持 undefined。 | SqlQueryComposer.ts, virtualView.ts |
| DuckDB compose(composer 节点) | 不做元数据推断(2026-08-27 重新定范围,PROD-10681)。终节点为语义层查询的管道,原样输出该查询自己的富化结果集。DuckDB 后处理节点是任意复杂 SQL:其列只携带 DuckDB 诚实知道的东西——reference+ 探针类型——不从上游节点继承 label/format/provenance。契约是接口,不是元数据:每个节点的结果集都是同一ResultColumns形状,走同一格式化管道。此前"把探针列与引用列做名称/类型匹配"的方案被放弃——匹配即伪造元数据(SUM(revenue) AS revenue会误报),正是本设计禁止的 fake-field 失败模式;若将来需要显式继承,必须是用户在管道上声明的映射,绝不推断。用测试钉住:单节点[semanticLayer]管道必须终结于指标查询自己的结果集,而不是 DuckDBSELECT *包装。 | runDuckdbSqlQuery 探针;PROD-10681 |
| Compose 合并 | 最廉价的落地原型点:在构建originalColumns的确切位置,compiledMerge.itemsMap[reference](label/format)和typedColumns[].origin(provenance)都在手边、目前被丢弃。仓库合并自动获得指标路径规则。 | AsyncQueryService.ts 合并构建点 |
| 外部数据源 | 仅 DuckDBDESCRIBE——裸列。二等数据源后续补类型。 | ExternalSourceService.ts |
| 静态自动补全结果 | 完整 field 对象在作用域内——平凡。 | AsyncQueryService.ts |
两个生产者侧隐患:
- 缓存命中把旧列拷进新行。部署后的缓存 TTL 期间,新行会服务未富化的列。选择接受(所有消费者本来就必须容忍
undefined),而不是 bumpCACHE_VERSION(会造成仓库负载尖峰)。保留窗口 32 天。 - 两种悬空状态:无 provenance(正常、静默)与provenance 解析失败(源查询已过期)都必须静默降级——后者绝不复用"field not found in dbt project"警告路径。
5. 消费者改造(按序)
所有消费者改造在填充之前都是惰性的,因此可以早于、晚于或交错于填充落地:
- 导出(高价值、自包含)。用
columnsToItemsMap(columns)合成替换SQL_QUERY_MOCK_EXPLORER_NAME字符串比较分支,携带label ?? friendlyName(reference)和format。因为formatItemValue先检查格式表达式、getExcelFormatExpression读取item.format——ExcelnumFmt就是ECMA-376,零转换——CsvService/ExcelService/PivotTableService/GSheets 无需变更。先例:SchedulerTask 中的buildItemMapFromColumns。命名阻塞项:GoogleDriveClient.formatCell按timeInterval和 TIMESTAMP-vs-DATE 分支——所以列上要有timeInterval。chart config 的用户customLabels必须继续压过column.label(测试断言)。golden-file 测试。不要试图对四个导出服务做完整的 ItemsMap→columns 重写。 - Labels(低)。
getAiArtifactTableConfig的label: column.label ?? column.reference(一行;注意它还喂给 saved-SQL-chart 创建——唯一的写路径副作用);SqlChartResultsRunner/SqlRunnerResultsRunnerFrontend停止丢弃originalColumns元数据;IResultsRunner增加getColumns(): ResultColumn[];TableDataModel.getResultOptions回退label ?? column.label ?? key。 - 格式感知单元格渲染器(中)。扩展 TanStack
ColumnMeta加resultColumn?: ResultColumn;useVirtualTable/useTableDataModel设置它;getValueCell在format存在时分支到formatValueWithExpression,formatRowValueFromWarehouse保留为回退。时区隐患:页面无resolvedTimezone时,客户端时间格式化会偏移到观看者的时区——页面级时区是时间列的前置条件。然后 AI artifact 表格保留raw并经此渲染(design B——保留 JSON 单元格与 copy-raw)。 - Agent 预览(中、eval 敏感)。CSV 值保持 raw(模型会把值重新引号化进 SQL;percent 显示 ×100 会腐蚀推理)。只用 label + format 丰富
columnSummary行,让语义作为元数据到达模型。snapshot 测试钉住这个面。 - 图表格式化器(高、最后)。新可视化栈今天只能表达 percent/SI/compact(CartesianChartDataModel 两分支 switch、PieChartDataModel 硬编码默认、BigNumber 仅 compact)。改走
formatValueWithExpression,优先级为display-config 压过column.format(用户显式的 "Percent" 选择覆盖列)。启用sqlRunnerPivotQueries.ts中四处一行改动(Object.values(pivotResults.columns)取代裸 reference)——自然时机是退役废弃的VizColumn别名。排在 (3) 之后落地,保证同一查询的表格与图表一致。性能注记:宽透视下每个 tooltip 回调做表达式格式化,需要每列一个 memoized formatter。
6. 兼容性论证(已验证)
- 仅增量:
ResultColumn出现在所有响应中,仅一个请求体(VirtualViewAsCode.columns)例外,那里未知的可选属性被忽略(tsoa 未开noImplicitAdditionalProperties)。oasdiff breaking把新增可选属性归类为非破坏性;release-safety marker 从 migrations/restApi/mcpApi/config 计算,无一触发。无需 release-safety 声明(按 CLAUDE.md,声明了反而是错的)。无需迁移——jsonb 列已存在。 - 本地运行
pnpm generate-api验证;pre-commit hook 会取消暂存生成产物(设计如此)。 - 旧的
query_history行无需读时默认值(可选字段读作undefined;项目风格是消费者侧默认)。若将来想要单一接缝:convertDbQueryHistoryToQueryHistory覆盖所有读路径。 VirtualViewCoder幂等性:保持其transform()输出最小{reference, type}形状,或在isEqual前归一化,否则每个已提交的 virtual-view YAML 会永远报告 UPDATE。- Payload 大小:列随行出现在每个结果页,且多两处持久化(query_history、pre-aggregate materializations);宽透视会倍增 label/format 字符串。需测量;若有压力,考虑对透视值列省略
format,改用源列 + provenance。 - Snapshot 抖动:
ProjectService.mock.ts的expectedColumns在AsyncQueryService.test.ts中被断言 8 次——填充 PR 更新它们;仅类型 PR 不更新。
7. PR 切分与 M1 收敛缺口
原始 PR 序列(各自独立落地为绿,1–2 对用户惰性):
- 类型 + 转换器缺口修复(G1–G4、G8、G9 最低集)+ round-trip 测试网格。
- 指标路径 + 透视路径填充(持久化富化列)+ snapshot 更新;列构建时的参数插值。
- 导出修复(
columnsToItemsMap)+ golden-file 测试 →首个用户可见收益:格式化、带标签的 SQL/CSV/Excel 导出。 - Labels 穿过可视化栈(消费者项 2)。
- 页面级
resolvedTimezone+ 格式感知单元格渲染器 + AI artifact 表格(消费者项 3)。 - DuckDB 传播(composer 终节点继承元数据)→composer 结果看起来像 Lightdash 数据——Step 1 退出标准。
- Agent
columnSummary富化(消费者项 4)。 8.(Step 2 边界)结果页fields投影 + provenance 消费者;图表格式化器(消费者项 5)。
线性映射(Standardize 项目 M1):PR1 ≈ PROD-9829/9830,PR2 ≈ PROD-9831,PR3 ≈ PROD-9833 + PROD-9835,PR6 关闭 M1 的 composer 半边;PROD-9832(诚实的 SQL 列元数据)即 §4 的 SQL 路径规则。
实施发现(PR 1–2):两条假设未能在代码中存活
设计文档 §7(2026-08-27 修订)首先给出对 §4 的两处修正:
- 参数插值需要先有迁移。原设计假设
usedParametersValues在列构建时于作用域内。NATS 启用的队列路径上并非如此:worker 完全从query_history行重建RunAsyncWarehouseQueryArgs(buildWarehouseQueryArgs),而持久化的只有request_parameters(调用方提供的值,不含已解析的项目默认值)——知道已解析值的 composer 已经不存在了。前置条件:把 composer 的getUsedParameters()输出持久化到新的query_history.used_parametersjsonb(创建时),穿引到runQueryAndTransformRows,在getResultColumnMetadataFromItem中插值。在此之前,填充完全省略参数依赖的格式(绝不存储未插值的占位符——它会在渲染时抛错并回退String(value))。 - SQL 路径得不到任何免费午餐。原设计声称
label = friendlyName(reference)"免费来自指标路径变更"。不成立:SqlQueryComposer以虚拟视图字段 id(${table}_${column})为键构建 fields map,而原始 SQL 仓库列以裸列名为键,items-map 查找永远落空,SQL 列"碰巧"保持裸列状态。PROD-9832 必须把规则变成显式代码:label = friendlyName(reference),且虚拟视图维度绝不带 provenance(守卫,防止未来键匹配悄悄盖上 fake-field provenance)。
决策:行是原始值,列携带渲染配方
今天存在两种行方言:页面端点经服务端按页格式化器提供的格式化ResultValue({raw, formatted}),以及 SQL runner 直接从/query/{uuid}/results流出的原始 JSONL(完全绕过格式化)。有了自描述列,接口契约即:行是原始值;列携带渲染所需的一切(format 表达式 + separator + formatOptions + timeInterval,只经formatValueWithExpression渲染,加上页面级resolvedTimezone以及——一旦持久化——参数值)。服务端格式化的{raw, formatted}形状和按页格式化闭包是遗留:现有消费者继续工作,但新消费者不得依赖服务端格式化的值,M2(共享格式化器,PROD-9834)收敛现有消费者。这是单一格式化路径决策;不允许逐消费者反复重审。
M1 缺口清单("接口感觉对了"标准)
一个引擎(指标层、原始 SQL、composer)在契约内,当且仅当它的结果页携带的列能让消费者在不了解引擎的前提下 label、format、chart。剩余工作:
| 缺口 | 位置 | 工单 |
|---|---|---|
| 已用参数值未持久化 → 队列路径上参数格式无法插值 | query_history迁移 +QueryHistoryModel+ 创建点 +getResultColumnMetadataFromItem | PROD-10680 |
| Composer 管道必须说这个接口——Step 1 退出标准 | 重新定范围为不推断:钉住单节点管道原样服务语义结果集、DuckDB 节点服务诚实裸列 | PROD-10681 |
结果页缺resolvedTimezone——仅凭页面无法完成时间渲染 | ReadyQueryResultsPage(execute 响应已携带) | PROD-10682 |
| 合并结果丢弃手边已有元数据 | originalColumns构建点:compiledMerge.itemsMap+typedColumns[].origin都在作用域内却被丢弃 | PROD-10683 |
透视值列硬编码type: NUMBER——对 MAX-of-timestamp / boolean ANY 是谎言 | getPivotedColumns经convertItemTypeToDimensionType;行为变更,单独分期 | PROD-10690 |
| SQL 列碰巧是裸的,规则从未显式化 | §4 的 SQL 路径规则变成故意代码 | PROD-9832 |
修订后的排序:填充不得先于参数前置条件再落地——列应自包含地出生,而不是事后修补。M1 剩余工作的修订顺序:(1)used_parameters持久化并入填充 PR,让富化完整出厂;(2) SQL 路径规则 + 合并路径 + 页面resolvedTimezone(小、独立);(3) composer 接口钉住(PROD-10681,重新定范围为不推断——小);(4) 透视类型诚实化(分期的行为变更);然后 M2 的导出/格式化器翻转来证明解耦成立。
8. 小结:这套设计解决了什么
把 01-design.md 放回 composer-viz-plan 计划索引 中看,Step 1 承担的是"接口"一层的建设:查询栈(composer 管道)→ 接口(泛型 v2 结果形状)→ 可视化栈。其核心贡献可以浓缩为三点:
- 自描述列类型:
label/format/separator/formatOptions/timeInterval/shiftsTimezone/provenance七个可选字段,全部增量式加入ResultColumn,配合"消费者侧默认"的项目风格实现零迁移、零破坏; - 格式保真的工程化验证:用
applyCustomFormat(v, f) === formatValueWithExpression(convert(f), v, locale)的 round-trip 测试网格,把 11 项转换器缺口(G1–G11)转化为可执行的失败项,并显式评估了修复对既有消费者的爆炸半径; - 诚实降级的原则:宁要返回
undefined的句柄,不要能通过类型守卫的伪造字段——这条原则同时约束了 provenance 形状(列内联而非 sidecar)、DuckDB 节点(不推断、只探针)和 SQL 路径(显式规则 + 防 provenance 守卫)。
而 §7 的修订部分则展示了设计文档的另一种价值:当假设(参数作用域、SQL 路径免费继承)在 PR 1–2 中被代码证伪时,文档就地记录修正、锁定"行原始、列携带配方"的单一格式化路径决策,并重新排出剩余工作的优先级——这使该文档成为 M1(Standardize 项目)收敛过程的活契约。
【免费下载链接】lightdashAgentic BI. Analytics at the speed of code ⚡️项目地址: https://gitcode.com/GitHub_Trending/li/lightdash
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考