Skip to content

feat(analytics,spec): executeAggregate 桥携带 ExecutionContext —— ADR-0021 D-C 第二层带子 (#3602)#3651

Merged
os-zhuang merged 4 commits into
mainfrom
claude/execute-aggregate-execution-context-cukwsp
Jul 27, 2026
Merged

feat(analytics,spec): executeAggregate 桥携带 ExecutionContext —— ADR-0021 D-C 第二层带子 (#3602)#3651
os-zhuang merged 4 commits into
mainfrom
claude/execute-aggregate-execution-context-cukwsp

Conversation

@os-zhuang

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

Copy link
Copy Markdown
Contributor

Closes #3602.

analytics→engine 的桥现在把请求的 ExecutionContext 转发给 engine.aggregate,于是 engine 自己的中间件链会独立于 analytics 层的 getReadScope 给聚合读加 scope。

#3639 的关系:本 PR 开出去之后,#3639 先一步落地了 #3602残留 1(给 label 查询加引用对象自己的 read scope)。已 merge main 并完整保留 #3639 的设计(LabelScopeResolver + scope 参数,fail-closed 逐维度跳过),本 PR 在其之上叠加 context —— 同一个 hook 上的两条带子,与聚合路径的切分一致。

为什么

BaseEngineOptions.context 一直是 .optional(),所以类型系统从没强迫这座桥传它 —— 它也确实没传。一次已认证的聚合到达 engine 时不带任何 principal,plugin-security 的无 principal fall-open 跳过了自己的 RLS 注入,于是唯一还在起作用的就只剩"strategy 记得调 getReadScope"这一条。#3597 就是某个 strategy 没调,两条带子同时失效。

getReadScope 保留:两者走的是不同的解析路径(engine 中间件 vs security.getReadFilter),而且没装 plugin-security 的部署只有 analytics 这一层。这是纵深,不是替代。

改了什么

主项 —— context 贯通

  • StrategyContext 新增 context?: ExecutionContext,由 AnalyticsService.callCtx 逐请求绑定。无条件绑定,包括没有配置 read-scope provider 的情况 —— 恰恰是那种部署最需要 engine 帮它兜底,不能让第二条带子依赖第一条是否接上。
  • StrategyContext.executeAggregate 以及 plugin / service 两处 executeAggregate 配置项新增 context?: ExecutionContext;auto-bridge 转发给 engine.aggregate。纯增量 —— 自定义桥忽略它的话行为与之前完全一致。

残留 1 —— fetchRecordLabels 的第二条带子

#3639 已给它接上 analytics 层的带子(引用对象自己的 read scope)。本 PR 在 fetchRecordLabels / resolveDimensionLabels 上再加一个尾部 context 参数并转发进 executeAggregate,让 engine 也独立 scope 这次行级读。

残留 2 —— ObjectQLStrategy.generateSql

之前完全不生成 WHERE,于是 /analytics/sql 的预览读起来像一次无 scope 全表扫描,而实际执行(#3601 之后)是加了 scope 的 —— 排查"这行为什么不见了"的人拿到的是一段无法复现结果的 SQL。现在渲染调用方的 filter + read scope,并跑与 execute() 相同的联表 scope 守卫(不该渲染一条 execute() 会拒绝的查询)。这段 SQL 从不执行,所以此前是误导性输出,不是泄露。

验证

顺带发现,未在此修

ObjectQLStrategy.execute() 完全忽略 timeDimensions[].dateRange(只有 where 会到 engine),而 NativeSQL 和 preview-evaluator 都处理了。因为带 dateGranularity 的查询会强制 NativeSQL decline,所以任意驱动上的时间分桶趋势图都会画出全量历史。属于正确性 bug,与本 issue 正交 —— 已按 Prime Directive #10 单独开 #3650generateSql 里也因此故意不渲染 BETWEEN(渲染出来就是编造一条执行路径从不施加的谓词),代码注释里引了该 issue。

🤖 Generated with Claude Code

https://claude.ai/code/session_013st2KArjmLQSuVzpDS91jj

…t — ADR-0021 D-C second belt (#3602)

The analytics→engine bridge now forwards the request's ExecutionContext to
`engine.aggregate`, so the engine's own middleware chain scopes analytics
reads independently of the analytics layer's `getReadScope`.

`BaseEngineOptions.context` has always been `.optional()`, so nothing forced
the bridge to pass it — and it did not. An authenticated aggregate reached the
engine with no principal, plugin-security's principal-less fall-open skipped
its RLS injection, and the only thing left scoping the query was the strategy
remembering to call `getReadScope`. #3597 was a strategy that did not, and both
belts were off at once.

`getReadScope` stays: the two resolve scope through different paths (engine
middleware vs `security.getReadFilter`), and a deployment without
plugin-security has only the analytics layer. Depth, not a replacement.

- `StrategyContext` gains `context?: ExecutionContext`, bound per call by
  `AnalyticsService.callCtx` — unconditionally, including when no read-scope
  provider is configured, since that deployment needs the engine belt most.
- `StrategyContext.executeAggregate` and the plugin/service `executeAggregate`
  config options gain `context?: ExecutionContext`. Additive: a custom bridge
  that ignores it behaves exactly as before.
- `fetchRecordLabels` — the dimension display-label lookup — is row-granular
  (one row per record, real display names) and ran with neither read scope nor
  context. Its ids come from a scoped aggregate today, so it leaked nothing,
  but that was the caller's invariant, not the bridge's. Now ANDs the target
  object's read scope in (`$and`, never a key merge) and forwards the context.
- `ObjectQLStrategy.generateSql` emitted no WHERE at all, so `/analytics/sql`
  read as an unscoped table scan while the real aggregate was scoped. Now
  renders the caller's filters and the read scope, and runs the same
  joined-scope guard `execute()` does. Never executed, so this was misleading
  output rather than a leak.
- `BootOptions.analytics` lets a gate boot with the analytics belt off; the new
  dogfood case asserts the engine belt alone still scopes a member to their own
  rows. Verified to bite: reverting the context forwarding makes it count 5
  instead of 2.

Deliberately not fixed here: `ObjectQLStrategy.execute()` ignores
`timeDimensions[].dateRange` entirely, so `generateSql` does not render a
BETWEEN either. Filed as #3650.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013st2KArjmLQSuVzpDS91jj
@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 1:57pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file tests tooling size/l labels Jul 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): packages/qa, packages/services, @objectstack/spec, @objectstack/verify.

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

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via packages/services, @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/audit-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/services, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via packages/qa, @objectstack/spec)
  • content/docs/permissions/delegated-administration.mdx (via packages/qa)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via packages/services, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/services, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec, @objectstack/verify)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v15.mdx (via @objectstack/verify)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

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.

…ate-execution-context-cukwsp

# Conflicts:
#	packages/services/service-analytics/src/analytics-service.ts
#	packages/services/service-analytics/src/dimension-labels.ts
#	packages/services/service-analytics/src/plugin.ts
@os-zhuang
os-zhuang marked this pull request as ready for review July 27, 2026 13:30
#3652 rewrote `ObjectQLStrategy.generateSql` (date_trunc for bucketed
dimensions, COUNT DISTINCT, ORDER BY/LIMIT/OFFSET) and made `execute()`
echo the rendered SQL on the result. Kept all of that.

Resolved the WHERE clause in favour of this branch's version, which
renders the same caller filters plus the two things #3652's inline loop
does not:

- the READ SCOPE (`compileScopedFilterToSql`), the point of #3602
  residual 2 — without it the echoed string still reads as an unscoped
  table scan while the aggregate is scoped;
- `assertJoinedScopesEnforceable`, so it never renders SQL for a query
  `execute()` would reject.

Its filter rendering is also closer to what runs: `contains` binds
`%v%` (a bare comparand under LIKE would only match exactly), and
values go through `coerceFilterValueForObjectQL`, so the comparand
shown is the one this path hands the engine.

Scope VALUES stay in `params`, which the `execute()` echo discards —
only the predicate shape reaches the browser, same as NativeSQL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013st2KArjmLQSuVzpDS91jj
#3664 replaced `assertJoinedScopesEnforceable` with
`assertNoCrossObjectReferences`, which rejects any cross-object
reference regardless of read scope. That is strictly better than what
this branch had: `engine.aggregate()` has no join and the SQL driver's
aggregate emits none, so a dotted member was never merely "unscopeable"
— it was silently wrong (one `(null)` bucket) or a hard error. Adopted
as-is; this branch's narrower scope-conditional guard is gone.

Made it derive its field set from `referencedFieldNames(cube, query)`
instead of `execute()`'s built `groupBy`/`filter`, so `generateSql()`
runs the IDENTICAL guard — the rendered SQL must not describe a query
`execute()` would reject. Same field set either way, so `execute()`'s
behaviour is unchanged.

Also kept from this branch: the read scope rendered into `generateSql`'s
WHERE (#3664 left rendering alone), and the `context` forwarded to the
aggregate bridge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013st2KArjmLQSuVzpDS91jj
@os-zhuang
os-zhuang merged commit 587fc91 into main Jul 27, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/execute-aggregate-execution-context-cukwsp branch July 27, 2026 14:12
os-zhuang added a commit that referenced this pull request Jul 27, 2026
…#3654) (#3691)

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 outright (#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 (and, per #3651's second
   belt, threading the ExecutionContext to the engine too);
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, so
preview and execution accept/reject the same set.

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 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 FK-resolution are scoped and the restricted bucket forms; reverting
the strategy turns all three red.

Re-applied cleanly on top of #3651/#3652 (executeAggregate carries
context; generateSql renders scope) after those landed on main.

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

analytics: 让 executeAggregate 桥携带 ExecutionContext(#3597 的纵深防御第二层)+ 两处残留无 scope 调用

2 participants