Skip to content

Commit a765cf7

Browse files
hayssamsclaude
andcommitted
Support DuckDB 2.0 recursive CTEs with USING KEY
Adds WITH RECURSIVE tbl (a, b) USING KEY (a, avg(b)) AS (...), which aggregates a recursive CTE by key instead of accumulating every row. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2
1 parent daa8118 commit a765cf7

4 files changed

Lines changed: 53 additions & 0 deletions

File tree

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import java.util.function.Consumer;
1313
import net.sf.jsqlparser.expression.Expression;
14+
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
1415

1516
import java.io.Serializable;
1617
import java.util.ArrayList;
@@ -35,6 +36,7 @@ public class WithItem<K extends ParenthesedStatement> implements Serializable {
3536
private boolean recursive = false;
3637
private boolean usingNot = false;
3738
private boolean materialized = false;
39+
private ExpressionList<Expression> usingKeyExpressions;
3840

3941
public WithItem(K statement, Alias alias) {
4042
this.statement = statement;
@@ -166,6 +168,23 @@ public WithItem<K> withCycleClause(WithCycleClause cycleClause) {
166168
return this;
167169
}
168170

171+
/**
172+
* DuckDB 2.0 aggregates a recursive CTE by key:
173+
* {@code WITH RECURSIVE tbl (a, b) USING KEY (a, avg(b)) AS (...)}.
174+
*/
175+
public ExpressionList<Expression> getUsingKeyExpressions() {
176+
return usingKeyExpressions;
177+
}
178+
179+
public void setUsingKeyExpressions(ExpressionList<Expression> usingKeyExpressions) {
180+
this.usingKeyExpressions = usingKeyExpressions;
181+
}
182+
183+
public WithItem<K> withUsingKeyExpressions(ExpressionList<Expression> usingKeyExpressions) {
184+
setUsingKeyExpressions(usingKeyExpressions);
185+
return this;
186+
}
187+
169188
public StringBuilder appendRecursiveClausesTo(StringBuilder builder,
170189
Consumer<Expression> expressionPrinter) {
171190
if (searchClause != null) {
@@ -197,6 +216,9 @@ public String toString() {
197216
}
198217
builder.append(")");
199218
}
219+
if (usingKeyExpressions != null) {
220+
builder.append(" USING KEY (").append(usingKeyExpressions).append(")");
221+
}
200222
builder.append(" AS ");
201223
if (materialized) {
202224
builder.append(usingNot

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ <S> StringBuilder deparseWithItem(WithItem<?> item, S context) {
9393
builder.append(' ')
9494
.append(PlainSelect.getStringList(item.getWithItemList(), true, true));
9595
}
96+
if (item.getUsingKeyExpressions() != null) {
97+
builder.append(" USING KEY (").append(item.getUsingKeyExpressions()).append(")");
98+
}
9699
builder.append(" AS ");
97100
if (item.isMaterialized()) {
98101
builder.append(item.isUsingNot() ? "NOT MATERIALIZED " : "MATERIALIZED ");

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7374,6 +7374,7 @@ WithItem<?> WithItem() #WithItem:
73747374
ParenthesedStatement statement = null;
73757375
WithSearchClause withSearchClause = null;
73767376
WithCycleClause withCycleClause = null;
7377+
ExpressionList<Expression> usingKeyExpressions = null;
73777378
WithItem<?> withItem;
73787379
}
73797380
{
@@ -7388,6 +7389,8 @@ WithItem<?> WithItem() #WithItem:
73887389
[ LOOKAHEAD(2) <K_RECURSIVE> { recursive = true; } ]
73897390
name=RelObjectName()
73907391
[ "(" selectItems=SelectItemsList() ")" ]
7392+
// DuckDB 2.0: WITH RECURSIVE tbl (a, b) USING KEY (a, avg(b)) AS (...)
7393+
[ LOOKAHEAD(2) <K_USING> <K_KEY> "(" usingKeyExpressions=ExpressionList() ")" ]
73917394
<K_AS>
73927395
[ LOOKAHEAD(2) [ <K_NOT> { usingNot = true; } ] <K_MATERIALIZED> { materialized = true; } ]
73937396
(
@@ -7403,6 +7406,7 @@ WithItem<?> WithItem() #WithItem:
74037406
withItem = new WithItem(statement, new Alias(name, false))
74047407
.withRecursive(recursive, usingNot, materialized)
74057408
.withWithItemList(selectItems);
7409+
withItem.setUsingKeyExpressions(usingKeyExpressions);
74067410
}
74077411
)
74087412
)

src/test/java/net/sf/jsqlparser/statement/select/DuckDBTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,4 +665,28 @@ void testApproxRemainsUsableAsIdentifier() throws JSQLParserException {
665665
String sqlStr = "SELECT approx, nearest, similarity FROM t";
666666
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
667667
}
668+
669+
@Test
670+
void testRecursiveCteUsingKey() throws JSQLParserException {
671+
String sqlStr = "WITH RECURSIVE tbl (a, b) USING KEY (a, avg(b)) AS "
672+
+ "(SELECT 1, 2 UNION ALL SELECT a + 1, b FROM tbl WHERE a < 3) SELECT * FROM tbl";
673+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
674+
WithItem<?> withItem = select.getWithItemsList().get(0);
675+
676+
Assertions.assertEquals(2, withItem.getUsingKeyExpressions().size());
677+
}
678+
679+
@Test
680+
void testRecursiveCteUsingKeySingleColumn() throws JSQLParserException {
681+
String sqlStr = "WITH RECURSIVE tbl USING KEY (a) AS (SELECT 1 AS a) SELECT * FROM tbl";
682+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
683+
}
684+
685+
@Test
686+
void testRecursiveCteWithoutUsingKey() throws JSQLParserException {
687+
String sqlStr = "WITH RECURSIVE tbl (a) AS (SELECT 1) SELECT * FROM tbl";
688+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
689+
690+
Assertions.assertNull(select.getWithItemsList().get(0).getUsingKeyExpressions());
691+
}
668692
}

0 commit comments

Comments
 (0)