Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion src/main/java/net/sf/jsqlparser/statement/drop/Drop.java
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -496,6 +534,9 @@ public StringBuilder appendTo(StringBuilder builder, Consumer<Table> tablePrinte
if (publicSynonym) {
builder.append("PUBLIC ");
}
if (procedural) {
builder.append("PROCEDURAL ");
}
builder.append(type);
if (concurrently) {
builder.append(" CONCURRENTLY");
Expand All @@ -505,6 +546,9 @@ public StringBuilder appendTo(StringBuilder builder, Consumer<Table> 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)");
}
Expand Down
43 changes: 39 additions & 4 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -20367,19 +20367,45 @@ Drop Drop():
List<AlgorithmOrLockOption> indexOptions = new ArrayList<AlgorithmOrLockOption>();
int indexOptionPosition = 0;
boolean useTemporary = false;
String foreignType = null;
String multiWordType = null;
boolean eventTrigger = false;
CascadeBehavior behavior = null;
String engine;
String accessMethod;
}
{
<K_DROP>
[ <K_MATERIALIZED> { drop.setMaterialized(true);} ]
(
<K_FOREIGN> (
<K_TABLE> { foreignType = "FOREIGN TABLE"; }
<K_TABLE> { multiWordType = "FOREIGN TABLE"; }
|
<K_DATA> ContextualKeyword("WRAPPER") { foreignType = "FOREIGN DATA WRAPPER"; }
<K_DATA> 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 }) <K_TEXT_LITERAL> <K_SEARCH>
(
<K_PARSER> { 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 [, ...]
Expand Down Expand Up @@ -20428,7 +20454,7 @@ Drop Drop():
|
tk=<K_GROUP>
)
{ 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 })
<K_CONCURRENTLY> { drop.setConcurrently(true); } ]
Expand All @@ -20437,6 +20463,15 @@ Drop Drop():
<K_IF> <K_EXISTS> {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() })
<K_USING> 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)) })
( <K_FORCE> | [ <K_WITH> ] "(" <K_FORCE> ")" ) { drop.setForce(true); }
Expand Down
7 changes: 6 additions & 1 deletion src/site/sphinx/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -187,6 +274,11 @@ void rejectsMisplacedClauses() {
}
}

private static List<String> 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));
}
Expand Down
Loading