Skip to content
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
76 changes: 18 additions & 58 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -13167,70 +13167,30 @@ JsonTableFunction.JsonTablePassingClause JsonTablePassingClause() : {
}
}

/** Compatibility entry point; both column response clauses use the same behaviors. */
JsonFunction.JsonOnResponseBehavior JsonTableOnEmptyBehavior() : {
JsonFunction.JsonOnResponseBehavior behavior = null;
JsonFunction.JsonOnResponseBehavior behavior;
}
{
behavior = JsonTableOnResponseBehavior()
{ return behavior; }
}

JsonFunction.JsonOnResponseBehavior JsonTableOnResponseBehavior() : {
JsonFunction.JsonOnResponseBehavior behavior;
Expression expression;
Token token;
}
{
(
<K_ERROR>
{
behavior = new JsonFunction.JsonOnResponseBehavior(
JsonFunction.JsonOnResponseBehaviorType.ERROR);
}
|
<K_NULL>
{
behavior = new JsonFunction.JsonOnResponseBehavior(
JsonFunction.JsonOnResponseBehaviorType.NULL);
}
|
<K_TRUE>
{
behavior = new JsonFunction.JsonOnResponseBehavior(
JsonFunction.JsonOnResponseBehaviorType.TRUE);
}
|
<K_FALSE>
{
behavior = new JsonFunction.JsonOnResponseBehavior(
JsonFunction.JsonOnResponseBehaviorType.FALSE);
}
|
<K_DEFAULT> expression = Expression()
{
behavior = new JsonFunction.JsonOnResponseBehavior(
JsonFunction.JsonOnResponseBehaviorType.DEFAULT, expression);
}
|
token = <S_IDENTIFIER>
{
if (!token.image.equalsIgnoreCase("EMPTY")) {
throw new ParseException(
"Expected EMPTY, ERROR, NULL or DEFAULT but found " + token.image);
}
}
(
LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("OBJECT") })
JsonKeyword("OBJECT")
{
behavior = new JsonFunction.JsonOnResponseBehavior(
JsonFunction.JsonOnResponseBehaviorType.EMPTY_OBJECT);
}
|
[ <K_ARRAY_LITERAL> ]
{
behavior = new JsonFunction.JsonOnResponseBehavior(
JsonFunction.JsonOnResponseBehaviorType.EMPTY_ARRAY);
}
)
behavior = JsonQueryOnResponseBehavior()
)
{
if (behavior != null) {
return behavior;
}
}
{ return behavior; }
}

JsonTableFunction.JsonTableWrapperClause JsonTableWrapperClause(boolean beforePathExpr) : {
Expand Down Expand Up @@ -13371,20 +13331,20 @@ JsonTableFunction.JsonTableColumnDefinition JsonTableColumnDefinition() : {
quotesClause = JsonTableQuotesClause() { valueColumnDefinition.setQuotesClause(quotesClause); }
]
[
LOOKAHEAD( JsonTableOnEmptyBehavior() <K_ON> <K_EMPTY> )
behavior = JsonTableOnEmptyBehavior()
LOOKAHEAD( JsonTableOnResponseBehavior() <K_ON> <K_EMPTY> )
behavior = JsonTableOnResponseBehavior()
<K_ON> <K_EMPTY>
{ valueColumnDefinition.setOnEmptyBehavior(behavior); }
]
[
LOOKAHEAD( JsonQueryOnResponseBehavior() <K_ON> <K_ERROR> )
behavior = JsonQueryOnResponseBehavior()
LOOKAHEAD( JsonTableOnResponseBehavior() <K_ON> <K_ERROR> )
behavior = JsonTableOnResponseBehavior()
<K_ON> <K_ERROR>
{ valueColumnDefinition.setOnErrorBehavior(behavior); }
]
[
LOOKAHEAD( JsonTableOnEmptyBehavior() <K_ON> <K_EMPTY> )
behavior = JsonTableOnEmptyBehavior()
LOOKAHEAD( JsonTableOnResponseBehavior() <K_ON> <K_EMPTY> )
behavior = JsonTableOnResponseBehavior()
<K_ON> <K_EMPTY>
{
valueColumnDefinition.setOnEmptyBehavior(behavior);
Expand Down
106 changes: 106 additions & 0 deletions src/test/java/net/sf/jsqlparser/expression/JsonTableResponseTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*-
* #%L
* JSQLParser library
* %%
* Copyright (C) 2004 - 2026 JSQLParser
* %%
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
* #L%
*/
package net.sf.jsqlparser.expression;

import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.JsonTableFunction.JsonTableValueColumnDefinition;
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.TableFunction;
import net.sf.jsqlparser.test.TestUtils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import java.util.ArrayList;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

class JsonTableResponseTest {
@ParameterizedTest
@ValueSource(strings = {
"DEFAULT '9' ON ERROR",
"DEFAULT '0' ON EMPTY DEFAULT '9' ON ERROR",
"DEFAULT '9' ON ERROR DEFAULT '0' ON EMPTY",
"NULL ON EMPTY DEFAULT '9' ON ERROR",
"ERROR ON EMPTY DEFAULT '9' ON ERROR"
})
void defaultsRoundTripInEitherOrder(String response) throws JSQLParserException {
String sql = sql("n INT PATH '$.n' " + response);
for (Dialect dialect : new Dialect[] {null, Dialect.MYSQL, Dialect.ORACLE}) {
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sql,
true, parser -> {
if (dialect != null) {
parser.withDialect(dialect);
}
});
TestUtils.assertSqlCanBeParsedAndDeparsed(select.toString(), false,
parser -> {
if (dialect != null) {
parser.withDialect(dialect);
}
});
}
}

@Test
void defaultExpressionsAreVisitedAndEditable() throws JSQLParserException {
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(
sql("n INT PATH '$.n' DEFAULT '0' ON EMPTY DEFAULT '9' ON ERROR"));
JsonTableFunction table =
(JsonTableFunction) ((TableFunction) select.getFromItem()).getFunction();
JsonTableValueColumnDefinition column =
(JsonTableValueColumnDefinition) table.getColumnsClause().getColumnDefinitions()
.get(0);
assertEquals(JsonFunction.JsonOnResponseBehaviorType.DEFAULT,
column.getOnErrorBehavior().getType());
assertEquals("9", ((StringValue) column.getOnErrorBehavior().getExpression()).getValue());
List<String> visited = new ArrayList<>();
table.accept(new ExpressionVisitorAdapter<Void>() {
@Override
public <S> Void visit(StringValue value, S context) {
visited.add(value.getValue());
if ("9".equals(value.getValue())) {
value.setValue("7");
}
return null;
}
}, null);
assertTrue(visited.containsAll(List.of("$.n", "0", "9")));
TestUtils.assertStatementCanBeDeparsedAs(select,
sql("n INT PATH '$.n' DEFAULT '0' ON EMPTY DEFAULT '7' ON ERROR"), true);
column.setOnErrorBehavior(new JsonFunction.JsonOnResponseBehavior(
JsonFunction.JsonOnResponseBehaviorType.DEFAULT, new StringValue("8")));
column.setOnEmptyAfterOnError(true);
TestUtils.assertStatementCanBeDeparsedAs(select,
sql("n INT PATH '$.n' DEFAULT '8' ON ERROR DEFAULT '0' ON EMPTY"), true);
TestUtils.assertSqlCanBeParsedAndDeparsed(select.toString());
}

@Test
void nestedDefaultsRoundTrip() throws JSQLParserException {
TestUtils.assertSqlCanBeParsedAndDeparsed(sql(
"NESTED PATH '$.items[*]' COLUMNS (n INT PATH '$.n' DEFAULT '9' ON ERROR)"));
}

@ParameterizedTest
@ValueSource(
strings = {"NULL", "ERROR", "TRUE", "FALSE", "EMPTY", "EMPTY ARRAY", "EMPTY OBJECT"})
void sharedNonDefaultResponsesRoundTrip(String behavior) throws JSQLParserException {
TestUtils.assertSqlCanBeParsedAndDeparsed(sql("n PATH '$.n' " + behavior + " ON ERROR"));
TestUtils.assertSqlCanBeParsedAndDeparsed(sql("n PATH '$.n' " + behavior + " ON EMPTY"));
}

private static String sql(String column) {
return "SELECT * FROM JSON_TABLE('[{\"n\":1},{},{\"n\":\"bad\"}]', '$[*]' COLUMNS ("
+ column + ")) AS jt";
}
}
Loading