Skip to content

Commit 3ded2a6

Browse files
committed
CALCITE-7814 Support the SQL-standard CYCLE clause in recursive CTEs
1 parent afdea2e commit 3ded2a6

14 files changed

Lines changed: 1095 additions & 21 deletions

File tree

‎core/src/main/codegen/templates/Parser.jj‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ import org.apache.calcite.sql.SqlUtil;
106106
import org.apache.calcite.sql.SqlWindow;
107107
import org.apache.calcite.sql.SqlWith;
108108
import org.apache.calcite.sql.SqlWithItem;
109+
import org.apache.calcite.sql.SqlCycleClause;
109110
import org.apache.calcite.sql.fun.SqlCase;
110111
import org.apache.calcite.sql.fun.SqlInternalOperators;
111112
import org.apache.calcite.sql.fun.SqlLibraryOperators;
@@ -3929,13 +3930,37 @@ void AddWithItem(List<SqlWithItem> list, SqlLiteral recursive) :
39293930
final SqlIdentifier id;
39303931
final SqlNodeList columnList;
39313932
final SqlNode definition;
3933+
SqlCycleClause cycle = null;
39323934
}
39333935
{
39343936
id = SimpleIdentifier()
39353937
( columnList = ParenthesizedSimpleIdentifierList() | { columnList = null; } )
39363938
<AS>
39373939
definition = ParenthesizedExpression(ExprContext.ACCEPT_QUERY)
3938-
{ list.add(new SqlWithItem(id.getParserPosition(), id, columnList, definition, recursive)); }
3940+
[ cycle = CycleClause() ]
3941+
{ list.add(new SqlWithItem(id.getParserPosition(), id, columnList, definition, recursive, cycle)); }
3942+
}
3943+
3944+
SqlCycleClause CycleClause() :
3945+
{
3946+
final Span s;
3947+
final List<SqlNode> columns = new ArrayList<SqlNode>();
3948+
SqlIdentifier column;
3949+
final SqlIdentifier markColumn;
3950+
final SqlNode markValue;
3951+
final SqlNode defaultValue;
3952+
final SqlIdentifier pathColumn;
3953+
}
3954+
{
3955+
<CYCLE> { s = span(); }
3956+
column = SimpleIdentifier() { columns.add(column); }
3957+
( <COMMA> column = SimpleIdentifier() { columns.add(column); } )*
3958+
<SET> markColumn = SimpleIdentifier()
3959+
<TO> markValue = Expression(ExprContext.ACCEPT_NON_QUERY)
3960+
<DEFAULT_> defaultValue = Expression(ExprContext.ACCEPT_NON_QUERY)
3961+
<USING> pathColumn = SimpleIdentifier()
3962+
{ return new SqlCycleClause(s.end(this), new SqlNodeList(columns, s.pos()),
3963+
markColumn, markValue, defaultValue, pathColumn); }
39393964
}
39403965

39413966
/**

‎core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@
130130
import static org.apache.calcite.linq4j.tree.ExpressionType.Subtract;
131131
import static org.apache.calcite.linq4j.tree.ExpressionType.SubtractChecked;
132132
import static org.apache.calcite.linq4j.tree.ExpressionType.UnaryPlus;
133+
import static org.apache.calcite.sql.fun.SqlInternalOperators.CYCLE_PATH_APPEND;
134+
import static org.apache.calcite.sql.fun.SqlInternalOperators.CYCLE_PATH_CONTAINS;
133135
import static org.apache.calcite.sql.fun.SqlInternalOperators.LITERAL_AGG;
134136
import static org.apache.calcite.sql.fun.SqlInternalOperators.THROW_UNLESS;
135137
import static org.apache.calcite.sql.fun.SqlLibraryOperators.ACOSD;
@@ -1108,6 +1110,8 @@ void populate2() {
11081110
defineMethod(STRUCT_ACCESS, BuiltInMethod.STRUCT_ACCESS.method, NullPolicy.SEMI_STRICT);
11091111
defineMethod(MEMBER_OF, BuiltInMethod.MEMBER_OF.method, NullPolicy.NONE);
11101112
defineMethod(ARRAY_APPEND, BuiltInMethod.ARRAY_APPEND.method, NullPolicy.ARG0);
1113+
defineMethod(CYCLE_PATH_APPEND, BuiltInMethod.ARRAY_APPEND.method, NullPolicy.ARG0);
1114+
defineMethod(CYCLE_PATH_CONTAINS, BuiltInMethod.CYCLE_PATH_CONTAINS.method, NullPolicy.NONE);
11111115
defineMethod(ARRAY_COMPACT, BuiltInMethod.ARRAY_COMPACT.method, NullPolicy.STRICT);
11121116
defineMethod(ARRAY_CONTAINS, BuiltInMethod.LIST_CONTAINS.method, NullPolicy.SEMI_STRICT);
11131117
defineMethod(ARRAY_DISTINCT, BuiltInMethod.ARRAY_DISTINCT.method, NullPolicy.STRICT);

‎core/src/main/java/org/apache/calcite/runtime/CalciteResource.java‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,24 @@ ExInst<CalciteException> illegalArgumentForTableFunctionCall(String a0,
935935
@BaseMessage("A recursive query only supports binary UNION [ALL] operator")
936936
ExInst<SqlValidatorException> recursiveWithMustHaveTwoChildUnionSetOp();
937937

938+
@BaseMessage("CYCLE requires WITH RECURSIVE")
939+
ExInst<SqlValidatorException> cycleRequiresRecursive();
940+
941+
@BaseMessage("CYCLE requires a UNION [ALL] with a recursive SELECT containing exactly one direct reference to its WITH item")
942+
ExInst<SqlValidatorException> cycleRequiresRecursiveSelect();
943+
944+
@BaseMessage("CYCLE is not supported with aggregation, DISTINCT, or window functions in the recursive SELECT")
945+
ExInst<SqlValidatorException> cycleRecursiveSelectNotSupported();
946+
947+
@BaseMessage("CYCLE column ''{0}'' is not a column of WITH item ''{1}''")
948+
ExInst<SqlValidatorException> cycleColumnNotFound(String column, String withItem);
949+
950+
@BaseMessage("CYCLE generated column ''{0}'' conflicts with another column")
951+
ExInst<SqlValidatorException> cycleColumnConflict(String column);
952+
953+
@BaseMessage("CYCLE mark and default must be non-null literals of compatible types with distinct values")
954+
ExInst<SqlValidatorException> cycleInvalidMarkValues();
955+
938956
@BaseMessage("First column of ORDER BY must be of type TIMESTAMP")
939957
ExInst<SqlValidatorException> firstColumnOfOrderByMustBeTimestamp();
940958

‎core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7145,6 +7145,18 @@ public static List compact(List list) {
71457145
return result;
71467146
}
71477147

7148+
/** Tests whether a recursive CTE path contains a key tuple.
7149+
* Implements the condition of {@code CASE WHEN key IN (path) THEN ...}:
7150+
* only TRUE comparisons close a cycle; UNKNOWN does not. */
7151+
public static boolean cyclePathContains(List<?> path, Object key) {
7152+
for (Object previous : path) {
7153+
if (Boolean.TRUE.equals(rowEq(previous, key))) {
7154+
return true;
7155+
}
7156+
}
7157+
return false;
7158+
}
7159+
71487160
/** Support the ARRAY_APPEND function. */
71497161
public static List arrayAppend(List list, Object element) {
71507162
final List result = new ArrayList(list.size() + 1);
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.calcite.sql;
18+
19+
import org.apache.calcite.sql.parser.SqlParserPos;
20+
21+
import com.google.common.collect.ImmutableList;
22+
23+
import org.checkerframework.checker.nullness.qual.Nullable;
24+
25+
import java.util.List;
26+
27+
import static java.util.Objects.requireNonNull;
28+
29+
/** SQL-standard cycle detection clause of a recursive common table expression.
30+
*
31+
* <p>Syntax: {@code CYCLE columns SET mark TO value DEFAULT default USING path}.
32+
* The mark and path columns are generated columns, not members of the WITH
33+
* item's explicit column list. */
34+
public class SqlCycleClause extends SqlCall {
35+
private static final SqlSpecialOperator OPERATOR =
36+
new SqlSpecialOperator("CYCLE", SqlKind.CYCLE) {
37+
@Override public SqlCall createCall(@Nullable SqlLiteral qualifier,
38+
SqlParserPos pos, @Nullable SqlNode... operands) {
39+
return new SqlCycleClause(pos,
40+
(SqlNodeList) requireNonNull(operands[0]),
41+
(SqlIdentifier) requireNonNull(operands[1]),
42+
requireNonNull(operands[2]), requireNonNull(operands[3]),
43+
(SqlIdentifier) requireNonNull(operands[4]));
44+
}
45+
};
46+
47+
public SqlNodeList columns;
48+
public SqlIdentifier markColumn;
49+
public SqlNode markValue;
50+
public SqlNode defaultValue;
51+
public SqlIdentifier pathColumn;
52+
53+
public SqlCycleClause(SqlParserPos pos, SqlNodeList columns,
54+
SqlIdentifier markColumn, SqlNode markValue, SqlNode defaultValue,
55+
SqlIdentifier pathColumn) {
56+
super(pos);
57+
this.columns = columns;
58+
this.markColumn = markColumn;
59+
this.markValue = markValue;
60+
this.defaultValue = defaultValue;
61+
this.pathColumn = pathColumn;
62+
}
63+
64+
@Override public SqlOperator getOperator() {
65+
return OPERATOR;
66+
}
67+
68+
@Override public List<SqlNode> getOperandList() {
69+
return ImmutableList.of(columns, markColumn, markValue, defaultValue, pathColumn);
70+
}
71+
72+
@Override public void setOperand(int i, @Nullable SqlNode operand) {
73+
requireNonNull(operand, "operand");
74+
switch (i) {
75+
case 0:
76+
columns = (SqlNodeList) operand;
77+
break;
78+
case 1:
79+
markColumn = (SqlIdentifier) operand;
80+
break;
81+
case 2:
82+
markValue = operand;
83+
break;
84+
case 3:
85+
defaultValue = operand;
86+
break;
87+
case 4:
88+
pathColumn = (SqlIdentifier) operand;
89+
break;
90+
default:
91+
throw new AssertionError(i);
92+
}
93+
}
94+
95+
@Override public void unparse(SqlWriter writer, int leftPrec, int rightPrec) {
96+
writer.keyword("CYCLE");
97+
final SqlWriter.Frame frame = writer.startList("", "");
98+
for (SqlNode column : columns) {
99+
writer.sep(",");
100+
column.unparse(writer, 0, 0);
101+
}
102+
writer.endList(frame);
103+
writer.keyword("SET");
104+
markColumn.unparse(writer, 0, 0);
105+
writer.keyword("TO");
106+
markValue.unparse(writer, 0, 0);
107+
writer.keyword("DEFAULT");
108+
defaultValue.unparse(writer, 0, 0);
109+
writer.keyword("USING");
110+
pathColumn.unparse(writer, 0, 0);
111+
}
112+
}

‎core/src/main/java/org/apache/calcite/sql/SqlKind.java‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,9 @@ public enum SqlKind {
204204
/** Item in WITH clause. */
205205
WITH_ITEM,
206206

207+
/** CYCLE clause of a recursive WITH item. */
208+
CYCLE,
209+
207210
/** Represents a recursive CTE as a table ref. */
208211
WITH_ITEM_TABLE_REF,
209212

@@ -1492,7 +1495,7 @@ public enum SqlKind {
14921495
LITERAL_CHAIN, JDBC_FN, PRECEDING, FOLLOWING, ORDER_BY,
14931496
ORDER_BY_ALL,
14941497
NULLS_FIRST, NULLS_LAST, COLLECTION_TABLE, TABLESAMPLE,
1495-
VALUES, WITH, WITH_ITEM, ITEM, SKIP_TO_FIRST, SKIP_TO_LAST,
1498+
VALUES, WITH, WITH_ITEM, CYCLE, ITEM, SKIP_TO_FIRST, SKIP_TO_LAST,
14961499
JSON_VALUE_EXPRESSION, UNNEST),
14971500
SET_QUERY, AGGREGATE, DML, DDL));
14981501

‎core/src/main/java/org/apache/calcite/sql/SqlWithItem.java‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@
2525

2626
/**
2727
* An item in a WITH clause of a query.
28-
* It has a name, an optional column list, and a query.
28+
* It has a name, an optional column list, a query, and an optional CYCLE clause.
2929
*/
3030
public class SqlWithItem extends SqlCall {
3131
public SqlIdentifier name;
3232
public @Nullable SqlNodeList columnList; // may be null
3333
public SqlLiteral recursive;
3434
public SqlNode query;
35+
public @Nullable SqlCycleClause cycleClause;
3536

3637
@Deprecated // to be removed before 2.0
3738
public SqlWithItem(SqlParserPos pos, SqlIdentifier name,
@@ -43,11 +44,18 @@ public SqlWithItem(SqlParserPos pos, SqlIdentifier name,
4344
public SqlWithItem(SqlParserPos pos, SqlIdentifier name,
4445
@Nullable SqlNodeList columnList, SqlNode query,
4546
SqlLiteral recursive) {
47+
this(pos, name, columnList, query, recursive, null);
48+
}
49+
50+
public SqlWithItem(SqlParserPos pos, SqlIdentifier name,
51+
@Nullable SqlNodeList columnList, SqlNode query,
52+
SqlLiteral recursive, @Nullable SqlCycleClause cycleClause) {
4653
super(pos);
4754
this.name = name;
4855
this.columnList = columnList;
4956
this.recursive = recursive;
5057
this.query = query;
58+
this.cycleClause = cycleClause;
5159
}
5260

5361
//~ Methods ----------------------------------------------------------------
@@ -58,7 +66,7 @@ public SqlWithItem(SqlParserPos pos, SqlIdentifier name,
5866

5967
@SuppressWarnings("nullness")
6068
@Override public List<SqlNode> getOperandList() {
61-
return ImmutableNullableList.of(name, columnList, query, recursive);
69+
return ImmutableNullableList.of(name, columnList, query, recursive, cycleClause);
6270
}
6371

6472
@SuppressWarnings("assignment.type.incompatible")
@@ -76,6 +84,9 @@ public SqlWithItem(SqlParserPos pos, SqlIdentifier name,
7684
case 3:
7785
recursive = (SqlLiteral) operand;
7886
break;
87+
case 4:
88+
cycleClause = (SqlCycleClause) operand;
89+
break;
7990
default:
8091
throw new AssertionError(i);
8192
}
@@ -111,15 +122,19 @@ private static class SqlWithItemOperator extends SqlSpecialOperator {
111122
}
112123
writer.keyword("AS");
113124
withItem.query.unparse(writer, MDX_PRECEDENCE, MDX_PRECEDENCE);
125+
if (withItem.cycleClause != null) {
126+
withItem.cycleClause.unparse(writer, 0, 0);
127+
}
114128
}
115129

116130
@SuppressWarnings("argument.type.incompatible")
117131
@Override public SqlCall createCall(@Nullable SqlLiteral functionQualifier,
118132
SqlParserPos pos, @Nullable SqlNode... operands) {
119133
assert functionQualifier == null;
120-
assert operands.length == 4;
134+
assert operands.length == 4 || operands.length == 5;
121135
return new SqlWithItem(pos, (SqlIdentifier) operands[0],
122-
(SqlNodeList) operands[1], operands[2], (SqlLiteral) operands[3]);
136+
(SqlNodeList) operands[1], operands[2], (SqlLiteral) operands[3],
137+
operands.length == 5 ? (SqlCycleClause) operands[4] : null);
123138
}
124139
}
125140
}

‎core/src/main/java/org/apache/calcite/sql/fun/SqlInternalOperators.java‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@ public abstract class SqlInternalOperators {
4949
private SqlInternalOperators() {
5050
}
5151

52+
/** Appends a key tuple to the path of a recursive CTE. */
53+
public static final SqlInternalOperator CYCLE_PATH_APPEND =
54+
new SqlInternalOperator("$CYCLE_PATH_APPEND", SqlKind.OTHER_FUNCTION,
55+
2, true, ReturnTypes.ARG0, null, OperandTypes.ANY_ANY);
56+
57+
/** Tests whether a key tuple occurs in a recursive CTE's path. */
58+
public static final SqlInternalOperator CYCLE_PATH_CONTAINS =
59+
new SqlInternalOperator("$CYCLE_PATH_CONTAINS", SqlKind.OTHER_FUNCTION,
60+
2, true, ReturnTypes.BOOLEAN_NOT_NULL, null, OperandTypes.ANY_ANY);
61+
5262
/** Similar to {@link SqlStdOperatorTable#ROW}, but does not print "ROW".
5363
*
5464
* <p>For arguments [1, TRUE], ROW would print "{@code ROW (1, TRUE)}",

0 commit comments

Comments
 (0)