Skip to content

feat(cubesql): Support joining ungrouped queries with SQL push down#11315

Open
MazterQyou wants to merge 1 commit into
masterfrom
cubesql/ungrouped-limited-join-grouped
Open

feat(cubesql): Support joining ungrouped queries with SQL push down#11315
MazterQyou wants to merge 1 commit into
masterfrom
cubesql/ungrouped-limited-join-grouped

Conversation

@MazterQyou

@MazterQyou MazterQyou commented Jul 21, 2026

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 supports SQL push down for joins where either side is an ungrouped (raw) query, e.g. a self-join of a CTE selecting dimensions, by planning them as plain SQL joins of standalone subqueries instead of failing with Use __cubeJoinField to join Cubes. Related tests are included.

@MazterQyou
MazterQyou requested a review from a team as a code owner July 21, 2026 17:37
@github-actions github-actions Bot added the rust Pull requests that update Rust code label Jul 21, 2026
@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MazterQyou's task in 1m 35s —— View job


Code Review

Verdict: LGTM overall — small, well-scoped rewrite rule that closely mirrors the existing wrapper-push-down-ungrouped-join-grouped pattern. A few minor observations below; nothing blocking.

Review details

Correctness

  • The rule's invariant (WrapperReplacerContextPushToCube:false + CubeScanUngrouped:true ⇒ scan has LIMIT or OFFSET) holds — transform_wrap_cube_scan in cube_scan_wrapper.rs:173 computes push_to_cube_out = ungrouped && has_no_limit_or_offset, so the only way to end up in this state is when limit/offset is present. The pattern implicitly relies on this; a brief pointer to transform_wrap_cube_scan in the comment near rust/cubesql/cubesql/src/compile/rewrite/rules/wrapper/join.rs:495-497 would make the coupling more discoverable.
  • is_subquery_join_type_supported(..., false) correctly forces the non-push-to-Cube path since the LHS is materialized as a standalone subquery — consistent with the grouped-join-grouped handling.
  • build_join_expr and the right_join_alias derivation follow the same shape as the existing rules, including the same TODO ("what's a proper way to find table expression alias?") — inherited limitation, fine to defer.
  • ?left_scan_join_hints, ?left_grouped_subqueries, ?left_in_projection are captured on LHS but unused. That's OK (the scan node itself is reproduced verbatim via limited_ungrouped_scan in the RHS), just noting they're intentional catch-alls.

Test coverage (test_join_limited_ungrouped_with_grouped)

The test covers the golden path well (INNER JOIN, LIMIT on ungrouped side, grouped MEASURE on the other). Consider adding follow-up cases:

  • OFFSET-only on the ungrouped side (PR description says offset is supported, but the test only exercises LIMIT).
  • LEFT JOIN where the ungrouped-limited side is the outer — verifies is_subquery_join_type_supported selects the right variant for the false-push-to-Cube path.
  • A negative test where data sources differ (should fall through to the "Use __cubeJoinField" error), so a future refactor of the ?input_data_source matching doesn't silently regress.

Style / minor

  • rust/cubesql/cubesql/src/compile/rewrite/rules/wrapper/join.rs:1652-1653 clones left_join_on / right_join_on up front, while transform_grouped_join_grouped clones only inside the inner loop. Not a bug — the inner loop only iterates over join types once — but worth aligning for consistency.
  • The rule body is ~230 lines of near-boilerplate wrapper_pullup_replacer calls with identical context blocks. A small helper (e.g. pullup_with_ctx(node, ctx_args)) could tighten this and its two sibling rules significantly in a follow-up.
  • Comment at join.rs:527-528: "Data sources must match for both sides / TODO support unrestricted data source on one side" — same TODO as in the two sibling rules; tracking these together in a single issue may be worthwhile.
• Branch: [`cubesql/ungrouped-limited-join-grouped`](https://github.com/cube-js/cube/tree/cubesql/ungrouped-limited-join-grouped)

@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.74874% with 43 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.58%. Comparing base (ae248c4) to head (82e1d8b).
⚠️ Report is 4 commits behind head on master.

Files with missing lines Patch % Lines
...c/compile/rewrite/rules/wrapper/wrapper_pull_up.rs 53.48% 20 Missing ⚠️
...t/cubesql/cubesql/src/compile/test/test_wrapper.rs 95.92% 9 Missing ⚠️
...cubesql/cubesql/src/compile/test/test_cube_join.rs 87.50% 7 Missing ⚠️
...src/compile/engine/df/optimizers/sort_push_down.rs 88.88% 2 Missing ⚠️
.../cubesql/src/compile/rewrite/rules/wrapper/join.rs 98.51% 2 Missing ⚠️
...l/cubesql/src/compile/test/test_cube_join_views.rs 93.33% 2 Missing ⚠️
...cubesql/src/compile/test/test_cube_join_grouped.rs 98.38% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #11315      +/-   ##
==========================================
- Coverage   83.84%   79.58%   -4.27%     
==========================================
  Files         257      473     +216     
  Lines       80408    98537   +18129     
  Branches        0     3549    +3549     
==========================================
+ Hits        67422    78418   +10996     
- Misses      12986    19610    +6624     
- Partials        0      509     +509     
Flag Coverage Δ
cube-backend 59.50% <ø> (?)
cubesql 83.92% <92.74%> (+0.07%) ⬆️

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.

@MazterQyou MazterQyou changed the title feat(cubesql): Support joining limited ungrouped query with grouped query feat(cubesql): Support joining ungrouped queries with SQL push down Jul 22, 2026
@MazterQyou
MazterQyou force-pushed the cubesql/ungrouped-limited-join-grouped branch from b0ad4c9 to b59ec24 Compare July 22, 2026 17:07
@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MazterQyou's task in 2m 20s —— View job


Code Review

Verdict: LGTM. Well-scoped extension that mirrors the existing grouped-join-grouped / ungrouped-join-grouped patterns. A few small observations below; nothing blocking.

Review details

Correctness

  • transform_ungrouped_join_ungrouped (rust/cubesql/cubesql/src/compile/rewrite/rules/wrapper/join.rs:1483) — the guard rejecting the rule unless at least one side has WrapperReplacerContextUngroupedScan:true correctly avoids overlapping with wrapper-push-down-grouped-join-grouped. The rest of the transform closely mirrors the sibling and looks right.
  • Nested-loop bailout style: on any failing check (is_subquery_join_type_supported, member checks, build_join_expr) the closure return falses rather than continue. This is fine here because there is only one output variant (non-push-to-Cube — LHS is materialized as a standalone subquery), unlike transform_grouped_join_grouped which needs continue at join.rs:1671 to give the non-push-to-Cube variant a chance. Worth a one-line comment explaining why the pattern differs from the sibling — future readers will notice the asymmetry.
  • __cubeJoinField name check at join.rs:1540 uses eq_ignore_ascii_case. The schema-defined name is exact (__cubeJoinField), so a case-insensitive check is defensive but not strictly necessary. Also — note that this string-based check is a fallback for cases where are_join_members_supported misses the qualifier; if that ever happens for other virtual members, they will silently be treated as real columns. Consider whether virtual/synthetic members deserve a more principled predicate on the member metadata rather than a name literal.
  • wrapper_pull_up.rs:524-559 — the extended non-trivial-nesting check (filter/order/window inequality, inner joins/limit/offset presence) is only in the Projection arm. The Aggregate arm still has the existing TODO write rules for non trivial wrapped aggregate — worth checking whether an aggregate on top of a limited/joined ungrouped inner could now slip through unrewrapped, or if that combination is unreachable in practice.
  • sort_push_down.rs:115-137 — the new "sort can reach scan" guard is correct: pushing a sort past a Filter into a subquery that is itself a Join produces invalid ORDER BY semantics in the emitted SQL. The recursive walk stops at Join/CrossJoin, which is the right boundary. Minor: the default _ => true arm treats Aggregate/Window/Sort/Limit as "reachable"; that's consistent with the existing behavior in this file, but the docstring's phrasing ("must not get stuck directly above a Join") is a bit misleading — the walk does recurse through Projection/Filter, so "directly above" isn't quite right. Consider "the sort must not end up above a Join or CrossJoin after recursion through Projection/Filter."

Test coverage

  • Good: test_wrapper_ungrouped_join_ungrouped (self-join CTE + LIMIT), test_join_limited_ungrouped_with_grouped (limit stays inside ungrouped subquery), test_join_cubes_on_regular_field_pushdown, and the updated view-join tests exercising the new "join two standalone subqueries" fallback.
  • Missing / would strengthen:
    • OFFSET-only ungrouped side (the guard treats limit || offset symmetrically but only LIMIT is exercised).
    • LEFT/RIGHT/FULL JOIN where the ungrouped side is on the outer/inner — verifies is_subquery_join_type_supported(..., false) selects the right variant. Right/Full are only permitted on the non-push-to-Cube path, which is exactly this rule's output.
    • Data-sources-differ negative test: ?input_data_source matching is load-bearing; a regression there would silently pass otherwise (or produce broken SQL).
    • __cubeJoinField on both sides, no LIMIT — verifies the name-string rejection still fires when member-based rejection covers it too (defense in depth).

Style / minor

  • The new rule body is ~230 lines of near-boilerplate wrapper_pullup_replacer(..., wrapper_replacer_context("?left_alias_to_cube", "WrapperReplacerContextPushToCube:false", "WrapperReplacerContextInProjection:false", "?left_cube_members", "?out_grouped_subqueries", "WrapperReplacerContextUngroupedScan:false", "?input_data_source")) calls. This is the third rule with this exact context shape (the two prior claude reviews on wrapper-push-down-limited-ungrouped-with-grouped and wrapper-push-down-grouped-join-grouped flagged the same). A small helper macro/fn would meaningfully tighten join.rs in a follow-up.
  • wrapper_pull_up.rs — the transform_check_wrapped_select_alias signature grows to 16 vars; passing this many &'static strs is unwieldy. Grouping outer/inner into a struct is a candidate for a small refactor.
  • test_cube_join_views.rs — the assert_not_merged_subquery_join helper is nice; the callers that then also assert LEFT JOIN ( in the wrapped SQL do so ad-hoc. Consider folding that assertion into the helper as an option.
• Branch: [`cubesql/ungrouped-limited-join-grouped`](https://github.com/cube-js/cube/tree/cubesql/ungrouped-limited-join-grouped)

Signed-off-by: Alex Qyoun-ae <4062971+MazterQyou@users.noreply.github.com>
@MazterQyou
MazterQyou force-pushed the cubesql/ungrouped-limited-join-grouped branch from b59ec24 to 82e1d8b Compare July 22, 2026 17:28
@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MazterQyou's task in 1m 59s —— View job


Code Review

Verdict: LGTM. The synchronize picks up all the test-coverage gaps the prior two reviews flagged (OFFSET-only, LEFT/RIGHT/FULL JOIN variants, __cubeJoinField defense-in-depth, unsupported-template FULL JOIN, and a data-sources-differ negative case with a new get_test_tenant_ctx_with_split_data_sources helper). Nothing blocking.

Review details

Correctness

  • Ungrouped-scan guard (rust/cubesql/cubesql/src/compile/rewrite/rules/wrapper/join.rs:1533-1546): the left_ungrouped || right_ungrouped requirement correctly prevents overlap with wrapper-push-down-grouped-join-grouped. The rule's output eclass has WrappedSelectPushToCube:false + WrapperReplacerContextUngroupedScan:false, which cleanly matches the "join of standalone subqueries" shape.
  • __cubeJoinField name-based reject (join.rs:1571-1578): defensive against qualifier-mismatch cases where are_join_members_supported can't identify a virtual member. test_join_cubes_on_cube_join_field_no_limit_merged and test_join_cubes_on_cube_join_field_with_limit_error cover both success (no LIMIT → merged) and failure (LIMIT → rejected) paths. eq_ignore_ascii_case is over-defensive vs. the schema's exact literal but harmless.
  • Control flow — return false vs continue: this rule has only the non-push-to-Cube output variant, so return false on any check inside the nested for loops is fine. The sibling transform_grouped_join_grouped uses continue at join.rs:1671 because it needs to give the non-push variant a chance when the push variant rejects a join type. Worth a one-line comment near join.rs:1560-1568 explaining the asymmetry — a future reader diffing the two will notice it.
  • Non-trivial nesting check (rust/cubesql/cubesql/src/compile/rewrite/rules/wrapper/wrapper_pull_up.rs:524-559): correctly extended so that identical projection exprs still count as non-trivial when the inner select carries filter/order/window differences, joins, limit, or offset. Only in the Projection arm — the Aggregate arm still has the pre-existing TODO write rules for non trivial wrapped aggregate; worth confirming (in a follow-up) that an aggregate over a limited-and-joined ungrouped inner cannot silently slip through unrewrapped.
  • sort_can_reach_scan (rust/cubesql/cubesql/src/compile/engine/df/optimizers/sort_push_down.rs:290-297): correct — the recursion through Projection/Filter with Join/CrossJoin as the stop-boundary is exactly the right shape. Minor: the docstring says "not get stuck directly above a Join", but the walk descends through Projection/Filter first, so "directly above" is slightly misleading; "must not end up above a Join or CrossJoin after recursion through Projection/Filter" would read more accurately.

Test coverage

The synchronize adds all previously-suggested tests:

  • test_wrapper_ungrouped_join_ungrouped_offset — OFFSET-only ungrouped side.
  • test_wrapper_ungrouped_left_join_ungrouped, test_wrapper_ungrouped_right_join_grouped, test_wrapper_ungrouped_full_join_ungrouped — all three outer variants (RIGHT/FULL only reachable on the non-push-to-Cube path).
  • test_wrapper_ungrouped_full_join_without_template — negative test for missing join_types/full.
  • test_wrapper_ungrouped_join_ungrouped_different_data_sources?input_data_source load-bearing check.
  • test_join_cubes_on_cube_join_field_no_limit_merged — defense-in-depth for the __cubeJoinField reject.
  • Updated view-join tests exercising the new "join two standalone subqueries" fallback via assert_not_merged_subquery_join.

Good coverage.

Style / minor

  • The three sibling rules (wrapper-push-down-grouped-join-grouped, -limited-ungrouped-with-grouped, -ungrouped-join-ungrouped) now share ~230 lines each of identical wrapper_pullup_replacer(..., wrapper_replacer_context(...)) boilerplate. A small helper (e.g. pullup_ctx!(node, alias, members, subqueries, ds)) would meaningfully tighten join.rs. Follow-up.
  • transform_check_wrapped_select_alias now takes 22 &'static str var names (wrapper_pull_up.rs:454-497). At this size, grouping the outer/inner var bundles into a struct would be worth a small refactor.
  • assert_not_merged_subquery_join in test_cube_join_views.rs:158-171 is a nice consolidation. Some callers still separately assert "LEFT JOIN (" in the wrapped SQL — folding the join-kind assertion into the helper as an optional param would tighten the tests further.
• Branch: [`cubesql/ungrouped-limited-join-grouped`](https://github.com/cube-js/cube/tree/cubesql/ungrouped-limited-join-grouped)

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

Labels

rust Pull requests that update Rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant