diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index ada4bfdf4..13b82a21f 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -58,4 +58,6 @@ exports net.sf.jsqlparser.util.validation.feature; exports net.sf.jsqlparser.util.validation.metadata; exports net.sf.jsqlparser.util.validation.validator; + exports net.sf.jsqlparser.expression.json; + exports net.sf.jsqlparser.statement.from; } diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java index aee8e7bf3..d74818b24 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java @@ -27,7 +27,7 @@ */ public class JsonFunction extends ASTNodeAccessImpl implements Expression { public enum JsonOnResponseBehaviorType { - ERROR, NULL, DEFAULT, EMPTY_ARRAY, EMPTY_OBJECT, TRUE, FALSE, UNKNOWN + ERROR, NULL, DEFAULT, EMPTY, EMPTY_ARRAY, EMPTY_OBJECT, TRUE, FALSE, UNKNOWN } public enum JsonWrapperType { @@ -42,6 +42,10 @@ public enum JsonQuotesType { KEEP, OMIT } + public enum ScalarsType { + ALLOW, DISALLOW + } + public static class JsonOnResponseBehavior { private JsonOnResponseBehaviorType type; private Expression expression; @@ -82,6 +86,9 @@ public StringBuilder append(StringBuilder builder) { case DEFAULT: builder.append("DEFAULT ").append(expression); break; + case EMPTY: + builder.append("EMPTY "); + break; case EMPTY_ARRAY: builder.append("EMPTY ARRAY"); break; @@ -98,6 +105,7 @@ public StringBuilder append(StringBuilder builder) { builder.append("UNKNOWN"); break; default: + throw new IllegalStateException("Unhandled JsonOnResponseBehavior: " + type ); // this should never happen } return builder; @@ -130,6 +138,7 @@ public String toString() { private boolean wrapperArray; private JsonQuotesType quotesType; private boolean quotesOnScalarString; + private ScalarsType scalarsType; public JsonFunction() {} @@ -294,6 +303,14 @@ public void setQuotesOnScalarString(boolean quotesOnScalarString) { this.quotesOnScalarString = quotesOnScalarString; } + public ScalarsType getScalarsType() { + return scalarsType; + } + + public void setScalarsType(ScalarsType type) { + this.scalarsType = type; + } + public boolean isEmpty() { return keyValuePairs.isEmpty(); } diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonTableFunction.java b/src/main/java/net/sf/jsqlparser/expression/JsonTableFunction.java index b7f5d0149..deae12bde 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonTableFunction.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonTableFunction.java @@ -33,7 +33,7 @@ public String getDisplay() { } public enum JsonTableOnErrorType { - ERROR, EMPTY + ERROR, NULL, EMPTY } public static class JsonTablePassingClause extends ASTNodeAccessImpl implements Serializable { @@ -395,6 +395,7 @@ public static class JsonTableValueColumnDefinition extends JsonTableColumnDefini private JsonTableQuotesClause quotesClause; private JsonFunction.JsonOnResponseBehavior onEmptyBehavior; private JsonFunction.JsonOnResponseBehavior onErrorBehavior; + private JsonFunction.ScalarsType scalarsType; public String getColumnName() { return columnName; @@ -489,6 +490,14 @@ public JsonTableValueColumnDefinition setOnErrorBehavior( return this; } + public void setScalarsType(JsonFunction.ScalarsType scalarsType) { + this.scalarsType = scalarsType; + } + + public JsonFunction.ScalarsType getScalarsType() { + return scalarsType; + } + @Override public void collectExpressions(List expressions) { if (pathExpression != null) { @@ -509,14 +518,20 @@ public String toString() { builder.append(" FOR ORDINALITY"); return builder.toString(); } - - builder.append(" ").append(dataType); + if (dataType != null) { + builder.append(" ").append(dataType); + } if (formatJson) { builder.append(" FORMAT JSON"); if (encoding != null) { builder.append(" ENCODING ").append(encoding); } } + if (scalarsType != null) { + builder.append(" "); + builder.append(scalarsType); + builder.append(" SCALARS"); + } if (pathExpression != null) { builder.append(" PATH ").append(pathExpression); } @@ -676,7 +691,10 @@ public T accept(ExpressionVisitor expressionVisitor, S context) { @Override public String toString() { StringBuilder builder = new StringBuilder("JSON_TABLE("); - builder.append(jsonInputExpression).append(", ").append(jsonPathExpression); + builder.append(jsonInputExpression); + if (jsonPathExpression != null) { + builder.append(", ").append(jsonPathExpression); + } if (pathName != null) { builder.append(" AS ").append(pathName); } diff --git a/src/main/java/net/sf/jsqlparser/expression/json/JsonOnEmptyType.java b/src/main/java/net/sf/jsqlparser/expression/json/JsonOnEmptyType.java new file mode 100644 index 000000000..c78a852d1 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/json/JsonOnEmptyType.java @@ -0,0 +1,48 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2021 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +/* + * Copyright (C) 2021 JSQLParser. + * + * This library is free software; you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation; either version + * 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this library; + * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +package net.sf.jsqlparser.expression.json; + +/** + * + */ +public enum JsonOnEmptyType { + ERROR("ERROR"), NULL("NULL"), EMPTY("EMPTY"), EMPTY_ARRAY("EMPTY ARRAY"), EMPTY_OBJECT( + "EMPTY OBJECT"), TRUE("TRUE"), FALSE("FALSE"), DEFAULT("DEFAULT"); + + private final String value; + + JsonOnEmptyType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static JsonOnEmptyType from(String type) { + return Enum.valueOf(JsonOnEmptyType.class, type.toUpperCase()); + } +} diff --git a/src/main/java/net/sf/jsqlparser/expression/json/JsonOnErrorType.java b/src/main/java/net/sf/jsqlparser/expression/json/JsonOnErrorType.java new file mode 100644 index 000000000..3c5b50b88 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/json/JsonOnErrorType.java @@ -0,0 +1,48 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2021 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +/* + * Copyright (C) 2021 JSQLParser. + * + * This library is free software; you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation; either version + * 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this library; + * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +package net.sf.jsqlparser.expression.json; + +/** + * + */ +public enum JsonOnErrorType { + ERROR("ERROR"), NULL("NULL"), EMPTY("EMPTY"), EMPTY_ARRAY("EMPTY ARRAY"), EMPTY_OBJECT( + "EMPTY OBJECT"), TRUE("TRUE"), FALSE("FALSE"), DEFAULT("DEFAULT"); + + private final String value; + + JsonOnErrorType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static JsonOnErrorType from(String type) { + return Enum.valueOf(JsonOnErrorType.class, type.toUpperCase()); + } +} diff --git a/src/main/java/net/sf/jsqlparser/expression/json/JsonQueryWrapperType.java b/src/main/java/net/sf/jsqlparser/expression/json/JsonQueryWrapperType.java new file mode 100644 index 000000000..bd719511f --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/json/JsonQueryWrapperType.java @@ -0,0 +1,77 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2021 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +/* + * Copyright (C) 2021 JSQLParser. + * + * This library is free software; you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation; either version + * 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this library; + * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +package net.sf.jsqlparser.expression.json; + +/** + * + */ +public enum JsonQueryWrapperType { + //@formatter:off + WITHOUT_WRAPPER("WITHOUT WRAPPER"), + WITHOUT_ARRAY_WRAPPER("WITHOUT ARRAY WRAPPER"), + WITH_WRAPPER("WITH WRAPPER"), + WITH_ARRAY_WRAPPER("WITH ARRAY WRAPPER"), + WITH_UNCONDITIONAL_WRAPPER("WITH UNCONDITIONAL WRAPPER"), + WITH_UNCONDITIONAL_ARRAY_WRAPPER("WITH UNCONDITIONAL ARRAY WRAPPER"), + WITH_CONDITIONAL_WRAPPER("WITH CONDITIONAL WRAPPER"), + WITH_CONDITIONAL_ARRAY_WRAPPER("WITH CONDITIONAL ARRAY WRAPPER"); + //@formatter:on + + private final String value; + + JsonQueryWrapperType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static JsonQueryWrapperType from(String type) { + return Enum.valueOf(JsonQueryWrapperType.class, type.toUpperCase()); + } + + public static JsonQueryWrapperType fromWithParts(boolean isArray, boolean isConditional, + boolean isUnconditional) { + if (isArray) { + if (isConditional) { + return JsonQueryWrapperType.WITH_CONDITIONAL_ARRAY_WRAPPER; + } else if (isUnconditional) { + return JsonQueryWrapperType.WITH_UNCONDITIONAL_ARRAY_WRAPPER; + } else { + return JsonQueryWrapperType.WITH_ARRAY_WRAPPER; + } + } else { + if (isConditional) { + return JsonQueryWrapperType.WITH_CONDITIONAL_WRAPPER; + } else if (isUnconditional) { + return JsonQueryWrapperType.WITH_UNCONDITIONAL_WRAPPER; + } else { + return JsonQueryWrapperType.WITH_WRAPPER; + } + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/expression/json/JsonReturnClause.java b/src/main/java/net/sf/jsqlparser/expression/json/JsonReturnClause.java new file mode 100644 index 000000000..b58c29c9c --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/json/JsonReturnClause.java @@ -0,0 +1,71 @@ +package net.sf.jsqlparser.expression.json; + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +public class JsonReturnClause extends ASTNodeAccessImpl { + + private JsonReturnType type; + + private Long varcharSize; + + public JsonReturnClause() {} + + public JsonReturnClause(JsonReturnType type) { + this.type = type; + } + + public JsonReturnType getType() { + return type; + } + + public void setType(JsonReturnType type) { + this.type = type; + } + + public JsonReturnClause withType(JsonReturnType type) { + setType(type); + return this; + } + + public Long getVarcharSize() { + return varcharSize; + } + + public void setVarcharSize(Long varcharSize) { + this.varcharSize = varcharSize; + } + + public JsonReturnClause withVarcharSize(Long varcharSize) { + setVarcharSize(varcharSize); + return this; + } + + public StringBuilder append(StringBuilder builder) { + builder.append(" "); + builder.append(type.getValue()); + switch (type) { + case VARCHAR2: + case VARCHAR2_BYTE: + case VARCHAR2_CHAR: + if (varcharSize != null) { + builder.append("("); + builder.append(varcharSize); + switch (type) { + case VARCHAR2_BYTE: + builder.append(" BYTE"); + break; + case VARCHAR2_CHAR: + builder.append(" CHAR"); + break; + } + builder.append(")"); + } + break; + default: + // Nothing to do + break; + } + return builder; + } + +} diff --git a/src/main/java/net/sf/jsqlparser/expression/json/JsonReturnType.java b/src/main/java/net/sf/jsqlparser/expression/json/JsonReturnType.java new file mode 100644 index 000000000..24b57739e --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/json/JsonReturnType.java @@ -0,0 +1,73 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2021 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +/* + * Copyright (C) 2021 JSQLParser. + * + * This library is free software; you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation; either version + * 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this library; + * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +package net.sf.jsqlparser.expression.json; + +/** + * + */ +public enum JsonReturnType { + VARCHAR2("VARCHAR2"), CLOB("CLOB"), BLOB("BLOB"), NUMBER("NUMBER"), DATE("DATE"), TIMESTAMP( + "TIMESTAMP"), TIMESTAMP_WITH_TIMEZONE( + "TIMESTAMP WITH TIMEZONE"), BOOLEAN("BOOLEAN"), VECTOR("VECTOR"), JSON("JSON"), + + // VARCHAR2( x BYTE) + VARCHAR2_BYTE("VARCHAR2"), + + // VARCHAR2( x CHAR) + VARCHAR2_CHAR("VARCHAR2"), + ; + + private final String value; + + JsonReturnType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static JsonReturnType from(String type) { + return Enum.valueOf(JsonReturnType.class, type.toUpperCase()); + } + + /** + * @see "https://docs.oracle.com/en/database/oracle/oracle-database/26/sqlrf/JSON_QUERY.html#GUID-6D396EC4-D2AA-43D2-8F5D-08D646A4A2D9__CJADJIIJ" + */ + public boolean isValidForJsonQueryReturnType() { + switch (this) { + case VARCHAR2: + case CLOB: + case BLOB: + case JSON: + case VECTOR: + return true; + default: + return false; + } + } + +} diff --git a/src/main/java/net/sf/jsqlparser/statement/from/JsonTable.java b/src/main/java/net/sf/jsqlparser/statement/from/JsonTable.java new file mode 100644 index 000000000..47f73c3d9 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/from/JsonTable.java @@ -0,0 +1,202 @@ +package net.sf.jsqlparser.statement.from; + +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.json.JsonOnEmptyType; +import net.sf.jsqlparser.expression.json.JsonOnErrorType; +import net.sf.jsqlparser.statement.select.AbstractFromitem; +import net.sf.jsqlparser.statement.select.FromItem; +import net.sf.jsqlparser.statement.select.FromItemVisitor; + +import java.util.ArrayList; +import java.util.List; + +public class JsonTable extends AbstractFromitem implements FromItem { + + private Expression expression; + private boolean isFormatJson = false; + + private String pathExpression; + + private JsonOnErrorType onErrorType; + private JsonTableType type; + private JsonOnEmptyType onEmptyType; + + private List jsonColumns = new ArrayList<>(); + + public StringBuilder append(StringBuilder builder) { + builder.append("JSON_TABLE("); + builder.append(expression.toString()); + + if (isFormatJson) { + builder.append(" FORMAT JSON"); + } + + if (pathExpression != null) { + builder.append(", '"); + builder.append(pathExpression); + builder.append("'"); + } + + if (onErrorType != null) { + builder.append(" "); + builder.append(onErrorType); + builder.append(" ON ERROR"); + } + + if (type != null) { + builder.append(" TYPE("); + builder.append(type); + builder.append(")"); + } + + if (onEmptyType != null) { + builder.append(" "); + builder.append(onEmptyType); + builder.append(" ON EMPTY"); + } + + builder.append(" COLUMNS("); + + for (JsonTableColumn column : jsonColumns) { + column.append(builder); + } + + builder.append("))"); + + super.appendTo(builder, getAlias(), getSampleClause(), getPivot(), getUnPivot()); + + return builder; + } + + public void setExpression(Expression expression) { + this.expression = expression; + } + + public Expression getExpression() { + return expression; + } + + public JsonTable withExpression(Expression expression) { + setExpression(expression); + return this; + } + + public void setPathExpression(String pathExpression) { + this.pathExpression = pathExpression; + } + + public String getPathExpression() { + return pathExpression; + } + + public JsonTable withPathExpression(String pathExpression) { + setPathExpression(pathExpression); + return this; + } + + public void setFormatJson(boolean usingJson) { + this.isFormatJson = true; + } + + public boolean isFormatJson() { + return isFormatJson; + } + + public JsonTable withFormatJson(boolean isFormatJson) { + setFormatJson(isFormatJson); + return this; + } + + public void setOnErrorType(JsonOnErrorType onErrorType) { + if (onErrorType != null) { + switch (onErrorType) { + case NULL: + case ERROR: + break; + default: + throw new IllegalArgumentException( + "OnError type " + onErrorType + " is not allowed in JSON_TABLE"); + } + } + + this.onErrorType = onErrorType; + } + + /** + * Returns the ON ERROR clause or NULL if none is set + * + * @return JsonOnErrorType or NULL + */ + public JsonOnErrorType getOnErrorType() { + return onErrorType; + } + + public JsonTable withOnErrorType(JsonOnErrorType onErrorType) { + setOnErrorType(onErrorType); + return this; + } + + public void setType(JsonTableType type) { + this.type = type; + } + + public JsonTableType getType() { + return type; + } + + public JsonTable withType(JsonTableType type) { + setType(type); + return this; + } + + public void setOnEmptyType(JsonOnEmptyType onEmptyType) { + if (onEmptyType != null) { + switch (onEmptyType) { + case NULL: + case ERROR: + break; + default: + throw new IllegalArgumentException( + "OnEmpty type " + onEmptyType + " is not allowed in JSON_TABLE"); + } + } + this.onEmptyType = onEmptyType; + } + + /** + * Returns the ON EMPTY clause or NULL if none is set + * + * @return JsonOnEmptyType or NULL + */ + public JsonOnEmptyType getOnEmptyType() { + return onEmptyType; + } + + public JsonTable withOnEmptyType(JsonOnEmptyType onEmptyType) { + setOnEmptyType(onEmptyType); + return this; + } + + public void addColumn(JsonTableColumn column) { + this.jsonColumns.add(column); + } + + public JsonTable withColumn(JsonTableColumn column) { + addColumn(column); + return this; + } + + public List getColumns() { + return jsonColumns; + } + + @Override + public String toString() { + return append(new StringBuilder()).toString(); + } + + @Override + public T accept(FromItemVisitor fromItemVisitor, S context) { + return fromItemVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/from/JsonTableColumn.java b/src/main/java/net/sf/jsqlparser/statement/from/JsonTableColumn.java new file mode 100644 index 000000000..44a791c52 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/from/JsonTableColumn.java @@ -0,0 +1,287 @@ +package net.sf.jsqlparser.statement.from; + +import net.sf.jsqlparser.expression.json.JsonOnEmptyType; +import net.sf.jsqlparser.expression.json.JsonOnErrorType; +import net.sf.jsqlparser.expression.json.JsonQueryWrapperType; +import net.sf.jsqlparser.expression.json.JsonReturnClause; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +public class JsonTableColumn extends ASTNodeAccessImpl { + + private String name; + private JsonTableColumnType type; + + private JsonReturnClause returnClause; + + private boolean isFormatJson = false; + private String jsonPath; + + // Can be true, false or NULL + private Boolean allowScalars; + private JsonQueryWrapperType queryWrapperType; + private JsonOnErrorType onErrorType; + private JsonOnEmptyType onEmptyType; + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public JsonTableColumn withName(String columnName) { + setName(columnName); + return this; + } + + public void setJsonPath(String jsonPath) { + this.jsonPath = jsonPath; + } + + public String getJsonPath() { + return jsonPath; + } + + public JsonTableColumn withJsonPath(String jsonPath) { + setJsonPath(jsonPath); + return this; + } + + public void setType(JsonTableColumnType type) { + this.type = type; + } + + public JsonTableColumn withType(JsonTableColumnType type) { + setType(type); + return this; + } + + public JsonTableColumnType getType() { + return type; + } + + public void setFormatJson(boolean usingJson) { + if (usingJson && type != JsonTableColumnType.JSON_QUERY) { + throw new IllegalArgumentException( + "FORMAT JSON can only be used on JSON_QUERY-Columns"); + } + this.isFormatJson = true; + } + + public boolean isFormatJson() { + return isFormatJson; + } + + public JsonTableColumn withFormatJson(boolean isFormatJson) { + setFormatJson(isFormatJson); + return this; + } + + public void setOnEmptyType(JsonOnEmptyType onEmptyType) { + if (onEmptyType != null) { + switch (type) { + case JSON_EXISTS: + switch (onEmptyType) { + case TRUE: + case FALSE: + case ERROR: + break; + default: + throw new IllegalArgumentException("OnEmpty type " + onEmptyType + + " is not allowed in JsonTableColumn of type " + type); + } + break; + case JSON_QUERY: + case ORDINALITY: + throw new IllegalArgumentException("OnEmpty type " + onEmptyType + + " is not allowed in JsonTableColumn of type " + type); + } + } + this.onEmptyType = onEmptyType; + } + + /** + * Returns the ON EMPTY clause or NULL if none is set + * + * @return JsonOnEmptyType or NULL + */ + public JsonOnEmptyType getOnEmptyType() { + return onEmptyType; + } + + public JsonTableColumn withOnEmptyType(JsonOnEmptyType onEmptyType) { + setOnEmptyType(onEmptyType); + return this; + } + + public void setOnErrorType(JsonOnErrorType onErrorType) { + if (onErrorType != null) { + switch (type) { + case JSON_EXISTS: + switch (onErrorType) { + case TRUE: + case FALSE: + case ERROR: + break; + default: + throw new IllegalArgumentException("OnError type " + onErrorType + + " is not allowed in JsonTableColumn of type " + type); + } + break; + case JSON_QUERY: + switch (onErrorType) { + case ERROR: + case NULL: + case EMPTY: + case EMPTY_ARRAY: + case EMPTY_OBJECT: + break; + default: + throw new IllegalArgumentException("OnError type " + onErrorType + + " is not allowed in JsonTableColumn of type " + type); + } + break; + case ORDINALITY: + throw new IllegalArgumentException("OnError type " + onErrorType + + " is not allowed in JsonTableColumn of type " + type); + } + } + + this.onErrorType = onErrorType; + } + + /** + * Returns the ON ERROR clause or NULL if none is set + * + * @return JsonOnErrorType or NULL + */ + public JsonOnErrorType getOnErrorType() { + return onErrorType; + } + + public JsonTableColumn withOnErrorType(JsonOnErrorType onErrorType) { + setOnErrorType(onErrorType); + return this; + } + + public void setQueryWrapperType(JsonQueryWrapperType queryWrapperType) { + if (type != JsonTableColumnType.JSON_QUERY) { + throw new IllegalArgumentException( + "QueryWrapperType is only allowed on columns with type JSON_QUERY"); + } + this.queryWrapperType = queryWrapperType; + } + + public JsonQueryWrapperType getQueryWrapperType() { + return queryWrapperType; + } + + public JsonTableColumn withQueryWrapperType(JsonQueryWrapperType queryWrapperType) { + setQueryWrapperType(queryWrapperType); + return this; + } + + public Boolean getAllowScalars() { + return allowScalars; + } + + public void setAllowScalars(Boolean allowScalars) { + if (allowScalars != null && type != JsonTableColumnType.JSON_QUERY) { + throw new IllegalArgumentException( + "AllowScalars is only allowed on columns with type JSON_QUERY"); + } + this.allowScalars = allowScalars; + } + + public JsonTableColumn withAllowScalars(Boolean allowScalars) { + setAllowScalars(allowScalars); + return this; + } + + public JsonReturnClause getReturnClause() { + return returnClause; + } + + public void setReturnClause(JsonReturnClause returnClause) { + this.returnClause = returnClause; + } + + public JsonTableColumn withReturnClause(JsonReturnClause returnClause) { + setReturnClause(returnClause); + return this; + } + + public StringBuilder append(StringBuilder builder) { + + builder.append(name); + + switch (type) { + case ORDINALITY: + builder.append(" FOR ORDINALITY"); + break; + case JSON_EXISTS: + appendJsonExists(builder); + break; + case JSON_QUERY: + appendJsonQuery(builder); + break; + default: + throw new IllegalStateException("Type " + type + " is unknown"); + } + + return builder; + } + + private void appendJsonQuery(StringBuilder builder) { + if (returnClause != null) { + returnClause.append(builder); + } + if (isFormatJson) { + builder.append(" FORMAT JSON"); + } + if (allowScalars != null) { + if (allowScalars) { + builder.append(" ALLOW"); + } else { + builder.append(" DISALLOW"); + } + builder.append(" SCALARS"); + } + if (queryWrapperType != null) { + builder.append(" "); + builder.append(queryWrapperType.getValue()); + } + if (jsonPath != null) { + builder.append(" PATH '"); + builder.append(jsonPath); + builder.append("'"); + } + if (onErrorType != null) { + builder.append(" "); + builder.append(onErrorType.getValue()); + builder.append(" ON ERROR"); + } + } + + private void appendJsonExists(StringBuilder builder) { + // TODO: Append return type + builder.append(" EXISTS"); + if (jsonPath != null) { + builder.append(" PATH '"); + builder.append(jsonPath); + builder.append("'"); + } + if (onErrorType != null) { + builder.append(" "); + builder.append(onErrorType); + builder.append(" ON ERROR"); + } + if (onEmptyType != null) { + builder.append(" "); + builder.append(onEmptyType); + builder.append(" ON EMPTY"); + } + } + +} diff --git a/src/main/java/net/sf/jsqlparser/statement/from/JsonTableColumnType.java b/src/main/java/net/sf/jsqlparser/statement/from/JsonTableColumnType.java new file mode 100644 index 000000000..82c978bbc --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/from/JsonTableColumnType.java @@ -0,0 +1,5 @@ +package net.sf.jsqlparser.statement.from; + +public enum JsonTableColumnType { + JSON_EXISTS, JSON_QUERY, JSON_VALUE, JSON_NESTED_PATH, ORDINALITY +} diff --git a/src/main/java/net/sf/jsqlparser/statement/from/JsonTableType.java b/src/main/java/net/sf/jsqlparser/statement/from/JsonTableType.java new file mode 100644 index 000000000..5b2ae37cd --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/from/JsonTableType.java @@ -0,0 +1,37 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2021 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +/* + * Copyright (C) 2021 JSQLParser. + * + * This library is free software; you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation; either version + * 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this library; + * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +package net.sf.jsqlparser.statement.from; + +/** + * + */ +public enum JsonTableType { + STRICT, LAX; + + public static JsonTableType from(String type) { + return Enum.valueOf(JsonTableType.class, type.toUpperCase()); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/AbstractFromitem.java b/src/main/java/net/sf/jsqlparser/statement/select/AbstractFromitem.java new file mode 100644 index 000000000..726a8ead5 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/AbstractFromitem.java @@ -0,0 +1,53 @@ +package net.sf.jsqlparser.statement.select; + +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +public abstract class AbstractFromitem extends ASTNodeAccessImpl implements FromItem { + private Alias alias; + private Pivot pivot; + private UnPivot unPivot; + private SampleClause sampleClause = null; + + @Override + public Alias getAlias() { + return alias; + } + + @Override + public void setAlias(Alias alias) { + this.alias = alias; + } + + @Override + public Pivot getPivot() { + return pivot; + } + + @Override + public void setPivot(Pivot pivot) { + this.pivot = pivot; + } + + @Override + public UnPivot getUnPivot() { + return unPivot; + } + + @Override + public void setUnPivot(UnPivot unpivot) { + this.unPivot = unpivot; + } + + @Override + public SampleClause getSampleClause() { + return sampleClause; + } + + @Override + public FromItem setSampleClause(SampleClause sampleClause) { + this.sampleClause = sampleClause; + return this; + } + +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitor.java b/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitor.java index ed4432003..4a3c43d67 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitor.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.statement.select; import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.from.JsonTable; import net.sf.jsqlparser.statement.imprt.Import; import net.sf.jsqlparser.statement.piped.FromQuery; @@ -104,4 +105,10 @@ default void visit(Import imprt) { } T visit(FromQuery fromQuery, S context); + + default void visit(JsonTable jsonTable) { + this.visit(jsonTable, null); + } + + T visit(JsonTable jsonTable, S context); } diff --git a/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java index 783b614f2..b634adf25 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java @@ -14,6 +14,7 @@ import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.from.JsonTable; import net.sf.jsqlparser.statement.imprt.Import; import net.sf.jsqlparser.statement.piped.FromQuery; @@ -148,4 +149,9 @@ public T visit(Import imprt, S context) { public T visit(FromQuery fromQuery, S context) { return fromQuery.accept(selectVisitor, context); } + + @Override + public T visit(JsonTable jsonTable, S context) { + return jsonTable.getExpression().accept(expressionVisitor, context); + } } diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 09ca9faba..dd6a820e7 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -123,6 +123,7 @@ import net.sf.jsqlparser.statement.select.FromItemVisitor; import net.sf.jsqlparser.statement.select.FunctionAllColumns; import net.sf.jsqlparser.statement.select.Join; +import net.sf.jsqlparser.statement.from.JsonTable; import net.sf.jsqlparser.statement.select.LateralSubSelect; import net.sf.jsqlparser.statement.select.OrderByElement; import net.sf.jsqlparser.statement.select.ParenthesedFromItem; @@ -1783,6 +1784,11 @@ public Void visit(JsonTableFunction expression, S context) { return null; } + @Override + public Void visit(JsonTable jsonTable, S context) { + return jsonTable.getExpression().accept(this, context); + } + @Override public Void visit(ConnectByRootOperator connectByRootOperator, S context) { connectByRootOperator.getColumn().accept(this, context); diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java index ba7d8ef3d..5b33a4d24 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java @@ -19,6 +19,7 @@ import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.ExpressionVisitor; +import net.sf.jsqlparser.statement.from.JsonTable; import net.sf.jsqlparser.expression.MySQLIndexHint; import net.sf.jsqlparser.expression.OracleHint; import net.sf.jsqlparser.expression.SQLServerHints; @@ -925,6 +926,12 @@ public StringBuilder visit(FromQuery fromQuery, S context) { return builder; } + @Override + public StringBuilder visit(JsonTable jsonTable, S context) { + jsonTable.append(builder); + return builder; + } + public void visit(TableFunction tableFunction) { visit(tableFunction, null); } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java index 36741ecd6..25da61708 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java @@ -12,6 +12,7 @@ import java.util.List; import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.statement.from.JsonTable; import net.sf.jsqlparser.expression.MySQLIndexHint; import net.sf.jsqlparser.expression.SQLServerHints; import net.sf.jsqlparser.parser.feature.Feature; @@ -422,6 +423,11 @@ public Void visit(FromQuery fromQuery, S context) { return null; } + @Override + public Void visit(JsonTable jsonTable, S context) { + return null; + } + public void visit(TableFunction tableFunction) { visit(tableFunction, null); } @@ -438,4 +444,6 @@ public void visit(Import imprt) { visit(imprt, null); } + + } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 8344b3309..5e7d258f4 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -38,6 +38,7 @@ import java.lang.Integer; import net.sf.jsqlparser.parser.feature.*; import net.sf.jsqlparser.expression.*; +import net.sf.jsqlparser.expression.json.*; import net.sf.jsqlparser.expression.operators.arithmetic.*; import net.sf.jsqlparser.expression.operators.conditional.*; import net.sf.jsqlparser.expression.operators.relational.*; @@ -58,6 +59,7 @@ import net.sf.jsqlparser.statement.create.table.*; import net.sf.jsqlparser.statement.create.view.*; import net.sf.jsqlparser.statement.delete.*; import net.sf.jsqlparser.statement.drop.*; +import net.sf.jsqlparser.statement.from.*; import net.sf.jsqlparser.statement.insert.*; import net.sf.jsqlparser.statement.execute.*; import net.sf.jsqlparser.statement.piped.*; @@ -772,6 +774,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -844,6 +847,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -857,6 +861,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1288,6 +1293,7 @@ TOKEN : /* Data Types */ | <#TYPE_BIT: "BISTRING"> | <#TYPE_BLOB: "BLOB" | "BYTEA" | | "VARBINARY" | > | <#TYPE_BOOLEAN: | "BOOL" > + | <#TYPE_CLOB: "CLOB"> | <#TYPE_ENUM: "ENUM" > | <#TYPE_MAP: "MAP" > | <#TYPE_DECIMAL: "DECIMAL" | "NUMBER" | "NUMERIC" > @@ -1303,7 +1309,7 @@ TOKEN : /* Data Types */ | <#TYPE_UHUGEINT: "UHUGEINT" > | <#TYPE_REAL: "REAL" | "FLOAT4" | "FLOAT"> | <#TYPE_DOUBLE: "DOUBLE" | "PRECISION" | "FLOAT8" | "FLOAT64"> - | <#TYPE_VARCHAR: "NVARCHAR" | "VARCHAR" | "NCHAR" | | "BPCHAR" | "TEXT" | "STRING" | | "VARYING"> + | <#TYPE_VARCHAR: "NVARCHAR" | "VARCHAR" | "NCHAR" | | "BPCHAR" | "TEXT" | "STRING" | | "VARYING" | "VARCHAR2"> | <#TYPE_TIME: "TIMETZ" > | <#TYPE_TIMESTAMP: "TIMESTAMP_NS" | "TIMESTAMP_MS" | "TIMESTAMP_S" > @@ -5681,6 +5687,8 @@ FromItem FromItem() #FromItem: || (getToken(1).kind == K_LATERAL && getToken(2).kind != OPENING_BRACKET) }) fromItem=TableFunction() | +// fromItem=JsonTable() +// | LOOKAHEAD(3) fromItem=Table() | LOOKAHEAD(ParenthesedFromItem()) fromItem = ParenthesedFromItem() @@ -8159,6 +8167,12 @@ JsonFunction.JsonOnResponseBehavior JsonValueOnResponseBehavior() : { JsonFunction.JsonOnResponseBehaviorType.NULL); } | + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.EMPTY); + } + | expression = Expression() { behavior = new JsonFunction.JsonOnResponseBehavior( @@ -8188,26 +8202,26 @@ JsonFunction.JsonOnResponseBehavior JsonQueryOnResponseBehavior() : { 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); } - ( - - { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.EMPTY_ARRAY); - } - | - JsonKeyword("OBJECT") - { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.EMPTY_OBJECT); - } - ) + [ + ( + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.EMPTY_ARRAY); + } + | + JsonKeyword("OBJECT") + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.EMPTY_OBJECT); + } + ) + ] ) { if (behavior != null) { @@ -8304,9 +8318,9 @@ JsonFunction JsonValueBody() : { [ dataType = ColDataType() { result.setReturningType(dataType); } ] [ - LOOKAHEAD( JsonValueOnResponseBehavior() ) + LOOKAHEAD( JsonValueOnResponseBehavior() ) behavior = JsonValueOnResponseBehavior() - JsonKeyword("EMPTY") + { result.setOnEmptyBehavior(behavior); } ] @@ -8410,9 +8424,9 @@ JsonFunction JsonQueryBody() : { ] [ - LOOKAHEAD( JsonQueryOnResponseBehavior() ) + LOOKAHEAD( JsonQueryOnResponseBehavior() ) behavior = JsonQueryOnResponseBehavior() - JsonKeyword("EMPTY") + { result.setOnEmptyBehavior(behavior); } ] @@ -8491,9 +8505,9 @@ JsonFunction JsonQueryBody() : { ] ] [ - LOOKAHEAD( JsonQueryOnResponseBehavior() ) + LOOKAHEAD( JsonQueryOnResponseBehavior() ) additionalOnEmptyBehavior = JsonQueryOnResponseBehavior() - JsonKeyword("EMPTY") + ] [ LOOKAHEAD( JsonQueryOnResponseBehavior() ) @@ -9494,14 +9508,21 @@ JsonTableFunction.JsonTableColumnDefinition JsonTableColumnDefinition() : { columnDefinition = valueColumnDefinition; } ( - JsonKeyword("ORDINALITY") - { valueColumnDefinition.setForOrdinality(true); } + JsonKeyword("ORDINALITY") { valueColumnDefinition.setForOrdinality(true); } | - dataType = ColDataType() { valueColumnDefinition.setDataType(dataType); } + [ dataType = ColDataType() { valueColumnDefinition.setDataType(dataType); } ] [ { valueColumnDefinition.setFormatJson(true); } [ encoding = JsonEncoding() { valueColumnDefinition.setEncoding(encoding); } ] ] + [ + ( + { valueColumnDefinition.setScalarsType(JsonFunction.ScalarsType.ALLOW); } + | + { valueColumnDefinition.setScalarsType(JsonFunction.ScalarsType.DISALLOW); } + ) + JsonKeyword("SCALARS") + ] [ expression = Expression() { valueColumnDefinition.setPathExpression(expression); } ] [ wrapperClause = JsonTableWrapperClause() { valueColumnDefinition.setWrapperClause(wrapperClause); } ] [ @@ -9513,14 +9534,14 @@ JsonTableFunction.JsonTableColumnDefinition JsonTableColumnDefinition() : { quotesClause = JsonTableQuotesClause() { valueColumnDefinition.setQuotesClause(quotesClause); } ] [ - LOOKAHEAD( JsonTableOnEmptyBehavior() ) + LOOKAHEAD( JsonTableOnEmptyBehavior() ) behavior = JsonTableOnEmptyBehavior() - JsonKeyword("EMPTY") + { valueColumnDefinition.setOnEmptyBehavior(behavior); } ] [ - LOOKAHEAD( JsonValueOnResponseBehavior() ) - behavior = JsonValueOnResponseBehavior() + LOOKAHEAD( JsonQueryOnResponseBehavior() ) + behavior = JsonQueryOnResponseBehavior() { valueColumnDefinition.setOnErrorBehavior(behavior); } ] @@ -9645,14 +9666,7 @@ JsonTableFunction.JsonTableOnErrorClause JsonTableOnErrorClause() : { ( { 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); - } + { onErrorClause.setType(JsonTableFunction.JsonTableOnErrorType.EMPTY); } ) { @@ -9677,12 +9691,14 @@ JsonTableFunction JsonTableBody() : { jsonInput = Expression() { function.setJsonInputExpression(jsonInput); } - "," - jsonPath = Expression() { - function.setJsonPathExpression(jsonPath); - function.setParameters(new ExpressionList(jsonInput, jsonPath)); - } - [ pathName = RelObjectName() { function.setPathName(pathName); } ] + [ + "," + 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") diff --git a/src/test/java/net/sf/jsqlparser/statement/from/JsonTableTest.java b/src/test/java/net/sf/jsqlparser/statement/from/JsonTableTest.java new file mode 100644 index 000000000..c6a9b8b41 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/from/JsonTableTest.java @@ -0,0 +1,282 @@ +package net.sf.jsqlparser.statement.from; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.JsonTableFunction; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.select.FromItem; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.TableFunction; +import net.sf.jsqlparser.test.TestUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.*; + +public class JsonTableTest { + + @ParameterizedTest + @ValueSource(strings = { + "SELECT jt.phones FROM j_purchaseorder,\n" + + "JSON_TABLE(po_document, '$.ShippingInstructions'\n" + + "COLUMNS(phones VARCHAR2(100) FORMAT JSON PATH '$.Phone')) AS jt", + "SELECT jt.phones FROM j_purchaseorder,\n" + + "JSON_TABLE(po_document, '$.ShippingInstructions'\n" + + "COLUMNS(phones FORMAT JSON PATH '$.Phone')) AS jt" + }) + void testObjectOracle(String sqlStr) throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @ParameterizedTest + @ValueSource(strings = { + "JSON_TABLE(document COLUMNS( id FOR ORDINALITY))", + "JSON_TABLE(document FORMAT JSON COLUMNS( id FOR ORDINALITY))", + "JSON_TABLE(document, '$.SubPath' COLUMNS( id FOR ORDINALITY))", + "JSON_TABLE(document NULL ON ERROR COLUMNS( id FOR ORDINALITY))", + "JSON_TABLE(document ERROR ON ERROR COLUMNS( id FOR ORDINALITY))", + "JSON_TABLE(document TYPE(LAX) COLUMNS( id FOR ORDINALITY))", + "JSON_TABLE(document TYPE(STRICT) COLUMNS( id FOR ORDINALITY))", + "JSON_TABLE(document NULL ON EMPTY COLUMNS( id FOR ORDINALITY))", + "JSON_TABLE(document ERROR ON EMPTY COLUMNS( id FOR ORDINALITY))", + }) + void testExpression(String jsonTableStr) throws JSQLParserException { + JsonTableFunction table = parseTable(jsonTableStr); + + assertThat(table.getColumnsClause().getColumnDefinitions()).hasSize(1); + } + + @ParameterizedTest + @ValueSource(strings = { + "JSON_TABLE(document COLUMNS( hasValue EXISTS PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( hasValue EXISTS PATH '$.pathTest' TRUE ON ERROR TRUE ON EMPTY))", + "JSON_TABLE(document COLUMNS( hasValue EXISTS PATH '$.pathTest' FALSE ON ERROR FALSE ON EMPTY))", + "JSON_TABLE(document COLUMNS( hasValue EXISTS PATH '$.pathTest' ERROR ON ERROR ERROR ON EMPTY))", + "JSON_TABLE(document COLUMNS( hasValue EXISTS PATH '$.pathTest' ERROR ON ERROR))", + "JSON_TABLE(document COLUMNS( hasValue EXISTS PATH '$.pathTest' ERROR ON EMPTY))", + "JSON_TABLE(document COLUMNS( hasValue EXISTS ERROR ON EMPTY))", + "JSON_TABLE(document COLUMNS( hasValue EXISTS))", + }) + void testExistsColumns(String jsonTableStr) throws JSQLParserException { + JsonTableFunction table = parseTable(jsonTableStr); + + assertThat(table.getColumnsClause().getColumnDefinitions()).hasSize(1); + } + + @ParameterizedTest + @ValueSource(strings = { + "JSON_TABLE(document COLUMNS( val PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( val FORMAT JSON PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( val ALLOW SCALARS PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( val DISALLOW SCALARS PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( val FORMAT JSON DISALLOW SCALARS PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( val WITH WRAPPER PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( val WITHOUT WRAPPER PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( val WITH ARRAY WRAPPER PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( val WITHOUT ARRAY WRAPPER PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( val WITH CONDITIONAL WRAPPER PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( val WITH CONDITIONAL ARRAY WRAPPER PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( val WITH UNCONDITIONAL WRAPPER PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( val WITH UNCONDITIONAL ARRAY WRAPPER PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( val WITH UNCONDITIONAL ARRAY WRAPPER PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( val PATH '$.pathTest' ERROR ON ERROR))", + "JSON_TABLE(document COLUMNS( val PATH '$.pathTest' NULL ON ERROR))", + "JSON_TABLE(document COLUMNS( val PATH '$.pathTest' EMPTY ON ERROR))", + "JSON_TABLE(document COLUMNS( val PATH '$.pathTest' EMPTY ARRAY ON ERROR))", + "JSON_TABLE(document COLUMNS( val PATH '$.pathTest' EMPTY OBJECT ON ERROR))", + "JSON_TABLE(document COLUMNS( val CLOB PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( val BLOB PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( val JSON PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( val VECTOR PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( val VARCHAR2 PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( val VARCHAR2(240) PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( val VARCHAR2(240) FORMAT JSON PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( val VARCHAR2(500 BYTE) PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( val VARCHAR2(100 CHAR) PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( val VARCHAR2(500 BYTE) FORMAT JSON DISALLOW SCALARS WITH UNCONDITIONAL ARRAY WRAPPER PATH '$.pathTest' EMPTY OBJECT ON ERROR))", + }) + void testQueryColumns(String jsonTableStr) throws JSQLParserException { + JsonTableFunction table = parseTable(jsonTableStr); + + assertThat(table.getColumnsClause().getColumnDefinitions()).hasSize(1); + } + +// @Test +// void testFormatJson() throws JSQLParserException { +// String expression = "JSON_TABLE(document FORMAT JSON COLUMNS( id FOR ORDINALITY))"; +// JsonTableFunction table = parseTable(expression); +// +// assertThat(table.isFormatJson()).isTrue(); +// } + + @Test + void testPathExpression() throws JSQLParserException { + String expression = "JSON_TABLE(document, '$.SubPath' COLUMNS( id FOR ORDINALITY))"; + JsonTableFunction table = parseTable(expression); + + assertThat(table.getJsonPathExpression()).isEqualTo("$.SubPath"); + } + + @Test + void testNullOnError() throws JSQLParserException { + String expression = "JSON_TABLE(document NULL ON ERROR COLUMNS( id FOR ORDINALITY))"; + JsonTableFunction table = parseTable(expression); + + assertThat(table.getOnErrorClause().getType()).isEqualTo(JsonTableFunction.JsonTableOnErrorType.NULL); + } + + @Test + void testErrorOnError() throws JSQLParserException { + String expression = "JSON_TABLE(document ERROR ON ERROR COLUMNS( id FOR ORDINALITY))"; + JsonTableFunction table = parseTable(expression); + + assertThat(table.getOnErrorClause().getType()).isEqualTo(JsonTableFunction.JsonTableOnErrorType.ERROR); + } + +// @Test +// void testNullOnEmpty() throws JSQLParserException { +// String expression = "JSON_TABLE(document NULL ON EMPTY COLUMNS( id FOR ORDINALITY))"; +// JsonTableFunction table = parseTable(expression); +// +// assertThat(table.getOnEmptyClause.getType()).isEqualTo(JsonOnEmptyType.NULL); +// } + +// @Test +// void testErrorOnEmpty() throws JSQLParserException { +// String expression = "JSON_TABLE(document ERROR ON EMPTY COLUMNS( id FOR ORDINALITY))"; +// JsonTable table = parseTable(expression); +// +// assertThat(table.getOnEmptyType()).isEqualTo(JsonOnEmptyType.ERROR); +// } + +// @Test +// void testTableTypeLax() throws JSQLParserException { +// String expression = "JSON_TABLE(document TYPE(LAX) COLUMNS( id FOR ORDINALITY))"; +// JsonTable table = parseTable(expression); +// +// assertThat(table.getType()).isEqualTo(JsonTableType.LAX); +// } + +// @Test +// void testTableTypeStrict() throws JSQLParserException { +// String expression = "JSON_TABLE(document TYPE(STRICT) COLUMNS( id FOR ORDINALITY))"; +// JsonTable table = parseTable(expression); +// +// assertThat(table.getType()).isEqualTo(JsonTableType.STRICT); +// } + +// @Test +// void testColumnTypeExists() throws JSQLParserException { +// String expression = "JSON_TABLE(document COLUMNS( hasValue EXISTS PATH '$.pathTest'))"; +// JsonTable table = parseTable(expression); +// +// assertThat(table.getColumns()).hasSize(1); +// +// JsonTableColumn col = table.getColumns().get(0); +// assertThat(col.getType()).isEqualTo(JsonTableColumnType.JSON_EXISTS); +// } + +// @Test +// void testBuilder() { +// Column c = new Column("document"); +// +// JsonTable table = new JsonTable().withExpression(c) +// .withPathExpression("$.subPath") +// .withFormatJson(true) +// .withType(JsonTableType.STRICT) +// .withOnEmptyType(JsonOnEmptyType.NULL) +// .withOnErrorType(JsonOnErrorType.ERROR) +// .withColumn(new JsonTableColumn().withName("id") +// .withType(JsonTableColumnType.ORDINALITY)); +// +// assertThat(table.toString()).isEqualTo( +// "JSON_TABLE(document FORMAT JSON, '$.subPath' ERROR ON ERROR TYPE(STRICT) NULL ON EMPTY COLUMNS(id FOR ORDINALITY))"); +// } +// +// @Test +// void testValidSetters() { +// JsonTable table = new JsonTable(); +// +// assertThatNoException().isThrownBy(() -> { +// table.setOnEmptyType(null); +// table.setOnEmptyType(JsonOnEmptyType.NULL); +// table.setOnEmptyType(JsonOnEmptyType.ERROR); +// +// table.setOnErrorType(null); +// table.setOnErrorType(JsonOnErrorType.NULL); +// table.setOnErrorType(JsonOnErrorType.ERROR); +// +// table.setType(null); +// table.setType(JsonTableType.LAX); +// table.setType(JsonTableType.STRICT); +// }); +// } +// +// @Test +// void testInvalidSetters() { +// JsonTable table = new JsonTable(); +// +// assertThatThrownBy(() -> table.setOnEmptyType(JsonOnEmptyType.EMPTY)) +// .isInstanceOf(IllegalArgumentException.class); +// assertThatThrownBy(() -> table.setOnEmptyType(JsonOnEmptyType.EMPTY_ARRAY)) +// .isInstanceOf(IllegalArgumentException.class); +// assertThatThrownBy(() -> table.setOnEmptyType(JsonOnEmptyType.EMPTY_OBJECT)) +// .isInstanceOf(IllegalArgumentException.class); +// assertThatThrownBy(() -> table.setOnEmptyType(JsonOnEmptyType.FALSE)) +// .isInstanceOf(IllegalArgumentException.class); +// assertThatThrownBy(() -> table.setOnEmptyType(JsonOnEmptyType.TRUE)) +// .isInstanceOf(IllegalArgumentException.class); +// assertThatThrownBy(() -> table.setOnEmptyType(JsonOnEmptyType.DEFAULT)) +// .isInstanceOf(IllegalArgumentException.class); +// +// assertThatThrownBy(() -> table.setOnErrorType(JsonOnErrorType.EMPTY)) +// .isInstanceOf(IllegalArgumentException.class); +// assertThatThrownBy(() -> table.setOnErrorType(JsonOnErrorType.EMPTY_ARRAY)) +// .isInstanceOf(IllegalArgumentException.class); +// assertThatThrownBy(() -> table.setOnErrorType(JsonOnErrorType.EMPTY_OBJECT)) +// .isInstanceOf(IllegalArgumentException.class); +// assertThatThrownBy(() -> table.setOnErrorType(JsonOnErrorType.FALSE)) +// .isInstanceOf(IllegalArgumentException.class); +// assertThatThrownBy(() -> table.setOnErrorType(JsonOnErrorType.TRUE)) +// .isInstanceOf(IllegalArgumentException.class); +// assertThatThrownBy(() -> table.setOnErrorType(JsonOnErrorType.DEFAULT)) +// .isInstanceOf(IllegalArgumentException.class); +// +// JsonTableColumn column = new JsonTableColumn(); +// +// assertThatThrownBy(() -> { +// column.setType(JsonTableColumnType.JSON_EXISTS); +// column.setFormatJson(true); +// }).isInstanceOf(IllegalArgumentException.class); +// assertThatThrownBy(() -> { +// column.setType(JsonTableColumnType.JSON_VALUE); +// column.setFormatJson(true); +// }).isInstanceOf(IllegalArgumentException.class); +// assertThatThrownBy(() -> { +// column.setType(JsonTableColumnType.ORDINALITY); +// column.setFormatJson(true); +// }).isInstanceOf(IllegalArgumentException.class); +// assertThatThrownBy(() -> { +// column.setType(JsonTableColumnType.JSON_NESTED_PATH); +// column.setFormatJson(true); +// }).isInstanceOf(IllegalArgumentException.class); +// } + + private JsonTableFunction parseTable(String jsonTableStr) throws JSQLParserException { + String sql = "SELECT * FROM " + jsonTableStr; + Statement stmt = CCJSqlParserUtil.parse(sql); + + TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true); + + FromItem fromItem = ((PlainSelect) stmt).getFromItem(); + assertThat(fromItem).isInstanceOf(TableFunction.class); + Function function = ((TableFunction) fromItem).getFunction(); + assertThat(function).isInstanceOf(JsonTableFunction.class); + + return (JsonTableFunction) function; + } + + +} diff --git a/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java index 1180417fb..44a27624c 100644 --- a/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java +++ b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java @@ -759,4 +759,16 @@ void testNestedTablesInJsonObject() throws JSQLParserException { assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("table1", "table2", "table3"); } + + @Test + void testJsonTable() throws JSQLParserException { + String sqlStr = "SELECT * FROM JSON_TABLE(" + + "(SELECT json_column FROM table_with_json), '$.jsonPath' COLUMNS( id FOR ORDINALITY ))"; + + Set tables = TablesNamesFinder.findTables(sqlStr); + + assertThat(tables).containsExactly("table_with_json"); + + } + }