fix(tesseract): resolve pre-agg refs interpolating the cube - #11602
Conversation
…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.
|
Claude finished @waralexrom's task in 3m 44s —— View job Review complete — approach looks right; 1 medium (panic path), 3 low, 2 discussion pointsFull review
OverallThe 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 Findings
Discussion (not blocking)
TestingCoverage 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 |
Codecov Report✅ All modified and coverable lines are covered by tests. 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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 finished @waralexrom's task in 1m 54s —— View job Re-review of
|
| 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
- 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. ${CUBE}.created_at.day— now pinned bytest_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.- 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.
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_datestays literal text, leaving no member symbol behind.The damage differed per reference kind: a
time_dimensionin that form failed every query on the cube (the error is raised while compiling all pre-aggregation candidates), while ameasures/dimensions/segmentsentry 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.PreAggregationsCompilerresolves the recovered paths throughSymbolPath::parse+add_{dimension,measure,segment}_evaluator_by_path, fortime_dimension,time_dimensions,measures,dimensionsandsegmentsalike.'<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.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 (
useNativeSqlPlannertoggled). Before:${CUBE}.membermatched 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 asgetCubeFields(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.packages/cubejs-schema-compiler/test/unit/pre-agg-interpolated-cube-refs.test.ts): the customer's shape — a helper-builtmeasures/dimensionsplustimeDimension: (CUBE) => `${CUBE}.issued_date`— asserted throughpreAggregationsDescription()against both planners. Verified it catches the bug: with the Rust change reverted the two Tesseract cases fail and the legacy ones pass.sql_call.rsover 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 inpre_aggregations_compiler.rsover compiled pre-aggregations, including a list mixing both forms and the error paths.{CUBE}.memberis 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. Fullcubejs-schema-compilerunit suite: 758 passed, 110 snapshots passed.