From 67a94cd60c670c04a8cd76083723156ba212fdb9 Mon Sep 17 00:00:00 2001 From: Andreas Reichel Date: Sat, 10 Jul 2021 14:16:50 +0700 Subject: [PATCH 1/9] Implement Json Aggregate Functions JSON_OBJECTAGG() and JSON_ARRAYAGG() --- .../expression/ExpressionVisitor.java | 2 + .../expression/ExpressionVisitorAdapter.java | 5 + .../jsqlparser/expression/FilterOverImpl.java | 144 +++++++++ .../expression/JsonAggregateFunction.java | 288 ++++++++++++++++++ .../expression/JsonAggregateOnNullType.java | 35 +++ .../JsonAggregateUniqueKeysType.java | 35 +++ .../expression/JsonFunctionType.java | 20 ++ .../sf/jsqlparser/util/TablesNamesFinder.java | 6 + .../util/deparser/ExpressionDeParser.java | 6 + .../validator/ExpressionValidator.java | 6 + .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 107 ++++++- .../expression/JsonFunctionTest.java | 50 +++ 12 files changed, 702 insertions(+), 2 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/expression/FilterOverImpl.java create mode 100644 src/main/java/net/sf/jsqlparser/expression/JsonAggregateFunction.java create mode 100644 src/main/java/net/sf/jsqlparser/expression/JsonAggregateOnNullType.java create mode 100644 src/main/java/net/sf/jsqlparser/expression/JsonAggregateUniqueKeysType.java create mode 100644 src/main/java/net/sf/jsqlparser/expression/JsonFunctionType.java create mode 100644 src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java index 711890f71..60a20526f 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java @@ -170,4 +170,6 @@ public interface ExpressionVisitor { void visit(XMLSerializeExpr aThis); void visit(TimezoneExpression aThis); + + void visit(JsonAggregateFunction aThis); } diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java index 45f7187ac..dfda4a2b9 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java @@ -590,4 +590,9 @@ public void visit(XMLSerializeExpr expr) { public void visit(TimezoneExpression expr) { expr.getLeftExpression().accept(this); } + + @Override + public void visit(JsonAggregateFunction expression) { + expression.accept(this); + } } diff --git a/src/main/java/net/sf/jsqlparser/expression/FilterOverImpl.java b/src/main/java/net/sf/jsqlparser/expression/FilterOverImpl.java new file mode 100644 index 000000000..58f5d4db6 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/FilterOverImpl.java @@ -0,0 +1,144 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2019 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.expression; + +import java.util.List; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.select.OrderByElement; + +/** + * Analytic function. The name of the function is variable but the parameters following the special + * analytic function path. e.g. row_number() over (order by test). Additional there can be an + * expression for an analytical aggregate like sum(col) or the "all collumns" wildcard like + * count(*). + * + * @author tw + */ +public class FilterOverImpl extends ASTNodeAccessImpl { + private final OrderByClause orderBy = new OrderByClause(); + private final PartitionByClause partitionBy = new PartitionByClause(); + private AnalyticType analyticType = AnalyticType.FILTER_ONLY; + private Expression filterExpression = null; + private WindowElement windowElement = null; + + public AnalyticType getAnalyticType() { + return analyticType; + } + + public void setAnalyticType(AnalyticType analyticType) { + this.analyticType = analyticType; + } + + public FilterOverImpl withAnalyticType(AnalyticType analyticType) { + this.setAnalyticType(analyticType); + return this; + } + + public List getOrderByElements() { + return orderBy.getOrderByElements(); + } + + public void setOrderByElements(List orderByElements) { + orderBy.setOrderByElements(orderByElements); + } + + public FilterOverImpl withOrderByElements(List orderByElements) { + this.setOrderByElements(orderByElements); + return this; + } + + public ExpressionList getPartitionExpressionList() { + return partitionBy.getPartitionExpressionList(); + } + + public void setPartitionExpressionList(ExpressionList partitionExpressionList) { + setPartitionExpressionList(partitionExpressionList, false); + } + + public void setPartitionExpressionList(ExpressionList partitionExpressionList, boolean brackets) { + partitionBy.setPartitionExpressionList(partitionExpressionList, brackets); + } + + public boolean isPartitionByBrackets() { + return partitionBy.isBrackets(); + } + + public Expression getFilterExpression() { + return filterExpression; + } + + public void setFilterExpression(Expression filterExpression) { + this.filterExpression = filterExpression; + } + + + public FilterOverImpl withFilterExpression(Expression filterExpression) { + this.setFilterExpression(filterExpression); + return this; + } + + public WindowElement getWindowElement() { + return windowElement; + } + + public void setWindowElement(WindowElement windowElement) { + this.windowElement = windowElement; + } + + public FilterOverImpl withWindowElement(WindowElement windowElement) { + this.setWindowElement(windowElement); + return this; + } + + @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) + public StringBuilder append(StringBuilder builder) { + if (filterExpression != null) { + builder.append("FILTER (WHERE "); + builder.append(filterExpression.toString()); + builder.append(")"); + if (analyticType != AnalyticType.FILTER_ONLY) { + builder.append(" "); + } + } + + switch (analyticType) { + case FILTER_ONLY: + return builder; + case WITHIN_GROUP: + builder.append("WITHIN GROUP"); + break; + default: + builder.append("OVER"); + } + builder.append(" ("); + + partitionBy.toStringPartitionBy(builder); + orderBy.toStringOrderByElements(builder); + + if (windowElement != null) { + if (orderBy.getOrderByElements() != null) { + builder.append(' '); + } + builder.append(windowElement); + } + + builder.append(")"); + + return builder; + } + + @Override + @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) + public String toString() { + StringBuilder builder = new StringBuilder(); + return append(builder).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonAggregateFunction.java b/src/main/java/net/sf/jsqlparser/expression/JsonAggregateFunction.java new file mode 100644 index 000000000..badf53188 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/JsonAggregateFunction.java @@ -0,0 +1,288 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2019 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.expression; + +import java.util.List; +import java.util.Objects; +import net.sf.jsqlparser.statement.select.OrderByElement; + +/** + * + * @author Andreas Reichel + */ + +public class JsonAggregateFunction extends FilterOverImpl implements Expression { + private JsonFunctionType functionType; + + private Expression expression = null; + private final OrderByClause expressionOrderBy = new OrderByClause(); + + private boolean usingKeyKeyword = false; + private String key; + private boolean usingValueKeyword = false; + private Object value; + + private boolean usingFormatJson = false; + + private JsonAggregateOnNullType onNullType; + private JsonAggregateUniqueKeysType uniqueKeysType; + + + public JsonAggregateOnNullType getOnNullType() { + return onNullType; + } + + public void setOnNullType(JsonAggregateOnNullType onNullType) { + this.onNullType = onNullType; + } + + public JsonAggregateFunction withOnNullType(JsonAggregateOnNullType onNullType) { + this.setOnNullType(onNullType); + return this; + } + + public JsonAggregateUniqueKeysType getUniqueKeysType() { + return uniqueKeysType; + } + + public void setUniqueKeysType(JsonAggregateUniqueKeysType uniqueKeysType) { + this.uniqueKeysType = uniqueKeysType; + } + + public JsonAggregateFunction withUniqueKeysType(JsonAggregateUniqueKeysType uniqueKeysType) { + this.setUniqueKeysType(uniqueKeysType); + return this; + } + + public JsonFunctionType getType() { + return functionType; + } + + public void setType(JsonFunctionType type) { + this.functionType = Objects.requireNonNull(type, "The Type of the JSON Aggregate Function must not be null"); + } + + public JsonAggregateFunction withType(JsonFunctionType type) { + this.setType(type); + return this; + } + + public void setType(String typeName) { + this.functionType = JsonFunctionType + .valueOf( Objects.requireNonNull(typeName, "The Type of the JSON Aggregate Function must not be null").toUpperCase()); + } + + public JsonAggregateFunction withType(String typeName) { + this.setType(typeName); + return this; + } + + public Expression getExpression() { + return expression; + } + + public void setExpression(Expression expression) { + this.expression = expression; + } + + public JsonAggregateFunction withExpression(Expression expression) { + this.setExpression(expression); + return this; + } + + public boolean isUsingKeyKeyword() { + return usingKeyKeyword; + } + + public void setUsingKeyKeyword(boolean usingKeyKeyword) { + this.usingKeyKeyword = usingKeyKeyword; + } + + public JsonAggregateFunction withUsingKeyKeyword(boolean usingKeyKeyword) { + this.setUsingKeyKeyword(usingKeyKeyword); + return this; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public JsonAggregateFunction withKey(String key) { + this.setKey(key); + return this; + } + + public boolean isUsingValueKeyword() { + return usingValueKeyword; + } + + public void setUsingValueKeyword(boolean usingValueKeyword) { + this.usingValueKeyword = usingValueKeyword; + } + + public JsonAggregateFunction withUsingValueKeyword(boolean usingValueKeyword) { + this.setUsingValueKeyword(usingValueKeyword); + return this; + } + + public Object getValue() { + return value; + } + + public void setValue(Object value) { + this.value = value; + } + + public JsonAggregateFunction withValue(Object value) { + this.setValue(value); + return this; + } + + public boolean isUsingFormatJson() { + return usingFormatJson; + } + + public void setUsingFormatJson(boolean usingFormatJson) { + this.usingFormatJson = usingFormatJson; + } + + public JsonAggregateFunction withUsingFormatJson(boolean usingFormatJson) { + this.setUsingFormatJson(usingFormatJson); + return this; + } + + public List getExpressionOrderByElements() { + return expressionOrderBy.getOrderByElements(); + } + + public void setExpressionOrderByElements(List orderByElements) { + expressionOrderBy.setOrderByElements(orderByElements); + } + + public FilterOverImpl withExpressionOrderByElements(List orderByElements) { + this.setExpressionOrderByElements(orderByElements); + return this; + } + + @Override + public void accept(ExpressionVisitor expressionVisitor) { + expressionVisitor.visit(this); + } + + // avoid countless Builder --> String conversion + @Override + public StringBuilder append(StringBuilder builder) { + switch (functionType) { + case OBJECT: + appendObject(builder); + break; + case ARRAY: + appendArray(builder); + break; + default: + // this should never happen really + throw new UnsupportedOperationException("JSON Aggregate Function of the type " + functionType.name() + " has not been implemented yet."); + } + return builder; + } + + @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) + public StringBuilder appendObject(StringBuilder builder) { + builder.append("JSON_OBJECTAGG( "); + if (usingValueKeyword) { + if (usingKeyKeyword) { + builder.append("KEY "); + } + builder.append(key).append(" VALUE ").append(value); + } else { + builder.append(key).append(":").append(value); + } + + if (usingFormatJson) { + builder.append(" FORMAT JSON"); + } + + if (onNullType!=null) { + switch(onNullType) { + case NULL: + builder.append(" NULL ON NULL"); + break; + case ABSENT: + builder.append(" ABSENT On NULL"); + break; + default: + // this should never happen + } + } + + if (uniqueKeysType!=null) { + switch(uniqueKeysType) { + case WITH: + builder.append(" WITH UNIQUE KEYS"); + break; + case WITHOUT: + builder.append(" WITHOUT UNIQUE KEYS"); + break; + default: + // this should never happen + } + } + + builder.append(" ) "); + + + // FILTER( WHERE expression ) OVER windowNameOrSpecification + super.append(builder); + + return builder; + } + + @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) + public StringBuilder appendArray(StringBuilder builder) { + builder.append("JSON_ARRAYAGG( "); + builder.append(expression).append(" "); + + if (usingFormatJson) { + builder.append("FORMAT JSON "); + } + + expressionOrderBy.toStringOrderByElements(builder); + + if (onNullType!=null) { + switch(onNullType) { + case NULL: + builder.append(" NULL ON NULL "); + break; + case ABSENT: + builder.append(" ABSENT On NULL "); + break; + default: + // "ON NULL" was ommitted + } + } + builder.append(") "); + + + // FILTER( WHERE expression ) OVER windowNameOrSpecification + super.append(builder); + + return builder; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return append(builder).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonAggregateOnNullType.java b/src/main/java/net/sf/jsqlparser/expression/JsonAggregateOnNullType.java new file mode 100644 index 000000000..bc3ec2a00 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/JsonAggregateOnNullType.java @@ -0,0 +1,35 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2021 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +/* + * Copyright (C) 2021 JSQLParser. + * + * This library is free software; you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation; either version + * 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this library; + * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +package net.sf.jsqlparser.expression; + +/** + * + * @author Andreas Reichel + */ +public enum JsonAggregateOnNullType { + NULL + , ABSENT +} diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonAggregateUniqueKeysType.java b/src/main/java/net/sf/jsqlparser/expression/JsonAggregateUniqueKeysType.java new file mode 100644 index 000000000..2f0b18d21 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/JsonAggregateUniqueKeysType.java @@ -0,0 +1,35 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2021 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +/* + * Copyright (C) 2021 JSQLParser. + * + * This library is free software; you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation; either version + * 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this library; + * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +package net.sf.jsqlparser.expression; + +/** + * + * @author Andreas Reichel + */ +public enum JsonAggregateUniqueKeysType { + WITH + , WITHOUT +} diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonFunctionType.java b/src/main/java/net/sf/jsqlparser/expression/JsonFunctionType.java new file mode 100644 index 000000000..750a32109 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/JsonFunctionType.java @@ -0,0 +1,20 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2021 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ + +package net.sf.jsqlparser.expression; + +/** + * + * @author Andreas Reichel + */ +public enum JsonFunctionType { + OBJECT + , ARRAY +} diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 1ee38e105..8d9b5b617 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -32,6 +32,7 @@ import net.sf.jsqlparser.expression.IntervalExpression; import net.sf.jsqlparser.expression.JdbcNamedParameter; import net.sf.jsqlparser.expression.JdbcParameter; +import net.sf.jsqlparser.expression.JsonAggregateFunction; import net.sf.jsqlparser.expression.JsonExpression; import net.sf.jsqlparser.expression.KeepExpression; import net.sf.jsqlparser.expression.LongValue; @@ -1007,4 +1008,9 @@ public void visit(RollbackStatement rollbackStatement) { public void visit(AlterSession alterSession) { } + + @Override + public void visit(JsonAggregateFunction expression) { + expression.accept(this); + } } 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 23817c4d0..6b9ed32b8 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java @@ -34,6 +34,7 @@ import net.sf.jsqlparser.expression.IntervalExpression; import net.sf.jsqlparser.expression.JdbcNamedParameter; import net.sf.jsqlparser.expression.JdbcParameter; +import net.sf.jsqlparser.expression.JsonAggregateFunction; import net.sf.jsqlparser.expression.JsonExpression; import net.sf.jsqlparser.expression.KeepExpression; import net.sf.jsqlparser.expression.LongValue; @@ -990,4 +991,9 @@ public void visit(TimezoneExpression var) { buffer.append(" AT TIME ZONE " + expr); } } + + @Override + public void visit(JsonAggregateFunction expression) { + expression.append(buffer); + } } 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 e5aa5e8b8..907991dbf 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 @@ -29,6 +29,7 @@ import net.sf.jsqlparser.expression.IntervalExpression; import net.sf.jsqlparser.expression.JdbcNamedParameter; import net.sf.jsqlparser.expression.JdbcParameter; +import net.sf.jsqlparser.expression.JsonAggregateFunction; import net.sf.jsqlparser.expression.JsonExpression; import net.sf.jsqlparser.expression.KeepExpression; import net.sf.jsqlparser.expression.LongValue; @@ -587,4 +588,9 @@ public void visit(XMLSerializeExpr xml) { // TODO this feature seams very close to a jsqlparser-user usecase } + @Override + public void visit(JsonAggregateFunction expression) { + expression.accept(this); + } + } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 68d85188a..c79f66516 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -119,7 +119,8 @@ SKIP: TOKEN: /* SQL Keywords. prefixed with K_ to avoid name clashes */ { - + +| | | | @@ -240,8 +241,13 @@ TOKEN: /* SQL Keywords. prefixed with K_ to avoid name clashes */ | | | +| +| +| +| | | +| | | | @@ -3441,11 +3447,13 @@ Expression PrimaryExpression() #PrimaryExpression: | LOOKAHEAD(JsonExpression()) retval=JsonExpression() + | LOOKAHEAD(JsonAggregateFunction()) retval = JsonAggregateFunction() + /* | LOOKAHEAD(FunctionWithCondParams()) retval = FunctionWithCondParams() */ | LOOKAHEAD(FullTextSearch()) retval = FullTextSearch() - | LOOKAHEAD(Function()) retval=Function() [ LOOKAHEAD(2) retval = AnalyticExpression((Function)retval) ] + | LOOKAHEAD(Function()) retval=Function() [ LOOKAHEAD(2) retval = AnalyticExpression( (Function) retval ) ] | LOOKAHEAD(2) retval = IntervalExpression() { dateExpressionAllowed = false; } @@ -3647,6 +3655,101 @@ JsonExpression JsonExpression() : { } } +JsonAggregateFunction JsonAggregateFunction() : { + JsonAggregateFunction result = new JsonAggregateFunction(); + Token token; + Expression expression; + List expressionOrderByList = null; + + Expression filter; + ExpressionList expressionList = null; + List olist = null; + WindowElement windowElement = null; + boolean partitionByBrackets = false; +} +{ + ( + ( + ( + "(" { result.setType( JsonFunctionType.OBJECT ); } + [ "KEY" { result.setUsingKeyKeyword( true ); } ] + token = { result.setKey( token.image ); } + ( ":" | "VALUE" {result.setUsingValueKeyword( true ); } ) + // token = | | | | { result.setValue( token.image ); } + token = { result.setValue( token.image ); } + + [ { result.setUsingFormatJson( true ); } ] + + [ + ( + { result.setOnNullType( JsonAggregateOnNullType.NULL ); } + ) + | + ( + { result.setOnNullType( JsonAggregateOnNullType.ABSENT ); } + ) + ] + + [ + ( + { result.setUniqueKeysType( JsonAggregateUniqueKeysType.WITH ); } + ) + | + ( + { result.setUniqueKeysType( JsonAggregateUniqueKeysType.WITHOUT ); } + ) + ] + ")" + ) + | + ( + + "(" { result.setType( JsonFunctionType.ARRAY ); } + expression=Expression() { result.setExpression( expression ); } + [ { result.setUsingFormatJson( true ); } ] + [ expressionOrderByList = OrderByElements() { result.setExpressionOrderByElements( expressionOrderByList ); } ] + + [ + ( + { result.setOnNullType( JsonAggregateOnNullType.NULL ); } + ) + | + ( + { result.setOnNullType( JsonAggregateOnNullType.ABSENT ); } + ) + ] + + ")" + ) + ) + + // -- Filter + [ "(" { result.setAnalyticType(AnalyticType.FILTER_ONLY); } filter = Expression() { result.setFilterExpression( filter ); } ")" ] + + // -- OVER + [ + {result.setAnalyticType(AnalyticType.OVER);} + "(" + [ + (LOOKAHEAD(ComplexExpressionList()) expressionList=ComplexExpressionList() + | "(" {partitionByBrackets = true;} expressionList=ComplexExpressionList() ")" ) + ] + [olist=OrderByElements() ] + [windowElement = WindowElement() ] + { + result.setPartitionExpressionList(expressionList, partitionByBrackets); + result.setOrderByElements(olist); + result.setWindowElement(windowElement); + } + ")" + ] + ) + + { + return result; + } +} + IntervalExpression IntervalExpression() : { IntervalExpression interval; Token token = null; diff --git a/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java new file mode 100644 index 000000000..caf5edce8 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java @@ -0,0 +1,50 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2021 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ + +package net.sf.jsqlparser.expression; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.test.TestUtils; +import org.junit.Test; + +/** + * + * @author Andreas Reichel + */ +public class JsonFunctionTest { + @Test + public void testObjectAgg() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECTAGG( KEY foo VALUE bar) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECTAGG( foo:bar) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECTAGG( foo:bar FORMAT JSON) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECTAGG( KEY foo VALUE bar NULL ON NULL) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECTAGG( KEY foo VALUE bar ABSENT ON NULL) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECTAGG( KEY foo VALUE bar WITH UNIQUE KEYS) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECTAGG( KEY foo VALUE bar WITHOUT UNIQUE KEYS) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECTAGG( KEY foo VALUE bar NULL ON NULL WITH UNIQUE KEYS ) FROM dual ", true); + + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECTAGG( KEY foo VALUE bar NULL ON NULL WITH UNIQUE KEYS ) FILTER( WHERE name = 'Raj' ) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECTAGG( KEY foo VALUE bar NULL ON NULL WITH UNIQUE KEYS ) OVER( PARTITION BY name ) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECTAGG( KEY foo VALUE bar NULL ON NULL WITH UNIQUE KEYS ) FILTER( WHERE name = 'Raj' ) OVER( PARTITION BY name ) FROM dual ", true); + } + + @Test + public void testArrayAgg() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAYAGG( a ) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAYAGG( a ORDER BY a ) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAYAGG( a NULL ON NULL ) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAYAGG( a FORMAT JSON ) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAYAGG( a FORMAT JSON NULL ON NULL ) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAYAGG( a FORMAT JSON ABSENT ON NULL ) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAYAGG( a FORMAT JSON ABSENT ON NULL ) FILTER( WHERE name = 'Raj' ) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAYAGG( a FORMAT JSON ABSENT ON NULL ) OVER( PARTITION BY name ) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAYAGG( a FORMAT JSON ABSENT ON NULL ) FILTER( WHERE name = 'Raj' ) OVER( PARTITION BY name ) FROM dual ", true); + } +} From 85efad59ba6f6332c1c2423834a957912b61fd95 Mon Sep 17 00:00:00 2001 From: Andreas Reichel Date: Sat, 10 Jul 2021 14:21:22 +0700 Subject: [PATCH 2/9] fix the returned type --- .../net/sf/jsqlparser/expression/JsonAggregateFunction.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonAggregateFunction.java b/src/main/java/net/sf/jsqlparser/expression/JsonAggregateFunction.java index badf53188..c49086106 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonAggregateFunction.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonAggregateFunction.java @@ -170,7 +170,7 @@ public void setExpressionOrderByElements(List orderByElements) { expressionOrderBy.setOrderByElements(orderByElements); } - public FilterOverImpl withExpressionOrderByElements(List orderByElements) { + public JsonAggregateFunction withExpressionOrderByElements(List orderByElements) { this.setExpressionOrderByElements(orderByElements); return this; } From e3fadf698b167f5008eccbf60a0eac0aefd0478b Mon Sep 17 00:00:00 2001 From: Andreas Reichel Date: Sun, 11 Jul 2021 10:50:46 +0700 Subject: [PATCH 3/9] Implement JSON_OBJECT and JSON_ARRAY Solves #1260 and dbeaver/dbeaver/#13141 --- .../expression/ExpressionVisitor.java | 2 + .../expression/ExpressionVisitorAdapter.java | 5 + .../jsqlparser/expression/JsonFunction.java | 230 ++++++++++++++++++ .../expression/JsonFunctionExpression.java | 54 ++++ .../expression/JsonKeyValuePair.java | 112 +++++++++ .../sf/jsqlparser/util/TablesNamesFinder.java | 6 + .../util/deparser/ExpressionDeParser.java | 6 + .../validator/ExpressionValidator.java | 6 + .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 104 +++++++- .../expression/JsonFunctionTest.java | 129 ++++++++-- .../net/sf/jsqlparser/test/TestUtils.java | 6 + 11 files changed, 630 insertions(+), 30 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/expression/JsonFunction.java create mode 100644 src/main/java/net/sf/jsqlparser/expression/JsonFunctionExpression.java create mode 100644 src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java index 60a20526f..51dbc9f32 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java @@ -172,4 +172,6 @@ public interface ExpressionVisitor { void visit(TimezoneExpression aThis); void visit(JsonAggregateFunction aThis); + + void visit(JsonFunction aThis); } diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java index dfda4a2b9..e53bbdb4c 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java @@ -595,4 +595,9 @@ public void visit(TimezoneExpression expr) { public void visit(JsonAggregateFunction expression) { expression.accept(this); } + + @Override + public void visit(JsonFunction expression) { + expression.accept(this); + } } diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java new file mode 100644 index 000000000..06253e492 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java @@ -0,0 +1,230 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2019 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.expression; + +import java.util.ArrayList; +import java.util.Objects; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +/** + * + * @author Andreas Reichel + */ + +public class JsonFunction extends ASTNodeAccessImpl implements Expression { + private JsonFunctionType functionType; + private final ArrayList keyValuePairs = new ArrayList<>(); + + private ArrayList expressions = new ArrayList<>(); + + private JsonAggregateOnNullType onNullType; + private JsonAggregateUniqueKeysType uniqueKeysType; + + public JsonKeyValuePair getKeyValuePair(int i) { + return keyValuePairs.get(i); + } + + public JsonFunctionExpression getExpression(int i) { + return expressions.get(i); + } + + public boolean add(JsonKeyValuePair keyValuePair) { + return keyValuePairs.add(keyValuePair); + } + + public void add(int i, JsonKeyValuePair keyValuePair) { + keyValuePairs.add(i, keyValuePair); + } + + public boolean add(JsonFunctionExpression expression) { + return expressions.add(expression); + } + + public void add(int i, JsonFunctionExpression expression) { + expressions.add(i, expression); + } + + public JsonAggregateOnNullType getOnNullType() { + return onNullType; + } + + public void setOnNullType(JsonAggregateOnNullType onNullType) { + this.onNullType = onNullType; + } + + public JsonFunction withOnNullType(JsonAggregateOnNullType onNullType) { + this.setOnNullType(onNullType); + return this; + } + + public JsonAggregateUniqueKeysType getUniqueKeysType() { + return uniqueKeysType; + } + + public void setUniqueKeysType(JsonAggregateUniqueKeysType uniqueKeysType) { + this.uniqueKeysType = uniqueKeysType; + } + + public JsonFunction withUniqueKeysType(JsonAggregateUniqueKeysType uniqueKeysType) { + this.setUniqueKeysType(uniqueKeysType); + return this; + } + + public JsonFunctionType getType() { + return functionType; + } + + public void setType(JsonFunctionType type) { + this.functionType = + Objects.requireNonNull(type, "The Type of the JSON Aggregate Function must not be null"); + } + + public JsonFunction withType(JsonFunctionType type) { + this.setType(type); + return this; + } + + public void setType(String typeName) { + this.functionType = JsonFunctionType.valueOf( + Objects.requireNonNull(typeName, "The Type of the JSON Aggregate Function must not be null") + .toUpperCase()); + } + + public JsonFunction withType(String typeName) { + this.setType(typeName); + return this; + } + + @Override + public void accept(ExpressionVisitor expressionVisitor) { + expressionVisitor.visit(this); + } + + // avoid countless Builder --> String conversion + public StringBuilder append(StringBuilder builder) { + switch (functionType) { + case OBJECT: + appendObject(builder); + break; + case ARRAY: + appendArray(builder); + break; + default: + // this should never happen really + throw new UnsupportedOperationException("JSON Aggregate Function of the type " + + functionType.name() + " has not been implemented yet."); + } + return builder; + } + + @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) + public StringBuilder appendObject(StringBuilder builder) { + builder.append("JSON_OBJECT( "); + int i = 0; + for (JsonKeyValuePair keyValuePair : keyValuePairs) { + if (i > 0) { + builder.append(", "); + } + if (keyValuePair.isUsingValueKeyword()) { + if (keyValuePair.isUsingKeyKeyword()) { + builder.append("KEY "); + } + builder.append(keyValuePair.getKey()).append(" VALUE ").append(keyValuePair.getValue()); + } else { + builder.append(keyValuePair.getKey()).append(":").append(keyValuePair.getValue()); + } + + if (keyValuePair.isUsingFormatJson()) { + builder.append(" FORMAT JSON"); + } + i++; + } + + if (onNullType != null) { + switch (onNullType) { + case NULL: + builder.append(" NULL ON NULL"); + break; + case ABSENT: + builder.append(" ABSENT On NULL"); + break; + default: + // this should never happen + } + } + + if (uniqueKeysType != null) { + switch (uniqueKeysType) { + case WITH: + builder.append(" WITH UNIQUE KEYS"); + break; + case WITHOUT: + builder.append(" WITHOUT UNIQUE KEYS"); + break; + default: + // this should never happen + } + } + + builder.append(" ) "); + + return builder; + } + + @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) + public StringBuilder appendArray(StringBuilder builder) { + builder.append("JSON_ARRAY( "); + int i = 0; + + // @fixme: this is a workaround for NULL ON NULL parsed as expressions + if (expressions.size() == 3 && (expressions.get(0).toString().equalsIgnoreCase("null") + || expressions.get(0).toString().equalsIgnoreCase("absent") + && expressions.get(1).toString().equalsIgnoreCase("on") + && expressions.get(2).toString().equalsIgnoreCase("null"))) { + for (JsonFunctionExpression expr : expressions) { + if (i > 0) { + builder.append(" "); + } + expr.append(builder); + i++; + } + } else { + for (JsonFunctionExpression expr : expressions) { + if (i > 0) { + builder.append(", "); + } + expr.append(builder); + i++; + } + } + + if (onNullType != null) { + switch (onNullType) { + case NULL: + builder.append(" NULL ON NULL "); + break; + case ABSENT: + builder.append(" ABSENT On NULL "); + break; + default: + // "ON NULL" was ommitted + } + } + builder.append(") "); + + return builder; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return append(builder).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonFunctionExpression.java b/src/main/java/net/sf/jsqlparser/expression/JsonFunctionExpression.java new file mode 100644 index 000000000..db6054227 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/JsonFunctionExpression.java @@ -0,0 +1,54 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2021 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ + +package net.sf.jsqlparser.expression; + +import java.util.Objects; + +/** + * + * @author Andreas Reichel + */ + +public class JsonFunctionExpression { + private final Expression expression; + + private boolean usingFormatJson = false; + + public JsonFunctionExpression(Expression expression) { + this.expression = Objects.requireNonNull(expression, "The EXPRESSION must not be null"); + } + public Expression getExpression() { + return expression; + } + + public boolean isUsingFormatJson() { + return usingFormatJson; + } + + public void setUsingFormatJson(boolean usingFormatJson) { + this.usingFormatJson = usingFormatJson; + } + + public JsonFunctionExpression withUsingFormatJson(boolean usingFormatJson) { + this.setUsingFormatJson(usingFormatJson); + return this; + } + + public StringBuilder append(StringBuilder builder) { + return builder.append(expression.toString()).append(usingFormatJson ? " FORMAT JSON" : ""); + } + + @Override + public String toString() { + return append(new StringBuilder()).toString(); + } + +} diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java b/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java new file mode 100644 index 000000000..5e2c0abc6 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java @@ -0,0 +1,112 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2021 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ + +package net.sf.jsqlparser.expression; + +import java.util.Objects; + +/** + * + * @author Andreas Reichel + */ + +public class JsonKeyValuePair { + private final String key; + private boolean usingKeyKeyword = false; + private final Object value; + private boolean usingValueKeyword = false; + private boolean usingFormatJson = false; + + public JsonKeyValuePair(String key, Object value, boolean usingKeyKeyword, boolean usingValueKeyword) { + this.key = Objects.requireNonNull(key, "The KEY of the Pair must not be null"); + this.value = Objects.requireNonNull(value, "The VALUE of the Pair must not be null"); + this.usingKeyKeyword = usingKeyKeyword; + this.usingValueKeyword = usingValueKeyword; + } + + public boolean isUsingKeyKeyword() { + return usingKeyKeyword; + } + + public void setUsingKeyKeyword(boolean usingKeyKeyword) { + this.usingKeyKeyword = usingKeyKeyword; + } + + public JsonKeyValuePair withUsingKeyKeyword(boolean usingKeyKeyword) { + this.setUsingKeyKeyword(usingKeyKeyword); + return this; + } + + public boolean isUsingValueKeyword() { + return usingValueKeyword; + } + + public void setUsingValueKeyword(boolean usingValueKeyword) { + this.usingValueKeyword = usingValueKeyword; + } + + public JsonKeyValuePair withUsingValueKeyword(boolean usingValueKeyword) { + this.setUsingValueKeyword(usingValueKeyword); + return this; + } + + public boolean isUsingFormatJson() { + return usingFormatJson; + } + + public void setUsingFormatJson(boolean usingFormatJson) { + this.usingFormatJson = usingFormatJson; + } + + public JsonKeyValuePair withUsingFormatJson(boolean usingFormatJson) { + this.setUsingFormatJson(usingFormatJson); + return this; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 83 * hash + Objects.hashCode(this.key); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final JsonKeyValuePair other = (JsonKeyValuePair) obj; + return Objects.equals(this.key, other.key); + } + + public String getKey() { + return key; + } + + public Object getValue() { + return value; + } + + public StringBuilder append(StringBuilder builder) { + return builder; + } + + @Override + public String toString() { + return append(new StringBuilder()).toString(); + } + +} diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 8d9b5b617..ad1ef1f6d 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -34,6 +34,7 @@ import net.sf.jsqlparser.expression.JdbcParameter; import net.sf.jsqlparser.expression.JsonAggregateFunction; import net.sf.jsqlparser.expression.JsonExpression; +import net.sf.jsqlparser.expression.JsonFunction; import net.sf.jsqlparser.expression.KeepExpression; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.MySQLGroupConcat; @@ -1013,4 +1014,9 @@ public void visit(AlterSession alterSession) { public void visit(JsonAggregateFunction expression) { expression.accept(this); } + + @Override + public void visit(JsonFunction expression) { + expression.accept(this); + } } 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 6b9ed32b8..7af7858ff 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java @@ -36,6 +36,7 @@ import net.sf.jsqlparser.expression.JdbcParameter; import net.sf.jsqlparser.expression.JsonAggregateFunction; import net.sf.jsqlparser.expression.JsonExpression; +import net.sf.jsqlparser.expression.JsonFunction; import net.sf.jsqlparser.expression.KeepExpression; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.MySQLGroupConcat; @@ -996,4 +997,9 @@ public void visit(TimezoneExpression var) { public void visit(JsonAggregateFunction expression) { expression.append(buffer); } + + @Override + public void visit(JsonFunction expression) { + expression.append(buffer); + } } 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 907991dbf..3f056a298 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 @@ -31,6 +31,7 @@ import net.sf.jsqlparser.expression.JdbcParameter; import net.sf.jsqlparser.expression.JsonAggregateFunction; import net.sf.jsqlparser.expression.JsonExpression; +import net.sf.jsqlparser.expression.JsonFunction; import net.sf.jsqlparser.expression.KeepExpression; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.MySQLGroupConcat; @@ -593,4 +594,9 @@ public void visit(JsonAggregateFunction expression) { expression.accept(this); } + @Override + public void visit(JsonFunction expression) { + expression.accept(this); + } + } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index c79f66516..c92ec6d2e 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -3447,6 +3447,8 @@ Expression PrimaryExpression() #PrimaryExpression: | LOOKAHEAD(JsonExpression()) retval=JsonExpression() + | LOOKAHEAD(JsonFunction()) retval = JsonFunction() + | LOOKAHEAD(JsonAggregateFunction()) retval = JsonAggregateFunction() /* | LOOKAHEAD(FunctionWithCondParams()) retval = FunctionWithCondParams() */ @@ -3655,6 +3657,102 @@ JsonExpression JsonExpression() : { } } +JsonFunction JsonFunction() : { + JsonFunction result = new JsonFunction(); + boolean usingKeyKeyword = false; + boolean usingValueKeyword = false; + Token keyToken; + Token valueToken; + JsonKeyValuePair keyValuePair; + + Expression expression; + JsonFunctionExpression functionExpression; + +} +{ + ( + ( + ( + "(" { result.setType( JsonFunctionType.OBJECT ); } + + ( + // --- First Element + [ "KEY" { usingKeyKeyword = true; } ] + keyToken = + ( ":" | "VALUE" { usingValueKeyword = true; } ) + // token = | | | | { result.setValue( token.image ); } + valueToken = { keyValuePair = new JsonKeyValuePair( keyToken.image, valueToken.image, usingKeyKeyword, usingValueKeyword ); result.add(keyValuePair); } + + [ { keyValuePair.setUsingFormatJson( true ); } ] + + // --- Next Elements + ( "," { usingKeyKeyword = false; usingValueKeyword = false; } + [ "KEY" { usingKeyKeyword = true; } ] + keyToken = + ( ":" | "VALUE" { usingValueKeyword = true; } ) + // token = | | | | { result.setValue( token.image ); } + valueToken = { keyValuePair = new JsonKeyValuePair( keyToken.image, valueToken.image, usingKeyKeyword, usingValueKeyword ); result.add(keyValuePair); } + + [ { keyValuePair.setUsingFormatJson( true ); } ] + )* + )* + + [ + ( + { result.setOnNullType( JsonAggregateOnNullType.NULL ); } + ) + | + ( + { result.setOnNullType( JsonAggregateOnNullType.ABSENT ); } + ) + ] + + [ + ( + { result.setUniqueKeysType( JsonAggregateUniqueKeysType.WITH ); } + ) + | + ( + { result.setUniqueKeysType( JsonAggregateUniqueKeysType.WITHOUT ); } + ) + ] + + ")" + ) + | + ( + { result.setType( JsonFunctionType.ARRAY ); } + "(" + ( LOOKAHEAD(2) + expression=Expression() { functionExpression = new JsonFunctionExpression( expression ); result.add( functionExpression ); } + [ LOOKAHEAD(2) { functionExpression.setUsingFormatJson( true ); } ] + ( + "," + expression=Expression() { functionExpression = new JsonFunctionExpression( expression ); result.add( functionExpression ); } + [ LOOKAHEAD(2) { functionExpression.setUsingFormatJson( true ); } ] + )* + )* + + [ + ( + { result.setOnNullType( JsonAggregateOnNullType.NULL ); } + ) + | + ( + { result.setOnNullType( JsonAggregateOnNullType.ABSENT ); } + ) + ] + + ")" + ) + ) + ) + + { + return result; + } +} + JsonAggregateFunction JsonAggregateFunction() : { JsonAggregateFunction result = new JsonAggregateFunction(); Token token; @@ -3681,7 +3779,7 @@ JsonAggregateFunction JsonAggregateFunction() : { [ { result.setUsingFormatJson( true ); } ] [ - ( + LOOKAHEAD(2) ( { result.setOnNullType( JsonAggregateOnNullType.NULL ); } ) | @@ -3710,7 +3808,7 @@ JsonAggregateFunction JsonAggregateFunction() : { [ expressionOrderByList = OrderByElements() { result.setExpressionOrderByElements( expressionOrderByList ); } ] [ - ( + LOOKAHEAD(2) ( { result.setOnNullType( JsonAggregateOnNullType.NULL ); } ) | @@ -3727,7 +3825,7 @@ JsonAggregateFunction JsonAggregateFunction() : { [ "(" { result.setAnalyticType(AnalyticType.FILTER_ONLY); } filter = Expression() { result.setFilterExpression( filter ); } ")" ] // -- OVER - [ + [ LOOKAHEAD(2) {result.setAnalyticType(AnalyticType.OVER);} "(" [ diff --git a/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java index caf5edce8..8251eb04d 100644 --- a/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java @@ -19,32 +19,107 @@ * @author Andreas Reichel */ public class JsonFunctionTest { - @Test - public void testObjectAgg() throws JSQLParserException { - TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECTAGG( KEY foo VALUE bar) FROM dual ", true); - TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECTAGG( foo:bar) FROM dual ", true); - TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECTAGG( foo:bar FORMAT JSON) FROM dual ", true); - TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECTAGG( KEY foo VALUE bar NULL ON NULL) FROM dual ", true); - TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECTAGG( KEY foo VALUE bar ABSENT ON NULL) FROM dual ", true); - TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECTAGG( KEY foo VALUE bar WITH UNIQUE KEYS) FROM dual ", true); - TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECTAGG( KEY foo VALUE bar WITHOUT UNIQUE KEYS) FROM dual ", true); - TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECTAGG( KEY foo VALUE bar NULL ON NULL WITH UNIQUE KEYS ) FROM dual ", true); - - TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECTAGG( KEY foo VALUE bar NULL ON NULL WITH UNIQUE KEYS ) FILTER( WHERE name = 'Raj' ) FROM dual ", true); - TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECTAGG( KEY foo VALUE bar NULL ON NULL WITH UNIQUE KEYS ) OVER( PARTITION BY name ) FROM dual ", true); - TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECTAGG( KEY foo VALUE bar NULL ON NULL WITH UNIQUE KEYS ) FILTER( WHERE name = 'Raj' ) OVER( PARTITION BY name ) FROM dual ", true); - } + @Test + public void testObjectAgg() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT JSON_OBJECTAGG( KEY foo VALUE bar) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECTAGG( foo:bar) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT JSON_OBJECTAGG( foo:bar FORMAT JSON) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT JSON_OBJECTAGG( KEY foo VALUE bar NULL ON NULL) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT JSON_OBJECTAGG( KEY foo VALUE bar ABSENT ON NULL) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT JSON_OBJECTAGG( KEY foo VALUE bar WITH UNIQUE KEYS) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT JSON_OBJECTAGG( KEY foo VALUE bar WITHOUT UNIQUE KEYS) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT JSON_OBJECTAGG( KEY foo VALUE bar NULL ON NULL WITH UNIQUE KEYS ) FROM dual ", + true); + + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT JSON_OBJECTAGG( KEY foo VALUE bar NULL ON NULL WITH UNIQUE KEYS ) FILTER( WHERE name = 'Raj' ) FROM dual ", + true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT JSON_OBJECTAGG( KEY foo VALUE bar NULL ON NULL WITH UNIQUE KEYS ) OVER( PARTITION BY name ) FROM dual ", + true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT JSON_OBJECTAGG( KEY foo VALUE bar NULL ON NULL WITH UNIQUE KEYS ) FILTER( WHERE name = 'Raj' ) OVER( PARTITION BY name ) FROM dual ", + true); + } + + @Test + public void testArrayAgg() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAYAGG( a ) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAYAGG( a ORDER BY a ) FROM dual ", + true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAYAGG( a NULL ON NULL ) FROM dual ", + true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAYAGG( a FORMAT JSON ) FROM dual ", + true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT JSON_ARRAYAGG( a FORMAT JSON NULL ON NULL ) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT JSON_ARRAYAGG( a FORMAT JSON ABSENT ON NULL ) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT JSON_ARRAYAGG( a FORMAT JSON ABSENT ON NULL ) FILTER( WHERE name = 'Raj' ) FROM dual ", + true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT JSON_ARRAYAGG( a FORMAT JSON ABSENT ON NULL ) OVER( PARTITION BY name ) FROM dual ", + true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT JSON_ARRAYAGG( a FORMAT JSON ABSENT ON NULL ) FILTER( WHERE name = 'Raj' ) OVER( PARTITION BY name ) FROM dual ", + true); + + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT json_arrayagg(json_array(\"v0\") order by \"t\".\"v0\") FROM dual ", true); + } + + @Test + public void testObject() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT JSON_OBJECT( KEY foo VALUE bar, KEY foo VALUE bar) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECT( foo:bar, foo:bar) FROM dual ", + true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT JSON_OBJECT( foo:bar, foo:bar FORMAT JSON) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT JSON_OBJECT( KEY foo VALUE bar, foo:bar FORMAT JSON, foo:bar NULL ON NULL) FROM dual ", + true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT JSON_OBJECT( KEY foo VALUE bar FORMAT JSON, foo:bar, foo:bar ABSENT ON NULL) FROM dual ", + true); + + TestUtils.assertExpressionCanBeParsedAndDeparsed("json_object(null on null)", true); + + TestUtils.assertExpressionCanBeParsedAndDeparsed("json_object(absent on null)", true); - @Test - public void testArrayAgg() throws JSQLParserException { - TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAYAGG( a ) FROM dual ", true); - TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAYAGG( a ORDER BY a ) FROM dual ", true); - TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAYAGG( a NULL ON NULL ) FROM dual ", true); - TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAYAGG( a FORMAT JSON ) FROM dual ", true); - TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAYAGG( a FORMAT JSON NULL ON NULL ) FROM dual ", true); - TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAYAGG( a FORMAT JSON ABSENT ON NULL ) FROM dual ", true); - TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAYAGG( a FORMAT JSON ABSENT ON NULL ) FILTER( WHERE name = 'Raj' ) FROM dual ", true); - TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAYAGG( a FORMAT JSON ABSENT ON NULL ) OVER( PARTITION BY name ) FROM dual ", true); - TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAYAGG( a FORMAT JSON ABSENT ON NULL ) FILTER( WHERE name = 'Raj' ) OVER( PARTITION BY name ) FROM dual ", true); - } + TestUtils.assertExpressionCanBeParsedAndDeparsed("json_object()", true); + } + + @Test + public void testArray() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT JSON_ARRAY( (SELECT * from dual) ) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAY( 1, 2, 3 ) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAY( \"v0\" ) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT json_array(null on null) FROM dual ", true); + TestUtils.assertExpressionCanBeParsedAndDeparsed("JSON_ARRAY( 1, 2, 3 )", true); + + TestUtils.assertExpressionCanBeParsedAndDeparsed("json_array(null on null)", true); + + TestUtils.assertExpressionCanBeParsedAndDeparsed("json_array(absent on null)", true); + + TestUtils.assertExpressionCanBeParsedAndDeparsed("json_array()", true); + } + + @Test + public void testIssue1260() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed("select \n" + " cast((\n" + " select coalesce(\n" + + " json_arrayagg(json_array(\"v0\") order by \"t\".\"v0\"),\n" + + " json_array(null on null)\n" + " )\n" + " from (\n" + + " select 2 \"v0\"\n" + " union\n" + " select 4 \"ID\"\n" + " ) \"t\"\n" + + " ) as text)", true); + } } diff --git a/src/test/java/net/sf/jsqlparser/test/TestUtils.java b/src/test/java/net/sf/jsqlparser/test/TestUtils.java index a3822d1f1..e3617f357 100644 --- a/src/test/java/net/sf/jsqlparser/test/TestUtils.java +++ b/src/test/java/net/sf/jsqlparser/test/TestUtils.java @@ -283,6 +283,12 @@ public static void assertExpressionCanBeDeparsedAs(final Expression parsed, Stri assertEquals(expression, stringBuilder.toString()); } + + public static void assertExpressionCanBeParsedAndDeparsed(String expressionStr, boolean laxDeparsingCheck) throws JSQLParserException { + Expression expression = CCJSqlParserUtil.parseExpression(expressionStr); + assertEquals(buildSqlString(expressionStr, laxDeparsingCheck), + buildSqlString(expression.toString(), laxDeparsingCheck)); + } public static void assertOracleHintExists(String sql, boolean assertDeparser, String... hints) throws JSQLParserException { From f36eb48dc93695e1469ac0e367f3426f9312f43d Mon Sep 17 00:00:00 2001 From: Andreas Reichel Date: Sun, 11 Jul 2021 11:45:49 +0700 Subject: [PATCH 4/9] Better workaround for NULL, NULL NULL ON NULL --- .../jsqlparser/expression/JsonFunction.java | 34 ++++++++----------- .../expression/JsonFunctionTest.java | 11 +++--- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java index 06253e492..823c9a0cb 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java @@ -182,27 +182,23 @@ public StringBuilder appendObject(StringBuilder builder) { public StringBuilder appendArray(StringBuilder builder) { builder.append("JSON_ARRAY( "); int i = 0; - + int n = expressions.size(); + // @fixme: this is a workaround for NULL ON NULL parsed as expressions - if (expressions.size() == 3 && (expressions.get(0).toString().equalsIgnoreCase("null") - || expressions.get(0).toString().equalsIgnoreCase("absent") - && expressions.get(1).toString().equalsIgnoreCase("on") - && expressions.get(2).toString().equalsIgnoreCase("null"))) { - for (JsonFunctionExpression expr : expressions) { - if (i > 0) { - builder.append(" "); - } - expr.append(builder); - i++; - } - } else { - for (JsonFunctionExpression expr : expressions) { - if (i > 0) { - builder.append(", "); - } - expr.append(builder); - i++; + // json_array(null on null) + // json_array(null null on null) + // json_array(null, null null on null) + boolean noSeparatorsForNullOnNull = + n >= 3 && expressions.get(n - 3).toString().equalsIgnoreCase("null") + && expressions.get(n - 2).toString().equalsIgnoreCase("on") + && expressions.get(n - 1).toString().equalsIgnoreCase("null"); + + for (JsonFunctionExpression expr : expressions) { + if (i > 0) { + builder.append(noSeparatorsForNullOnNull && i >= n - 3 ? " " : ", "); } + expr.append(builder); + i++; } if (onNullType != null) { diff --git a/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java index 8251eb04d..8eee6a416 100644 --- a/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java @@ -104,13 +104,14 @@ public void testArray() throws JSQLParserException { "SELECT JSON_ARRAY( (SELECT * from dual) ) FROM dual ", true); TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAY( 1, 2, 3 ) FROM dual ", true); TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAY( \"v0\" ) FROM dual ", true); - TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT json_array(null on null) FROM dual ", true); + } + + @Test + public void testArrayWithNullExpressions() throws JSQLParserException { TestUtils.assertExpressionCanBeParsedAndDeparsed("JSON_ARRAY( 1, 2, 3 )", true); - TestUtils.assertExpressionCanBeParsedAndDeparsed("json_array(null on null)", true); - - TestUtils.assertExpressionCanBeParsedAndDeparsed("json_array(absent on null)", true); - + TestUtils.assertExpressionCanBeParsedAndDeparsed("json_array(null null on null)", true); + TestUtils.assertExpressionCanBeParsedAndDeparsed("json_array(null, null null on null)", true); TestUtils.assertExpressionCanBeParsedAndDeparsed("json_array()", true); } From 5dd8b47b9ed6e5fb1925a98d830699edc41b91f0 Mon Sep 17 00:00:00 2001 From: Andreas Reichel Date: Sun, 11 Jul 2021 12:11:44 +0700 Subject: [PATCH 5/9] Remove the workaround for NULL ON NULL (without expression) --- .../sf/jsqlparser/expression/JsonFunction.java | 12 +----------- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 15 +++++++-------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java index 823c9a0cb..63f3f62bf 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java @@ -182,20 +182,10 @@ public StringBuilder appendObject(StringBuilder builder) { public StringBuilder appendArray(StringBuilder builder) { builder.append("JSON_ARRAY( "); int i = 0; - int n = expressions.size(); - - // @fixme: this is a workaround for NULL ON NULL parsed as expressions - // json_array(null on null) - // json_array(null null on null) - // json_array(null, null null on null) - boolean noSeparatorsForNullOnNull = - n >= 3 && expressions.get(n - 3).toString().equalsIgnoreCase("null") - && expressions.get(n - 2).toString().equalsIgnoreCase("on") - && expressions.get(n - 1).toString().equalsIgnoreCase("null"); for (JsonFunctionExpression expr : expressions) { if (i > 0) { - builder.append(noSeparatorsForNullOnNull && i >= n - 3 ? " " : ", "); + builder.append(", "); } expr.append(builder); i++; diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index c92ec6d2e..232b25ebe 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -3723,8 +3723,13 @@ JsonFunction JsonFunction() : { ( { result.setType( JsonFunctionType.ARRAY ); } "(" - ( LOOKAHEAD(2) + ( + LOOKAHEAD(2) ( + { result.setOnNullType( JsonAggregateOnNullType.NULL ); } + ) + | expression=Expression() { functionExpression = new JsonFunctionExpression( expression ); result.add( functionExpression ); } + [ LOOKAHEAD(2) { functionExpression.setUsingFormatJson( true ); } ] ( "," @@ -3734,13 +3739,7 @@ JsonFunction JsonFunction() : { )* [ - ( - { result.setOnNullType( JsonAggregateOnNullType.NULL ); } - ) - | - ( - { result.setOnNullType( JsonAggregateOnNullType.ABSENT ); } - ) + { result.setOnNullType( JsonAggregateOnNullType.ABSENT ); } ] ")" From a7ad67169541cb7ff7d662e6e5277a2fa2b142e4 Mon Sep 17 00:00:00 2001 From: Andreas Reichel Date: Sun, 11 Jul 2021 15:14:08 +0700 Subject: [PATCH 6/9] Implement "PMD.MissingBreakInSwitch" in order to appease Codacy --- pmd-rules.xml | 4 ++++ .../net/sf/jsqlparser/expression/AnalyticExpression.java | 2 +- .../java/net/sf/jsqlparser/expression/FilterOverImpl.java | 6 +----- .../net/sf/jsqlparser/util/deparser/ExpressionDeParser.java | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pmd-rules.xml b/pmd-rules.xml index 912f4f0b2..500f050b7 100644 --- a/pmd-rules.xml +++ b/pmd-rules.xml @@ -104,6 +104,10 @@ under the License. + + + + diff --git a/src/main/java/net/sf/jsqlparser/expression/AnalyticExpression.java b/src/main/java/net/sf/jsqlparser/expression/AnalyticExpression.java index 2b9e7e472..7edd1e717 100644 --- a/src/main/java/net/sf/jsqlparser/expression/AnalyticExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/AnalyticExpression.java @@ -179,7 +179,7 @@ public void setIgnoreNulls(boolean ignoreNulls) { } @Override - @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) + @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity", "PMD.MissingBreakInSwitch"}) public String toString() { StringBuilder b = new StringBuilder(); diff --git a/src/main/java/net/sf/jsqlparser/expression/FilterOverImpl.java b/src/main/java/net/sf/jsqlparser/expression/FilterOverImpl.java index 58f5d4db6..f3a28349d 100644 --- a/src/main/java/net/sf/jsqlparser/expression/FilterOverImpl.java +++ b/src/main/java/net/sf/jsqlparser/expression/FilterOverImpl.java @@ -15,10 +15,6 @@ import net.sf.jsqlparser.statement.select.OrderByElement; /** - * Analytic function. The name of the function is variable but the parameters following the special - * analytic function path. e.g. row_number() over (order by test). Additional there can be an - * expression for an analytical aggregate like sum(col) or the "all collumns" wildcard like - * count(*). * * @author tw */ @@ -98,7 +94,7 @@ public FilterOverImpl withWindowElement(WindowElement windowElement) { return this; } - @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) + @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity", "PMD.MissingBreakInSwitch"}) public StringBuilder append(StringBuilder builder) { if (filterExpression != null) { builder.append("FILTER (WHERE "); 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 7af7858ff..cc75c42ba 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java @@ -680,7 +680,7 @@ public void visit(Modulo modulo) { } @Override - @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.ExcessiveMethodLength"}) + @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.ExcessiveMethodLength", "PMD.MissingBreakInSwitch"}) public void visit(AnalyticExpression aexpr) { String name = aexpr.getName(); Expression expression = aexpr.getExpression(); From 57ed8f9c06f4605f39af01db4d5039f4beac69e5 Mon Sep 17 00:00:00 2001 From: Andreas Reichel Date: Sun, 11 Jul 2021 16:29:35 +0700 Subject: [PATCH 7/9] Improve Test Coverage --- .../jsqlparser/expression/JsonFunction.java | 12 +- .../expression/JsonFunctionExpression.java | 3 +- .../expression/JsonKeyValuePair.java | 194 ++++++++++-------- .../sf/jsqlparser/util/TablesNamesFinder.java | 15 +- .../validator/ExpressionValidator.java | 6 +- .../expression/JsonFunctionTest.java | 40 ++++ .../util/TablesNamesFinderTest.java | 21 ++ .../validator/ExpressionValidatorTest.java | 24 +++ 8 files changed, 214 insertions(+), 101 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java index 63f3f62bf..41c480d93 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java @@ -21,12 +21,18 @@ public class JsonFunction extends ASTNodeAccessImpl implements Expression { private JsonFunctionType functionType; private final ArrayList keyValuePairs = new ArrayList<>(); - - private ArrayList expressions = new ArrayList<>(); - + private final ArrayList expressions = new ArrayList<>(); private JsonAggregateOnNullType onNullType; private JsonAggregateUniqueKeysType uniqueKeysType; + public ArrayList getKeyValuePairs() { + return keyValuePairs; + } + + public ArrayList getExpressions() { + return expressions; + } + public JsonKeyValuePair getKeyValuePair(int i) { return keyValuePairs.get(i); } diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonFunctionExpression.java b/src/main/java/net/sf/jsqlparser/expression/JsonFunctionExpression.java index db6054227..c0558bb9e 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonFunctionExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonFunctionExpression.java @@ -43,12 +43,11 @@ public JsonFunctionExpression withUsingFormatJson(boolean usingFormatJson) { } public StringBuilder append(StringBuilder builder) { - return builder.append(expression.toString()).append(usingFormatJson ? " FORMAT JSON" : ""); + return builder.append(getExpression()).append(isUsingFormatJson() ? " FORMAT JSON" : ""); } @Override public String toString() { return append(new StringBuilder()).toString(); } - } diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java b/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java index 5e2c0abc6..7e52ffeb9 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java @@ -18,95 +18,109 @@ */ public class JsonKeyValuePair { - private final String key; - private boolean usingKeyKeyword = false; - private final Object value; - private boolean usingValueKeyword = false; - private boolean usingFormatJson = false; - - public JsonKeyValuePair(String key, Object value, boolean usingKeyKeyword, boolean usingValueKeyword) { - this.key = Objects.requireNonNull(key, "The KEY of the Pair must not be null"); - this.value = Objects.requireNonNull(value, "The VALUE of the Pair must not be null"); - this.usingKeyKeyword = usingKeyKeyword; - this.usingValueKeyword = usingValueKeyword; - } - - public boolean isUsingKeyKeyword() { - return usingKeyKeyword; - } - - public void setUsingKeyKeyword(boolean usingKeyKeyword) { - this.usingKeyKeyword = usingKeyKeyword; - } - - public JsonKeyValuePair withUsingKeyKeyword(boolean usingKeyKeyword) { - this.setUsingKeyKeyword(usingKeyKeyword); - return this; - } - - public boolean isUsingValueKeyword() { - return usingValueKeyword; - } - - public void setUsingValueKeyword(boolean usingValueKeyword) { - this.usingValueKeyword = usingValueKeyword; - } - - public JsonKeyValuePair withUsingValueKeyword(boolean usingValueKeyword) { - this.setUsingValueKeyword(usingValueKeyword); - return this; - } - - public boolean isUsingFormatJson() { - return usingFormatJson; - } + private final String key; + private boolean usingKeyKeyword = false; + private final Object value; + private boolean usingValueKeyword = false; + private boolean usingFormatJson = false; + + public JsonKeyValuePair(String key, Object value, boolean usingKeyKeyword, + boolean usingValueKeyword) { + this.key = Objects.requireNonNull(key, "The KEY of the Pair must not be null"); + this.value = Objects.requireNonNull(value, "The VALUE of the Pair must not be null"); + this.usingKeyKeyword = usingKeyKeyword; + this.usingValueKeyword = usingValueKeyword; + } + + public boolean isUsingKeyKeyword() { + return usingKeyKeyword; + } + + public void setUsingKeyKeyword(boolean usingKeyKeyword) { + this.usingKeyKeyword = usingKeyKeyword; + } + + public JsonKeyValuePair withUsingKeyKeyword(boolean usingKeyKeyword) { + this.setUsingKeyKeyword(usingKeyKeyword); + return this; + } + + public boolean isUsingValueKeyword() { + return usingValueKeyword; + } + + public void setUsingValueKeyword(boolean usingValueKeyword) { + this.usingValueKeyword = usingValueKeyword; + } + + public JsonKeyValuePair withUsingValueKeyword(boolean usingValueKeyword) { + this.setUsingValueKeyword(usingValueKeyword); + return this; + } + + public boolean isUsingFormatJson() { + return usingFormatJson; + } + + public void setUsingFormatJson(boolean usingFormatJson) { + this.usingFormatJson = usingFormatJson; + } + + public JsonKeyValuePair withUsingFormatJson(boolean usingFormatJson) { + this.setUsingFormatJson(usingFormatJson); + return this; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 83 * hash + Objects.hashCode(this.key); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final JsonKeyValuePair other = (JsonKeyValuePair) obj; + return Objects.equals(this.key, other.key); + } + + public String getKey() { + return key; + } + + public Object getValue() { + return value; + } + + public StringBuilder append(StringBuilder builder) { + if (isUsingValueKeyword()) { + if (isUsingKeyKeyword()) { + builder.append("KEY "); + } + builder.append(getKey()).append(" VALUE ").append(getValue()); + } else { + builder.append(getKey()).append(":").append(getValue()); + } + + if (isUsingFormatJson()) { + builder.append(" FORMAT JSON"); + } + + return builder; + } + + @Override + public String toString() { + return append(new StringBuilder()).toString(); + } - public void setUsingFormatJson(boolean usingFormatJson) { - this.usingFormatJson = usingFormatJson; - } - - public JsonKeyValuePair withUsingFormatJson(boolean usingFormatJson) { - this.setUsingFormatJson(usingFormatJson); - return this; - } - - @Override - public int hashCode() { - int hash = 7; - hash = 83 * hash + Objects.hashCode(this.key); - return hash; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - final JsonKeyValuePair other = (JsonKeyValuePair) obj; - return Objects.equals(this.key, other.key); - } - - public String getKey() { - return key; - } - - public Object getValue() { - return value; - } - - public StringBuilder append(StringBuilder builder) { - return builder; - } - - @Override - public String toString() { - return append(new StringBuilder()).toString(); - } - } diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index ad1ef1f6d..3712ad876 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -35,6 +35,7 @@ import net.sf.jsqlparser.expression.JsonAggregateFunction; import net.sf.jsqlparser.expression.JsonExpression; import net.sf.jsqlparser.expression.JsonFunction; +import net.sf.jsqlparser.expression.JsonFunctionExpression; import net.sf.jsqlparser.expression.KeepExpression; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.MySQLGroupConcat; @@ -1012,11 +1013,21 @@ public void visit(AlterSession alterSession) { @Override public void visit(JsonAggregateFunction expression) { - expression.accept(this); + Expression expr = expression.getExpression(); + if (expr!=null) { + expr.accept(this); + } + + expr = expression.getFilterExpression(); + if (expr!=null) { + expr.accept(this); + } } @Override public void visit(JsonFunction expression) { - expression.accept(this); + for (JsonFunctionExpression expr: expression.getExpressions()) { + expr.getExpression().accept(this); + } } } 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 3f056a298..b5af9dc61 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 @@ -105,8 +105,6 @@ */ @SuppressWarnings({"PMD.CyclomaticComplexity"}) public class ExpressionValidator extends AbstractValidator implements ExpressionVisitor { - - @Override public void visit(Addition addition) { visitBinaryExpression(addition, " + "); @@ -591,12 +589,12 @@ public void visit(XMLSerializeExpr xml) { @Override public void visit(JsonAggregateFunction expression) { - expression.accept(this); + // no idea what this is good for } @Override public void visit(JsonFunction expression) { - expression.accept(this); + // no idea what this is good for } } diff --git a/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java index 8eee6a416..878f5585d 100644 --- a/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java @@ -10,6 +10,8 @@ package net.sf.jsqlparser.expression; +import java.util.Objects; +import junit.framework.Assert; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.test.TestUtils; import org.junit.Test; @@ -48,6 +50,44 @@ public void testObjectAgg() throws JSQLParserException { "SELECT JSON_OBJECTAGG( KEY foo VALUE bar NULL ON NULL WITH UNIQUE KEYS ) FILTER( WHERE name = 'Raj' ) OVER( PARTITION BY name ) FROM dual ", true); } + + @Test + public void testObjectBuilder() throws JSQLParserException { + JsonFunction f = new JsonFunction(); + f.setType(JsonFunctionType.OBJECT); + + JsonKeyValuePair keyValuePair1 = new JsonKeyValuePair("foo", "bar", false, false); + keyValuePair1.setUsingKeyKeyword(true); + keyValuePair1.setUsingValueKeyword(true); + f.add(keyValuePair1.withUsingFormatJson(true)); + + JsonKeyValuePair keyValuePair2 = new JsonKeyValuePair("foo", "bar", false, false).withUsingKeyKeyword(true).withUsingValueKeyword(true).withUsingFormatJson(false); + + // this should work because we compare based on KEY only + Assert.assertEquals(keyValuePair1, keyValuePair2); + + // this must fail because all the properties are considered + Assert.assertFalse(Objects.equals(keyValuePair1.toString(), keyValuePair2.toString())); + + f.add(keyValuePair2); + } + + @Test + public void testArrayBuilder() throws JSQLParserException { + JsonFunction f = new JsonFunction(); + f.setType(JsonFunctionType.ARRAY); + + JsonFunctionExpression expression1 = new JsonFunctionExpression(new NullValue()); + expression1.setUsingFormatJson(true); + + JsonFunctionExpression expression2 = new JsonFunctionExpression(new NullValue()).withUsingFormatJson( + true); + + Assert.assertTrue(Objects.equals(expression1.toString(), expression2.toString())); + + f.add(expression1); + f.add(expression2); + } @Test public void testArrayAgg() throws JSQLParserException { diff --git a/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java index 12d694cb2..e17fd1c33 100644 --- a/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java +++ b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java @@ -677,5 +677,26 @@ public void testUsing() throws JSQLParserException { assertTrue(tableList.contains("A")); assertTrue(tableList.contains("B.C")); } + + @Test + public void testJsonFunction() throws JSQLParserException { + String sql = "SELECT JSON_ARRAY( 1, 2, 3 ) FROM mytbl"; + Statement stmt = CCJSqlParserUtil.parse(sql); + TablesNamesFinder tablesNamesFinder = new TablesNamesFinder(); + List tableList = tablesNamesFinder.getTableList(stmt); + assertEquals(1, tableList.size()); + assertTrue(tableList.contains("mytbl")); + } + + @Test + public void testJsonAggregateFunction() throws JSQLParserException { + String sql = "SELECT JSON_ARRAYAGG( (SELECT * from dual) FORMAT JSON) FROM mytbl"; + Statement stmt = CCJSqlParserUtil.parse(sql); + TablesNamesFinder tablesNamesFinder = new TablesNamesFinder(); + List tableList = tablesNamesFinder.getTableList(stmt); + assertEquals(2, tableList.size()); + assertTrue(tableList.contains("dual")); + assertTrue(tableList.contains("mytbl")); + } } diff --git a/src/test/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidatorTest.java b/src/test/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidatorTest.java index dae05ff7c..3f85effbf 100644 --- a/src/test/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidatorTest.java +++ b/src/test/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidatorTest.java @@ -204,4 +204,28 @@ public void testAtTimeZoneExpression() throws JSQLParserException { validateNoErrors("SELECT DATE(date1 AT TIME ZONE 'UTC' AT TIME ZONE 'australia/sydney') AS another_date FROM mytbl", 1, EXPRESSIONS); } + + @Test + public void testJsonFunctionExpression() throws JSQLParserException { + validateNoErrors("SELECT json_array(null on null) FROM mytbl", 1, + EXPRESSIONS); + validateNoErrors("SELECT json_array(null null on null) FROM mytbl", 1, + EXPRESSIONS); + validateNoErrors("SELECT json_array(null, null null on null) FROM mytbl", 1, + EXPRESSIONS); + + validateNoErrors("SELECT json_object(null on null) FROM mytbl", 1, + EXPRESSIONS); + + validateNoErrors("SELECT json_object() FROM mytbl", 1, + EXPRESSIONS); + } + + @Test + public void testJsonAggregartFunctionExpression() throws JSQLParserException { + validateNoErrors("SELECT JSON_ARRAYAGG( a FORMAT JSON ABSENT ON NULL ) FILTER( WHERE name = 'Raj' ) OVER( PARTITION BY name ) FROM mytbl", 1, + EXPRESSIONS); + validateNoErrors("SELECT JSON_OBJECT( KEY foo VALUE bar FORMAT JSON, foo:bar, foo:bar ABSENT ON NULL) FROM mytbl", 1, + EXPRESSIONS); + } } From e386eaeac5100b8c17ce0216b845be1169786867 Mon Sep 17 00:00:00 2001 From: Andreas Reichel Date: Sun, 11 Jul 2021 17:37:05 +0700 Subject: [PATCH 8/9] Improve Test Coverage --- .../expression/ExpressionVisitorAdapter.java | 14 ++++++++++-- .../ExpressionVisitorAdapterTest.java | 22 +++++++++++++++++++ .../expression/JsonFunctionTest.java | 9 +++++++- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java index e53bbdb4c..b626b27d3 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java @@ -593,11 +593,21 @@ public void visit(TimezoneExpression expr) { @Override public void visit(JsonAggregateFunction expression) { - expression.accept(this); + Expression expr = expression.getExpression(); + if (expr!=null) { + expr.accept(this); + } + + expr = expression.getFilterExpression(); + if (expr!=null) { + expr.accept(this); + } } @Override public void visit(JsonFunction expression) { - expression.accept(this); + for (JsonFunctionExpression expr: expression.getExpressions()) { + expr.getExpression().accept(this); + } } } diff --git a/src/test/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapterTest.java b/src/test/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapterTest.java index 38924d6d9..32cbb3016 100644 --- a/src/test/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapterTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapterTest.java @@ -234,4 +234,26 @@ public void testAtTimeZoneExpression() throws JSQLParserException { ExpressionVisitorAdapter adapter = new ExpressionVisitorAdapter(); expr.accept(adapter); } + + @Test + public void testJsonFunction() throws JSQLParserException { + ExpressionVisitorAdapter adapter = new ExpressionVisitorAdapter(); + CCJSqlParserUtil + .parseExpression("JSON_OBJECT( KEY foo VALUE bar, KEY foo VALUE bar)") + .accept(adapter); + CCJSqlParserUtil + .parseExpression("JSON_ARRAY( (SELECT * from dual) )") + .accept(adapter); + } + + @Test + public void testJsonAggregateFunction() throws JSQLParserException { + ExpressionVisitorAdapter adapter = new ExpressionVisitorAdapter(); + CCJSqlParserUtil + .parseExpression("JSON_OBJECTAGG( KEY foo VALUE bar NULL ON NULL WITH UNIQUE KEYS ) FILTER( WHERE name = 'Raj' ) OVER( PARTITION BY name )") + .accept(adapter); + CCJSqlParserUtil + .parseExpression("JSON_ARRAYAGG( a FORMAT JSON ABSENT ON NULL ) FILTER( WHERE name = 'Raj' ) OVER( PARTITION BY name )") + .accept(adapter); + } } diff --git a/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java index 878f5585d..4bbe85007 100644 --- a/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java @@ -66,9 +66,16 @@ public void testObjectBuilder() throws JSQLParserException { // this should work because we compare based on KEY only Assert.assertEquals(keyValuePair1, keyValuePair2); - // this must fail because all the properties are considered + // this must fail because all the properties are considered Assert.assertFalse(Objects.equals(keyValuePair1.toString(), keyValuePair2.toString())); + JsonKeyValuePair keyValuePair3 = new JsonKeyValuePair("foo", "bar", false, false).withUsingKeyKeyword(false).withUsingValueKeyword(false).withUsingFormatJson(false); + Assert.assertNotNull(keyValuePair3 ); + Assert.assertEquals(keyValuePair3, keyValuePair3); + Assert.assertFalse(Objects.equals( keyValuePair3, f)); + + Assert.assertTrue(keyValuePair3.hashCode()!=0); + f.add(keyValuePair2); } From a470fe21e41c981195046a7f760cc8a06af6dec6 Mon Sep 17 00:00:00 2001 From: Andreas Reichel Date: Tue, 13 Jul 2021 21:12:06 +0700 Subject: [PATCH 9/9] KEYs can be SQL Value Expressions Add another testcase --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 10 ++++------ .../expression/JsonFunctionTest.java | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 8add9bd96..77f490c83 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -3680,10 +3680,9 @@ JsonFunction JsonFunction() : { ( // --- First Element [ "KEY" { usingKeyKeyword = true; } ] - keyToken = + ( keyToken = | keyToken = | keyToken = | keyToken = | keyToken = | keyToken = | keyToken = ) ( ":" | "VALUE" { usingValueKeyword = true; } ) - // token = | | | | { result.setValue( token.image ); } - valueToken = { keyValuePair = new JsonKeyValuePair( keyToken.image, valueToken.image, usingKeyKeyword, usingValueKeyword ); result.add(keyValuePair); } + ( valueToken = | valueToken = ) { keyValuePair = new JsonKeyValuePair( keyToken.image, valueToken.image, usingKeyKeyword, usingValueKeyword ); result.add(keyValuePair); } [ { keyValuePair.setUsingFormatJson( true ); } ] @@ -3772,10 +3771,9 @@ JsonAggregateFunction JsonAggregateFunction() : { ( "(" { result.setType( JsonFunctionType.OBJECT ); } [ "KEY" { result.setUsingKeyKeyword( true ); } ] - token = { result.setKey( token.image ); } + ( token = | token = | token = | token = | token = | token = | token = ) { result.setKey( token.image ); } ( ":" | "VALUE" {result.setUsingValueKeyword( true ); } ) - // token = | | | | { result.setValue( token.image ); } - token = { result.setValue( token.image ); } + ( token = | token = ) { result.setValue( token.image ); } [ { result.setUsingFormatJson( true ); } ] diff --git a/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java index 4bbe85007..e15dd9197 100644 --- a/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java @@ -169,5 +169,25 @@ public void testIssue1260() throws JSQLParserException { + " json_array(null on null)\n" + " )\n" + " from (\n" + " select 2 \"v0\"\n" + " union\n" + " select 4 \"ID\"\n" + " ) \"t\"\n" + " ) as text)", true); + + TestUtils.assertExpressionCanBeParsedAndDeparsed("listagg( json_object(key 'v0' value \"v0\"), ',' )", true); + + TestUtils.assertSqlCanBeParsedAndDeparsed("select (\n" + + " select coalesce(\n" + + " cast(('[' || listagg(\n" + + " json_object(key 'v0' value \"v0\"),\n" + + " ','\n" + + " ) || ']') as varchar(32672)),\n" + + " json_array()\n" + + " )\n" + + " from (\n" + + " select cast(null as timestamp) \"v0\"\n" + + " from SYSIBM.DUAL\n" + + " union all\n" + + " select timestamp '2000-03-15 10:15:00.0' \"a\"\n" + + " from SYSIBM.DUAL\n" + + " ) \"t\"\n" + + ")\n" + + "from SYSIBM.DUAL", true); } }