Skip to content

fix(tesseract): resolve pre-agg refs interpolating the cube - #11602

Merged
waralexrom merged 2 commits into
masterfrom
tesseract-two-level-arrow-member-refs
Aug 24, 2026
Merged

fix(tesseract): resolve pre-agg refs interpolating the cube#11602
waralexrom merged 2 commits into
masterfrom
tesseract-two-level-arrow-member-refs

Conversation

@waralexrom

Copy link
Copy Markdown
Member

Summary

A pre-aggregation whose reference is written by interpolating the whole cube — time_dimension: (CUBE) => `${CUBE}.issued_date` — works on the legacy planner but not on Tesseract, so upgrading to the native planner breaks such a model. Legacy evaluates a reference function to a string, where ${CUBE} stringifies to the cube name and the result happens to be a valid member path; Tesseract compiles the same function as a SQL template, where ${CUBE} becomes a cube-name dependency and .issued_date stays literal text, leaving no member symbol behind.

The damage differed per reference kind: a time_dimension in that form failed every query on the cube (the error is raised while compiling all pre-aggregation candidates), while a measures / dimensions / segments entry was silently dropped — the rollup is still built and refreshed by the JS side, but never matched again.

Fixes CORE-559.

Changes

  • SqlCall::reference_items() reads a compiled reference declaration element by element, in declaration order, reporting each as a member symbol, a member path recovered from a cube-name interpolation (the cube reference's path plus the literal segments — join paths included), or unresolved.
  • PreAggregationsCompiler resolves the recovered paths through SymbolPath::parse + add_{dimension,measure,segment}_evaluator_by_path, for time_dimension, time_dimensions, measures, dimensions and segments alike.
  • An element that names no member is now reported as '<member>' not found for path '<path>' in pre-aggregation '<cube>.<name>' instead of vanishing from the compiled description — the same message legacy raises for the same model.
  • Declaration order of a reference list is preserved when it mixes both forms; join-hint order and lambda member matching read those lists positionally.
  • Test fixtures: mock pre-aggregation reference lists accept template elements, so a list mixing interpolated and plain references can be expressed.

Reference kinds that reach Rust as plain strings (reduce_by, group_by, add_group_by, time_shift) are resolved on the JS side and never had this gap.

Testing

Behaviour was measured on both planners before and after, on the same model and query (useNativeSqlPlanner toggled). Before: ${CUBE}.member matched the rollup on legacy, and on Tesseract raised 'created_at' not found for path 'orders.created_at' … for a time dimension / silently failed to match for measures and dimensions. Every other form (CUBE.x, ${CUBE.x}, and helper indirection such as getCubeFields(CUBE, [...])) already worked on both. A granularity-suffixed name (${CUBE}.created_at_day) errors on both planners, before and after — that parity is intentional and kept.

  • New JS parity test (packages/cubejs-schema-compiler/test/unit/pre-agg-interpolated-cube-refs.test.ts): the customer's shape — a helper-built measures/dimensions plus timeDimension: (CUBE) => `${CUBE}.issued_date` — asserted through preAggregationsDescription() against both planners. Verified it catches the bug: with the Rust change reverted the two Tesseract cases fail and the legacy ones pass.
  • Rust unit tests: 10 in sql_call.rs over the template analysis (join paths, whitespace, per-element lists, declaration order in a mixed list, and the negative shapes — expressions, {CUBE.sql()}, bare placeholder, out-of-bounds index); 18 in pre_aggregations_compiler.rs over compiled pre-aggregations, including a list mixing both forms and the error paths.
  • Rust integration tests (Postgres): a rollup declaring every reference as {CUBE}.member is picked for the same queries as its symbol-form twin and reads back the same rows, at matching and at coarser granularity. Rows are pinned by snapshots and were checked by hand against the seed. Verified these fail with the change reverted.
  • cargo test -p cubesqlplanner --features integration-postgres: 1238 passed, 0 failed. Full cubejs-schema-compiler unit suite: 758 passed, 110 snapshots passed.

…cube

A pre-aggregation reference written as (CUBE) => `${CUBE}.issued_date`
stringifies the cube, so the member arrives as literal text next to a cube
reference rather than as a member symbol, and the reference resolved to no
member at all: a time_dimension in that form failed every query on the cube,
while a measures / dimensions / segments entry was dropped, leaving a rollup
that is built and refreshed but never matched.

Read such an element back as a member path — the cube reference's path
followed by the literal segments — and resolve it against the data model.
Reference elements are now read in declaration order, and an element naming no
member is reported with the member and pre-aggregation names instead of being
dropped.
@waralexrom
waralexrom requested a review from a team as a code owner August 19, 2026 16:13
@github-actions github-actions Bot added rust Pull requests that update Rust code javascript Pull requests that update Javascript code labels Aug 19, 2026
@claude

claude Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Claude finished @waralexrom's task in 3m 44s —— View job


Review complete — approach looks right; 1 medium (panic path), 3 low, 2 discussion points

Full review
  • Gather context (diff, existing review threads — none pre-existing)
  • Review sql_call.rs reference_items logic
  • Review pre_aggregations_compiler.rs changes
  • Review tests (Rust unit + integration, JS parity)
  • Post review

Overall

The diagnosis is precise and the fix sits at the right layer: legacy stringifies a reference function while Tesseract compiles it as a template, so recovering cube-name dep + literal segments back into a member path in SqlCall::reference_items() and resolving it through SymbolPath::parse is the minimal change that restores parity. Turning a reference that names no member into the same error legacy raises — instead of silently dropping it from the compiled description — is a real improvement: a dropped dimension makes a rollup look more general than it is, which is a wrong-results failure mode rather than a missed-match one. Deps (regex, lazy_static) are already in Cargo.toml, SqlCallReference is re-exported via pub use sql_call::*, and the docs on the new functions explain the why, not just the what.

Findings

Sev Location Issue
Medium sql_call.rs:592-603 taken[index] / deps[index] index unchecked with template-derived indices → panic on an out-of-range placeholder in an element that also names a symbol. The sibling branch handles that shape gracefully. inline
Low pre_aggregations_compiler.rs:605 `map_err(
Low sql_call.rs:646 format! needle rebuilt inside the while loop. inline
Low sql_generation.rs:1730-1738 Row-parity + snapshot assertions become a no-op (None == None) when PG execution is off. inline

Discussion (not blocking)

  1. Newly fatal errors for measures/dimensions/segments. Pre-agg candidates are compiled for every query on the cube, so a model with one oddly-shaped reference that previously just failed to match now fails all queries on that cube. For time_dimension this is pre-existing Tesseract behaviour and matches legacy, so the delta is only the three list kinds. I think erroring is the right call (silent drop ⇒ over-general rollup), but it's a behaviour change that can surface as "upgrade broke my queries" — worth a changelog/release note, and worth confirming legacy really throws for the expression shape covered by test_reference_that_names_no_member_returns_error rather than tolerating it.

  2. Asymmetry between the two element shapes. An element that wraps a member symbol in an expression (`${CUBE.status} = 'x'`) still yields the symbol silently, while the same expression written over the cube (`${CUBE}.status = 'x'`) is now Unresolved and errors. Backwards-compatible, and probably deliberate, but the two shapes mean the same thing to a user — a short note in the reference_items doc comment would save the next reader the trace.

  3. Untested edge: `${CUBE}.created_at.day` passes the regex and SymbolPath::parse resolves it as a dimension with a granularity, which then meets static_data.granularity in time_dimension handling. Unlike created_at_day (covered, errors on both planners) this shape isn't tested — worth a case pinning whatever it does today.

Testing

Coverage is strong and well-targeted: the Rust unit tests exercise the template analysis directly (join paths, whitespace, mixed lists, declaration order, and the negative shapes including {CUBE.sql()} and out-of-bounds), the compiler tests assert both the resolved-symbol and error paths, the integration tests pin an interpolated rollup against its symbol-form twin at two granularities, and the JS test asserts both planners through preAggregationsDescription(). The stated revert-check on each layer is the part that makes the suite credible. Only gaps: the medium finding above has no test, and the integration snapshot can silently not run (row 4).

· branch `tesseract-two-level-arrow-member-refs`

Comment thread rust/cube/cubesqlplanner/cubesqlplanner/src/planner/sql_call.rs
Comment thread rust/cube/cubesqlplanner/cubesqlplanner/src/planner/sql_call.rs Outdated
@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.09%. Comparing base (8b6e94c) to head (d17e4ce).
⚠️ Report is 18 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #11602      +/-   ##
==========================================
+ Coverage   84.06%   84.09%   +0.03%     
==========================================
  Files         257      257              
  Lines       81582    81789     +207     
==========================================
+ Hits        68581    68784     +203     
- Misses      13001    13005       +4     
Flag Coverage Δ
cubesql 84.09% <ø> (+0.03%) ⬆️

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.

An element that names a member through one placeholder while carrying an index
the recorded dependencies don't cover — `{arg:0} || {arg:7}` — indexed the
dependency list unchecked and panicked instead of reading what it could. Every
index now goes through `get`, and one it doesn't cover names nothing.

Alongside it: a failure to reach the data model while resolving a recovered
path is passed through as it is rather than reported as a missing member, and
the placeholder needle is built once per element instead of once per match.

Pins two shapes that had no coverage: an out-of-range index next to a member,
and a granularity named inside the reference (`${CUBE}.created_at.day`), which
is rejected exactly as the equivalent symbol reference is. The integration
helper now states that its row assertions do run when Postgres execution is
enabled.
@claude

claude Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Claude finished @waralexrom's task in 1m 54s —— View job


Re-review of d17e4ce — all 4 prior findings addressed, no new issues; ready to merge

Full review
  • Read the d17e4ce delta
  • Verify each prior finding is addressed
  • Check the new tests
  • Resolve stale threads (4 resolved) / post follow-up

Prior findings

Sev Finding Status
Medium taken[index] / deps[index] panic on out-of-range placeholder Fixed. sql_call.rs:599 now goes through self.deps.get(index).and_then(as_symbol) with a let … else { continue }; taken[index] afterwards is provably in-bounds since taken.len() == deps.len(). test_placeholder_out_of_bounds_next_to_a_member_is_skipped pins the exact shape from the report ({arg:0} || {arg:7} → just symbol:orders.status).
Low map_err(|_| …) swallowed the real SymbolPath::parse cause Fixed. Only CubeErrorCauseType::User is rewritten to the legacy-parity message; Internal / NeonThrow pass through untouched. Checked SymbolPath::resolve_parts — its resolution failures (Cannot resolve: x, Empty path) are CubeError::user, while an evaluator call failing mid-walk is not, so the split lands where intended.
Low format! needle rebuilt per loop iteration Fixed. Hoisted. Also cleaner: start + needle.len() replaces the hand-computed + ARG_PREFIX.len() + 2 — same value, one fewer thing to keep in sync.
Low Integration row-parity assertion could be a green no-op Fixed. assert!(interpolated_result.is_some()) under #[cfg(feature = "integration-postgres")], so a broken PG harness fails loudly instead of skipping the snapshot.

Discussion points

  1. Shape asymmetry — addressed with the doc note on reference_items: "An element wrapping a member reference in an expression still yields that member, since the member is a dependency of its own — only the cube-name form has nothing to fall back on." That's the trace the next reader needed.
  2. ${CUBE}.created_at.day — now pinned by test_granularity_inside_the_reference_is_rejected_like_the_symbol_form, which asserts the interpolated and symbol forms produce the same message (Pre-aggregation time dimension must be a dimension). Asserting message equality rather than just "both fail" is the stronger form of that test.
  3. Newly fatal errors for measures/dimensions/segments — still stands as a release-note item, not a code issue. A model that previously had one silently-dropped reference (rollup built and refreshed, never matched) will now fail every query on that cube. I still think erroring is right — a dropped dimension makes a rollup look more general than it is, which is a wrong-results mode — but it's the kind of change that reads as "upgrade broke my queries" without a note.

New issues

None. I re-read reference_items and the compiler path in full against the delta and found nothing further.

· branch `tesseract-two-level-arrow-member-refs`

@waralexrom
waralexrom merged commit cc16c17 into master Aug 24, 2026
156 of 159 checks passed
@waralexrom
waralexrom deleted the tesseract-two-level-arrow-member-refs branch August 24, 2026 09:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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.

2 participants