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));
}