From a87f797abad57541a20cbfec37886a3692c5b60b Mon Sep 17 00:00:00 2001 From: Andreas Neumann Date: Tue, 7 Oct 2025 22:11:09 +0200 Subject: [PATCH 1/6] [feat] JSON_OBJECT support for AllColumns and AllTableColumns --- build.gradle | 8 +++- .../jsqlparser/expression/JsonFunction.java | 42 ++++++++++++++++--- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 15 +++++-- .../expression/JsonFunctionTest.java | 19 +++++++++ 4 files changed, 75 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index 843119600..4462257f8 100644 --- a/build.gradle +++ b/build.gradle @@ -131,7 +131,13 @@ 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. Setting it to false does NOT disable it + DEBUG_PARSER: '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..4d8d8f8a7 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java @@ -25,6 +25,8 @@ public class JsonFunction extends ASTNodeAccessImpl implements Expression { private JsonAggregateOnNullType onNullType; private JsonAggregateUniqueKeysType uniqueKeysType; + private boolean isStrict = false; + public ArrayList getKeyValuePairs() { return keyValuePairs; } @@ -114,6 +116,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); @@ -164,19 +179,33 @@ public StringBuilder appendObject(StringBuilder 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,10 +218,6 @@ public StringBuilder appendObject(StringBuilder builder) { // this should never happen } } - - builder.append(" ) "); - - return builder; } @@ -205,6 +230,13 @@ public StringBuilder appendPostgresObject(StringBuilder builder) { builder.append(", ").append(keyValuePair.getValue()); } } + + appendOnNullType(builder); + if (isStrict) { + builder.append(" STRICT"); + } + appendUniqueKeys(builder); + builder.append(" ) "); return builder; diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 9998043a7..33bb9fa95 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -6967,9 +6967,13 @@ JsonFunction JsonFunction() : { "KEY" { usingKeyKeyword = true; } ( keyToken = { key = keyToken.image; } | key = Column() ) ) | - keyToken = { key = keyToken.image; } + LOOKAHEAD(2) ( key = AllTableColumns() ) + | + key = AllColumns() | key = Column() + | + keyToken = { key = keyToken.image; } ) ( LOOKAHEAD(2) @@ -6998,9 +7002,12 @@ JsonFunction JsonFunction() : { "KEY" { usingKeyKeyword = true; } ( keyToken = { key = keyToken.image; } | key = Column() ) ) | - keyToken = { key = keyToken.image; } + LOOKAHEAD(2) ( key = AllTableColumns() ) | key = Column() + | + keyToken = { key = keyToken.image; } + ) ( ":" | "," { result.setType( JsonFunctionType.MYSQL_OBJECT ); } | "VALUE" { usingValueKeyword = true; } ) expression = Expression() { keyValuePair = new JsonKeyValuePair( key, expression, usingKeyKeyword, usingValueKeyword ); result.add(keyValuePair); } @@ -7017,7 +7024,9 @@ JsonFunction JsonFunction() : { { result.setOnNullType( JsonAggregateOnNullType.ABSENT ); } ) ] - + [ + { result.setStrict(true); } + ] [ ( { result.setUniqueKeysType( JsonAggregateUniqueKeysType.WITH ); } diff --git a/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java index ef1335e6c..3066f0e38 100644 --- a/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java @@ -164,6 +164,25 @@ public void testObject() throws JSQLParserException { TestUtils.assertExpressionCanBeParsedAndDeparsed("json_object()", true); } + @Test + void testObjectOracle() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECT(*) FROM employees", true); + + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECT(* ABSENT ON NULL) FROM employees", true); + + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECT(e.*) FROM employees e", true); + + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECT(e.*, d.* NULL ON NULL) FROM employees e, departments d", true); + + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECT(e.* WITH UNIQUE KEYS) FROM employees e", true); + + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT JSON_OBJECT( 'foo':bar, 'fob':baz FORMAT JSON STRICT ) FROM dual ", true); + + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT JSON_OBJECT( 'foo':bar, 'fob':baz NULL ON NULL STRICT WITH UNIQUE KEYS) FROM dual ", true); + } + @Test public void testObjectWithExpression() throws JSQLParserException { TestUtils.assertSqlCanBeParsedAndDeparsed( From 46f3679db6642849893adb596f14a4236c975a8a Mon Sep 17 00:00:00 2001 From: Andreas Neumann Date: Wed, 8 Oct 2025 20:34:32 +0200 Subject: [PATCH 2/6] [feat] JSON_OBJECT support for AllColumns and AllTableColumns --- .../jsqlparser/expression/JsonFunction.java | 92 +++++--------- .../expression/JsonFunctionType.java | 15 ++- .../expression/JsonKeyValuePair.java | 41 ++++++- .../expression/JsonKeyValuePairSeparator.java | 25 ++++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 74 ++++++++---- .../expression/JsonFunctionTest.java | 112 ++++++++++++++---- 6 files changed, 240 insertions(+), 119 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePairSeparator.java diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java index 4d8d8f8a7..56695d037 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 and JSON_OBJECTAGG the parameters are available from {@link #getKeyValuePairs()}
+ *
+ * For JSON_ARRAY and JSON_ARRAYAGG 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<>(); @@ -27,10 +33,21 @@ public class JsonFunction extends ASTNodeAccessImpl implements Expression { private boolean isStrict = false; + /** + * Returns the Parameters of an JSON_OBJECT or JSON_OBJECTAGG
+ * The KeyValuePairs may not have both key and value set, in some cases only the Key is set. + * + * @return A List of KeyValuePairs, never NULL + */ public ArrayList getKeyValuePairs() { return keyValuePairs; } + /** + * Returns the parameters of JSON_ARRAY or JSON_ARRAYAGG
+ * + * @return A List of {@link JsonFunctionExpression}s, never NULL + */ public ArrayList getExpressions() { return expressions; } @@ -138,13 +155,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); @@ -163,14 +176,14 @@ 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.isUsingKeyKeyword() && keyValuePair.getSeparator() == JsonKeyValuePairSeparator.VALUE) { + builder.append("KEY "); + } + builder.append(keyValuePair.getKey()); + + if (keyValuePair.getValue() != null) { + builder.append(keyValuePair.getSeparator().getSeparatorString()); + builder.append(keyValuePair.getValue()); } if (keyValuePair.isUsingFormatJson()) { @@ -220,44 +233,6 @@ private void appendUniqueKeys(StringBuilder 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()); - } - } - - appendOnNullType(builder); - if (isStrict) { - builder.append(" STRICT"); - } - appendUniqueKeys(builder); - - 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"}) public StringBuilder appendArray(StringBuilder builder) { builder.append("JSON_ARRAY( "); @@ -271,18 +246,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..c61f471ec 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonFunctionType.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonFunctionType.java @@ -14,7 +14,20 @@ * @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..c5391a350 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java @@ -20,16 +20,20 @@ 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; + 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, - boolean usingValueKeyword) { + 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 = separator; } public boolean isUsingKeyKeyword() { @@ -45,19 +49,44 @@ 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; } 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..56e31385b --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePairSeparator.java @@ -0,0 +1,25 @@ +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/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 33bb9fa95..4b30635da 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 @@ -6944,16 +6956,17 @@ JsonExpression JsonExpression(Expression expr, List { key = keyToken.image; } | key = Column() ) ) | - LOOKAHEAD(2) ( key = AllTableColumns() ) + LOOKAHEAD(2) ( key = AllTableColumns(false) { isSingleEntryKeyValue = true; }) | - key = AllColumns() + key = AllColumns(false) { isSingleEntryKeyValue = true; } | key = Column() | @@ -6977,40 +6990,55 @@ JsonFunction JsonFunction() : { ) ( LOOKAHEAD(2) - ( ":" | "," { result.setType( JsonFunctionType.POSTGRES_OBJECT ); } | "VALUE" { usingValueKeyword = true; } ) + ( ":" { kvSeparator = JsonKeyValuePairSeparator.COLON; } | + "," { if (!isSingleEntryKeyValue) kvSeparator = JsonKeyValuePairSeparator.COMMA; } | + "VALUE" { kvSeparator = JsonKeyValuePairSeparator.VALUE; } ) ( expression = Expression() ) [ { usingFormatJason = true; } ] )? { - if (expression !=null) { - keyValuePair = new JsonKeyValuePair( key, expression, usingKeyKeyword, usingValueKeyword ); - keyValuePair.setUsingFormatJson( usingFormatJason ); - result.add(keyValuePair); + if (expression != null) { + if (isSingleEntryKeyValue) { + result.add(new JsonKeyValuePair( key, null, false, JsonKeyValuePairSeparator.NOT_USED )); + result.add(new JsonKeyValuePair( expression, null, false, JsonKeyValuePairSeparator.NOT_USED )); + } else { + keyValuePair = new JsonKeyValuePair( key, expression, usingKeyKeyword, kvSeparator ); + keyValuePair.setUsingFormatJson( usingFormatJason ); + result.add(keyValuePair); + } } else { - result.setType( JsonFunctionType.POSTGRES_OBJECT ); - keyValuePair = new JsonKeyValuePair( key, null, false, false ); - result.add(keyValuePair); + result.add(new JsonKeyValuePair( key, null, false, JsonKeyValuePairSeparator.NOT_USED )); } } // --- Next Elements - ( "," { usingKeyKeyword = false; usingValueKeyword = false; } + ( "," { usingKeyKeyword = false; kvSeparator = null; isSingleEntryKeyValue = false; } ( LOOKAHEAD(2) ( "KEY" { usingKeyKeyword = true; } ( keyToken = { key = keyToken.image; } | key = Column() ) ) | - LOOKAHEAD(2) ( key = AllTableColumns() ) + LOOKAHEAD(2) ( key = AllTableColumns(false) { isSingleEntryKeyValue = true; } ) | key = Column() | keyToken = { key = keyToken.image; } ) - ( ":" | "," { result.setType( JsonFunctionType.MYSQL_OBJECT ); } | "VALUE" { usingValueKeyword = true; } ) - expression = Expression() { keyValuePair = new JsonKeyValuePair( key, expression, usingKeyKeyword, usingValueKeyword ); result.add(keyValuePair); } + ( ":" { kvSeparator = JsonKeyValuePairSeparator.COLON; } | + "," { kvSeparator = JsonKeyValuePairSeparator.COMMA; } | + "VALUE" { kvSeparator = JsonKeyValuePairSeparator.VALUE; } ) + expression = Expression() { + if (isSingleEntryKeyValue) { + result.add(new JsonKeyValuePair( key, null, false, JsonKeyValuePairSeparator.NOT_USED )); + result.add(new JsonKeyValuePair( expression, null, false, JsonKeyValuePairSeparator.NOT_USED )); + } else { + keyValuePair = new JsonKeyValuePair( key, expression, usingKeyKeyword, kvSeparator ); + result.add(keyValuePair); + } + } [ { keyValuePair.setUsingFormatJson( true ); } ] )* )? diff --git a/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java index 3066f0e38..4511be7f5 100644 --- a/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java @@ -11,9 +11,15 @@ import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.parser.CCJSqlParserUtil; +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 +72,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 +100,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,23 +170,34 @@ public void testObject() throws JSQLParserException { TestUtils.assertExpressionCanBeParsedAndDeparsed("json_object()", true); } - @Test - void testObjectOracle() throws JSQLParserException { - TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECT(*) FROM employees", true); - - TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECT(* ABSENT ON NULL) FROM employees", true); - - TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECT(e.*) FROM employees e", true); - - TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECT(e.*, d.* NULL ON NULL) FROM employees e, departments d", true); - - TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECT(e.* WITH UNIQUE KEYS) FROM employees e", true); - - TestUtils.assertSqlCanBeParsedAndDeparsed( - "SELECT JSON_OBJECT( 'foo':bar, 'fob':baz FORMAT JSON STRICT ) FROM dual ", 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", + + // STRICT Keyword + "SELECT JSON_OBJECT( 'foo':bar, 'fob':baz FORMAT JSON 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); + } - TestUtils.assertSqlCanBeParsedAndDeparsed( - "SELECT JSON_OBJECT( 'foo':bar, 'fob':baz NULL ON NULL STRICT WITH UNIQUE KEYS) FROM dual ", 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" + }) + void testInvalidObjectOracle(String sqlStr) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sqlStr)); } @Test @@ -287,23 +304,68 @@ public void testJavaMethods() throws JSQLParserException { "JSON_OBJECT( KEY 'foo' VALUE bar FORMAT JSON, 'foo':bar, 'foo':bar 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("JSON_OBJECT( 'foo':bar, 'fob':baz FORMAT JSON ) ", jsonFunction.toString()); + + } + + @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"; From 64221d28194e2ef4411bce94f34359005f966ba3 Mon Sep 17 00:00:00 2001 From: Andreas Neumann Date: Fri, 10 Oct 2025 11:51:10 +0200 Subject: [PATCH 3/6] [feat] JSON_OBJECT support for AllColumns and AllTableColumns --- build.gradle | 2 +- .../java/net/sf/jsqlparser/expression/JsonKeyValuePair.java | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4462257f8..45a1184dd 100644 --- a/build.gradle +++ b/build.gradle @@ -135,7 +135,7 @@ compileJavacc { grammar_encoding: 'UTF-8', static: 'false', java_template_type: 'modern', - // Comment this in to build the parser with tracing. Setting it to false does NOT disable it + // Comment this in to build the parser with tracing. DEBUG_PARSER: 'false' ] } diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java b/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java index c5391a350..71547b1ff 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java @@ -24,6 +24,10 @@ public class JsonKeyValuePair implements Serializable { 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); } From d2f762ed804dc34044871a03523f83bd58b88f7a Mon Sep 17 00:00:00 2001 From: Andreas Neumann Date: Sat, 11 Oct 2025 14:14:55 +0200 Subject: [PATCH 4/6] [feat] Split the parser syntax for JsonFunction, added more Tests, added Feature to disable Commas as key value separators --- build.gradle | 3 +- .../jsqlparser/expression/JsonFunction.java | 19 +- .../expression/JsonFunctionType.java | 3 +- .../expression/JsonKeyValuePair.java | 9 +- .../expression/JsonKeyValuePairSeparator.java | 3 +- .../sf/jsqlparser/parser/feature/Feature.java | 7 +- .../sf/jsqlparser/util/TablesNamesFinder.java | 70 +----- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 229 +++++++++--------- .../expression/JsonExpressionTest.java | 2 + .../expression/JsonFunctionTest.java | 94 ++++++- .../util/TablesNamesFinderTest.java | 13 + 11 files changed, 258 insertions(+), 194 deletions(-) diff --git a/build.gradle b/build.gradle index 45a1184dd..e566a0a10 100644 --- a/build.gradle +++ b/build.gradle @@ -136,7 +136,8 @@ compileJavacc { static: 'false', java_template_type: 'modern', // Comment this in to build the parser with tracing. - DEBUG_PARSER: 'false' + DEBUG_PARSER: 'false', + DEBUG_LOOKAHEAD: 'false' ] } diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java index 56695d037..275b1f971 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java @@ -18,9 +18,9 @@ * Represents a JSON-Function.
* Currently supported are the types in {@link JsonFunctionType}.
*
- * For JSON_OBJECT and JSON_OBJECTAGG the parameters are available from {@link #getKeyValuePairs()}
+ * For JSON_OBJECT the parameters are available from {@link #getKeyValuePairs()}
*
- * For JSON_ARRAY and JSON_ARRAYAGG the parameters are availble from {@link #getExpressions()}.
+ * For JSON_ARRAY the parameters are availble from {@link #getExpressions()}.
* * @author Andreas Reichel */ @@ -33,10 +33,18 @@ public class JsonFunction extends ASTNodeAccessImpl implements Expression { private boolean isStrict = false; + public JsonFunction() {} + + public JsonFunction(JsonFunctionType functionType) { + this.functionType = functionType; + } + /** - * Returns the Parameters of an JSON_OBJECT or JSON_OBJECTAGG
+ * 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() { @@ -44,7 +52,7 @@ public ArrayList getKeyValuePairs() { } /** - * Returns the parameters of JSON_ARRAY or JSON_ARRAYAGG
+ * Returns the parameters of JSON_ARRAY
* * @return A List of {@link JsonFunctionExpression}s, never NULL */ @@ -176,7 +184,8 @@ public StringBuilder appendObject(StringBuilder builder) { if (i > 0) { builder.append(", "); } - if (keyValuePair.isUsingKeyKeyword() && keyValuePair.getSeparator() == JsonKeyValuePairSeparator.VALUE) { + if (keyValuePair.isUsingKeyKeyword() + && keyValuePair.getSeparator() == JsonKeyValuePairSeparator.VALUE) { builder.append("KEY "); } builder.append(keyValuePair.getKey()); diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonFunctionType.java b/src/main/java/net/sf/jsqlparser/expression/JsonFunctionType.java index c61f471ec..821416c9c 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonFunctionType.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonFunctionType.java @@ -14,8 +14,7 @@ * @author Andreas Reichel */ public enum JsonFunctionType { - OBJECT, - ARRAY, + OBJECT, ARRAY, /** * Not used anymore diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java b/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java index 71547b1ff..156f3554b 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java @@ -28,8 +28,10 @@ public class JsonKeyValuePair implements Serializable { * 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, + boolean usingValueKeyword) { + this(key, value, usingKeyKeyword, usingValueKeyword ? JsonKeyValuePairSeparator.VALUE + : JsonKeyValuePairSeparator.COLON); } public JsonKeyValuePair(Object key, Object value, boolean usingKeyKeyword, @@ -66,7 +68,8 @@ public boolean isUsingValueKeyword() { */ @Deprecated public void setUsingValueKeyword(boolean usingValueKeyword) { - separator = usingValueKeyword ? JsonKeyValuePairSeparator.VALUE : JsonKeyValuePairSeparator.COLON; + separator = usingValueKeyword ? JsonKeyValuePairSeparator.VALUE + : JsonKeyValuePairSeparator.COLON; } /** diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePairSeparator.java b/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePairSeparator.java index 56e31385b..aa0e599a4 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePairSeparator.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePairSeparator.java @@ -4,8 +4,7 @@ * Describes the string used to separate the key from the value. */ public enum JsonKeyValuePairSeparator { - VALUE(" VALUE "), - COLON(":"), + VALUE(" VALUE "), COLON(":"), // Used in MySQL dialect COMMA(","), 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..6888d7cce 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -809,7 +809,12 @@ 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); private final Object value; private final boolean configurable; 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 4b30635da..1f2a761be 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -6953,146 +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() ) - ) - | - LOOKAHEAD(2) ( key = AllTableColumns(false) { isSingleEntryKeyValue = true; }) - | - key = AllColumns(false) { isSingleEntryKeyValue = true; } - | + // Key part + ( + // lookahead because key is a valid column name + LOOKAHEAD(2) ( + { usingKeyKeyword = true; } + ( + keyToken = { key = keyToken.image; } | key = Column() - | - keyToken = { key = keyToken.image; } - ) + ) + ) + | + LOOKAHEAD(16) ( key = AllTableColumns(false) { isWildcard = true; } ) + | + key = AllColumns(false) { isWildcard = true; } + | + key = Column() + | + key = Expression() + | + keyToken = { key = keyToken.image; } + ) - ( LOOKAHEAD(2) - ( ":" { kvSeparator = JsonKeyValuePairSeparator.COLON; } | - "," { if (!isSingleEntryKeyValue) kvSeparator = JsonKeyValuePairSeparator.COMMA; } | - "VALUE" { kvSeparator = JsonKeyValuePairSeparator.VALUE; } ) - ( - expression = Expression() - ) - [ { usingFormatJason = true; } ] - )? - { - if (expression != null) { - if (isSingleEntryKeyValue) { - result.add(new JsonKeyValuePair( key, null, false, JsonKeyValuePairSeparator.NOT_USED )); - result.add(new JsonKeyValuePair( expression, null, false, JsonKeyValuePairSeparator.NOT_USED )); - } else { - keyValuePair = new JsonKeyValuePair( key, expression, usingKeyKeyword, kvSeparator ); - keyValuePair.setUsingFormatJson( usingFormatJason ); - result.add(keyValuePair); - } - } else { - result.add(new JsonKeyValuePair( key, null, false, JsonKeyValuePairSeparator.NOT_USED )); - } - } + // 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; kvSeparator = null; isSingleEntryKeyValue = false; } - ( - LOOKAHEAD(2) ( - "KEY" { usingKeyKeyword = true; } ( keyToken = { key = keyToken.image; } | key = Column() ) - ) - | - LOOKAHEAD(2) ( key = AllTableColumns(false) { isSingleEntryKeyValue = true; } ) - | - key = Column() - | - keyToken = { key = keyToken.image; } + // 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; + } +} - ) - ( ":" { kvSeparator = JsonKeyValuePairSeparator.COLON; } | - "," { kvSeparator = JsonKeyValuePairSeparator.COMMA; } | - "VALUE" { kvSeparator = JsonKeyValuePairSeparator.VALUE; } ) - expression = Expression() { - if (isSingleEntryKeyValue) { - result.add(new JsonKeyValuePair( key, null, false, JsonKeyValuePairSeparator.NOT_USED )); - result.add(new JsonKeyValuePair( expression, null, false, JsonKeyValuePairSeparator.NOT_USED )); - } else { - keyValuePair = new JsonKeyValuePair( key, expression, usingKeyKeyword, kvSeparator ); - result.add(keyValuePair); - } - } - [ { keyValuePair.setUsingFormatJson( true ); } ] - )* - )? +JsonFunction JsonObjectBody() : { + JsonFunction result = new JsonFunction(JsonFunctionType.OBJECT); - [ - ( - { result.setOnNullType( JsonAggregateOnNullType.NULL ); } - ) - | - ( - { result.setOnNullType( JsonAggregateOnNullType.ABSENT ); } - ) - ] - [ - { result.setStrict(true); } - ] - [ - ( - { 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 4511be7f5..4bbd5e5cd 100644 --- a/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java @@ -11,6 +11,8 @@ 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; @@ -163,13 +165,23 @@ public void testObject() throws JSQLParserException { "SELECT JSON_OBJECT( KEY 'foo' VALUE bar FORMAT JSON, 'foo':bar, 'foo':bar ABSENT ON NULL WITHOUT UNIQUE KEYS) FROM dual ", true); - TestUtils.assertExpressionCanBeParsedAndDeparsed("json_object(null on null)", true); + // TestUtils.assertExpressionCanBeParsedAndDeparsed("json_object(null on null)", true); TestUtils.assertExpressionCanBeParsedAndDeparsed("json_object(absent on null)", true); 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 @@ -181,8 +193,15 @@ public void testObject() throws JSQLParserException { "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 { @@ -194,7 +213,20 @@ void testObjectOracle(String sqlStr) throws JSQLParserException { // 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" + "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", + + // This is valid syntax on oracle but makes parsing the key as an expression very hard + // because "null" is a valid expression + "SELECT json_object(null on null) FROM employees", }) void testInvalidObjectOracle(String sqlStr) { assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sqlStr)); @@ -298,10 +330,62 @@ 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); assertEquals(JsonFunctionType.OBJECT, jsonFunction.getType()); @@ -330,7 +414,9 @@ void testJavaMethodsStrict() throws JSQLParserException { jsonFunction.withStrict(false); - assertEquals("JSON_OBJECT( 'foo':bar, 'fob':baz FORMAT JSON ) ", jsonFunction.toString()); + assertEquals( + TestUtils.buildSqlString("JSON_OBJECT( 'foo':bar, 'fob':baz FORMAT JSON ) ", true), + TestUtils.buildSqlString(jsonFunction.toString(), true)); } diff --git a/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java index a40d52509..57513dc86 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 testNestedTablesInJSON_OBJECT() 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"); + } } From efcafec578c49ad1ead785c6323bb533a7c9ed11 Mon Sep 17 00:00:00 2001 From: Andreas Neumann Date: Sat, 11 Oct 2025 14:27:01 +0200 Subject: [PATCH 5/6] [feat] Disable Expression as JSON_OBJECT key value via feature flag --- .../java/net/sf/jsqlparser/parser/feature/Feature.java | 10 +++++++++- .../jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt | 2 +- .../net/sf/jsqlparser/expression/JsonFunctionTest.java | 6 +----- .../net/sf/jsqlparser/util/TablesNamesFinderTest.java | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) 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 6888d7cce..a33f05f02 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -814,7 +814,15 @@ public enum Feature { * MySQL allows a ',' as a separator between key and value entries. We allow that by default, * but it can be disabled here */ - allowCommaAsKeyValueSeparator(true); + 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; private final boolean configurable; diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 1f2a761be..0eceb687c 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -6985,7 +6985,7 @@ JsonKeyValuePair JsonKeyValuePair(boolean isFirstEntry) : { | key = Column() | - key = Expression() + LOOKAHEAD({getAsBoolean(Feature.allowExpressionAsJsonObjectKey)}) key = Expression() | keyToken = { key = keyToken.image; } ) diff --git a/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java index 4bbd5e5cd..5475f8ec7 100644 --- a/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java @@ -165,7 +165,7 @@ public void testObject() throws JSQLParserException { "SELECT JSON_OBJECT( KEY 'foo' VALUE bar FORMAT JSON, 'foo':bar, 'foo':bar ABSENT ON NULL WITHOUT UNIQUE KEYS) FROM dual ", true); - // TestUtils.assertExpressionCanBeParsedAndDeparsed("json_object(null on null)", true); + TestUtils.assertExpressionCanBeParsedAndDeparsed("json_object(null on null)", true); TestUtils.assertExpressionCanBeParsedAndDeparsed("json_object(absent on null)", true); @@ -223,10 +223,6 @@ void testObjectOracle(String sqlStr) throws JSQLParserException { "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", - - // This is valid syntax on oracle but makes parsing the key as an expression very hard - // because "null" is a valid expression - "SELECT json_object(null on null) FROM employees", }) void testInvalidObjectOracle(String sqlStr) { assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sqlStr)); diff --git a/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java index 57513dc86..a36143565 100644 --- a/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java +++ b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java @@ -715,7 +715,7 @@ void assertWithItemWithFunctionDeclarationReturnsTableInSelect() throws JSQLPars } @Test - void testNestedTablesInJSON_OBJECT() throws JSQLParserException { + 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" + From 050e98a3eb502ab046db54fa9a28fc20c5fe3fdf Mon Sep 17 00:00:00 2001 From: Andreas Neumann Date: Sat, 11 Oct 2025 14:32:47 +0200 Subject: [PATCH 6/6] [chore] spotlessApply --- src/main/java/net/sf/jsqlparser/parser/feature/Feature.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 a33f05f02..d786f5170 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -817,10 +817,10 @@ public enum Feature { allowCommaAsKeyValueSeparator(true), /** - * DB2 and Oracle allow Expressions as JSON_OBJECT key values. This clashes with Informix and Snowflake - * Json-Extraction syntax + * DB2 and Oracle allow Expressions as JSON_OBJECT key values. This clashes with Informix and + * Snowflake Json-Extraction syntax */ - allowExpressionAsJsonObjectKey( false) + allowExpressionAsJsonObjectKey(false) ;