diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java index aee8e7bf3..3cc409873 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,7 +105,8 @@ public StringBuilder append(StringBuilder builder) { builder.append("UNKNOWN"); break; default: - // this should never happen + 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..5ee166e6f 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonTableFunction.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonTableFunction.java @@ -18,6 +18,18 @@ import net.sf.jsqlparser.statement.create.table.ColDataType; public class JsonTableFunction extends Function { + + private Expression jsonInputExpression; + private Expression jsonPathExpression; + private String pathName; + private final List passingClauses = new ArrayList<>(); + private JsonTableColumnsClause columnsClause; + private JsonTablePlanClause planClause; + private JsonTableOnErrorClause onErrorClause; + private JsonTableParsingTypeClause parsingTypeClause; + private JsonTableOnEmptyClause onEmptyClause; + private boolean formatJson; + public enum JsonTablePlanOperator { COMMA(", "), INNER(" INNER "), OUTER(" OUTER "), CROSS(" CROSS "), UNION(" UNION "); @@ -33,7 +45,15 @@ public String getDisplay() { } public enum JsonTableOnErrorType { - ERROR, EMPTY + ERROR, NULL, EMPTY, TRUE, FALSE + } + + public enum JsonTableOnEmptyType { + ERROR, NULL, EMPTY, TRUE, FALSE + } + + public enum JsonTableParsingType { + STRICT, LAX } public static class JsonTablePassingClause extends ASTNodeAccessImpl implements Serializable { @@ -78,10 +98,30 @@ public String toString() { } public static class JsonTableWrapperClause extends ASTNodeAccessImpl implements Serializable { + private boolean beforePathExpression; private JsonFunction.JsonWrapperType wrapperType; private JsonFunction.JsonWrapperMode wrapperMode; private boolean array; + /** + * Creates a wrapper clause. Depending on the dialect, this clause can come before or after + * the PATH expression. + * + * + * @param beforePathExpression A flag to determine wether the clause is rendered before or + * after the PATH expression + */ + public JsonTableWrapperClause(boolean beforePathExpression) { + this.beforePathExpression = beforePathExpression; + } + + public boolean isBeforePathExpression() { + return beforePathExpression; + } + public JsonFunction.JsonWrapperType getWrapperType() { return wrapperType; } @@ -159,6 +199,15 @@ public String toString() { public static class JsonTableOnErrorClause extends ASTNodeAccessImpl implements Serializable { private JsonTableOnErrorType type; + private boolean beforeColumns = true; + + public JsonTableOnErrorClause(boolean beforeColumns) { + this.beforeColumns = beforeColumns; + } + + public boolean isBeforeColumns() { + return beforeColumns; + } public JsonTableOnErrorType getType() { return type; @@ -175,6 +224,43 @@ public String toString() { } } + public static class JsonTableOnEmptyClause extends ASTNodeAccessImpl implements Serializable { + private JsonTableOnEmptyType type; + + public JsonTableOnEmptyType getType() { + return type; + } + + public JsonTableOnEmptyClause setType(JsonTableOnEmptyType type) { + this.type = type; + return this; + } + + @Override + public String toString() { + return type + " ON EMPTY"; + } + } + + public static class JsonTableParsingTypeClause extends ASTNodeAccessImpl + implements Serializable { + private JsonTableParsingType type; + + public JsonTableParsingType getType() { + return type; + } + + public JsonTableParsingTypeClause setType(JsonTableParsingType type) { + this.type = type; + return this; + } + + @Override + public String toString() { + return "TYPE(" + type + ")"; + } + } + public static class JsonTablePlanTerm extends ASTNodeAccessImpl implements Serializable { private JsonTablePlanExpression nestedPlanExpression; private String name; @@ -389,12 +475,15 @@ public static class JsonTableValueColumnDefinition extends JsonTableColumnDefini private boolean forOrdinality; private ColDataType dataType; private boolean formatJson; + private boolean exists; + private boolean onEmptyAfterOnError; private String encoding; private Expression pathExpression; private JsonTableWrapperClause wrapperClause; private JsonTableQuotesClause quotesClause; private JsonFunction.JsonOnResponseBehavior onEmptyBehavior; private JsonFunction.JsonOnResponseBehavior onErrorBehavior; + private JsonFunction.ScalarsType scalarsType; public String getColumnName() { return columnName; @@ -405,6 +494,20 @@ public JsonTableValueColumnDefinition setColumnName(String columnName) { return this; } + public boolean isExists() { + return exists; + } + + public JsonTableValueColumnDefinition setExistsKeyword(boolean exists) { + this.exists = exists; + return this; + } + + public JsonTableValueColumnDefinition setOnEmptyAfterOnError(boolean b) { + this.onEmptyAfterOnError = b; + return this; + } + public boolean isForOrdinality() { return forOrdinality; } @@ -489,6 +592,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,29 +620,44 @@ public String toString() { builder.append(" FOR ORDINALITY"); return builder.toString(); } - - builder.append(" ").append(dataType); + if (exists) { + builder.append(" EXISTS"); + } + 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 (wrapperClause != null && wrapperClause.isBeforePathExpression()) { + builder.append(" ").append(wrapperClause); + } if (pathExpression != null) { builder.append(" PATH ").append(pathExpression); } - if (wrapperClause != null) { + if (wrapperClause != null && !wrapperClause.isBeforePathExpression()) { builder.append(" ").append(wrapperClause); } if (quotesClause != null) { builder.append(" ").append(quotesClause); } - if (onEmptyBehavior != null) { + if (onEmptyBehavior != null && !onEmptyAfterOnError) { builder.append(" ").append(onEmptyBehavior).append(" ON EMPTY"); } if (onErrorBehavior != null) { builder.append(" ").append(onErrorBehavior).append(" ON ERROR"); } + if (onEmptyBehavior != null && onEmptyAfterOnError) { + builder.append(" ").append(onEmptyBehavior).append(" ON EMPTY"); + } return builder.toString(); } } @@ -573,18 +699,19 @@ public String 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 boolean getFormatJson() { + return formatJson; + } + + public JsonTableFunction setFormatJson(boolean formatJson) { + this.formatJson = formatJson; + return this; + } + public Expression getJsonInputExpression() { return jsonInputExpression; } @@ -648,6 +775,24 @@ public JsonTableFunction setOnErrorClause(JsonTableOnErrorClause onErrorClause) return this; } + public JsonTableParsingTypeClause getParsingTypeClause() { + return parsingTypeClause; + } + + public JsonTableFunction setParsingTypeClause(JsonTableParsingTypeClause parsingTypeClause) { + this.parsingTypeClause = parsingTypeClause; + return this; + } + + public JsonTableOnEmptyClause getOnEmptyClause() { + return onEmptyClause; + } + + public JsonTableFunction setOnEmptyClause(JsonTableOnEmptyClause onEmptyClause) { + this.onEmptyClause = onEmptyClause; + return this; + } + public List getAllExpressions() { List expressions = new ArrayList<>(); if (jsonInputExpression != null) { @@ -676,7 +821,13 @@ 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 (formatJson) { + builder.append(" FORMAT JSON"); + } + if (jsonPathExpression != null) { + builder.append(", ").append(jsonPathExpression); + } if (pathName != null) { builder.append(" AS ").append(pathName); } @@ -691,11 +842,20 @@ public String toString() { first = false; } } + if (onErrorClause != null && onErrorClause.isBeforeColumns()) { + builder.append(" ").append(onErrorClause); + } + if (parsingTypeClause != null) { + builder.append(" ").append(parsingTypeClause); + } + if (onEmptyClause != null) { + builder.append(" ").append(onEmptyClause); + } builder.append(" ").append(columnsClause); if (planClause != null) { builder.append(" ").append(planClause); } - if (onErrorClause != null) { + if (onErrorClause != null && !onErrorClause.isBeforeColumns()) { builder.append(" ").append(onErrorClause); } builder.append(")"); 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..6b1048031 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitor.java @@ -104,4 +104,5 @@ default void visit(Import imprt) { } T visit(FromQuery fromQuery, 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..23bd480b8 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java @@ -148,4 +148,5 @@ public T visit(Import imprt, S context) { public T visit(FromQuery fromQuery, S context) { return fromQuery.accept(selectVisitor, context); } + } 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 bbe176f3e..d680e9faa 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 @@ -449,4 +449,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 fbfd80c3e..86e7971c2 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -892,6 +892,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1330,6 +1331,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" > @@ -8310,6 +8312,12 @@ JsonFunction.JsonOnResponseBehavior JsonValueOnResponseBehavior() : { JsonFunction.JsonOnResponseBehaviorType.NULL); } | + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.EMPTY); + } + | expression = Expression() { behavior = new JsonFunction.JsonOnResponseBehavior( @@ -8339,26 +8347,38 @@ 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.TRUE); } - ( - - { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.EMPTY_ARRAY); - } - | - JsonKeyword("OBJECT") - { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.EMPTY_OBJECT); - } - ) + | + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.FALSE); + } + | + + { + 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); + } + ) + ] ) { if (behavior != null) { @@ -8455,9 +8475,9 @@ JsonFunction JsonValueBody() : { [ dataType = ColDataType() { result.setReturningType(dataType); } ] [ - LOOKAHEAD( JsonValueOnResponseBehavior() ) + LOOKAHEAD( JsonValueOnResponseBehavior() ) behavior = JsonValueOnResponseBehavior() - JsonKeyword("EMPTY") + { result.setOnEmptyBehavior(behavior); } ] @@ -8561,9 +8581,9 @@ JsonFunction JsonQueryBody() : { ] [ - LOOKAHEAD( JsonQueryOnResponseBehavior() ) + LOOKAHEAD( JsonQueryOnResponseBehavior() ) behavior = JsonQueryOnResponseBehavior() - JsonKeyword("EMPTY") + { result.setOnEmptyBehavior(behavior); } ] @@ -8642,9 +8662,9 @@ JsonFunction JsonQueryBody() : { ] ] [ - LOOKAHEAD( JsonQueryOnResponseBehavior() ) + LOOKAHEAD( JsonQueryOnResponseBehavior() ) additionalOnEmptyBehavior = JsonQueryOnResponseBehavior() - JsonKeyword("EMPTY") + ] [ LOOKAHEAD( JsonQueryOnResponseBehavior() ) @@ -9521,6 +9541,18 @@ JsonFunction.JsonOnResponseBehavior JsonTableOnEmptyBehavior() : { JsonFunction.JsonOnResponseBehaviorType.NULL); } | + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.TRUE); + } + | + + { + behavior = new JsonFunction.JsonOnResponseBehavior( + JsonFunction.JsonOnResponseBehaviorType.FALSE); + } + | expression = Expression() { behavior = new JsonFunction.JsonOnResponseBehavior( @@ -9556,9 +9588,9 @@ JsonFunction.JsonOnResponseBehavior JsonTableOnEmptyBehavior() : { } } -JsonTableFunction.JsonTableWrapperClause JsonTableWrapperClause() : { +JsonTableFunction.JsonTableWrapperClause JsonTableWrapperClause(boolean beforePathExpr) : { JsonTableFunction.JsonTableWrapperClause wrapperClause = - new JsonTableFunction.JsonTableWrapperClause(); + new JsonTableFunction.JsonTableWrapperClause(beforePathExpr); Token token; } { @@ -9645,16 +9677,39 @@ JsonTableFunction.JsonTableColumnDefinition JsonTableColumnDefinition() : { columnDefinition = valueColumnDefinition; } ( - JsonKeyword("ORDINALITY") - { valueColumnDefinition.setForOrdinality(true); } + { valueColumnDefinition.setForOrdinality(true); } | - dataType = ColDataType() { valueColumnDefinition.setDataType(dataType); } + [ + // Very ugly: ColDataType can consume an IDENTIFIER, which is fine, but we don't want it to + // consume an ALLOW or DISALLOW because that's a keyword for Oracle in this place. + // So we make a LOOKAHEAD on ColDataType, BUT we exclude the two cases for the IDENTIFIER + LOOKAHEAD( + ColDataType(), + { !(getToken(1).kind == S_IDENTIFIER && ( + getToken(1).image.equalsIgnoreCase("ALLOW") + || getToken(1).image.equalsIgnoreCase("DISALLOW"))) } ) + dataType = ColDataType() { valueColumnDefinition.setDataType(dataType); } + ] [ { valueColumnDefinition.setFormatJson(true); } [ encoding = JsonEncoding() { valueColumnDefinition.setEncoding(encoding); } ] ] + [ + ( + LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("ALLOW") }) + JsonKeyword("ALLOW") { valueColumnDefinition.setScalarsType(JsonFunction.ScalarsType.ALLOW); } + | + LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("DISALLOW") }) + JsonKeyword("DISALLOW") { valueColumnDefinition.setScalarsType(JsonFunction.ScalarsType.DISALLOW); } + ) + JsonKeyword("SCALARS") + ] + [ { valueColumnDefinition.setExistsKeyword(true); } ] + // In Oracle, the wrapper clause comes before the PATH expression + [ wrapperClause = JsonTableWrapperClause(true) { valueColumnDefinition.setWrapperClause(wrapperClause); } ] [ expression = Expression() { valueColumnDefinition.setPathExpression(expression); } ] - [ wrapperClause = JsonTableWrapperClause() { valueColumnDefinition.setWrapperClause(wrapperClause); } ] + // In Truno the wrapper clause comes after the PATH expression + [ wrapperClause = JsonTableWrapperClause(false) { valueColumnDefinition.setWrapperClause(wrapperClause); } ] [ LOOKAHEAD({ getToken(1).kind == K_KEEP @@ -9664,17 +9719,26 @@ 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); } ] + [ + LOOKAHEAD( JsonTableOnEmptyBehavior() ) + behavior = JsonTableOnEmptyBehavior() + + { + valueColumnDefinition.setOnEmptyBehavior(behavior); + valueColumnDefinition.setOnEmptyAfterOnError(true); + } + ] ) ) { @@ -9787,23 +9851,22 @@ JsonTableFunction.JsonTablePlanClause JsonTablePlanClause() : { } } -JsonTableFunction.JsonTableOnErrorClause JsonTableOnErrorClause() : { +JsonTableFunction.JsonTableOnErrorClause JsonTableOnErrorClause(boolean beforeColumns) : { JsonTableFunction.JsonTableOnErrorClause onErrorClause = - new JsonTableFunction.JsonTableOnErrorClause(); + new JsonTableFunction.JsonTableOnErrorClause(beforeColumns); 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); - } + { onErrorClause.setType(JsonTableFunction.JsonTableOnErrorType.EMPTY); } + | + { onErrorClause.setType(JsonTableFunction.JsonTableOnErrorType.TRUE); } + | + { onErrorClause.setType(JsonTableFunction.JsonTableOnErrorType.FALSE); } + | + { onErrorClause.setType(JsonTableFunction.JsonTableOnErrorType.NULL); } ) { @@ -9813,6 +9876,49 @@ JsonTableFunction.JsonTableOnErrorClause JsonTableOnErrorClause() : { } } +JsonTableFunction.JsonTableOnEmptyClause JsonTableOnEmptyClause() : { + JsonTableFunction.JsonTableOnEmptyClause onEmptyClause = + new JsonTableFunction.JsonTableOnEmptyClause(); + Token token; +} +{ + ( + { onEmptyClause.setType(JsonTableFunction.JsonTableOnEmptyType.ERROR); } + | + { onEmptyClause.setType(JsonTableFunction.JsonTableOnEmptyType.EMPTY); } + | + { onEmptyClause.setType(JsonTableFunction.JsonTableOnEmptyType.TRUE); } + | + { onEmptyClause.setType(JsonTableFunction.JsonTableOnEmptyType.FALSE); } + | + { onEmptyClause.setType(JsonTableFunction.JsonTableOnEmptyType.NULL); } + ) + + { + if (onEmptyClause.getType() != null) { + return onEmptyClause; + } + } +} + +JsonTableFunction.JsonTableParsingTypeClause JsonTableParsingTypeClause() : { + JsonTableFunction.JsonTableParsingTypeClause parsingType = new JsonTableFunction.JsonTableParsingTypeClause(); +} +{ + + + ( + { parsingType.setType(JsonTableFunction.JsonTableParsingType.STRICT); } + | + LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("LAX") }) + JsonKeyword("LAX") { parsingType.setType(JsonTableFunction.JsonTableParsingType.LAX); } + ) + + { + return parsingType; + } +} + JsonTableFunction JsonTableBody() : { JsonTableFunction function = new JsonTableFunction(); Expression jsonInput; @@ -9822,18 +9928,23 @@ JsonTableFunction JsonTableBody() : { JsonTableFunction.JsonTableColumnsClause columnsClause; JsonTableFunction.JsonTablePlanClause planClause = null; JsonTableFunction.JsonTableOnErrorClause onErrorClause = null; + JsonTableFunction.JsonTableParsingTypeClause parsingTypeClause = null; + JsonTableFunction.JsonTableOnEmptyClause onEmptyClause = null; } { "(" jsonInput = Expression() { function.setJsonInputExpression(jsonInput); } - "," - jsonPath = Expression() { - function.setJsonPathExpression(jsonPath); - function.setParameters(new ExpressionList(jsonInput, jsonPath)); - } - [ pathName = RelObjectName() { function.setPathName(pathName); } ] + [ { function.setFormatJson(true); } ] + [ + "," + 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") @@ -9843,9 +9954,12 @@ JsonTableFunction JsonTableBody() : { passingClause = JsonTablePassingClause() { function.addPassingClause(passingClause); } )* ] + [ LOOKAHEAD(3) onErrorClause = JsonTableOnErrorClause(true) { function.setOnErrorClause(onErrorClause); } ] + [ parsingTypeClause = JsonTableParsingTypeClause() { function.setParsingTypeClause(parsingTypeClause); } ] + [ onEmptyClause = JsonTableOnEmptyClause() { function.setOnEmptyClause(onEmptyClause); } ] columnsClause = JsonTableColumnsClause() { function.setColumnsClause(columnsClause); } [ planClause = JsonTablePlanClause() { function.setPlanClause(planClause); } ] - [ onErrorClause = JsonTableOnErrorClause() { function.setOnErrorClause(onErrorClause); } ] + [ onErrorClause = JsonTableOnErrorClause(false) { function.setOnErrorClause(onErrorClause); } ] ")" { return function; @@ -10346,6 +10460,7 @@ ColDataType DataType(): ] [ LOOKAHEAD(2) "(" ( tk= { precision = Integer.valueOf(tk.image); } | tk= { precision = Integer.MAX_VALUE; } ) + [ | ] [ "," tk = { scale = Integer.valueOf(tk.image); } ] ")" ] diff --git a/src/test/java/net/sf/jsqlparser/expression/JsonTableOracleTest.java b/src/test/java/net/sf/jsqlparser/expression/JsonTableOracleTest.java new file mode 100644 index 000000000..14875413f --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/JsonTableOracleTest.java @@ -0,0 +1,214 @@ +package net.sf.jsqlparser.expression; + +import net.sf.jsqlparser.JSQLParserException; +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 JsonTableOracleTest { + + @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 PATH '$.pathTest' TRUE ON ERROR))", + "JSON_TABLE(document COLUMNS( hasValue EXISTS PATH '$.pathTest' TRUE ON EMPTY))", + "JSON_TABLE(document COLUMNS( hasValue EXISTS PATH '$.pathTest' FALSE ON ERROR))", + "JSON_TABLE(document COLUMNS( hasValue EXISTS PATH '$.pathTest' FALSE 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 VARCHAR(240) ALLOW SCALARS PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( val INT 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 VARCHAR PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( val VARCHAR(240) PATH '$.pathTest'))", + "JSON_TABLE(document COLUMNS( val VARCHAR(240) FORMAT JSON PATH '$.pathTest'))", + + // These would require adapting ColDataType in Line 10176 + // "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 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.getFormatJson()).isTrue(); + } + + @Test + void testPathExpression() throws JSQLParserException { + String expression = "JSON_TABLE(document, '$.SubPath' COLUMNS( id FOR ORDINALITY))"; + JsonTableFunction table = parseTable(expression); + + assertThat(table.getJsonPathExpression().toString()).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(JsonTableFunction.JsonTableOnEmptyType.NULL); + } + + @Test + void testErrorOnEmpty() throws JSQLParserException { + String expression = "JSON_TABLE(document ERROR ON EMPTY COLUMNS( id FOR ORDINALITY))"; + JsonTableFunction table = parseTable(expression); + + assertThat(table.getOnEmptyClause().getType()) + .isEqualTo(JsonTableFunction.JsonTableOnEmptyType.ERROR); + } + + @Test + void testParsingTypeLax() throws JSQLParserException { + String expression = "JSON_TABLE(document TYPE(LAX) COLUMNS( id FOR ORDINALITY))"; + JsonTableFunction table = parseTable(expression); + + assertThat(table.getParsingTypeClause().getType()) + .isEqualTo(JsonTableFunction.JsonTableParsingType.LAX); + } + + @Test + void testParsingTypeStrict() throws JSQLParserException { + String expression = "JSON_TABLE(document TYPE(STRICT) COLUMNS( id FOR ORDINALITY))"; + JsonTableFunction table = parseTable(expression); + + assertThat(table.getParsingTypeClause().getType()) + .isEqualTo(JsonTableFunction.JsonTableParsingType.STRICT); + } + + @Test + void testColumnTypeExists() throws JSQLParserException { + String expression = "JSON_TABLE(document COLUMNS( hasValue EXISTS PATH '$.pathTest'))"; + JsonTableFunction table = parseTable(expression); + + assertThat(table.getColumnsClause().getColumnDefinitions()).hasSize(1); + + JsonTableFunction.JsonTableColumnDefinition col = + table.getColumnsClause().getColumnDefinitions().get(0); + assertThat(col).isInstanceOf(JsonTableFunction.JsonTableValueColumnDefinition.class); + + JsonTableFunction.JsonTableValueColumnDefinition valueCol = + (JsonTableFunction.JsonTableValueColumnDefinition) col; + + assertThat(valueCol.isExists()).isTrue(); + } + + 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"); + + } + }