From 643ea842d692743bb9a5e3c0b97b4f75f0adc7ab Mon Sep 17 00:00:00 2001 From: minleejae Date: Wed, 23 Sep 2026 09:39:45 +0900 Subject: [PATCH] feat: model MySQL JSON_TABLE column collations Signed-off-by: minleejae --- .../expression/JsonTableFunction.java | 14 +++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 9 ++ .../expression/JsonTableCollationTest.java | 86 +++++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 src/test/java/net/sf/jsqlparser/expression/JsonTableCollationTest.java diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonTableFunction.java b/src/main/java/net/sf/jsqlparser/expression/JsonTableFunction.java index 5ee166e6fc..32330e80a0 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonTableFunction.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonTableFunction.java @@ -474,6 +474,7 @@ public static class JsonTableValueColumnDefinition extends JsonTableColumnDefini private String columnName; private boolean forOrdinality; private ColDataType dataType; + private String collation; private boolean formatJson; private boolean exists; private boolean onEmptyAfterOnError; @@ -526,6 +527,16 @@ public JsonTableValueColumnDefinition setDataType(ColDataType dataType) { return this; } + /** Returns the column collation, including any identifier quoting, or null if absent. */ + public String getCollation() { + return collation; + } + + public JsonTableValueColumnDefinition setCollation(String collation) { + this.collation = collation; + return this; + } + public boolean isFormatJson() { return formatJson; } @@ -626,6 +637,9 @@ public String toString() { if (dataType != null) { builder.append(" ").append(dataType); } + if (collation != null) { + builder.append(" COLLATE ").append(collation); + } if (formatJson) { builder.append(" FORMAT JSON"); if (encoding != null) { diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 1411612de7..d65aa60102 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -13302,6 +13302,8 @@ JsonTableFunction.JsonTableColumnDefinition JsonTableColumnDefinition() : { JsonTableFunction.JsonTableWrapperClause wrapperClause; JsonTableFunction.JsonTableQuotesClause quotesClause; String encoding; + String collation; + Token collationLiteral; } { ( @@ -13341,6 +13343,13 @@ JsonTableFunction.JsonTableColumnDefinition JsonTableColumnDefinition() : { getToken(1).image.equalsIgnoreCase("ALLOW") || getToken(1).image.equalsIgnoreCase("DISALLOW"))) } ) dataType = ColDataType() { valueColumnDefinition.setDataType(dataType); } + [ LOOKAHEAD({ Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) + && getToken(1).kind == K_COLLATE }) + + ( collation = RelObjectName() + | collationLiteral = { collation = collationLiteral.image; } ) + { valueColumnDefinition.setCollation(collation); } + ] ] [ { valueColumnDefinition.setFormatJson(true); } diff --git a/src/test/java/net/sf/jsqlparser/expression/JsonTableCollationTest.java b/src/test/java/net/sf/jsqlparser/expression/JsonTableCollationTest.java new file mode 100644 index 0000000000..84eac19a7e --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/JsonTableCollationTest.java @@ -0,0 +1,86 @@ +/*- + * #%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.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.table.ColDataType; +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 static org.junit.jupiter.api.Assertions.*; + +class JsonTableCollationTest { + @ParameterizedTest + @ValueSource(strings = { + "n VARCHAR(10) COLLATE utf8mb4_bin PATH '$.n'", + "n VARCHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin PATH '$.n'", + "n VARCHAR(10) COLLATE `utf8mb4_bin` PATH '$.n'", + "n VARCHAR(10) COLLATE 'utf8mb4_bin' PATH '$.n'", + "n VARCHAR(10) COLLATE \"utf8mb4_bin\" PATH '$.n'", + "NESTED PATH '$.items[*]' COLUMNS (n VARCHAR(10) COLLATE utf8mb4_bin PATH '$.n')" + }) + void collatedColumnsRoundTrip(String column) throws JSQLParserException { + PlainSelect select = parse(sql(column)); + parse(select.toString()); + } + + @Test + void collationCanBeInspectedEditedRemovedAndConstructed() throws JSQLParserException { + PlainSelect select = parse(sql("n VARCHAR(10) COLLATE utf8mb4_bin PATH '$.n'")); + JsonTableFunction table = + (JsonTableFunction) ((TableFunction) select.getFromItem()).getFunction(); + JsonTableValueColumnDefinition column = + (JsonTableValueColumnDefinition) table.getColumnsClause().getColumnDefinitions() + .get(0); + assertEquals("utf8mb4_bin", column.getCollation()); + assertEquals("VARCHAR", column.getDataType().getBaseTypeName()); + column.setCollation("utf8mb4_general_ci"); + TestUtils.assertStatementCanBeDeparsedAs(select, + sql("n VARCHAR(10) COLLATE utf8mb4_general_ci PATH '$.n'"), true); + parse(select.toString()); + column.setCollation(null); + TestUtils.assertStatementCanBeDeparsedAs(select, sql("n VARCHAR(10) PATH '$.n'"), true); + parse(select.toString()); + JsonTableValueColumnDefinition constructed = new JsonTableValueColumnDefinition() + .setColumnName("v") + .setDataType(new ColDataType("VARCHAR").addArgumentsStringList("10")) + .setCollation("utf8mb4_bin").setPathExpression(new StringValue("$.v")); + table.getColumnsClause().getColumnDefinitions().set(0, constructed); + TestUtils.assertStatementCanBeDeparsedAs(select, + sql("v VARCHAR(10) COLLATE utf8mb4_bin PATH '$.v'"), true); + parse(select.toString()); + } + + @Test + void columnCollationRequiresMySqlDialect() { + String sql = sql("n VARCHAR(10) COLLATE utf8mb4_bin PATH '$.n'"); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.POSTGRESQL))); + } + + private static PlainSelect parse(String sql) throws JSQLParserException { + return (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true, + parser -> parser.withDialect(Dialect.MYSQL)); + } + + private static String sql(String column) { + return "SELECT * FROM JSON_TABLE('[{\"n\":\"ab\"}]', '$[*]' COLUMNS (" + column + + ")) AS jt"; + } +}