From 7afe21891895b585b2bd4895d396d90f36650504 Mon Sep 17 00:00:00 2001 From: minleejae Date: Wed, 23 Sep 2026 09:36:09 +0900 Subject: [PATCH] fix: share JSON_TABLE column response parsing Signed-off-by: minleejae --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 76 +++---------- .../expression/JsonTableResponseTest.java | 106 ++++++++++++++++++ 2 files changed, 124 insertions(+), 58 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/expression/JsonTableResponseTest.java diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 1411612de..cbfc2c98b 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -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; } { ( - - { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.ERROR); - } - | - - { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.NULL); - } - | - - { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.TRUE); - } - | - - { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.FALSE); - } - | expression = Expression() { behavior = new JsonFunction.JsonOnResponseBehavior( JsonFunction.JsonOnResponseBehaviorType.DEFAULT, expression); } | - token = - { - 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); - } - | - [ ] - { - behavior = new JsonFunction.JsonOnResponseBehavior( - JsonFunction.JsonOnResponseBehaviorType.EMPTY_ARRAY); - } - ) + behavior = JsonQueryOnResponseBehavior() ) - { - if (behavior != null) { - return behavior; - } - } + { return behavior; } } JsonTableFunction.JsonTableWrapperClause JsonTableWrapperClause(boolean beforePathExpr) : { @@ -13371,20 +13331,20 @@ JsonTableFunction.JsonTableColumnDefinition JsonTableColumnDefinition() : { quotesClause = JsonTableQuotesClause() { valueColumnDefinition.setQuotesClause(quotesClause); } ] [ - LOOKAHEAD( JsonTableOnEmptyBehavior() ) - behavior = JsonTableOnEmptyBehavior() + LOOKAHEAD( JsonTableOnResponseBehavior() ) + behavior = JsonTableOnResponseBehavior() { valueColumnDefinition.setOnEmptyBehavior(behavior); } ] [ - LOOKAHEAD( JsonQueryOnResponseBehavior() ) - behavior = JsonQueryOnResponseBehavior() + LOOKAHEAD( JsonTableOnResponseBehavior() ) + behavior = JsonTableOnResponseBehavior() { valueColumnDefinition.setOnErrorBehavior(behavior); } ] [ - LOOKAHEAD( JsonTableOnEmptyBehavior() ) - behavior = JsonTableOnEmptyBehavior() + LOOKAHEAD( JsonTableOnResponseBehavior() ) + behavior = JsonTableOnResponseBehavior() { valueColumnDefinition.setOnEmptyBehavior(behavior); diff --git a/src/test/java/net/sf/jsqlparser/expression/JsonTableResponseTest.java b/src/test/java/net/sf/jsqlparser/expression/JsonTableResponseTest.java new file mode 100644 index 000000000..a63d5aa80 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/JsonTableResponseTest.java @@ -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 visited = new ArrayList<>(); + table.accept(new ExpressionVisitorAdapter() { + @Override + public 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"; + } +}