diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java index ad0d1b974..b6e96a83a 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java @@ -112,6 +112,9 @@ public T visit(Function function, S context) { if (function.getParameters() != null) { subExpressions.addAll(function.getParameters()); } + if (function.getChainedParameters() != null) { + subExpressions.addAll(function.getChainedParameters()); + } if (function.getKeep() != null) { subExpressions.add(function.getKeep()); } diff --git a/src/main/java/net/sf/jsqlparser/expression/Function.java b/src/main/java/net/sf/jsqlparser/expression/Function.java index d8ef6cb2e..5f0d3e2e7 100644 --- a/src/main/java/net/sf/jsqlparser/expression/Function.java +++ b/src/main/java/net/sf/jsqlparser/expression/Function.java @@ -26,6 +26,7 @@ public class Function extends ASTNodeAccessImpl implements Expression { private List nameparts; private ExpressionList parameters; + private ExpressionList chainedParameters; private NamedExpressionList namedParameters; private boolean allColumns = false; private boolean distinct = false; @@ -192,6 +193,20 @@ public void setParameters(ExpressionList list) { parameters = list; } + /** + * Additional function-call parameters for dialects that support chained function calls, e.g. + * quantile(0.95)(cost) in ClickHouse. + * + * @return the chained parameters of the function (if any, else null) + */ + public ExpressionList getChainedParameters() { + return chainedParameters; + } + + public void setChainedParameters(ExpressionList chainedParameters) { + this.chainedParameters = chainedParameters; + } + /** * the parameters might be named parameters, e.g. substring('foobar' from 2 for 3) * @@ -335,6 +350,10 @@ public String toString() { String ans = getName() + params; + if (chainedParameters != null) { + ans += "(" + chainedParameters + ")"; + } + if (nullHandling != null && isIgnoreNullsOutside()) { switch (nullHandling) { case IGNORE_NULLS: @@ -393,6 +412,11 @@ public Function withParameters(Expression... parameters) { return withParameters(new ExpressionList<>(parameters)); } + public Function withChainedParameters(ExpressionList chainedParameters) { + this.setChainedParameters(chainedParameters); + return this; + } + public Function withNamedParameters(NamedExpressionList namedParameters) { this.setNamedParameters(namedParameters); return this; diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index e19524076..549ffd04c 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -429,6 +429,10 @@ public Void visit(Function function, S context) { if (exprList != null) { visit(exprList, context); } + exprList = function.getChainedParameters(); + if (exprList != null) { + visit(exprList, context); + } return null; } 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 d8fe4054f..58da9deb5 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java @@ -920,6 +920,12 @@ public StringBuilder visit(Function function, S context) { builder.append(")"); } + if (function.getChainedParameters() != null) { + builder.append("("); + function.getChainedParameters().accept(this, context); + builder.append(")"); + } + if (function.getNullHandling() != null && function.isIgnoreNullsOutside()) { switch (function.getNullHandling()) { case IGNORE_NULLS: 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 48448ec9b..f489869b0 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 @@ -542,6 +542,7 @@ public Void visit(Function function, S context) { validateOptionalExpressionList(function.getNamedParameters()); validateOptionalExpressionList(function.getParameters()); + validateOptionalExpressionList(function.getChainedParameters()); Object attribute = function.getAttribute(); if (attribute instanceof Expression) { diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 800bc0b61..36bf0d00b 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -8308,6 +8308,7 @@ Function InternalFunction(boolean escaped): Function retval = new Function(); ObjectNames funcName; ExpressionList expressionList = null; + ExpressionList chainedExpressionList = null; KeepExpression keep = null; Expression expr = null; Expression attributeExpression = null; @@ -8374,6 +8375,10 @@ Function InternalFunction(boolean escaped): ")" + [ + LOOKAHEAD(2) "(" chainedExpressionList = ExpressionList() ")" + ] + [ LOOKAHEAD(2) "." ( // tricky lookahead since we do need to support the following constructs @@ -8409,6 +8414,7 @@ Function InternalFunction(boolean escaped): { retval.setEscaped(escaped); retval.setParameters(expressionList); + retval.setChainedParameters(chainedExpressionList); retval.setName(funcName.getNames()); retval.setKeep(keep); return retval; diff --git a/src/test/java/net/sf/jsqlparser/statement/select/ClickHouseTest.java b/src/test/java/net/sf/jsqlparser/statement/select/ClickHouseTest.java index 39f7d8799..648c65835 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/ClickHouseTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/ClickHouseTest.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.statement.select; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Function; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -116,4 +117,19 @@ public void testPreWhereWithWhereClause() throws JSQLParserException { Assertions.assertNotNull(select.getPreWhere()); Assertions.assertNotNull(select.getWhere()); } + + @Test + public void testParameterizedAggregateFunctionIssue2125() throws JSQLParserException { + String sql = + "SELECT toStartOfDay(timestamp) AS date, count(1) AS count, quantile(0.95)(cost) AS cost95 FROM apm_log_event"; + Select select = (Select) assertSqlCanBeParsedAndDeparsed(sql, true); + + Function function = ((PlainSelect) select.getSelectBody()) + .getSelectItem(2) + .getExpression(Function.class); + Assertions.assertNotNull(function.getParameters()); + Assertions.assertNotNull(function.getChainedParameters()); + Assertions.assertEquals(1, function.getParameters().size()); + Assertions.assertEquals(1, function.getChainedParameters().size()); + } }