Skip to content

fix(service-analytics): 编译期拒绝 dataset 的跨 datasource JOIN (#5115) - #5287

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-5115-dataset-cross-datasource-compile-gate
Aug 4, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-5115-dataset-cross-datasource-compile-gate

Conversation

@claude

@claude claude Bot commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

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 路径,以及两条出路:

[dataset-compiler] dataset "revenue_by_region" declares a JOIN that crosses datasources:
its base object "opportunity" is on datasource "billing_db", but the joined object
"crm_account" — reached via the `include` path "account" — is on datasource "crm_db".
A dataset JOIN cannot cross datasources: the whole dataset is executed as ONE statement
on the base object's datasource, so "crm_account" is simply not there. Fix it by binding
both objects to the same datasource, or by dropping "account" from the dataset's
`include` (and every dimension/measure that references it).

与 #5033 查询期那句同源(同样的「不能跨库 + 两条修法」),不会读成两个 bug。

分级放行的判据(这是本 PR 最需要评审的部分)

拒绝只在元数据自己能证明冲突时发生:基对象与某个 join 目标各自显式声明了 object.datasource,且两个名字不同(大小写不敏感比较)。以下一律放行,由 #5033 的查询期防线兜底:

  1. 宿主没接探针(无 data engine)—— 编译行为与改动前完全一致。有专门的测试钉住:不传 options、传 {}、以及用 new AnalyticsService({ relationshipResolver }) 直接注册,三种都照常编译。
  2. 探针答不上来(对基对象或对某个 join 目标返回 undefined)。基对象答不上来时不判任何 join —— 不能因为「问不出基对象」就拒绝所有 join。
  3. 任意一侧停在 '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 才会把默认值物化出来)。误放行的代价是一条已经存在的响亮运行时错误;误拒绝的代价是升级后看板直接变白 —— 所以这里选了「证明不了就不拒绝」。
  4. 任意一侧是 federated(external)对象。NativeSQLStrategy.canHandle 本就 decline 这种 cube(base 或 join 目标为 external),查询由 ObjectQL FK-expand 路径(两段读 + 内存 join)承接,那条路径天然跨库。在这里拒绝会打断今天能工作的路径。

明确不在范围

  • 第 2 步(换策略)不做:让跨库看板真正能工作(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)落点的调研结论 —— 只调研,不实现

维护者要求本单只做注册期拒绝,发布闸门另立单。调研结论如下,供开那一单时直接用:

STALE-PREMISE 核对(issue 基于 c272e48be,已对最新 origin/main 重核)

  • ✅ getObjectDatasource 在 AnalyticsServiceConfig 上,plugin 侧从 engine.getObject(name).datasource 接出 —— 仍然成立。
  • ✅ compileDataset 已校验 include 关系是否存在于基对象上,并已通过 relationshipResolver 拿到 join 目标对象名 —— 仍然成立,新判定就接在这条链上。
  • ✅ service-analytics 的 executeRawSql 自动桥接丢弃 objectName —— dataset 原始 SQL 永远打在默认 datasource 上,凡被路由到非默认 datasource 的对象一律读成 0 #5033 的查询期响亮失败文案仍在 analytics-service.ts 的 missing-source 分诊里,措辞未变。
  • ⚠️ 一处补正:issue 说「只需要问每个参与对象绑定在哪个 datasource 上」。现场比这句更微妙 —— '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 修)


Generated by Claude Code

#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
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 4, 2026 3:52pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l labels Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-analytics.

8 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/api/data-api.mdx (via @objectstack/service-analytics)
  • content/docs/api/index.mdx (via @objectstack/service-analytics)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/service-analytics)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/service-analytics)
  • content/docs/plugins/packages.mdx (via @objectstack/service-analytics)
  • content/docs/releases/implementation-status.mdx (via @objectstack/service-analytics)
  • content/docs/releases/v17.mdx (via @objectstack/service-analytics)
  • content/docs/releases/v9.mdx (via @objectstack/service-analytics)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dataset 跨 datasource JOIN 应在编译/发布期就被拒绝,而不是留到查询期才响亮失败(#5033 的后续)

2 participants