From 25fe2d608c7efaffee54a64e451ff9d54a83ffdf Mon Sep 17 00:00:00 2001 From: Andreas Reichel Date: Sat, 16 Oct 2021 20:40:12 +0700 Subject: [PATCH 1/2] Fixes #1371 Postgres specific JSON_OBJECT syntax supporting: SELECT json_object('{a, 1, b, 2}'); SELECT json_object('{{a, 1}, {b, 2}}'); SELECT json_object('{a, b}', '{1,2 }'); --- .../jsqlparser/expression/JsonFunction.java | 19 +++++++++ .../expression/JsonFunctionType.java | 1 + .../expression/JsonKeyValuePair.java | 2 +- .../sf/jsqlparser/parser/feature/Feature.java | 5 +++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 42 +++++++++++++------ .../expression/JsonFunctionTest.java | 8 ++++ .../net/sf/jsqlparser/test/TestUtils.java | 16 +++---- 7 files changed, 68 insertions(+), 25 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java index 41c480d93..c3fcbc8e1 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java @@ -12,6 +12,9 @@ import java.util.ArrayList; import java.util.Objects; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.parser.feature.FeatureConfiguration; +import net.sf.jsqlparser.parser.feature.FeatureSet; /** * @@ -119,6 +122,9 @@ public StringBuilder append(StringBuilder builder) { case OBJECT: appendObject(builder); break; + case POSTGRES_OBJECT: + appendPostgresObject(builder); + break; case ARRAY: appendArray(builder); break; @@ -184,6 +190,19 @@ public StringBuilder appendObject(StringBuilder builder) { 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; + } + @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) public StringBuilder appendArray(StringBuilder builder) { builder.append("JSON_ARRAY( "); diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonFunctionType.java b/src/main/java/net/sf/jsqlparser/expression/JsonFunctionType.java index 750a32109..b044d191d 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonFunctionType.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonFunctionType.java @@ -17,4 +17,5 @@ public enum JsonFunctionType { OBJECT , ARRAY + , POSTGRES_OBJECT } diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java b/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java index 7e52ffeb9..740b8eaf5 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java @@ -27,7 +27,7 @@ public class JsonKeyValuePair { public JsonKeyValuePair(String key, Object value, boolean usingKeyKeyword, boolean usingValueKeyword) { this.key = Objects.requireNonNull(key, "The KEY of the Pair must not be null"); - this.value = Objects.requireNonNull(value, "The VALUE of the Pair must not be null"); + this.value = value; this.usingKeyKeyword = usingKeyKeyword; this.usingValueKeyword = usingValueKeyword; } 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 9d6fe2eda..baaf40138 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -714,6 +714,11 @@ public enum Feature { */ allowSquareBracketQuotation(false), + /** + allow parsing of RDBMS specific syntax by switching off SQL Standard Compliant Syntax + */ + allowPostgresSpecificSyntax(false), + // PERFORMANCE /** diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index be9cd46d8..4b7194cf6 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -3910,7 +3910,7 @@ JsonFunction JsonFunction() : { boolean usingKeyKeyword = false; boolean usingValueKeyword = false; Token keyToken; - Token valueToken; + Token valueToken = null; JsonKeyValuePair keyValuePair; Expression expression; @@ -3921,22 +3921,39 @@ JsonFunction JsonFunction() : { ( ( ( - "(" { result.setType( JsonFunctionType.OBJECT ); } - + "(" { result.setType( JsonFunctionType.OBJECT ); } + ( // --- First Element - [ "KEY" { usingKeyKeyword = true; } ] - ( keyToken = | keyToken = | keyToken = | keyToken = | keyToken = | keyToken = | keyToken = ) - ( ":" | "VALUE" { usingValueKeyword = true; } ) - ( valueToken = | valueToken = ) { keyValuePair = new JsonKeyValuePair( keyToken.image, valueToken.image, usingKeyKeyword, usingValueKeyword ); result.add(keyValuePair); } + LOOKAHEAD(2) + ( + // Postgres Specific Syntax: + // SELECT json_object('{a, 1, b, 2}'); + // SELECT json_object('{{a, 1}, {b, 2}}'); + // SELECT json_object('{a, b}', '{1,2 }'); + { result.setType( JsonFunctionType.POSTGRES_OBJECT ); } + keyToken = + [ "," valueToken = ] + { keyValuePair = new JsonKeyValuePair( keyToken.image, valueToken !=null ? valueToken.image : null, false, false ); result.add(keyValuePair); } + ) + | + ( + + // SQL2016 compliant Syntax + [ "KEY" { usingKeyKeyword = true; } ] + + ( keyToken = | keyToken = | keyToken = | keyToken = | keyToken = | keyToken = | keyToken = ) + ( ":" | "VALUE" { usingValueKeyword = true; } ) + ( valueToken = | valueToken = ) { keyValuePair = new JsonKeyValuePair( keyToken.image, valueToken.image, usingKeyKeyword, usingValueKeyword ); result.add(keyValuePair); } + ) [ { keyValuePair.setUsingFormatJson( true ); } ] // --- Next Elements ( "," { usingKeyKeyword = false; usingValueKeyword = false; } [ "KEY" { usingKeyKeyword = true; } ] - keyToken = - ( ":" | "VALUE" { usingValueKeyword = true; } ) + keyToken = + ( ":" | "VALUE" { usingValueKeyword = true; } ) // token = | | | | { result.setValue( token.image ); } valueToken = { keyValuePair = new JsonKeyValuePair( keyToken.image, valueToken.image, usingKeyKeyword, usingValueKeyword ); result.add(keyValuePair); } @@ -3944,12 +3961,12 @@ JsonFunction JsonFunction() : { )* )* - [ - ( + [ + ( { result.setOnNullType( JsonAggregateOnNullType.NULL ); } ) | - ( + ( { result.setOnNullType( JsonAggregateOnNullType.ABSENT ); } ) ] @@ -3963,7 +3980,6 @@ JsonFunction JsonFunction() : { { result.setUniqueKeysType( JsonAggregateUniqueKeysType.WITHOUT ); } ) ] - ")" ) | diff --git a/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java index e15dd9197..92cdb8700 100644 --- a/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java @@ -190,4 +190,12 @@ public void testIssue1260() throws JSQLParserException { ")\n" + "from SYSIBM.DUAL", true); } + + + @Test + public void testIssue1371() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT json_object('{a, 1, b, 2}')", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT json_object('{{a, 1}, {b, 2}}')", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT json_object('{a, b}', '{1,2 }')", true); + } } diff --git a/src/test/java/net/sf/jsqlparser/test/TestUtils.java b/src/test/java/net/sf/jsqlparser/test/TestUtils.java index e3617f357..309f3fbcc 100644 --- a/src/test/java/net/sf/jsqlparser/test/TestUtils.java +++ b/src/test/java/net/sf/jsqlparser/test/TestUtils.java @@ -304,7 +304,7 @@ public static void assertOracleHintExists(String sql, boolean assertDeparser, St OracleHint hint = ps.getOracleHint(); assertNotNull(hint); assertEquals(hints[0], hint.getValue()); - } else { + } else if (stmt.getSelectBody() instanceof SetOperationList) { SetOperationList setop = (SetOperationList) stmt.getSelectBody(); for (int i = 0; i < setop.getSelects().size(); i++) { @@ -318,22 +318,16 @@ public static void assertOracleHintExists(String sql, boolean assertDeparser, St } } } - } } else if (statement instanceof Update) { Update stmt = (Update) statement; OracleHint hint = stmt.getOracleHint(); assertNotNull(hint); assertEquals(hints[0], hint.getValue()); } else if (statement instanceof Insert) { - Insert stmt = (Insert) statement; - OracleHint hint = stmt.getOracleHint(); - assertNotNull(hint); - assertEquals(hints[0], hint.getValue()); - } else if (statement instanceof Update) { - Update stmt = (Update) statement; - OracleHint hint = stmt.getOracleHint(); - assertNotNull(hint); - assertEquals(hints[0], hint.getValue()); + Insert stmt = (Insert) statement; + OracleHint hint = stmt.getOracleHint(); + assertNotNull(hint); + assertEquals(hints[0], hint.getValue()); } else if (statement instanceof Delete) { Delete stmt = (Delete) statement; OracleHint hint = stmt.getOracleHint(); From 1edeb5ffda51692285165437f4d7d124bc7b96b6 Mon Sep 17 00:00:00 2001 From: Andreas Reichel Date: Sat, 16 Oct 2021 21:53:18 +0700 Subject: [PATCH 2/2] Improve Test Coverage --- .../jsqlparser/expression/JsonFunction.java | 13 ++++----- .../expression/JsonFunctionTest.java | 28 +++++++++++++++++++ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java index c3fcbc8e1..54d269533 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java @@ -12,9 +12,6 @@ import java.util.ArrayList; import java.util.Objects; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; -import net.sf.jsqlparser.parser.feature.Feature; -import net.sf.jsqlparser.parser.feature.FeatureConfiguration; -import net.sf.jsqlparser.parser.feature.FeatureSet; /** * @@ -130,8 +127,6 @@ public StringBuilder append(StringBuilder builder) { break; default: // this should never happen really - throw new UnsupportedOperationException("JSON Aggregate Function of the type " - + functionType.name() + " has not been implemented yet."); } return builder; } @@ -196,7 +191,9 @@ 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()); + if (keyValuePair.getValue()!=null) { + builder.append(", ").append(keyValuePair.getValue()); + } } builder.append(" ) "); @@ -222,10 +219,10 @@ public StringBuilder appendArray(StringBuilder builder) { builder.append(" NULL ON NULL "); break; case ABSENT: - builder.append(" ABSENT On NULL "); + builder.append(" ABSENT ON NULL "); break; default: - // "ON NULL" was ommitted + // "ON NULL" was omitted } } builder.append(") "); diff --git a/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java index 92cdb8700..1116666e4 100644 --- a/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java @@ -13,8 +13,10 @@ import java.util.Objects; import junit.framework.Assert; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.test.TestUtils; import org.junit.Test; +import org.junit.jupiter.api.Assertions; /** * @@ -138,6 +140,14 @@ public void testObject() throws JSQLParserException { "SELECT JSON_OBJECT( KEY foo VALUE bar FORMAT JSON, foo:bar, foo:bar ABSENT ON NULL) FROM dual ", true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT JSON_OBJECT( KEY foo VALUE bar FORMAT JSON, foo:bar, foo:bar ABSENT ON NULL WITH UNIQUE KEYS) FROM dual ", + true); + + TestUtils.assertSqlCanBeParsedAndDeparsed( + "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(absent on null)", true); @@ -198,4 +208,22 @@ public void testIssue1371() throws JSQLParserException { TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT json_object('{{a, 1}, {b, 2}}')", true); TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT json_object('{a, b}', '{1,2 }')", true); } + + @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)"; + JsonFunction jsonFunction = (JsonFunction) CCJSqlParserUtil.parseExpression(expressionStr); + + Assertions.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), jsonFunction.getKeyValuePair(0)); + + jsonFunction.setOnNullType(JsonAggregateOnNullType.NULL); + Assertions.assertEquals(JsonAggregateOnNullType.ABSENT, jsonFunction.withOnNullType(JsonAggregateOnNullType.ABSENT).getOnNullType()); + + jsonFunction.setUniqueKeysType(JsonAggregateUniqueKeysType.WITH); + Assertions.assertEquals(JsonAggregateUniqueKeysType.WITH, jsonFunction.withUniqueKeysType(JsonAggregateUniqueKeysType.WITH).getUniqueKeysType()); + } }