From 85edc1abeef2e2178b27d8932e2f34fe52247d95 Mon Sep 17 00:00:00 2001 From: minleejae Date: Fri, 25 Sep 2026 22:45:01 +0900 Subject: [PATCH] feat: support PostgreSQL text search configuration DDL Signed-off-by: minleejae --- .../sf/jsqlparser/parser/feature/Feature.java | 2 + .../statement/StatementVisitor.java | 18 ++ .../alter/AlterTextSearchConfiguration.java | 162 ++++++++++++++++++ .../CreateTextSearchConfiguration.java | 62 +++++++ .../sf/jsqlparser/util/TablesNamesFinder.java | 12 ++ .../util/deparser/StatementDeParser.java | 12 ++ .../validation/feature/FeaturesAllowed.java | 2 + .../validation/feature/PostgresqlVersion.java | 1 + .../validator/StatementValidator.java | 14 ++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 74 ++++++++ ...PostgreSqlTextSearchConfigurationTest.java | 98 +++++++++++ 11 files changed, 457 insertions(+) create mode 100644 src/main/java/net/sf/jsqlparser/statement/alter/AlterTextSearchConfiguration.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/textsearch/CreateTextSearchConfiguration.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTextSearchConfigurationTest.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..668ee42d9 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. */ + createTextSearchConfiguration, alterTextSearchConfiguration, + 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..c07644f36 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.textsearch.CreateTextSearchConfiguration; +import net.sf.jsqlparser.statement.alter.AlterTextSearchConfiguration; 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(CreateTextSearchConfiguration statement, S context) { + return null; + } + + default void visit(CreateTextSearchConfiguration statement) { + visit(statement, null); + } + + default T visit(AlterTextSearchConfiguration statement, S context) { + return null; + } + + default void visit(AlterTextSearchConfiguration statement) { + visit(statement, null); + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterTextSearchConfiguration.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterTextSearchConfiguration.java new file mode 100644 index 000000000..cd7b1bfd4 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterTextSearchConfiguration.java @@ -0,0 +1,162 @@ +/*- + * #%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; +import java.util.ArrayList; +import java.util.List; + +public class AlterTextSearchConfiguration implements Statement { + public enum Action { + ADD_MAPPING, ALTER_MAPPING, REPLACE_MAPPING, DROP_MAPPING, RENAME, OWNER, SET_SCHEMA + } + + private String name; + private Action action; + private List tokenTypes = new ArrayList<>(); + private List dictionaries = new ArrayList<>(); + private String oldDictionary; + private String newDictionary; + private boolean ifExists; + 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 List getTokenTypes() { + return tokenTypes; + } + + public void setTokenTypes(List value) { + tokenTypes = value; + } + + public List getDictionaries() { + return dictionaries; + } + + public void setDictionaries(List value) { + dictionaries = value; + } + + public String getOldDictionary() { + return oldDictionary; + } + + public void setOldDictionary(String value) { + oldDictionary = value; + } + + public String getNewDictionary() { + return newDictionary; + } + + public void setNewDictionary(String value) { + newDictionary = value; + } + + public boolean isIfExists() { + return ifExists; + } + + public void setIfExists(boolean value) { + ifExists = 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 TEXT SEARCH CONFIGURATION ").append(name); + switch (action) { + case ADD_MAPPING: + case ALTER_MAPPING: + sql.append( + action == Action.ADD_MAPPING ? " ADD MAPPING FOR " : " ALTER MAPPING FOR ") + .append(String.join(", ", tokenTypes)).append(" WITH ") + .append(String.join(", ", dictionaries)); + break; + case REPLACE_MAPPING: + sql.append(" ALTER MAPPING"); + if (!tokenTypes.isEmpty()) { + sql.append(" FOR ").append(String.join(", ", tokenTypes)); + } + sql.append(" REPLACE ").append(oldDictionary).append(" WITH ") + .append(newDictionary); + break; + case DROP_MAPPING: + sql.append(" DROP MAPPING"); + if (ifExists) { + sql.append(" IF EXISTS"); + } + sql.append(" FOR ").append(String.join(", ", tokenTypes)); + 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 text-search 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/textsearch/CreateTextSearchConfiguration.java b/src/main/java/net/sf/jsqlparser/statement/create/textsearch/CreateTextSearchConfiguration.java new file mode 100644 index 000000000..dbca5ef66 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/textsearch/CreateTextSearchConfiguration.java @@ -0,0 +1,62 @@ +/*- + * #%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.textsearch; + +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +public class CreateTextSearchConfiguration implements Statement { + public enum SourceKind { + PARSER, COPY + } + + private String name; + private SourceKind sourceKind; + private String sourceName; + + public String getName() { + return name; + } + + public void setName(String value) { + name = value; + } + + public SourceKind getSourceKind() { + return sourceKind; + } + + public void setSourceKind(SourceKind value) { + sourceKind = value; + } + + public String getSourceName() { + return sourceName; + } + + public void setSourceName(String value) { + sourceName = value; + } + + public StringBuilder appendTo(StringBuilder sql) { + return sql.append("CREATE TEXT SEARCH CONFIGURATION ").append(name) + .append(" (").append(sourceKind).append(" = ").append(sourceName).append(')'); + } + + @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/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 9d21d8377..06c78eec0 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.textsearch.CreateTextSearchConfiguration; +import net.sf.jsqlparser.statement.alter.AlterTextSearchConfiguration; 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,14 @@ public Void visit(CreateStatistics statement, S context) { public Void visit(AlterStatistics statement, S context) { return null; } + + @Override + public Void visit(CreateTextSearchConfiguration statement, S context) { + return null; + } + + @Override + public Void visit(AlterTextSearchConfiguration 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..4186bfeaf 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.textsearch.CreateTextSearchConfiguration; +import net.sf.jsqlparser.statement.alter.AlterTextSearchConfiguration; 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,14 @@ public StringBuilder visit(CreateStatistics statement, S context) { public StringBuilder visit(AlterStatistics statement, S context) { return statement.appendTo(builder); } + + @Override + public StringBuilder visit(CreateTextSearchConfiguration statement, S context) { + return statement.appendTo(builder); + } + + @Override + public StringBuilder visit(AlterTextSearchConfiguration 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..e898d3d02 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.createTextSearchConfiguration, 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.alterTextSearchConfiguration, 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..5859c9787 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.createTextSearchConfiguration, Feature.alterTextSearchConfiguration, 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..a9dd55424 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.textsearch.CreateTextSearchConfiguration; +import net.sf.jsqlparser.statement.alter.AlterTextSearchConfiguration; 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,16 @@ public Void visit(OracleNullStatement statement, S context) { return null; } + + @Override + public Void visit(CreateTextSearchConfiguration statement, S context) { + validateFeature(Feature.createTextSearchConfiguration); + return null; + } + + @Override + public Void visit(AlterTextSearchConfiguration statement, S context) { + validateFeature(Feature.alterTextSearchConfiguration); + 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..41028da90 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.textsearch.CreateTextSearchConfiguration; 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,10 @@ Statement Alter(): ( ( + LOOKAHEAD({ getToken(1).kind == K_TEXT_LITERAL && getToken(2).kind == K_SEARCH + && "CONFIGURATION".equalsIgnoreCase(getToken(3).image) }) + statement = AlterTextSearchConfiguration() + | LOOKAHEAD({ isKeywordAhead("STATISTICS") }) statement = AlterStatistics() | LOOKAHEAD() statement = AlterPolicy() @@ -19650,6 +19655,10 @@ Statement Create(): { [ { isUsingOrReplace = true; } ] ( + LOOKAHEAD({ getToken(1).kind == K_TEXT_LITERAL && getToken(2).kind == K_SEARCH + && "CONFIGURATION".equalsIgnoreCase(getToken(3).image) }) + { requireDdlSyntax(!isUsingOrReplace, "CREATE TEXT SEARCH CONFIGURATION does not support OR REPLACE"); } statement = CreateTextSearchConfiguration() + | LOOKAHEAD({ isKeywordAhead("STATISTICS") }) statement = CreateStatistics() | LOOKAHEAD({ isPostgreSqlRoleAhead() }) statement = CreateRole() @@ -20952,3 +20961,68 @@ AlterStatistics AlterStatistics(): statement.setStatistics(target.intValue()); } ) ) ) { return statement; } } + +CreateTextSearchConfiguration CreateTextSearchConfiguration(): +{ CreateTextSearchConfiguration statement = new CreateTextSearchConfiguration(); String name; } +{ + ContextualKeyword("CONFIGURATION") + name=AccessQualifiedName() { statement.setName(name); } + "(" ( { statement.setSourceKind(CreateTextSearchConfiguration.SourceKind.PARSER); } + | { statement.setSourceKind(CreateTextSearchConfiguration.SourceKind.COPY); } ) + "=" name=TextSearchConfigurationSource() ")" { statement.setSourceName(name); return statement; } +} + +AlterTextSearchConfiguration AlterTextSearchConfiguration(): +{ + AlterTextSearchConfiguration statement = new AlterTextSearchConfiguration(); + String name; List names; +} +{ + ContextualKeyword("CONFIGURATION") + name=AccessQualifiedName() { statement.setName(name); } + ( + ContextualKeyword("MAPPING") names=AccessNames() { statement.setTokenTypes(names); } + names=TextSearchDictionaryNames() + { statement.setAction(AlterTextSearchConfiguration.Action.ADD_MAPPING); statement.setDictionaries(names); } + | + ContextualKeyword("MAPPING") + [ names=AccessNames() { statement.setTokenTypes(names); } ] + ( name=AccessQualifiedName() { statement.setOldDictionary(name); } + name=AccessQualifiedName() + { statement.setAction(AlterTextSearchConfiguration.Action.REPLACE_MAPPING); statement.setNewDictionary(name); } + | names=TextSearchDictionaryNames() { + requireDdlSyntax(!statement.getTokenTypes().isEmpty(), "ALTER MAPPING WITH requires token types"); + statement.setAction(AlterTextSearchConfiguration.Action.ALTER_MAPPING); statement.setDictionaries(names); + } ) + | + ContextualKeyword("MAPPING") + [ LOOKAHEAD(2) { statement.setIfExists(true); } ] + names=AccessNames() + { statement.setAction(AlterTextSearchConfiguration.Action.DROP_MAPPING); statement.setTokenTypes(names); } + | + name=RelObjectName() + { statement.setAction(AlterTextSearchConfiguration.Action.RENAME); statement.setNewName(name); } + | + name=RelObjectName() + { statement.setAction(AlterTextSearchConfiguration.Action.SET_SCHEMA); statement.setSchemaName(name); } + | + ContextualKeyword("OWNER") name=RelObjectName() + { statement.setAction(AlterTextSearchConfiguration.Action.OWNER); statement.setOwner(name); } + ) + { return statement; } +} + +List TextSearchDictionaryNames(): +{ List names = new ArrayList(); String name; } +{ + name=AccessQualifiedName() { names.add(name); } + ( "," name=AccessQualifiedName() { names.add(name); } )* + { return names; } +} + +String TextSearchConfigurationSource(): +{ String name; Token value; } +{ + ( value= { name = value.image; } | name=AccessQualifiedName() ) + { return name; } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTextSearchConfigurationTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTextSearchConfigurationTest.java new file mode 100644 index 000000000..2a2f358cf --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTextSearchConfigurationTest.java @@ -0,0 +1,98 @@ +/*- + * #%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.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.AlterTextSearchConfiguration; +import net.sf.jsqlparser.statement.create.textsearch.CreateTextSearchConfiguration; +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 PostgreSqlTextSearchConfigurationTest { + @ParameterizedTest + @ValueSource(strings = {"CREATE TEXT SEARCH CONFIGURATION cfg (COPY=pg_catalog.english)", + "CREATE TEXT SEARCH CONFIGURATION cfg (COPY='english')", + "CREATE TEXT SEARCH CONFIGURATION app.cfg (PARSER=pg_catalog.default)", + "ALTER TEXT SEARCH CONFIGURATION cfg ADD MAPPING FOR word WITH simple", + "ALTER TEXT SEARCH CONFIGURATION cfg ADD MAPPING FOR word,asciiword WITH pg_catalog.simple,english_stem", + "ALTER TEXT SEARCH CONFIGURATION cfg ALTER MAPPING FOR word,asciiword WITH simple", + "ALTER TEXT SEARCH CONFIGURATION cfg ALTER MAPPING REPLACE english_stem WITH simple", + "ALTER TEXT SEARCH CONFIGURATION cfg ALTER MAPPING FOR word REPLACE english_stem WITH pg_catalog.simple", + "ALTER TEXT SEARCH CONFIGURATION cfg DROP MAPPING FOR word,asciiword", + "ALTER TEXT SEARCH CONFIGURATION cfg DROP MAPPING IF EXISTS FOR word", + "ALTER TEXT SEARCH CONFIGURATION app.cfg RENAME TO new_cfg", + "ALTER TEXT SEARCH CONFIGURATION cfg OWNER TO CURRENT_USER", + "ALTER TEXT SEARCH CONFIGURATION cfg SET SCHEMA app"}) + void configurationAndMappingActionsRoundTrip(String sql) throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql); + assertTrue(statement instanceof CreateTextSearchConfiguration + || statement instanceof AlterTextSearchConfiguration); + 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 sourceAndMappingsCanBeChangedWithoutRawTokens() throws JSQLParserException { + CreateTextSearchConfiguration create = + (CreateTextSearchConfiguration) CCJSqlParserUtil.parse( + "CREATE TEXT SEARCH CONFIGURATION cfg(COPY=pg_catalog.english)"); + assertEquals(CreateTextSearchConfiguration.SourceKind.COPY, create.getSourceKind()); + create.setSourceKind(CreateTextSearchConfiguration.SourceKind.PARSER); + create.setSourceName("pg_catalog.default"); + assertEquals("CREATE TEXT SEARCH CONFIGURATION cfg (PARSER = pg_catalog.default)", + create.toString()); + roundTrip(create); + AlterTextSearchConfiguration alter = (AlterTextSearchConfiguration) CCJSqlParserUtil.parse( + "ALTER TEXT SEARCH CONFIGURATION cfg ALTER MAPPING FOR word REPLACE english_stem WITH simple"); + assertEquals(AlterTextSearchConfiguration.Action.REPLACE_MAPPING, alter.getAction()); + assertEquals(List.of("word"), alter.getTokenTypes()); + assertEquals("english_stem", alter.getOldDictionary()); + alter.setNewDictionary("pg_catalog.simple"); + alter.getTokenTypes().add("asciiword"); + roundTrip(alter); + alter.getTokenTypes().clear(); + assertEquals( + "ALTER TEXT SEARCH CONFIGURATION cfg ALTER MAPPING REPLACE english_stem WITH pg_catalog.simple", + alter.toString()); + roundTrip(alter); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE TEXT SEARCH CONFIGURATION cfg()", + "CREATE TEXT SEARCH CONFIGURATION cfg(COPY=)", + "CREATE TEXT SEARCH CONFIGURATION cfg(COPY=english,PARSER=default)", + "CREATE OR REPLACE TEXT SEARCH CONFIGURATION cfg(COPY=english)", + "ALTER TEXT SEARCH CONFIGURATION cfg ADD MAPPING WITH simple", + "ALTER TEXT SEARCH CONFIGURATION cfg ALTER MAPPING WITH simple", + "ALTER TEXT SEARCH CONFIGURATION cfg DROP MAPPING FOR", + "ALTER TEXT SEARCH CONFIGURATION cfg ALTER MAPPING REPLACE simple WITH", + "ALTER TEXT SEARCH CONFIGURATION cfg ADD MAPPING FOR word WITH simple,"}) + void rejectsIncompleteOrIncompatibleForms(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()); + } +}