From cfe3f0756c061e646fbb839f889e2d924462cc92 Mon Sep 17 00:00:00 2001 From: dengliming Date: Sat, 22 Aug 2026 20:08:27 +0800 Subject: [PATCH 1/2] feat(parser): support DuckDB MAP literals and PIVOT queries --- .../expression/ExpressionVisitor.java | 8 + .../expression/ExpressionVisitorAdapter.java | 11 ++ .../jsqlparser/expression/MapExpression.java | 79 +++++++++ .../statement/select/FromItemVisitor.java | 13 +- .../select/FromItemVisitorAdapter.java | 10 +- .../statement/select/PivotQuery.java | 160 ++++++++++++++++++ .../statement/select/SelectVisitor.java | 11 +- .../select/SelectVisitorAdapter.java | 27 +++ .../sf/jsqlparser/util/TablesNamesFinder.java | 51 +++++- .../util/deparser/ExpressionDeParser.java | 21 ++- .../util/deparser/SelectDeParser.java | 81 +++++++++ .../validator/ExpressionValidator.java | 19 ++- .../validation/validator/SelectValidator.java | 39 ++++- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 88 +++++++++- .../select/DuckDBCompatibilityTest.java | 70 ++++++++ .../statement/select/PivotQueryTest.java | 94 ++++++++++ 16 files changed, 762 insertions(+), 20 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/expression/MapExpression.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/select/PivotQuery.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/select/DuckDBCompatibilityTest.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/select/PivotQueryTest.java diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java index d07d9179a..2eb909a98 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java @@ -621,6 +621,14 @@ default void visit(ArrayConstructor arrayConstructor) { this.visit(arrayConstructor, null); } + default T visit(MapExpression mapExpression, S context) { + return null; + } + + default void visit(MapExpression mapExpression) { + this.visit(mapExpression, null); + } + T visit(VariableAssignment variableAssignment, S context); default void visit(VariableAssignment variableAssignment) { diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java index 4727e93e8..274d4e00a 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java @@ -12,6 +12,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Map; import java.util.Optional; import net.sf.jsqlparser.expression.operators.arithmetic.Addition; import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseAnd; @@ -693,6 +694,16 @@ public T visit(ArrayConstructor arrayConstructor, S context) { return visitExpressions(arrayConstructor, context, arrayConstructor.getExpressions()); } + @Override + public T visit(MapExpression mapExpression, S context) { + ArrayList subExpressions = new ArrayList<>(); + for (Map.Entry entry : mapExpression.getEntries()) { + subExpressions.add(entry.getKey()); + subExpressions.add(entry.getValue()); + } + return visitExpressions(mapExpression, context, subExpressions); + } + @Override public T visit(VariableAssignment variableAssignment, S context) { return visitExpressions(variableAssignment, context, variableAssignment.getVariable(), diff --git a/src/main/java/net/sf/jsqlparser/expression/MapExpression.java b/src/main/java/net/sf/jsqlparser/expression/MapExpression.java new file mode 100644 index 000000000..1e64a7db8 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/MapExpression.java @@ -0,0 +1,79 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.expression; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +/** + * DuckDB's {@code MAP {key: value, ...}} literal expression. + * + * @see DuckDB MAP type + */ +public class MapExpression extends ASTNodeAccessImpl implements Expression { + + private List> entries = new ArrayList<>(); + + public MapExpression() {} + + public MapExpression(Collection> entries) { + this.entries.addAll(entries); + } + + public List> getEntries() { + return entries; + } + + public void setEntries(List> entries) { + this.entries = entries; + } + + public MapExpression withEntries(List> entries) { + setEntries(entries); + return this; + } + + public MapExpression addEntries( + Collection> entries) { + this.entries.addAll(entries); + return this; + } + + public MapExpression addEntry(Expression key, Expression value) { + entries.add(new AbstractMap.SimpleEntry<>(key, value)); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("MAP {"); + for (int i = 0; i < entries.size(); i++) { + if (i > 0) { + builder.append(", "); + } + Map.Entry entry = entries.get(i); + builder.append(entry.getKey()).append(": ").append(entry.getValue()); + } + return builder.append("}"); + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(ExpressionVisitor expressionVisitor, S context) { + return expressionVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitor.java b/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitor.java index 6b1048031..69af24f55 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitor.java @@ -9,13 +9,12 @@ */ package net.sf.jsqlparser.statement.select; +import java.util.Collection; +import java.util.List; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.imprt.Import; import net.sf.jsqlparser.statement.piped.FromQuery; -import java.util.Collection; -import java.util.List; - public interface FromItemVisitor { default T visitFromItem(FromItem fromItem, S context) { @@ -85,6 +84,14 @@ default void visit(PlainSelect plainSelect) { this.visit(plainSelect, null); } + default T visit(PivotQuery pivotQuery, S context) { + return null; + } + + default void visit(PivotQuery pivotQuery) { + this.visit(pivotQuery, null); + } + T visit(SetOperationList setOperationList, S context); default void visit(SetOperationList setOperationList) { diff --git a/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java index 23bd480b8..3c88d63c4 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java @@ -9,6 +9,8 @@ */ package net.sf.jsqlparser.statement.select; +import java.util.ArrayList; +import java.util.Collection; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; @@ -17,9 +19,6 @@ import net.sf.jsqlparser.statement.imprt.Import; import net.sf.jsqlparser.statement.piped.FromQuery; -import java.util.ArrayList; -import java.util.Collection; - @SuppressWarnings({"PMD.UncommentedEmptyMethodBody"}) public class FromItemVisitorAdapter implements FromItemVisitor { private SelectVisitor selectVisitor; @@ -125,6 +124,11 @@ public T visit(PlainSelect plainSelect, S context) { return plainSelect.accept(selectVisitor, context); } + @Override + public T visit(PivotQuery pivotQuery, S context) { + return pivotQuery.accept(selectVisitor, context); + } + @Override public T visit(SetOperationList setOperationList, S context) { ArrayList results = new ArrayList<>(); diff --git a/src/main/java/net/sf/jsqlparser/statement/select/PivotQuery.java b/src/main/java/net/sf/jsqlparser/statement/select/PivotQuery.java new file mode 100644 index 000000000..24f298907 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/PivotQuery.java @@ -0,0 +1,160 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.select; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; + +/** + * DuckDB's simplified {@code PIVOT} query. + * + *

+ * Unlike {@link Pivot}, which is a clause attached to a {@link FromItem}, this construct is a query + * in its own right and can therefore be used as a statement, CTE, or parenthesized subquery. + * + * @see DuckDB PIVOT + */ +public class PivotQuery extends Select { + + private FromItem fromItem; + private ExpressionList onExpressions; + private List> usingItems; + private ExpressionList groupByExpressions; + + public FromItem getFromItem() { + return fromItem; + } + + public void setFromItem(FromItem fromItem) { + this.fromItem = fromItem; + } + + public PivotQuery withFromItem(FromItem fromItem) { + setFromItem(fromItem); + return this; + } + + public ExpressionList getOnExpressions() { + return onExpressions; + } + + public void setOnExpressions(ExpressionList onExpressions) { + this.onExpressions = onExpressions; + } + + public PivotQuery withOnExpressions(ExpressionList onExpressions) { + setOnExpressions(onExpressions); + return this; + } + + public PivotQuery addOnExpressions(Expression... expressions) { + return addOnExpressions(Arrays.asList(expressions)); + } + + public PivotQuery addOnExpressions(Collection expressions) { + ExpressionList collection = + Optional.ofNullable(getOnExpressions()).orElseGet(ExpressionList::new); + collection.addAll(expressions); + return withOnExpressions(collection); + } + + public List> getUsingItems() { + return usingItems; + } + + public void setUsingItems(List> usingItems) { + this.usingItems = usingItems; + } + + public PivotQuery withUsingItems(List> usingItems) { + setUsingItems(usingItems); + return this; + } + + public PivotQuery addUsingItems(SelectItem... usingItems) { + List> collection = + Optional.ofNullable(getUsingItems()).orElseGet(ArrayList::new); + Collections.addAll(collection, usingItems); + return withUsingItems(collection); + } + + public PivotQuery addUsingItems(Collection> usingItems) { + List> collection = + Optional.ofNullable(getUsingItems()).orElseGet(ArrayList::new); + collection.addAll(usingItems); + return withUsingItems(collection); + } + + public ExpressionList getGroupByExpressions() { + return groupByExpressions; + } + + public void setGroupByExpressions(ExpressionList groupByExpressions) { + this.groupByExpressions = groupByExpressions; + } + + public PivotQuery withGroupByExpressions(ExpressionList groupByExpressions) { + setGroupByExpressions(groupByExpressions); + return this; + } + + public PivotQuery addGroupByExpressions(Expression... expressions) { + return addGroupByExpressions(Arrays.asList(expressions)); + } + + public PivotQuery addGroupByExpressions(Collection expressions) { + ExpressionList collection = + Optional.ofNullable(getGroupByExpressions()).orElseGet(ExpressionList::new); + collection.addAll(expressions); + return withGroupByExpressions(collection); + } + + @Override + public StringBuilder appendSelectBodyTo(StringBuilder builder) { + builder.append("PIVOT ").append(fromItem); + if (onExpressions != null) { + builder.append(" ON ").append(onExpressions); + } + if (usingItems != null) { + builder.append(" USING ").append(Select.getStringList(usingItems)); + } + if (groupByExpressions != null) { + builder.append(" GROUP BY ").append(groupByExpressions); + } + return builder; + } + + @Override + public T accept(SelectVisitor selectVisitor, S context) { + return selectVisitor.visit(this, context); + } + + @Override + public T accept(FromItemVisitor fromItemVisitor, S context) { + return fromItemVisitor.visit(this, context); + } + + @Override + public SampleClause getSampleClause() { + return null; + } + + @Override + public FromItem setSampleClause(SampleClause sampleClause) { + return null; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitor.java b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitor.java index db5ce3175..f5b88bd35 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitor.java @@ -9,11 +9,10 @@ */ package net.sf.jsqlparser.statement.select; +import java.util.List; import net.sf.jsqlparser.statement.OutputClause; import net.sf.jsqlparser.statement.piped.FromQuery; -import java.util.List; - public interface SelectVisitor { default T visitWithItems(List> withItemsList, S context) { if (withItemsList != null) { @@ -40,6 +39,14 @@ default void visit(PlainSelect plainSelect) { this.visit(plainSelect, null); } + default T visit(PivotQuery pivotQuery, S context) { + return null; + } + + default void visit(PivotQuery pivotQuery) { + this.visit(pivotQuery, null); + } + T visit(FromQuery fromQuery, S context); T visit(SetOperationList setOpList, S context); diff --git a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java index 962e73719..7180dfab5 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java @@ -11,6 +11,7 @@ import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.Function; import net.sf.jsqlparser.statement.OutputClause; import net.sf.jsqlparser.statement.piped.FromQuery; @@ -217,6 +218,32 @@ public T visit(PlainSelect plainSelect, S context) { return null; } + @Override + public T visit(PivotQuery pivotQuery, S context) { + visitWithItems(pivotQuery.getWithItemsList(), context); + + fromItemVisitor.visitFromItem(pivotQuery.getFromItem(), context); + expressionVisitor.visitExpressions(pivotQuery.getOnExpressions(), context); + + if (pivotQuery.getUsingItems() != null) { + for (SelectItem item : pivotQuery.getUsingItems()) { + item.accept(selectItemVisitor, context); + } + } + + expressionVisitor.visitExpressions(pivotQuery.getGroupByExpressions(), context); + expressionVisitor.visitOrderBy(pivotQuery.getOrderByElements(), context); + expressionVisitor.visitLimit(pivotQuery.getLimit(), context); + + if (pivotQuery.getOffset() != null) { + expressionVisitor.visitExpression(pivotQuery.getOffset().getOffset(), context); + } + if (pivotQuery.getFetch() != null) { + expressionVisitor.visitExpression(pivotQuery.getFetch().getExpression(), context); + } + return null; + } + @Override public T visit(FromQuery fromQuery, S context) { return null; diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 3e15be9a7..10eb8c6ba 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -73,6 +73,7 @@ import net.sf.jsqlparser.statement.DescribeStatement; import net.sf.jsqlparser.statement.ExplainStatement; import net.sf.jsqlparser.statement.IfElseStatement; +import net.sf.jsqlparser.statement.OutputClause; import net.sf.jsqlparser.statement.PurgeObjectType; import net.sf.jsqlparser.statement.PurgeStatement; import net.sf.jsqlparser.statement.ResetStatement; @@ -93,7 +94,6 @@ import net.sf.jsqlparser.statement.alter.AlterSystemStatement; import net.sf.jsqlparser.statement.alter.RenameTableStatement; import net.sf.jsqlparser.statement.alter.sequence.AlterSequence; -import net.sf.jsqlparser.statement.OutputClause; import net.sf.jsqlparser.statement.analyze.Analyze; import net.sf.jsqlparser.statement.comment.Comment; import net.sf.jsqlparser.statement.create.database.CreateDatabase; @@ -156,6 +156,7 @@ import net.sf.jsqlparser.statement.select.OrderByElement; import net.sf.jsqlparser.statement.select.ParenthesedFromItem; import net.sf.jsqlparser.statement.select.ParenthesedSelect; +import net.sf.jsqlparser.statement.select.PivotQuery; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.statement.select.SelectItem; @@ -372,11 +373,50 @@ public Void visit(PlainSelect plainSelect, S context) { return null; } + @Override + public Void visit(PivotQuery pivotQuery, S context) { + List> withItemsList = pivotQuery.getWithItemsList(); + if (withItemsList != null && !withItemsList.isEmpty()) { + for (WithItem withItem : withItemsList) { + withItem.accept((SelectVisitor) this, context); + } + } + + if (pivotQuery.getFromItem() != null) { + pivotQuery.getFromItem().accept(this, context); + } + if (pivotQuery.getOnExpressions() != null) { + pivotQuery.getOnExpressions().accept(this, context); + } + if (pivotQuery.getUsingItems() != null) { + for (SelectItem item : pivotQuery.getUsingItems()) { + item.accept(this, context); + } + } + if (pivotQuery.getGroupByExpressions() != null) { + pivotQuery.getGroupByExpressions().accept(this, context); + } + visitOrderBy(pivotQuery.getOrderByElements(), context); + visitLimit(pivotQuery.getLimit(), context); + if (pivotQuery.getOffset() != null) { + pivotQuery.getOffset().getOffset().accept(this, context); + } + if (pivotQuery.getFetch() != null && pivotQuery.getFetch().getExpression() != null) { + pivotQuery.getFetch().getExpression().accept(this, context); + } + return null; + } + @Override public void visit(PlainSelect plainSelect) { SelectVisitor.super.visit(plainSelect); } + @Override + public void visit(PivotQuery pivotQuery) { + SelectVisitor.super.visit(pivotQuery); + } + /** * Override to adapt the tableName generation (e.g. with / without schema). * @@ -1848,6 +1888,15 @@ public Void visit(ArrayConstructor array, S context) { return null; } + @Override + public Void visit(MapExpression mapExpression, S context) { + for (Map.Entry entry : mapExpression.getEntries()) { + entry.getKey().accept(this, context); + entry.getValue().accept(this, context); + } + return null; + } + @Override public Void visit(CreateSequence createSequence, S context) { throwUnsupported(createSequence); diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java index 2767f5e23..8328dc799 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java @@ -45,12 +45,12 @@ import net.sf.jsqlparser.expression.JsonExpression; import net.sf.jsqlparser.expression.JsonFunction; import net.sf.jsqlparser.expression.JsonTableFunction; -import net.sf.jsqlparser.expression.XmlTableFunction; import net.sf.jsqlparser.expression.KeepExpression; import net.sf.jsqlparser.expression.KeyExpression; import net.sf.jsqlparser.expression.LambdaExpression; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.LowExpression; +import net.sf.jsqlparser.expression.MapExpression; import net.sf.jsqlparser.expression.MySQLGroupConcat; import net.sf.jsqlparser.expression.NextValExpression; import net.sf.jsqlparser.expression.NotExpression; @@ -62,12 +62,12 @@ import net.sf.jsqlparser.expression.OverlapsCondition; import net.sf.jsqlparser.expression.PostgresNamedFunctionParameter; import net.sf.jsqlparser.expression.RangeExpression; -import net.sf.jsqlparser.expression.TernaryExpression; import net.sf.jsqlparser.expression.RowConstructor; import net.sf.jsqlparser.expression.RowGetExpression; import net.sf.jsqlparser.expression.SignedExpression; import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.expression.StructType; +import net.sf.jsqlparser.expression.TernaryExpression; import net.sf.jsqlparser.expression.TimeKeyExpression; import net.sf.jsqlparser.expression.TimeValue; import net.sf.jsqlparser.expression.TimestampValue; @@ -79,6 +79,7 @@ import net.sf.jsqlparser.expression.WhenClause; import net.sf.jsqlparser.expression.WindowElement; import net.sf.jsqlparser.expression.XMLSerializeExpr; +import net.sf.jsqlparser.expression.XmlTableFunction; import net.sf.jsqlparser.expression.operators.arithmetic.Addition; import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseAnd; import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseLeftShift; @@ -1620,6 +1621,22 @@ public StringBuilder visit(ArrayConstructor arrayConstructor, S context) { return builder; } + @Override + public StringBuilder visit(MapExpression mapExpression, S context) { + builder.append("MAP {"); + for (int i = 0; i < mapExpression.getEntries().size(); i++) { + if (i > 0) { + builder.append(", "); + } + Map.Entry entry = mapExpression.getEntries().get(i); + entry.getKey().accept(this, context); + builder.append(": "); + entry.getValue().accept(this, context); + } + builder.append("}"); + return builder; + } + @Override void deParse(Expression statement) { statement.accept(this, null); diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java index 24fea616a..b506b0b27 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java @@ -62,6 +62,7 @@ import net.sf.jsqlparser.statement.select.ParenthesedFromItem; import net.sf.jsqlparser.statement.select.ParenthesedSelect; import net.sf.jsqlparser.statement.select.Pivot; +import net.sf.jsqlparser.statement.select.PivotQuery; import net.sf.jsqlparser.statement.select.PivotVisitor; import net.sf.jsqlparser.statement.select.PivotXml; import net.sf.jsqlparser.statement.select.PlainSelect; @@ -429,6 +430,81 @@ public StringBuilder visit(PlainSelect plainSelect, S context) { return builder; } + @Override + public StringBuilder visit(PivotQuery pivotQuery, S context) { + List> withItemsList = pivotQuery.getWithItemsList(); + if (withItemsList != null && !withItemsList.isEmpty()) { + builder.append("WITH "); + for (Iterator> iter = withItemsList.iterator(); iter.hasNext();) { + iter.next().accept((SelectVisitor) this, context); + if (iter.hasNext()) { + builder.append(","); + } + builder.append(" "); + } + } + + builder.append("PIVOT "); + pivotQuery.getFromItem().accept(this, context); + + if (pivotQuery.getOnExpressions() != null) { + builder.append(" ON "); + pivotQuery.getOnExpressions().accept(expressionVisitor, context); + } + if (pivotQuery.getUsingItems() != null) { + builder.append(" USING "); + for (Iterator> iter = + pivotQuery.getUsingItems().iterator(); iter.hasNext();) { + iter.next().accept(this, context); + if (iter.hasNext()) { + builder.append(", "); + } + } + } + if (pivotQuery.getGroupByExpressions() != null) { + builder.append(" GROUP BY "); + pivotQuery.getGroupByExpressions().accept(expressionVisitor, context); + } + + Alias alias = pivotQuery.getAlias(); + if (alias != null) { + builder.append(alias); + } + Pivot pivot = pivotQuery.getPivot(); + if (pivot != null) { + pivot.accept(this, context); + } + UnPivot unpivot = pivotQuery.getUnPivot(); + if (unpivot != null) { + unpivot.accept(this, context); + } + + if (pivotQuery.getOrderByElements() != null) { + new OrderByDeParser(expressionVisitor, builder).deParse(pivotQuery.isOracleSiblings(), + pivotQuery.getOrderByElements()); + deParseInterpolate(pivotQuery.getInterpolate()); + } + if (pivotQuery.getOption() != null) { + builder.append(pivotQuery.getOption()); + } + if (pivotQuery.getLimitBy() != null) { + new LimitDeparser(expressionVisitor, builder).deParse(pivotQuery.getLimitBy()); + } + if (pivotQuery.getLimit() != null) { + new LimitDeparser(expressionVisitor, builder).deParse(pivotQuery.getLimit()); + } + if (pivotQuery.getOffset() != null) { + visit(pivotQuery.getOffset()); + } + if (pivotQuery.getFetch() != null) { + visit(pivotQuery.getFetch()); + } + if (pivotQuery.getIsolation() != null) { + builder.append(pivotQuery.getIsolation()); + } + return builder; + } + protected void deparseWhereClause(PlainSelect plainSelect) { if (plainSelect.getWhere() != null) { builder.append(" WHERE "); @@ -911,6 +987,11 @@ public void visit(PlainSelect plainSelect) { visit(plainSelect, null); } + @Override + public void visit(PivotQuery pivotQuery) { + visit(pivotQuery, null); + } + public void visit(SelectItem selectExpressionItem) { visit(selectExpressionItem, null); } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java index 128fc7d92..e44fee241 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.util.validation.validator; +import java.util.Map; import net.sf.jsqlparser.expression.AllValue; import net.sf.jsqlparser.expression.AnalyticExpression; import net.sf.jsqlparser.expression.AnyComparisonExpression; @@ -39,12 +40,12 @@ import net.sf.jsqlparser.expression.JsonExpression; import net.sf.jsqlparser.expression.JsonFunction; import net.sf.jsqlparser.expression.JsonTableFunction; -import net.sf.jsqlparser.expression.XmlTableFunction; import net.sf.jsqlparser.expression.KeepExpression; import net.sf.jsqlparser.expression.KeyExpression; import net.sf.jsqlparser.expression.LambdaExpression; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.LowExpression; +import net.sf.jsqlparser.expression.MapExpression; import net.sf.jsqlparser.expression.MySQLGroupConcat; import net.sf.jsqlparser.expression.NextValExpression; import net.sf.jsqlparser.expression.NotExpression; @@ -56,12 +57,12 @@ import net.sf.jsqlparser.expression.OverlapsCondition; import net.sf.jsqlparser.expression.PostgresNamedFunctionParameter; import net.sf.jsqlparser.expression.RangeExpression; -import net.sf.jsqlparser.expression.TernaryExpression; import net.sf.jsqlparser.expression.RowConstructor; import net.sf.jsqlparser.expression.RowGetExpression; import net.sf.jsqlparser.expression.SignedExpression; import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.expression.StructType; +import net.sf.jsqlparser.expression.TernaryExpression; import net.sf.jsqlparser.expression.TimeKeyExpression; import net.sf.jsqlparser.expression.TimeValue; import net.sf.jsqlparser.expression.TimestampValue; @@ -75,6 +76,7 @@ import net.sf.jsqlparser.expression.WindowOffset; import net.sf.jsqlparser.expression.WindowRange; import net.sf.jsqlparser.expression.XMLSerializeExpr; +import net.sf.jsqlparser.expression.XmlTableFunction; import net.sf.jsqlparser.expression.operators.arithmetic.Addition; import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseAnd; import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseLeftShift; @@ -1008,6 +1010,15 @@ public Void visit(ArrayConstructor aThis, S context) { return null; } + @Override + public Void visit(MapExpression mapExpression, S context) { + for (Map.Entry entry : mapExpression.getEntries()) { + entry.getKey().accept(this, context); + entry.getValue().accept(this, context); + } + return null; + } + @Override public void validate(Expression expression) { expression.accept(this, null); @@ -1250,6 +1261,10 @@ public void visit(ArrayConstructor aThis) { visit(aThis, null); } + public void visit(MapExpression mapExpression) { + visit(mapExpression, null); + } + public void visit(VariableAssignment a) { visit(a, null); diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java index a0511d480..b9f22aada 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java @@ -28,17 +28,18 @@ import net.sf.jsqlparser.statement.select.MinusOp; import net.sf.jsqlparser.statement.select.MySqlSelectIntoClause; import net.sf.jsqlparser.statement.select.Offset; +import net.sf.jsqlparser.statement.select.OptionClause; +import net.sf.jsqlparser.statement.select.OptionHint; import net.sf.jsqlparser.statement.select.ParenthesedFromItem; import net.sf.jsqlparser.statement.select.ParenthesedSelect; import net.sf.jsqlparser.statement.select.Pivot; +import net.sf.jsqlparser.statement.select.PivotQuery; import net.sf.jsqlparser.statement.select.PivotVisitor; import net.sf.jsqlparser.statement.select.PivotXml; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.statement.select.SelectItem; import net.sf.jsqlparser.statement.select.SelectItemVisitor; import net.sf.jsqlparser.statement.select.SelectVisitor; -import net.sf.jsqlparser.statement.select.OptionClause; -import net.sf.jsqlparser.statement.select.OptionHint; import net.sf.jsqlparser.statement.select.SetOperationList; import net.sf.jsqlparser.statement.select.TableFunction; import net.sf.jsqlparser.statement.select.TableStatement; @@ -159,6 +160,36 @@ public Void visit(PlainSelect plainSelect, S context) { return null; } + @Override + public Void visit(PivotQuery pivotQuery, S context) { + if (isNotEmpty(pivotQuery.getWithItemsList())) { + pivotQuery.getWithItemsList() + .forEach(withItem -> withItem.accept((SelectVisitor) this, context)); + } + + validateFeature(Feature.pivot); + validateOptionalFromItem(pivotQuery.getFromItem()); + validateOptionalExpressions(pivotQuery.getOnExpressions()); + if (isNotEmpty(pivotQuery.getUsingItems())) { + pivotQuery.getUsingItems().forEach(item -> item.accept(this, context)); + } + validateOptionalExpressions(pivotQuery.getGroupByExpressions()); + validateOptionalOrderByElements(pivotQuery.getOrderByElements()); + validateOptionalInterpolate(pivotQuery.getInterpolate()); + validateOptionalOption(pivotQuery.getOption()); + + if (pivotQuery.getLimit() != null) { + getValidator(LimitValidator.class).validate(pivotQuery.getLimit()); + } + if (pivotQuery.getOffset() != null) { + validateOffset(pivotQuery.getOffset()); + } + if (pivotQuery.getFetch() != null) { + validateFetch(pivotQuery.getFetch()); + } + return null; + } + private void validateOptionalOption(OptionClause option) { if (option != null) { for (OptionHint optionHint : option.getOptionHints()) { @@ -420,6 +451,10 @@ public void visit(PlainSelect plainSelect) { visit(plainSelect, null); } + public void visit(PivotQuery pivotQuery) { + visit(pivotQuery, null); + } + public void visit(SelectItem selectExpressionItem) { visit(selectExpressionItem, null); } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 1ed5192eb..f56523b4f 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -106,6 +106,10 @@ public class CCJSqlParser extends AbstractJSqlParser { // then-branch closes the ternary and must not be taken as the JSON path operator private int ternaryThenBranchDepth = 0; + // depth of DuckDB MAP keys: the following ":" separates a key from its value + // and must not be taken as the JSON path operator + private int mapKeyDepth = 0; + private void linkAST(ASTNodeAccess access, Node node) { access.setASTNode(node); node.jjtSetValue(access); @@ -536,10 +540,10 @@ public class CCJSqlParser extends AbstractJSqlParser { try { if (getToken(1).kind != OPENING_BRACKET) return false; int k2 = getToken(2).kind; - // Direct subquery: ( SELECT/WITH ) → ParenthesedSelect handles it + // Direct subquery: ( SELECT/WITH/PIVOT ) → ParenthesedSelect handles it // ( VALUES ) can be ParenthesedFromItem(Values) or ParenthesedSelect; // ParenthesedFromItem is checked first so Values-as-FROM-item works - if (k2 == K_SELECT || k2 == K_WITH) return false; + if (k2 == K_SELECT || k2 == K_WITH || k2 == K_PIVOT) return false; // ( ( SELECT ) UNION ( SELECT ) ) → isNestedSetOperationAhead handles it // Other (( patterns: ( (SELECT...) JOIN ... ) or ( (table) ) → ParenthesedFromItem if (k2 == K_LATERAL) return false; // handled by LateralSubSelect @@ -4810,6 +4814,8 @@ Select Select() #Select: | ( ( + LOOKAHEAD(2) select = PivotQuery() + | LOOKAHEAD(3) select = PlainSelect() | LOOKAHEAD(3) select = Values() @@ -6101,10 +6107,39 @@ List> PivotFunctionItems(): } { item = FunctionItem() {functionItems.add(item);} - ( "," item = FunctionItem() {functionItems.add(item);} )* + ( LOOKAHEAD(2) "," item = FunctionItem() {functionItems.add(item);} )* { return functionItems; } } +PivotQuery PivotQuery(): +{ + PivotQuery pivotQuery = new PivotQuery(); + FromItem fromItem; + ExpressionList onExpressions = null; + List> usingItems = null; + ExpressionList groupByExpressions = null; +} +{ + fromItem = FromItem() + ( + onExpressions = ExpressionList() + [ LOOKAHEAD(2) usingItems = PivotFunctionItems() ] + [ LOOKAHEAD(2) groupByExpressions = ExpressionList() ] + | + usingItems = PivotFunctionItems() + [ LOOKAHEAD(2) groupByExpressions = ExpressionList() ] + | + groupByExpressions = ExpressionList() + ) + { + pivotQuery.setFromItem(fromItem); + pivotQuery.setOnExpressions(onExpressions); + pivotQuery.setUsingItems(usingItems); + pivotQuery.setGroupByExpressions(groupByExpressions); + return pivotQuery; + } +} + SelectItem> ExpressionListItem(): { ExpressionList expressionList; @@ -8203,6 +8238,10 @@ Expression PrimaryExpression() #PrimaryExpression: | LOOKAHEAD(2, {!interrupted}) retval=CharacterPrimary() + | LOOKAHEAD({ !interrupted && getToken(1).kind == DATA_TYPE + && "MAP".equalsIgnoreCase(getToken(1).image) + && getToken(2).kind == OPENING_CURLY_BRACKET }) retval=MapExpression() + | LOOKAHEAD({!interrupted && isImplicitCastAhead()}) retval=ImplicitCast() | retval = JdbcParameter() @@ -8352,7 +8391,8 @@ Expression PrimaryExpression() #PrimaryExpression: // Check for JSON operands [ - LOOKAHEAD(2, { ternaryThenBranchDepth == 0 || !":".equals(getToken(1).image) }) ( + LOOKAHEAD(2, { (ternaryThenBranchDepth == 0 && mapKeyDepth == 0) + || !":".equals(getToken(1).image) }) ( LOOKAHEAD(2) ( token="->" | @@ -8576,6 +8616,43 @@ ArrayConstructor ArrayConstructor(boolean arrayKeyword) : { { return array; } } +MapExpression MapExpression() #MapExpression: +{ + MapExpression mapExpression = new MapExpression(); + Expression key; + Expression value; +} +{ + + [ + key = MapKeyExpression() value = Expression() + { mapExpression.addEntry(key, value); } + ( + "," key = MapKeyExpression() value = Expression() + { mapExpression.addEntry(key, value); } + )* + ] + + { + linkAST(mapExpression, jjtThis); + return mapExpression; + } +} + +Expression MapKeyExpression(): +{ + Expression key; +} +{ + try { + { mapKeyDepth++; } + key = Expression() + } finally { + mapKeyDepth--; + } + { return key; } +} + List> StructParameters(): { String parameterName = ""; @@ -8682,7 +8759,8 @@ JsonExpression JsonExpression(Expression expr, List Date: Sat, 22 Aug 2026 20:31:45 +0800 Subject: [PATCH 2/2] refactor: reduce PivotQuery visitor complexity --- .../sf/jsqlparser/util/TablesNamesFinder.java | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 10eb8c6ba..471a971a4 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -375,36 +375,33 @@ public Void visit(PlainSelect plainSelect, S context) { @Override public Void visit(PivotQuery pivotQuery, S context) { - List> withItemsList = pivotQuery.getWithItemsList(); - if (withItemsList != null && !withItemsList.isEmpty()) { - for (WithItem withItem : withItemsList) { - withItem.accept((SelectVisitor) this, context); - } - } + visitWithItems(pivotQuery.getWithItemsList(), context); + visitFromItem(pivotQuery.getFromItem(), context); + visitExpressions(pivotQuery.getOnExpressions(), context); + visitSelectItems(pivotQuery.getUsingItems(), context); + visitExpressions(pivotQuery.getGroupByExpressions(), context); + visitOrderBy(pivotQuery.getOrderByElements(), context); + visitLimit(pivotQuery.getLimit(), context); - if (pivotQuery.getFromItem() != null) { - pivotQuery.getFromItem().accept(this, context); - } - if (pivotQuery.getOnExpressions() != null) { - pivotQuery.getOnExpressions().accept(this, context); - } - if (pivotQuery.getUsingItems() != null) { - for (SelectItem item : pivotQuery.getUsingItems()) { + visitPivotPagination(pivotQuery, context); + return null; + } + + private void visitSelectItems(List> selectItems, S context) { + if (selectItems != null) { + for (SelectItem item : selectItems) { item.accept(this, context); } } - if (pivotQuery.getGroupByExpressions() != null) { - pivotQuery.getGroupByExpressions().accept(this, context); - } - visitOrderBy(pivotQuery.getOrderByElements(), context); - visitLimit(pivotQuery.getLimit(), context); + } + + private void visitPivotPagination(PivotQuery pivotQuery, S context) { if (pivotQuery.getOffset() != null) { pivotQuery.getOffset().getOffset().accept(this, context); } if (pivotQuery.getFetch() != null && pivotQuery.getFetch().getExpression() != null) { pivotQuery.getFetch().getExpression().accept(this, context); } - return null; } @Override