diff --git a/build.gradle b/build.gradle
index 843119600..e566a0a10 100644
--- a/build.gradle
+++ b/build.gradle
@@ -131,7 +131,14 @@ configurations.configureEach {
}
compileJavacc {
- arguments = [grammar_encoding: 'UTF-8', static: 'false', java_template_type: 'modern']
+ arguments = [
+ grammar_encoding: 'UTF-8',
+ static: 'false',
+ java_template_type: 'modern',
+ // Comment this in to build the parser with tracing.
+ DEBUG_PARSER: 'false',
+ DEBUG_LOOKAHEAD: 'false'
+ ]
}
java {
diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java
index 4422c1beb..176759c6d 100644
--- a/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java
+++ b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java
@@ -15,9 +15,15 @@
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
/**
+ * Represents a JSON-Function.
+ * Currently supported are the types in {@link JsonFunctionType}.
+ *
+ * For JSON_OBJECT the parameters are available from {@link #getKeyValuePairs()}
+ *
+ * For JSON_ARRAY the parameters are availble from {@link #getExpressions()}.
+ *
* @author Andreas Reichel
*/
-
public class JsonFunction extends ASTNodeAccessImpl implements Expression {
private final ArrayList keyValuePairs = new ArrayList<>();
private final ArrayList expressions = new ArrayList<>();
@@ -25,10 +31,31 @@ public class JsonFunction extends ASTNodeAccessImpl implements Expression {
private JsonAggregateOnNullType onNullType;
private JsonAggregateUniqueKeysType uniqueKeysType;
+ private boolean isStrict = false;
+
+ public JsonFunction() {}
+
+ public JsonFunction(JsonFunctionType functionType) {
+ this.functionType = functionType;
+ }
+
+ /**
+ * Returns the Parameters of an JSON_OBJECT
+ * The KeyValuePairs may not have both key and value set, in some cases only the Key is set.
+ *
+ * @see net.sf.jsqlparser.parser.feature.Feature#allowCommaAsKeyValueSeparator
+ *
+ * @return A List of KeyValuePairs, never NULL
+ */
public ArrayList getKeyValuePairs() {
return keyValuePairs;
}
+ /**
+ * Returns the parameters of JSON_ARRAY
+ *
+ * @return A List of {@link JsonFunctionExpression}s, never NULL
+ */
public ArrayList getExpressions() {
return expressions;
}
@@ -114,6 +141,19 @@ public JsonFunction withType(String typeName) {
return this;
}
+ public boolean isStrict() {
+ return isStrict;
+ }
+
+ public void setStrict(boolean strict) {
+ isStrict = strict;
+ }
+
+ public JsonFunction withStrict(boolean strict) {
+ this.setStrict(strict);
+ return this;
+ }
+
@Override
public T accept(ExpressionVisitor expressionVisitor, S context) {
return expressionVisitor.visit(this, context);
@@ -123,13 +163,9 @@ public T accept(ExpressionVisitor expressionVisitor, S context) {
public StringBuilder append(StringBuilder builder) {
switch (functionType) {
case OBJECT:
- appendObject(builder);
- break;
case POSTGRES_OBJECT:
- appendPostgresObject(builder);
- break;
case MYSQL_OBJECT:
- appendMySqlObject(builder);
+ appendObject(builder);
break;
case ARRAY:
appendArray(builder);
@@ -148,35 +184,37 @@ public StringBuilder appendObject(StringBuilder builder) {
if (i > 0) {
builder.append(", ");
}
- if (keyValuePair.isUsingValueKeyword()) {
- if (keyValuePair.isUsingKeyKeyword()) {
- builder.append("KEY ");
- }
- builder.append(keyValuePair.getKey()).append(" VALUE ")
- .append(keyValuePair.getValue());
- } else {
- builder.append(keyValuePair.getKey()).append(":").append(keyValuePair.getValue());
- }
-
- if (keyValuePair.isUsingFormatJson()) {
- builder.append(" FORMAT JSON");
- }
+ keyValuePair.append(builder);
i++;
}
+ appendOnNullType(builder);
+ if (isStrict) {
+ builder.append(" STRICT");
+ }
+ appendUniqueKeys(builder);
+
+ builder.append(" ) ");
+
+ return builder;
+ }
+
+ private void appendOnNullType(StringBuilder builder) {
if (onNullType != null) {
switch (onNullType) {
case NULL:
builder.append(" NULL ON NULL");
break;
case ABSENT:
- builder.append(" ABSENT On NULL");
+ builder.append(" ABSENT ON NULL");
break;
default:
// this should never happen
}
}
+ }
+ private void appendUniqueKeys(StringBuilder builder) {
if (uniqueKeysType != null) {
switch (uniqueKeysType) {
case WITH:
@@ -189,41 +227,6 @@ public StringBuilder appendObject(StringBuilder builder) {
// this should never happen
}
}
-
- builder.append(" ) ");
-
- return builder;
- }
-
-
- @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"})
- public StringBuilder appendPostgresObject(StringBuilder builder) {
- builder.append("JSON_OBJECT( ");
- for (JsonKeyValuePair keyValuePair : keyValuePairs) {
- builder.append(keyValuePair.getKey());
- if (keyValuePair.getValue() != null) {
- builder.append(", ").append(keyValuePair.getValue());
- }
- }
- builder.append(" ) ");
-
- return builder;
- }
-
- public StringBuilder appendMySqlObject(StringBuilder builder) {
- builder.append("JSON_OBJECT( ");
- int i = 0;
- for (JsonKeyValuePair keyValuePair : keyValuePairs) {
- if (i > 0) {
- builder.append(", ");
- }
- builder.append(keyValuePair.getKey());
- builder.append(", ").append(keyValuePair.getValue());
- i++;
- }
- builder.append(" ) ");
-
- return builder;
}
@SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"})
@@ -239,18 +242,7 @@ public StringBuilder appendArray(StringBuilder builder) {
i++;
}
- if (onNullType != null) {
- switch (onNullType) {
- case NULL:
- builder.append(" NULL ON NULL ");
- break;
- case ABSENT:
- builder.append(" ABSENT ON NULL ");
- break;
- default:
- // "ON NULL" was omitted
- }
- }
+ appendOnNullType(builder);
builder.append(") ");
return builder;
diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonFunctionType.java b/src/main/java/net/sf/jsqlparser/expression/JsonFunctionType.java
index 43a33aab6..821416c9c 100644
--- a/src/main/java/net/sf/jsqlparser/expression/JsonFunctionType.java
+++ b/src/main/java/net/sf/jsqlparser/expression/JsonFunctionType.java
@@ -14,7 +14,19 @@
* @author Andreas Reichel
*/
public enum JsonFunctionType {
- OBJECT, ARRAY, POSTGRES_OBJECT, MYSQL_OBJECT;
+ OBJECT, ARRAY,
+
+ /**
+ * Not used anymore
+ */
+ @Deprecated
+ POSTGRES_OBJECT,
+
+ /**
+ * Not used anymore
+ */
+ @Deprecated
+ MYSQL_OBJECT;
public static JsonFunctionType from(String type) {
return Enum.valueOf(JsonFunctionType.class, type.toUpperCase());
diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java b/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java
index 82c8a355a..f8d43aa97 100644
--- a/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java
+++ b/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java
@@ -20,16 +20,27 @@
public class JsonKeyValuePair implements Serializable {
private final Object key;
private final Object value;
- private boolean usingKeyKeyword = false;
- private boolean usingValueKeyword = false;
+ private boolean usingKeyKeyword;
+ private JsonKeyValuePairSeparator separator;
private boolean usingFormatJson = false;
+ /**
+ * Please use the Constructor with {@link JsonKeyValuePairSeparator} parameter.
+ */
+ @Deprecated
public JsonKeyValuePair(Object key, Object value, boolean usingKeyKeyword,
boolean usingValueKeyword) {
+ this(key, value, usingKeyKeyword, usingValueKeyword ? JsonKeyValuePairSeparator.VALUE
+ : JsonKeyValuePairSeparator.COLON);
+ }
+
+ public JsonKeyValuePair(Object key, Object value, boolean usingKeyKeyword,
+ JsonKeyValuePairSeparator separator) {
this.key = Objects.requireNonNull(key, "The KEY of the Pair must not be null");
this.value = value;
this.usingKeyKeyword = usingKeyKeyword;
- this.usingValueKeyword = usingValueKeyword;
+ this.separator =
+ Objects.requireNonNull(separator, "The KeyValuePairSeparator must not be NULL");
}
public boolean isUsingKeyKeyword() {
@@ -45,19 +56,45 @@ public JsonKeyValuePair withUsingKeyKeyword(boolean usingKeyKeyword) {
return this;
}
+ /**
+ * Use {@link #getSeparator()}
+ */
+ @Deprecated
public boolean isUsingValueKeyword() {
- return usingValueKeyword;
+ return separator == JsonKeyValuePairSeparator.VALUE;
}
+ /**
+ * Use {@link #setSeparator(JsonKeyValuePairSeparator)}
+ */
+ @Deprecated
public void setUsingValueKeyword(boolean usingValueKeyword) {
- this.usingValueKeyword = usingValueKeyword;
+ separator = usingValueKeyword ? JsonKeyValuePairSeparator.VALUE
+ : JsonKeyValuePairSeparator.COLON;
}
+ /**
+ * Use {@link #withSeparator(JsonKeyValuePairSeparator)}
+ */
+ @Deprecated
public JsonKeyValuePair withUsingValueKeyword(boolean usingValueKeyword) {
this.setUsingValueKeyword(usingValueKeyword);
return this;
}
+ public JsonKeyValuePairSeparator getSeparator() {
+ return separator;
+ }
+
+ public void setSeparator(JsonKeyValuePairSeparator separator) {
+ this.separator = separator;
+ }
+
+ public JsonKeyValuePair withSeparator(JsonKeyValuePairSeparator separator) {
+ this.setSeparator(separator);
+ return this;
+ }
+
public boolean isUsingFormatJson() {
return usingFormatJson;
}
@@ -102,13 +139,14 @@ public Object getValue() {
}
public StringBuilder append(StringBuilder builder) {
- if (isUsingValueKeyword()) {
- if (isUsingKeyKeyword()) {
- builder.append("KEY ");
- }
- builder.append(getKey()).append(" VALUE ").append(getValue());
- } else {
- builder.append(getKey()).append(":").append(getValue());
+ if (isUsingKeyKeyword() && getSeparator() == JsonKeyValuePairSeparator.VALUE) {
+ builder.append("KEY ");
+ }
+ builder.append(getKey());
+
+ if (getValue() != null) {
+ builder.append(getSeparator().getSeparatorString());
+ builder.append(getValue());
}
if (isUsingFormatJson()) {
diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePairSeparator.java b/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePairSeparator.java
new file mode 100644
index 000000000..aa0e599a4
--- /dev/null
+++ b/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePairSeparator.java
@@ -0,0 +1,24 @@
+package net.sf.jsqlparser.expression;
+
+/**
+ * Describes the string used to separate the key from the value.
+ */
+public enum JsonKeyValuePairSeparator {
+ VALUE(" VALUE "), COLON(":"),
+
+ // Used in MySQL dialect
+ COMMA(","),
+
+ // Is used in case they KeyValuePair has only a key and no value
+ NOT_USED("");
+
+ private final String separator;
+
+ JsonKeyValuePairSeparator(String separator) {
+ this.separator = separator;
+ }
+
+ public String getSeparatorString() {
+ return separator;
+ }
+}
diff --git a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java
index 7f4cf2af0..d786f5170 100644
--- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java
+++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java
@@ -809,6 +809,19 @@ public enum Feature {
* "EXPORT"
*/
export,
+
+ /**
+ * MySQL allows a ',' as a separator between key and value entries. We allow that by default,
+ * but it can be disabled here
+ */
+ allowCommaAsKeyValueSeparator(true),
+
+ /**
+ * DB2 and Oracle allow Expressions as JSON_OBJECT key values. This clashes with Informix and
+ * Snowflake Json-Extraction syntax
+ */
+ allowExpressionAsJsonObjectKey(false)
+
;
private final Object value;
diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java
index 26568d644..7e79d6e94 100644
--- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java
+++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java
@@ -10,64 +10,7 @@
package net.sf.jsqlparser.util;
import net.sf.jsqlparser.JSQLParserException;
-import net.sf.jsqlparser.expression.AllValue;
-import net.sf.jsqlparser.expression.AnalyticExpression;
-import net.sf.jsqlparser.expression.AnyComparisonExpression;
-import net.sf.jsqlparser.expression.ArrayConstructor;
-import net.sf.jsqlparser.expression.ArrayExpression;
-import net.sf.jsqlparser.expression.BinaryExpression;
-import net.sf.jsqlparser.expression.BooleanValue;
-import net.sf.jsqlparser.expression.CaseExpression;
-import net.sf.jsqlparser.expression.CastExpression;
-import net.sf.jsqlparser.expression.CollateExpression;
-import net.sf.jsqlparser.expression.ConnectByRootOperator;
-import net.sf.jsqlparser.expression.ConnectByPriorOperator;
-import net.sf.jsqlparser.expression.DateTimeLiteralExpression;
-import net.sf.jsqlparser.expression.DateValue;
-import net.sf.jsqlparser.expression.DoubleValue;
-import net.sf.jsqlparser.expression.Expression;
-import net.sf.jsqlparser.expression.ExpressionVisitor;
-import net.sf.jsqlparser.expression.ExtractExpression;
-import net.sf.jsqlparser.expression.Function;
-import net.sf.jsqlparser.expression.HexValue;
-import net.sf.jsqlparser.expression.HighExpression;
-import net.sf.jsqlparser.expression.IntervalExpression;
-import net.sf.jsqlparser.expression.Inverse;
-import net.sf.jsqlparser.expression.JdbcNamedParameter;
-import net.sf.jsqlparser.expression.JdbcParameter;
-import net.sf.jsqlparser.expression.JsonAggregateFunction;
-import net.sf.jsqlparser.expression.JsonExpression;
-import net.sf.jsqlparser.expression.JsonFunction;
-import net.sf.jsqlparser.expression.JsonFunctionExpression;
-import net.sf.jsqlparser.expression.KeepExpression;
-import net.sf.jsqlparser.expression.LambdaExpression;
-import net.sf.jsqlparser.expression.LongValue;
-import net.sf.jsqlparser.expression.LowExpression;
-import net.sf.jsqlparser.expression.MySQLGroupConcat;
-import net.sf.jsqlparser.expression.NextValExpression;
-import net.sf.jsqlparser.expression.NotExpression;
-import net.sf.jsqlparser.expression.NullValue;
-import net.sf.jsqlparser.expression.NumericBind;
-import net.sf.jsqlparser.expression.OracleHierarchicalExpression;
-import net.sf.jsqlparser.expression.OracleHint;
-import net.sf.jsqlparser.expression.OracleNamedFunctionParameter;
-import net.sf.jsqlparser.expression.OverlapsCondition;
-import net.sf.jsqlparser.expression.RangeExpression;
-import net.sf.jsqlparser.expression.RowConstructor;
-import net.sf.jsqlparser.expression.RowGetExpression;
-import net.sf.jsqlparser.expression.SignedExpression;
-import net.sf.jsqlparser.expression.StringValue;
-import net.sf.jsqlparser.expression.StructType;
-import net.sf.jsqlparser.expression.TimeKeyExpression;
-import net.sf.jsqlparser.expression.TimeValue;
-import net.sf.jsqlparser.expression.TimestampValue;
-import net.sf.jsqlparser.expression.TimezoneExpression;
-import net.sf.jsqlparser.expression.TranscodingFunction;
-import net.sf.jsqlparser.expression.TrimFunction;
-import net.sf.jsqlparser.expression.UserVariable;
-import net.sf.jsqlparser.expression.VariableAssignment;
-import net.sf.jsqlparser.expression.WhenClause;
-import net.sf.jsqlparser.expression.XMLSerializeExpr;
+import net.sf.jsqlparser.expression.*;
import net.sf.jsqlparser.expression.operators.arithmetic.Addition;
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseAnd;
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseLeftShift;
@@ -1760,6 +1703,17 @@ public Void visit(JsonAggregateFunction expression, S context) {
@Override
public Void visit(JsonFunction expression, S context) {
+ for (JsonKeyValuePair keyValuePair : expression.getKeyValuePairs()) {
+ Object key = keyValuePair.getKey();
+ Object value = keyValuePair.getValue();
+ if (key instanceof Expression) {
+ ((Expression) key).accept(this, context);
+ }
+ if (value instanceof Expression) {
+ ((Expression) value).accept(this, context);
+ }
+ }
+
for (JsonFunctionExpression expr : expression.getExpressions()) {
expr.getExpression().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 9998043a7..0eceb687c 100644
--- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
+++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
@@ -4493,7 +4493,12 @@ SelectItem> SelectItem() #SelectItem:
}
}
-AllColumns AllColumns():
+/**
+ * Parses the AllColumns-Pattern '*'.
+ *
+ * If the allowAdditions is true, it parses additional Keywords.
+ */
+AllColumns AllColumns(boolean allowAdditions):
{
ParenthesedExpressionList exceptColumns = null;
List> replaceExpressions = null;
@@ -4502,21 +4507,28 @@ AllColumns AllColumns():
}
{
"*"
- [ LOOKAHEAD(2) ( tk= | tk= ) exceptColumns = ParenthesedColumnList() { exceptKeyword=tk.image; } ]
- [ LOOKAHEAD(2) "(" replaceExpressions = SelectItemsList() ")" ]
+ // BigData allows EXCEPT, DuckDB allows EXCLUDE
+ [ LOOKAHEAD(2, { allowAdditions }) ( tk= | tk= ) exceptColumns = ParenthesedColumnList() { exceptKeyword=tk.image; } ]
+ // BigData allows REPLACE
+ [ LOOKAHEAD(2, { allowAdditions }) "(" replaceExpressions = SelectItemsList() ")" ]
{
return new AllColumns(exceptColumns, replaceExpressions, exceptKeyword);
}
}
-AllTableColumns AllTableColumns():
+/**
+ * Parses the AllTableColumns-Pattern 'table.*'
+ *
+ * If the allowAdditions is true, it parses additional Keywords.
+ */
+AllTableColumns AllTableColumns(boolean allowAdditions):
{
Table table = null;
AllColumns allColumns;
}
{
- table=Table() "." allColumns=AllColumns()
+ table=Table() "." allColumns=AllColumns(allowAdditions)
{
return new AllTableColumns(table, allColumns);
}
@@ -6527,9 +6539,9 @@ Expression PrimaryExpression() #PrimaryExpression:
| token= { retval = new HexValue(token.image); }
- | LOOKAHEAD(3) retval=AllColumns()
+ | LOOKAHEAD(3) retval=AllColumns(true)
- | LOOKAHEAD(16) retval=AllTableColumns()
+ | LOOKAHEAD(16) retval=AllTableColumns(true)
// See issue #2207
// there is a huge! performance deterioration from this production
@@ -6941,121 +6953,139 @@ JsonExpression JsonExpression(Expression expr, List
- "(" { result.setType( JsonFunctionType.OBJECT ); }
- (
- // SQL2016 compliant Syntax
- LOOKAHEAD(2) (
- LOOKAHEAD(2) (
- "KEY" { usingKeyKeyword = true; } ( keyToken = { key = keyToken.image; } | key = Column() )
- )
- |
- keyToken = { key = keyToken.image; }
- |
+ // Key part
+ (
+ // lookahead because key is a valid column name
+ LOOKAHEAD(2) (
+ { usingKeyKeyword = true; }
+ (
+ keyToken = { key = keyToken.image; } |
key = Column()
- )
+ )
+ )
+ |
+ LOOKAHEAD(16) ( key = AllTableColumns(false) { isWildcard = true; } )
+ |
+ key = AllColumns(false) { isWildcard = true; }
+ |
+ key = Column()
+ |
+ LOOKAHEAD({getAsBoolean(Feature.allowExpressionAsJsonObjectKey)}) key = Expression()
+ |
+ keyToken = { key = keyToken.image; }
+ )
- ( LOOKAHEAD(2)
- ( ":" | "," { result.setType( JsonFunctionType.POSTGRES_OBJECT ); } | "VALUE" { usingValueKeyword = true; } )
- (
- expression = Expression()
- )
- [ { usingFormatJason = true; } ]
- )?
- {
- if (expression !=null) {
- keyValuePair = new JsonKeyValuePair( key, expression, usingKeyKeyword, usingValueKeyword );
- keyValuePair.setUsingFormatJson( usingFormatJason );
- result.add(keyValuePair);
- } else {
- result.setType( JsonFunctionType.POSTGRES_OBJECT );
- keyValuePair = new JsonKeyValuePair( key, null, false, false );
- result.add(keyValuePair);
- }
- }
+ // Optional Separator + Value - Is not allowed with * or t1.*
+ [ LOOKAHEAD(1, { !isWildcard } )
+ (
+ { kvSeparator = JsonKeyValuePairSeparator.VALUE; }
+ |
+ { kvSeparator = JsonKeyValuePairSeparator.COLON; }
+ |
+ LOOKAHEAD({getAsBoolean(Feature.allowCommaAsKeyValueSeparator)}) { kvSeparator = JsonKeyValuePairSeparator.COMMA; }
+ )
+ expression = Expression()
+ ]
- // --- Next Elements
- ( "," { usingKeyKeyword = false; usingValueKeyword = false; }
- (
- LOOKAHEAD(2) (
- "KEY" { usingKeyKeyword = true; } ( keyToken = { key = keyToken.image; } | key = Column() )
- )
- |
- keyToken = { key = keyToken.image; }
- |
- key = Column()
- )
- ( ":" | "," { result.setType( JsonFunctionType.MYSQL_OBJECT ); } | "VALUE" { usingValueKeyword = true; } )
- expression = Expression() { keyValuePair = new JsonKeyValuePair( key, expression, usingKeyKeyword, usingValueKeyword ); result.add(keyValuePair); }
- [ { keyValuePair.setUsingFormatJson( true ); } ]
- )*
- )?
+ // Optional: FORMAT JSON - Is not allowed with * or t1.*
+ [ LOOKAHEAD(1, { !isWildcard } ) { usingFormatJason = true; } ]
+ )
+ {
+ final JsonKeyValuePair keyValuePair = new JsonKeyValuePair( key, expression, usingKeyKeyword, kvSeparator );
+ keyValuePair.setUsingFormatJson( usingFormatJason );
+ return keyValuePair;
+ }
+}
- [
- (
- { result.setOnNullType( JsonAggregateOnNullType.NULL ); }
- )
- |
- (
- { result.setOnNullType( JsonAggregateOnNullType.ABSENT ); }
- )
- ]
+JsonFunction JsonObjectBody() : {
+ JsonFunction result = new JsonFunction(JsonFunctionType.OBJECT);
- [
- (
- { result.setUniqueKeysType( JsonAggregateUniqueKeysType.WITH ); }
- )
- |
- (
- { result.setUniqueKeysType( JsonAggregateUniqueKeysType.WITHOUT ); }
- )
- ]
- ")"
- )
- |
- (
- { result.setType( JsonFunctionType.ARRAY ); }
- "("
+ JsonKeyValuePair keyValuePair;
+}
+{
+ ( "("
+ (
+ // First Element
+ LOOKAHEAD(2) keyValuePair = JsonKeyValuePair(true) { result.add(keyValuePair);}
+
+ // Next Elements
(
- LOOKAHEAD(2) (
- { result.setOnNullType( JsonAggregateOnNullType.NULL ); }
- )
- |
- expression=Expression() { functionExpression = new JsonFunctionExpression( expression ); result.add( functionExpression ); }
+ keyValuePair = JsonKeyValuePair(false) { result.add(keyValuePair); }
+ )*
+ )?
+ [
+ ( { result.setOnNullType( JsonAggregateOnNullType.NULL ); } )
+ |
+ ( { result.setOnNullType( JsonAggregateOnNullType.ABSENT ); } )
+ ]
+ [ { result.setStrict(true); } ]
+ [
+ ( { result.setUniqueKeysType( JsonAggregateUniqueKeysType.WITH ); } )
+ |
+ ( { result.setUniqueKeysType( JsonAggregateUniqueKeysType.WITHOUT ); } )
+ ]
+ ")" )
+ {
+ return result;
+ }
+}
+
+JsonFunction JsonArrayBody() : {
+ JsonFunction result = new JsonFunction(JsonFunctionType.ARRAY);
+
+ Expression expression = null;
+ JsonFunctionExpression functionExpression;
+}
+{
+ ( "("
+ (
+ LOOKAHEAD(2) (
+ { result.setOnNullType( JsonAggregateOnNullType.NULL ); }
+ )
+ |
+ expression=Expression() { functionExpression = new JsonFunctionExpression( expression ); result.add( functionExpression ); }
+ [ LOOKAHEAD(2) { functionExpression.setUsingFormatJson( true ); } ]
+ (
+ ","
+ expression=Expression() { functionExpression = new JsonFunctionExpression( expression ); result.add( functionExpression ); }
[ LOOKAHEAD(2) { functionExpression.setUsingFormatJson( true ); } ]
- (
- ","
- expression=Expression() { functionExpression = new JsonFunctionExpression( expression ); result.add( functionExpression ); }
- [ LOOKAHEAD(2) { functionExpression.setUsingFormatJson( true ); } ]
- )*
)*
+ )*
- [
- { result.setOnNullType( JsonAggregateOnNullType.ABSENT ); }
- ]
+ [
+ { result.setOnNullType( JsonAggregateOnNullType.ABSENT ); }
+ ]
+ ")" )
+ {
+ return result;
+ }
+}
- ")"
- )
+JsonFunction JsonFunction() : {
+ JsonFunction result;
+}
+{
+ (
+ ( result = JsonObjectBody() )
+ |
+ ( result = JsonArrayBody() )
)
-
{
return result;
}
diff --git a/src/test/java/net/sf/jsqlparser/expression/JsonExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/JsonExpressionTest.java
index c29af21f9..5fc72bea8 100644
--- a/src/test/java/net/sf/jsqlparser/expression/JsonExpressionTest.java
+++ b/src/test/java/net/sf/jsqlparser/expression/JsonExpressionTest.java
@@ -41,6 +41,7 @@ void testIssue1792() throws JSQLParserException {
@Test
void testSnowflakeGetOperator() throws JSQLParserException {
+ // https://docs.snowflake.com/en/user-guide/querying-semistructured
String sqlStr = "SELECT v:'attr[0].name' FROM vartab;";
PlainSelect st = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sqlStr, true);
Assertions.assertInstanceOf(JsonExpression.class, st.getSelectItem(0).getExpression());
@@ -48,6 +49,7 @@ void testSnowflakeGetOperator() throws JSQLParserException {
@Test
void testDataBricksExtractPathOperator() throws JSQLParserException {
+ // https://docs.databricks.com/aws/en/sql/language-manual/sql-ref-json-path-expression
String sqlStr = "SELECT C1:PRICE J FROM VALUES('{\"price\":5}')AS T(C1)";
PlainSelect st = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sqlStr, true);
Assertions.assertInstanceOf(JsonExpression.class, st.getSelectItem(0).getExpression());
diff --git a/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java
index ef1335e6c..5475f8ec7 100644
--- a/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java
+++ b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java
@@ -11,9 +11,17 @@
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.parser.feature.Feature;
+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.test.TestUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.junit.jupiter.api.Assertions.*;
/**
*
@@ -66,15 +74,15 @@ public void testObjectBuilder() throws JSQLParserException {
.withUsingKeyKeyword(true).withUsingValueKeyword(true).withUsingFormatJson(false);
// this should work because we compare based on KEY only
- Assertions.assertEquals(keyValuePair1, keyValuePair2);
+ assertEquals(keyValuePair1, keyValuePair2);
// this must fail because all the properties are considered
Assertions.assertNotEquals(keyValuePair1.toString(), keyValuePair2.toString());
JsonKeyValuePair keyValuePair3 = new JsonKeyValuePair("foo", "bar", false, false)
.withUsingKeyKeyword(false).withUsingValueKeyword(false).withUsingFormatJson(false);
- Assertions.assertNotNull(keyValuePair3);
- Assertions.assertEquals(keyValuePair3, keyValuePair3);
+ assertNotNull(keyValuePair3);
+ assertEquals(keyValuePair3, keyValuePair3);
Assertions.assertNotEquals(keyValuePair3, f);
Assertions.assertTrue(keyValuePair3.hashCode() != 0);
@@ -94,7 +102,7 @@ public void testArrayBuilder() throws JSQLParserException {
new JsonFunctionExpression(new NullValue()).withUsingFormatJson(
true);
- Assertions.assertEquals(expression1.toString(), expression2.toString());
+ assertEquals(expression1.toString(), expression2.toString());
f.add(expression1);
f.add(expression2);
@@ -164,6 +172,62 @@ public void testObject() throws JSQLParserException {
TestUtils.assertExpressionCanBeParsedAndDeparsed("json_object()", true);
}
+ @Test
+ public void nestedObjects() throws JSQLParserException {
+ TestUtils.assertSqlCanBeParsedAndDeparsed(
+ "WITH Items AS (SELECT 'hello' AS key, 'world' AS value), \n" +
+ " SubItems AS (SELECT 'nestedValue' AS 'nestedKey', 'nestedWorld' AS nestedValue)\n"
+ +
+ "SELECT JSON_OBJECT(key: value, nested : (SELECT JSON_OBJECT(nestedKey, nestedValue) FROM SubItems)) AS json_data FROM Items",
+ true);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ // AllColumns
+ "SELECT JSON_OBJECT(*) FROM employees",
+ "SELECT JSON_OBJECT(* ABSENT ON NULL) FROM employees",
+
+ // AllTableColumns
+ "SELECT JSON_OBJECT(e.*) FROM employees e",
+ "SELECT JSON_OBJECT(e.*, d.* NULL ON NULL) FROM employees e, departments d",
+ "SELECT JSON_OBJECT(e.* WITH UNIQUE KEYS) FROM employees e",
+
+ // Single Column as entry
+ "SELECT JSON_OBJECT(first_name, last_name, address) FROM employees t1",
+ "SELECT JSON_OBJECT(t1.first_name, t1.last_name, t1.address) FROM employees t1",
+ "SELECT JSON_OBJECT(first_name, last_name FORMAT JSON, address) FROM employees t1",
+ "SELECT JSON_OBJECT(t1.first_name, t1.last_name FORMAT JSON, t1.address FORMAT JSON) FROM employees t1",
+
+ // STRICT Keyword
+ "SELECT JSON_OBJECT( 'foo':bar, 'fob':baz FORMAT JSON STRICT ) FROM dual",
+ "SELECT JSON_OBJECT( 'foo':bar FORMAT JSON, 'fob':baz STRICT ) FROM dual",
+ "SELECT JSON_OBJECT( 'foo':bar, 'fob':baz NULL ON NULL STRICT WITH UNIQUE KEYS) FROM dual"
+ })
+ void testObjectOracle(String sqlStr) throws JSQLParserException {
+ TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ // BigQuery EXCEPT/REPLACE are not allowed here
+ "SELECT JSON_OBJECT(* EXCEPT(first_name)) FROM employees",
+ "SELECT JSON_OBJECT(* EXCLUDE(first_name)) FROM employees",
+ "SELECT JSON_OBJECT(* REPLACE(\"first_name\" AS first_name)) FROM employees",
+
+ // FORMAT JSON is not allowed on wildcards
+ "SELECT JSON_OBJECT(* FORMAT JSON) FROM employees",
+ "SELECT JSON_OBJECT(e.* FORMAT JSON) FROM employees e",
+
+ // Value is not allowed on wildcards
+ "SELECT JSON_OBJECT(* : bar) FROM employees",
+ "SELECT JSON_OBJECT(e.* VALUE bar) FROM employees e",
+ "SELECT JSON_OBJECT(KEY e.* VALUE bar) FROM employees e",
+ })
+ void testInvalidObjectOracle(String sqlStr) {
+ assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sqlStr));
+ }
+
@Test
public void testObjectWithExpression() throws JSQLParserException {
TestUtils.assertSqlCanBeParsedAndDeparsed(
@@ -262,29 +326,128 @@ public void testIssue1371() throws JSQLParserException {
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT json_object('{a, b}', '{1,2 }')", true);
}
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "JSON_OBJECT( KEY 'foo' VALUE bar, 'fob' : baz)",
+
+ "JSON_OBJECT( t1.*, t2.* )",
+ "JSON_OBJECT( 'foo' VALUE bar, t1.*)",
+ "JSON_OBJECT( t1.*, 'foo' VALUE bar)",
+
+ // The FORMAT JSON forces the parser to correctly identify the entries as single entries
+ "JSON_OBJECT(first_name FORMAT JSON, last_name)",
+ "JSON_OBJECT(t1.first_name FORMAT JSON, t1.last_name FORMAT JSON)",
+
+ // MySQL syntax
+ "JSON_OBJECT( 'foo', bar, 'fob', baz)",
+ })
+ void testEntriesAreParsedCorrectly(String expressionStr) throws JSQLParserException {
+ JsonFunction jsonFunction = (JsonFunction) CCJSqlParserUtil.parseExpression(expressionStr);
+ assertEquals(2, jsonFunction.getKeyValuePairs().size());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "JSON_OBJECT( t1.*, t2.*, t3.* )",
+ "JSON_OBJECT( 'foo' VALUE bar, t1.*, t2.single_column)",
+ "JSON_OBJECT( t1.*, 'foo' VALUE bar, KEY fob : baz)",
+
+ // MySQL syntax
+ "JSON_OBJECT( 'foo', bar, 'fob', baz, 'for', buz)",
+ })
+ void testEntriesAreParsedCorrectly3Entries(String expressionStr) throws JSQLParserException {
+ JsonFunction jsonFunction = (JsonFunction) CCJSqlParserUtil.parseExpression(expressionStr);
+ assertEquals(3, jsonFunction.getKeyValuePairs().size());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "JSON_OBJECT(first_name, last_name, address)",
+ "JSON_OBJECT(t1.first_name, t1.last_name, t1.address)",
+ "JSON_OBJECT(first_name, last_name FORMAT JSON, address)",
+ "JSON_OBJECT(first_name FORMAT JSON, last_name FORMAT JSON, address)",
+ "JSON_OBJECT(t1.first_name, t1.last_name FORMAT JSON, t1.address FORMAT JSON)",
+ })
+ void testSingleEntriesAreParsedCorrectlyWithouCommaAsKeyValueSeparator(String expressionStr)
+ throws JSQLParserException {
+ FeatureConfiguration fc =
+ new FeatureConfiguration().setValue(Feature.allowCommaAsKeyValueSeparator, false);
+
+ JsonFunction jsonFunction = (JsonFunction) CCJSqlParserUtil.parseExpression(expressionStr,
+ true, parser -> parser.withConfiguration(fc));
+ assertEquals(3, jsonFunction.getKeyValuePairs().size());
+ }
+
@Test
public void testJavaMethods() throws JSQLParserException {
String expressionStr =
- "JSON_OBJECT( KEY 'foo' VALUE bar FORMAT JSON, 'foo':bar, 'foo':bar ABSENT ON NULL WITHOUT UNIQUE KEYS)";
+ "JSON_OBJECT( KEY 'foo' VALUE bar FORMAT JSON, 'fob':baz, 'fod':bag ABSENT ON NULL WITHOUT UNIQUE KEYS)";
JsonFunction jsonFunction = (JsonFunction) CCJSqlParserUtil.parseExpression(expressionStr);
- Assertions.assertEquals(JsonFunctionType.OBJECT, jsonFunction.getType());
+ assertEquals(JsonFunctionType.OBJECT, jsonFunction.getType());
Assertions.assertNotEquals(jsonFunction.withType(JsonFunctionType.POSTGRES_OBJECT),
jsonFunction.getType());
- Assertions.assertEquals(3, jsonFunction.getKeyValuePairs().size());
- Assertions.assertEquals(new JsonKeyValuePair("'foo'", "bar", true, true),
+ assertEquals(3, jsonFunction.getKeyValuePairs().size());
+ assertEquals(new JsonKeyValuePair("'foo'", "bar", true, true),
jsonFunction.getKeyValuePair(0));
jsonFunction.setOnNullType(JsonAggregateOnNullType.NULL);
- Assertions.assertEquals(JsonAggregateOnNullType.ABSENT,
+ assertEquals(JsonAggregateOnNullType.ABSENT,
jsonFunction.withOnNullType(JsonAggregateOnNullType.ABSENT).getOnNullType());
jsonFunction.setUniqueKeysType(JsonAggregateUniqueKeysType.WITH);
- Assertions.assertEquals(JsonAggregateUniqueKeysType.WITH, jsonFunction
+ assertEquals(JsonAggregateUniqueKeysType.WITH, jsonFunction
.withUniqueKeysType(JsonAggregateUniqueKeysType.WITH).getUniqueKeysType());
}
+ @Test
+ void testJavaMethodsStrict() throws JSQLParserException {
+ String expressionStr = "JSON_OBJECT( 'foo':bar, 'fob':baz FORMAT JSON STRICT )";
+ JsonFunction jsonFunction = (JsonFunction) CCJSqlParserUtil.parseExpression(expressionStr);
+
+ assertTrue(jsonFunction.isStrict());
+
+ jsonFunction.withStrict(false);
+
+ assertEquals(
+ TestUtils.buildSqlString("JSON_OBJECT( 'foo':bar, 'fob':baz FORMAT JSON ) ", true),
+ TestUtils.buildSqlString(jsonFunction.toString(), true));
+
+ }
+
+ @Test
+ void testJavaMethodsAllColumns() throws JSQLParserException {
+ String expressionStr = "JSON_OBJECT(* NULL ON NULL)";
+ JsonFunction jsonFunction = (JsonFunction) CCJSqlParserUtil.parseExpression(expressionStr);
+
+ assertEquals(1, jsonFunction.getKeyValuePairs().size());
+ JsonKeyValuePair kv = jsonFunction.getKeyValuePair(0);
+ assertNotNull(kv);
+
+ assertNull(kv.getValue());
+ assertInstanceOf(AllColumns.class, kv.getKey());
+ }
+
+ @Test
+ void testJavaMethodsAllTableColumns() throws JSQLParserException {
+ String expressionStr = "JSON_OBJECT(a.*, b.* NULL ON NULL)";
+ JsonFunction jsonFunction = (JsonFunction) CCJSqlParserUtil.parseExpression(expressionStr);
+
+ assertEquals(2, jsonFunction.getKeyValuePairs().size());
+
+ JsonKeyValuePair kv1 = jsonFunction.getKeyValuePair(0);
+ assertNotNull(kv1);
+ assertInstanceOf(AllTableColumns.class, kv1.getKey());
+ assertNull(kv1.getValue());
+
+ JsonKeyValuePair kv2 = jsonFunction.getKeyValuePair(1);
+ assertNotNull(kv2);
+ assertInstanceOf(AllTableColumns.class, kv2.getKey());
+ assertNull(kv2.getValue());
+
+ }
+
@Test
void testIssue1753JSonObjectAggWithColumns() throws JSQLParserException {
String sqlStr = "SELECT JSON_OBJECTAGG( KEY q.foo VALUE q.bar) FROM dual";
diff --git a/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java
index a40d52509..a36143565 100644
--- a/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java
+++ b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java
@@ -713,5 +713,18 @@ void assertWithItemWithFunctionDeclarationReturnsTableInSelect() throws JSQLPars
"WITH FUNCTION my_with_item(param1 INT) RETURNS INT RETURN param1 + 1 SELECT * FROM my_table;";
assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactly("my_table");
}
+
+ @Test
+ void testNestedTablesInJsonObject() throws JSQLParserException {
+ String sqlStr = "select JSON_OBJECT(\n" +
+ " t1.*, \n" +
+ " nested1 : (SELECT JSON_OBJECT(tn2.*) FROM table2 tn2 WHERE tn2.fk = t1.pk), \n" +
+ " nested2 : (SELECT JSON_OBJECT(tn3.*) FROM table3 tn3 WHERE tn3.fk = t1.pk)\n" +
+ " )\n" +
+ "FROM table1 t1;";
+
+ assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("table1",
+ "table2", "table3");
+ }
}