Skip to content

Add Support for CROSS JOIN - #4721

Merged
alecgrieser merged 6 commits into
FoundationDB:mainfrom
shivanshrustagi:cross-joins
Oct 14, 2026
Merged

alecgrieser merged 6 commits into
FoundationDB:mainfrom
shivanshrustagi:cross-joins

Conversation

@shivanshrustagi

Copy link
Copy Markdown
Contributor

CROSS JOIN now has its own grammar rule and visitor method. Previously it was parsed as an inner join with an optional ON / USING. a CROSS JOIN b now means the same as FROM a, b: the Cartesian product, optionally filtered by WHERE.

Changes:

  • Grammar: CROSS is removed from #innerJoin, and a new #crossJoin alternative is added: CROSS JOIN tableSourceItem, with no ON or USING.
  • Visitor: QueryVisitor.visitCrossJoin adds the right-hand table to the current plan fragment and no join predicate. visitSimpleTable later merges all tables into one flat SelectExpression, as it already does for inner joins.
  • Planner: unchanged. The planner receives the same SelectExpression as for the comma syntax, and explain assertions confirm the plans are identical.

Testing

The new cross-join-tests.yamsql covers:

  • the Cartesian product, compared with the comma syntax
  • column resolution, including ambiguous and undefined columns
  • WHERE as the join predicate, compared with JOIN … ON
  • chains mixing CROSS with inner, USING, comma and LEFT joins
  • self-joins, derived tables and correlated EXISTS / NOT EXISTS
  • empty inputs
  • syntax that the grammar rejects.

Known issue (#4170)

When an equality join is planned as an index or primary-key lookup, a NULL key matches other NULL keys. The cause is ScanComparisons.toTupleRange(). This isn't specific to cross joins: JOIN … ON and comma joins behave the same way. The affected tests are marked TODO Issue #4170.

shivanshrustagi and others added 2 commits October 7, 2026 14:46
CROSS JOIN now has its own grammar rule and visitor method. Previously it was parsed as an inner join with
an optional ON / USING. a CROSS JOIN b now means the same as FROM a, b: the Cartesian product, optionally
filtered by WHERE.

Changes

- Grammar: CROSS is removed from #innerJoin, and a new #crossJoin alternative is added: CROSS JOIN
  tableSourceItem, with no ON or USING.
- Visitor: QueryVisitor.visitCrossJoin adds the right-hand table to the current plan fragment and no join
  predicate. visitSimpleTable later merges all tables into one flat SelectExpression, as it already does
  for inner joins.
- Planner: unchanged. The planner receives the same SelectExpression as for the comma syntax, and explain
  assertions confirm the plans are identical.

Testing

The new cross-join-tests.yamsql covers:
- the Cartesian product, compared with the comma syntax
- column resolution, including ambiguous and undefined columns
- WHERE as the join predicate, compared with JOIN … ON
- chains mixing CROSS with inner, USING, comma and LEFT joins
- self-joins, derived tables and correlated EXISTS / NOT EXISTS
- empty inputs
- syntax that the grammar rejects.

Known issue (FoundationDB#4170)

When an equality join is planned as an index or primary-key lookup, a NULL key matches other NULL keys. The
cause is ScanComparisons.toTupleRange(). This isn't specific to cross joins: JOIN … ON and comma joins
behave the same way. The affected tests are marked TODO Issue FoundationDB#4170.
@shivanshrustagi shivanshrustagi added the enhancement New feature or request label Oct 7, 2026
@shivanshrustagi
shivanshrustagi requested review from alecgrieser and hatyo and removed request for hatyo October 7, 2026 14:31
@shivanshrustagi shivanshrustagi added the Run mixed-mode Label to add to Pull Requests to have it run mixed mode tests label Oct 7, 2026
@shivanshrustagi
shivanshrustagi requested review from alecgrieser and hatyo and removed request for alecgrieser and hatyo October 7, 2026 14:54

@hatyo hatyo left a comment

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.

The PR is good, though I think we should mark it as a breaking change. See my comment.

# Syntax Rejected by Grammar
-
# CROSS JOIN does not accept an ON join predicate
- query: SELECT fname, lname, name FROM emp CROSS JOIN dept ON emp.dept_id = dept.id

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.

this syntax was accepted before your change, but now it is correctly rejected, this is fine, but we might have customer using that syntax, so their queries will break if they attempt to consume a newer record-layer version with this PR.

This means we should probably tag this PR as breaking change.

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 think this PR is backward and forward compatible when it comes to continuation handling though, so marking it as breaking change is a bit heavy handed ...

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.

Removing the label.

@shivanshrustagi shivanshrustagi added the breaking change Changes that are not backwards compatible label Oct 9, 2026

@alecgrieser alecgrieser left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM! I like the breadth of the new test cases here. Teamscale is flagging two test gaps, but once those are covered (probably by adding a unit test to DeletegatingVisitorTest), this should be good to go.

@Nullable
@Override
public Object visitCrossJoin(@Nonnull RelationalParser.CrossJoinContext ctx) {
return getDelegate().visitCrossJoin(ctx);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Teamscale is marking this as a test gap. The exact flow here is a little subtle, but I believe this is because we only ever try to visit the crossJoin nodes from within a QueryVisitor, and so the QueryVisitor's visitCrossJoin is the only one that gets called.

I think that's the right approach for this code, so I think we just need to add some tests to make sure that we do delegate appropriately in case we change how that works in the future. We have a test file (DelegatingVisitorTest) that we use for adding tests of that. The tests in their also allow for testing the BaseVisitor.

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.

Thanks Alec, noted. Will update the PR with the Unit test.

@shivanshrustagi shivanshrustagi removed the breaking change Changes that are not backwards compatible label Oct 9, 2026
shivanshrustagi and others added 4 commits October 12, 2026 13:33
Updated the documentation to include CROSS JOIN examples and more
information on the usage and limtiations of CROSS JOIN.

Added a few more additional tests to compare the equivalence of CROSS
JOIN ... WHERE with INNER JOIN ... ON in certain conditions
During the logical plan creation, BaseVisitor's visitCrossJoin should
not be reached since the BaseVisitor hands the responsibility to the
QueryVisitor.
The test verifies that the BaseVisitor's visitCrossJoin should not be
invoked and the responsibility remains with QueryVisitor. If the
expectation or code handling changes, the test should also change to
reflect this
Teamscale flagged DelegatingVisitor.visitCrossJoin() and
BaseVisitor.visitCrossJoin() as test gaps: during plan generation the
joinPart nodes are visited from within the QueryVisitor, so only
QueryVisitor.visitCrossJoin() is ever called.

visitCrossJoinTest checks that DelegatingVisitor.visitCrossJoin()
forwards the call to its delegate. The override calls super, so
BaseVisitor.visitCrossJoin() is called as well.

visitAtomTableItem is stubbed because only a joinPart is parsed due to
incomplete query and no plan fragment exists.
@github-actions

Copy link
Copy Markdown

📊 Metrics Diff Analysis Report

Summary

  • New queries: 20
  • Dropped queries: 0
  • Plan changed + metrics changed: 0
  • Plan unchanged + metrics changed: 0
ℹ️ About this analysis

This automated analysis compares query planner metrics between the base branch and this PR. It categorizes changes into:

  • New queries: Queries added in this PR
  • Dropped queries: Queries removed in this PR. These should be reviewed to ensure we are not losing coverage.
  • Plan changed + metrics changed: The query plan has changed along with planner metrics.
  • Metrics only changed: Same plan but different metrics

The last category in particular may indicate planner regressions that should be investigated.

New Queries

Count of new queries by file:

  • yaml-tests/src/test/resources/cross-join-tests.metrics.yaml: 20

@alecgrieser
alecgrieser merged commit a5dfe60 into FoundationDB:main Oct 14, 2026
16 of 22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request Run mixed-mode Label to add to Pull Requests to have it run mixed mode tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants