Skip to content

Commit 6f6dea4

Browse files
hayssamsclaude
andcommitted
Support DuckDB UNPIVOT statement
Adds the simplified statement form UNPIVOT tbl ON columns [INTO NAME name VALUE values] as UnPivotQuery, the counterpart of the existing PivotQuery. The FROM-clause form UNPIVOT (...) is unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2
1 parent 0f685b8 commit 6f6dea4

10 files changed

Lines changed: 246 additions & 0 deletions

File tree

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,14 @@ default void visit(PivotQuery pivotQuery) {
9292
this.visit(pivotQuery, null);
9393
}
9494

95+
default <S> T visit(UnPivotQuery unPivotQuery, S context) {
96+
return null;
97+
}
98+
99+
default void visit(UnPivotQuery unPivotQuery) {
100+
this.visit(unPivotQuery, null);
101+
}
102+
95103
<S> T visit(SetOperationList setOperationList, S context);
96104

97105
default void visit(SetOperationList setOperationList) {

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,11 @@ public <S> T visit(PivotQuery pivotQuery, S context) {
129129
return pivotQuery.accept(selectVisitor, context);
130130
}
131131

132+
@Override
133+
public <S> T visit(UnPivotQuery unPivotQuery, S context) {
134+
return unPivotQuery.accept(selectVisitor, context);
135+
}
136+
132137
@Override
133138
public <S> T visit(SetOperationList setOperationList, S context) {
134139
ArrayList<T> results = new ArrayList<>();

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ default void visit(PivotQuery pivotQuery) {
4747
this.visit(pivotQuery, null);
4848
}
4949

50+
default <S> T visit(UnPivotQuery unPivotQuery, S context) {
51+
return null;
52+
}
53+
54+
default void visit(UnPivotQuery unPivotQuery) {
55+
this.visit(unPivotQuery, null);
56+
}
57+
5058
<S> T visit(FromQuery fromQuery, S context);
5159

5260
<S> T visit(SetOperationList setOpList, S context);

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,16 @@ public <S> T visit(PivotQuery pivotQuery, S context) {
272272
return null;
273273
}
274274

275+
@Override
276+
public <S> T visit(UnPivotQuery unPivotQuery, S context) {
277+
visitWithItems(unPivotQuery.getWithItemsList(), context);
278+
279+
fromItemVisitor.visitFromItem(unPivotQuery.getFromItem(), context);
280+
expressionVisitor.visitExpressions(unPivotQuery.getOnExpressions(), context);
281+
expressionVisitor.visitExpressions(unPivotQuery.getValueColumns(), context);
282+
return null;
283+
}
284+
275285
@Override
276286
public <S> T visit(FromQuery fromQuery, S context) {
277287
return null;
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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 net.sf.jsqlparser.expression.Expression;
13+
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
14+
import net.sf.jsqlparser.schema.Column;
15+
16+
/**
17+
* DuckDB's simplified UNPIVOT statement:
18+
* {@code UNPIVOT tbl ON columns [INTO NAME name VALUE values]}, the counterpart of
19+
* {@link PivotQuery}.
20+
*
21+
* @see <a href="https://duckdb.org/docs/stable/sql/statements/unpivot">UNPIVOT</a>
22+
*/
23+
public class UnPivotQuery extends Select {
24+
private FromItem fromItem;
25+
private ExpressionList<Expression> onExpressions;
26+
private String nameColumn;
27+
private ExpressionList<Column> valueColumns;
28+
29+
public FromItem getFromItem() {
30+
return fromItem;
31+
}
32+
33+
public void setFromItem(FromItem fromItem) {
34+
this.fromItem = fromItem;
35+
}
36+
37+
public UnPivotQuery withFromItem(FromItem fromItem) {
38+
setFromItem(fromItem);
39+
return this;
40+
}
41+
42+
public ExpressionList<Expression> getOnExpressions() {
43+
return onExpressions;
44+
}
45+
46+
public void setOnExpressions(ExpressionList<Expression> onExpressions) {
47+
this.onExpressions = onExpressions;
48+
}
49+
50+
public UnPivotQuery withOnExpressions(ExpressionList<Expression> onExpressions) {
51+
setOnExpressions(onExpressions);
52+
return this;
53+
}
54+
55+
public String getNameColumn() {
56+
return nameColumn;
57+
}
58+
59+
public void setNameColumn(String nameColumn) {
60+
this.nameColumn = nameColumn;
61+
}
62+
63+
public UnPivotQuery withNameColumn(String nameColumn) {
64+
setNameColumn(nameColumn);
65+
return this;
66+
}
67+
68+
public ExpressionList<Column> getValueColumns() {
69+
return valueColumns;
70+
}
71+
72+
public void setValueColumns(ExpressionList<Column> valueColumns) {
73+
this.valueColumns = valueColumns;
74+
}
75+
76+
public UnPivotQuery withValueColumns(ExpressionList<Column> valueColumns) {
77+
setValueColumns(valueColumns);
78+
return this;
79+
}
80+
81+
@Override
82+
public StringBuilder appendSelectBodyTo(StringBuilder builder) {
83+
builder.append("UNPIVOT ").append(fromItem);
84+
if (onExpressions != null) {
85+
builder.append(" ON ").append(onExpressions);
86+
}
87+
if (nameColumn != null) {
88+
builder.append(" INTO NAME ").append(nameColumn);
89+
if (valueColumns != null) {
90+
builder.append(" VALUE ").append(valueColumns);
91+
}
92+
}
93+
return builder;
94+
}
95+
96+
@Override
97+
public <T, S> T accept(SelectVisitor<T> selectVisitor, S context) {
98+
return selectVisitor.visit(this, context);
99+
}
100+
101+
@Override
102+
public <T, S> T accept(FromItemVisitor<T> fromItemVisitor, S context) {
103+
return fromItemVisitor.visit(this, context);
104+
}
105+
106+
@Override
107+
public SampleClause getSampleClause() {
108+
return null;
109+
}
110+
111+
@Override
112+
public FromItem setSampleClause(SampleClause sampleClause) {
113+
return null;
114+
}
115+
}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@
189189
import net.sf.jsqlparser.statement.select.ParenthesedSelect;
190190
import net.sf.jsqlparser.statement.select.Pivot;
191191
import net.sf.jsqlparser.statement.select.PivotQuery;
192+
import net.sf.jsqlparser.statement.select.UnPivotQuery;
192193
import net.sf.jsqlparser.statement.select.PivotVisitor;
193194
import net.sf.jsqlparser.statement.select.PivotXml;
194195
import net.sf.jsqlparser.statement.select.PlainSelect;
@@ -494,6 +495,20 @@ public <S> Void visit(PivotQuery pivotQuery, S context) {
494495
return null;
495496
}
496497

498+
@Override
499+
public <S> Void visit(UnPivotQuery unPivotQuery, S context) {
500+
visitWithItems(unPivotQuery.getWithItemsList(), context);
501+
visitFromItem(unPivotQuery.getFromItem(), context);
502+
visitExpressions(unPivotQuery.getOnExpressions(), context);
503+
visitExpressions(unPivotQuery.getValueColumns(), context);
504+
return null;
505+
}
506+
507+
@Override
508+
public void visit(UnPivotQuery unPivotQuery) {
509+
SelectVisitor.super.visit(unPivotQuery);
510+
}
511+
497512
private <S> void visitSelectItems(List<? extends SelectItem<?>> selectItems, S context) {
498513
if (selectItems != null) {
499514
for (SelectItem<?> item : selectItems) {

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
import net.sf.jsqlparser.statement.select.ParenthesedSelect;
6464
import net.sf.jsqlparser.statement.select.Pivot;
6565
import net.sf.jsqlparser.statement.select.PivotQuery;
66+
import net.sf.jsqlparser.statement.select.UnPivotQuery;
6667
import net.sf.jsqlparser.statement.select.PivotVisitor;
6768
import net.sf.jsqlparser.statement.select.PivotXml;
6869
import net.sf.jsqlparser.statement.select.PlainSelect;
@@ -436,6 +437,17 @@ private <S> void deparseFromClause(PlainSelect plainSelect, S context) {
436437
}
437438

438439

440+
@Override
441+
public <S> StringBuilder visit(UnPivotQuery unPivotQuery, S context) {
442+
unPivotQuery.appendSelectBodyTo(builder);
443+
return builder;
444+
}
445+
446+
@Override
447+
public void visit(UnPivotQuery unPivotQuery) {
448+
SelectVisitor.super.visit(unPivotQuery);
449+
}
450+
439451
@Override
440452
public <S> StringBuilder visit(PivotQuery pivotQuery, S context) {
441453
List<WithItem<?>> withItemsList = pivotQuery.getWithItemsList();

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import net.sf.jsqlparser.statement.select.ParenthesedSelect;
3535
import net.sf.jsqlparser.statement.select.Pivot;
3636
import net.sf.jsqlparser.statement.select.PivotQuery;
37+
import net.sf.jsqlparser.statement.select.UnPivotQuery;
3738
import net.sf.jsqlparser.statement.select.PivotVisitor;
3839
import net.sf.jsqlparser.statement.select.PivotXml;
3940
import net.sf.jsqlparser.statement.select.PlainSelect;
@@ -160,6 +161,14 @@ public <S> Void visit(PlainSelect plainSelect, S context) {
160161
return null;
161162
}
162163

164+
@Override
165+
public <S> Void visit(UnPivotQuery unPivotQuery, S context) {
166+
validateFeature(Feature.pivot);
167+
validateOptionalFromItem(unPivotQuery.getFromItem());
168+
validateOptionalExpressions(unPivotQuery.getOnExpressions());
169+
return null;
170+
}
171+
163172
@Override
164173
public <S> Void visit(PivotQuery pivotQuery, S context) {
165174
if (isNotEmpty(pivotQuery.getWithItemsList())) {
@@ -464,6 +473,11 @@ public void visit(PivotQuery pivotQuery) {
464473
visit(pivotQuery, null);
465474
}
466475

476+
@Override
477+
public void visit(UnPivotQuery unPivotQuery) {
478+
visit(unPivotQuery, null);
479+
}
480+
467481
public void visit(SelectItem<?> selectExpressionItem) {
468482
visit(selectExpressionItem, null);
469483
}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6356,6 +6356,8 @@ Select Select() #Select:
63566356
(
63576357
LOOKAHEAD(2) select = PivotQuery()
63586358
|
6359+
LOOKAHEAD(2) select = UnPivotQuery()
6360+
|
63596361
LOOKAHEAD(3) select = PlainSelect()
63606362
|
63616363
LOOKAHEAD(3) select = Values()
@@ -7728,6 +7730,28 @@ List<SelectItem<Function>> PivotFunctionItems():
77287730
{ return functionItems; }
77297731
}
77307732

7733+
/**
7734+
* DuckDB's simplified UNPIVOT statement:
7735+
* UNPIVOT tbl ON columns [INTO NAME name VALUE values].
7736+
*/
7737+
UnPivotQuery UnPivotQuery():
7738+
{
7739+
UnPivotQuery unPivotQuery = new UnPivotQuery();
7740+
ExpressionList<Expression> onExpressions;
7741+
ExpressionList<Column> valueColumns;
7742+
FromItem fromItem;
7743+
String nameColumn;
7744+
}
7745+
{
7746+
<K_UNPIVOT> fromItem=FromItem() { unPivotQuery.setFromItem(fromItem); }
7747+
<K_ON> onExpressions=ExpressionList() { unPivotQuery.setOnExpressions(onExpressions); }
7748+
[
7749+
<K_INTO> <K_NAME> nameColumn=RelObjectName() { unPivotQuery.setNameColumn(nameColumn); }
7750+
<K_VALUE> valueColumns=ColumnList() { unPivotQuery.setValueColumns(valueColumns); }
7751+
]
7752+
{ return unPivotQuery; }
7753+
}
7754+
77317755
PivotQuery PivotQuery():
77327756
{
77337757
PivotQuery pivotQuery = new PivotQuery();

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,4 +603,39 @@ void testBarePipeQueryStillParses() throws JSQLParserException {
603603
String sqlStr = "FROM t |> WHERE a > 1";
604604
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
605605
}
606+
607+
@Test
608+
void testUnPivotStatement() throws JSQLParserException {
609+
String sqlStr = "UNPIVOT monthly_sales ON jan, feb INTO NAME month VALUE sales";
610+
UnPivotQuery unPivotQuery =
611+
(UnPivotQuery) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
612+
613+
Assertions.assertEquals("month", unPivotQuery.getNameColumn());
614+
Assertions.assertEquals(2, unPivotQuery.getOnExpressions().size());
615+
Assertions.assertEquals(1, unPivotQuery.getValueColumns().size());
616+
}
617+
618+
@Test
619+
void testUnPivotStatementWithoutIntoClause() throws JSQLParserException {
620+
String sqlStr = "UNPIVOT monthly_sales ON jan, feb";
621+
UnPivotQuery unPivotQuery =
622+
(UnPivotQuery) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
623+
624+
Assertions.assertNull(unPivotQuery.getNameColumn());
625+
}
626+
627+
@Test
628+
void testUnPivotStatementTablesNamesFinder() throws JSQLParserException {
629+
String sqlStr = "UNPIVOT ds.monthly_sales ON jan, feb INTO NAME month VALUE sales";
630+
631+
Assertions.assertEquals(java.util.Collections.singletonList("ds.monthly_sales"),
632+
new net.sf.jsqlparser.util.TablesNamesFinder<Void>()
633+
.getTableList(net.sf.jsqlparser.parser.CCJSqlParserUtil.parse(sqlStr)));
634+
}
635+
636+
@Test
637+
void testUnPivotInFromClauseStillParses() throws JSQLParserException {
638+
String sqlStr = "SELECT * FROM monthly_sales UNPIVOT (sales FOR month IN (jan, feb))";
639+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
640+
}
606641
}

0 commit comments

Comments
 (0)