From ea2d4fcb8cdd8936b7a3b2b0dd2468692a82501c Mon Sep 17 00:00:00 2001 From: minleejae Date: Sat, 31 Oct 2026 20:55:50 +0900 Subject: [PATCH] fix: parse PostgreSQL DROP for object types of several words DROP ACCESS METHOD and DROP PROCEDURAL LANGUAGE parsed with ACCESS or PROCEDURAL as the object type and METHOD or LANGUAGE as the name, and failed with IF EXISTS. DROP OPERATOR CLASS and FAMILY ... USING and the four DROP TEXT SEARCH statements failed. pg_dump --clean writes all of them, so such a dump could not be parsed as a script. Drop.ObjectType gains ACCESS_METHOD, LANGUAGE, OPERATOR_CLASS, OPERATOR_FAMILY and TEXT_SEARCH_CONFIGURATION, _DICTIONARY, _PARSER and _TEMPLATE, read with the words COMMENT ON uses for them. The index access method of an operator class or family is kept in getAccessMethod(), and isProcedural() keeps the PROCEDURAL spelling. Signed-off-by: minleejae --- .../sf/jsqlparser/statement/drop/Drop.java | 46 +++++++++- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 43 ++++++++- src/site/sphinx/usage.rst | 7 +- .../statement/drop/DropObjectTypeTest.java | 92 +++++++++++++++++++ 4 files changed, 182 insertions(+), 6 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java b/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java index d0e751a587..2c29b8dc8a 100644 --- a/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java +++ b/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java @@ -32,7 +32,7 @@ public class Drop extends ASTNodeAccessImpl implements Statement { public enum ObjectType { - DATABASE, EVENT, FUNCTION, INDEX, PROCEDURE, SCHEMA, SEQUENCE, SERVER, TABLE, TABLESPACE, TRIGGER, VIEW, FOREIGN_TABLE, FOREIGN_DATA_WRAPPER, AGGREGATE, DOMAIN, EVENT_TRIGGER, GROUP, OWNED, ROLE, ROUTINE, RULE, TYPE, USER, SYNONYM, OTHER; + DATABASE, EVENT, FUNCTION, INDEX, PROCEDURE, SCHEMA, SEQUENCE, SERVER, TABLE, TABLESPACE, TRIGGER, VIEW, FOREIGN_TABLE, FOREIGN_DATA_WRAPPER, AGGREGATE, DOMAIN, EVENT_TRIGGER, GROUP, OWNED, ROLE, ROUTINE, RULE, TYPE, USER, SYNONYM, ACCESS_METHOD, LANGUAGE, OPERATOR_CLASS, OPERATOR_FAMILY, TEXT_SEARCH_CONFIGURATION, TEXT_SEARCH_DICTIONARY, TEXT_SEARCH_PARSER, TEXT_SEARCH_TEMPLATE, OTHER; /** Targets are {@link RoutineReference}s, each with an optional argument signature. */ public boolean hasRoutineTargets() { @@ -48,6 +48,11 @@ public boolean hasAccountTargets() { public boolean hasOwnerTable() { return this == INDEX || this == TRIGGER || this == RULE; } + + /** The target is named within the index access method given with USING. */ + public boolean hasAccessMethod() { + return this == OPERATOR_CLASS || this == OPERATOR_FAMILY; + } } private String type; @@ -68,6 +73,8 @@ public boolean hasOwnerTable() { private boolean undo; private boolean force; private boolean publicSynonym; + private boolean procedural; + private String accessMethod; private String engine; private boolean engineUseEquals; @@ -369,6 +376,37 @@ public Drop withPublicSynonym(boolean publicSynonym) { return this; } + /** Whether a {@link ObjectType#LANGUAGE} is written as PostgreSQL PROCEDURAL LANGUAGE. */ + public boolean isProcedural() { + return procedural; + } + + public void setProcedural(boolean procedural) { + this.procedural = procedural; + } + + public Drop withProcedural(boolean procedural) { + setProcedural(procedural); + return this; + } + + /** + * The index access method of an {@link ObjectType#OPERATOR_CLASS} or + * {@link ObjectType#OPERATOR_FAMILY}, written with USING. + */ + public String getAccessMethod() { + return accessMethod; + } + + public void setAccessMethod(String accessMethod) { + this.accessMethod = accessMethod; + } + + public Drop withAccessMethod(String accessMethod) { + setAccessMethod(accessMethod); + return this; + } + /** The storage engine of MySQL DROP TABLESPACE ... ENGINE, or null when it is omitted. */ public String getEngine() { return engine; @@ -496,6 +534,9 @@ public StringBuilder appendTo(StringBuilder builder, Consumer tablePrinte if (publicSynonym) { builder.append("PUBLIC "); } + if (procedural) { + builder.append("PROCEDURAL "); + } builder.append(type); if (concurrently) { builder.append(" CONCURRENTLY"); @@ -505,6 +546,9 @@ public StringBuilder appendTo(StringBuilder builder, Consumer
tablePrinte } builder.append(ifExists ? " IF EXISTS " : " "); appendTargets(builder, tablePrinter); + if (accessMethod != null) { + builder.append(" USING ").append(accessMethod); + } if (force) { builder.append(objectType == ObjectType.SYNONYM ? " FORCE" : " WITH (FORCE)"); } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index a361fa20f4..8b2096ba5f 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -20367,19 +20367,45 @@ Drop Drop(): List indexOptions = new ArrayList(); int indexOptionPosition = 0; boolean useTemporary = false; - String foreignType = null; + String multiWordType = null; boolean eventTrigger = false; CascadeBehavior behavior = null; String engine; + String accessMethod; } { [ { drop.setMaterialized(true);} ] ( ( - { foreignType = "FOREIGN TABLE"; } + { multiWordType = "FOREIGN TABLE"; } | - ContextualKeyword("WRAPPER") { foreignType = "FOREIGN DATA WRAPPER"; } + ContextualKeyword("WRAPPER") { multiWordType = "FOREIGN DATA WRAPPER"; } + ) + | + // PostgreSQL object types of several words, written as COMMENT ON names them + LOOKAHEAD({ isKeywordAhead("ACCESS") && "METHOD".equalsIgnoreCase(getToken(2).image) }) + ContextualKeyword("ACCESS") ContextualKeyword("METHOD") { multiWordType = "ACCESS METHOD"; } + | + LOOKAHEAD({ isKeywordAhead("PROCEDURAL") && "LANGUAGE".equalsIgnoreCase(getToken(2).image) }) + ContextualKeyword("PROCEDURAL") ContextualKeyword("LANGUAGE") + { drop.setProcedural(true); multiWordType = "LANGUAGE"; } + | + // a schema named CLASS or FAMILY may qualify an operator, as in class.+ + LOOKAHEAD({ isKeywordAhead("OPERATOR") && !".".equals(getToken(3).image) + && ("CLASS".equalsIgnoreCase(getToken(2).image) || "FAMILY".equalsIgnoreCase(getToken(2).image)) }) + ContextualKeyword("OPERATOR") + ( LOOKAHEAD({ isKeywordAhead("CLASS") }) ContextualKeyword("CLASS") { multiWordType = "OPERATOR CLASS"; } + | ContextualKeyword("FAMILY") { multiWordType = "OPERATOR FAMILY"; } ) + | + LOOKAHEAD({ isKeywordAhead("TEXT") && getToken(2).kind == K_SEARCH }) + ( + { multiWordType = "TEXT SEARCH PARSER"; } + | LOOKAHEAD({ isKeywordAhead("CONFIGURATION") }) ContextualKeyword("CONFIGURATION") + { multiWordType = "TEXT SEARCH CONFIGURATION"; } + | LOOKAHEAD({ isKeywordAhead("DICTIONARY") }) ContextualKeyword("DICTIONARY") + { multiWordType = "TEXT SEARCH DICTIONARY"; } + | ContextualKeyword("TEMPLATE") { multiWordType = "TEXT SEARCH TEMPLATE"; } ) | // PostgreSQL DROP OWNED BY role [, ...] @@ -20428,7 +20454,7 @@ Drop Drop(): | tk= ) - { drop.setType(foreignType != null ? foreignType : eventTrigger ? "EVENT TRIGGER" : tk.image); } + { drop.setType(multiWordType != null ? multiWordType : eventTrigger ? "EVENT TRIGGER" : tk.image); } [ LOOKAHEAD({ drop.getObjectType() == Drop.ObjectType.INDEX && getToken(1).kind == K_CONCURRENTLY }) { drop.setConcurrently(true); } ] @@ -20437,6 +20463,15 @@ Drop Drop(): {drop.setIfExists(true);} ] DropTargets(drop) + [ + // PostgreSQL DROP OPERATOR CLASS and FAMILY require USING: they name one object of an + // index access method. The semantic lookahead alone selects this clause. + LOOKAHEAD({ drop.getObjectType().hasAccessMethod() }) + accessMethod=RelObjectName() { + requireDdlSyntax(drop.getNames().size() == 1, "DROP " + drop.getType() + " takes one name"); + drop.setAccessMethod(accessMethod); + } + ] [ LOOKAHEAD({ DropLookahead.isForceAhead(drop, getToken(1), getToken(2), getToken(3)) }) ( | [ ] "(" ")" ) { drop.setForce(true); } diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index c784f81d3a..abccd18014 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -271,9 +271,14 @@ typed properties: ``DROP [UNDO] TABLESPACE name [ENGINE [=] engine]``. ``isForce()`` covers PostgreSQL ``DROP DATABASE name [WITH] (FORCE)``; the optional WITH is always rendered. +* ``getAccessMethod()`` holds the index access method of PostgreSQL + ``DROP OPERATOR CLASS|FAMILY name USING method``, which takes one name. + ``isProcedural()`` records the ``PROCEDURAL LANGUAGE`` spelling. TYPE, DOMAIN, EVENT, EVENT TRIGGER, TABLESPACE, USER and GROUP are recognized -object types. PostgreSQL ``REASSIGN OWNED BY old_role [, ...] TO new_role`` is +object types, and so are PostgreSQL's ACCESS METHOD, LANGUAGE, OPERATOR CLASS, +OPERATOR FAMILY and TEXT SEARCH CONFIGURATION, DICTIONARY, PARSER and TEMPLATE. +``getType()`` returns their words separated by spaces, as in ``ACCESS METHOD``. PostgreSQL ``REASSIGN OWNED BY old_role [, ...] TO new_role`` is parsed into ``ReassignOwned``, whose roles use the same ``UserAccount`` model. .. code-block:: java diff --git a/src/test/java/net/sf/jsqlparser/statement/drop/DropObjectTypeTest.java b/src/test/java/net/sf/jsqlparser/statement/drop/DropObjectTypeTest.java index 5906128b25..aeff84ef4a 100644 --- a/src/test/java/net/sf/jsqlparser/statement/drop/DropObjectTypeTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/drop/DropObjectTypeTest.java @@ -14,6 +14,7 @@ import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; import net.sf.jsqlparser.parser.CCJSqlParserUtil; @@ -122,6 +123,92 @@ void postgreSqlObjectTypes(String sql) throws JSQLParserException { } } + @ParameterizedTest + @ValueSource(strings = {"DROP ACCESS METHOD heptree", + "DROP ACCESS METHOD IF EXISTS heptree CASCADE", "DROP PROCEDURAL LANGUAGE plsample", + "DROP PROCEDURAL LANGUAGE IF EXISTS plsample RESTRICT", + "DROP LANGUAGE IF EXISTS plsample CASCADE", + "DROP OPERATOR CLASS IF EXISTS public.myops USING btree CASCADE", + "DROP OPERATOR FAMILY myfam USING hash", + "DROP TEXT SEARCH CONFIGURATION IF EXISTS my_cfg CASCADE", + "DROP TEXT SEARCH DICTIONARY english_stem, public.french_stem", + "DROP TEXT SEARCH PARSER my_parser", + "DROP TEXT SEARCH TEMPLATE IF EXISTS my_tmpl RESTRICT"}) + void postgreSqlMultiWordObjectTypes(String sql) throws JSQLParserException { + for (Drop drop : roundTrip(sql, Dialect.POSTGRESQL)) { + assertEquals(sql, drop.toString()); + assertNotEquals(Drop.ObjectType.OTHER, drop.getObjectType()); + assertTrue(new TablesNamesFinder().getTables(drop).isEmpty()); + } + assertEquals(sql, CCJSqlParserUtil.parse(sql).toString()); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1", + parser -> parser.withDialect(Dialect.POSTGRESQL)).size()); + } + + @Test + void multiWordObjectTypeFields() throws JSQLParserException { + Drop accessMethod = + parse("DROP ACCESS METHOD IF EXISTS heptree CASCADE", Dialect.POSTGRESQL); + assertEquals(Drop.ObjectType.ACCESS_METHOD, accessMethod.getObjectType()); + assertEquals("ACCESS METHOD", accessMethod.getType()); + assertEquals(List.of("heptree"), names(accessMethod)); + assertTrue(accessMethod.isIfExists()); + assertEquals(CascadeBehavior.CASCADE, accessMethod.getBehavior()); + + Drop language = parse("DROP PROCEDURAL LANGUAGE plsample", Dialect.POSTGRESQL); + assertEquals(Drop.ObjectType.LANGUAGE, language.getObjectType()); + assertTrue(language.isProcedural()); + assertEquals(List.of("plsample"), names(language)); + language.setProcedural(false); + assertEquals("DROP LANGUAGE plsample", language.toString()); + assertFalse(parse("DROP LANGUAGE plsample", Dialect.POSTGRESQL).isProcedural()); + + Drop operatorClass = parse("DROP OPERATOR CLASS IF EXISTS public.myops USING btree", + Dialect.POSTGRESQL); + assertEquals(Drop.ObjectType.OPERATOR_CLASS, operatorClass.getObjectType()); + assertEquals(List.of("public.myops"), names(operatorClass)); + assertEquals("btree", operatorClass.getAccessMethod()); + assertNull(operatorClass.getParameters()); + operatorClass.setAccessMethod("hash"); + assertEquals("DROP OPERATOR CLASS IF EXISTS public.myops USING hash", + operatorClass.toString()); + assertEquals(Drop.ObjectType.OPERATOR_FAMILY, + parse("DROP OPERATOR FAMILY myfam USING gist", Dialect.POSTGRESQL).getObjectType()); + + Drop dictionary = parse("DROP TEXT SEARCH DICTIONARY english_stem, public.french_stem", + Dialect.POSTGRESQL); + assertEquals(Drop.ObjectType.TEXT_SEARCH_DICTIONARY, dictionary.getObjectType()); + assertEquals(List.of("english_stem", "public.french_stem"), names(dictionary)); + + assertEquals("DROP OPERATOR FAMILY f USING btree CASCADE", + new Drop().withObjectType(Drop.ObjectType.OPERATOR_FAMILY) + .withName(new Table("f")).withAccessMethod("btree") + .withBehavior(CascadeBehavior.CASCADE).toString()); + assertEquals("DROP PROCEDURAL LANGUAGE l", new Drop() + .withObjectType(Drop.ObjectType.LANGUAGE).withProcedural(true) + .withName(new Table("l")).toString()); + } + + @Test + void multiWordTypeWordsRemainNames() throws JSQLParserException { + assertEquals(List.of("procedural"), + names(parse("DROP LANGUAGE procedural", Dialect.POSTGRESQL))); + assertEquals(List.of("method"), + names(parse("DROP ACCESS METHOD method", Dialect.POSTGRESQL))); + assertEquals(List.of("access"), names(parse("DROP TABLE access", Dialect.POSTGRESQL))); + assertEquals(List.of("family"), + names(parse("DROP OPERATOR CLASS family USING btree", Dialect.POSTGRESQL))); + } + + @ParameterizedTest + @ValueSource(strings = {"DROP OPERATOR CLASS myops", "DROP OPERATOR FAMILY f1, f2 USING btree", + "DROP OPERATOR CLASS myops USING", "DROP ACCESS METHOD", + "DROP ACCESS METHOD IF EXISTS", "DROP TEXT SEARCH INDEX x", "DROP TEXT SEARCH", + "DROP PROCEDURAL LANGUAGE", "DROP ACCESS METHOD heptree USING btree"}) + void rejectsIncompleteMultiWordTypes(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql, Dialect.POSTGRESQL), sql); + } + @ParameterizedTest @ValueSource(strings = {"DROP EVENT IF EXISTS ev1", "DROP EVENT db1.ev1", "DROP TABLESPACE ts1", "DROP TABLESPACE ts1 ENGINE = InnoDB", "DROP UNDO TABLESPACE ts1", @@ -187,6 +274,11 @@ void rejectsMisplacedClauses() { } } + private static List names(Drop drop) { + return drop.getNames().stream().map(Table::getFullyQualifiedName) + .collect(Collectors.toList()); + } + private static Drop parse(String sql, Dialect dialect) throws JSQLParserException { return (Drop) CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)); }