Skip to content

service-analytics 的 executeRawSql 自动桥接丢弃 objectName —— dataset 原始 SQL 永远打在默认 datasource 上,凡被路由到非默认 datasource 的对象一律读成 0 #5033

Description

@os-zhuang

结论先行

AnalyticsServicePluginexecuteRawSql 自动桥接收到了对象名却把它丢掉了,于是 dataset 的原始 SQL 永远在默认 datasource 上执行,而不是在该对象自己解析出来的 datasource 上。任何被路由到非默认 datasource 的对象(ADR-0057 §3.6 的 telemetry 拆分、显式 object.datasourcedatasourceMapping 规则),它的 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 loadedTelemetryDatasourceAudit。两个 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_runsys_notification_deliverysys_http_deliveryai_traces …),以及任何用 object.datasource / datasourceMapping 显式外挂到别的库的业务对象,只要有 dataset/看板读它,都是同一个 0。生产环境 OS_TELEMETRY_DB= 显式开启后同样中招。

建议方向(未实现,留给该车道决策)

  1. 桥接把对象名透传下去 —— engine.execute(knexSql, { args: params, object: objectName })。改动最小,且与同文件 executeAggregate 的既有姿态一致:两条路径对"这个对象在哪"给同一个答案。
  2. 需要同时想清楚的:一条 dataset SQL 里带 LEFT JOIN(NativeSQLStrategyaccount.industry 这类点号维度生成的),如果 join 两端落在不同 datasource 上,按 (1) 修完会在对象自己的 datasource 上响亮地失败,而不是像今天这样静默读错库。我认为响亮失败是对的(契约优先),但这是个需要确认的行为变更 —— 也可能该在编译期就拒绝跨 datasource 的 dataset join。
  3. 顺带值得决策的语义问题(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。

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions