fix(tesseract): project a primary key once in the keys subquery - #11527
Conversation
A measure multiplied by a fan-out join is read through the keys subquery: a DISTINCT grid of the query dimensions plus the primary key of the measure's cube, re-joined to that cube by the key so the measure aggregates once per row. The primary key was projected unconditionally, next to the query dimensions, so a query that asks for that key as a dimension got two columns under one alias and the re-join's reference to it became ambiguous — Postgres rejects the query with `column reference "..." is ambiguous`. Project the key once when it plays both roles. The re-join still resolves to the surviving column: it looks the column up by member symbol, not by alias, so a key projected under a dimension's alias is found too. Covered by a planner integration test against Postgres and a schema-compiler test on the reported model, green under both planners. Fixes #11455 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @waralexrom's task in 4m 27s —— View job Review: looks correct and well-scoped — 0 blocking, 2 non-blocking (1 maintainability, 1 test-coverage)Full review
VerdictThe fix is minimal and lands in the right place. The duplicate projection was pure dead weight: Things I checked that hold up:
Two non-blocking points, both posted inline:
Nice touches: the code comment explains why the skip is safe (the symbol-matching invariant with the re-join), not just what it does; the TS fixture asserts the measure isn't double-counted rather than only that the query doesn't throw; and the reproduction was verified to fail with the fix reverted. |
| let resolved = member.clone().resolve_reference_chain(); | ||
| if keys_subquery | ||
| .schema() | ||
| .all_dimensions() | ||
| .any(|dim| dim.clone().resolve_reference_chain() == resolved) | ||
| { | ||
| continue; | ||
| } |
There was a problem hiding this comment.
Correctness looks right, but this re-implements Schema::find_column_for_member's matching rule (origin_member.resolve_reference_chain() == member.resolve_reference_chain()) against the logical schema instead of asking the physical schema that's actually being built. The comment acknowledges the coupling — the risk is that the two rules drift: if find_column_for_member ever changes (e.g. to also compare granularity or discriminant explicitly), this dedup silently stops matching it and the ambiguity comes back, with no test in default CI to catch it (see the other comment).
SelectBuilder already accumulates result_schema and add_projection_member records SchemaColumn::new(alias, Some(member)) for every projection above, so a small accessor would let this be expressed in terms of the single source of truth:
if select_builder.result_schema().find_column_for_member(member).is_some() {
continue;
}That also covers columns added through paths other than the all_dimensions() loop. Non-blocking — just a suggestion to keep one matching rule instead of two.
| - id: customers.id | ||
| - id: orders.status | ||
| "}; | ||
|
|
There was a problem hiding this comment.
This test can't fail in CI. unit-core in .github/workflows/push.yml:233 runs cargo test --workspace --features cubesqlplanner/integration-cubestore — no integration-postgres — so try_execute_pg returns None and only build_sql(query).unwrap() runs, which succeeds with the bug present (the failure is Postgres-side). The PR description notes this, and it does match the surrounding suite, so it's not a blocker; the schema-compiler test does run in CI (integration:postgres, and for postgres the matrix covers both CUBEJS_TESSERACT_SQL_PLANNER true and false), so the fix isn't unguarded.
Still, the defect here is purely a SQL-shape one and is cheap to pin without a database — tests/common_sql_generation.rs has the pattern (insta::assert_snapshot! over build_sql). A snapshot showing the keys subquery projecting "a__id" once would make unit-core catch a regression directly on the changed line, and would also catch drift between this dedup and find_column_for_member.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #11527 +/- ##
==========================================
+ Coverage 79.46% 83.96% +4.49%
==========================================
Files 480 257 -223
Lines 98861 80919 -17942
Branches 3636 0 -3636
==========================================
- Hits 78563 67940 -10623
+ Misses 19778 12979 -6799
+ Partials 520 0 -520
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:
|
Summary
Fixes #11455 : a query that asks for a cube's
primary_keyas a dimension together with measures from 2+ cubes fails on Postgres withcolumn reference "a__id" is ambiguous. Regression against 1.6.x, and against the legacy planner, which dedupes the same projection.Changes
keys_sub_query.rs: project a primary key once when it is also a query dimension. A measure multiplied by a fan-out join is read through the keys subquery — a DISTINCT grid of the query dimensions plus the primary key of the measure's cube, re-joined to that cube by the key. The key was projected unconditionally next to the query dimensions, so asking for it as a dimension put two columns under one alias and made the re-join'skeys.<pk>reference ambiguous.Schema::find_column_for_member), not by alias, so a key projected under a dimension's alias — a view or reference dimension — is found too. The dedup uses that same symbol comparison.tests::integration::multi_fact::test_multiplied_aggregate_grouped_by_own_primary_key(planner, against Postgres) andpackages/cubejs-schema-compiler/test/integration/postgres/primary-key-multi-fact.test.ts(the reported model, verifying the multiplied measure is not double-counted).Before / after
The keys subquery for the multiplied measure, before:
after:
Testing
cargo test --features integration-postgres— 1207 passed, 0 failed, no snapshot drift.column reference "a__id" is ambiguous.CUBEJS_TESSERACT_SQL_PLANNER=trueand=false— the legacy planner already dedupes this inBaseQuery.keyDimensions, so the fix restores parity.sql-generation,multi-fact-join,calculated-measure-multi-fact,sub-query-dimensions,views,multi-stage): 147 passed, 1 skipped.Note: the planner test only guards the bug under
--features integration-postgres—build_sql()itself succeeds, the failure comes from Postgres. That matches the existing pattern in that suite.🤖 Generated with Claude Code