diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java index e9b5f1b37..351feba1c 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) { @@ -652,6 +658,14 @@ default void visit(JsonFunction jsonFunction) { this.visit(jsonFunction, null); } + default T visit(JsonTableFunction jsonTableFunction, S context) { + return visit((Function) jsonTableFunction, context); + } + + default void visit(JsonTableFunction jsonTableFunction) { + this.visit(jsonTableFunction, null); + } + T visit(ConnectByRootOperator connectByRootOperator, S context); default void visit(ConnectByRootOperator connectByRootOperator) { diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java index 88f92369b..985671abb 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); @@ -722,12 +729,40 @@ public T visit(JsonAggregateFunction jsonAggregateFunction, S context) { @Override public T visit(JsonFunction jsonFunction, S context) { ArrayList subExpressions = new ArrayList<>(); + for (JsonKeyValuePair keyValuePair : jsonFunction.getKeyValuePairs()) { + if (keyValuePair.getKey() instanceof Expression) { + subExpressions.add((Expression) keyValuePair.getKey()); + } + if (keyValuePair.getValue() instanceof Expression) { + subExpressions.add((Expression) keyValuePair.getValue()); + } + } for (JsonFunctionExpression expr : jsonFunction.getExpressions()) { subExpressions.add(expr.getExpression()); } + if (jsonFunction.getInputExpression() != null) { + subExpressions.add(jsonFunction.getInputExpression().getExpression()); + } + if (jsonFunction.getJsonPathExpression() != null) { + subExpressions.add(jsonFunction.getJsonPathExpression()); + } + subExpressions.addAll(jsonFunction.getPassingExpressions()); + if (jsonFunction.getOnEmptyBehavior() != null + && jsonFunction.getOnEmptyBehavior().getExpression() != null) { + subExpressions.add(jsonFunction.getOnEmptyBehavior().getExpression()); + } + if (jsonFunction.getOnErrorBehavior() != null + && jsonFunction.getOnErrorBehavior().getExpression() != null) { + subExpressions.add(jsonFunction.getOnErrorBehavior().getExpression()); + } return visitExpressions(jsonFunction, context, subExpressions); } + @Override + public T visit(JsonTableFunction jsonTableFunction, S context) { + return visitExpressions(jsonTableFunction, context, jsonTableFunction.getAllExpressions()); + } + @Override public T visit(ConnectByRootOperator connectByRootOperator, S context) { return connectByRootOperator.getColumn().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/expression/JsonFunction.java b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java index 176759c6d..aee8e7bf3 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java @@ -13,6 +13,7 @@ import java.util.Objects; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.create.table.ColDataType; /** * Represents a JSON-Function.
@@ -25,13 +26,110 @@ * @author Andreas Reichel */ public class JsonFunction extends ASTNodeAccessImpl implements Expression { + public enum JsonOnResponseBehaviorType { + ERROR, NULL, DEFAULT, EMPTY_ARRAY, EMPTY_OBJECT, TRUE, FALSE, UNKNOWN + } + + public enum JsonWrapperType { + WITHOUT, WITH + } + + public enum JsonWrapperMode { + CONDITIONAL, UNCONDITIONAL + } + + public enum JsonQuotesType { + KEEP, OMIT + } + + public static class JsonOnResponseBehavior { + private JsonOnResponseBehaviorType type; + private Expression expression; + + public JsonOnResponseBehavior(JsonOnResponseBehaviorType type) { + this(type, null); + } + + public JsonOnResponseBehavior(JsonOnResponseBehaviorType type, Expression expression) { + this.type = type; + this.expression = expression; + } + + public JsonOnResponseBehaviorType getType() { + return type; + } + + public void setType(JsonOnResponseBehaviorType type) { + this.type = type; + } + + public Expression getExpression() { + return expression; + } + + public void setExpression(Expression expression) { + this.expression = expression; + } + + public StringBuilder append(StringBuilder builder) { + switch (type) { + case ERROR: + builder.append("ERROR"); + break; + case NULL: + builder.append("NULL"); + break; + case DEFAULT: + builder.append("DEFAULT ").append(expression); + break; + case EMPTY_ARRAY: + builder.append("EMPTY ARRAY"); + break; + case EMPTY_OBJECT: + builder.append("EMPTY OBJECT"); + break; + case TRUE: + builder.append("TRUE"); + break; + case FALSE: + builder.append("FALSE"); + break; + case UNKNOWN: + builder.append("UNKNOWN"); + break; + default: + // this should never happen + } + return builder; + } + + @Override + public String toString() { + return append(new StringBuilder()).toString(); + } + } + private final ArrayList keyValuePairs = new ArrayList<>(); private final ArrayList expressions = new ArrayList<>(); + private final ArrayList passingExpressions = new ArrayList<>(); + private final ArrayList additionalQueryPathArguments = new ArrayList<>(); private JsonFunctionType functionType; private JsonAggregateOnNullType onNullType; private JsonAggregateUniqueKeysType uniqueKeysType; private boolean isStrict = false; + private JsonFunctionExpression inputExpression; + private Expression jsonPathExpression; + private ColDataType returningType; + private boolean returningFormatJson; + private String returningEncoding; + private JsonOnResponseBehavior onEmptyBehavior; + private JsonOnResponseBehavior onErrorBehavior; + private JsonWrapperType wrapperType; + private JsonWrapperMode wrapperMode; + private boolean wrapperArray; + private JsonQuotesType quotesType; + private boolean quotesOnScalarString; public JsonFunction() {} @@ -84,6 +182,118 @@ public void add(int i, JsonFunctionExpression expression) { expressions.add(i, expression); } + public ArrayList getPassingExpressions() { + return passingExpressions; + } + + public boolean addPassingExpression(Expression expression) { + return passingExpressions.add(expression); + } + + public ArrayList getAdditionalQueryPathArguments() { + return additionalQueryPathArguments; + } + + public boolean addAdditionalQueryPathArgument(String argument) { + return additionalQueryPathArguments.add(argument); + } + + public JsonFunctionExpression getInputExpression() { + return inputExpression; + } + + public void setInputExpression(JsonFunctionExpression inputExpression) { + this.inputExpression = inputExpression; + } + + public Expression getJsonPathExpression() { + return jsonPathExpression; + } + + public void setJsonPathExpression(Expression jsonPathExpression) { + this.jsonPathExpression = jsonPathExpression; + } + + public ColDataType getReturningType() { + return returningType; + } + + public void setReturningType(ColDataType returningType) { + this.returningType = returningType; + } + + public boolean isReturningFormatJson() { + return returningFormatJson; + } + + public void setReturningFormatJson(boolean returningFormatJson) { + this.returningFormatJson = returningFormatJson; + } + + public String getReturningEncoding() { + return returningEncoding; + } + + public void setReturningEncoding(String returningEncoding) { + this.returningEncoding = returningEncoding; + } + + public JsonOnResponseBehavior getOnEmptyBehavior() { + return onEmptyBehavior; + } + + public void setOnEmptyBehavior(JsonOnResponseBehavior onEmptyBehavior) { + this.onEmptyBehavior = onEmptyBehavior; + } + + public JsonOnResponseBehavior getOnErrorBehavior() { + return onErrorBehavior; + } + + public void setOnErrorBehavior(JsonOnResponseBehavior onErrorBehavior) { + this.onErrorBehavior = onErrorBehavior; + } + + public JsonWrapperType getWrapperType() { + return wrapperType; + } + + public void setWrapperType(JsonWrapperType wrapperType) { + this.wrapperType = wrapperType; + } + + public JsonWrapperMode getWrapperMode() { + return wrapperMode; + } + + public void setWrapperMode(JsonWrapperMode wrapperMode) { + this.wrapperMode = wrapperMode; + } + + public boolean isWrapperArray() { + return wrapperArray; + } + + public void setWrapperArray(boolean wrapperArray) { + this.wrapperArray = wrapperArray; + } + + public JsonQuotesType getQuotesType() { + return quotesType; + } + + public void setQuotesType(JsonQuotesType quotesType) { + this.quotesType = quotesType; + } + + public boolean isQuotesOnScalarString() { + return quotesOnScalarString; + } + + public void setQuotesOnScalarString(boolean quotesOnScalarString) { + this.quotesOnScalarString = quotesOnScalarString; + } + public boolean isEmpty() { return keyValuePairs.isEmpty(); } @@ -170,6 +380,15 @@ public StringBuilder append(StringBuilder builder) { case ARRAY: appendArray(builder); break; + case VALUE: + appendValue(builder); + break; + case QUERY: + appendQuery(builder); + break; + case EXISTS: + appendExists(builder); + break; default: // this should never happen really } @@ -193,6 +412,7 @@ public StringBuilder appendObject(StringBuilder builder) { builder.append(" STRICT"); } appendUniqueKeys(builder); + appendReturningClause(builder, true); builder.append(" ) "); @@ -243,11 +463,126 @@ public StringBuilder appendArray(StringBuilder builder) { } appendOnNullType(builder); + appendReturningClause(builder, true); builder.append(") "); return builder; } + @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) + public StringBuilder appendValue(StringBuilder builder) { + builder.append("JSON_VALUE("); + appendValueOrQueryPrefix(builder); + + if (returningType != null) { + builder.append(" RETURNING ").append(returningType); + } + + appendOnResponseClause(builder, onEmptyBehavior, "EMPTY"); + appendOnResponseClause(builder, onErrorBehavior, "ERROR"); + + builder.append(")"); + return builder; + } + + @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) + public StringBuilder appendQuery(StringBuilder builder) { + builder.append("JSON_QUERY("); + appendValueOrQueryPrefix(builder); + + appendReturningClause(builder, true); + + appendWrapperClause(builder); + appendQuotesClause(builder); + appendOnResponseClause(builder, onEmptyBehavior, "EMPTY"); + appendOnResponseClause(builder, onErrorBehavior, "ERROR"); + + for (String additionalQueryPathArgument : additionalQueryPathArguments) { + builder.append(", ").append(additionalQueryPathArgument); + } + + builder.append(")"); + return builder; + } + + @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) + public StringBuilder appendExists(StringBuilder builder) { + builder.append("JSON_EXISTS("); + appendValueOrQueryPrefix(builder); + appendOnResponseClause(builder, onErrorBehavior, "ERROR"); + builder.append(")"); + return builder; + } + + private void appendValueOrQueryPrefix(StringBuilder builder) { + if (inputExpression != null) { + inputExpression.append(builder); + } + + if (jsonPathExpression != null) { + if (inputExpression != null) { + builder.append(", "); + } + builder.append(jsonPathExpression); + } + + if (!passingExpressions.isEmpty()) { + builder.append(" PASSING "); + boolean comma = false; + for (Expression passingExpression : passingExpressions) { + if (comma) { + builder.append(", "); + } else { + comma = true; + } + builder.append(passingExpression); + } + } + } + + private void appendOnResponseClause(StringBuilder builder, JsonOnResponseBehavior behavior, + String clause) { + if (behavior != null) { + builder.append(" "); + behavior.append(builder); + builder.append(" ON ").append(clause); + } + } + + private void appendReturningClause(StringBuilder builder, boolean formatJsonAllowed) { + if (returningType != null) { + builder.append(" RETURNING ").append(returningType); + if (formatJsonAllowed && returningFormatJson) { + builder.append(" FORMAT JSON"); + if (returningEncoding != null) { + builder.append(" ENCODING ").append(returningEncoding); + } + } + } + } + + private void appendWrapperClause(StringBuilder builder) { + if (wrapperType != null) { + builder.append(" ").append(wrapperType); + if (wrapperMode != null) { + builder.append(" ").append(wrapperMode); + } + if (wrapperArray) { + builder.append(" ARRAY"); + } + builder.append(" WRAPPER"); + } + } + + private void appendQuotesClause(StringBuilder builder) { + if (quotesType != null) { + builder.append(" ").append(quotesType).append(" QUOTES"); + if (quotesOnScalarString) { + builder.append(" ON SCALAR STRING"); + } + } + } + @Override public String toString() { StringBuilder builder = new StringBuilder(); diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonFunctionExpression.java b/src/main/java/net/sf/jsqlparser/expression/JsonFunctionExpression.java index 5df7ad310..738c09fc2 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonFunctionExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonFunctionExpression.java @@ -21,6 +21,7 @@ public class JsonFunctionExpression implements Serializable { private final Expression expression; private boolean usingFormatJson = false; + private String encoding; public JsonFunctionExpression(Expression expression) { this.expression = Objects.requireNonNull(expression, "The EXPRESSION must not be null"); @@ -43,8 +44,28 @@ public JsonFunctionExpression withUsingFormatJson(boolean usingFormatJson) { return this; } + public String getEncoding() { + return encoding; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public JsonFunctionExpression withEncoding(String encoding) { + this.setEncoding(encoding); + return this; + } + public StringBuilder append(StringBuilder builder) { - return builder.append(getExpression()).append(isUsingFormatJson() ? " FORMAT JSON" : ""); + builder.append(getExpression()); + if (isUsingFormatJson()) { + builder.append(" FORMAT JSON"); + if (encoding != null) { + builder.append(" ENCODING ").append(encoding); + } + } + return builder; } @Override diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonFunctionType.java b/src/main/java/net/sf/jsqlparser/expression/JsonFunctionType.java index 821416c9c..ebd497e79 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonFunctionType.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonFunctionType.java @@ -14,7 +14,7 @@ * @author Andreas Reichel */ public enum JsonFunctionType { - OBJECT, ARRAY, + OBJECT, ARRAY, VALUE, QUERY, EXISTS, /** * Not used anymore diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java b/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java index f8d43aa97..18fb4752d 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java @@ -23,6 +23,7 @@ public class JsonKeyValuePair implements Serializable { private boolean usingKeyKeyword; private JsonKeyValuePairSeparator separator; private boolean usingFormatJson = false; + private String encoding; /** * Please use the Constructor with {@link JsonKeyValuePairSeparator} parameter. @@ -108,6 +109,19 @@ public JsonKeyValuePair withUsingFormatJson(boolean usingFormatJson) { return this; } + public String getEncoding() { + return encoding; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public JsonKeyValuePair withEncoding(String encoding) { + this.setEncoding(encoding); + return this; + } + @Override public int hashCode() { int hash = 7; @@ -151,6 +165,9 @@ public StringBuilder append(StringBuilder builder) { if (isUsingFormatJson()) { builder.append(" FORMAT JSON"); + if (encoding != null) { + builder.append(" ENCODING ").append(encoding); + } } return builder; diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonTableFunction.java b/src/main/java/net/sf/jsqlparser/expression/JsonTableFunction.java new file mode 100644 index 000000000..b7f5d0149 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/JsonTableFunction.java @@ -0,0 +1,704 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.expression; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.create.table.ColDataType; + +public class JsonTableFunction extends Function { + public enum JsonTablePlanOperator { + COMMA(", "), INNER(" INNER "), OUTER(" OUTER "), CROSS(" CROSS "), UNION(" UNION "); + + private final String display; + + JsonTablePlanOperator(String display) { + this.display = display; + } + + public String getDisplay() { + return display; + } + } + + public enum JsonTableOnErrorType { + ERROR, EMPTY + } + + public static class JsonTablePassingClause extends ASTNodeAccessImpl implements Serializable { + private Expression valueExpression; + private String parameterName; + + public JsonTablePassingClause() {} + + public JsonTablePassingClause(Expression valueExpression, String parameterName) { + this.valueExpression = valueExpression; + this.parameterName = parameterName; + } + + public Expression getValueExpression() { + return valueExpression; + } + + public JsonTablePassingClause setValueExpression(Expression valueExpression) { + this.valueExpression = valueExpression; + return this; + } + + public String getParameterName() { + return parameterName; + } + + public JsonTablePassingClause setParameterName(String parameterName) { + this.parameterName = parameterName; + return this; + } + + public void collectExpressions(List expressions) { + if (valueExpression != null) { + expressions.add(valueExpression); + } + } + + @Override + public String toString() { + return valueExpression + " AS " + parameterName; + } + } + + public static class JsonTableWrapperClause extends ASTNodeAccessImpl implements Serializable { + private JsonFunction.JsonWrapperType wrapperType; + private JsonFunction.JsonWrapperMode wrapperMode; + private boolean array; + + public JsonFunction.JsonWrapperType getWrapperType() { + return wrapperType; + } + + public JsonTableWrapperClause setWrapperType(JsonFunction.JsonWrapperType wrapperType) { + this.wrapperType = wrapperType; + return this; + } + + public JsonFunction.JsonWrapperMode getWrapperMode() { + return wrapperMode; + } + + public JsonTableWrapperClause setWrapperMode(JsonFunction.JsonWrapperMode wrapperMode) { + this.wrapperMode = wrapperMode; + return this; + } + + public boolean isArray() { + return array; + } + + public JsonTableWrapperClause setArray(boolean array) { + this.array = array; + return this; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(wrapperType); + if (wrapperMode != null) { + builder.append(" ").append(wrapperMode); + } + if (array) { + builder.append(" ARRAY"); + } + builder.append(" WRAPPER"); + return builder.toString(); + } + } + + public static class JsonTableQuotesClause extends ASTNodeAccessImpl implements Serializable { + private JsonFunction.JsonQuotesType quotesType; + private boolean onScalarString; + + public JsonFunction.JsonQuotesType getQuotesType() { + return quotesType; + } + + public JsonTableQuotesClause setQuotesType(JsonFunction.JsonQuotesType quotesType) { + this.quotesType = quotesType; + return this; + } + + public boolean isOnScalarString() { + return onScalarString; + } + + public JsonTableQuotesClause setOnScalarString(boolean onScalarString) { + this.onScalarString = onScalarString; + return this; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(quotesType).append(" QUOTES"); + if (onScalarString) { + builder.append(" ON SCALAR STRING"); + } + return builder.toString(); + } + } + + public static class JsonTableOnErrorClause extends ASTNodeAccessImpl implements Serializable { + private JsonTableOnErrorType type; + + public JsonTableOnErrorType getType() { + return type; + } + + public JsonTableOnErrorClause setType(JsonTableOnErrorType type) { + this.type = type; + return this; + } + + @Override + public String toString() { + return type + " ON ERROR"; + } + } + + public static class JsonTablePlanTerm extends ASTNodeAccessImpl implements Serializable { + private JsonTablePlanExpression nestedPlanExpression; + private String name; + private Expression expression; + + public JsonTablePlanExpression getNestedPlanExpression() { + return nestedPlanExpression; + } + + public JsonTablePlanTerm setNestedPlanExpression( + JsonTablePlanExpression nestedPlanExpression) { + this.nestedPlanExpression = nestedPlanExpression; + return this; + } + + public String getName() { + return name; + } + + public JsonTablePlanTerm setName(String name) { + this.name = name; + return this; + } + + public Expression getExpression() { + return expression; + } + + public JsonTablePlanTerm setExpression(Expression expression) { + this.expression = expression; + return this; + } + + public void collectExpressions(List expressions) { + if (expression != null) { + expressions.add(expression); + } + if (nestedPlanExpression != null) { + nestedPlanExpression.collectExpressions(expressions); + } + } + + @Override + public String toString() { + if (nestedPlanExpression != null) { + return "(" + nestedPlanExpression + ")"; + } + if (name != null) { + return name; + } + return expression != null ? expression.toString() : ""; + } + } + + public static class JsonTablePlanExpression extends ASTNodeAccessImpl implements Serializable { + private final List terms = new ArrayList<>(); + private final List operators = new ArrayList<>(); + + public List getTerms() { + return terms; + } + + public JsonTablePlanExpression addTerm(JsonTablePlanTerm term) { + terms.add(term); + return this; + } + + public List getOperators() { + return operators; + } + + public JsonTablePlanExpression addOperator(JsonTablePlanOperator operator) { + operators.add(operator); + return this; + } + + public void collectExpressions(List expressions) { + for (JsonTablePlanTerm term : terms) { + if (term != null) { + term.collectExpressions(expressions); + } + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (!terms.isEmpty()) { + builder.append(terms.get(0)); + } + for (int i = 0; i < operators.size() && i + 1 < terms.size(); i++) { + builder.append(operators.get(i).getDisplay()).append(terms.get(i + 1)); + } + return builder.toString(); + } + } + + public static class JsonTablePlanClause extends ASTNodeAccessImpl implements Serializable { + private boolean defaultPlan; + private JsonTablePlanExpression planExpression; + + public boolean isDefaultPlan() { + return defaultPlan; + } + + public JsonTablePlanClause setDefaultPlan(boolean defaultPlan) { + this.defaultPlan = defaultPlan; + return this; + } + + public JsonTablePlanExpression getPlanExpression() { + return planExpression; + } + + public JsonTablePlanClause setPlanExpression(JsonTablePlanExpression planExpression) { + this.planExpression = planExpression; + return this; + } + + public void collectExpressions(List expressions) { + if (planExpression != null) { + planExpression.collectExpressions(expressions); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("PLAN"); + if (defaultPlan) { + builder.append(" DEFAULT"); + } + builder.append(" (").append(planExpression).append(")"); + return builder.toString(); + } + } + + public abstract static class JsonTableColumnDefinition extends ASTNodeAccessImpl + implements Serializable { + public abstract void collectExpressions(List expressions); + } + + public static class JsonTableNestedColumnDefinition extends JsonTableColumnDefinition { + private boolean pathKeyword; + private Expression pathExpression; + private String pathName; + private JsonTableColumnsClause columnsClause; + + public boolean isPathKeyword() { + return pathKeyword; + } + + public JsonTableNestedColumnDefinition setPathKeyword(boolean pathKeyword) { + this.pathKeyword = pathKeyword; + return this; + } + + public Expression getPathExpression() { + return pathExpression; + } + + public JsonTableNestedColumnDefinition setPathExpression(Expression pathExpression) { + this.pathExpression = pathExpression; + return this; + } + + public String getPathName() { + return pathName; + } + + public JsonTableNestedColumnDefinition setPathName(String pathName) { + this.pathName = pathName; + return this; + } + + public JsonTableColumnsClause getColumnsClause() { + return columnsClause; + } + + public JsonTableNestedColumnDefinition setColumnsClause( + JsonTableColumnsClause columnsClause) { + this.columnsClause = columnsClause; + return this; + } + + @Override + public void collectExpressions(List expressions) { + if (pathExpression != null) { + expressions.add(pathExpression); + } + if (columnsClause != null) { + columnsClause.collectExpressions(expressions); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("NESTED"); + if (pathKeyword) { + builder.append(" PATH"); + } + builder.append(" ").append(pathExpression); + if (pathName != null) { + builder.append(" AS ").append(pathName); + } + builder.append(" ").append(columnsClause); + return builder.toString(); + } + } + + public static class JsonTableValueColumnDefinition extends JsonTableColumnDefinition { + private String columnName; + private boolean forOrdinality; + private ColDataType dataType; + private boolean formatJson; + private String encoding; + private Expression pathExpression; + private JsonTableWrapperClause wrapperClause; + private JsonTableQuotesClause quotesClause; + private JsonFunction.JsonOnResponseBehavior onEmptyBehavior; + private JsonFunction.JsonOnResponseBehavior onErrorBehavior; + + public String getColumnName() { + return columnName; + } + + public JsonTableValueColumnDefinition setColumnName(String columnName) { + this.columnName = columnName; + return this; + } + + public boolean isForOrdinality() { + return forOrdinality; + } + + public JsonTableValueColumnDefinition setForOrdinality(boolean forOrdinality) { + this.forOrdinality = forOrdinality; + return this; + } + + public ColDataType getDataType() { + return dataType; + } + + public JsonTableValueColumnDefinition setDataType(ColDataType dataType) { + this.dataType = dataType; + return this; + } + + public boolean isFormatJson() { + return formatJson; + } + + public JsonTableValueColumnDefinition setFormatJson(boolean formatJson) { + this.formatJson = formatJson; + return this; + } + + public String getEncoding() { + return encoding; + } + + public JsonTableValueColumnDefinition setEncoding(String encoding) { + this.encoding = encoding; + return this; + } + + public Expression getPathExpression() { + return pathExpression; + } + + public JsonTableValueColumnDefinition setPathExpression(Expression pathExpression) { + this.pathExpression = pathExpression; + return this; + } + + public JsonTableWrapperClause getWrapperClause() { + return wrapperClause; + } + + public JsonTableValueColumnDefinition setWrapperClause( + JsonTableWrapperClause wrapperClause) { + this.wrapperClause = wrapperClause; + return this; + } + + public JsonTableQuotesClause getQuotesClause() { + return quotesClause; + } + + public JsonTableValueColumnDefinition setQuotesClause(JsonTableQuotesClause quotesClause) { + this.quotesClause = quotesClause; + return this; + } + + public JsonFunction.JsonOnResponseBehavior getOnEmptyBehavior() { + return onEmptyBehavior; + } + + public JsonTableValueColumnDefinition setOnEmptyBehavior( + JsonFunction.JsonOnResponseBehavior onEmptyBehavior) { + this.onEmptyBehavior = onEmptyBehavior; + return this; + } + + public JsonFunction.JsonOnResponseBehavior getOnErrorBehavior() { + return onErrorBehavior; + } + + public JsonTableValueColumnDefinition setOnErrorBehavior( + JsonFunction.JsonOnResponseBehavior onErrorBehavior) { + this.onErrorBehavior = onErrorBehavior; + return this; + } + + @Override + public void collectExpressions(List expressions) { + if (pathExpression != null) { + expressions.add(pathExpression); + } + if (onEmptyBehavior != null && onEmptyBehavior.getExpression() != null) { + expressions.add(onEmptyBehavior.getExpression()); + } + if (onErrorBehavior != null && onErrorBehavior.getExpression() != null) { + expressions.add(onErrorBehavior.getExpression()); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(columnName); + if (forOrdinality) { + builder.append(" FOR ORDINALITY"); + return builder.toString(); + } + + builder.append(" ").append(dataType); + if (formatJson) { + builder.append(" FORMAT JSON"); + if (encoding != null) { + builder.append(" ENCODING ").append(encoding); + } + } + if (pathExpression != null) { + builder.append(" PATH ").append(pathExpression); + } + if (wrapperClause != null) { + builder.append(" ").append(wrapperClause); + } + if (quotesClause != null) { + builder.append(" ").append(quotesClause); + } + if (onEmptyBehavior != null) { + builder.append(" ").append(onEmptyBehavior).append(" ON EMPTY"); + } + if (onErrorBehavior != null) { + builder.append(" ").append(onErrorBehavior).append(" ON ERROR"); + } + return builder.toString(); + } + } + + public static class JsonTableColumnsClause extends ASTNodeAccessImpl implements Serializable { + private final List columnDefinitions = new ArrayList<>(); + + public List getColumnDefinitions() { + return columnDefinitions; + } + + public JsonTableColumnsClause addColumnDefinition( + JsonTableColumnDefinition columnDefinition) { + columnDefinitions.add(columnDefinition); + return this; + } + + public void collectExpressions(List expressions) { + for (JsonTableColumnDefinition columnDefinition : columnDefinitions) { + if (columnDefinition != null) { + columnDefinition.collectExpressions(expressions); + } + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("COLUMNS ("); + boolean first = true; + for (JsonTableColumnDefinition columnDefinition : columnDefinitions) { + if (!first) { + builder.append(", "); + } + builder.append(columnDefinition); + first = false; + } + builder.append(")"); + return builder.toString(); + } + } + + private Expression jsonInputExpression; + private Expression jsonPathExpression; + private String pathName; + private final List passingClauses = new ArrayList<>(); + private JsonTableColumnsClause columnsClause; + private JsonTablePlanClause planClause; + private JsonTableOnErrorClause onErrorClause; + + public JsonTableFunction() { + setName("JSON_TABLE"); + } + + public Expression getJsonInputExpression() { + return jsonInputExpression; + } + + public JsonTableFunction setJsonInputExpression(Expression jsonInputExpression) { + this.jsonInputExpression = jsonInputExpression; + return this; + } + + public Expression getJsonPathExpression() { + return jsonPathExpression; + } + + public JsonTableFunction setJsonPathExpression(Expression jsonPathExpression) { + this.jsonPathExpression = jsonPathExpression; + return this; + } + + public String getPathName() { + return pathName; + } + + public JsonTableFunction setPathName(String pathName) { + this.pathName = pathName; + return this; + } + + public List getPassingClauses() { + return passingClauses; + } + + public JsonTableFunction addPassingClause(JsonTablePassingClause passingClause) { + passingClauses.add(Objects.requireNonNull(passingClause, "passingClause")); + return this; + } + + public JsonTableColumnsClause getColumnsClause() { + return columnsClause; + } + + public JsonTableFunction setColumnsClause(JsonTableColumnsClause columnsClause) { + this.columnsClause = columnsClause; + return this; + } + + public JsonTablePlanClause getPlanClause() { + return planClause; + } + + public JsonTableFunction setPlanClause(JsonTablePlanClause planClause) { + this.planClause = planClause; + return this; + } + + public JsonTableOnErrorClause getOnErrorClause() { + return onErrorClause; + } + + public JsonTableFunction setOnErrorClause(JsonTableOnErrorClause onErrorClause) { + this.onErrorClause = onErrorClause; + return this; + } + + public List getAllExpressions() { + List expressions = new ArrayList<>(); + if (jsonInputExpression != null) { + expressions.add(jsonInputExpression); + } + if (jsonPathExpression != null) { + expressions.add(jsonPathExpression); + } + for (JsonTablePassingClause passingClause : passingClauses) { + passingClause.collectExpressions(expressions); + } + if (columnsClause != null) { + columnsClause.collectExpressions(expressions); + } + if (planClause != null) { + planClause.collectExpressions(expressions); + } + return expressions; + } + + @Override + public T accept(ExpressionVisitor expressionVisitor, S context) { + return expressionVisitor.visit(this, context); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("JSON_TABLE("); + builder.append(jsonInputExpression).append(", ").append(jsonPathExpression); + if (pathName != null) { + builder.append(" AS ").append(pathName); + } + if (!passingClauses.isEmpty()) { + builder.append(" PASSING "); + boolean first = true; + for (JsonTablePassingClause passingClause : passingClauses) { + if (!first) { + builder.append(", "); + } + builder.append(passingClause); + first = false; + } + } + builder.append(" ").append(columnsClause); + if (planClause != null) { + builder.append(" ").append(planClause); + } + if (onErrorClause != null) { + builder.append(" ").append(onErrorClause); + } + builder.append(")"); + return builder.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/expression/RawFunction.java b/src/main/java/net/sf/jsqlparser/expression/RawFunction.java new file mode 100644 index 000000000..1c2d5b874 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/RawFunction.java @@ -0,0 +1,41 @@ +/*- + * #%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; + +/** + * Function with a raw argument body preserved as-is for deparsing. + */ +public class RawFunction extends Function { + private String rawArguments; + + public RawFunction() {} + + public RawFunction(String name, String rawArguments) { + setName(name); + this.rawArguments = rawArguments; + } + + public String getRawArguments() { + return rawArguments; + } + + public void setRawArguments(String rawArguments) { + this.rawArguments = rawArguments; + } + + @Override + public String toString() { + String name = getName(); + if (rawArguments == null) { + return name + "()"; + } + return name + "(" + rawArguments + ")"; + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index f61c02e70..e0a5fbc19 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); @@ -1723,6 +1732,38 @@ public Void visit(JsonFunction expression, S context) { for (JsonFunctionExpression expr : expression.getExpressions()) { expr.getExpression().accept(this, context); } + + if (expression.getInputExpression() != null) { + expression.getInputExpression().getExpression().accept(this, context); + } + + if (expression.getJsonPathExpression() != null) { + expression.getJsonPathExpression().accept(this, context); + } + + for (Expression passingExpression : expression.getPassingExpressions()) { + passingExpression.accept(this, context); + } + + if (expression.getOnEmptyBehavior() != null + && expression.getOnEmptyBehavior().getExpression() != null) { + expression.getOnEmptyBehavior().getExpression().accept(this, context); + } + + if (expression.getOnErrorBehavior() != null + && expression.getOnErrorBehavior().getExpression() != null) { + expression.getOnErrorBehavior().getExpression().accept(this, context); + } + return null; + } + + @Override + public Void visit(JsonTableFunction expression, S context) { + for (Expression jsonExpression : expression.getAllExpressions()) { + if (jsonExpression != null) { + jsonExpression.accept(this, 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 c97a28423..1144e30a4 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; @@ -39,6 +40,7 @@ import net.sf.jsqlparser.expression.JsonAggregateFunction; import net.sf.jsqlparser.expression.JsonExpression; import net.sf.jsqlparser.expression.JsonFunction; +import net.sf.jsqlparser.expression.JsonTableFunction; import net.sf.jsqlparser.expression.KeepExpression; import net.sf.jsqlparser.expression.LambdaExpression; import net.sf.jsqlparser.expression.LongValue; @@ -939,6 +941,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); @@ -1629,6 +1638,12 @@ public StringBuilder visit(JsonFunction expression, S context) { return builder; } + @Override + public StringBuilder visit(JsonTableFunction expression, S context) { + builder.append(expression); + return builder; + } + @Override public StringBuilder visit(ConnectByRootOperator connectByRootOperator, S context) { builder.append("CONNECT_BY_ROOT "); 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..631c8e491 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; @@ -38,6 +39,7 @@ import net.sf.jsqlparser.expression.JsonAggregateFunction; import net.sf.jsqlparser.expression.JsonExpression; import net.sf.jsqlparser.expression.JsonFunction; +import net.sf.jsqlparser.expression.JsonTableFunction; import net.sf.jsqlparser.expression.KeepExpression; import net.sf.jsqlparser.expression.LambdaExpression; import net.sf.jsqlparser.expression.LongValue; @@ -547,6 +549,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 @@ -1034,6 +1043,14 @@ public Void visit(JsonFunction expression, S context) { return null; } + @Override + public Void visit(JsonTableFunction expression, S context) { + for (Expression jsonExpression : expression.getAllExpressions()) { + validateOptionalExpression(jsonExpression, this); + } + return null; + } + @Override public Void visit(ConnectByRootOperator connectByRootOperator, S context) { connectByRootOperator.getColumn().accept(this, context); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index d57b29f16..8501e9cd9 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) @@ -4860,6 +4949,19 @@ FromItem FromItem() #FromItem: ( LOOKAHEAD(3, { !getAsBoolean(Feature.allowUnparenthesizedSubSelects) }) fromItem = Values() | + LOOKAHEAD({ + getToken(1).kind == S_IDENTIFIER + && getToken(1).image.equalsIgnoreCase("JSON_TABLE") + && getToken(2).kind == OPENING_BRACKET + }) fromItem=TableFunction() + | + LOOKAHEAD({ + getToken(1).kind == K_LATERAL + && getToken(2).kind == S_IDENTIFIER + && getToken(2).image.equalsIgnoreCase("JSON_TABLE") + && getToken(3).kind == OPENING_BRACKET + }) fromItem=TableFunction() + | LOOKAHEAD(16) fromItem=TableFunction() | LOOKAHEAD(3) fromItem=Table() @@ -6571,7 +6673,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() @@ -7014,6 +7116,7 @@ JsonKeyValuePair JsonKeyValuePair(boolean isFirstEntry) : { boolean usingKeyKeyword = false; boolean usingFormatJason = false; + String encoding = null; boolean isWildcard = false; Object key = null; @@ -7056,11 +7159,15 @@ JsonKeyValuePair JsonKeyValuePair(boolean isFirstEntry) : { expression = Expression() ] - // Optional: FORMAT JSON - Is not allowed with * or t1.* - [ LOOKAHEAD(1, { !isWildcard } ) { usingFormatJason = true; } ] + // Optional: FORMAT JSON [ ENCODING ... ] - Is not allowed with * or t1.* + [ + LOOKAHEAD(1, { !isWildcard } ) { usingFormatJason = true; } + [ encoding = JsonEncoding() ] + ] { final JsonKeyValuePair keyValuePair = new JsonKeyValuePair( key, expression, usingKeyKeyword, kvSeparator ); keyValuePair.setUsingFormatJson( usingFormatJason ); + keyValuePair.setEncoding(encoding); return keyValuePair; } } @@ -7069,6 +7176,8 @@ JsonFunction JsonObjectBody() : { JsonFunction result = new JsonFunction(JsonFunctionType.OBJECT); JsonKeyValuePair keyValuePair; + ColDataType dataType; + String encoding; } { ( "(" @@ -7092,6 +7201,13 @@ JsonFunction JsonObjectBody() : { | ( { result.setUniqueKeysType( JsonAggregateUniqueKeysType.WITHOUT ); } ) ] + [ + dataType = ColDataType() { result.setReturningType(dataType); } + [ + { result.setReturningFormatJson(true); } + [ encoding = JsonEncoding() { result.setReturningEncoding(encoding); } ] + ] + ] ")" ) { return result; @@ -7103,6 +7219,8 @@ JsonFunction JsonArrayBody() : { Expression expression = null; JsonFunctionExpression functionExpression; + ColDataType dataType; + String encoding; } { ( "(" @@ -7113,23 +7231,483 @@ JsonFunction JsonArrayBody() : { | expression=Expression() { functionExpression = new JsonFunctionExpression( expression ); result.add( functionExpression ); } - [ LOOKAHEAD(2) { functionExpression.setUsingFormatJson( true ); } ] + [ + LOOKAHEAD(2) { functionExpression.setUsingFormatJson( true ); } + [ encoding = JsonEncoding() { functionExpression.setEncoding(encoding); } ] + ] ( "," expression=Expression() { functionExpression = new JsonFunctionExpression( expression ); result.add( functionExpression ); } - [ LOOKAHEAD(2) { functionExpression.setUsingFormatJson( true ); } ] + [ + LOOKAHEAD(2) { functionExpression.setUsingFormatJson( true ); } + [ encoding = JsonEncoding() { functionExpression.setEncoding(encoding); } ] + ] )* )* [ { result.setOnNullType( JsonAggregateOnNullType.ABSENT ); } ] + [ + dataType = ColDataType() { result.setReturningType(dataType); } + [ + { result.setReturningFormatJson(true); } + [ encoding = JsonEncoding() { result.setReturningEncoding(encoding); } ] + ] + ] ")" ) { return result; } } +void JsonKeyword(String expectedKeyword) : { + Token token; +} +{ + token = + { + if (!token.image.equalsIgnoreCase(expectedKeyword)) { + throw new ParseException( + "Expected keyword " + expectedKeyword + " but found " + token.image); + } + } +} + +String JsonEncoding() : { + Token token; +} +{ + token = + { + if (token.image.equalsIgnoreCase("UTF8")) { + return "UTF8"; + } else if (token.image.equalsIgnoreCase("UTF16")) { + return "UTF16"; + } else if (token.image.equalsIgnoreCase("UTF32")) { + return "UTF32"; + } + throw new ParseException( + "Expected ENCODING value UTF8, UTF16 or UTF32 but found " + token.image); + } +} + +JsonFunctionExpression JsonValueOrQueryInputExpression() : { + Expression expression; + JsonFunctionExpression functionExpression; + String encoding; +} +{ + expression = Expression() { functionExpression = new JsonFunctionExpression(expression); } + [ + { functionExpression.setUsingFormatJson(true); } + [ encoding = JsonEncoding() { functionExpression.setEncoding(encoding); } ] + ] + { + return functionExpression; + } +} + +JsonFunction.JsonOnResponseBehavior JsonValueOnResponseBehavior() : { + JsonFunction.JsonOnResponseBehavior behavior; + Expression expression; +} +{ + ( + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.ERROR); + } + | + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.NULL); + } + | + expression = Expression() + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.DEFAULT, expression); + } + ) + { + return behavior; + } +} + +JsonFunction.JsonOnResponseBehavior JsonQueryOnResponseBehavior() : { + JsonFunction.JsonOnResponseBehavior behavior = null; + Token token; +} +{ + ( + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.ERROR); + } + | + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.NULL); + } + | + token = + { + if (!token.image.equalsIgnoreCase("EMPTY")) { + throw new ParseException( + "Expected EMPTY, ERROR or NULL but found " + token.image); + } + } + ( + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.EMPTY_ARRAY); + } + | + JsonKeyword("OBJECT") + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.EMPTY_OBJECT); + } + ) + ) + { + if (behavior != null) { + return behavior; + } + } +} + +JsonFunction.JsonOnResponseBehavior JsonExistsOnResponseBehavior() : { + JsonFunction.JsonOnResponseBehavior behavior = null; +} +{ + ( + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.TRUE); + } + | + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.FALSE); + } + | + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.UNKNOWN); + } + | + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.ERROR); + } + ) + { + return behavior; + } +} + +JsonFunction JsonExistsBody() : { + JsonFunction result = new JsonFunction(JsonFunctionType.EXISTS); + JsonFunctionExpression inputExpression; + Expression expression; + JsonFunction.JsonOnResponseBehavior behavior; +} +{ + "(" + inputExpression = JsonValueOrQueryInputExpression() { result.setInputExpression(inputExpression); } + "," + expression = Expression() { result.setJsonPathExpression(expression); } + + [ + LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("PASSING") }) + JsonKeyword("PASSING") + expression = Expression() { result.addPassingExpression(expression); } + ( "," expression = Expression() { result.addPassingExpression(expression); } )* + ] + + [ + LOOKAHEAD( JsonExistsOnResponseBehavior() ) + behavior = JsonExistsOnResponseBehavior() + + { result.setOnErrorBehavior(behavior); } + ] + ")" + { + return result; + } +} + +JsonFunction JsonValueBody() : { + JsonFunction result = new JsonFunction(JsonFunctionType.VALUE); + JsonFunctionExpression inputExpression; + Expression expression; + ColDataType dataType; + JsonFunction.JsonOnResponseBehavior behavior; +} +{ + "(" + inputExpression = JsonValueOrQueryInputExpression() { result.setInputExpression(inputExpression); } + "," + expression = Expression() { result.setJsonPathExpression(expression); } + + [ + LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("PASSING") }) + JsonKeyword("PASSING") + expression = Expression() { result.addPassingExpression(expression); } + ( "," expression = Expression() { result.addPassingExpression(expression); } )* + ] + + [ dataType = ColDataType() { result.setReturningType(dataType); } ] + + [ + LOOKAHEAD( JsonValueOnResponseBehavior() ) + behavior = JsonValueOnResponseBehavior() + JsonKeyword("EMPTY") + { result.setOnEmptyBehavior(behavior); } + ] + + [ + LOOKAHEAD( JsonValueOnResponseBehavior() ) + behavior = JsonValueOnResponseBehavior() + + { result.setOnErrorBehavior(behavior); } + ] + ")" + { + return result; + } +} + +JsonFunction JsonQueryBody() : { + JsonFunction result = new JsonFunction(JsonFunctionType.QUERY); + JsonFunctionExpression inputExpression; + Expression expression; + ColDataType dataType; + JsonFunction.JsonOnResponseBehavior behavior; + Token token; + String encoding; + ColDataType additionalReturningType; + boolean additionalReturningFormatJson; + String additionalReturningEncoding; + JsonFunction.JsonWrapperType additionalWrapperType; + JsonFunction.JsonWrapperMode additionalWrapperMode; + boolean additionalWrapperArray; + JsonFunction.JsonQuotesType additionalQuotesType; + boolean additionalQuotesOnScalarString; + JsonFunction.JsonOnResponseBehavior additionalOnEmptyBehavior; + JsonFunction.JsonOnResponseBehavior additionalOnErrorBehavior; + StringBuilder additionalBuilder; +} +{ + "(" + inputExpression = JsonValueOrQueryInputExpression() { result.setInputExpression(inputExpression); } + "," + expression = Expression() { result.setJsonPathExpression(expression); } + + [ + LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("PASSING") }) + JsonKeyword("PASSING") + expression = Expression() { result.addPassingExpression(expression); } + ( "," expression = Expression() { result.addPassingExpression(expression); } )* + ] + + [ + dataType = ColDataType() { result.setReturningType(dataType); } + [ + { result.setReturningFormatJson(true); } + [ encoding = JsonEncoding() { result.setReturningEncoding(encoding); } ] + ] + ] + + [ + ( + { result.setWrapperType(JsonFunction.JsonWrapperType.WITHOUT); } + [ { result.setWrapperArray(true); } ] + JsonKeyword("WRAPPER") + | + { result.setWrapperType(JsonFunction.JsonWrapperType.WITH); } + [ + LOOKAHEAD({ + getToken(1).kind == S_IDENTIFIER + && (getToken(1).image.equalsIgnoreCase("CONDITIONAL") + || getToken(1).image.equalsIgnoreCase("UNCONDITIONAL")) + }) + token = + { + if (token.image.equalsIgnoreCase("CONDITIONAL")) { + result.setWrapperMode(JsonFunction.JsonWrapperMode.CONDITIONAL); + } else { + result.setWrapperMode(JsonFunction.JsonWrapperMode.UNCONDITIONAL); + } + } + ] + [ { result.setWrapperArray(true); } ] + JsonKeyword("WRAPPER") + ) + ] + + [ + LOOKAHEAD({ + getToken(1).kind == K_KEEP + || (getToken(1).kind == S_IDENTIFIER + && getToken(1).image.equalsIgnoreCase("OMIT")) + }) + ( + { result.setQuotesType(JsonFunction.JsonQuotesType.KEEP); } + | + JsonKeyword("OMIT") { result.setQuotesType(JsonFunction.JsonQuotesType.OMIT); } + ) + JsonKeyword("QUOTES") + [ + JsonKeyword("SCALAR") + { result.setQuotesOnScalarString(true); } + ] + ] + + [ + LOOKAHEAD( JsonQueryOnResponseBehavior() ) + behavior = JsonQueryOnResponseBehavior() + JsonKeyword("EMPTY") + { result.setOnEmptyBehavior(behavior); } + ] + + [ + LOOKAHEAD( JsonQueryOnResponseBehavior() ) + behavior = JsonQueryOnResponseBehavior() + + { result.setOnErrorBehavior(behavior); } + ] + + ( + "," + { + additionalReturningType = null; + additionalReturningFormatJson = false; + additionalReturningEncoding = null; + additionalWrapperType = null; + additionalWrapperMode = null; + additionalWrapperArray = false; + additionalQuotesType = null; + additionalQuotesOnScalarString = false; + additionalOnEmptyBehavior = null; + additionalOnErrorBehavior = null; + } + expression = Expression() + [ + additionalReturningType = ColDataType() + [ + { additionalReturningFormatJson = true; } + [ additionalReturningEncoding = JsonEncoding() ] + ] + ] + [ + ( + + { additionalWrapperType = JsonFunction.JsonWrapperType.WITHOUT; } + [ { additionalWrapperArray = true; } ] + JsonKeyword("WRAPPER") + | + + { additionalWrapperType = JsonFunction.JsonWrapperType.WITH; } + [ + LOOKAHEAD({ + getToken(1).kind == S_IDENTIFIER + && (getToken(1).image.equalsIgnoreCase("CONDITIONAL") + || getToken(1).image.equalsIgnoreCase("UNCONDITIONAL")) + }) + token = + { + if (token.image.equalsIgnoreCase("CONDITIONAL")) { + additionalWrapperMode = JsonFunction.JsonWrapperMode.CONDITIONAL; + } else { + additionalWrapperMode = JsonFunction.JsonWrapperMode.UNCONDITIONAL; + } + } + ] + [ { additionalWrapperArray = true; } ] + JsonKeyword("WRAPPER") + ) + ] + [ + LOOKAHEAD({ + getToken(1).kind == K_KEEP + || (getToken(1).kind == S_IDENTIFIER + && getToken(1).image.equalsIgnoreCase("OMIT")) + }) + ( + { additionalQuotesType = JsonFunction.JsonQuotesType.KEEP; } + | + JsonKeyword("OMIT") { additionalQuotesType = JsonFunction.JsonQuotesType.OMIT; } + ) + JsonKeyword("QUOTES") + [ + JsonKeyword("SCALAR") { additionalQuotesOnScalarString = true; } + ] + ] + [ + LOOKAHEAD( JsonQueryOnResponseBehavior() ) + additionalOnEmptyBehavior = JsonQueryOnResponseBehavior() + JsonKeyword("EMPTY") + ] + [ + LOOKAHEAD( JsonQueryOnResponseBehavior() ) + additionalOnErrorBehavior = JsonQueryOnResponseBehavior() + + ] + { + additionalBuilder = new StringBuilder(); + additionalBuilder.append(expression); + if (additionalReturningType != null) { + additionalBuilder.append(" RETURNING ").append(additionalReturningType); + if (additionalReturningFormatJson) { + additionalBuilder.append(" FORMAT JSON"); + if (additionalReturningEncoding != null) { + additionalBuilder.append(" ENCODING ").append(additionalReturningEncoding); + } + } + } + if (additionalWrapperType != null) { + additionalBuilder.append(" ").append(additionalWrapperType); + if (additionalWrapperMode != null) { + additionalBuilder.append(" ").append(additionalWrapperMode); + } + if (additionalWrapperArray) { + additionalBuilder.append(" ARRAY"); + } + additionalBuilder.append(" WRAPPER"); + } + if (additionalQuotesType != null) { + additionalBuilder.append(" ").append(additionalQuotesType).append(" QUOTES"); + if (additionalQuotesOnScalarString) { + additionalBuilder.append(" ON SCALAR STRING"); + } + } + if (additionalOnEmptyBehavior != null) { + additionalBuilder.append(" ").append(additionalOnEmptyBehavior).append(" ON EMPTY"); + } + if (additionalOnErrorBehavior != null) { + additionalBuilder.append(" ").append(additionalOnErrorBehavior).append(" ON ERROR"); + } + result.addAdditionalQueryPathArgument(additionalBuilder.toString()); + } + )* + ")" + { + return result; + } +} + JsonFunction JsonFunction() : { JsonFunction result; } @@ -7138,6 +7716,33 @@ JsonFunction JsonFunction() : { ( result = JsonObjectBody() ) | ( result = JsonArrayBody() ) + | + ( + LOOKAHEAD({ + getToken(1).kind == S_IDENTIFIER + && getToken(1).image.equalsIgnoreCase("JSON_VALUE") + }) + JsonKeyword("JSON_VALUE") + result = JsonValueBody() + ) + | + ( + LOOKAHEAD({ + getToken(1).kind == S_IDENTIFIER + && getToken(1).image.equalsIgnoreCase("JSON_QUERY") + }) + JsonKeyword("JSON_QUERY") + result = JsonQueryBody() + ) + | + ( + LOOKAHEAD({ + getToken(1).kind == S_IDENTIFIER + && getToken(1).image.equalsIgnoreCase("JSON_EXISTS") + }) + JsonKeyword("JSON_EXISTS") + result = JsonExistsBody() + ) ) { return result; @@ -7810,6 +8415,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 +8503,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; @@ -7896,16 +8565,386 @@ MySQLGroupConcat MySQLGroupConcat():{ } } +JsonTableFunction.JsonTablePassingClause JsonTablePassingClause() : { + Expression valueExpression; + String parameterName; +} +{ + valueExpression = Expression() + + parameterName = RelObjectName() + { + return new JsonTableFunction.JsonTablePassingClause(valueExpression, parameterName); + } +} + +JsonFunction.JsonOnResponseBehavior JsonTableOnEmptyBehavior() : { + JsonFunction.JsonOnResponseBehavior behavior = null; + Expression expression; + Token token; +} +{ + ( + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.ERROR); + } + | + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.NULL); + } + | + expression = Expression() + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.DEFAULT, expression); + } + | + token = + { + if (!token.image.equalsIgnoreCase("EMPTY")) { + throw new ParseException( + "Expected EMPTY, ERROR, NULL or DEFAULT but found " + token.image); + } + } + ( + LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("OBJECT") }) + JsonKeyword("OBJECT") + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.EMPTY_OBJECT); + } + | + [ ] + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.EMPTY_ARRAY); + } + ) + ) + { + if (behavior != null) { + return behavior; + } + } +} + +JsonTableFunction.JsonTableWrapperClause JsonTableWrapperClause() : { + JsonTableFunction.JsonTableWrapperClause wrapperClause = + new JsonTableFunction.JsonTableWrapperClause(); + Token token; +} +{ + ( + { + wrapperClause.setWrapperType(JsonFunction.JsonWrapperType.WITHOUT); + } + | + { + wrapperClause.setWrapperType(JsonFunction.JsonWrapperType.WITH); + } + [ + LOOKAHEAD({ + getToken(1).kind == S_IDENTIFIER + && (getToken(1).image.equalsIgnoreCase("CONDITIONAL") + || getToken(1).image.equalsIgnoreCase("UNCONDITIONAL")) + }) + token = + { + if (token.image.equalsIgnoreCase("CONDITIONAL")) { + wrapperClause.setWrapperMode(JsonFunction.JsonWrapperMode.CONDITIONAL); + } else { + wrapperClause.setWrapperMode(JsonFunction.JsonWrapperMode.UNCONDITIONAL); + } + } + ] + ) + [ { wrapperClause.setArray(true); } ] + JsonKeyword("WRAPPER") + { + return wrapperClause; + } +} + +JsonTableFunction.JsonTableQuotesClause JsonTableQuotesClause() : { + JsonTableFunction.JsonTableQuotesClause quotesClause = + new JsonTableFunction.JsonTableQuotesClause(); +} +{ + ( + { quotesClause.setQuotesType(JsonFunction.JsonQuotesType.KEEP); } + | + JsonKeyword("OMIT") { quotesClause.setQuotesType(JsonFunction.JsonQuotesType.OMIT); } + ) + JsonKeyword("QUOTES") + [ + JsonKeyword("SCALAR") { quotesClause.setOnScalarString(true); } + ] + { + return quotesClause; + } +} + +JsonTableFunction.JsonTableColumnDefinition JsonTableColumnDefinition() : { + JsonTableFunction.JsonTableColumnDefinition columnDefinition = null; + JsonTableFunction.JsonTableNestedColumnDefinition nestedColumnDefinition; + JsonTableFunction.JsonTableValueColumnDefinition valueColumnDefinition; + String columnName; + ColDataType dataType; + Expression expression; + String pathName = null; + JsonTableFunction.JsonTableColumnsClause columnsClause; + JsonFunction.JsonOnResponseBehavior behavior; + JsonTableFunction.JsonTableWrapperClause wrapperClause; + JsonTableFunction.JsonTableQuotesClause quotesClause; + String encoding; +} +{ + ( + LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("NESTED") }) + JsonKeyword("NESTED") + { nestedColumnDefinition = new JsonTableFunction.JsonTableNestedColumnDefinition(); } + [ { nestedColumnDefinition.setPathKeyword(true); } ] + expression = Expression() { nestedColumnDefinition.setPathExpression(expression); } + [ pathName = RelObjectName() { nestedColumnDefinition.setPathName(pathName); } ] + columnsClause = JsonTableColumnsClause() { + nestedColumnDefinition.setColumnsClause(columnsClause); + columnDefinition = nestedColumnDefinition; + } + | + columnName = RelObjectName() { + valueColumnDefinition = new JsonTableFunction.JsonTableValueColumnDefinition(); + valueColumnDefinition.setColumnName(columnName); + columnDefinition = valueColumnDefinition; + } + ( + JsonKeyword("ORDINALITY") + { valueColumnDefinition.setForOrdinality(true); } + | + dataType = ColDataType() { valueColumnDefinition.setDataType(dataType); } + [ + { valueColumnDefinition.setFormatJson(true); } + [ encoding = JsonEncoding() { valueColumnDefinition.setEncoding(encoding); } ] + ] + [ expression = Expression() { valueColumnDefinition.setPathExpression(expression); } ] + [ wrapperClause = JsonTableWrapperClause() { valueColumnDefinition.setWrapperClause(wrapperClause); } ] + [ + LOOKAHEAD({ + getToken(1).kind == K_KEEP + || (getToken(1).kind == S_IDENTIFIER + && getToken(1).image.equalsIgnoreCase("OMIT")) + }) + quotesClause = JsonTableQuotesClause() { valueColumnDefinition.setQuotesClause(quotesClause); } + ] + [ + LOOKAHEAD( JsonTableOnEmptyBehavior() ) + behavior = JsonTableOnEmptyBehavior() + JsonKeyword("EMPTY") + { valueColumnDefinition.setOnEmptyBehavior(behavior); } + ] + [ + LOOKAHEAD( JsonValueOnResponseBehavior() ) + behavior = JsonValueOnResponseBehavior() + + { valueColumnDefinition.setOnErrorBehavior(behavior); } + ] + ) + ) + { + return columnDefinition; + } +} + +JsonTableFunction.JsonTableColumnsClause JsonTableColumnsClause() : { + JsonTableFunction.JsonTableColumnsClause columnsClause = + new JsonTableFunction.JsonTableColumnsClause(); + JsonTableFunction.JsonTableColumnDefinition columnDefinition; +} +{ + "(" + [ + columnDefinition = JsonTableColumnDefinition() { + columnsClause.addColumnDefinition(columnDefinition); + } + ( + "," + columnDefinition = JsonTableColumnDefinition() { + columnsClause.addColumnDefinition(columnDefinition); + } + )* + ] + ")" + { + return columnsClause; + } +} + +JsonTableFunction.JsonTablePlanTerm JsonTablePlanTerm() : { + JsonTableFunction.JsonTablePlanTerm term = null; + String value; + Expression expression; + JsonTableFunction.JsonTablePlanExpression nestedPlanExpression; +} +{ + ( + LOOKAHEAD(2) + "(" nestedPlanExpression = JsonTablePlanExpression() ")" { + term = new JsonTableFunction.JsonTablePlanTerm(); + term.setNestedPlanExpression(nestedPlanExpression); + } + | + value = RelObjectName() { + term = new JsonTableFunction.JsonTablePlanTerm(); + term.setName(value); + } + | + expression = Expression() { + term = new JsonTableFunction.JsonTablePlanTerm(); + term.setExpression(expression); + } + ) + { + return term; + } +} + +JsonTableFunction.JsonTablePlanExpression JsonTablePlanExpression() : { + JsonTableFunction.JsonTablePlanExpression planExpression = + new JsonTableFunction.JsonTablePlanExpression(); + JsonTableFunction.JsonTablePlanTerm term; + Token operator = null; +} +{ + term = JsonTablePlanTerm() { planExpression.addTerm(term); } + ( + ( + operator = { + planExpression.addOperator(JsonTableFunction.JsonTablePlanOperator.COMMA); + } + | + operator = { + planExpression.addOperator(JsonTableFunction.JsonTablePlanOperator.INNER); + } + | + operator = { + planExpression.addOperator(JsonTableFunction.JsonTablePlanOperator.OUTER); + } + | + operator = { + planExpression.addOperator(JsonTableFunction.JsonTablePlanOperator.CROSS); + } + | + operator = { + planExpression.addOperator(JsonTableFunction.JsonTablePlanOperator.UNION); + } + ) + term = JsonTablePlanTerm() { planExpression.addTerm(term); } + )* + { + return planExpression; + } +} + +JsonTableFunction.JsonTablePlanClause JsonTablePlanClause() : { + JsonTableFunction.JsonTablePlanClause planClause = + new JsonTableFunction.JsonTablePlanClause(); + JsonTableFunction.JsonTablePlanExpression planExpression; +} +{ + + [ { planClause.setDefaultPlan(true); } ] + "(" planExpression = JsonTablePlanExpression() ")" { planClause.setPlanExpression(planExpression); } + { + return planClause; + } +} + +JsonTableFunction.JsonTableOnErrorClause JsonTableOnErrorClause() : { + JsonTableFunction.JsonTableOnErrorClause onErrorClause = + new JsonTableFunction.JsonTableOnErrorClause(); + Token token; +} +{ + ( + { onErrorClause.setType(JsonTableFunction.JsonTableOnErrorType.ERROR); } + | + token = + { + if (!token.image.equalsIgnoreCase("EMPTY")) { + throw new ParseException( + "Expected EMPTY or ERROR but found " + token.image); + } + onErrorClause.setType(JsonTableFunction.JsonTableOnErrorType.EMPTY); + } + ) + + { + if (onErrorClause.getType() != null) { + return onErrorClause; + } + } +} + +JsonTableFunction JsonTableBody() : { + JsonTableFunction function = new JsonTableFunction(); + Expression jsonInput; + Expression jsonPath; + JsonTableFunction.JsonTablePassingClause passingClause; + String pathName = null; + JsonTableFunction.JsonTableColumnsClause columnsClause; + JsonTableFunction.JsonTablePlanClause planClause = null; + JsonTableFunction.JsonTableOnErrorClause onErrorClause = null; +} +{ + "(" + jsonInput = Expression() { + function.setJsonInputExpression(jsonInput); + } + "," + jsonPath = Expression() { + function.setJsonPathExpression(jsonPath); + function.setParameters(new ExpressionList(jsonInput, jsonPath)); + } + [ pathName = RelObjectName() { function.setPathName(pathName); } ] + [ + LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("PASSING") }) + JsonKeyword("PASSING") + passingClause = JsonTablePassingClause() { function.addPassingClause(passingClause); } + ( + "," + passingClause = JsonTablePassingClause() { function.addPassingClause(passingClause); } + )* + ] + columnsClause = JsonTableColumnsClause() { function.setColumnsClause(columnsClause); } + [ planClause = JsonTablePlanClause() { function.setPlanClause(planClause); } ] + [ onErrorClause = JsonTableOnErrorClause() { function.setOnErrorClause(onErrorClause); } ] + ")" + { + return function; + } +} + TableFunction TableFunction(): { Token prefix = null; Function function; - TableFunction functionItem; Token withClause = null; } { [ prefix = ] - function=Function() + ( + LOOKAHEAD({ + getToken(1).kind == S_IDENTIFIER + && getToken(1).image.equalsIgnoreCase("JSON_TABLE") + }) + JsonKeyword("JSON_TABLE") + function = JsonTableBody() + | + function=Function() + ) [ LOOKAHEAD(2) ( withClause = | withClause = ) ] { return prefix!=null @@ -10528,6 +11567,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/java/net/sf/jsqlparser/expression/JsonFunctionTest.java b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java index 5475f8ec7..03a6e486b 100644 --- a/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java @@ -15,6 +15,9 @@ import net.sf.jsqlparser.parser.feature.FeatureConfiguration; import net.sf.jsqlparser.statement.select.AllColumns; import net.sf.jsqlparser.statement.select.AllTableColumns; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.statement.select.TableFunction; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -286,6 +289,183 @@ public void testArrayWithNullExpressions() throws JSQLParserException { TestUtils.assertExpressionCanBeParsedAndDeparsed("json_array()", true); } + @Test + public void testJsonValue() throws JSQLParserException { + String expressionStr = + "JSON_VALUE(payload FORMAT JSON ENCODING UTF8, '$.customer.id' PASSING customer_id RETURNING VARCHAR(32) DEFAULT 'missing' ON EMPTY NULL ON ERROR)"; + JsonFunction jsonFunction = (JsonFunction) CCJSqlParserUtil.parseExpression(expressionStr); + + assertEquals(JsonFunctionType.VALUE, jsonFunction.getType()); + assertNotNull(jsonFunction.getInputExpression()); + assertEquals("UTF8", jsonFunction.getInputExpression().getEncoding()); + assertEquals(1, jsonFunction.getPassingExpressions().size()); + assertNotNull(jsonFunction.getOnEmptyBehavior()); + assertEquals(JsonFunction.JsonOnResponseBehaviorType.DEFAULT, + jsonFunction.getOnEmptyBehavior().getType()); + assertNotNull(jsonFunction.getOnErrorBehavior()); + assertEquals(JsonFunction.JsonOnResponseBehaviorType.NULL, + jsonFunction.getOnErrorBehavior().getType()); + + TestUtils.assertExpressionCanBeParsedAndDeparsed(expressionStr, true); + } + + @Test + public void testJsonQuery() throws JSQLParserException { + String expressionStr = + "JSON_QUERY(payload FORMAT JSON ENCODING UTF16, '$.items[*]' PASSING item_filter RETURNING VARCHAR(200) FORMAT JSON ENCODING UTF32 WITH CONDITIONAL ARRAY WRAPPER OMIT QUOTES ON SCALAR STRING EMPTY ARRAY ON EMPTY ERROR ON ERROR)"; + JsonFunction jsonFunction = (JsonFunction) CCJSqlParserUtil.parseExpression(expressionStr); + + assertEquals(JsonFunctionType.QUERY, jsonFunction.getType()); + assertNotNull(jsonFunction.getInputExpression()); + assertEquals("UTF16", jsonFunction.getInputExpression().getEncoding()); + assertEquals("UTF32", jsonFunction.getReturningEncoding()); + assertEquals(JsonFunction.JsonWrapperType.WITH, jsonFunction.getWrapperType()); + assertEquals(JsonFunction.JsonWrapperMode.CONDITIONAL, jsonFunction.getWrapperMode()); + assertTrue(jsonFunction.isWrapperArray()); + assertEquals(JsonFunction.JsonQuotesType.OMIT, jsonFunction.getQuotesType()); + assertTrue(jsonFunction.isQuotesOnScalarString()); + assertNotNull(jsonFunction.getOnEmptyBehavior()); + assertEquals(JsonFunction.JsonOnResponseBehaviorType.EMPTY_ARRAY, + jsonFunction.getOnEmptyBehavior().getType()); + assertNotNull(jsonFunction.getOnErrorBehavior()); + assertEquals(JsonFunction.JsonOnResponseBehaviorType.ERROR, + jsonFunction.getOnErrorBehavior().getType()); + + TestUtils.assertExpressionCanBeParsedAndDeparsed(expressionStr, true); + TestUtils.assertExpressionCanBeParsedAndDeparsed( + "JSON_QUERY(payload, '$' WITHOUT WRAPPER KEEP QUOTES EMPTY OBJECT ON ERROR)", true); + } + + @Test + public void testJsonQueryLegacyAdditionalPathArguments() throws JSQLParserException { + String sql = + "select json_query('{\"customer\" : 100, \"region\" : \"AFRICA\"}', 'strict $.keyvalue()' WITH ARRAY WRAPPER, '$.region') from tbl"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true); + + TestUtils.assertSqlCanBeParsedAndDeparsed( + "select json_query('{\"a\":1}', '$' ERROR ON ERROR, '$.x' RETURNING VARCHAR(10), '$.z' WITH ARRAY WRAPPER) from tbl", + true); + } + + @Test + public void testJsonExists() throws JSQLParserException { + String expressionStr = + "JSON_EXISTS(payload FORMAT JSON ENCODING UTF8, '$.children[2]' PASSING child_idx UNKNOWN ON ERROR)"; + JsonFunction jsonFunction = (JsonFunction) CCJSqlParserUtil.parseExpression(expressionStr); + + assertEquals(JsonFunctionType.EXISTS, jsonFunction.getType()); + assertNotNull(jsonFunction.getInputExpression()); + assertEquals("UTF8", jsonFunction.getInputExpression().getEncoding()); + assertNotNull(jsonFunction.getOnErrorBehavior()); + assertEquals(JsonFunction.JsonOnResponseBehaviorType.UNKNOWN, + jsonFunction.getOnErrorBehavior().getType()); + + TestUtils.assertExpressionCanBeParsedAndDeparsed(expressionStr, true); + } + + @Test + public void testJsonArrayAndObjectReturning() throws JSQLParserException { + TestUtils.assertExpressionCanBeParsedAndDeparsed( + "JSON_ARRAY(true, 1 RETURNING VARBINARY FORMAT JSON ENCODING UTF16)", true); + TestUtils.assertExpressionCanBeParsedAndDeparsed( + "JSON_OBJECT('x' : 1 RETURNING VARBINARY FORMAT JSON ENCODING UTF32)", true); + TestUtils.assertExpressionCanBeParsedAndDeparsed( + "JSON_OBJECT('x' : X'5B0035005D00' FORMAT JSON ENCODING UTF16)", true); + } + + @Test + public void testJsonTableAstParity() throws JSQLParserException { + String sqlStr = + "SELECT * FROM JSON_TABLE(payload, 'lax $' AS \"root_path\" " + + "PASSING filter_expr AS filter " + + "COLUMNS (" + + "a VARCHAR(10) FORMAT JSON ENCODING UTF8 PATH 'lax $.a' " + + "WITH CONDITIONAL ARRAY WRAPPER KEEP QUOTES ON SCALAR STRING " + + "DEFAULT 'missing' ON EMPTY NULL ON ERROR, " + + "NESTED PATH 'lax $[*]' AS \"nested_path\" " + + "COLUMNS (b INTEGER PATH 'lax $.b')" + + ") " + + "PLAN DEFAULT (\"root_path\" OUTER \"nested_path\") EMPTY ON ERROR)"; + + Select select = (Select) CCJSqlParserUtil.parse(sqlStr, + parser -> parser.withAllowComplexParsing(false)); + PlainSelect plainSelect = select.getPlainSelect(); + assertNotNull(plainSelect); + assertInstanceOf(TableFunction.class, plainSelect.getFromItem()); + + TableFunction tableFunction = (TableFunction) plainSelect.getFromItem(); + assertInstanceOf(JsonTableFunction.class, tableFunction.getFunction()); + JsonTableFunction jsonTableFunction = (JsonTableFunction) tableFunction.getFunction(); + + assertEquals("payload", jsonTableFunction.getJsonInputExpression().toString()); + assertEquals("'lax $'", jsonTableFunction.getJsonPathExpression().toString()); + assertEquals("\"root_path\"", jsonTableFunction.getPathName()); + assertEquals(1, jsonTableFunction.getPassingClauses().size()); + assertEquals("filter_expr", + jsonTableFunction.getPassingClauses().get(0).getValueExpression().toString()); + assertEquals("filter", jsonTableFunction.getPassingClauses().get(0).getParameterName()); + + JsonTableFunction.JsonTableColumnsClause columnsClause = + jsonTableFunction.getColumnsClause(); + assertNotNull(columnsClause); + assertEquals(2, columnsClause.getColumnDefinitions().size()); + assertInstanceOf(JsonTableFunction.JsonTableValueColumnDefinition.class, + columnsClause.getColumnDefinitions().get(0)); + assertInstanceOf(JsonTableFunction.JsonTableNestedColumnDefinition.class, + columnsClause.getColumnDefinitions().get(1)); + + JsonTableFunction.JsonTableValueColumnDefinition firstColumn = + (JsonTableFunction.JsonTableValueColumnDefinition) columnsClause + .getColumnDefinitions().get(0); + assertEquals("a", firstColumn.getColumnName()); + assertEquals("UTF8", firstColumn.getEncoding()); + assertTrue(firstColumn.isFormatJson()); + assertEquals("'lax $.a'", firstColumn.getPathExpression().toString()); + assertEquals(JsonFunction.JsonWrapperType.WITH, + firstColumn.getWrapperClause().getWrapperType()); + assertEquals(JsonFunction.JsonWrapperMode.CONDITIONAL, + firstColumn.getWrapperClause().getWrapperMode()); + assertTrue(firstColumn.getWrapperClause().isArray()); + assertEquals(JsonFunction.JsonQuotesType.KEEP, + firstColumn.getQuotesClause().getQuotesType()); + assertTrue(firstColumn.getQuotesClause().isOnScalarString()); + assertEquals(JsonFunction.JsonOnResponseBehaviorType.DEFAULT, + firstColumn.getOnEmptyBehavior().getType()); + assertEquals("'missing'", firstColumn.getOnEmptyBehavior().getExpression().toString()); + assertEquals(JsonFunction.JsonOnResponseBehaviorType.NULL, + firstColumn.getOnErrorBehavior().getType()); + + JsonTableFunction.JsonTableNestedColumnDefinition nestedColumn = + (JsonTableFunction.JsonTableNestedColumnDefinition) columnsClause + .getColumnDefinitions().get(1); + assertTrue(nestedColumn.isPathKeyword()); + assertEquals("'lax $[*]'", nestedColumn.getPathExpression().toString()); + assertEquals("\"nested_path\"", nestedColumn.getPathName()); + assertNotNull(nestedColumn.getColumnsClause()); + assertEquals(1, nestedColumn.getColumnsClause().getColumnDefinitions().size()); + + JsonTableFunction.JsonTableValueColumnDefinition nestedValueColumn = + (JsonTableFunction.JsonTableValueColumnDefinition) nestedColumn.getColumnsClause() + .getColumnDefinitions().get(0); + assertEquals("b", nestedValueColumn.getColumnName()); + assertEquals("'lax $.b'", nestedValueColumn.getPathExpression().toString()); + + assertNotNull(jsonTableFunction.getPlanClause()); + assertTrue(jsonTableFunction.getPlanClause().isDefaultPlan()); + assertEquals(2, jsonTableFunction.getPlanClause().getPlanExpression().getTerms().size()); + assertEquals(1, + jsonTableFunction.getPlanClause().getPlanExpression().getOperators().size()); + assertEquals(JsonTableFunction.JsonTablePlanOperator.OUTER, + jsonTableFunction.getPlanClause().getPlanExpression().getOperators().get(0)); + + assertNotNull(jsonTableFunction.getOnErrorClause()); + assertEquals(JsonTableFunction.JsonTableOnErrorType.EMPTY, + jsonTableFunction.getOnErrorClause().getType()); + + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true, + parser -> parser.withAllowComplexParsing(false)); + } + @Test public void testIssue1260() throws JSQLParserException { TestUtils.assertSqlCanBeParsedAndDeparsed( diff --git a/src/test/java/net/sf/jsqlparser/statement/simpleparsing/CCJSqlParserManagerTest.java b/src/test/java/net/sf/jsqlparser/statement/simpleparsing/CCJSqlParserManagerTest.java index 1a342b42d..ba588c417 100644 --- a/src/test/java/net/sf/jsqlparser/statement/simpleparsing/CCJSqlParserManagerTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/simpleparsing/CCJSqlParserManagerTest.java @@ -13,32 +13,42 @@ import java.io.InputStreamReader; import java.io.StringReader; import java.util.Objects; +import java.util.stream.Stream; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.parser.CCJSqlParserManager; import net.sf.jsqlparser.statement.create.CreateTableTest; import net.sf.jsqlparser.test.TestException; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + public class CCJSqlParserManagerTest { - @Test - public void testParse() throws Exception { - CCJSqlParserManager parserManager = new CCJSqlParserManager(); + // Create a DynamicTest stream for every statement in the file simple_parsing.txt + @TestFactory + Stream testParsePerStatement() { BufferedReader in = new BufferedReader(new InputStreamReader(Objects .requireNonNull(CreateTableTest.class.getResourceAsStream("/simple_parsing.txt")))); - String statement = ""; - while (true) { + // Convert buffered reader to stream of statements + return Stream.generate(() -> { try { - statement = CCJSqlParserManagerTest.getStatement(in); - if (statement == null) { - break; - } - - parserManager.parse(new StringReader(statement)); - } catch (JSQLParserException e) { - throw new TestException("impossible to parse statement: " + statement, e); + return CCJSqlParserManagerTest.getStatement(in); + } catch (Exception e) { + throw new RuntimeException(e); } + }).takeWhile(Objects::nonNull) + .map(statement -> DynamicTest.dynamicTest("Parsing statement: " + statement, () -> { + testParse(statement); + })); + } + + private void testParse(String statement) throws Exception { + CCJSqlParserManager parserManager = new CCJSqlParserManager(); + try { + parserManager.parse(new StringReader(statement)); + } catch (JSQLParserException e) { + throw new TestException("impossible to parse statement: " + statement, e); } } 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..30e335a9e 100644 --- a/src/test/resources/simple_parsing.txt +++ b/src/test/resources/simple_parsing.txt @@ -248,4 +248,237 @@ 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 + id, + json_exists( + description, + 'lax $.children[*]?(@ > 10)' + ) AS children_above_ten +FROM customers; + +SELECT + id, + json_exists( + description, + 'strict $.children[2]?(@ > 10)' + UNKNOWN ON ERROR + ) AS child_3_above_ten +FROM customers; + +SELECT + id, + json_query( + description, + 'lax $.children' + ) AS children +FROM customers; + +SELECT + id, + json_query( + description, + 'lax $.children[*]' + WITHOUT ARRAY WRAPPER + NULL ON ERROR + ) AS children +FROM customers; + +SELECT + id, + json_query( + description, + 'lax $.children[last]' + WITH ARRAY WRAPPER + ) AS last_child +FROM customers; + +SELECT + id, + json_query( + description, + 'strict $.children[*]?(@ > 12)' + WITH ARRAY WRAPPER + EMPTY ARRAY ON EMPTY + ) AS children +FROM customers; + +SELECT + id, + json_query(description, 'strict $.comment' KEEP QUOTES) AS quoted_comment, + json_query(description, 'strict $.comment' OMIT QUOTES) AS unquoted_comment +FROM customers; + +SELECT id, json_value( + description, + 'lax $.comment' + RETURNING char(12) + ) AS comment +FROM customers; + +SELECT id, json_value( + description, + 'lax $.children[0]' + RETURNING tinyint + ) AS child +FROM customers; + +SELECT id, json_value( + description, + 'strict $.children[2]' + DEFAULT 'err' ON ERROR + ) AS child +FROM customers; + +SELECT id, json_value( + description, + 'lax $.children[2]' + DEFAULT 'missing' ON EMPTY + ) AS child +FROM customers; + +SELECT + * +FROM + json_table( + '[ + {"id":1,"name":"Africa","wikiDataId":"Q15"}, + {"id":2,"name":"Americas","wikiDataId":"Q828"}, + {"id":3,"name":"Asia","wikiDataId":"Q48"}, + {"id":4,"name":"Europe","wikiDataId":"Q51"} + ]', + 'strict $' COLUMNS ( + NESTED PATH 'strict $[*]' COLUMNS ( + id integer PATH 'strict $.id', + name varchar PATH 'strict $.name', + wiki_data_id varchar PATH 'strict $."wikiDataId"' + ) + ) + ); + +SELECT + * +FROM + json_table( + '[ + {"continent": "Asia", "countries": [ + {"name": "Japan", "population": 125.7}, + {"name": "Thailand", "population": 71.6} + ]}, + {"continent": "Europe", "countries": [ + {"name": "France", "population": 67.4}, + {"name": "Germany", "population": 83.2} + ]} + ]', + 'lax $' COLUMNS ( + NESTED PATH 'lax $[*]' COLUMNS ( + continent varchar PATH 'lax $.continent', + NESTED PATH 'lax $.countries[*]' COLUMNS ( + country varchar PATH 'lax $.name', + population double PATH 'lax $.population' + ) + ) + )); + +SELECT + * +FROM + JSON_TABLE( + '[]', + 'lax $' AS "root_path" + COLUMNS( + a varchar(1) PATH 'lax "A"', + NESTED PATH 'lax $[*]' AS "nested_path" + COLUMNS (b varchar(1) PATH 'lax "B"')) + PLAN ("root_path" OUTER "nested_path") + ); + +SELECT + * +FROM + JSON_TABLE( + '[]', + 'lax $' AS "root_path" + COLUMNS( + a varchar(1) PATH 'lax "A"', + NESTED PATH 'lax $[*]' AS "nested_path" + COLUMNS (b varchar(1) PATH 'lax "B"')) + PLAN ("root_path" INNER "nested_path") + ); + +SELECT json_array(true, 12e-1, 'text'); + +SELECT json_array( + '[ "text" ] ' FORMAT JSON, + X'5B0035005D00' FORMAT JSON ENCODING UTF16 + ); + +SELECT json_array( + json_query('{"key" : [ "value" ]}', 'lax $.key') + ); + +SELECT json_array( + DATE '2001-01-31', + UUID '12151fd2-7586-11e9-8f9e-2a86e4085a59' + ); + +SELECT json_array(); + +SELECT json_array(true, null, 1); + +SELECT json_array(true, null, 1 ABSENT ON NULL); + +SELECT json_array(true, null, 1 NULL ON NULL); + +SELECT json_array(true, 1 RETURNING VARCHAR(100)); + +SELECT json_array(true, 1 RETURNING VARBINARY); + +SELECT json_array(true, 1 RETURNING VARBINARY FORMAT JSON ENCODING UTF8); + +SELECT json_array(true, 1 RETURNING VARBINARY FORMAT JSON ENCODING UTF16); + +SELECT json_array(true, 1 RETURNING VARBINARY FORMAT JSON ENCODING UTF32); + +SELECT json_object('key1' : 1, 'key2' : true); + +SELECT json_object(KEY 'key1' VALUE 1, KEY 'key2' VALUE true); + +SELECT json_object('key1' VALUE 1, 'key2' VALUE true); + +SELECT json_object('x' : true, 'y' : 12e-1, 'z' : 'text'); + +SELECT json_object( + 'x' : '[ "text" ] ' FORMAT JSON, + 'y' : X'5B0035005D00' FORMAT JSON ENCODING UTF16 + ); + +SELECT json_object( + 'x' : json_query('{"key" : [ "value" ]}', 'lax $.key') + ); + +SELECT json_object( + 'x' : DATE '2001-01-31', + 'y' : UUID '12151fd2-7586-11e9-8f9e-2a86e4085a59' + ); + +SELECT json_object(); + +SELECT json_object('x' : null, 'y' : 1); + +SELECT json_object('x' : null, 'y' : 1 NULL ON NULL); + +SELECT json_object('x' : null, 'y' : 1 ABSENT ON NULL); + +SELECT json_object('x' : null, 'x' : 1 WITH UNIQUE KEYS); + +SELECT json_object('x' : 1 RETURNING VARCHAR(100)); + +SELECT json_object('x' : 1 RETURNING VARBINARY); + +SELECT json_object('x' : 1 RETURNING VARBINARY FORMAT JSON ENCODING UTF8); + +SELECT json_object('x' : 1 RETURNING VARBINARY FORMAT JSON ENCODING UTF16); + +SELECT json_object('x' : 1 RETURNING VARBINARY FORMAT JSON ENCODING UTF32); \ No newline at end of file