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
The following commit(s) were added to refs/heads/main by this push:
new 180f167e65 [CALCITE-7911] FROM a, b JOIN c is parsed incorrectly
180f167e65 is described below
commit 180f167e65f28c160d4d6ffab35fa37303f0ebe0
Author: Mihai Budiu <[email protected]>
AuthorDate: Sat Oct 31 15:56:59 2026 -0700
[CALCITE-7911] FROM a, b JOIN c is parsed incorrectly
Signed-off-by: Mihai Budiu <[email protected]>
---
core/src/main/codegen/templates/Parser.jj | 42 +++++----
.../main/java/org/apache/calcite/sql/SqlJoin.java | 2 +-
.../calcite/sql/validate/SqlValidatorImpl.java | 13 ++-
.../apache/calcite/sql2rel/RelDecorrelator.java | 36 +++++++-
.../apache/calcite/sql2rel/SqlToRelConverter.java | 91 ++++++++++++++----
.../calcite/rel/rel2sql/RelToSqlConverterTest.java | 6 +-
.../calcite/sql2rel/RelDecorrelatorTest.java | 34 ++++---
.../org/apache/calcite/test/SqlValidatorTest.java | 26 +++++-
.../apache/calcite/test/SqlToRelConverterTest.xml | 36 +++++---
core/src/test/resources/sql/join.iq | 86 +++++++++++++++++
core/src/test/resources/sql/lateral.iq | 102 ++++++++++++++++++++-
site/_docs/reference.md | 6 ++
.../apache/calcite/sql/parser/SqlParserTest.java | 71 +++++++++++---
13 files changed, 457 insertions(+), 94 deletions(-)
diff --git a/core/src/main/codegen/templates/Parser.jj
b/core/src/main/codegen/templates/Parser.jj
index 2c776169ef..f3d5a9396e 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -2449,39 +2449,43 @@ SqlNode FromClause() :
SqlLiteral joinType;
}
{
- e = TableRef1(ExprContext.ACCEPT_QUERY_OR_JOIN)
+ e = Join()
(
// Comma joins should only occur at top-level in the FROM clause.
+ // JOIN binds tighter than comma, as required by the SQL standard:
+ // "FROM a, b JOIN c" means "FROM a, (b JOIN c)".
// Valid:
// * FROM a, b
// * FROM (a CROSS JOIN b), c
// Not valid:
// * FROM a CROSS JOIN (b, c)
- LOOKAHEAD(1)
- e = JoinOrCommaTable(e)
+ LOOKAHEAD(2)
+ <COMMA> { joinType = JoinType.COMMA.symbol(getPos()); }
+ e2 = Join() {
+ e = new SqlJoin(joinType.getParserPosition(),
+ e,
+ SqlLiteral.createBoolean(false, joinType.getParserPosition()),
+ joinType,
+ e2,
+ JoinConditionType.NONE.symbol(SqlParserPos.ZERO),
+ null);
+ }
)*
{ return e; }
}
-SqlNode JoinOrCommaTable(SqlNode e) :
+/** Parses a table reference followed by zero or more joins. */
+SqlNode Join() :
{
- SqlNode e2;
- SqlLiteral joinType;
+ SqlNode e;
}
{
- LOOKAHEAD(2)
- <COMMA> { joinType = JoinType.COMMA.symbol(getPos()); }
- e2 = TableRef1(ExprContext.ACCEPT_QUERY_OR_JOIN) {
- return new SqlJoin(joinType.getParserPosition(),
- e,
- SqlLiteral.createBoolean(false, joinType.getParserPosition()),
- joinType,
- e2,
- JoinConditionType.NONE.symbol(SqlParserPos.ZERO),
- null);
- }
-|
- e2 = JoinTable(e) { return e2; }
+ e = TableRef1(ExprContext.ACCEPT_QUERY_OR_JOIN)
+ (
+ LOOKAHEAD(2)
+ e = JoinTable(e)
+ )*
+ { return e; }
}
/** Matches "LEFT JOIN t ON ...", "RIGHT JOIN t USING ...", "JOIN t". */
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlJoin.java
b/core/src/main/java/org/apache/calcite/sql/SqlJoin.java
index f948a7d668..7cb62854a5 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlJoin.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlJoin.java
@@ -36,7 +36,7 @@
*/
public class SqlJoin extends SqlCall {
static final SqlJoinOperator COMMA_OPERATOR =
- new SqlJoinOperator("COMMA-JOIN", 18);
+ new SqlJoinOperator("COMMA-JOIN", 16);
public static final SqlJoinOperator OPERATOR =
new SqlJoinOperator("JOIN", 18);
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 bbfa06cd03..1bccf17877 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
@@ -3003,8 +3003,17 @@ private SqlNode registerFrom(
}
final SqlNode node2 = s != null ? s.getNode() : node;
final TableScope tableScope = new TableScope(parentScope0, node2);
- if (usingScope instanceof ListScope) {
- for (ScopeChild child : ((ListScope) usingScope).children) {
+ // The items to the left of this one are visible, also those before an
+ // enclosing join, as "a" in "FROM a, LATERAL (...) CROSS JOIN b".
+ // JoinScope propagates each child to the enclosing JoinScope, so the
+ // outermost one holds all of them, in FROM-list order.
+ SqlValidatorScope outer = usingScope;
+ while (outer instanceof JoinScope
+ && ((JoinScope) outer).getUsingScope() instanceof JoinScope) {
+ outer = ((JoinScope) outer).getUsingScope();
+ }
+ if (outer instanceof ListScope) {
+ for (ScopeChild child : ((ListScope) outer).children) {
tableScope.addChild(child.namespace, child.name, child.nullable);
}
}
diff --git a/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
b/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
index 585b9d0196..0701e592ff 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
@@ -1920,6 +1920,26 @@ private static boolean isWidening(RelDataType type,
RelDataType type1) {
assert rel.getRequiredColumns().cardinality()
<= rightFrame.corDefOutputs.keySet().size();
+ // In a LEFT correlate, the right input gives the values of the variables
of
+ // enclosing correlates, and they are NULL in the rows that the join pads.
+ // Unless the parent supplies these NULLs again, get the values from the
+ // left input too; the conditions below match the copies.
+ Frame newLeftFrame = leftFrame;
+ if (rel.getJoinType().generatesNullsOnRight()
+ && !parentPropagatesNullValues) {
+ final List<CorRef> outerCorVars = new ArrayList<>();
+ for (CorRef corRef : collectExternalCorVars(oldRight)) {
+ if (!corRef.corr.equals(rel.getCorrelationId())) {
+ outerCorVars.add(corRef);
+ }
+ }
+ if (!outerCorVars.isEmpty()) {
+ newLeftFrame =
+ supplyMissingCorVars(oldLeft, leftFrame, outerCorVars,
+ new TreeMap<>());
+ }
+ }
+
// Change correlator rel into a join.
// Join all the correlated variables produced by this correlator rel
// with the values generated and propagated from the right input
@@ -1927,7 +1947,7 @@ private static boolean isWidening(RelDataType type,
RelDataType type1) {
new TreeMap<>(rightFrame.corDefOutputs);
final List<RexNode> conditions = new ArrayList<>();
final List<RelDataTypeField> newLeftOutput =
- leftFrame.r.getRowType().getFieldList();
+ newLeftFrame.r.getRowType().getFieldList();
int newLeftFieldCount = newLeftOutput.size();
final List<RelDataTypeField> newRightOutput =
@@ -1939,7 +1959,7 @@ private static boolean isWidening(RelDataType type,
RelDataType type1) {
if (!corDef.corr.equals(rel.getCorrelationId())) {
continue;
}
- final int newLeftPos =
requireNonNull(leftFrame.oldToNewOutputs.get(corDef.field));
+ final int newLeftPos =
requireNonNull(newLeftFrame.oldToNewOutputs.get(corDef.field));
final int newRightPos = rightOutput.getValue();
// Using `equals` instead of `IS NOT DISTINCT FROM` is an optimization
@@ -1960,6 +1980,12 @@ private static boolean isWidening(RelDataType type,
RelDataType type1) {
corDefOutputs.remove(corDef);
}
+ // If both inputs propagate the same reference to an enclosing correlate,
+ // the two copies must have the same value.
+ conditions.addAll(
+ buildCorDefJoinConditions(newLeftFrame.corDefOutputs, corDefOutputs,
+ newLeftFrame.r, rightFrame.r, relBuilder));
+
// Update the output position for the corVars: only pass on the cor
// vars that are not used in the join key.
for (Map.Entry<CorDef, Integer> entry : corDefOutputs.entrySet()) {
@@ -1968,7 +1994,7 @@ private static boolean isWidening(RelDataType type,
RelDataType type1) {
// then add any corVar from the left input. Do not need to change
// output positions.
- corDefOutputs.putAll(leftFrame.corDefOutputs);
+ corDefOutputs.putAll(newLeftFrame.corDefOutputs);
// Create the mapping between the output of the old correlation rel
// and the new join rel
@@ -1982,7 +2008,7 @@ private static boolean isWidening(RelDataType type,
RelDataType type1) {
== oldLeftFieldCount + oldRightFieldCount;
// Left input positions are not changed.
- mapOldToNewOutputs.putAll(leftFrame.oldToNewOutputs);
+ mapOldToNewOutputs.putAll(newLeftFrame.oldToNewOutputs);
// Right input positions are shifted by newLeftFieldCount.
for (int i = 0; i < oldRightFieldCount; i++) {
@@ -1992,7 +2018,7 @@ private static boolean isWidening(RelDataType type,
RelDataType type1) {
final RexNode condition =
RexUtil.composeConjunction(relBuilder.getRexBuilder(), conditions);
- RelNode newJoin = relBuilder.push(leftFrame.r).push(rightFrame.r)
+ RelNode newJoin = relBuilder.push(newLeftFrame.r).push(rightFrame.r)
.join(rel.getJoinType(), condition).build();
return register(rel, newJoin, mapOldToNewOutputs, corDefOutputs);
diff --git
a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
index 45ffb51997..83b2341e4d 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
@@ -164,6 +164,7 @@
import org.apache.calcite.sql.validate.AggregatingSelectScope;
import org.apache.calcite.sql.validate.CollectNamespace;
import org.apache.calcite.sql.validate.DelegatingScope;
+import org.apache.calcite.sql.validate.JoinScope;
import org.apache.calcite.sql.validate.ListScope;
import org.apache.calcite.sql.validate.MatchRecognizeScope;
import org.apache.calcite.sql.validate.MeasureScope;
@@ -3564,6 +3565,22 @@ protected RelNode createAsofJoin(
return node;
}
+ /** Returns the number of fields before a namespace in the FROM list of a
+ * scope, or -1 if the namespace is not in the list. */
+ private static int fieldOffset(SqlValidatorScope scope,
+ SqlValidatorNamespace namespace) {
+ int offset = 0;
+ if (scope instanceof ListScope) {
+ for (SqlValidatorNamespace child : ((ListScope) scope).getChildren()) {
+ if (child == namespace) {
+ return offset;
+ }
+ offset += child.getRowType().getFieldCount();
+ }
+ }
+ return -1;
+ }
+
private @Nullable CorrelationUse getCorrelationUse(Blackboard bb, final
RelNode r0) {
final Set<CorrelationId> correlatedVariables =
RelOptUtil.getVariablesUsed(r0);
@@ -3572,8 +3589,13 @@ protected RelNode createAsofJoin(
}
final ImmutableBitSet.Builder requiredColumns = ImmutableBitSet.builder();
final List<CorrelationId> correlNames = new ArrayList<>();
- // Mapping from (correlId, originalFieldIndex) to projectedFieldIndex for
aggregation
+ // Mapping from (correlId, originalFieldIndex) to the new field index: the
+ // projected index for aggregation, or the index relative to the join
inputs
final Map<Pair<CorrelationId, Integer>, Integer> fieldMapping = new
HashMap<>();
+ // Offset in the FROM list of the first input of the join that defines the
+ // variable, and the row type of the variable, which spans the FROM list
+ int offset = 0;
+ @Nullable RelDataType correlRowType = null;
for (CorrelationId correlName : correlatedVariables) {
DeferredLookup lookup =
@@ -3599,6 +3621,19 @@ protected RelNode createAsofJoin(
continue;
}
+ if (bb.scope instanceof JoinScope) {
+ // The variable belongs to the join whose inputs contain the item it
+ // references. These inputs need not start the FROM list, as
+ // "b CROSS JOIN LATERAL (... b.x ...)" in "FROM a, b CROSS JOIN ...".
+ final List<SqlValidatorNamespace> joinChildren =
+ ((JoinScope) bb.scope).getChildren();
+ if (joinChildren.stream().noneMatch(ns -> ns == resolve.namespace)) {
+ continue;
+ }
+ offset = fieldOffset(ancestorScope, joinChildren.get(0));
+ Litmus.THROW.check(offset >= 0, "join input is not in the FROM list");
+ }
+
int namespaceOffset = 0;
if (childNamespaceIndex > 0) {
// If not the first child, need to figure out the width
@@ -3644,10 +3679,17 @@ protected RelNode createAsofJoin(
throw new AssertionError("Identifier '" + lookup.originalRelName
+ "." + originalFieldName + "' is not a group expr");
}
- }
+ } // end for correlName
- requiredColumns.set(pos);
+ requiredColumns.set(pos - offset);
correlNames.add(correlName);
+ // Each reference to the FROM list has its own variable, and all of them
+ // have the row type of the FROM list; they are merged below
+ final RelDataType type =
topLevelFieldAccess.getReferenceExpr().getType();
+ Litmus.THROW.check(correlRowType == null || correlRowType.equals(type),
+ "variables of one FROM list have different row types: {} and {}",
+ correlRowType, type);
+ correlRowType = type;
}
if (correlNames.isEmpty()) {
@@ -3666,28 +3708,36 @@ protected RelNode createAsofJoin(
leaves.put(r, r.getRowType().getFieldCount());
}
- // If there are field mappings (due to aggregation), rewrite the RelNode
tree
- // to update correlation variable row type and field indices
+ // A join scope never has an aggregate root
+ Litmus.THROW.check(fieldMapping.isEmpty() || offset == 0,
+ "aggregate root in a join scope");
+ @Nullable RelDataType newCorrelRowType = null;
if (!fieldMapping.isEmpty()) {
- r =
- r.accept(
- new CorrelationFieldMappingShuttle(rexBuilder,
correlNames.get(0),
- bb.root().getRowType(), fieldMapping));
+ newCorrelRowType = bb.root().getRowType();
+ } else if (offset > 0) {
+ final List<RelDataTypeField> fields =
+ requireNonNull(correlRowType, "correlRowType").getFieldList();
+ for (int i = offset; i < fields.size(); i++) {
+ fieldMapping.put(Pair.of(correlNames.get(0), i), i - offset);
+ }
+ newCorrelRowType =
+ typeFactory.createStructType(fields.subList(offset, fields.size()));
} else if (readsPipeInput(bb.scope)) {
// The rel of a pipe operator's input may have trailing columns for
// sort keys (see convertFrom), which the correlation variable lacks.
- // Use the CorrelationFieldMappingShuttle to change the type of the
- // variable to the rel's row type.
- final RelDataType rowType = bb.root().getRowType();
- final Map<Pair<CorrelationId, Integer>, Integer> identityMapping =
- new HashMap<>();
- for (int i = 0; i < rowType.getFieldCount(); i++) {
- identityMapping.put(Pair.of(correlNames.get(0), i), i);
+ // Change the type of the variable to the rel's row type.
+ newCorrelRowType = bb.root().getRowType();
+ for (int i = 0; i < newCorrelRowType.getFieldCount(); i++) {
+ fieldMapping.put(Pair.of(correlNames.get(0), i), i);
}
+ }
+ if (newCorrelRowType != null) {
+ // Rewrite the RelNode tree to update the row type and field indices of
+ // the correlation variable
r =
r.accept(
new CorrelationFieldMappingShuttle(rexBuilder,
correlNames.get(0),
- rowType, identityMapping));
+ newCorrelRowType, fieldMapping));
}
return new CorrelationUse(correlNames.get(0), requiredColumns.build(), r);
@@ -6788,6 +6838,13 @@ private static class CorrelationFieldMappingShuttle
extends RelHomogeneousShuttl
}
return super.visitFieldAccess(fieldAccess);
}
+
+ @Override public RexNode visitSubQuery(RexSubQuery subQuery) {
+ final RelNode r =
+ subQuery.rel.accept(CorrelationFieldMappingShuttle.this);
+ return super.visitSubQuery(
+ r == subQuery.rel ? subQuery : subQuery.clone(r));
+ }
});
}
}
diff --git
a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
index 7d716d0947..bbaf33dd16 100644
---
a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
+++
b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
@@ -662,13 +662,13 @@ private static RelNode sqlToRel(String sql, SchemaPlus
defaultSchema,
+ "match_condition \"employee\".\"department_id\" <=
\"product\".\"product_id\"\n"
+ "on \"employee\".\"salary\" = \"product\".\"gross_weight\"";
final String expected = "SELECT \"t\".\"full_name\"\nFROM "
- + "((SELECT \"employee_id\", \"full_name\", \"first_name\",
\"last_name\", \"position_id\", "
+ + "(SELECT \"employee_id\", \"full_name\", \"first_name\",
\"last_name\", \"position_id\", "
+ "\"position_title\", \"store_id\", \"department_id\",
\"birth_date\", \"hire_date\", "
+ "\"end_date\", \"salary\", \"supervisor_id\", \"education_level\",
\"marital_status\", "
+ "\"gender\", \"management_role\", CAST(\"salary\" AS DOUBLE) AS
\"salary0\"\n"
- + "FROM \"foodmart\".\"employee\") AS \"t\" ASOF JOIN
\"foodmart\".\"product\" "
+ + "FROM \"foodmart\".\"employee\") AS \"t\"\nASOF JOIN
\"foodmart\".\"product\" "
+ "MATCH_CONDITION \"t\".\"department_id\" <=
\"product\".\"product_id\" ON "
- + "\"t\".\"salary0\" = \"product\".\"gross_weight\")";
+ + "\"t\".\"salary0\" = \"product\".\"gross_weight\"";
sql(sql).ok(expected);
}
diff --git
a/core/src/test/java/org/apache/calcite/sql2rel/RelDecorrelatorTest.java
b/core/src/test/java/org/apache/calcite/sql2rel/RelDecorrelatorTest.java
index 2757a7a49b..d8f1fb54b2 100644
--- a/core/src/test/java/org/apache/calcite/sql2rel/RelDecorrelatorTest.java
+++ b/core/src/test/java/org/apache/calcite/sql2rel/RelDecorrelatorTest.java
@@ -287,13 +287,15 @@ public static Frameworks.ConfigBuilder config() {
final String planAfter = ""
+ "LogicalSort(sort0=[$0], dir0=[ASC])\n"
+ " LogicalProject(ENAME_FROM_DEPT=[$2])\n"
- + " LogicalJoin(condition=[IS NOT DISTINCT FROM($0, $1)],
joinType=[left])\n"
+ + " LogicalJoin(condition=[=($0, $1)], joinType=[left])\n"
+ " LogicalValues(tuples=[[{ 7369 }, { 7499 }]])\n"
+ " LogicalAggregate(group=[{0}], agg#0=[SINGLE_VALUE($1)])\n"
- + " LogicalProject(EMPNO1=[$11], EXPR$0=[||(||($1, ' from dept
'), $12)])\n"
- + " LogicalJoin(condition=[AND(=($7, $9), =($8, $10))],
joinType=[left])\n"
- + " LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2],
MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7],
EMPNO0=[CAST($0):INTEGER NOT NULL])\n"
- + " LogicalTableScan(table=[[scott, EMP]])\n"
+ + " LogicalProject(EMPNO1=[$9], EXPR$0=[||(||($1, ' from dept
'), $13)])\n"
+ + " LogicalJoin(condition=[AND(=($7, $10), =($8, $11), IS NOT
DISTINCT FROM($9, $12))], joinType=[left])\n"
+ + " LogicalJoin(condition=[true], joinType=[inner])\n"
+ + " LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2],
MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7],
EMPNO0=[CAST($0):INTEGER NOT NULL])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n"
+ + " LogicalValues(tuples=[[{ 7369 }, { 7499 }]])\n"
+ " LogicalAggregate(group=[{0, 1, 2}],
agg#0=[SINGLE_VALUE($3)])\n"
+ " LogicalProject(DEPTNO0=[$3], EMPNO0=[$4], EMPNO=[$5],
DNAME=[$1])\n"
+ " LogicalJoin(condition=[=($0, $3)],
joinType=[inner])\n"
@@ -910,9 +912,12 @@ public static Frameworks.ConfigBuilder config() {
+ " LogicalProject(DEPTNO0=[$0], $f1=[true])\n"
+ " LogicalAggregate(group=[{0}])\n"
+ " LogicalProject(DEPTNO0=[$8])\n"
- + " LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2],
MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7],
DEPTNO0=[CAST($8):TINYINT], ENAME0=[$9], EXPR$0=[CAST($10):BIGINT])\n"
- + " LogicalJoin(condition=[AND(IS NOT DISTINCT FROM($1,
$9), =($7, $8))], joinType=[inner])\n"
- + " LogicalTableScan(table=[[scott, EMP]])\n"
+ + " LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2],
MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], DEPTNO0=[$8],
DEPTNO1=[CAST($9):TINYINT], ENAME0=[$10], EXPR$0=[CAST($11):BIGINT])\n"
+ + " LogicalJoin(condition=[AND(IS NOT DISTINCT FROM($1,
$10), IS NOT DISTINCT FROM($8, $9))], joinType=[inner])\n"
+ + " LogicalJoin(condition=[=($7, $8)],
joinType=[inner])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n"
+ + " LogicalProject(DEPTNO=[$0])\n"
+ + " LogicalTableScan(table=[[scott, DEPT]])\n"
+ " LogicalFilter(condition=[>($2, 5)])\n"
+ " LogicalAggregate(group=[{0, 1}],
EXPR$0=[SUM($2)])\n"
+ " LogicalProject(DEPTNO=[$0], ENAME=[$1], X=[$2])\n"
@@ -1025,14 +1030,17 @@ public static Frameworks.ConfigBuilder config() {
// Verify plan
final String planAfter = ""
+ "LogicalProject(DNAME=[$1])\n"
- + " LogicalJoin(condition=[IS NOT DISTINCT FROM($0, $3)],
joinType=[inner])\n"
+ + " LogicalJoin(condition=[=($0, $3)], joinType=[inner])\n"
+ " LogicalTableScan(table=[[scott, DEPT]])\n"
+ " LogicalProject(DEPTNO0=[$0], $f1=[true])\n"
+ " LogicalAggregate(group=[{0}])\n"
- + " LogicalProject(DEPTNO0=[$9])\n"
- + " LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2],
MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], ENAME0=[$8],
DEPTNO0=[CAST($9):TINYINT], EXPR$0=[CAST($10):BIGINT])\n"
- + " LogicalJoin(condition=[IS NOT DISTINCT FROM($1, $8)],
joinType=[inner])\n"
- + " LogicalTableScan(table=[[scott, EMP]])\n"
+ + " LogicalProject(DEPTNO0=[$8])\n"
+ + " LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2],
MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], DEPTNO0=[$8],
ENAME0=[$9], DEPTNO1=[CAST($10):TINYINT], EXPR$0=[CAST($11):BIGINT])\n"
+ + " LogicalJoin(condition=[AND(IS NOT DISTINCT FROM($1,
$9), IS NOT DISTINCT FROM($8, $10))], joinType=[inner])\n"
+ + " LogicalJoin(condition=[true], joinType=[inner])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n"
+ + " LogicalProject(DEPTNO=[$0])\n"
+ + " LogicalTableScan(table=[[scott, DEPT]])\n"
+ " LogicalFilter(condition=[>($2, 5)])\n"
+ " LogicalAggregate(group=[{0, 1}],
EXPR$0=[SUM($2)])\n"
+ " LogicalProject(ENAME=[$0], DEPTNO=[$1], X=[$2])\n"
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 7f94e4e7e6..313e7e5703 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -10978,9 +10978,8 @@ void
testOrderByAliasPreservesValidatedExpressions(String query) {
* <a
href="https://issues.apache.org/jira/browse/CALCITE-6266">[CALCITE-6266]
* SqlValidatorException with LATERAL TABLE and JOIN</a>. */
@Test void testCollectionTableWithLateral3() {
- // The cause of [CALCITE-6266] was that CROSS JOIN had higher precedence
- // than ",", and therefore "table(ramp(deptno)" was being associated with
- // "values".
+ // CROSS JOIN binds tighter than ",", so "table(ramp(deptno))" is joined
+ // with "values"; LATERAL still sees "dept", which is before the comma.
sql("select *\n"
+ "from dept,\n"
+ " lateral table(ramp(deptno))\n"
@@ -11004,6 +11003,27 @@ void
testOrderByAliasPreservesValidatedExpressions(String query) {
+ " lateral (select * from dept where dept.deptno = emp.deptno),\n"
+ " ^emp^")
.fails("Duplicate relation name 'EMP' in FROM clause");
+ // LATERAL on the right of a join sees the items on the left of the join
+ // and the items before the comma
+ sql("select *\n"
+ + "from dept,\n"
+ + " emp cross join lateral (select * from bonus\n"
+ + " where bonus.ename = emp.ename and bonus.job = dept.name)")
+ .ok();
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7911">[CALCITE-7911]
+ * FROM a, b JOIN c is parsed incorrectly</a>. */
+ @Test void testJoinOnAfterComma() {
+ // "emp, dept join bonus on c" means "emp, (dept join bonus on c)",
+ // so c sees dept and bonus, but not emp
+ sql("select *\n"
+ + "from emp, dept join bonus on dept.name = bonus.job")
+ .ok();
+ sql("select *\n"
+ + "from emp, dept join bonus on ^emp^.ename = bonus.ename")
+ .fails("Table 'EMP' not found");
}
/** Test case for
diff --git
a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
index a881a357a4..39d005983b 100644
--- a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
@@ -6586,17 +6586,21 @@ from (select 2+deptno d2, 3+deptno d3 from emp) e
<Resource name="plan">
<![CDATA[
LogicalProject(D2=[$0], D3=[$1])
- LogicalProject(D2=[$0], D3=[$1], D1=[CAST($2):INTEGER], D6=[$3],
$f2=[CAST($4):BOOLEAN])
- LogicalJoin(condition=[AND(=($0, $2), IS NOT DISTINCT FROM($1, $3))],
joinType=[inner])
+ LogicalProject(D2=[$0], D3=[$1], D1=[CAST($2):INTEGER],
D30=[CAST($3):INTEGER], $f2=[CAST($4):BOOLEAN])
+ LogicalJoin(condition=[AND(=($0, $2), =($1, $3))], joinType=[inner])
LogicalProject(D2=[+(2, $7)], D3=[+(3, $7)])
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
LogicalAggregate(group=[{0, 1}], agg#0=[MIN($2)])
- LogicalProject(D1=[$0], D6=[$2], $f0=[true])
+ LogicalProject(D1=[$0], D3=[$2], $f0=[true])
LogicalFilter(condition=[IS NOT NULL($1)])
- LogicalProject(D1=[$0], $f0=[$3], D6=[$2])
- LogicalJoin(condition=[=($0, $1)], joinType=[left])
- LogicalProject(D1=[+($0, 1)])
- LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+ LogicalProject(D1=[$0], $f0=[$4], D3=[$1])
+ LogicalJoin(condition=[AND(=($0, $2), IS NOT DISTINCT FROM($1,
$3))], joinType=[left])
+ LogicalJoin(condition=[true], joinType=[inner])
+ LogicalProject(D1=[+($0, 1)])
+ LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+ LogicalAggregate(group=[{0}])
+ LogicalProject(D3=[+(3, $7)])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
LogicalAggregate(group=[{0, 1}], agg#0=[MIN($2)])
LogicalProject(D4=[$0], D6=[$2], $f0=[true])
LogicalFilter(condition=[=($1, $0)])
@@ -6616,17 +6620,21 @@ from (select 2+deptno d2, 3+deptno d3 from emp) e
<Resource name="plan">
<![CDATA[
LogicalProject(D2=[$0], D3=[$1])
- LogicalProject(D2=[$0], D3=[$1], D1=[CAST($2):INTEGER], D6=[$3],
$f2=[CAST($4):BOOLEAN])
- LogicalJoin(condition=[AND(=($0, $2), IS NOT DISTINCT FROM($1, $3))],
joinType=[inner])
+ LogicalProject(D2=[$0], D3=[$1], D1=[CAST($2):INTEGER],
D30=[CAST($3):INTEGER], $f2=[CAST($4):BOOLEAN])
+ LogicalJoin(condition=[AND(=($0, $2), =($1, $3))], joinType=[inner])
LogicalProject(D2=[+(2, $7)], D3=[+(3, $7)])
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
LogicalAggregate(group=[{0, 1}], agg#0=[MIN($2)])
- LogicalProject(D1=[$0], D6=[$2], $f0=[true])
+ LogicalProject(D1=[$0], D3=[$2], $f0=[true])
LogicalFilter(condition=[IS NOT NULL($1)])
- LogicalProject(D1=[$0], $f0=[$3], D6=[$2])
- LogicalJoin(condition=[=($0, $1)], joinType=[left])
- LogicalProject(D1=[+($0, 1)])
- LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+ LogicalProject(D1=[$0], $f0=[$4], D3=[$1])
+ LogicalJoin(condition=[AND(=($0, $2), IS NOT DISTINCT FROM($1,
$3))], joinType=[left])
+ LogicalJoin(condition=[true], joinType=[inner])
+ LogicalProject(D1=[+($0, 1)])
+ LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+ LogicalAggregate(group=[{0}])
+ LogicalProject(D3=[+(3, $7)])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
LogicalAggregate(group=[{0, 1}], agg#0=[MIN($2)])
LogicalProject(D4=[$0], D6=[$2], $f0=[true])
LogicalFilter(condition=[=($1, $0)])
diff --git a/core/src/test/resources/sql/join.iq
b/core/src/test/resources/sql/join.iq
index bac15e2a08..baf040d0ab 100644
--- a/core/src/test/resources/sql/join.iq
+++ b/core/src/test/resources/sql/join.iq
@@ -91,6 +91,92 @@ where t1.i + t2.i + t3.i + t4.i = 1;
!ok
+# [CALCITE-7911] FROM a, b JOIN c is parsed incorrectly
+# JOIN binds tighter than comma: s CROSS JOIN (t RIGHT JOIN u ON t.x = u.y)
+# Same result on PostgreSQL 14.
+with s (c0) as (values 'a', 'b'),
+ t (x) as (values 1, 2),
+ u (y) as (values 10, 20)
+select s.c0, t.x, u.y
+from s, t right join u on t.x = u.y;
++----+---+----+
+| C0 | X | Y |
++----+---+----+
+| a | | 10 |
+| a | | 20 |
+| b | | 10 |
+| b | | 20 |
++----+---+----+
+(4 rows)
+
+!ok
+
+# s CROSS JOIN (u LEFT JOIN t ON t.x = u.y)
+# Same result on PostgreSQL 14.
+with s (c0) as (values 'a', 'b'),
+ t (x) as (values 1, 2),
+ u (y) as (values 10, 20)
+select s.c0, t.x, u.y
+from s, u left join t on t.x = u.y;
++----+---+----+
+| C0 | X | Y |
++----+---+----+
+| a | | 10 |
+| a | | 20 |
+| b | | 10 |
+| b | | 20 |
++----+---+----+
+(4 rows)
+
+!ok
+
+# s CROSS JOIN (t FULL JOIN u ON t.x = u.y)
+# Same result on PostgreSQL 14.
+with s (c0) as (values 'a', 'b'),
+ t (x) as (values 1, 2),
+ u (y) as (values 10, 20)
+select s.c0, t.x, u.y
+from s, t full join u on t.x = u.y;
++----+---+----+
+| C0 | X | Y |
++----+---+----+
+| a | 1 | |
+| a | 2 | |
+| a | | 10 |
+| a | | 20 |
+| b | 1 | |
+| b | 2 | |
+| b | | 10 |
+| b | | 20 |
++----+---+----+
+(8 rows)
+
+!ok
+
+# [CALCITE-7911] FROM a, b JOIN c is parsed incorrectly
+# Correlated sub-query in the ON clause of a join after a comma.
+# Same result on PostgreSQL 14.
+with emp (ename, deptno, sal)
+ as (values ('SMITH', 20, 800), ('CLARK', 10, 2450),
+ ('ALLEN', 30, 1600), ('JONES', 20, 2975)),
+ dept (deptno) as (values (10), (20))
+select v.v, e.ename, d.deptno
+from (values ('A'), ('M')) as v (v),
+ emp as e join dept as d
+ on d.deptno = e.deptno
+ and e.sal = (select max(sal) from emp as e2 where e2.deptno = d.deptno);
++---+-------+--------+
+| V | ENAME | DEPTNO |
++---+-------+--------+
+| A | CLARK | 10 |
+| A | JONES | 20 |
+| M | CLARK | 10 |
+| M | JONES | 20 |
++---+-------+--------+
+(4 rows)
+
+!ok
+
# OR is a nestedLoop join
select *
from emp
diff --git a/core/src/test/resources/sql/lateral.iq
b/core/src/test/resources/sql/lateral.iq
index f560c78d67..80d07648b5 100644
--- a/core/src/test/resources/sql/lateral.iq
+++ b/core/src/test/resources/sql/lateral.iq
@@ -152,9 +152,9 @@ from dept,
!ok
# [CALCITE-6266] SqlValidatorException with LATERAL TABLE and JOIN
-# The problem also occurred in ', LATERAL (SELECT ...) JOIN', because
-# ',' should had lower precedence than 'JOIN', but should have had equal
-# precedence.
+# The problem also occurred in ', LATERAL (SELECT ...) JOIN'. 'JOIN' binds
+# tighter than ',', so the LATERAL item is inside a join, and it must see
+# the items before the ','. Same result on PostgreSQL 14.
with dept (deptno, dname)
as (values (10, 'ACCOUNTING'), (20,'RESEARCH')),
emp (empno, deptno, ename)
@@ -175,6 +175,102 @@ from dept,
!ok
+# [CALCITE-7911] FROM a, b JOIN c is parsed incorrectly
+# LATERAL on the right of a join sees the item on the left of the join (v)
+# and the item before the ',' (dept). Same result on PostgreSQL 14.
+with dept (deptno, dname)
+ as (values (10, 'ACCOUNTING'), (20,'RESEARCH')),
+ emp (empno, deptno, ename)
+ as (values (7369, 20, 'SMITH'), (7782, 10, 'CLARK'), (7499, 30, 'ALLEN'))
+select *
+from dept,
+ (values 'A', 'M') as v (v)
+ cross join lateral (select * from emp
+ where emp.deptno = dept.deptno
+ and position(v.v in emp.ename) > 0) as emp;
++--------+------------+---+-------+---------+-------+
+| DEPTNO | DNAME | V | EMPNO | DEPTNO0 | ENAME |
++--------+------------+---+-------+---------+-------+
+| 10 | ACCOUNTING | A | 7782 | 10 | CLARK |
+| 20 | RESEARCH | M | 7369 | 20 | SMITH |
++--------+------------+---+-------+---------+-------+
+(2 rows)
+
+!ok
+
+# As above, with LEFT JOIN; a row of v that has no match in emp still appears
+# once for each dept, with a null ENAME. Same result on PostgreSQL 14.
+with dept (deptno, dname)
+ as (values (10, 'ACCOUNTING'), (20,'RESEARCH')),
+ emp (empno, deptno, ename)
+ as (values (7369, 20, 'SMITH'), (7782, 10, 'CLARK'), (7499, 30, 'ALLEN'))
+select dept.deptno, emp.ename, v.v
+from dept,
+ (values ('A'), ('M')) as v (v)
+ left join lateral (select * from emp
+ where emp.deptno = dept.deptno
+ and position(v.v in emp.ename) > 0) as emp on true;
++--------+-------+---+
+| DEPTNO | ENAME | V |
++--------+-------+---+
+| 10 | CLARK | A |
+| 10 | | M |
+| 20 | SMITH | M |
+| 20 | | A |
++--------+-------+---+
+(4 rows)
+
+!ok
+
+# [CALCITE-7911] FROM a, b JOIN c is parsed incorrectly
+# LATERAL sees two items before the ',' (v1 and dept) and the item on the
+# left of the join (v2). Same result on PostgreSQL 14.
+with dept (deptno, dname)
+ as (values (10, 'ACCOUNTING'), (20,'RESEARCH')),
+ emp (empno, deptno, ename)
+ as (values (7369, 20, 'SMITH'), (7782, 10, 'CLARK'), (7499, 30, 'ALLEN'))
+select v1.v as v1, dept.deptno, emp.ename, v2.v as v2
+from (values ('A'), ('M')) as v1 (v),
+ dept,
+ (values ('A'), ('M')) as v2 (v)
+ cross join lateral (select * from emp
+ where emp.deptno = dept.deptno
+ and position(v2.v in emp.ename) > 0
+ and v1.v = 'A') as emp;
++----+--------+-------+----+
+| V1 | DEPTNO | ENAME | V2 |
++----+--------+-------+----+
+| A | 10 | CLARK | A |
+| A | 20 | SMITH | M |
++----+--------+-------+----+
+(2 rows)
+
+!ok
+
+# [CALCITE-7911] FROM a, b JOIN c is parsed incorrectly
+# LATERAL on the right of a join uses a column of the join's left input (v.v)
+# two times. Same result on PostgreSQL 14.
+with dept (deptno, dname)
+ as (values (10, 'ACCOUNTING'), (20,'RESEARCH')),
+ emp (empno, deptno, ename)
+ as (values (7369, 20, 'SMITH'), (7782, 10, 'CLARK'), (7499, 30, 'ALLEN'))
+select dept.deptno, emp.ename, v.v
+from dept,
+ (values ('A'), ('M')) as v (v)
+ cross join lateral (select * from emp
+ where emp.deptno = dept.deptno
+ and position(v.v in emp.ename) > 0
+ and position(v.v in emp.ename) < 4) as emp;
++--------+-------+---+
+| DEPTNO | ENAME | V |
++--------+-------+---+
+| 10 | CLARK | A |
+| 20 | SMITH | M |
++--------+-------+---+
+(2 rows)
+
+!ok
+
# UNNEST applied to VALUES
with t (x, ys)
as (values (1, array [2,3]), (4, array [5]))
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 4d3fa1dfae..294f701f26 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -434,6 +434,12 @@ ## Grammar
in those same conformance levels, any *column* in *insert* may be replaced by
*columnDecl*, which has a similar effect to including it in an EXTEND clause.
+In *tableExpression*, JOIN binds tighter than comma, as in the SQL standard:
+"FROM a, b JOIN c ON c.x = b.x" is equivalent to
+"FROM a, (b JOIN c ON c.x = b.x)", so the ON condition can reference
+`b` and `c` but not `a`. A LATERAL item can reference the items to its left,
+also those before a comma.
+
In *orderItem*, if *expression* is a positive integer *n*, it denotes
the <em>n</em>th item in the SELECT clause.
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 cdcd5b2766..3c4b9ca435 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
@@ -16,6 +16,7 @@
*/
package org.apache.calcite.sql.parser;
import org.apache.calcite.avatica.util.Quoting;
+import org.apache.calcite.sql.JoinType;
import org.apache.calcite.sql.SqlCall;
import org.apache.calcite.sql.SqlDelete;
import org.apache.calcite.sql.SqlDialect;
@@ -3632,19 +3633,61 @@ void checkPeriodPredicate(Checker checker) {
sql("select * from a as a2, b cross join c")
.node(
customMatches("custom", node -> {
- // Parsed as left-deep:
- // select * from (a as a2, b) cross join c
- // (This is not valid SQL, but illustrates operator
- // associativity.)
+ // JOIN binds tighter than comma:
+ // select * from a as a2, (b cross join c)
assertThat(node, instanceOf(SqlSelect.class));
final SqlSelect select = (SqlSelect) node;
assertThat(select.getFrom(), instanceOf(SqlJoin.class));
final SqlJoin from = requireNonNull((SqlJoin) select.getFrom());
- assertThat(from.getLeft(), instanceOf(SqlJoin.class));
- assertThat(from.getRight(), instanceOf(SqlIdentifier.class));
+ assertThat(from.getJoinType(), is(JoinType.COMMA));
+ assertThat(from.getLeft().getKind(), is(SqlKind.AS));
+ assertThat(from.getRight(), instanceOf(SqlJoin.class));
}));
}
+ private static String joinTree(SqlNode node) {
+ if (node instanceof SqlJoin) {
+ final SqlJoin join = (SqlJoin) node;
+ return join.getJoinType() + "(" + joinTree(join.getLeft()) + ", "
+ + joinTree(join.getRight()) + ")";
+ }
+ return node.toString();
+ }
+
+ /** Returns a matcher for a query whose FROM clause has a given join tree;
+ * for example "COMMA(A, RIGHT(B, C))" for "FROM a, b RIGHT JOIN c". */
+ private static Matcher<SqlNode> hasJoinTree(String expected) {
+ return customMatches(expected, node -> {
+ final SqlSelect select = (SqlSelect) node;
+ assertThat(joinTree(requireNonNull(select.getFrom())), is(expected));
+ });
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7911">[CALCITE-7911]
+ * FROM a, b JOIN c is parsed incorrectly</a>. */
+ @Test void testJoinCommaPrecedence() {
+ // An explicit JOIN binds tighter than comma; joins and commas are each
+ // left-associative.
+ sql("select * from a, b right join c on b.x = c.x")
+ .node(hasJoinTree("COMMA(A, RIGHT(B, C))"));
+ sql("select * from a, b join c on true, d left join e on true")
+ .node(hasJoinTree("COMMA(COMMA(A, INNER(B, C)), LEFT(D, E))"));
+ sql("select * from a join b on true join c on true, d")
+ .node(hasJoinTree("COMMA(INNER(INNER(A, B), C), D)"));
+ // Printed SQL must parse to the same tree.
+ final String expected = "SELECT *\n"
+ + "FROM `A`,\n"
+ + "`B`\n"
+ + "RIGHT JOIN `C` ON (`B`.`X` = `C`.`X`)";
+ sql("select * from a, (b right join c on b.x = c.x)")
+ .ok(expected)
+ .node(hasJoinTree("COMMA(A, RIGHT(B, C))"));
+ sql(expected)
+ .withConfig(c -> c.withQuoting(Quoting.BACK_TICK))
+ .node(hasJoinTree("COMMA(A, RIGHT(B, C))"));
+ }
+
@Test void testInternalComma() {
sql("select * from (a^,^ b) cross join c")
.fails("(?s)Encountered \",\" at .*");
@@ -5301,19 +5344,19 @@ void checkPeriodPredicate(Checker checker) {
+ "match_condition orders.ts <= products.expiry\n"
+ "on orders.productid = products.productid";
final String expected0 = "SELECT *\n"
- + "FROM (`ORDERS` "
+ + "FROM `ORDERS`\n"
+ "ASOF JOIN `PRODUCTS` "
+ "MATCH_CONDITION (`ORDERS`.`TS` <= `PRODUCTS`.`EXPIRY`) "
- + "ON (`ORDERS`.`PRODUCTID` = `PRODUCTS`.`PRODUCTID`))";
+ + "ON (`ORDERS`.`PRODUCTID` = `PRODUCTS`.`PRODUCTID`)";
sql(sql0).ok(expected0);
final String sql1 = "select * from orders left asof join products\n"
+ "match_condition orders.ts <= products.expiry\n"
+ "on orders.productid = products.productid";
final String expected1 = "SELECT *\n"
- + "FROM (`ORDERS` "
+ + "FROM `ORDERS`\n"
+ "LEFT ASOF JOIN `PRODUCTS` "
+ "MATCH_CONDITION (`ORDERS`.`TS` <= `PRODUCTS`.`EXPIRY`) "
- + "ON (`ORDERS`.`PRODUCTID` = `PRODUCTS`.`PRODUCTID`))";
+ + "ON (`ORDERS`.`PRODUCTID` = `PRODUCTS`.`PRODUCTID`)";
sql(sql1).ok(expected1);
sql("select * from orders asof join products\n"
@@ -7099,8 +7142,8 @@ private static Matcher<SqlNode> isCharLiteral(String s) {
.ok("SELECT (ARRAY (SELECT `X`\n"
+ "FROM (VALUES (ROW(1))) AS `X`\n"
+ "ORDER BY `X`))");
- sql("SELECT array(SELECT x FROM (VALUES(1)) x^,^ SELECT x FROM (VALUES(1))
x)")
- .fails("(?s)Encountered \", SELECT\" at.*");
+ sql("SELECT array(SELECT x FROM (VALUES(1)) x, ^SELECT^ x FROM (VALUES(1))
x)")
+ .fails("(?s)Incorrect syntax near the keyword 'SELECT' at.*");
sql("SELECT array(1, ^SELECT^ x FROM (VALUES(1)) x)")
.fails("(?s)Incorrect syntax near the keyword 'SELECT'.*");
}
@@ -7239,8 +7282,8 @@ private static Matcher<SqlNode> isCharLiteral(String s) {
sql("SELECT map(1, ^SELECT^ x FROM (VALUES(1)) x)")
.fails("(?s)Incorrect syntax near the keyword 'SELECT'.*");
- sql("SELECT map(SELECT x FROM (VALUES(1)) x^,^ SELECT x FROM (VALUES(1))
x)")
- .fails("(?s)Encountered \", SELECT\" at.*");
+ sql("SELECT map(SELECT x FROM (VALUES(1)) x, ^SELECT^ x FROM (VALUES(1))
x)")
+ .fails("(?s)Incorrect syntax near the keyword 'SELECT' at.*");
}
@Test void testVisitSqlInsertWithSqlShuttle() {