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 d0e751a58..2c29b8dc8 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 a361fa20f..8b2096ba5 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 c784f81d3..abccd1801 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 5906128b2..aeff84ef4 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)); }