Skip to content

[CALCITE-7608] Enhance Uncollect#5031

Open
mihaibudiu wants to merge 2 commits into
apache:mainfrom
mihaibudiu:issue7608
Open

[CALCITE-7608] Enhance Uncollect#5031
mihaibudiu wants to merge 2 commits into
apache:mainfrom
mihaibudiu:issue7608

Conversation

@mihaibudiu

@mihaibudiu mihaibudiu commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Jira Link

CALCITE-7608

Changes Proposed

This PR has several improvements to the Uncollect operator:

  • support LEFT JOIN UNNEST
  • support Trino semantics (no structure expansion)
  • support for carrying over other input fields

The first two changes are actually bug-fixes which could be applied independently to the original Uncollect operator. Only the last modification is a true change of the operator's semantics, and it is strictly backwards-compatible: if the set of carried input fields is empty this operator behaves like the old version of Uncollect.

@iwanttobepowerful

Copy link
Copy Markdown
Contributor

We believe this PR is quite important, so I’d really appreciate it if you could take another look when you have a moment. @zabetak @julianhyde

# Validated on Postgres
SELECT n, a, b
FROM (SELECT 'test' AS n, ARRAY[1, 2, 3] AS arr1, ARRAY[10, 20] AS arr2) AS u,
UNNEST(u.arr1, u.arr2) AS t(a, b);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about add order by a

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to make the output deterministic?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

@iwanttobepowerful

iwanttobepowerful commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Generating LogicalSelectMany directly within SqlToRelConverter would feel cleaner and more natural, I think. I’m fully on board with deprecating Uncollect in favor of this new operator.
Also, would it be reasonable to rename LogicalSelectMany to LogicalUnnest? The current name feels a bit too LINQ-specific for a core Calcite relational operator.

@iwanttobepowerful

iwanttobepowerful commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Thanks a lot for introducing SelectMany — this is a much cleaner abstraction than the current Correlate + Uncollect pattern.

You note in the description that "in the future this rewrite could be moved into SqlToRelConverter as well." I'd like to make the case for doing (at least an initial version of) that in this PR, because I think the rule-based approach has an inherent limitation:

A pattern-matching rule only fires when the plan matches the exact shape it expects, and that shape is easily perturbed — by an intervening Project or pushed-down Filter, a trait change, or decorrelation running first. When that happens, a perfectly convertible query silently misses the rewrite and falls back to the less efficient Correlate form, and trying to generalize the pattern to cover every shape tends to become an endless game of whack-a-mole.

Generating LogicalSelectMany directly in convertUnnest() sidesteps this. The converter already has everything it needs — the input row, the array expressions, withOrdinality, and the join type. By projecting both the pass-through columns and the collection columns onto the same input row, the array expressions stay as ordinary input references (no correlation variable is ever introduced), and SelectMany can be emitted deterministically — before any optimization can disturb the shape. This makes FROM t CROSS/LEFT JOIN UNNEST(t.arr) skip both the Correlate intermediate form and the subsequent decorrelation step entirely, and guarantees the operator is produced whenever it's applicable, rather than "whenever the rule happens to match."

The two approaches are complementary, not competing: the rule is still valuable for plans that already contain Correlate + Unnest (e.g. coming from other frontends), while direct generation covers the SQL path. To stay backwards-compatible, the sql2rel path could be gated behind a SqlToRelConverter.Config flag (off by default), mirroring how you disabled the rewrite rule by default.

Happy to help with this if you think it's in scope — otherwise it could make a good follow-up.

@mihaibudiu

Copy link
Copy Markdown
Contributor Author

There so many config flags in Calcite...

Thinking of this, having a way to do it in SqlToRelConverter is probably very useful, so we should offer this possibility.
Deprecating Uncollect is a big move, it may cause problems for other people who depend on it.
We should probably give it some time.

SelectMany is actually not a very good name, the "real" selectMany is actually much more general, it takes an arbitrary function which returns an iterator. We could call it LogicalUnnest.

Let's see if there are other comments which I can address in an updated commit. I will try to add it to SqlToRel, but not sure I will get to it during the week-end. Happy to get other naming suggestions as well.

@mihaibudiu mihaibudiu added the discussion-in-jira There's open discussion in JIRA to be resolved before proceeding with the PR label Jun 24, 2026
@mihaibudiu mihaibudiu marked this pull request as draft June 25, 2026 18:07
@mihaibudiu

Copy link
Copy Markdown
Contributor Author

Converted to draft pending discussion in Jira.

@mihaibudiu mihaibudiu changed the title [CALCITE-7608] Introduce a SelectMany operator [CALCITE-7608] Enhance Uncollect Jul 2, 2026
@mihaibudiu

Copy link
Copy Markdown
Contributor Author

I have completely reworked this PR to enhance Uncollect to support passing through fields.
There is now a single commit, with changes to all relevant classes.

The only thing I have not yet done is to build this new form of the operator directly in SqlToRelConverter. We can do that in a separate PR, this one is big enough.

@mihaibudiu mihaibudiu force-pushed the issue7608 branch 2 times, most recently from d1bb726 to f3ee809 Compare July 2, 2026 21:01
@mihaibudiu mihaibudiu marked this pull request as ready for review July 3, 2026 16:17
Signed-off-by: Mihai Budiu <mbudiu@feldera.com>
@mihaibudiu

Copy link
Copy Markdown
Contributor Author

This will need a bit more work, I found out that Presto/Trino actually give a different semantics to Uncollect (do not expand struct fields), so that needs to be modeled as well.

super(config);
}

@Override public void onMatch(RelOptRuleCall call) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I played locally and seems found a case where current implementation fails

    final String sql = "with t1 as (select array['a', 'b'] as sa, array[1, 2] as ia)\n"
        + "select u.x, u.y\n"
        + "from t1, unnest(t1.ia, t1.sa) as u(x, y)";
    sql(sql)
        .withPreRule(CoreRules.PROJECT_REMOVE)
        .withRule(CoreRules.CORRELATE_UNCOLLECT_MERGE)
        .check();

currently fails as

    java.lang.AssertionError: Cannot add expression of different type to set:
    set type is RecordType(INTEGER X, CHAR(1) Y) NOT NULL
    expression type is RecordType(CHAR(1) SA, INTEGER IA) NOT NULL
    set is rel#302523:LogicalProject.(input=HepRelVertex#302522,exprs=[$2, $3])
    expression is Uncollect
      LogicalProject(SA=[ARRAY('a', 'b')], IA=[ARRAY(1, 2)])
        LogicalValues(tuples=[[{ 0 }]])
    Type mismatch:
    rowtype of original rel: RecordType(INTEGER X, CHAR(1) Y) NOT NULL
    rowtype of new rel: RecordType(CHAR(1) SA, INTEGER IA) NOT NULL
    Difference:
    X: INTEGER -> CHAR(1)
    Y: CHAR(1) -> INTEGER

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add this as a test, thank you.

@asolimando

Copy link
Copy Markdown
Member

This will need a bit more work, I found out that Presto/Trino actually give a different semantics to Uncollect (do not expand struct fields), so that needs to be modeled as well.

@mihaibudiu I have been following the discussion with lots of interest but I missed this comment, can you elaborate a bit more on the different semantics you found in Trino?

@mihaibudiu

Copy link
Copy Markdown
Contributor Author

@asolimando UNNEST(a) of an array of structs will create as many columns as there fields in the struct. But apparently Trino's semantics requires that only one column is created, of struct type. So I fixed UNNEST to support both semantics, as a function of the conformance.

Signed-off-by: Mihai Budiu <mbudiu@feldera.com>
@mihaibudiu

mihaibudiu commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

I apologize for the large PR, but I thought that changing the semantics of a core logical operator should be done ideally in a single change rather than several smaller changes. This PR has several improvements to UNNEST:

  • support LEFT JOIN UNNEST
  • support Trino semantics (no structure expansion)
  • support for carrying over other input fields

The first two changes are actually bug-fixes which could be applied independently to the original UNNEST operator. Only the last modification is a true change of the operator's semantics.

@sonarqubecloud

sonarqubecloud Bot commented Jul 6, 2026

Copy link
Copy Markdown

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

Labels

discussion-in-jira There's open discussion in JIRA to be resolved before proceeding with the PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants