fix(service-analytics): 编译期拒绝 dataset 的跨 datasource JOIN (#5115) - #5287
Merged
os-zhuang merged 1 commit intoAug 4, 2026
Merged
Conversation
#5033 把 dataset 的原始 SQL 路由到基对象自己的 datasource 之后,join 目标 落在另一个库上的 dataset 会在查询期响亮失败 —— 正确,但太晚:这类 dataset 仍然可以被保存、发布、挂上看板,失败最终发生在别的环境、别的时间、别人面前。 它是一个纯元数据错误:整个 dataset 被降解为基对象 datasource 上的一条语句, 绑在别处的 join 目标根本不在那里,这件事在编译的那一刻就完全可判定。 现在由 `compileDataset` 判定。`AnalyticsService.registerDataset` —— 每个 dataset(启动预注册、保存、Studio 草稿预览)都要走的那一道门 —— 把 `AnalyticsServiceConfig` 上早已存在的 datasource / federation 探针交给编译器, 被证明的冲突在任何 SQL 被构造之前就被拒绝。错误文案点名两个对象、两个 datasource、出问题的 `include` 路径,以及两条出路(把两个对象绑到同一个 datasource,或者删掉这条关系),与 #5033 的查询期文案同源,读起来不会像两个 bug。 判据刻意收窄:只在元数据自己能证明冲突时才拒绝 —— 基对象与某个 join 目标 各自显式声明了 `object.datasource` 且两个名字不同。以下情形一律按仓里既有的 "cannot answer, do not block" 分级放行: - 宿主根本没有接 datasource 探针(无 data engine)—— 编译行为与改动前完全一致; - 任意一侧停留在默认值。`'default'` 是 schema 的默认**值**,不是路由决定: `ObjectQL.getDriver` 只在显式的非 `'default'` 名字上短路,随后落到 `datasourceMapping` 规则、ADR-0057 §3.6 生命周期分流(audit/telemetry/event) 和所属 package 的 `defaultDatasource` —— 这些编译器都看不见。把 `'default'` 当成「主库」会误拒那些实际被 mapping 规则落到同一个库的 dataset, 并且会让判定取决于该对象是否恰好被 Zod parse 过(那才会把默认值物化出来); - 任意一侧是 federated(external)对象。`NativeSQLStrategy` 本就会 decline 这样的 cube(ADR-0062 D6),查询由 ObjectQL FK-expand 路径承接, 那条路径天然跨库 —— 在这里拒绝会打断今天能工作的路径。 证明不了的部分继续由 #5033 的查询期防线响亮兜底。让跨库看板真正能工作 (在 `NativeSQLStrategy` decline + 两段读内存 join)是另一件事,不在本次改动内。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 8 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
August 4, 2026 16:06
os-zhuang
deleted the
claude/issue-5115-dataset-cross-datasource-compile-gate
branch
August 4, 2026 16:15
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5115
按维护者在 issue 里的拍板,本 PR 只做第 1 步:
compileDataset在编译期拒绝跨 datasource 的 join。做了什么
compileDataset新增一个可选的DatasetCompileOptions(第三个参数),里面是两个早已存在于AnalyticsServiceConfig上的探针:getObjectDatasource(#5033 为诊断文案引入)和isExternalObject(ADR-0062 D6)。AnalyticsService.registerDataset—— 启动预注册、保存、Studio 草稿预览都要走的那一道门 —— 把它们交给编译器,于是被证明的冲突在任何 SQL 被构造之前就被拒绝。判定接在既有的
include解析链路上:resolveHop每解析出一个 join 目标就判一次,没有另起一套关系解析。多跳路径的每一跳都是同一条语句里的 join,所以每一跳都判(不只是最后一段)。错误文案点名两个对象、两个 datasource、出问题的
include路径,以及两条出路:与 #5033 查询期那句同源(同样的「不能跨库 + 两条修法」),不会读成两个 bug。
分级放行的判据(这是本 PR 最需要评审的部分)
拒绝只在元数据自己能证明冲突时发生:基对象与某个 join 目标各自显式声明了
object.datasource,且两个名字不同(大小写不敏感比较)。以下一律放行,由 #5033 的查询期防线兜底:options、传{}、以及用new AnalyticsService({ relationshipResolver })直接注册,三种都照常编译。undefined)。基对象答不上来时不判任何 join —— 不能因为「问不出基对象」就拒绝所有 join。'default'。这条是本 PR 唯一一处需要判断力的地方,理由写在代码注释里:ObjectQL.getDriver的解析顺序是「显式非'default'的object.datasource→datasourceMapping规则 → ADR-0057 §3.6 生命周期分流(audit/telemetry/event →telemetry)→ package 的defaultDatasource→ 全局默认」。也就是说'default'是 schema 的默认值(ObjectSchema.datasource带.default('default')),不是一个路由决定;声明'default'的对象仍然可能被 mapping 规则或生命周期分流落到别的库,甚至正好落到对面那个库。把'default'当成「主库」会误拒这类今天能正常工作的 dataset,还会让判定取决于该对象是否恰好被 Zod parse 过(parse 才会把默认值物化出来)。误放行的代价是一条已经存在的响亮运行时错误;误拒绝的代价是升级后看板直接变白 —— 所以这里选了「证明不了就不拒绝」。NativeSQLStrategy.canHandle本就 decline 这种 cube(base 或 join 目标为 external),查询由 ObjectQL FK-expand 路径(两段读 + 内存 join)承接,那条路径天然跨库。在这里拒绝会打断今天能工作的路径。明确不在范围
NativeSQLStrategy.canHandle在 join 跨库时 decline + 两段读内存 join)完全不在本 PR 内,也没有为它预埋任何抽象。顺带记录一个对第 2 步有用的现场事实:那条路径的机制已经存在 ——ObjectQLStrategy的executeCrossObject(FK-expand,analytics/objectql: 跨对象(点号 lookup)维度聚合在 ObjectQL 路径上静默塌成 (null) 桶 / native SQL 报错 #3654)就是「按 FK 分组 + 单独读关联对象」,每一段各自按对象路由,因此天然跨库;第 2 步大概率是让NativeSQLStrategy在跨库时 decline,而不是新写一条路径。packages/spec/**、packages/lint/**;content/docs/releases/**未触碰,变更记录只走.changeset/。发布闸门(publish-time lint)落点的调研结论 —— 只调研,不实现
维护者要求本单只做注册期拒绝,发布闸门另立单。调研结论如下,供开那一单时直接用:
packages/lint,进reference-integrity-suite.ts那一族(「metadata 里写下的名字能不能解析到东西」),新增一条形如dataset-cross-datasource-join的规则,由 suite 统一接到os validate/os lint/os compile三个入口(该文件的存在意义就是让新规则的接线是一行编辑,避免三处漂移)。StackSchema同时持有objects(每个带datasource)、datasources、datasourceMapping和datasets。也就是说 lint 规则能把getDriver五步里的前两步(显式绑定 + mapping 规则)都算出来,而运行期的compileDataset只能通过探针一次问一个对象、且看不见 mapping。发布闸门天生能比本 PR 判得更准,而不只是更早。defaultDatasource(跨 package 安装时才确定)与「telemetrydatasource 是否真的注册」属于部署事实,规则应对这两类保持同样的「证明不了就不拒绝」姿态。getObjectDatasource只报告声明值。所以 service-analytics 的 executeRawSql 自动桥接丢弃 objectName —— dataset 原始 SQL 永远打在默认 datasource 上,凡被路由到非默认 datasource 的对象一律读成 0 #5033 催生本单的那个真实场景(sys_audit_log靠lifecycle.class: 'audit'被分流到 telemetry,而不是靠显式字段)对本 PR 的闸门是不可见的 —— 补上那个访问器,本 PR 的判据可以在不引入误拒的前提下扩到隐式路由的对象。STALE-PREMISE 核对(issue 基于
c272e48be,已对最新origin/main重核)getObjectDatasource在AnalyticsServiceConfig上,plugin 侧从engine.getObject(name).datasource接出 —— 仍然成立。compileDataset已校验include关系是否存在于基对象上,并已通过relationshipResolver拿到 join 目标对象名 —— 仍然成立,新判定就接在这条链上。analytics-service.ts的 missing-source 分诊里,措辞未变。'default'不是一个绑定答案,而是「没有绑定」;真正的路由还要经过 mapping / 生命周期分流 / package 默认值。本 PR 因此把判据收窄到「双方都显式声明且不同」,并把理由写进代码注释与 changeset。行为变更(收紧)
原本能编译通过、要到查询期才失败的跨库 dataset,现在在注册期就被拒。启动时预注册的 dataset 走既有的 per-dataset try/catch:记一条点名冲突的 WARN 并跳过,同一宿主里其它 dataset 照常注册,不会因此起不来。changeset 里写清了受影响面。
验证
pnpm --filter @objectstack/service-analytics test—— 42 个文件 / 555 个用例全绿(新增 17 个:dataset-compiler.test.ts里 11 个判定与放行用例,dataset-cross-datasource-registration.test.ts里 6 个接线用例)。tsc --noEmit -p packages/services/service-analytics/tsconfig.json—— 与改动前同样的 13 行既有报错(全部在我未触碰的测试文件里:analytics-service.test.ts、measure-source-field-gate.test.ts、objectql-timedimension-projection.test.ts),没有新增。改动前后各跑一次对比确认。eslint --no-inline-config packages/services/service-analytics/src/—— 干净;测试里没有as any。pnpm --filter @objectstack/service-analytics build—— 通过(含 DTS)。顺手记的账(不在本 PR 修)
getObjectDatasource报告的是声明值而非有效 datasource(getDriver五步里它只覆盖第 1 步)。两个后果:service-analytics 的 executeRawSql 自动桥接丢弃 objectName —— dataset 原始 SQL 永远打在默认 datasource 上,凡被路由到非默认 datasource 的对象一律读成 0 #5033 的查询期文案对靠生命周期分流跨库的对象(比如sys_audit_log)会点错库;本 PR 的闸门因此只能收窄到「双方都显式声明」。已按未分配、无标签提交 PM 分诊,本 PR 不依赖它。Generated by Claude Code