Skip to content

feat(api-gateway): Expose usedPreAggregations on data responses - #11591

Open
MazterQyou wants to merge 1 commit into
masterfrom
api-gateway/used-pre-aggregations-cubesql
Open

feat(api-gateway): Expose usedPreAggregations on data responses#11591
MazterQyou wants to merge 1 commit into
masterfrom
api-gateway/used-pre-aggregations-cubesql

Conversation

@MazterQyou

Copy link
Copy Markdown
Member

Check List

  • Tests have been run in packages where changes have been made if available
  • Linter has been run for changed code
  • Tests for the changes have been added if not covered yet
  • Docs have been added / updated if required

Description of Changes Made

This PR exposes pre-aggregation identity as usedPreAggregations on the REST, GraphQL and SQL API data responses, without the refresh key values, so clients can match a result to the pre-aggregation build behind it. Related tests are included.

Two behavior changes worth noting:

  • reverses the earlier decision that pre-aggregation names must not reach ordinary consumers; the test asserting it was rewritten
  • the queriesWithPreAggregations telemetry counter was structurally always 0 in production because it read a dev-only field, and now counts real usage

Signed-off-by: Alex Qyoun-ae <4062971+MazterQyou@users.noreply.github.com>
@MazterQyou
MazterQyou requested review from a team and keydunov as code owners August 18, 2026 20:03
@mintlify

mintlify Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
cubed3 🟢 Ready View Preview Aug 18, 2026, 8:04 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@github-actions github-actions Bot added client:core Issues relating to the JavaScript client SDK rust Pull requests that update Rust code javascript Pull requests that update Javascript code labels Aug 18, 2026
@claude

claude Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MazterQyou's task in 5m 9s —— View job


Review: solid, well-documented change — 1 design question (public targetTableName), 3 low-severity issues

Full review — click to expand
  • Gather context / read diff
  • Review JS/TS changes (api-gateway, query-orchestrator, client-core)
  • Review Rust changes (cubesql scan.rs, transport, backend-native)
  • Review docs & tests
  • Check existing review threads / post findings

What the change does

Threads pre-aggregation identity from the orchestrator (preAggregationId + type stamped in PreAggregations.loadAllPreAggregationsIfNeeded, surfaced by QueryOrchestrator.fetchQuery) all the way to REST/GraphQL/SQL-API responses, with a whitelist projection in the gateway that keeps refreshKeyValues dev-only. The cubesql side carries it as JSON in Arrow schema metadata plus a per-span union, mirroring the existing lastRefreshTime / external plumbing.

What's good

  • The whitelist approach in publicUsedPreAggregations (gateway.ts:147) is the right direction — new orchestrator fields default to not exposed rather than leaking by omission. The undefined-dropping filter with the comment about the native pipeline turning undefined into JSON null is a genuinely non-obvious detail worth having written down.
  • SpanId::merge_used_pre_aggregations unions rather than overwrites, and the docblock explains why last-write-wins per key is safe. Correctly not modeled on the conservative set_external fold — different semantics, and the difference is explained at the read site in node_export.rs.
  • is_empty_used_pre_aggregations + the None-when-empty span accessor keep "no pre-aggregation" from becoming a useless {} key on every plain SQL result. Tested at both layers.
  • Refactoring the three positional args of build_response_schema into ResultMetadata before adding a fourth was the right call.
  • Test coverage is genuinely good for a cross-language change: Rust unit tests for schema encoding + span merge, JS unit tests for the gateway projection on both the prepareResultTransformData and pushdown branches, and an end-to-end sql.test.ts case asserting the JSONL header round-trip including the empty/absent cases.
  • The queriesWithPreAggregations telemetry fix is a real bug fix (the counter read a dev-only field, so it was structurally 0 in production) and is correct now that the field is populated unconditionally.

Findings

# Severity Where Issue
1 Design question gateway.ts:147 targetTableName (schema name + build version hashes) is now returned to every consumer, including public embedded tokens. preAggregationId + lastUpdatedAt alone satisfy the stated use case; consider keeping the table name dev/playground-only, or gating the projection behind an env flag so operators can restore the old behavior.
2 Low gateway.ts:1989 The dev/playground spread re-assigns response.usedPreAggregations verbatim, so dev responses carry usedPreAggregations: {} for queries that hit no pre-aggregation while production omits the key. Not a regression, but now that the key is public contract, 'usedPreAggregations' in res differs across modes. Rust already normalizes this.
3 Low node_export.rs:440-449 Schema-metadata fallback doesn't apply the empty/null normalization build_response_schema does (from_str("null")Some(Value::Null)), and .ok() swallows parse errors where the equivalent parse in load_data logs a warn!. Defensive only today, since build_response_schema is the sole writer.
4 Low (docs) rest-api/reference.mdx:45 rollupJoin/rollupLambda expand to their referenced rollups in preAggregationDescriptionsFor, so clients never see the lambda/join pre-aggregation itself in usedPreAggregations — worth documenting. Also, the adjacent external bullet claims "present only when the query hit a pre-aggregation", but /load always emits it (this PR's own test asserts false).

Non-blocking observations

  • PreAggregationType is now exported from PreAggregations.ts as 'rollup' \| 'originalSql', while cubejs-client-core's same-named type is 'rollup' \| 'rollupJoin' \| 'rollupLambda' \| 'originalSql'. The narrow one looks correct at runtime (join/lambda descriptions expand to their referenced rollups), so the client-core union is the stale one — but two exported types with the same name and different unions across packages is a trap for the next reader. Worth a comment on one of them.
  • The request.streaming pushdown branch of sqlApiLoad doesn't attach usedPreAggregations, same as lastRefreshTime. The node_export.rs comment documents the gap honestly; the docs say the header "optionally" carries it, which technically covers it, but a reader may expect it in stream mode.
  • No test exercises the GraphQL res.extensions.usedPreAggregations path (which passes through the unredacted object in dev mode). Low value, just noting it's uncovered.

Verification

I read the diff and traced the data path across all three layers but did not execute the test suites — running the api-gateway/native tests needs a compiled cubejs-backend-native binary, and the cd && npx jest invocation was blocked by the sandbox permission prompt, so cargo test / yarn test results here are unverified by me. Everything above is from static reading.

· branch [`api-gateway/used-pre-aggregations-cubesql`](https://github.com/cube-js/cube/tree/api-gateway/used-pre-aggregations-cubesql)

* data the caller cannot otherwise reach. The full object, including them, is
* still returned in dev mode and to the Playground.
*/
function publicUsedPreAggregations(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design/security posture — worth an explicit decision from reviewers.

The docblock explains well why refreshKeyValues stays out, but the flip side isn't argued: targetTableName is now returned to every consumer, and it embeds the pre-aggregation schema name plus the content/structure version hashes of a specific build (e.g. stb_pre_aggs.foo_foo_main20240101_abc_def_1712). For a public embedded-analytics deployment that's internal storage layout leaking to untrusted browsers, and it's exactly what the test this PR rewrote was guarding.

Two things that would de-risk the reversal:

  1. preAggregationId + lastUpdatedAt alone already satisfy the stated use case ("match a result to the build behind it" / "join a chart to the build it is watching"). targetTableName is only needed if the client wants the physical table, which a data-API consumer can't use anyway. Consider dropping it from the public whitelist and keeping it dev/playground-only.
  2. If it must stay, consider gating the whole public projection behind an env flag (defaulting to on or off, your call) so operators who relied on the previous behavior can restore it without pinning a version.

Either way, the security-relevant part of this change (what ordinary consumers can now see) deserves a line in the docs, not just the usedPreAggregations field description.

// Identity of the pre-aggregations behind this result, so a client can
// join it to the build it is waiting on. The dev-mode block below
// replaces it with the unredacted object.
usedPreAggregations: publicUsedPreAggregations(response.usedPreAggregations),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor consistency gap: publicUsedPreAggregations deliberately collapses {} to undefined ("a query that hit no pre-aggregation reports nothing rather than an empty object"), but the dev/playground spread just below re-assigns response.usedPreAggregations verbatim. Since the orchestrator builds that object with R.fromPairs over the used-tables list, it's {} for every query that hit no pre-aggregation — so in dev mode the response carries usedPreAggregations: {} while in production the key is absent.

Not a regression (dev mode did this before too), but now that the key is part of the public contract, a client written against 'usedPreAggregations' in res behaves differently in dev vs prod. Cheap to align:

usedPreAggregations: Object.keys(response.usedPreAggregations || {}).length
  ? response.usedPreAggregations
  : undefined,

The Rust side already has the same normalization (is_empty_used_pre_aggregations), so this would make all three layers agree.

Comment on lines +440 to +449
let used_pre_aggregations = span_used_pre_aggregations.or_else(|| {
stream
.schema()
.metadata()
.get("usedPreAggregations")
.and_then(|v| serde_json::from_str(v).ok())
});
if let Some(used_pre_aggregations) = used_pre_aggregations {
schema_response.insert("usedPreAggregations".into(), used_pre_aggregations);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two small robustness gaps in the schema-metadata fallback:

  1. It doesn't apply the empty/null normalization that build_response_schema does. serde_json::from_str("null")Some(Value::Null) and "{}"Some({}), either of which would put "usedPreAggregations": null / {} on the JSONL header — the exact thing is_empty_used_pre_aggregations was added to prevent. Today only build_response_schema writes this key and it pre-filters, so this is defensive, but the invariant lives in a different crate from the reader. Exporting is_empty_used_pre_aggregations (or a parse_used_pre_aggregations(&str) -> Option<Value> helper next to it in scan.rs) and calling it here would keep the normalization in one place.

  2. .ok() drops parse errors silently, while the same parse in load_data logs warn!("Unable to parse usedPreAggregations of a load response: {}", e). A malformed blob here would just make the field vanish with no trace — worth a matching warn!.

- `external` - A boolean indicating whether the query was served from a
pre-aggregation in Cube Store. Present only when the query hit a pre-aggregation.
- `usedPreAggregations` - The pre-aggregations this result was served from, keyed
by pre-aggregation table name. Each entry carries `preAggregationId` (the name in

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two doc notes while this section is being touched:

  1. For a rollupJoin / rollupLambda pre-aggregation, preAggregationDescriptionsFor in the schema compiler expands to the referenced rollups, so the entries a client sees are the underlying rollups (their ids and type: "rollup"), never the lambda/join pre-aggregation itself. Worth one sentence — otherwise a user who defined Orders.lambda and looks for it in usedPreAggregations will conclude it's a bug.

  2. The external bullet two lines above claims "Present only when the query hit a pre-aggregation", but /load always emits it (the test in this PR asserts external === false for a non-pre-agg query). Only the SQL API JSONL header omits it when false. Pre-existing inaccuracy, but adjacent and cheap to fix in the same pass.

@codecov

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.90780% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.54%. Comparing base (8b6e94c) to head (05be8da).

Files with missing lines Patch % Lines
rust/cubesql/cubesql/src/compile/engine/df/scan.rs 89.65% 9 Missing ⚠️
...st/cubesql/cubeclient/src/models/v1_load_result.rs 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #11591      +/-   ##
==========================================
- Coverage   84.06%   79.54%   -4.52%     
==========================================
  Files         257      484     +227     
  Lines       81582    99864   +18282     
  Branches        0     3673    +3673     
==========================================
+ Hits        68581    79441   +10860     
- Misses      13001    19875    +6874     
- Partials        0      548     +548     
Flag Coverage Δ
cube-backend 59.21% <100.00%> (?)
cubesql 84.06% <92.53%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

client:core Issues relating to the JavaScript client SDK javascript Pull requests that update Javascript code rust Pull requests that update Rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant