Skip to content

feat(analytics): cross-object grouping via FK-expand + generateSql scope preview (#3654 residual 2 + capability)#3672

Closed
os-zhuang wants to merge 2 commits into
mainfrom
feat/objectql-crossobj-cap-3654
Closed

feat(analytics): cross-object grouping via FK-expand + generateSql scope preview (#3654 residual 2 + capability)#3672
os-zhuang wants to merge 2 commits into
mainfrom
feat/objectql-crossobj-cap-3654

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

#3654 收尾:残留 2(SQL 预览)+ 能力项(跨对象分组)。两个独立 commit。

Commit 1 — 残留 2:generateSql 渲出 read scope(展示层)

ObjectQLStrategy.generateSql(回显给 /analytics/sql)#3588 后已渲 WHERE,但只含查询 filter,漏了 execute 注入的 tenant/RLS scope —— 预览低估真实约束。现在把 base 对象 scope 也渲进 WHERE(参数化,$and)。不执行、不泄露。

Commit 2 — 能力项:信封内跨对象分组走 FK 展开(#3654 主体)

引擎不会 join,所以 ObjectQL 回退路径(日期分桶 / in-memory / federated)此前拒绝跨对象分组如 revenue by account.region(#3664 止血)。现在直接服务常见情形:

  1. 按 lookup FK 列(account)分组跑 base 聚合(引擎能做),加 base 对象 scope;
  2. 被引用对象自己的 RLS scope 读该对象,把 FK id 解析成属性(region);
  3. 内存按属性重并桶,重组度量(sum/count 相加,min/max 取极值)。

受限行归 (restricted)(你拍板的语义):读不到被引用记录的行,度量仍计入、总计守恒,但隐藏记录的属性名绝不出现 —— 无泄露(ADR-0021 D-C / #3602 类)。/analytics/sql 渲出等价 LEFT JOIN

刻意有界 —— 信封外继续 LOUD 拒绝(绝不静默错答):跨对象出现在度量/过滤器(需真 join)、多跳(a.b.c)、avg/count_distinct(跨桶不可重组)配跨对象维。NativeSQLStrategy(正常 SQL 路径)不变。

测试

  • 纯 rebucket(6):重组 sum/count/min/max、restricted 总计守恒、null≠restricted、多维、双跨对象维。
  • 展开集成(3):真实 AnalyticsService + 两次 aggregate stub,断言 base 与 FK 解析都加 scope、restricted 桶成形、/analytics/sql 渲 LEFT JOIN。回退策略 → 三个全红
  • 信封外拒绝(3):跨对象度量 / 跨对象过滤 / avg 配跨对象维 —— 均 LOUD 拒绝且未触引擎。
  • 残留 2(4):scope 渲进 WHERE、与 filter $and、无 provider 不变、信封外预览拒绝。
  • service-analytics 218 passed;tsc 无新增错误。

边界(诚实)

未加真栈 dogfood e2e:能力的正确性核心(重聚合)已被纯函数测穷举,编排 + 双 scope 已被真实 AnalyticsService 集成测覆盖,真实引擎 FK 分组与被引用 scoped 读均由既有合并工作在栈内证过。若需端到端 capstone 可后补。

🤖 Generated with Claude Code

…#3654 residual 2)

generateSql (the representative statement echoed to /analytics/sql and
dataset responses) rendered the query's own filters but NOT the tenant/RLS
read scope that execute() injects via withReadScope — so the previewed
WHERE understated what actually runs. It now renders the base object's
scope too (parameterized, $n placeholders), ANDed with the query filters.

For consistency generateSql also applies the #3654 cross-object guard, so
/analytics/sql no longer previews a dotted-column statement for a query
execute() refuses to run — same clear error instead of misleading SQL.

Display-only (no data leak — the string is documentation, values stay
placeholders); no change when no read-scope provider is configured.

Tests: generateSql now renders "opportunity"."organization_id" = $n and
ANDs it with the query filter; a cross-object preview rejects; the
no-provider case is unchanged. Reverting the change turns the three
load-bearing assertions red.

Co-Authored-By: Claude <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 27, 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 Jul 27, 2026 2:08pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): packages/services.

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

  • content/docs/automation/webhooks.mdx (via packages/services)
  • content/docs/kernel/runtime-services/audit-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/index.mdx (via packages/services)
  • content/docs/kernel/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/plugins/packages.mdx (via packages/services)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/services)

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.

…#3654)

The ObjectQL fallback path (date-granularity bucketing, in-memory driver,
federated) could not join, so cross-object grouping like
`revenue by account.region` was rejected (#3664 stopgap). It now serves
the common case by FK-expand:

1. group the base aggregate on the lookup FK column (`account`) — which
   the engine CAN do — scoped to the base object;
2. resolve each FK id to the related attribute (`region`) with a read of
   the referenced object scoped to THAT object's own RLS;
3. re-bucket by the resolved attribute in memory, recombining measures
   (sum/count add, min/max take the extremum).

A base row whose referenced record the caller cannot read buckets under
an explicit `(restricted)` group: the measure still counts (grand totals
preserved) but the hidden attribute never appears — no leak (ADR-0021
D-C / #3602). `/analytics/sql` renders the equivalent LEFT JOIN.

Bounded — still rejected LOUD (never silently wrong): cross-object in a
MEASURE or FILTER, multi-hop dims, and non-recombinable measures
(avg/count_distinct) with a cross-object dim. NativeSQLStrategy
(the normal SQL path) is unchanged.

The pure re-bucketing step is isolated in cross-object-rebucket.ts and
exhaustively unit-tested (recombination, restricted-total conservation,
null-vs-restricted, multi-dim). Integration tests drive the real
AnalyticsService with a two-call aggregate stub, asserting both the base
and the FK-resolution are scoped and the restricted bucket forms;
reverting the strategy turns all three red.

Co-Authored-By: Claude <noreply@anthropic.com>
@os-zhuang os-zhuang changed the title fix(analytics): render the read scope in ObjectQLStrategy.generateSql (#3654 residual 2) feat(analytics): cross-object grouping via FK-expand + generateSql scope preview (#3654 residual 2 + capability) Jul 27, 2026
@github-actions github-actions Bot added size/l and removed size/m labels Jul 27, 2026
@os-zhuang

Copy link
Copy Markdown
Contributor Author

#3691 取代。#3672 基于旧 main,与并发合并的 #3651(executeAggregate 携带 context)/#3652(generateSql 渲 scope,即本 PR 的残留 2)真冲突;#3691 在当前 main 上干净重叠加、只保留能力项(残留 2 已由 #3652 做掉),单 commit。

@os-zhuang os-zhuang closed this Jul 27, 2026
@os-zhuang
os-zhuang deleted the feat/objectql-crossobj-cap-3654 branch July 27, 2026 14:33
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.

1 participant