Skip to content

Commit eddb1fb

Browse files
authored
Support recursive CTE CYCLE clauses with shared recursive clause rendering (#2566)
1 parent c1a611d commit eddb1fb

9 files changed

Lines changed: 350 additions & 14 deletions

File tree

src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,9 @@ public <S> T visit(SetOperationList setOpList, S context) {
297297
*/
298298
@Override
299299
public <S> T visit(WithItem<?> withItem, S context) {
300+
if (withItem.getCycleClause() != null) {
301+
withItem.getCycleClause().accept(expressionVisitor, context);
302+
}
300303
ParenthesedStatement body = withItem.getParenthesedStatement();
301304

302305
// ParenthesedSelect is a Select and stays on the select path
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.select;
11+
12+
import java.io.Serializable;
13+
import java.util.function.Consumer;
14+
import net.sf.jsqlparser.expression.Expression;
15+
import net.sf.jsqlparser.expression.ExpressionVisitor;
16+
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
17+
import net.sf.jsqlparser.schema.Column;
18+
19+
/** Cycle detection metadata for a recursive common table expression. */
20+
public class WithCycleClause implements Serializable {
21+
private ExpressionList<Column> cycleColumns;
22+
private String markColumnName;
23+
private Expression markValue;
24+
private Expression markDefault;
25+
private String pathColumnName;
26+
27+
public ExpressionList<Column> getCycleColumns() {
28+
return cycleColumns;
29+
}
30+
31+
public void setCycleColumns(ExpressionList<Column> cycleColumns) {
32+
this.cycleColumns = cycleColumns;
33+
}
34+
35+
public WithCycleClause withCycleColumns(ExpressionList<Column> cycleColumns) {
36+
setCycleColumns(cycleColumns);
37+
return this;
38+
}
39+
40+
public String getMarkColumnName() {
41+
return markColumnName;
42+
}
43+
44+
public void setMarkColumnName(String markColumnName) {
45+
this.markColumnName = markColumnName;
46+
}
47+
48+
public WithCycleClause withMarkColumnName(String markColumnName) {
49+
setMarkColumnName(markColumnName);
50+
return this;
51+
}
52+
53+
public Expression getMarkValue() {
54+
return markValue;
55+
}
56+
57+
public void setMarkValue(Expression markValue) {
58+
this.markValue = markValue;
59+
}
60+
61+
public WithCycleClause withMarkValue(Expression markValue) {
62+
setMarkValue(markValue);
63+
return this;
64+
}
65+
66+
public Expression getMarkDefault() {
67+
return markDefault;
68+
}
69+
70+
public void setMarkDefault(Expression markDefault) {
71+
this.markDefault = markDefault;
72+
}
73+
74+
public WithCycleClause withMarkDefault(Expression markDefault) {
75+
setMarkDefault(markDefault);
76+
return this;
77+
}
78+
79+
public String getPathColumnName() {
80+
return pathColumnName;
81+
}
82+
83+
public void setPathColumnName(String pathColumnName) {
84+
this.pathColumnName = pathColumnName;
85+
}
86+
87+
public WithCycleClause withPathColumnName(String pathColumnName) {
88+
setPathColumnName(pathColumnName);
89+
return this;
90+
}
91+
92+
public <S> void accept(ExpressionVisitor<?> visitor, S context) {
93+
if (cycleColumns != null) {
94+
cycleColumns.forEach(column -> column.accept(visitor, context));
95+
}
96+
if (markValue != null) {
97+
markValue.accept(visitor, context);
98+
}
99+
if (markDefault != null) {
100+
markDefault.accept(visitor, context);
101+
}
102+
}
103+
104+
public StringBuilder appendTo(StringBuilder builder, Consumer<Expression> expressionPrinter) {
105+
builder.append("CYCLE ");
106+
if (cycleColumns != null) {
107+
for (int i = 0; i < cycleColumns.size(); i++) {
108+
if (i > 0) {
109+
builder.append(", ");
110+
}
111+
expressionPrinter.accept(cycleColumns.get(i));
112+
}
113+
}
114+
builder.append(" SET ").append(markColumnName);
115+
if (markValue != null) {
116+
builder.append(" TO ");
117+
expressionPrinter.accept(markValue);
118+
builder.append(" DEFAULT ");
119+
expressionPrinter.accept(markDefault);
120+
}
121+
return builder.append(" USING ").append(pathColumnName);
122+
}
123+
124+
@Override
125+
public String toString() {
126+
StringBuilder builder = new StringBuilder();
127+
return appendTo(builder, expression -> builder.append(expression)).toString();
128+
}
129+
}

src/main/java/net/sf/jsqlparser/statement/select/WithItem.java

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
*/
1010
package net.sf.jsqlparser.statement.select;
1111

12+
import java.util.function.Consumer;
13+
import net.sf.jsqlparser.expression.Expression;
14+
1215
import java.io.Serializable;
1316
import java.util.ArrayList;
1417
import java.util.Collection;
@@ -28,6 +31,7 @@ public class WithItem<K extends ParenthesedStatement> implements Serializable {
2831
private List<SelectItem<?>> withItemList;
2932
private WithFunctionDeclaration withFunctionDeclaration;
3033
private WithSearchClause searchClause;
34+
private WithCycleClause cycleClause;
3135
private boolean recursive = false;
3236
private boolean usingNot = false;
3337
private boolean materialized = false;
@@ -149,6 +153,32 @@ public WithItem<K> withSearchClause(WithSearchClause searchClause) {
149153
return this;
150154
}
151155

156+
public WithCycleClause getCycleClause() {
157+
return cycleClause;
158+
}
159+
160+
public void setCycleClause(WithCycleClause cycleClause) {
161+
this.cycleClause = cycleClause;
162+
}
163+
164+
public WithItem<K> withCycleClause(WithCycleClause cycleClause) {
165+
setCycleClause(cycleClause);
166+
return this;
167+
}
168+
169+
public StringBuilder appendRecursiveClausesTo(StringBuilder builder,
170+
Consumer<Expression> expressionPrinter) {
171+
if (searchClause != null) {
172+
builder.append(" ");
173+
searchClause.appendTo(builder, expressionPrinter);
174+
}
175+
if (cycleClause != null) {
176+
builder.append(" ");
177+
cycleClause.appendTo(builder, expressionPrinter);
178+
}
179+
return builder;
180+
}
181+
152182
@Override
153183
public String toString() {
154184
StringBuilder builder = new StringBuilder();
@@ -174,9 +204,7 @@ public String toString() {
174204
: "MATERIALIZED ");
175205
}
176206
builder.append(statement);
177-
if (searchClause != null) {
178-
builder.append(" ").append(searchClause);
179-
}
207+
appendRecursiveClausesTo(builder, expression -> builder.append(expression));
180208
}
181209
return builder.toString();
182210
}

src/main/java/net/sf/jsqlparser/statement/select/WithSearchClause.java

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
*/
1010
package net.sf.jsqlparser.statement.select;
1111

12+
import java.util.function.Consumer;
13+
import net.sf.jsqlparser.expression.Expression;
14+
1215
import java.io.Serializable;
1316
import java.util.Collection;
1417

@@ -86,15 +89,23 @@ public WithSearchClause withSequenceColumnName(String sequenceColumnName) {
8689
return this;
8790
}
8891

92+
public StringBuilder appendTo(StringBuilder builder,
93+
Consumer<Expression> expressionPrinter) {
94+
builder.append("SEARCH ").append(searchOrder).append(" FIRST BY ");
95+
if (searchColumns != null) {
96+
for (int i = 0; i < searchColumns.size(); i++) {
97+
if (i > 0) {
98+
builder.append(", ");
99+
}
100+
expressionPrinter.accept(searchColumns.get(i));
101+
}
102+
}
103+
return builder.append(" SET ").append(sequenceColumnName);
104+
}
105+
89106
@Override
90107
public String toString() {
91-
return new StringBuilder()
92-
.append("SEARCH ")
93-
.append(searchOrder)
94-
.append(" FIRST BY ")
95-
.append(Select.getStringList(searchColumns))
96-
.append(" SET ")
97-
.append(sequenceColumnName)
98-
.toString();
108+
StringBuilder builder = new StringBuilder();
109+
return appendTo(builder, expression -> builder.append(expression)).toString();
99110
}
100111
}

src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,9 @@ public <S> Void visit(WithItem<?> withItem, S context) {
333333
if (withItem.getAlias() != null) {
334334
otherItemNames.add(withItem.getAlias().getName());
335335
}
336+
if (withItem.getCycleClause() != null) {
337+
withItem.getCycleClause().accept(this, context);
338+
}
336339
// dispatch any ParenthesedStatement payload (Select, Delete, Update, Insert)
337340
withItem.accept((StatementVisitor<?>) this, context);
338341
return null;

src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -900,9 +900,8 @@ public <S> StringBuilder visit(WithItem<?> withItem, S context) {
900900
StatementDeParser statementDeParser =
901901
new StatementDeParser((ExpressionDeParser) expressionVisitor, this, builder);
902902
statementDeParser.deParse(withItem.getParenthesedStatement());
903-
if (withItem.getSearchClause() != null) {
904-
builder.append(" ").append(withItem.getSearchClause());
905-
}
903+
withItem.appendRecursiveClausesTo(builder,
904+
expression -> expression.accept(expressionVisitor, context));
906905
} else {
907906
builder.append(withItem.getWithFunctionDeclaration().toString());
908907
}

src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,9 @@ public <S> Void visit(WithItem<?> withItem, S context) {
394394
if (isNotEmpty(withItem.getWithItemList())) {
395395
withItem.getWithItemList().forEach(wi -> wi.accept(this, context));
396396
}
397+
if (withItem.getCycleClause() != null) {
398+
withItem.getCycleClause().accept(getValidator(ExpressionValidator.class), context);
399+
}
397400
withItem.accept(getValidator(StatementValidator.class), context);
398401
return null;
399402
}

src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6422,6 +6422,7 @@ WithItem<?> WithItem() #WithItem:
64226422
WithFunctionDeclaration withFunctionDeclaration = null;
64236423
ParenthesedStatement statement = null;
64246424
WithSearchClause withSearchClause = null;
6425+
WithCycleClause withCycleClause = null;
64256426
WithItem<?> withItem;
64266427
}
64276428
{
@@ -6455,6 +6456,7 @@ WithItem<?> WithItem() #WithItem:
64556456
)
64566457
)
64576458
[ withSearchClause = WithSearchClause() { withItem.setSearchClause(withSearchClause); } ]
6459+
[ withCycleClause = WithCycleClause() { withItem.setCycleClause(withCycleClause); } ]
64586460
{
64596461
return withItem;
64606462
}
@@ -6485,6 +6487,24 @@ WithSearchClause WithSearchClause() #WithSearchClause:
64856487
}
64866488
}
64876489

6490+
WithCycleClause WithCycleClause() #WithCycleClause:
6491+
{
6492+
WithCycleClause cycle = new WithCycleClause();
6493+
ExpressionList<Column> columns = new ExpressionList<Column>();
6494+
String name;
6495+
Expression value;
6496+
}
6497+
{
6498+
<K_CYCLE>
6499+
name=RelObjectName() { columns.add(new Column(name)); }
6500+
( "," name=RelObjectName() { columns.add(new Column(name)); } )*
6501+
<K_SET> name=RelObjectName() { cycle.setMarkColumnName(name); }
6502+
[ <K_TO> value=Expression() { cycle.setMarkValue(value); }
6503+
<K_DEFAULT> value=Expression() { cycle.setMarkDefault(value); } ]
6504+
<K_USING> name=RelObjectName() { cycle.setPathColumnName(name); }
6505+
{ return cycle.withCycleColumns(columns); }
6506+
}
6507+
64886508
WithFunctionDeclaration WithFunctionDeclaration() #WithFunctionDeclaration:
64896509
{
64906510
String functionName;

0 commit comments

Comments
 (0)