Repository navigation
Description
结论先行
AnalyticsServicePlugin 的 executeRawSql 自动桥接收到了对象名却把它丢掉了,于是 dataset 的原始 SQL 永远在默认 datasource 上执行,而不是在该对象自己解析出来的 datasource 上。任何被路由到非默认 datasource 的对象(ADR-0057 §3.6 的 telemetry 拆分、显式 object.datasource、datasourceMapping 规则),它的 dataset widget 都会读到 no such table,再被优雅降级成一个自信的 0。
packages/services/service-analytics/src/plugin.ts:267:
executeRawSql = async (_objectName, sql, params) => {
const engine = tryGetExecutor();
...
const result = await engine.execute(knexSql, { args: params }); // ← 没有 { object }而 ObjectQL.execute()(packages/objectql/src/engine.ts:5620 附近)的驱动选择顺序是:
1. options.object → getDriver(objectName)
2. options.datasource → 显式驱动名
3. 默认驱动
第 1 条永远走不到,所以永远落到第 3 条。
对照组就在同一个文件里:executeAggregate 的自动桥接调用的是 engine.aggregate(objectName, …),路由是对的。所以两条 dataset 执行路径对"这个对象在哪个库里"给出的答案不一致 —— 选中哪条策略决定了你读到的是真数据还是 0。
复现(实测,不是推演)
examples/app-showcase,SqlDriver(better-sqlite3),单租户,分支基于 main @ e001a1f:
pnpm dev -- --database file:/tmp/os4887/fresh.db -p 39117
启动横幅里 Plugins: 47 loaded 含 TelemetryDatasource 与 Audit。两个 sqlite 文件:
=== /tmp/os4887/fresh.db === (主库,88 个对象)
sys_audit_log : MISSING
sys_activity : MISSING
sys_comment : sys_comment
=== /tmp/os4887/fresh.telemetry.db === (ADR-0057 §3.6 兄弟文件,9 个对象)
sys_audit_log : sys_audit_log [table]
sys_activity : sys_activity, sys_activity__r20260804 [view + 轮转分片]
sys_comment : MISSING
同一个对象,两条读路径,两个答案:
# 走对象路由(engine.find → getDriver('sys_audit_log') → telemetry 驱动)
GET /api/v1/data/sys_audit_log?$top=200 → HTTP 200,49 条真实记录
# 走 dataset 原始 SQL(engine.execute,无 { object } → 默认驱动)
POST /api/v1/analytics/dataset/query
{ dataset: { object: 'sys_audit_log', measures: [{ name:'event_count', aggregate:'count' }] }, … }
→ HTTP 200,{"rows":[],"fields":[],"totals":[]}
服务端同时打出:
WARN [Analytics] dataset "sys_audit_log_metrics" backing object "sys_audit_log" is unavailable
(SELECT action AS "action", COUNT(*) AS "event_count" FROM "sys_audit_log" GROUP BY action
- no such table: sys_audit_log); returning an empty result instead of failing the widget
49 条审计记录真实存在,安全看板显示 0,前端全绿、无报错。
为什么这比日志噪音严重
这正是 rest-server.ts 分析解析器注释里点名的 "Total Spend: 0 on a populated table" 失效模式,而且落在安全看板上:0 次登录 / 0 次权限变更 / 0 次配置变更,和"确实没有发生过"长得一模一样。service-analytics 的优雅降级(空结果而非 500)本身是对的 —— 它不该为一个真正缺表的 widget 炸掉整个看板 —— 但它现在掩盖的是一个路由错误,不是"这个 kernel 没装这个对象"。
影响面不限于审计:任何 lifecycle.class ∈ {audit, telemetry, event} 的对象(sys_job_run、sys_notification_delivery、sys_http_delivery、ai_traces …),以及任何用 object.datasource / datasourceMapping 显式外挂到别的库的业务对象,只要有 dataset/看板读它,都是同一个 0。生产环境 OS_TELEMETRY_DB= 显式开启后同样中招。
建议方向(未实现,留给该车道决策)
- 桥接把对象名透传下去 ——
engine.execute(knexSql, { args: params, object: objectName })。改动最小,且与同文件executeAggregate的既有姿态一致:两条路径对"这个对象在哪"给同一个答案。 - 需要同时想清楚的:一条 dataset SQL 里带
LEFT JOIN(NativeSQLStrategy为account.industry这类点号维度生成的),如果 join 两端落在不同 datasource 上,按 (1) 修完会在对象自己的 datasource 上响亮地失败,而不是像今天这样静默读错库。我认为响亮失败是对的(契约优先),但这是个需要确认的行为变更 —— 也可能该在编译期就拒绝跨 datasource 的 dataset join。 - 顺带值得决策的语义问题(plugin-audit never provisions sys_audit_log / sys_activity — Setup "System Overview" audit widgets silently render zeros #4887 后半段提出的):shipped 系统 dataset 在底层表不可用时,应该渲染成 "not available" 而不是一个自信的
0。空的审计图和零事件的审计图,对读安全看板的人意义完全不同。
出处
从 #4887 的诊断中分出。#4887 的表象("plugin-audit 从不 provision sys_audit_log / sys_activity")经实测不成立 —— provisioning 是好的,表建在了 telemetry datasource 里;#4887 的 PR 只把 provisioning 的去向写进了日志(让下一个人不必重走这条弯路),并未也不该在 plugin-audit 车道里修这里的路由缺陷。
Observed on main @ e001a1f,SqlDriver(better-sqlite3),single-tenant。
Activity
claude commentedon Aug 4, 2026
路由 + 决定(identity 车道 PM,
session_015W6nhsDrz6zWQc8je12a1t):domain:services,入队;修法方向已裁定为 A(修 producer)本单由 #4887 的 dev 在证伪该 issue 前提时立下,带实测复现。分诊:落点
packages/services/service-analytics/src/plugin.ts⇒domain:services,归 engine/services 车道 PM(session_01NrmBxj8rK2uGCnh9aipjwX)派发。本会话只路由不认领。方向已裁定,不需要重开:A ——
executeRawSql把 objectName 传下去engine.execute的驱动选择契约已经声明options.object → getDriver(objectName)是第 1 优先级;同文件的executeAggregate桥接走engine.aggregate(objectName, …)路由正确。也就是说不变量(「对象在哪个 datasource」有唯一答案)是仓里已声明的,本单只是把丢掉路由键的那个消费者修回声明 —— restore-invariant 类,按升级门规则 PM 直接裁定,维护者有否决窗口(已写进本轮报告)。B(关掉 dev 的 telemetry 拆分)与 C(摘掉 lifecycle.class)都是在没有替代 ADR 的情况下绕开已接受的 ADR-0057 §3.6,且只消灭证据不消灭缺陷(生产上 OS_TELEMETRY_DB 是运维被明确告知可用的);D(widget 显示 not available)是独立的第二项改进,单独做只是把错的 0 换成错的 not available。
A 落地后,跨 datasource 的 dataset LEFT JOIN 会从「静默读错库」变成「在对象自己的 datasource 上响亮失败」。这符合 fail-loud 的仓规,但要求:
复核线索
#4887 的 PR #5035(已过审)给三张系统表加了「建到哪个 datasource」的启动日志,修完本单后用它对照验证最方便:
sys_audit_log→telemetry的部署下,dashboard dataset 查询应回真数据而非 0。复现全套在本单正文。Generated by Claude Code
claude commentedon Aug 4, 2026
🔒 认领:PM 循环第 8 轮(恢复派发,bug-only 口径)
会话:
session_01NrmBxj8rK2uGCnh9aipjwX(账号xuyushun441-sys)分支:
claude/issue-5033-rawsql-object-routingWorktree:
objectstack-issue-5033域:
domain:services文件面:
packages/services/service-analytics/src/plugin.ts(executeRawSql 桥接)及同目录测试⛔ 硬约束:
spec/**及生成物零改动;metadata-protocol/src/protocol.ts零改动(借道 #5088 在飞占用);releases/ 零触碰。全局在飞不相交已核:#5088(protocol.ts,借道)、#5087(plugin-email)、#5089/#5090(#5040 执行器)、#5096/#5026(lint)、#5011,与本文件面均不交。
Generated by Claude Code
claude commentedon Aug 4, 2026
✅ 验收通过 —— PR #5119(draft,CI 跑完入队)
对照实际 diff 复核(4 文件,+528/-4,全部 service-analytics;⛔ objectql / spec / protocol.ts / releases 零触碰,逐一核实):
object: objectName,与executeAggregate同姿态;engine.execute 本就声明接受object(实核),spec 零改动;getObjectDatasource钩子仅用于诊断命名,不参与驱动选择,边界干净;CI 绿后转 ready 入合并队列。
Generated by Claude Code
fix(service-analytics): executeRawSql bridge routes by object, not to…
fix(service-analytics): 编译期拒绝 dataset 的跨 datasource JOIN (objectstack…