This is an automated email from the ASF dual-hosted git repository. mihaibudiu pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/calcite.git
commit 0de42de0006b911f107ba31df20edb6b60f00873 Author: Mihai Budiu <[email protected]> AuthorDate: Fri Jun 19 00:01:30 2026 -0700 Stronger validation for JOIN UNNEST Signed-off-by: Mihai Budiu <[email protected]> --- .../apache/calcite/runtime/CalciteResource.java | 3 +++ .../calcite/sql/validate/SqlValidatorImpl.java | 30 ++++++++++++++++++++++ .../calcite/runtime/CalciteResource.properties | 1 + .../org/apache/calcite/test/SqlValidatorTest.java | 24 +++++++++++++++++ 4 files changed, 58 insertions(+) diff --git a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java index c6e1a4dbdc..c5047574a3 100644 --- a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java +++ b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java @@ -337,6 +337,9 @@ ExInst<CalciteException> invalidCompare(String a0, String a1, String a2, @BaseMessage("Cannot specify condition (NATURAL keyword, or ON or USING clause) following CROSS JOIN") ExInst<SqlValidatorException> crossJoinDisallowsCondition(); + @BaseMessage("UNNEST is only supported with INNER, LEFT, CROSS, or COMMA join, not ''{0}''") + ExInst<SqlValidatorException> unnestInvalidJoinType(String a0); + @BaseMessage("Cannot specify NATURAL keyword with ON or USING clause") ExInst<SqlValidatorException> naturalDisallowsOnOrUsing(); diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java index 023de1b77f..7021696b67 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java @@ -4116,6 +4116,20 @@ protected void validateJoin(SqlJoin join, SqlValidatorScope scope) { } } + // UNNEST on the right side is only meaningful with INNER, LEFT, CROSS, or COMMA. + if (isUnnestNode(right)) { + switch (joinType) { + case INNER: + case LEFT: + case CROSS: + case COMMA: + break; + default: + throw newValidationError(join.getJoinTypeNode(), + RESOURCE.unnestInvalidJoinType(joinType.name())); + } + } + // Which join types require/allow a ON/USING condition, or allow // a NATURAL keyword? switch (joinType) { @@ -4192,6 +4206,22 @@ protected void validateJoin(SqlJoin join, SqlValidatorScope scope) { } } + /** + * Returns whether {@code node} is (or wraps, via AS or LATERAL) an + * {@code UNNEST} call. + */ + private static boolean isUnnestNode(SqlNode node) { + switch (node.getKind()) { + case UNNEST: + return true; + case AS: + case LATERAL: + return isUnnestNode(((SqlCall) node).operand(0)); + default: + return false; + } + } + /** * Shuttle which determines whether all SqlCalls that are * comparisons are comparing columns from both namespaces. diff --git a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties index f4f16d7326..a90099d7cb 100644 --- a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties +++ b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties @@ -115,6 +115,7 @@ AliasListDuplicate=Duplicate name ''{0}'' in column alias list JoinRequiresCondition=INNER, LEFT, RIGHT, FULL, or ASOF join requires a condition (NATURAL keyword or ON or USING clause) DisallowsQualifyingCommonColumn=Cannot qualify common column ''{0}'' CrossJoinDisallowsCondition=Cannot specify condition (NATURAL keyword, or ON or USING clause) following CROSS JOIN +UnnestInvalidJoinType=UNNEST is only supported with INNER, LEFT, CROSS, or COMMA join, not ''{0}'' NaturalDisallowsOnOrUsing=Cannot specify NATURAL keyword with ON or USING clause ColumnInUsingNotUnique=Column name ''{0}'' in NATURAL join or USING clause is not unique on one side of join NaturalOrUsingColumnNotCompatible=Column ''{0}'' matched using NATURAL keyword or USING clause has incompatible types: cannot compare ''{1}'' to ''{2}'' diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java index 8f95099ab9..018c27e898 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java @@ -9855,6 +9855,30 @@ void testGroupExpressionEquivalenceParams() { .fails("Column 'ORDINALITY' not found in any table"); } + /** UNNEST is valid with INNER, LEFT, CROSS, and COMMA joins; + * all other join kinds must be rejected by the validator. */ + @Test void testUnnestJoinType() { + // Allowed join kinds — these must all validate without error. + sql("select * from dept inner join unnest(array[1, 2]) as u(x) on true").ok(); + sql("select * from dept left join unnest(array[1, 2]) as u(x) on true").ok(); + sql("select * from dept cross join unnest(array[1, 2]) as u(x)").ok(); + sql("select * from dept, unnest(array[1, 2]) as u(x)").ok(); + + // LATERAL wrapping must also be allowed for valid join kinds. + sql("select * from dept cross join lateral unnest(array[1, 2]) as u(x)").ok(); + sql("select * from dept left join lateral unnest(array[1, 2]) as u(x) on true").ok(); + + // Disallowed join kinds — validator must reject these. + sql("select * from dept right ^join^ unnest(array[1, 2]) as u(x) on true") + .fails("UNNEST is only supported with INNER, LEFT, CROSS, or COMMA join, not 'RIGHT'"); + sql("select * from dept full ^join^ unnest(array[1, 2]) as u(x) on true") + .fails("UNNEST is only supported with INNER, LEFT, CROSS, or COMMA join, not 'FULL'"); + + // LATERAL wrapping must also be rejected for invalid join kinds. + sql("select * from dept right ^join^ lateral unnest(array[1, 2]) as u(x) on true") + .fails("UNNEST is only supported with INNER, LEFT, CROSS, or COMMA join, not 'RIGHT'"); + } + @Test void unnestMapMustNameColumnsKeyAndValueWhenNotAliased() { sql("select * from unnest(map[1, 12, 2, 22])") .type("RecordType(INTEGER NOT NULL KEY, INTEGER NOT NULL VALUE) NOT NULL");
