Skip to content

Json function Improvements #1506

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Apr 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ plugins {
}

group = 'com.github.jsqlparser'
version = '4.3-SNAPSHOT'
version = '4.4-SNAPSHOT'
description = 'JSQLParser library'
java.sourceCompatibility = JavaVersion.VERSION_1_8

Expand Down
23 changes: 23 additions & 0 deletions src/main/java/net/sf/jsqlparser/expression/JsonFunction.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ public void add(int i, JsonFunctionExpression expression) {
expressions.add(i, expression);
}

public boolean isEmpty() {
return keyValuePairs.isEmpty();
}

public JsonAggregateOnNullType getOnNullType() {
return onNullType;
}
Expand Down Expand Up @@ -122,6 +126,9 @@ public StringBuilder append(StringBuilder builder) {
case POSTGRES_OBJECT:
appendPostgresObject(builder);
break;
case MYSQL_OBJECT:
appendMySqlObject(builder);
break;
case ARRAY:
appendArray(builder);
break;
Expand Down Expand Up @@ -200,6 +207,22 @@ public StringBuilder appendPostgresObject(StringBuilder builder) {
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( ");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ public enum JsonFunctionType {
OBJECT
, ARRAY
, POSTGRES_OBJECT
, MYSQL_OBJECT
}
116 changes: 60 additions & 56 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -3729,6 +3729,8 @@ Expression PrimaryExpression() #PrimaryExpression:

| LOOKAHEAD(JsonExpression()) retval=JsonExpression()

| LOOKAHEAD(Function()) retval=Function() [ LOOKAHEAD(2) retval = AnalyticExpression( (Function) retval ) ]

| LOOKAHEAD(JsonFunction()) retval = JsonFunction()

| LOOKAHEAD(JsonAggregateFunction()) retval = JsonAggregateFunction()
Expand All @@ -3737,7 +3739,7 @@ Expression PrimaryExpression() #PrimaryExpression:

| LOOKAHEAD(FullTextSearch()) retval = FullTextSearch()

| LOOKAHEAD(Function()) retval=Function() [ LOOKAHEAD(2) retval = AnalyticExpression( (Function) retval ) ]


| LOOKAHEAD(2) retval = IntervalExpression() { dateExpressionAllowed = false; }

Expand Down Expand Up @@ -3975,11 +3977,13 @@ JsonFunction JsonFunction() : {
JsonFunction result = new JsonFunction();
boolean usingKeyKeyword = false;
boolean usingValueKeyword = false;
boolean usingFormatJason = false;
Token keyToken;
Token valueToken = null;
Column column = null;
JsonKeyValuePair keyValuePair;

Expression expression;
Expression expression = null;
JsonFunctionExpression functionExpression;

}
Expand All @@ -3988,64 +3992,64 @@ JsonFunction JsonFunction() : {
(
( <K_JSON_OBJECT>
"(" { result.setType( JsonFunctionType.OBJECT ); }

(
// --- First Element
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 = <S_CHAR_LITERAL>
[ "," valueToken = <S_CHAR_LITERAL> ]
{ keyValuePair = new JsonKeyValuePair( keyToken.image, valueToken !=null ? valueToken.image : null, false, false ); result.add(keyValuePair); }
)
|
(
(
// SQL2016 compliant Syntax
(
[ "KEY" { usingKeyKeyword = true; } ]
keyToken = <S_CHAR_LITERAL>

( LOOKAHEAD(2)
( ":" | "," { result.setType( JsonFunctionType.POSTGRES_OBJECT ); } | "VALUE" { usingValueKeyword = true; } )
(
expression = Expression()
)
[ <K_FORMAT> <K_JSON> { usingFormatJason = true; } ]
)? {
if (expression !=null) {
keyValuePair = new JsonKeyValuePair( keyToken.image, expression, usingKeyKeyword, usingValueKeyword );
keyValuePair.setUsingFormatJson( usingFormatJason );
result.add(keyValuePair);
} else {
result.setType( JsonFunctionType.POSTGRES_OBJECT );
keyValuePair = new JsonKeyValuePair( keyToken.image, null, false, false );
result.add(keyValuePair);
}
}

// --- Next Elements
( "," { usingKeyKeyword = false; usingValueKeyword = false; }
[ "KEY" { usingKeyKeyword = true; } ]
keyToken = <S_CHAR_LITERAL>
( ":" | "," { result.setType( JsonFunctionType.MYSQL_OBJECT ); } | "VALUE" { usingValueKeyword = true; } )
(
expression = Expression() { keyValuePair = new JsonKeyValuePair( keyToken.image, expression, usingKeyKeyword, usingValueKeyword ); result.add(keyValuePair); }
)
[ <K_FORMAT> <K_JSON> { keyValuePair.setUsingFormatJson( true ); } ]
)*
)
)?

// SQL2016 compliant Syntax
[ "KEY" { usingKeyKeyword = true; } ]
[
(
<K_NULL> <K_ON> <K_NULL> { result.setOnNullType( JsonAggregateOnNullType.NULL ); }
)
|
(
<K_ABSENT> <K_ON> <K_NULL> { result.setOnNullType( JsonAggregateOnNullType.ABSENT ); }
)
]

( keyToken = <DT_ZONE> | keyToken = <S_DOUBLE> | keyToken = <S_LONG> | keyToken = <S_HEX> | keyToken = <S_CHAR_LITERAL> | keyToken = <S_IDENTIFIER> | keyToken = <S_QUOTED_IDENTIFIER> )
( ":" | "VALUE" { usingValueKeyword = true; } )
( valueToken = <S_IDENTIFIER> | valueToken = <S_QUOTED_IDENTIFIER> ) { keyValuePair = new JsonKeyValuePair( keyToken.image, valueToken.image, usingKeyKeyword, usingValueKeyword ); result.add(keyValuePair); }
[
(
<K_WITH> <K_UNIQUE> <K_KEYS> { result.setUniqueKeysType( JsonAggregateUniqueKeysType.WITH ); }
)
|
(
<K_WITHOUT> <K_UNIQUE> <K_KEYS> { result.setUniqueKeysType( JsonAggregateUniqueKeysType.WITHOUT ); }
)
]
)

[ <K_FORMAT> <K_JSON> { keyValuePair.setUsingFormatJson( true ); } ]

// --- Next Elements
( "," { usingKeyKeyword = false; usingValueKeyword = false; }
[ "KEY" { usingKeyKeyword = true; } ]
keyToken = <S_IDENTIFIER>
( ":" | "VALUE" { usingValueKeyword = true; } )
// token = <DT_ZONE> | <S_DOUBLE> | <S_LONG> | <S_HEX> | <S_CHAR_LITERAL> { result.setValue( token.image ); }
valueToken = <S_IDENTIFIER> { keyValuePair = new JsonKeyValuePair( keyToken.image, valueToken.image, usingKeyKeyword, usingValueKeyword ); result.add(keyValuePair); }

[ <K_FORMAT> <K_JSON> { keyValuePair.setUsingFormatJson( true ); } ]
)*
)*

[
(
<K_NULL> <K_ON> <K_NULL> { result.setOnNullType( JsonAggregateOnNullType.NULL ); }
)
|
(
<K_ABSENT> <K_ON> <K_NULL> { result.setOnNullType( JsonAggregateOnNullType.ABSENT ); }
)
]

[
(
<K_WITH> <K_UNIQUE> <K_KEYS> { result.setUniqueKeysType( JsonAggregateUniqueKeysType.WITH ); }
)
|
(
<K_WITHOUT> <K_UNIQUE> <K_KEYS> { result.setUniqueKeysType( JsonAggregateUniqueKeysType.WITHOUT ); }
)
]
")"
)
|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ public void testAtTimeZoneExpression() throws JSQLParserException {
public void testJsonFunction() throws JSQLParserException {
ExpressionVisitorAdapter adapter = new ExpressionVisitorAdapter();
CCJSqlParserUtil
.parseExpression("JSON_OBJECT( KEY foo VALUE bar, KEY foo VALUE bar)")
.parseExpression("JSON_OBJECT( KEY 'foo' VALUE bar, KEY 'foo' VALUE bar)")
.accept(adapter);
CCJSqlParserUtil
.parseExpression("JSON_ARRAY( (SELECT * from dual) )")
Expand Down
73 changes: 52 additions & 21 deletions src/test/java/net/sf/jsqlparser/expression/JsonFunctionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,10 @@
*/
package net.sf.jsqlparser.expression;

import java.util.Objects;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.test.TestUtils;
import org.junit.jupiter.api.Assertions;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;

/**
Expand Down Expand Up @@ -69,17 +64,17 @@ public void testObjectBuilder() throws JSQLParserException {
JsonKeyValuePair keyValuePair2 = new JsonKeyValuePair("foo", "bar", false, false).withUsingKeyKeyword(true).withUsingValueKeyword(true).withUsingFormatJson(false);

// this should work because we compare based on KEY only
assertEquals(keyValuePair1, keyValuePair2);
Assertions.assertEquals(keyValuePair1, keyValuePair2);

// this must fail because all the properties are considered
assertFalse(Objects.equals(keyValuePair1.toString(), keyValuePair2.toString()));
Assertions.assertNotEquals(keyValuePair1.toString(), keyValuePair2.toString());

JsonKeyValuePair keyValuePair3 = new JsonKeyValuePair("foo", "bar", false, false).withUsingKeyKeyword(false).withUsingValueKeyword(false).withUsingFormatJson(false);
assertNotNull(keyValuePair3);
assertEquals(keyValuePair3, keyValuePair3);
assertFalse(Objects.equals(keyValuePair3, f));
Assertions.assertNotNull(keyValuePair3);
Assertions.assertEquals(keyValuePair3, keyValuePair3);
Assertions.assertNotEquals(keyValuePair3, f);

assertTrue(keyValuePair3.hashCode() != 0);
Assertions.assertTrue(keyValuePair3.hashCode() != 0);

f.add(keyValuePair2);
}
Expand All @@ -95,7 +90,7 @@ public void testArrayBuilder() throws JSQLParserException {
JsonFunctionExpression expression2 = new JsonFunctionExpression(new NullValue()).withUsingFormatJson(
true);

assertTrue(Objects.equals(expression1.toString(), expression2.toString()));
Assertions.assertEquals(expression1.toString(), expression2.toString());

f.add(expression1);
f.add(expression2);
Expand Down Expand Up @@ -131,24 +126,24 @@ public void testArrayAgg() throws JSQLParserException {
@Test
public void testObject() throws JSQLParserException {
TestUtils.assertSqlCanBeParsedAndDeparsed(
"SELECT JSON_OBJECT( KEY foo VALUE bar, KEY foo VALUE bar) FROM dual ", true);
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECT( foo:bar, foo:bar) FROM dual ",
"SELECT JSON_OBJECT( KEY 'foo' VALUE bar, KEY 'foo' VALUE bar) FROM dual ", true);
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECT( 'foo' : bar, 'foo' : bar) FROM dual ",
true);
TestUtils.assertSqlCanBeParsedAndDeparsed(
"SELECT JSON_OBJECT( foo:bar, foo:bar FORMAT JSON) FROM dual ", true);
"SELECT JSON_OBJECT( 'foo':bar, 'foo':bar FORMAT JSON) FROM dual ", true);
TestUtils.assertSqlCanBeParsedAndDeparsed(
"SELECT JSON_OBJECT( KEY foo VALUE bar, foo:bar FORMAT JSON, foo:bar NULL ON NULL) FROM dual ",
"SELECT JSON_OBJECT( KEY 'foo' VALUE bar, 'foo':bar FORMAT JSON, 'foo':bar NULL ON NULL) FROM dual ",
true);
TestUtils.assertSqlCanBeParsedAndDeparsed(
"SELECT JSON_OBJECT( KEY foo VALUE bar FORMAT JSON, foo:bar, foo:bar ABSENT ON NULL) FROM dual ",
"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 ",
"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 ",
"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);
Expand All @@ -158,6 +153,42 @@ public void testObject() throws JSQLParserException {
TestUtils.assertExpressionCanBeParsedAndDeparsed("json_object()", true);
}

@Test
public void testObjectWithExpression() throws JSQLParserException {
TestUtils.assertSqlCanBeParsedAndDeparsed(
"SELECT JSON_OBJECT( KEY 'foo' VALUE cast( bar AS VARCHAR(40)), KEY 'foo' VALUE bar) FROM dual ", true);

TestUtils.assertSqlCanBeParsedAndDeparsed(
"SELECT JSON_ARRAYAGG(obj) FROM (SELECT trt.relevance_id,JSON_OBJECT('id',CAST(trt.id AS CHAR),'taskName',trt.task_name,'openStatus',trt.open_status,'taskSort',trt.task_sort) as obj FROM tb_review_task trt ORDER BY trt.task_sort ASC)", true);
}

@Test
public void testObjectIssue1504() throws JSQLParserException {
TestUtils.assertSqlCanBeParsedAndDeparsed(
"SELECT JSON_OBJECT(key 'person' value tp.account) obj", true);

TestUtils.assertSqlCanBeParsedAndDeparsed(
"SELECT JSON_OBJECT(key 'person' value tp.account, key 'person' value tp.account) obj", true);

TestUtils.assertSqlCanBeParsedAndDeparsed(
"SELECT JSON_OBJECT( 'person' : tp.account) obj", true);

TestUtils.assertSqlCanBeParsedAndDeparsed(
"SELECT JSON_OBJECT( 'person' : tp.account, 'person' : tp.account) obj", true);

TestUtils.assertSqlCanBeParsedAndDeparsed(
"SELECT JSON_OBJECT( 'person' : '1', 'person' : '2') obj", true);

TestUtils.assertSqlCanBeParsedAndDeparsed(
"SELECT JSON_OBJECT( 'person' VALUE tp.person, 'account' VALUE tp.account) obj", true);
}

@Test
public void testObjectMySQL() throws JSQLParserException {
TestUtils.assertSqlCanBeParsedAndDeparsed(
"SELECT JSON_OBJECT('person', tp.person, 'account', tp.account) obj", true);
}

@Test
public void testArray() throws JSQLParserException {
TestUtils.assertSqlCanBeParsedAndDeparsed(
Expand Down Expand Up @@ -213,14 +244,14 @@ public void testIssue1371() throws JSQLParserException {

@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)";
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));
Assertions.assertEquals(new JsonKeyValuePair("'foo'", "bar", true, true), jsonFunction.getKeyValuePair(0));

jsonFunction.setOnNullType(JsonAggregateOnNullType.NULL);
Assertions.assertEquals(JsonAggregateOnNullType.ABSENT, jsonFunction.withOnNullType(JsonAggregateOnNullType.ABSENT).getOnNullType());
Expand Down
5 changes: 3 additions & 2 deletions src/test/java/net/sf/jsqlparser/test/TestUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ public class TestUtils {
private static final Pattern SQL_SANITATION_PATTERN
= Pattern.compile("(\\s+)", Pattern.MULTILINE);

// Assure SPACE around Syntax Characters
private static final Pattern SQL_SANITATION_PATTERN2
= Pattern.compile("\\s*([!/,()=+\\-*|\\]<>])\\s*", Pattern.MULTILINE);
= Pattern.compile("\\s*([!/,()=+\\-*|\\]<>:])\\s*", Pattern.MULTILINE);

/**
* @param statement
Expand Down Expand Up @@ -265,7 +266,7 @@ public static String buildSqlString(final String originalSql, boolean laxDeparsi
// redundant white space
sanitizedSqlStr = SQL_SANITATION_PATTERN.matcher(sanitizedSqlStr).replaceAll(" ");

// replace some more stuff
// assure spacing around Syntax Characters
sanitizedSqlStr = SQL_SANITATION_PATTERN2.matcher(sanitizedSqlStr).replaceAll("$1");
return sanitizedSqlStr.trim().toLowerCase();
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ public void testJsonFunctionExpression() throws JSQLParserException {
public void testJsonAggregartFunctionExpression() throws JSQLParserException {
validateNoErrors("SELECT JSON_ARRAYAGG( a FORMAT JSON ABSENT ON NULL ) FILTER( WHERE name = 'Raj' ) OVER( PARTITION BY name ) FROM mytbl", 1,
EXPRESSIONS);
validateNoErrors("SELECT JSON_OBJECT( KEY foo VALUE bar FORMAT JSON, foo:bar, foo:bar ABSENT ON NULL) FROM mytbl", 1,
validateNoErrors("SELECT JSON_OBJECT( KEY 'foo' VALUE bar FORMAT JSON, 'foo':bar, 'foo':bar ABSENT ON NULL) FROM mytbl", 1,
EXPRESSIONS);
}

Expand Down