diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java index e9b5f1b37..0a0d89faf 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java @@ -176,6 +176,12 @@ default void visit(Function function) { this.visit(function, null); } + T visit(FunctionParameterClauseExpression functionParameterClauseExpression, S context); + + default void visit(FunctionParameterClauseExpression functionParameterClauseExpression) { + this.visit(functionParameterClauseExpression, null); + } + T visit(SignedExpression signedExpression, S context); default void visit(SignedExpression signedExpression) { diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java index 88f92369b..17aa6dd99 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java @@ -123,6 +123,13 @@ public T visit(Function function, S context) { return visitExpressions(function, context, subExpressions); } + @Override + public T visit(FunctionParameterClauseExpression functionParameterClauseExpression, + S context) { + return visitExpressions(functionParameterClauseExpression, context, + functionParameterClauseExpression.getExpression()); + } + @Override public T visit(SignedExpression signedExpression, S context) { return signedExpression.getExpression().accept(this, context); diff --git a/src/main/java/net/sf/jsqlparser/expression/Function.java b/src/main/java/net/sf/jsqlparser/expression/Function.java index d8ef6cb2e..b95592885 100644 --- a/src/main/java/net/sf/jsqlparser/expression/Function.java +++ b/src/main/java/net/sf/jsqlparser/expression/Function.java @@ -16,6 +16,7 @@ import net.sf.jsqlparser.statement.select.Limit; import net.sf.jsqlparser.statement.select.OrderByElement; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -192,6 +193,58 @@ public void setParameters(ExpressionList list) { parameters = list; } + /** + * Returns the parameter expression at the given index without any attached clause wrapper. + * Returns {@code null} if there are no parameters. + */ + public Expression getParameterExpression(int parameterIndex) { + if (parameters == null) { + return null; + } + + Expression parameter = parameters.get(parameterIndex); + if (parameter instanceof FunctionParameterClauseExpression) { + return ((FunctionParameterClauseExpression) parameter).getExpression(); + } + return parameter; + } + + /** + * Returns the trailing clause attached to a parameter, e.g. {@code ERROR ON ERROR}, or + * {@code null} when no clause is attached. + */ + public String getParameterTrailingClause(int parameterIndex) { + if (parameters == null) { + return null; + } + + Object parameter = parameters.get(parameterIndex); + if (parameter instanceof FunctionParameterClauseExpression) { + return ((FunctionParameterClauseExpression) parameter).getClause(); + } + return null; + } + + /** + * Returns one entry per parameter with the attached trailing clause at that index, or + * {@code null} when no clause is attached to that parameter. + */ + public List getParameterTrailingClauses() { + if (parameters == null) { + return Collections.emptyList(); + } + + List clauses = new ArrayList<>(parameters.size()); + for (Object parameter : parameters) { + if (parameter instanceof FunctionParameterClauseExpression) { + clauses.add(((FunctionParameterClauseExpression) parameter).getClause()); + } else { + clauses.add(null); + } + } + return clauses; + } + /** * the parameters might be named parameters, e.g. substring('foobar' from 2 for 3) * diff --git a/src/main/java/net/sf/jsqlparser/expression/FunctionParameterClauseExpression.java b/src/main/java/net/sf/jsqlparser/expression/FunctionParameterClauseExpression.java new file mode 100644 index 000000000..e0c58d1a6 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/FunctionParameterClauseExpression.java @@ -0,0 +1,53 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.expression; + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +public class FunctionParameterClauseExpression extends ASTNodeAccessImpl implements Expression { + private Expression expression; + private String clause; + + public FunctionParameterClauseExpression(Expression expression, String clause) { + this.expression = expression; + this.clause = clause; + } + + public Expression getExpression() { + return expression; + } + + public FunctionParameterClauseExpression setExpression(Expression expression) { + this.expression = expression; + return this; + } + + public String getClause() { + return clause; + } + + public FunctionParameterClauseExpression setClause(String clause) { + this.clause = clause; + return this; + } + + @Override + public T accept(ExpressionVisitor expressionVisitor, S context) { + return expressionVisitor.visit(this, context); + } + + @Override + public String toString() { + if (clause == null || clause.isEmpty()) { + return expression != null ? expression.toString() : ""; + } + return expression + " " + clause; + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index f61c02e70..a7d40fec7 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -429,6 +429,15 @@ public Void visit(Function function, S context) { return null; } + @Override + public Void visit(FunctionParameterClauseExpression functionParameterClauseExpression, + S context) { + if (functionParameterClauseExpression.getExpression() != null) { + functionParameterClauseExpression.getExpression().accept(this, context); + } + return null; + } + @Override public Void visit(GreaterThan greaterThan, S context) { visitBinaryExpression(greaterThan); 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 c97a28423..9e7471c35 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java @@ -30,6 +30,7 @@ import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.expression.ExtractExpression; import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.FunctionParameterClauseExpression; import net.sf.jsqlparser.expression.HexValue; import net.sf.jsqlparser.expression.HighExpression; import net.sf.jsqlparser.expression.IntervalExpression; @@ -939,6 +940,13 @@ public StringBuilder visit(Function function, S context) { return builder; } + @Override + public StringBuilder visit( + FunctionParameterClauseExpression functionParameterClauseExpression, S context) { + builder.append(functionParameterClauseExpression); + return builder; + } + @Override public StringBuilder visit(ParenthesedSelect selectBody, S context) { selectBody.getSelect().accept(this, context); diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionListDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionListDeParser.java index 62b4c829b..f0782b00b 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionListDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionListDeParser.java @@ -11,6 +11,7 @@ import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.ExpressionVisitor; +import net.sf.jsqlparser.expression.FunctionParameterClauseExpression; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.expression.operators.relational.NamedExpressionList; import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; @@ -55,7 +56,11 @@ public void deParse(ExpressionList expressionList) { builder.append(name); builder.append(" "); } - expression.accept(expressionVisitor, null); + if (expression instanceof FunctionParameterClauseExpression) { + builder.append(expression); + } else { + expression.accept(expressionVisitor, null); + } i++; } 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 87f0205d8..785e6724b 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.ExpressionVisitor; import net.sf.jsqlparser.expression.ExtractExpression; import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.FunctionParameterClauseExpression; import net.sf.jsqlparser.expression.HexValue; import net.sf.jsqlparser.expression.HighExpression; import net.sf.jsqlparser.expression.IntervalExpression; @@ -547,6 +548,13 @@ public Void visit(Function function, S context) { return null; } + @Override + public Void visit(FunctionParameterClauseExpression functionParameterClauseExpression, + S context) { + validateOptionalExpression(functionParameterClauseExpression.getExpression(), this); + return null; + } + @Override public Void visit(DateValue dateValue, S context) { // nothing to validate diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index d57b29f16..caabec04e 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -164,6 +164,95 @@ public class CCJSqlParser extends AbstractJSqlParser { return true; } + private boolean isFunctionStartNoThrow() { + int index = 1; + Token token = getToken(index); + + if (token.kind == OPENING_CURLY_BRACKET) { + return true; + } + + if (token.kind == K_APPROXIMATE) { + index++; + token = getToken(index); + } + + if (token.kind == K_STRUCT) { + // BigQuery STRUCT(...) is parsed by StructType(), not generic function parsing. + return false; + } + + if (!isPotentialFunctionNameToken(token)) { + return false; + } + index++; + + while (true) { + token = getToken(index); + if (token.kind == OPENING_BRACKET) { + Token firstInnerToken = getToken(index + 1); + Token secondInnerToken = getToken(index + 2); + if ("+".equals(firstInnerToken.image) && secondInnerToken.kind == CLOSING_BRACKET) { + // Oracle outer join marker "(+)" belongs to a column reference, not a function. + return false; + } + return true; + } + + if (".".equals(token.image) || ":".equals(token.image)) { + index++; + token = getToken(index); + if (!isPotentialFunctionNameToken(token)) { + return false; + } + index++; + continue; + } + + return false; + } + } + + private boolean isPotentialFunctionNameToken(Token token) { + if (token == null || token.kind == EOF) { + return false; + } + + String image = token.image; + return !",".equals(image) + && !"(".equals(image) + && !")".equals(image) + && !"*".equals(image); + } + + private boolean isFunctionParameterClauseStartToken(Token token) { + if (token == null || token.kind == EOF || token.image == null) { + return false; + } + + if ("(".equals(token.image)) { + return "+".equals(getToken(2).image) && ")".equals(getToken(3).image); + } + + String image = token.image.toUpperCase(Locale.ROOT); + return "USING".equals(image) + || "WITH".equals(image) + || "WITHOUT".equals(image) + || "RETURNING".equals(image) + || "ERROR".equals(image) + || "DEFAULT".equals(image) + || "NULL".equals(image) + || "EMPTY".equals(image) + || "WRAPPER".equals(image) + || "STRICT".equals(image) + || "LAX".equals(image) + || "FORMAT".equals(image) + || "JSON".equals(image) + || "KEEP".equals(image) + || "OMIT".equals(image) + || "ABSENT".equals(image); + } + } PARSER_END(CCJSqlParser) @@ -6571,7 +6660,7 @@ Expression PrimaryExpression() #PrimaryExpression: | LOOKAHEAD(2, {!interrupted}) retval= CastExpression() - | LOOKAHEAD(16) retval = Function() [ LOOKAHEAD(2) retval = AnalyticExpression( (Function) retval ) ] + | LOOKAHEAD(2, { !interrupted && isFunctionStartNoThrow() }) retval = Function() [ LOOKAHEAD(2) retval = AnalyticExpression( (Function) retval ) ] | LOOKAHEAD(2) retval = DateUnitExpression() @@ -7810,6 +7899,51 @@ Function InternalFunction(boolean escaped): limit = PlainLimit() { retval.setLimit(limit); } ] + ( + LOOKAHEAD({ expressionList != null + && !expressionList.isEmpty() + && isFunctionParameterClauseStartToken(getToken(1)) + && hasTopLevelCommaBeforeClosingParenthesis() }) + { + if (!",".equals(getToken(1).image) && !")".equals(getToken(1).image)) { + List parameterClauseTokens = + captureFunctionParameterClauseUntilCommaOrClosingParenthesis(); + String currentParameterClause = parameterClauseTokens.isEmpty() + ? null + : String.join(" ", parameterClauseTokens); + if (currentParameterClause != null) { + Expression previousExpression = + (Expression) expressionList.get(expressionList.size() - 1); + expressionList.set(expressionList.size() - 1, + new FunctionParameterClauseExpression(previousExpression, + currentParameterClause)); + } + } + } + "," + expr = FunctionParameterExpression() { expressionList.add(expr); } + )* + + { + if (!")".equals(getToken(1).image) + && isFunctionParameterClauseStartToken(getToken(1)) + && expressionList != null + && !expressionList.isEmpty()) { + List trailingParameterClauseTokens = + captureFunctionParameterClauseUntilCommaOrClosingParenthesis(); + String trailingParameterClause = trailingParameterClauseTokens.isEmpty() + ? null + : String.join(" ", trailingParameterClauseTokens); + if (trailingParameterClause != null) { + Expression previousExpression = + (Expression) expressionList.get(expressionList.size() - 1); + expressionList.set(expressionList.size() - 1, + new FunctionParameterClauseExpression(previousExpression, + trailingParameterClause)); + } + } + } + ")" @@ -7853,6 +7987,25 @@ Function InternalFunction(boolean escaped): } } +Expression FunctionParameterExpression(): +{ + Expression expression; +} +{ + ( + LOOKAHEAD(2) expression = OracleNamedFunctionParameter() + | + LOOKAHEAD(7) expression = LambdaExpression() + | + LOOKAHEAD({ getAsBoolean(Feature.allowComplexParsing) && !interrupted }) expression = Expression() + | + expression = SimpleExpression() + ) + { + return expression; + } +} + XMLSerializeExpr XMLSerializeExpr(): { XMLSerializeExpr result; Expression expression; @@ -10528,6 +10681,61 @@ List captureRest() { return tokens; } +JAVACODE +boolean hasTopLevelCommaBeforeClosingParenthesis() { + Token tok; + int parenthesisDepth = 0; + int index = 1; + + while (true) { + tok = getToken(index++); + if (tok.kind == EOF) { + return false; + } + if ("(".equals(tok.image)) { + parenthesisDepth++; + } else if (")".equals(tok.image)) { + if (parenthesisDepth == 0) { + return false; + } + parenthesisDepth--; + } else if (",".equals(tok.image) && parenthesisDepth == 0) { + return true; + } + } +} + +JAVACODE +List captureFunctionParameterClauseUntilCommaOrClosingParenthesis() { + List tokens = new LinkedList(); + Token tok; + int parenthesisDepth = 0; + + while (true) { + tok = getToken(1); + int l = tokens.size(); + + if (tok.kind == EOF + || (parenthesisDepth == 0 && (")".equals(tok.image) || ",".equals(tok.image)))) { + break; + } else if (l > 0 && (tok.image.equals(".") || tokens.get(l - 1).endsWith("."))) { + tokens.set(l - 1, tokens.get(l - 1) + tok.image); + } else { + tokens.add(tok.image); + } + + if ("(".equals(tok.image)) { + parenthesisDepth++; + } else if (")".equals(tok.image) && parenthesisDepth > 0) { + parenthesisDepth--; + } + + tok = getNextToken(); + } + + return tokens; +} + /** * Reads the tokens of a function or procedure body. * A function body can end in 2 ways: diff --git a/src/test/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapterTest.java b/src/test/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapterTest.java index 0654480da..3b4f98334 100644 --- a/src/test/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapterTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapterTest.java @@ -328,4 +328,37 @@ public void testIntervalWithNoExpression() throws JSQLParserException { ExpressionVisitorAdapter adapter = new ExpressionVisitorAdapter<>(); expr.accept(adapter, null); } + + @Test + public void testFunctionParameterClauseExpressionVisitor() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseExpression( + "json_query('{\"a\":1}', '$' ERROR ON ERROR, '$.x' RETURNING VARCHAR(10), '$.z' WITH ARRAY WRAPPER)"); + + final List parameterClauses = new ArrayList<>(); + final List stringValues = new ArrayList<>(); + + expr.accept(new ExpressionVisitorAdapter() { + @Override + public Void visit(FunctionParameterClauseExpression functionParameterClauseExpression, + S context) { + parameterClauses.add(functionParameterClauseExpression.getClause()); + return super.visit(functionParameterClauseExpression, context); + } + + @Override + public Void visit(StringValue stringValue, S context) { + stringValues.add(stringValue.toString()); + return super.visit(stringValue, context); + } + }, null); + + assertEquals(3, parameterClauses.size()); + assertEquals("ERROR ON ERROR", parameterClauses.get(0)); + assertEquals("RETURNING VARCHAR ( 10 )", parameterClauses.get(1)); + assertEquals("WITH ARRAY WRAPPER", parameterClauses.get(2)); + assertEquals(4, stringValues.size()); + assertEquals("'$'", stringValues.get(1)); + assertEquals("'$.x'", stringValues.get(2)); + assertEquals("'$.z'", stringValues.get(3)); + } } diff --git a/src/test/java/net/sf/jsqlparser/expression/FunctionTest.java b/src/test/java/net/sf/jsqlparser/expression/FunctionTest.java index f977d558d..6c5302d00 100644 --- a/src/test/java/net/sf/jsqlparser/expression/FunctionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/FunctionTest.java @@ -10,7 +10,9 @@ package net.sf.jsqlparser.expression; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.test.TestUtils; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -126,4 +128,78 @@ void TestIntervalParameterIssue2272() throws JSQLParserException { "SELECT DATE_SUB('2025-06-19', INTERVAL QUARTER(STR_TO_DATE('20250619', '%Y%m%d')) - 1 QUARTER) from dual"; TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + + @ParameterizedTest + @ValueSource(strings = { + "select json_query('{\"a\":1}', 'strict $.keyvalue()' WITH WRAPPER) from tbl", + "select json_query('{\"a\":1}', 'strict $.keyvalue()' WITH ARRAY WRAPPER) from tbl", + "select json_query('{\"a\":1}', 'strict $.keyvalue()' WITHOUT WRAPPER) from tbl", + "select json_query('{\"a\":1}', 'strict $.keyvalue()' WITHOUT ARRAY WRAPPER) from tbl", + "select json_query('{\"a\":1}', 'strict $.keyvalue()' WITH CONDITIONAL ARRAY WRAPPER) from tbl", + "select json_query('{\"a\":1}', '$' ERROR ON ERROR) from tbl", + "select json_query('{\"a\":1}', '$' RETURNING VARCHAR(100) NULL ON EMPTY) from tbl" + }) + void testJsonQueryWithWrapperClauseInsideFunctionParameters(String sqlStr) + throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true, + parser -> parser.withAllowComplexParsing(false)); + } + + @ParameterizedTest + @ValueSource(strings = { + "select json_query('{\"customer\" : 100, \"region\" : \"AFRICA\"}', 'strict $.keyvalue()' WITH ARRAY WRAPPER, '$.region') from tbl", + "select json_query('{\"customer\" : 100, \"region\" : \"AFRICA\"}', '$' RETURNING VARCHAR(100), '$.region') from tbl", + "select json_query('{\"customer\" : 100, \"region\" : \"AFRICA\"}', '$' ERROR ON ERROR, '$.region') from tbl" + }) + void testJsonQueryTrailingClauseBeforeLastParameterKeepsAdditionalParameters(String sqlStr) + throws JSQLParserException { + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true, + parser -> parser.withAllowComplexParsing(false)); + Function function = select.getSelectItem(0).getExpression(Function.class); + + Assertions.assertThat(function.getParameters()).hasSize(3); + if (sqlStr.contains("WITH ARRAY WRAPPER")) { + Assertions.assertThat(function.getParameters().get(1).toString()) + .isEqualTo("'strict $.keyvalue()' WITH ARRAY WRAPPER"); + } else if (sqlStr.contains("RETURNING")) { + Assertions.assertThat(function.getParameters().get(1).toString()) + .isEqualTo("'$' RETURNING VARCHAR ( 100 )"); + } else { + Assertions.assertThat(function.getParameters().get(1).toString()) + .isEqualTo("'$' ERROR ON ERROR"); + } + Assertions.assertThat(function.getParameters().get(2)).isInstanceOf(StringValue.class); + Assertions.assertThat(function.getParameters().get(2).toString()).isEqualTo("'$.region'"); + Assertions.assertThat(function.getParameterTrailingClauses()).hasSize(3); + } + + @Test + void testFunctionSupportsMultipleParameterLevelTrailingClauses() throws JSQLParserException { + String sqlStr = + "select json_query('{\"a\":1}', '$' ERROR ON ERROR, '$.x' RETURNING VARCHAR(10), '$.z' WITH ARRAY WRAPPER) from tbl"; + + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true, + parser -> parser.withAllowComplexParsing(false)); + Function function = select.getSelectItem(0).getExpression(Function.class); + + Assertions.assertThat(function.getParameters()).hasSize(4); + Assertions.assertThat(function.getParameters().get(1).toString()) + .isEqualTo("'$' ERROR ON ERROR"); + Assertions.assertThat(function.getParameters().get(2).toString()) + .isEqualTo("'$.x' RETURNING VARCHAR ( 10 )"); + Assertions.assertThat(function.getParameters().get(3).toString()) + .isEqualTo("'$.z' WITH ARRAY WRAPPER"); + + Assertions.assertThat(function.getParameterExpression(1).toString()).isEqualTo("'$'"); + Assertions.assertThat(function.getParameterExpression(2).toString()).isEqualTo("'$.x'"); + Assertions.assertThat(function.getParameterExpression(3).toString()).isEqualTo("'$.z'"); + Assertions.assertThat(function.getParameterTrailingClause(1)).isEqualTo("ERROR ON ERROR"); + Assertions.assertThat(function.getParameterTrailingClause(2)) + .isEqualTo("RETURNING VARCHAR ( 10 )"); + Assertions.assertThat(function.getParameterTrailingClause(3)) + .isEqualTo("WITH ARRAY WRAPPER"); + Assertions.assertThat(function.getParameterTrailingClauses()) + .containsExactly(null, "ERROR ON ERROR", "RETURNING VARCHAR ( 10 )", + "WITH ARRAY WRAPPER"); + } } diff --git a/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/analytic_query07.sql b/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/analytic_query07.sql index 328f42a4e..d182a0c82 100644 --- a/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/analytic_query07.sql +++ b/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/analytic_query07.sql @@ -49,4 +49,8 @@ --@FAILURE: Encountered unexpected token: "(" "(" recorded first on Aug 3, 2021, 7:20:08 AM --@FAILURE: Encountered: "(" / "(", at line 36, column 15, in lexical state DEFAULT. recorded first on 23 May 2025, 22:04:10 ---@FAILURE: Encountered: / "(", at line 36, column 15, in lexical state DEFAULT. recorded first on 9 Jul 2025, 17:09:17 \ No newline at end of file +--@FAILURE: Encountered: / "(", at line 36, column 15, in lexical state DEFAULT. recorded first on 9 Jul 2025, 17:09:17 +--@FAILURE: Encountered: / "(", at line 21, column 12, in lexical state DEFAULT. recorded first on 13 Feb 2026, 12:31:35 +--@FAILURE: Encountered: "+" / "+", at line 24, column 34, in lexical state DEFAULT. recorded first on 13 Feb 2026, 12:40:36 +--@SUCCESSFULLY_PARSED_AND_DEPARSED first on 13 Feb 2026, 12:43:01 +--@FAILURE: Encountered: / "using", at line 36, column 45, in lexical state DEFAULT. recorded first on 13 Feb 2026, 12:47:04 \ No newline at end of file diff --git a/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/cast_multiset07.sql b/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/cast_multiset07.sql index 03155690b..daea7e399 100644 --- a/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/cast_multiset07.sql +++ b/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/cast_multiset07.sql @@ -48,4 +48,5 @@ where --@FAILURE: select "a3"."r_id" "r_id" from "pe" "a3","me" "a2" where "a3"."m_id"="a2"."m_id" and "a2"."mi_t"=any((select "a4"."sys$"."id" from the(select "qa"."u_pkg"."getchartable"("qa"."u_pkg"."glist"(cursor(select "qa"."u_pkg"."glist"(cursor(select "a6"."mi_t" "mi_t" from "me" "a6" connect by "a6"."mi_uid"=prior "a6"."mi_id" start with "a6"."mi_t"=:b1))"lst" from "sys"."dual" "a5")))from dual)"a4")) recorded first on Aug 3, 2021, 7:20:08 AM ---@SUCCESSFULLY_PARSED_AND_DEPARSED first on Mar 25, 2023, 9:18:30 AM \ No newline at end of file +--@SUCCESSFULLY_PARSED_AND_DEPARSED first on Mar 25, 2023, 9:18:30 AM +--@FAILURE: Encountered: / "(", at line 22, column 3, in lexical state DEFAULT. recorded first on 13 Feb 2026, 12:31:35 \ No newline at end of file diff --git a/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/cluster_set01.sql b/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/cluster_set01.sql index d2ae24b1e..199928a46 100644 --- a/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/cluster_set01.sql +++ b/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/cluster_set01.sql @@ -44,4 +44,7 @@ order by prob desc, cl_id asc, conf desc, attr asc, val asc --@FAILURE: Encountered unexpected token: "(" "(" recorded first on Aug 3, 2021, 7:20:08 AM --@FAILURE: Encountered: "(" / "(", at line 31, column 36, in lexical state DEFAULT. recorded first on 15 May 2025, 16:24:08 ---@FAILURE: Encountered: / "(", at line 31, column 36, in lexical state DEFAULT. recorded first on 9 Jul 2025, 17:09:17 \ No newline at end of file +--@FAILURE: Encountered: / "(", at line 31, column 36, in lexical state DEFAULT. recorded first on 9 Jul 2025, 17:09:17 +--@FAILURE: Encountered: / "(", at line 18, column 13, in lexical state DEFAULT. recorded first on 13 Feb 2026, 12:31:35 +--@SUCCESSFULLY_PARSED_AND_DEPARSED first on 13 Feb 2026, 12:40:35 +--@FAILURE: Encountered: / "using", at line 31, column 66, in lexical state DEFAULT. recorded first on 13 Feb 2026, 12:47:04 \ No newline at end of file diff --git a/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/condition06.sql b/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/condition06.sql index d6295170c..564edd3f4 100644 --- a/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/condition06.sql +++ b/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/condition06.sql @@ -23,4 +23,5 @@ and ( ( t1.scode like 'mmm' and t2.scode like 'xax' ) ) --@FAILURE: Encountered unexpected token: "(" "(" recorded first on Aug 3, 2021, 7:20:08 AM ---@FAILURE: Encountered: / "is", at line 19, column 31, in lexical state DEFAULT. recorded first on 15 May 2025, 16:24:08 \ No newline at end of file +--@FAILURE: Encountered: / "is", at line 19, column 31, in lexical state DEFAULT. recorded first on 15 May 2025, 16:24:08 +--@FAILURE: Encountered: "+" / "+", at line 13, column 13, in lexical state DEFAULT. recorded first on 13 Feb 2026, 12:40:36 \ No newline at end of file diff --git a/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/condition11.sql b/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/condition11.sql index 2b4866121..dfbf7f978 100644 --- a/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/condition11.sql +++ b/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/condition11.sql @@ -17,4 +17,7 @@ and 0 = Lib.SKU(X.sid, nvl(Z.cid, '^')) --@FAILURE: Encountered unexpected token: "(" "(" recorded first on Aug 3, 2021, 7:20:08 AM --@FAILURE: Encountered: "(" / "(", at line 14, column 26, in lexical state DEFAULT. recorded first on 15 May 2025, 16:24:08 ---@FAILURE: Encountered: / "(", at line 14, column 26, in lexical state DEFAULT. recorded first on 9 Jul 2025, 17:09:17 \ No newline at end of file +--@FAILURE: Encountered: / "(", at line 14, column 26, in lexical state DEFAULT. recorded first on 9 Jul 2025, 17:09:17 +--@FAILURE: Encountered: / ",", at line 14, column 41, in lexical state DEFAULT. recorded first on 13 Feb 2026, 12:31:35 +--@FAILURE: Encountered: "+" / "+", at line 13, column 18, in lexical state DEFAULT. recorded first on 13 Feb 2026, 12:40:36 +--@SUCCESSFULLY_PARSED_AND_DEPARSED first on 13 Feb 2026, 12:43:01 \ No newline at end of file diff --git a/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/function07.sql b/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/function07.sql index aa451e33f..9e93c00cf 100644 --- a/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/function07.sql +++ b/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/function07.sql @@ -17,4 +17,6 @@ select cust_gender, count(*) as cnt, round(avg(age)) as avg_age --@FAILURE: Encountered unexpected token: "(" "(" recorded first on Aug 3, 2021, 7:20:08 AM --@FAILURE: Encountered: "(" / "(", at line 12, column 20, in lexical state DEFAULT. recorded first on 15 May 2025, 16:24:08 ---@FAILURE: Encountered: / "(", at line 12, column 20, in lexical state DEFAULT. recorded first on 9 Jul 2025, 17:09:17 \ No newline at end of file +--@FAILURE: Encountered: / "(", at line 12, column 20, in lexical state DEFAULT. recorded first on 9 Jul 2025, 17:09:17 +--@FAILURE: Encountered: "=" / "=", at line 13, column 61, in lexical state DEFAULT. recorded first on 13 Feb 2026, 12:31:35 +--@FAILURE: Encountered: / "cost", at line 12, column 39, in lexical state DEFAULT. recorded first on 13 Feb 2026, 12:47:04 \ No newline at end of file diff --git a/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/join04.sql b/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/join04.sql index 790d0fc9c..2894f1041 100644 --- a/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/join04.sql +++ b/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/join04.sql @@ -14,4 +14,5 @@ select d.department_id, e.last_name ---@SUCCESSFULLY_PARSED_AND_DEPARSED first on Aug 3, 2021, 7:20:08 AM \ No newline at end of file +--@SUCCESSFULLY_PARSED_AND_DEPARSED first on Aug 3, 2021, 7:20:08 AM +--@FAILURE: Encountered: "+" / "+", at line 12, column 44, in lexical state DEFAULT. recorded first on 13 Feb 2026, 12:40:36 \ No newline at end of file diff --git a/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/keywordasidentifier03.sql b/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/keywordasidentifier03.sql index 2344d13eb..b2c4edaf0 100644 --- a/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/keywordasidentifier03.sql +++ b/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/keywordasidentifier03.sql @@ -32,4 +32,5 @@ ALL_IND_EXPRESSIONS ie where ind.index_name = ie.index_name(+) and ind.index_owner = ie.index_owner(+) ---@SUCCESSFULLY_PARSED_AND_DEPARSED first on Aug 3, 2021, 7:20:08 AM \ No newline at end of file +--@SUCCESSFULLY_PARSED_AND_DEPARSED first on Aug 3, 2021, 7:20:08 AM +--@FAILURE: Encountered: "+" / "+", at line 32, column 38, in lexical state DEFAULT. recorded first on 13 Feb 2026, 12:40:36 \ No newline at end of file diff --git a/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/query_factoring11.sql b/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/query_factoring11.sql index 4cfde12dc..625ff712b 100644 --- a/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/query_factoring11.sql +++ b/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/query_factoring11.sql @@ -17,4 +17,5 @@ from col_generator group by batch_id order by 1 ---@SUCCESSFULLY_PARSED_AND_DEPARSED first on Aug 3, 2021, 7:20:08 AM \ No newline at end of file +--@SUCCESSFULLY_PARSED_AND_DEPARSED first on Aug 3, 2021, 7:20:08 AM +--@FAILURE: Encountered: "+" / "+", at line 14, column 19, in lexical state DEFAULT. recorded first on 13 Feb 2026, 12:40:36 \ No newline at end of file diff --git a/src/test/resources/simple_parsing.txt b/src/test/resources/simple_parsing.txt index 7fc390fab..b67171a52 100644 --- a/src/test/resources/simple_parsing.txt +++ b/src/test/resources/simple_parsing.txt @@ -248,4 +248,29 @@ WITH FUNCTION takesArray(x array) RETURNS double RETURN x[1] + x[2] + x[3] -SELECT takesArray(array[1.0, 2.0, 3.0]); \ No newline at end of file +SELECT takesArray(array[1.0, 2.0, 3.0]); + +select + json_query('{"customer" : 100, "region" : "AFRICA"},{"region" : "ASIA"},{"customer" : 300, "region" : "AFRICA", "comment" : null}', 'strict $.keyvalue()' WITH ARRAY WRAPPER) +from + tbl; + +select + json_query('{"customer" : 100, "region" : "AFRICA"}', 'strict $.keyvalue()' WITH ARRAY WRAPPER, '$.region') +from + tbl; + +select + json_query('{"customer" : 100, "region" : "AFRICA"}', '$' RETURNING VARCHAR(100), '$.region') +from + tbl; + +select + json_query('{"customer" : 100, "region" : "AFRICA"}', '$' ERROR ON ERROR, '$.region') +from + tbl; + +select + json_query('{"a":1}', '$' ERROR ON ERROR, '$.x' RETURNING VARCHAR(10), '$.z' WITH ARRAY WRAPPER) +from + tbl;