From 1890ce300ec9e68ad38f0b01d163f4907c3c4b4d Mon Sep 17 00:00:00 2001 From: minleejae Date: Fri, 25 Sep 2026 22:45:01 +0900 Subject: [PATCH 1/2] feat: support PostgreSQL CREATE and ALTER COLLATION Signed-off-by: minleejae --- .../sf/jsqlparser/parser/feature/Feature.java | 2 + .../statement/StatementVisitor.java | 18 ++++ .../statement/alter/AlterCollation.java | 96 ++++++++++++++++++ .../create/collation/CreateCollation.java | 97 +++++++++++++++++++ .../sf/jsqlparser/util/TablesNamesFinder.java | 13 +++ .../util/deparser/StatementDeParser.java | 13 +++ .../validation/feature/FeaturesAllowed.java | 2 + .../validation/feature/PostgresqlVersion.java | 1 + .../validator/StatementValidator.java | 15 +++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 38 ++++++++ .../create/PostgreSqlCollationTest.java | 87 +++++++++++++++++ 11 files changed, 382 insertions(+) create mode 100644 src/main/java/net/sf/jsqlparser/statement/alter/AlterCollation.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/collation/CreateCollation.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlCollationTest.java 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 e09ae8553..630d54d3d 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -580,6 +580,8 @@ public enum Feature { */ createSequence, /** Publication and subscription definitions. */ + createCollation, alterCollation, + createPublication, alterPublication, createSubscription, alterSubscription, /** * Structured type, domain and extension statements. diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java index 03755d652..4b52e26a4 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -9,6 +9,8 @@ */ package net.sf.jsqlparser.statement; +import net.sf.jsqlparser.statement.create.collation.CreateCollation; +import net.sf.jsqlparser.statement.alter.AlterCollation; import net.sf.jsqlparser.statement.alter.AlterPolicy; import net.sf.jsqlparser.statement.drop.DropPolicy; import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; @@ -733,4 +735,20 @@ default T visit(AlterStatistics statement, S context) { default void visit(AlterStatistics statement) { visit(statement, null); } + + default T visit(CreateCollation statement, S context) { + return null; + } + + default void visit(CreateCollation statement) { + visit(statement, null); + } + + default T visit(AlterCollation statement, S context) { + return null; + } + + default void visit(AlterCollation statement) { + visit(statement, null); + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterCollation.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterCollation.java new file mode 100644 index 000000000..fd4bf30fe --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterCollation.java @@ -0,0 +1,96 @@ +/*- + * #%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.statement.alter; + +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +public class AlterCollation implements Statement { + public enum Action { + REFRESH_VERSION, RENAME, OWNER, SET_SCHEMA + } + + private String name; + private Action action; + private String newName; + private String owner; + private String schemaName; + + public String getName() { + return name; + } + + public void setName(String value) { + name = value; + } + + public Action getAction() { + return action; + } + + public void setAction(Action value) { + action = value; + } + + public String getNewName() { + return newName; + } + + public void setNewName(String value) { + newName = value; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String value) { + owner = value; + } + + public String getSchemaName() { + return schemaName; + } + + public void setSchemaName(String value) { + schemaName = value; + } + + public StringBuilder appendTo(StringBuilder sql) { + sql.append("ALTER COLLATION ").append(name); + switch (action) { + case REFRESH_VERSION: + sql.append(" REFRESH VERSION"); + break; + case RENAME: + sql.append(" RENAME TO ").append(newName); + break; + case OWNER: + sql.append(" OWNER TO ").append(owner); + break; + case SET_SCHEMA: + sql.append(" SET SCHEMA ").append(schemaName); + break; + default: + throw new IllegalStateException("Unknown collation action: " + action); + } + return sql; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/collation/CreateCollation.java b/src/main/java/net/sf/jsqlparser/statement/create/collation/CreateCollation.java new file mode 100644 index 000000000..d2d485304 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/collation/CreateCollation.java @@ -0,0 +1,97 @@ +/*- + * #%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.statement.create.collation; + +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.create.table.Index; + +/** PostgreSQL collation copy or parameter definition, with mutually exclusive forms. */ +public class CreateCollation implements Statement { + private String name; + private boolean ifNotExists; + + public String getName() { + return name; + } + + public void setName(String value) { + name = value; + } + + public boolean isIfNotExists() { + return ifNotExists; + } + + public void setIfNotExists(boolean value) { + ifNotExists = value; + } + + private String sourceCollation; + private List options; + + public String getSourceCollation() { + return sourceCollation; + } + + public void setSourceCollation(String source) { + sourceCollation = source; + if (source != null) { + options = null; + } + } + + public List getOptions() { + return options; + } + + public void setOptions(List options) { + this.options = options; + if (options != null) { + sourceCollation = null; + } + } + + public StringBuilder appendTo(StringBuilder sql, Consumer printer) { + sql.append("CREATE COLLATION "); + if (isIfNotExists()) { + sql.append("IF NOT EXISTS "); + } + sql.append(getName()); + if (sourceCollation != null) { + sql.append(" FROM ").append(sourceCollation); + } else { + sql.append(' '); + Index.Option.appendListTo(sql, options, printer); + } + return sql; + } + + public void visitExpressions(Consumer visitor) { + if (sourceCollation == null && options != null) { + options.stream().map(Index.Option::getValue).filter(java.util.Objects::nonNull) + .forEach(visitor); + } + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + return appendTo(sql, sql::append).toString(); + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 9d21d8377..cedf00fb4 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -9,6 +9,8 @@ */ package net.sf.jsqlparser.util; +import net.sf.jsqlparser.statement.create.collation.CreateCollation; +import net.sf.jsqlparser.statement.alter.AlterCollation; import net.sf.jsqlparser.statement.alter.AlterPolicy; import net.sf.jsqlparser.statement.drop.DropPolicy; import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; @@ -2968,4 +2970,15 @@ public Void visit(CreateStatistics statement, S context) { public Void visit(AlterStatistics statement, S context) { return null; } + + @Override + public Void visit(CreateCollation statement, S context) { + statement.visitExpressions(expression -> expression.accept(this, context)); + return null; + } + + @Override + public Void visit(AlterCollation statement, S context) { + return null; + } } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java index d949c3e62..109da91c0 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -9,6 +9,8 @@ */ package net.sf.jsqlparser.util.deparser; +import net.sf.jsqlparser.statement.create.collation.CreateCollation; +import net.sf.jsqlparser.statement.alter.AlterCollation; import net.sf.jsqlparser.statement.alter.AlterPolicy; import net.sf.jsqlparser.statement.drop.DropPolicy; import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; @@ -853,4 +855,15 @@ public StringBuilder visit(CreateStatistics statement, S context) { public StringBuilder visit(AlterStatistics statement, S context) { return statement.appendTo(builder); } + + @Override + public StringBuilder visit(CreateCollation statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(AlterCollation statement, S context) { + return statement.appendTo(builder); + } } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java index 08e137e54..ece1dde69 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java @@ -101,6 +101,7 @@ public class FeaturesAllowed implements FeatureSetValidation, ModifyableFeatureS * all "CREATE" {@link Feature}'s */ public static final FeaturesAllowed CREATE = new FeaturesAllowed("CREATE", Feature.createIndex, + Feature.createCollation, Feature.createSchema, Feature.createSequence, Feature.createTable, Feature.createTableUnlogged, Feature.createTableCreateOptionStrings, Feature.createTableTableOptionStrings, @@ -113,6 +114,7 @@ public class FeaturesAllowed implements FeatureSetValidation, ModifyableFeatureS */ public static final FeaturesAllowed ALTER = new FeaturesAllowed("ALTER", Feature.alterTable, Feature.alterSequence, + Feature.alterCollation, Feature.alterView, Feature.alterIndex, Feature.alterSchema) .unmodifyable(); /** diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java index e5a9b43f1..8d2eb1b9a 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java @@ -97,6 +97,7 @@ public enum PostgresqlVersion implements Version { // https://www.postgresql.org/docs/current/sql-createsequence.html Feature.createSequence, + Feature.createCollation, Feature.alterCollation, Feature.createPublication, Feature.alterPublication, Feature.createSubscription, Feature.alterSubscription, Feature.createType, Feature.alterType, diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java index 0b8d2765d..b9e9879d0 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java @@ -9,6 +9,8 @@ */ package net.sf.jsqlparser.util.validation.validator; +import net.sf.jsqlparser.statement.create.collation.CreateCollation; +import net.sf.jsqlparser.statement.alter.AlterCollation; import net.sf.jsqlparser.statement.alter.schema.AlterSchema; import net.sf.jsqlparser.statement.oracle.OracleBlock; import net.sf.jsqlparser.statement.oracle.OracleAssignment; @@ -1024,4 +1026,17 @@ public Void visit(OracleNullStatement statement, S context) { return null; } + + @Override + public Void visit(CreateCollation statement, S context) { + validateFeature(Feature.createCollation); + statement.visitExpressions(this::validateOptionalExpression); + return null; + } + + @Override + public Void visit(AlterCollation statement, S context) { + validateFeature(Feature.alterCollation); + return null; + } } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 4b505dab0..714a34bba 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -49,6 +49,7 @@ import net.sf.jsqlparser.schema.*; import net.sf.jsqlparser.statement.*; import net.sf.jsqlparser.statement.analyze.*; import net.sf.jsqlparser.statement.alter.*; +import net.sf.jsqlparser.statement.create.collation.CreateCollation; import net.sf.jsqlparser.statement.alter.schema.AlterSchema; import net.sf.jsqlparser.statement.alter.database.AlterDatabase; import net.sf.jsqlparser.statement.alter.sequence.*; @@ -18441,6 +18442,8 @@ Statement Alter(): ( ( + LOOKAHEAD({ isKeywordAhead("COLLATION") }) statement = AlterCollation() + | LOOKAHEAD({ isKeywordAhead("STATISTICS") }) statement = AlterStatistics() | LOOKAHEAD() statement = AlterPolicy() @@ -19650,6 +19653,8 @@ Statement Create(): { [ { isUsingOrReplace = true; } ] ( + LOOKAHEAD({ isKeywordAhead("COLLATION") }) { requireDdlSyntax(!isUsingOrReplace, "CREATE COLLATION does not support OR REPLACE"); } statement = CreateCollation() + | LOOKAHEAD({ isKeywordAhead("STATISTICS") }) statement = CreateStatistics() | LOOKAHEAD({ isPostgreSqlRoleAhead() }) statement = CreateRole() @@ -20952,3 +20957,36 @@ AlterStatistics AlterStatistics(): statement.setStatistics(target.intValue()); } ) ) ) { return statement; } } + +CreateCollation CreateCollation(): +{ + CreateCollation statement = new CreateCollation(); String name; List options; +} +{ + ContextualKeyword("COLLATION") + [ LOOKAHEAD(2) { statement.setIfNotExists(true); } ] + name=AccessQualifiedName() { statement.setName(name); } + ( name=AccessQualifiedName() { statement.setSourceCollation(name); } + | options=PostgreSqlOptions(true) { + for (Index.Option option : options) { + requireDdlSyntax(option.getValue() == null || option.isUseEquals(), + "Collation parameter values require an equals sign"); + } + statement.setOptions(options); + } ) + { return statement; } +} + +AlterCollation AlterCollation(): +{ AlterCollation statement = new AlterCollation(); String name; } +{ + ContextualKeyword("COLLATION") name=AccessQualifiedName() { statement.setName(name); } + ( { statement.setAction(AlterCollation.Action.REFRESH_VERSION); } + | name=RelObjectName() + { statement.setAction(AlterCollation.Action.RENAME); statement.setNewName(name); } + | name=RelObjectName() + { statement.setAction(AlterCollation.Action.SET_SCHEMA); statement.setSchemaName(name); } + | ContextualKeyword("OWNER") name=RelObjectName() + { statement.setAction(AlterCollation.Action.OWNER); statement.setOwner(name); } ) + { return statement; } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlCollationTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlCollationTest.java new file mode 100644 index 000000000..b96c1608f --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlCollationTest.java @@ -0,0 +1,87 @@ +/*- + * #%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.statement.create; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.AlterCollation; +import net.sf.jsqlparser.statement.create.collation.CreateCollation; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlCollationTest { + @ParameterizedTest + @ValueSource(strings = {"CREATE COLLATION c FROM \"C\"", + "CREATE COLLATION IF NOT EXISTS app.c FROM pg_catalog.\"C\"", + "CREATE COLLATION c (locale='C')", + "CREATE COLLATION c (locale='C',deterministic)", + "CREATE COLLATION c (provider=builtin,locale='C.UTF-8')", + "CREATE COLLATION c (lc_collate='C',lc_ctype='C',provider=libc)", + "CREATE COLLATION c (provider=icu,locale='und',deterministic=false,rules='&V << w',version='1')", + "ALTER COLLATION app.c REFRESH VERSION", "ALTER COLLATION app.c RENAME TO c2", + "ALTER COLLATION c OWNER TO CURRENT_USER", "ALTER COLLATION c SET SCHEMA app"}) + void structuredDefinitionsAndChangesRoundTrip(String sql) throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql); + assertTrue(statement instanceof CreateCollation || statement instanceof AlterCollation); + assertTrue(new TablesNamesFinder().getTables(statement).isEmpty()); + roundTrip(statement); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size()); + assertTrue(new Validation(List.of(FeaturesAllowed.DDL), sql).validate().isEmpty()); + } + + @Test + void switchingBetweenCopyAndOptionsClearsInactiveState() throws JSQLParserException { + CreateCollation statement = + (CreateCollation) CCJSqlParserUtil.parse("CREATE COLLATION c FROM \"C\""); + statement.setOptions( + new ArrayList<>(List.of(new Index.Option("locale", new StringValue("C"), true)))); + assertNull(statement.getSourceCollation()); + statement.getOptions().get(0).setValue(new StringValue("C.UTF-8")); + List expressions = new ArrayList<>(); + statement.visitExpressions(expressions::add); + assertEquals(1, expressions.size()); + roundTrip(statement); + statement.setSourceCollation("pg_catalog.\"C\""); + assertNull(statement.getOptions()); + assertEquals("CREATE COLLATION c FROM pg_catalog.\"C\"", statement.toString()); + expressions.clear(); + statement.visitExpressions(expressions::add); + assertTrue(expressions.isEmpty()); + roundTrip(statement); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE COLLATION c", "CREATE COLLATION c ()", + "CREATE COLLATION c (locale=)", "CREATE COLLATION c (locale 'C')", + "CREATE OR REPLACE COLLATION c FROM \"C\"", "ALTER COLLATION c REFRESH", + "ALTER COLLATION c RENAME TO c2, SET SCHEMA app"}) + void rejectsIncompleteDefinitions(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } + + private static void roundTrip(Statement statement) throws JSQLParserException { + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output)); + assertEquals(statement.toString(), output.toString()); + assertEquals(output.toString(), CCJSqlParserUtil.parse(output.toString()).toString()); + } +} From f4c98ce05ba8252ca3722048b65314382c8044cb Mon Sep 17 00:00:00 2001 From: minleejae Date: Sat, 26 Sep 2026 09:15:51 +0900 Subject: [PATCH 2/2] fix: declare collation fields before accessors Signed-off-by: minleejae --- .../statement/create/collation/CreateCollation.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/statement/create/collation/CreateCollation.java b/src/main/java/net/sf/jsqlparser/statement/create/collation/CreateCollation.java index d2d485304..a93d93dfe 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/collation/CreateCollation.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/collation/CreateCollation.java @@ -20,6 +20,8 @@ public class CreateCollation implements Statement { private String name; private boolean ifNotExists; + private String sourceCollation; + private List options; public String getName() { return name; @@ -37,9 +39,6 @@ public void setIfNotExists(boolean value) { ifNotExists = value; } - private String sourceCollation; - private List options; - public String getSourceCollation() { return sourceCollation; }