Skip to content

Commit dd2d9d4

Browse files
committed
Support recursive CTE CYCLE clauses with shared recursive clause rendering
1 parent bc18fdf commit dd2d9d4

9 files changed

Lines changed: 330 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
@@ -290,6 +290,9 @@ public <S> T visit(SetOperationList setOpList, S context) {
290290
*/
291291
@Override
292292
public <S> T visit(WithItem<?> withItem, S context) {
293+
if (withItem.getCycleClause() != null) {
294+
withItem.getCycleClause().accept(expressionVisitor, context);
295+
}
293296
ParenthesedStatement body = withItem.getParenthesedStatement();
294297

295298
// 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
@@ -328,6 +328,9 @@ public <S> Void visit(WithItem<?> withItem, S context) {
328328
if (withItem.getAlias() != null) {
329329
otherItemNames.add(withItem.getAlias().getName());
330330
}
331+
if (withItem.getCycleClause() != null) {
332+
withItem.getCycleClause().accept(this, context);
333+
}
331334
// dispatch any ParenthesedStatement payload (Select, Delete, Update, Insert)
332335
withItem.accept((StatementVisitor<?>) this, context);
333336
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
@@ -902,9 +902,8 @@ public <S> StringBuilder visit(WithItem<?> withItem, S context) {
902902
StatementDeParser statementDeParser =
903903
new StatementDeParser((ExpressionDeParser) expressionVisitor, this, builder);
904904
statementDeParser.deParse(withItem.getParenthesedStatement());
905-
if (withItem.getSearchClause() != null) {
906-
builder.append(" ").append(withItem.getSearchClause());
907-
}
905+
withItem.appendRecursiveClausesTo(builder,
906+
expression -> expression.accept(expressionVisitor, context));
908907
} else {
909908
builder.append(withItem.getWithFunctionDeclaration().toString());
910909
}

‎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
@@ -390,6 +390,9 @@ public <S> Void visit(WithItem<?> withItem, S context) {
390390
if (isNotEmpty(withItem.getWithItemList())) {
391391
withItem.getWithItemList().forEach(wi -> wi.accept(this, context));
392392
}
393+
if (withItem.getCycleClause() != null) {
394+
withItem.getCycleClause().accept(getValidator(ExpressionValidator.class), context);
395+
}
393396
withItem.getSelect().accept((SelectVisitor<?>) this, context);
394397
return null;
395398
}

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6279,6 +6279,7 @@ WithItem<?> WithItem() #WithItem:
62796279
WithFunctionDeclaration withFunctionDeclaration = null;
62806280
ParenthesedStatement statement = null;
62816281
WithSearchClause withSearchClause = null;
6282+
WithCycleClause withCycleClause = null;
62826283
WithItem<?> withItem;
62836284
}
62846285
{
@@ -6312,6 +6313,7 @@ WithItem<?> WithItem() #WithItem:
63126313
)
63136314
)
63146315
[ withSearchClause = WithSearchClause() { withItem.setSearchClause(withSearchClause); } ]
6316+
[ withCycleClause = WithCycleClause() { withItem.setCycleClause(withCycleClause); } ]
63156317
{
63166318
return withItem;
63176319
}
@@ -6342,6 +6344,24 @@ WithSearchClause WithSearchClause() #WithSearchClause:
63426344
}
63436345
}
63446346

6347+
WithCycleClause WithCycleClause() #WithCycleClause:
6348+
{
6349+
WithCycleClause cycle = new WithCycleClause();
6350+
ExpressionList<Column> columns = new ExpressionList<Column>();
6351+
String name;
6352+
Expression value;
6353+
}
6354+
{
6355+
<K_CYCLE>
6356+
name=RelObjectName() { columns.add(new Column(name)); }
6357+
( "," name=RelObjectName() { columns.add(new Column(name)); } )*
6358+
<K_SET> name=RelObjectName() { cycle.setMarkColumnName(name); }
6359+
[ <K_TO> value=Expression() { cycle.setMarkValue(value); }
6360+
<K_DEFAULT> value=Expression() { cycle.setMarkDefault(value); } ]
6361+
<K_USING> name=RelObjectName() { cycle.setPathColumnName(name); }
6362+
{ return cycle.withCycleColumns(columns); }
6363+
}
6364+
63456365
WithFunctionDeclaration WithFunctionDeclaration() #WithFunctionDeclaration:
63466366
{
63476367
String functionName;

0 commit comments

Comments
 (0)