[pull] master from cube-js:master - #690
Merged
Merged
Conversation
* test(cubesql): Cover UNION of SELECT DISTINCTs in limitless post processing A UNION of two SELECT DISTINCTs over different cubes cannot be pushed down: the branches read different cubes, so the UNION only runs in DataFusion, and the per-branch DISTINCTs collapse into the one above it, leaving each branch an ungrouped scan of raw rows. Pin that shape - the plan it compiles to by default, that the check rejects it, that grouping each branch does not change the verdict, and that a limit on each branch does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xo5DArPYy2Q7ER7HyMo1Ao * test(cubesql): Pin where an outer limit reaches a union's branches UnionSortLimitPushDown copies a limit above a UNION into each of its inputs, so UNION ALL under a limit ends up with bounded scans. A plain UNION dedups above the branches, the limit cannot move through it, and the branches stay unlimited. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xo5DArPYy2Q7ER7HyMo1Ao * feat(cubesql): Push UNION down to the data source A UNION of queries that all reach the same data source had no representation in the wrapper, so the set operation always ran in DataFusion on top of Cube queries the row limit had already truncated. `SELECT DISTINCT ... UNION SELECT DISTINCT ...` over two views is the shape this shows up as, and it answers with values silently missing. Add a WrappedUnion node the rewriter can put a union into, a rule that moves the wrappers of every query into one wrapper around them, and a rule that folds the DISTINCT of a plain UNION into it rather than leaving the deduplication above. Anything above the union - a projection, a filter - is then pushed into a select that reads from it, and the row cap bounds the result of the whole operation instead of each query. Queries that reach different data sources are left alone: one data source cannot evaluate a set operation over another's rows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xo5DArPYy2Q7ER7HyMo1Ao * fix(cubesql): Pass assert messages as format arguments The crate is on the 2018 edition, where an inline argument in a panic message is not interpolated, and clippy rejects it. Also render the row cap of a pushed down union in the dialects that have no LIMIT: T-SQL reads the set operation through a derived table with TOP, Oracle ends it with FETCH NEXT. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xo5DArPYy2Q7ER7HyMo1Ao * fix(cubesql): Only push a union down where the data source can render it Gate the rule on the data source having a union template, the way the join and subquery rules gate on theirs. Without it a data source with no template fails at SQL generation, where the plan is already chosen and there is no way back to post processing. Give ClickHouse a template of its own: it rejects a bare UNION unless union_default_mode is set, so the set operation has to name which one it is. Read the e-graph so the rule fails closed: an input list is only read when its e-class holds one list, and an input's data source only when every way of wrapping it agrees on the same one, rather than committing to whichever representation comes first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xo5DArPYy2Q7ER7HyMo1Ao * test(cubesql): Cover joining against a pushed down union Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xo5DArPYy2Q7ER7HyMo1Ao * fix(cubesql): Give every dialect a union it can parse, or none at all BigQuery reads a bare UNION as a parse error the way ClickHouse does: GoogleSQL wants the set operator to name its mode. Give it the same template. SQLite takes no parenthesised operands in a compound select, Druid has no UNION of distinct rows, and ksqlDB has no set operations at all. Drop the template in those three: the rule only pushes a union down where a template says the data source can render one, so they keep doing what they do today and leave it to post processing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xo5DArPYy2Q7ER7HyMo1Ao * test(cubesql): Pin that the join reads the union as its subquery The assertion held on any SQL that mentioned a union anywhere, so a join that lost its union, or a union that lost its join, would both have passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xo5DArPYy2Q7ER7HyMo1Ao * test(schema-compiler): Cover the union template of every dialect Seven dialects now decide something about statements/union - four write their own, three drop it - and nothing pinned any of it. Read the templates off the adapter directory rather than a list, so a dialect added later cannot escape the invariants: every query rendered once, the row cap rendered, ALL on the branch that keeps duplicates and no other, and blocks that close. A missing endif and a renamed loop variable both fail it. Druid and ksqlDB drop the template in their own packages and are not covered here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xo5DArPYy2Q7ER7HyMo1Ao * test(schema-compiler): Assert the union template renders what it loops over The per-dialect assertions pinned the loop and the limit guard but neither interpolation, so a template that rendered the operator, the parentheses and a LIMIT while dropping the query body satisfied all of them - under a test named for rendering every query. Pin both, and pin every ALL to the branch that keeps duplicates rather than to its presence anywhere in the string. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xo5DArPYy2Q7ER7HyMo1Ao * test(schema-compiler): Share one dialect scan between the tests that need it Two copies of "read the adapter directory so a dialect added later cannot escape this" is one copy too many. Move it to test/unit/allDialects.ts, returning each dialect with its name so a failure says which one, and let positional-params drop the names it does not use. Also name the assumption the union template test rests on: it reads templates off a bare prototype, which holds for a template a dialect writes unconditionally and would not for one gated on instance state. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xo5DArPYy2Q7ER7HyMo1Ao * test(schema-compiler): assert the union template names the operator Tightening the `ALL` placement check dropped the only assertion that the word `UNION` appears at all, so a template rendering `(q1)\nALL (q2)` passed every assertion in a file about the union operator. Assert it, and make the loop and tag-balance patterns tolerate a tag written without a space after the opener. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xo5DArPYy2Q7ER7HyMo1Ao * test(schema-compiler): name the dialect floor and drop a wrapper alias The floor message interpolated the count it found rather than the one it wanted, so a scan that matched three dialects reported "more dialects than 3" without saying how many were expected. Name the threshold in both the constant and the message. In the positional-params caller, the local one-call wrapper discarded each dialect's name and then recovered it from `QueryClass.name`; filter and map over the pairs instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xo5DArPYy2Q7ER7HyMo1Ao * refactor(cubesql): Read the union input list the way the converter does `list_node_ids` walked `UnionInputs` as cons cells and declined anything else, while `match_list_node_ids!` — which the converter uses to read this same list — recurses and treats any non-list node as an element. The cons shape is only what `add_plan_list_node!` happens to build today, so the rule was coupled to a detail the reader on the other side does not assume. Mirror that traversal, and use `var_list_iter!` rather than an open-coded `filter_map`. The guards stay: an e-class holding more than one `UnionInputs` still fails closed, since picking one would drop or reorder the queries of the union, and a self-containing list still terminates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xo5DArPYy2Q7ER7HyMo1Ao * docs(cubesql): Record how rewrite rules must traverse lists Every matcher should match one level, and lists are consumed by dedicated push-down/pull-up rules that peel one cons cell at a time. Write down that rule, the helpers that generate the traversal, and why an imperative reader over an e-class is the wrong shape: an e-class holds many representations, so picking one is arbitrary, and the cons shape is an implementation detail of how the list was built. Also record that `wrapper-subqueries-wrapped-scan-to-pull` re-contexts a pushdown replacer without comparing input data sources, so unification on a shared pattern variable does not by itself guarantee agreement for plan-level list elements. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xo5DArPYy2Q7ER7HyMo1Ao * refactor(cubesql): Traverse the union input list with rewrite rules The rule read `UnionInputs` by walking it in a transform, which is not how lists are consumed here: every matcher should match one level, and a list is peeled one cons cell at a time by dedicated rules. Split it into a push-down rule that matches the union and the head of its list, a pull-up rule that peels one query per application, and a tail rule for the terminal, and drop `list_node_ids` and `wrapped_input` entirely. The queries of a union arrive already pulled up, so the list carries a pull-up replacer rather than a push-down one. A push-down replacer over a finished wrapper is the subquery path's shape, and reaching it here would have matched `wrapper-subqueries-wrapped-scan-to-pull`, which re-contexts an element without comparing input data sources. Data sources now agree by unification instead of by comparison: the head binds `?input_data_source`, and the pull-up rule shares that variable between each query's own context and the list's, so a query reaching another source leaves the list unconsumed and the post processing plan outprices it. Record the rule in CLAUDE.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xo5DArPYy2Q7ER7HyMo1Ao * docs(cubesql): Record that e-graph lists are flat, not head/tail Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xo5DArPYy2Q7ER7HyMo1Ao * refactor(cubesql): Make the union input list flat `UnionInputs` was built as cons cells by `add_plan_list_node!`, and the rules walked it one cell at a time. Head/tail lists are legacy here, so build it flat — one node holding every query — and match the whole list in one rule. The union now needs a single rule rather than three. It reads every query out of the list at once and unwraps each into the set operation, with `?input_data_source` as a top level element variable so the queries have to agree on a data source for the rule to match at all. Nothing compares data sources any more, and there is no cons walk left to get wrong. That rule needs a transform for what a pattern cannot spell out: the alias converted from `UnionAlias` to `WrappedUnionAlias`, the cleared context, and the union template check. So add `transforming_list_rewrite_with_lists_and_vars`, and run the transform inside `ListNodeApplier` once the substitution carries the list's own variables — wrapping the applier from outside is too early, as the elements' variables are merged in by the applier itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xo5DArPYy2Q7ER7HyMo1Ao * test(cubesql): Pin that a post-processed union keeps every query `UnionInputs` is flat now, and the converter reads it back with `match_list_node_ids!`. That reader is shape agnostic, but nothing pinned it above two queries — where flat and cons actually differ — and the existing no-template test only asserted the plan is still a `Union`, not how many queries survived. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xo5DArPYy2Q7ER7HyMo1Ao * fix(cubesql): Restore the two query minimum and the applier var check Three review findings, all on the flat list rework. A single query is not a union. Moving the list walk into the pattern dropped the `inputs.len() < 2` guard the transform used to carry, and with one query `wrapper-push-down-distinct-to-union` folds the `Distinct` in while the template emits no operator at all — the deduplication would go silently missing, which is the failure this PR exists to fix. Express it where the matching happens: `ListNodeSearcher` now takes a minimum element count, and the union rule asks for two. `ListNodeApplier::vars` returned nothing whenever a transform was set, which switched off egg's check for the list and element patterns too, turning a typo there from a panic at rule construction into one on the first matching plan. Take the transform's own variables alongside it and subtract only those, the way `new_list_var` already is. The round trip test counted three inputs and three scans, but identity and order are what a positional read puts at risk: a reversed list, or one query twice, leaves three of everything. Assert each query's members in order. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xo5DArPYy2Q7ER7HyMo1Ao --------- Co-authored-by: Claude <noreply@anthropic.com>
* docs: document Cube Store message size limits as a known limitation
Cube talks to the cache and queue store over one WebSocket connection per
node, and query results are delivered through it (QUEUE ACK) and written to
the cache — so a result set above the transport ceiling cannot be delivered
at all. A message over that ceiling is not answered with an error: Cube
Store logs `Capacity(MessageTooLong { .. })` and closes the socket, and the
driver resends everything in flight on reconnect, including the oversized
message, which kills the new connection too. The deployment then reports
`ConnectionError: CubeStore connection error: write EPIPE` in a loop, and
unrelated queries sharing that connection fail alongside it.
Adds a "Message size limits" section to the Cube Store architecture page
describing the limits, the known limitation, and how to stay under it, plus
the three previously undocumented env vars:
| Limit | Env var | Default | Range |
| ---------------------- | -------------------------------------- | ------- | -------------- |
| Transport message size | `CUBESTORE_TRANSPORT_MAX_MESSAGE_SIZE` | 64 MiB | 16 MiB–256 MiB |
| Transport frame size | `CUBESTORE_TRANSPORT_MAX_FRAME_SIZE` | 64 MiB | 4 MiB–256 MiB |
| Single cache entry | `CUBESTORE_CACHE_MAX_ENTRY_SIZE` | 63 MiB | ≤ message − 1 MiB |
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: trim the Cube Store message size known limitation callout
Drop the resend-loop mechanics and the blast-radius note, keeping just the
symptom users search for: `write EPIPE`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: drop the per-node connection claim from Cube Store message limits
The number of connections between Cube and the cache and queue store is an
implementation detail that may change; the size ceiling is what the section
is about.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Update docs-mintlify/docs/pre-aggregations/cube-store-architecture.mdx
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* Update docs-mintlify/docs/pre-aggregations/cube-store-architecture.mdx
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* docs: name both transport limits in the Cube Store mitigation advice
A message is sent as a single frame, so raising the message size alone still
trips `CUBESTORE_TRANSPORT_MAX_FRAME_SIZE`. Also shortens the paragraph.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )