This is an automated email from the ASF dual-hosted git repository.
xuzifu666 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/main by this push:
new d3a5d8d9e6 [CALCITE-5168] Allow AS after parenthesized JOIN
d3a5d8d9e6 is described below
commit d3a5d8d9e6713c5fd483810e1aa1f38652d2dd8d
Author: Yu Xu <[email protected]>
AuthorDate: Wed Aug 19 19:08:42 2026 +0800
[CALCITE-5168] Allow AS after parenthesized JOIN
---
core/src/main/codegen/templates/Parser.jj | 4 +---
.../calcite/sql/validate/AliasNamespace.java | 16 ++++++++++++-
.../calcite/sql/validate/SqlValidatorImpl.java | 10 ++++++--
.../org/apache/calcite/test/SqlValidatorTest.java | 17 +++++++++-----
core/src/test/resources/sql/join.iq | 17 ++++++++++++++
.../apache/calcite/sql/parser/SqlParserTest.java | 27 ++++++++++++----------
6 files changed, 67 insertions(+), 24 deletions(-)
diff --git a/core/src/main/codegen/templates/Parser.jj
b/core/src/main/codegen/templates/Parser.jj
index 185a85f508..16bf6b5e95 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -2516,9 +2516,7 @@ SqlNode TableRef3(ExprContext exprContext, boolean
lateral) :
// Standard SQL (and Postgres) allow applying "AS alias" to a JOIN,
// e.g. "FROM (a CROSS JOIN b) AS c". The new alias obscures the
// internal aliases, and columns cannot be referenced if they are
- // not unique. TODO: Support this behavior; see
- // [CALCITE-5168] Allow AS after parenthesized JOIN
- checkNotJoin(tableRef);
+ // not unique.
if (columnAliasList == null) {
tableRef = SqlStdOperatorTable.AS.createCall(
Span.of(tableRef).end(this), tableRef, alias);
diff --git
a/core/src/main/java/org/apache/calcite/sql/validate/AliasNamespace.java
b/core/src/main/java/org/apache/calcite/sql/validate/AliasNamespace.java
index 04296d9817..99f84154e7 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/AliasNamespace.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/AliasNamespace.java
@@ -20,6 +20,7 @@
import org.apache.calcite.rel.type.RelDataTypeFactoryImpl;
import org.apache.calcite.rel.type.RelDataTypeField;
import org.apache.calcite.rel.type.SingleColumnAliasRelDataType;
+import org.apache.calcite.rel.type.StructKind;
import org.apache.calcite.sql.SqlBasicCall;
import org.apache.calcite.sql.SqlCall;
import org.apache.calcite.sql.SqlIdentifier;
@@ -82,7 +83,20 @@ protected AliasNamespace(
final List<SqlNode> operands = call.getOperandList();
final SqlValidatorNamespace childNs =
validator.getNamespaceOrThrow(operands.get(0));
- final RelDataType rowType = childNs.getRowTypeSansSystemColumns();
+ final RelDataType rowType0 = childNs.getRowTypeSansSystemColumns();
+ final RelDataType rowType;
+ if (rowType0.isStruct()) {
+ rowType = rowType0;
+ } else {
+ // Joins produce RelCrossType, which is not a struct. Convert to a struct
+ // so that columns can be resolved via the alias.
+ rowType = validator.getTypeFactory().builder()
+ .kind(StructKind.FULLY_QUALIFIED)
+ .addAll(
+ Util.transform(rowType0.getFieldList(),
+ f -> Pair.of(f.getName(), f.getType())))
+ .build();
+ }
final RelDataType aliasedType;
if (operands.size() == 2) {
final SqlNode node = operands.get(0);
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 c2d12ffd18..6642a1522b 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
@@ -2864,11 +2864,17 @@ private SqlNode registerFrom(
expr = call.operand(0);
final boolean needAliasNamespace = call.operandCount() > 2
|| expr.getKind() == SqlKind.VALUES || expr.getKind() ==
SqlKind.UNNEST
- || expr.getKind() == SqlKind.COLLECTION_TABLE;
+ || expr.getKind() == SqlKind.COLLECTION_TABLE
+ || expr.getKind() == SqlKind.JOIN;
+ // For an aliased join, the join's children must not be visible outside
+ // the alias. Prevent JoinScope.addChild from propagating children to
+ // the using scope by using parentScope.
+ final SqlValidatorScope exprUsingScope =
+ expr.getKind() == SqlKind.JOIN ? parentScope : usingScope;
newExpr =
registerFrom(
parentScope,
- usingScope,
+ exprUsingScope,
!needAliasNamespace,
expr,
enclosingNode,
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 f0494539f0..854a5ce8ff 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -6649,12 +6649,17 @@ void testReturnsCorrectRowTypeOnCombinedJoin() {
sql("select * from (emp join bonus using (job))\n"
+ "join dept using (deptno)").ok();
- // Cannot alias a JOIN (until
- // [CALCITE-5168] Allow AS after parenthesized JOIN
- // is fixed).
- sql("select * from (emp ^join^ bonus using (job)) as x\n"
- + "join dept using (deptno)")
- .fails("Join expression encountered in illegal context");
+ // [CALCITE-5168] Allow AS after parenthesized JOIN.
+ sql("select x.empno from (emp cross join dept) as x").ok();
+ sql("select x.empno from (emp join bonus using (job)) as x").ok();
+ sql("select x.a from ((select empno from emp) cross join "
+ + "(select deptno from dept)) as x (a, c)")
+ .ok();
+ // Inner aliases are obscured by the new alias.
+ sql("select ^emp^.empno from (emp cross join dept) as x")
+ .fails("Table 'EMP' not found");
+ sql("select ^bonus^.job from (emp join bonus using (job)) as x")
+ .fails("Table 'BONUS' not found");
sql("select * from (emp join bonus using (job))\n"
+ "join dept using (^dname^)")
.fails("Column 'DNAME' not found in any table");
diff --git a/core/src/test/resources/sql/join.iq
b/core/src/test/resources/sql/join.iq
index 35363424e9..ca1ba1451c 100644
--- a/core/src/test/resources/sql/join.iq
+++ b/core/src/test/resources/sql/join.iq
@@ -302,6 +302,23 @@ cross join (bonus as b
!ok
+# [CALCITE-5168] Allow AS after parenthesized JOIN
+select d.dname, j.empno, j.ename
+from dept as d
+cross join (emp as e cross join (values (1)) as b(dummy)) as j
+where j.empno = 7369;
++------------+-------+-------+
+| DNAME | EMPNO | ENAME |
++------------+-------+-------+
+| ACCOUNTING | 7369 | SMITH |
+| RESEARCH | 7369 | SMITH |
+| SALES | 7369 | SMITH |
+| OPERATIONS | 7369 | SMITH |
++------------+-------+-------+
+(4 rows)
+
+!ok
+
# Join plus TABLE
select e.ename, d.dname
from emp as e
diff --git
a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
index 5e4271e4c8..32f52812ce 100644
--- a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
+++ b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
@@ -7860,9 +7860,9 @@ private static Consumer<List<? extends Throwable>>
checkWarnings(
// is syntactically and semantically valid; but
// "select t.i from (t cross join u) as x"
// is semantically invalid.
- // TODO: Support this in Calcite.
- sql("select * from (t cross ^join^ u) as x")
- .fails("Join expression encountered in illegal context");
+ sql("select * from (t cross join u) as x")
+ .ok("SELECT *\n"
+ + "FROM (`T` CROSS JOIN `U`) AS `X`");
sql("select *\n"
+ "from (t cross ^join^ u)\n"
+ " tablesample substitute('medium')")
@@ -8026,21 +8026,24 @@ private static Consumer<List<? extends Throwable>>
checkWarnings(
+ "CROSS JOIN (TABLE `T2`)";
sql(sql3).ok(expected3);
- // Adding an alias to the previous query makes it invalid
- // (The error message and location could be improved)
+ final String expected4 = "SELECT *\n"
+ + "FROM ((SELECT *\n"
+ + "FROM `T`) CROSS JOIN (TABLE `T2`)) AS `X`";
final String sql4 = "SELECT *\n"
+ "FROM ((((((((((((SELECT * FROM t)))\n"
- + " cross ^join^ ((table t2))))))))))) X";
+ + " cross join ((table t2))))))))))) X";
final String sql5 = "SELECT *\n"
+ "FROM ((((((((((((SELECT * FROM t)))\n"
- + " cross ^join^ ((table t2))))))))))) as X";
+ + " cross join ((table t2))))))))))) as X";
final String sql6 = "SELECT *\n"
+ "FROM ((((((((((((SELECT * FROM t)))\n"
- + " cross ^join^ ((table t2))))))))))) as X (a, b, c)";
- final String message = "Join expression encountered in illegal context";
- sql(sql4).fails(message);
- sql(sql5).fails(message);
- sql(sql6).fails(message);
+ + " cross join ((table t2))))))))))) as X (a, b, c)";
+ sql(sql4).ok(expected4);
+ sql(sql5).ok(expected4);
+ final String expected6 = "SELECT *\n"
+ + "FROM ((SELECT *\n"
+ + "FROM `T`) CROSS JOIN (TABLE `T2`)) AS `X` (`A`, `B`, `C`)";
+ sql(sql6).ok(expected6);
}
@Test void testProcedureCall() {