Skip to content

fix(service-analytics): executeRawSql 桥接透传 objectName —— dataset 原始 SQL 打回对象自己的 datasource (#5033) - #5119

Merged
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-5033-rawsql-object-routing
Aug 4, 2026
Merged

xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-5033-rawsql-object-routing

Conversation

@xuyushun441-sys

Copy link
Copy Markdown
Collaborator

Fixes #5033

结论

AnalyticsServicePlugin 的 executeRawSql 自动桥接收到了对象名却把它丢掉了 —— engine.execute(knexSql, { args: params })。ObjectQL.execute() 的驱动选择顺序是 options.object → getDriver(object),其次 options.datasource,最后默认驱动;第 1 条永远走不到,于是每一次 dataset 原始 SQL 读都打在默认 datasource 上。凡被路由到别处的对象(ADR-0057 §3.6 telemetry 拆分、显式 object.datasource、datasourceMapping 规则)都读到 no such table,再被 widget 层的优雅降级换成一个自信的 0 —— 而底下是真实存在的行。

桥接现在传 { args: params, object: objectName },与同文件里一直路由正确的 executeAggregate(engine.aggregate(objectName, …))同姿态。两条 dataset 执行路径从此对「这个对象在哪个库里」给同一个答案,选中哪条策略不再决定你读到的是数据还是零。

顺带把 DataEngineLike.execute 的 options 从 Record< string, unknown > 展开成具名的 { args?, object? }:这个键是承重的,应该在调用点就看得见,而不是藏在一个什么都收的索引签名里。

⛔ packages/spec/** 与生成物零改动 —— engine.execute 的契约本来就声明了 object 优先级(见 packages/objectql/src/engine.ts 的 execute() 注释),本单只是把丢掉路由键的那个消费者修回声明,不需要动契约。packages/objectql/** 只读未改;metadata-protocol/src/protocol.ts 未触碰;content/docs/releases/ 未触碰。

已接受的行为变更:跨 datasource 的 dataset JOIN 现在响亮失败

NativeSQLStrategy 为点号维度(account.region)生成 LEFT JOIN。语句现在跑在基对象自己的 datasource 上,join 目标要是不在那个库里,就会在那里失败 —— 而不再像以前那样静默读错库。这是对的(fail-loud),但它必须以自己的身份失败:如果继续报「backing object … is unavailable」并返回空结果,那就是换了个原因让同一个自信的 0 活下来,而且基表明明就在那儿。

所以缺表分诊现在问的是「驱动指名的是哪张表」:

驱动指名的关系 处理
dataset 自己的 object 降级成空结果 + WARN(不变,这正是降级存在的理由)
某张 join 表,但该 object 在本 kernel 根本没注册 降级(真实缺席,形状不变)
某张 join 表,且该 object 已注册 抛错,指名表 X、基对象所在的 datasource、X 实际所在的 datasource,以及修法

第三行的实际文案:

[Analytics] dataset "audit_by_actor" cannot be executed as one statement:
table "account" is not on datasource "telemetry", which is where its base object
"sys_audit_log" lives — "account" is registered on the default datasource.
A dataset JOIN cannot cross datasources. Fix it by binding both objects to the
same datasource, or by dropping the cross-datasource relationship from the
dataset's `include`/dimensions.

AnalyticsServiceConfig 为此新增一个仅用于诊断的可选钩子 getObjectDatasource(objectName),由 plugin.ts 从 engine 的 schema registry 接出。它不参与任何驱动选择。

派发裁定里的方向 3(widget 在底层表不可用时渲染 "not available" 而非 0)不在本单内 —— 那是跨 objectui 的 UI/契约语义问题。本单只保证:服务端对真正缺表的降级日志继续存在,而被误路由的那批场景修完后根本不会再走到降级路径。

测试

新增 packages/services/service-analytics/src/__tests__/raw-sql-object-routing.test.ts,沿用 execution-context-bridge.test.ts 立下的 fakePluginContext 惯例,驱动真实的 plugin 装配,对着一个「按 execute() 文档的顺序解析驱动」的 engine double 跑 —— 这个缺陷对每一个直接 stub executeRawSql 的测试都是不可见的,这也是它活到现在的原因。

9 个用例:

  • 桥接把 object 交给 engine.execute(驱动选择的第一把钥匙);
  • issue 的原样:同一个 telemetry 路由对象,原始 SQL 路径与对象路由的 aggregate 路径返回同一批行;
  • 该场景不再触达降级路径(有行、无 is unavailable WARN);
  • 默认 datasource 的对象原地不动(指名对象后仍解析到同一个默认驱动);
  • 跨 datasource JOIN:拒绝而非降级成空结果;
  • 错误文案指名表 X + datasource Y + 修法,且不是误导性的旧形状;
  • 拓扑错误不打 widget 降级 WARN;
  • dataset 自己的 object 缺表 → 空结果 + WARN(降级存活);
  • join 目标 object 在本 kernel 未注册 → 降级而非抛错。

本地闸门

pnpm --filter @objectstack/service-analytics test
  Test Files  40 passed (40)
       Tests  529 passed (529)

vitest run src/__tests__/raw-sql-object-routing.test.ts --reporter=verbose
  Test Files  1 passed (1)
       Tests  9 passed (9)

tsc --noEmit -p packages/services/service-analytics/tsconfig.json
  8 errors, 全部在本 PR 未触碰的文件里
  (analytics-service.test.ts / measure-source-field-gate.test.ts /
   objectql-timedimension-projection.test.ts) —— 本 PR 新增 0 个

eslint --no-inline-config <三个改动文件>   → clean

23 个仓级 check 脚本:22 绿。唯一红的 check:i18n 与本 diff 无关 ——
它经 CLI 跑 extract,而本 worktree 没有 packages/cli/dist,报的是
"command i18n:extract:… not found"。已用 git stash 把工作树退回
origin/main 复验:同样 EXIT=1。CI 全新 checkout + build,不受影响。

@objectstack/service-analytics 的 patch 级 changeset 已随本 PR 提交(.changeset/analytics-raw-sql-object-routing.md),内含上面那条行为变更的说明与迁移指引。

范围外发现

🤖 Generated with Claude Code

https://claude.ai/code/session_01NrmBxj8rK2uGCnh9aipjwX


Generated by Claude Code

… the default datasource (#5033)

`AnalyticsServicePlugin`'s `executeRawSql` auto-bridge received the object name
and dropped it: `engine.execute(knexSql, { args: params })`. `ObjectQL.execute()`
selects its driver in the order `options.object` -> `getDriver(object)`, then
`options.datasource`, then the default driver, so rule 1 could never fire and
every dataset raw-SQL read landed on the DEFAULT datasource. Any object routed
elsewhere (ADR-0057 3.6 telemetry split, an explicit `object.datasource`, a
`datasourceMapping` rule) raised `no such table`, which the widget-level
graceful degradation turned into a confident `0` over live rows. Measured on the
showcase: `sys_audit_log` returned 49 records object-routed and `{"rows":[]}`
through the dataset raw-SQL path, on one running kernel.

The bridge now passes `{ args: params, object: objectName }`, matching the
`executeAggregate` bridge beside it, so both dataset execution paths give one
answer to "which datasource is this object in". `DataEngineLike.execute`'s
options bag is spelled out instead of `Record<string, unknown>` so the
load-bearing key is visible at the call site.

Accepted behaviour change: a dataset whose SQL joins across datasources now runs
on the base object's own datasource and fails there rather than silently reading
the wrong database. It must fail as ITSELF -- reporting it as "backing object is
unavailable" would keep the confident `0` alive under a new cause -- so the
missing-source triage now asks WHICH relation the driver named:

  - the dataset's own object      -> degrade to an empty result + WARN (unchanged)
  - a joined table whose object   -> degrade (genuine absence, unchanged shape)
    is not registered here
  - a joined table whose object   -> throw, naming table X, the datasource its
    IS registered                    base object lives on, where X actually is,
                                     and the remedy

`AnalyticsServiceConfig` gains one optional diagnostics-only hook,
`getObjectDatasource(objectName)`, wired in `plugin.ts` from the engine's schema
registry. It never selects a driver.

Tests: `src/__tests__/raw-sql-object-routing.test.ts` drives the real plugin
wiring against an engine double that resolves its driver the way `execute()`
documents -- the defect was invisible to every test that stubbed `executeRawSql`
directly. Covers the issue's exact shape (raw SQL == object-routed rows, no
degradation WARN), default-datasource objects unchanged, the cross-datasource
loud failure and its wording, and both surviving degradation paths.

Compile-time rejection of cross-datasource dataset joins is deliberately not in
this PR; filed as #5115.

Fixes #5033

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NrmBxj8rK2uGCnh9aipjwX
@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 5:24am

Request Review

@github-actions github-actions Bot added the size/l label 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

2 participants