Skip to content

Commit ea2d4fc

Browse files
committed
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 <mmj9808@gmail.com>
1 parent dd03d6f commit ea2d4fc

4 files changed

Lines changed: 182 additions & 6 deletions

File tree

‎src/main/java/net/sf/jsqlparser/statement/drop/Drop.java‎

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
public class Drop extends ASTNodeAccessImpl implements Statement {
3333

3434
public enum ObjectType {
35-
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;
35+
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;
3636

3737
/** Targets are {@link RoutineReference}s, each with an optional argument signature. */
3838
public boolean hasRoutineTargets() {
@@ -48,6 +48,11 @@ public boolean hasAccountTargets() {
4848
public boolean hasOwnerTable() {
4949
return this == INDEX || this == TRIGGER || this == RULE;
5050
}
51+
52+
/** The target is named within the index access method given with USING. */
53+
public boolean hasAccessMethod() {
54+
return this == OPERATOR_CLASS || this == OPERATOR_FAMILY;
55+
}
5156
}
5257

5358
private String type;
@@ -68,6 +73,8 @@ public boolean hasOwnerTable() {
6873
private boolean undo;
6974
private boolean force;
7075
private boolean publicSynonym;
76+
private boolean procedural;
77+
private String accessMethod;
7178
private String engine;
7279
private boolean engineUseEquals;
7380

@@ -369,6 +376,37 @@ public Drop withPublicSynonym(boolean publicSynonym) {
369376
return this;
370377
}
371378

379+
/** Whether a {@link ObjectType#LANGUAGE} is written as PostgreSQL PROCEDURAL LANGUAGE. */
380+
public boolean isProcedural() {
381+
return procedural;
382+
}
383+
384+
public void setProcedural(boolean procedural) {
385+
this.procedural = procedural;
386+
}
387+
388+
public Drop withProcedural(boolean procedural) {
389+
setProcedural(procedural);
390+
return this;
391+
}
392+
393+
/**
394+
* The index access method of an {@link ObjectType#OPERATOR_CLASS} or
395+
* {@link ObjectType#OPERATOR_FAMILY}, written with USING.
396+
*/
397+
public String getAccessMethod() {
398+
return accessMethod;
399+
}
400+
401+
public void setAccessMethod(String accessMethod) {
402+
this.accessMethod = accessMethod;
403+
}
404+
405+
public Drop withAccessMethod(String accessMethod) {
406+
setAccessMethod(accessMethod);
407+
return this;
408+
}
409+
372410
/** The storage engine of MySQL DROP TABLESPACE ... ENGINE, or null when it is omitted. */
373411
public String getEngine() {
374412
return engine;
@@ -496,6 +534,9 @@ public StringBuilder appendTo(StringBuilder builder, Consumer<Table> tablePrinte
496534
if (publicSynonym) {
497535
builder.append("PUBLIC ");
498536
}
537+
if (procedural) {
538+
builder.append("PROCEDURAL ");
539+
}
499540
builder.append(type);
500541
if (concurrently) {
501542
builder.append(" CONCURRENTLY");
@@ -505,6 +546,9 @@ public StringBuilder appendTo(StringBuilder builder, Consumer<Table> tablePrinte
505546
}
506547
builder.append(ifExists ? " IF EXISTS " : " ");
507548
appendTargets(builder, tablePrinter);
549+
if (accessMethod != null) {
550+
builder.append(" USING ").append(accessMethod);
551+
}
508552
if (force) {
509553
builder.append(objectType == ObjectType.SYNONYM ? " FORCE" : " WITH (FORCE)");
510554
}

‎src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt‎

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20367,19 +20367,45 @@ Drop Drop():
2036720367
List<AlgorithmOrLockOption> indexOptions = new ArrayList<AlgorithmOrLockOption>();
2036820368
int indexOptionPosition = 0;
2036920369
boolean useTemporary = false;
20370-
String foreignType = null;
20370+
String multiWordType = null;
2037120371
boolean eventTrigger = false;
2037220372
CascadeBehavior behavior = null;
2037320373
String engine;
20374+
String accessMethod;
2037420375
}
2037520376
{
2037620377
<K_DROP>
2037720378
[ <K_MATERIALIZED> { drop.setMaterialized(true);} ]
2037820379
(
2037920380
<K_FOREIGN> (
20380-
<K_TABLE> { foreignType = "FOREIGN TABLE"; }
20381+
<K_TABLE> { multiWordType = "FOREIGN TABLE"; }
2038120382
|
20382-
<K_DATA> ContextualKeyword("WRAPPER") { foreignType = "FOREIGN DATA WRAPPER"; }
20383+
<K_DATA> ContextualKeyword("WRAPPER") { multiWordType = "FOREIGN DATA WRAPPER"; }
20384+
)
20385+
|
20386+
// PostgreSQL object types of several words, written as COMMENT ON names them
20387+
LOOKAHEAD({ isKeywordAhead("ACCESS") && "METHOD".equalsIgnoreCase(getToken(2).image) })
20388+
ContextualKeyword("ACCESS") ContextualKeyword("METHOD") { multiWordType = "ACCESS METHOD"; }
20389+
|
20390+
LOOKAHEAD({ isKeywordAhead("PROCEDURAL") && "LANGUAGE".equalsIgnoreCase(getToken(2).image) })
20391+
ContextualKeyword("PROCEDURAL") ContextualKeyword("LANGUAGE")
20392+
{ drop.setProcedural(true); multiWordType = "LANGUAGE"; }
20393+
|
20394+
// a schema named CLASS or FAMILY may qualify an operator, as in class.+
20395+
LOOKAHEAD({ isKeywordAhead("OPERATOR") && !".".equals(getToken(3).image)
20396+
&& ("CLASS".equalsIgnoreCase(getToken(2).image) || "FAMILY".equalsIgnoreCase(getToken(2).image)) })
20397+
ContextualKeyword("OPERATOR")
20398+
( LOOKAHEAD({ isKeywordAhead("CLASS") }) ContextualKeyword("CLASS") { multiWordType = "OPERATOR CLASS"; }
20399+
| ContextualKeyword("FAMILY") { multiWordType = "OPERATOR FAMILY"; } )
20400+
|
20401+
LOOKAHEAD({ isKeywordAhead("TEXT") && getToken(2).kind == K_SEARCH }) <K_TEXT_LITERAL> <K_SEARCH>
20402+
(
20403+
<K_PARSER> { multiWordType = "TEXT SEARCH PARSER"; }
20404+
| LOOKAHEAD({ isKeywordAhead("CONFIGURATION") }) ContextualKeyword("CONFIGURATION")
20405+
{ multiWordType = "TEXT SEARCH CONFIGURATION"; }
20406+
| LOOKAHEAD({ isKeywordAhead("DICTIONARY") }) ContextualKeyword("DICTIONARY")
20407+
{ multiWordType = "TEXT SEARCH DICTIONARY"; }
20408+
| ContextualKeyword("TEMPLATE") { multiWordType = "TEXT SEARCH TEMPLATE"; }
2038320409
)
2038420410
|
2038520411
// PostgreSQL DROP OWNED BY role [, ...]
@@ -20428,7 +20454,7 @@ Drop Drop():
2042820454
|
2042920455
tk=<K_GROUP>
2043020456
)
20431-
{ drop.setType(foreignType != null ? foreignType : eventTrigger ? "EVENT TRIGGER" : tk.image); }
20457+
{ drop.setType(multiWordType != null ? multiWordType : eventTrigger ? "EVENT TRIGGER" : tk.image); }
2043220458
[ LOOKAHEAD({ drop.getObjectType() == Drop.ObjectType.INDEX
2043320459
&& getToken(1).kind == K_CONCURRENTLY })
2043420460
<K_CONCURRENTLY> { drop.setConcurrently(true); } ]
@@ -20437,6 +20463,15 @@ Drop Drop():
2043720463
<K_IF> <K_EXISTS> {drop.setIfExists(true);} ]
2043820464

2043920465
DropTargets(drop)
20466+
[
20467+
// PostgreSQL DROP OPERATOR CLASS and FAMILY require USING: they name one object of an
20468+
// index access method. The semantic lookahead alone selects this clause.
20469+
LOOKAHEAD({ drop.getObjectType().hasAccessMethod() })
20470+
<K_USING> accessMethod=RelObjectName() {
20471+
requireDdlSyntax(drop.getNames().size() == 1, "DROP " + drop.getType() + " takes one name");
20472+
drop.setAccessMethod(accessMethod);
20473+
}
20474+
]
2044020475
[
2044120476
LOOKAHEAD({ DropLookahead.isForceAhead(drop, getToken(1), getToken(2), getToken(3)) })
2044220477
( <K_FORCE> | [ <K_WITH> ] "(" <K_FORCE> ")" ) { drop.setForce(true); }

‎src/site/sphinx/usage.rst‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,9 +271,14 @@ typed properties:
271271
``DROP [UNDO] TABLESPACE name [ENGINE [=] engine]``. ``isForce()`` covers
272272
PostgreSQL ``DROP DATABASE name [WITH] (FORCE)``; the optional WITH is
273273
always rendered.
274+
* ``getAccessMethod()`` holds the index access method of PostgreSQL
275+
``DROP OPERATOR CLASS|FAMILY name USING method``, which takes one name.
276+
``isProcedural()`` records the ``PROCEDURAL LANGUAGE`` spelling.
274277

275278
TYPE, DOMAIN, EVENT, EVENT TRIGGER, TABLESPACE, USER and GROUP are recognized
276-
object types. PostgreSQL ``REASSIGN OWNED BY old_role [, ...] TO new_role`` is
279+
object types, and so are PostgreSQL's ACCESS METHOD, LANGUAGE, OPERATOR CLASS,
280+
OPERATOR FAMILY and TEXT SEARCH CONFIGURATION, DICTIONARY, PARSER and TEMPLATE.
281+
``getType()`` returns their words separated by spaces, as in ``ACCESS METHOD``. PostgreSQL ``REASSIGN OWNED BY old_role [, ...] TO new_role`` is
277282
parsed into ``ReassignOwned``, whose roles use the same ``UserAccount`` model.
278283

279284
.. code-block:: java

‎src/test/java/net/sf/jsqlparser/statement/drop/DropObjectTypeTest.java‎

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import java.util.List;
1616
import java.util.Set;
17+
import java.util.stream.Collectors;
1718
import net.sf.jsqlparser.JSQLParserException;
1819
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
1920
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
@@ -122,6 +123,92 @@ void postgreSqlObjectTypes(String sql) throws JSQLParserException {
122123
}
123124
}
124125

126+
@ParameterizedTest
127+
@ValueSource(strings = {"DROP ACCESS METHOD heptree",
128+
"DROP ACCESS METHOD IF EXISTS heptree CASCADE", "DROP PROCEDURAL LANGUAGE plsample",
129+
"DROP PROCEDURAL LANGUAGE IF EXISTS plsample RESTRICT",
130+
"DROP LANGUAGE IF EXISTS plsample CASCADE",
131+
"DROP OPERATOR CLASS IF EXISTS public.myops USING btree CASCADE",
132+
"DROP OPERATOR FAMILY myfam USING hash",
133+
"DROP TEXT SEARCH CONFIGURATION IF EXISTS my_cfg CASCADE",
134+
"DROP TEXT SEARCH DICTIONARY english_stem, public.french_stem",
135+
"DROP TEXT SEARCH PARSER my_parser",
136+
"DROP TEXT SEARCH TEMPLATE IF EXISTS my_tmpl RESTRICT"})
137+
void postgreSqlMultiWordObjectTypes(String sql) throws JSQLParserException {
138+
for (Drop drop : roundTrip(sql, Dialect.POSTGRESQL)) {
139+
assertEquals(sql, drop.toString());
140+
assertNotEquals(Drop.ObjectType.OTHER, drop.getObjectType());
141+
assertTrue(new TablesNamesFinder().getTables(drop).isEmpty());
142+
}
143+
assertEquals(sql, CCJSqlParserUtil.parse(sql).toString());
144+
assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1",
145+
parser -> parser.withDialect(Dialect.POSTGRESQL)).size());
146+
}
147+
148+
@Test
149+
void multiWordObjectTypeFields() throws JSQLParserException {
150+
Drop accessMethod =
151+
parse("DROP ACCESS METHOD IF EXISTS heptree CASCADE", Dialect.POSTGRESQL);
152+
assertEquals(Drop.ObjectType.ACCESS_METHOD, accessMethod.getObjectType());
153+
assertEquals("ACCESS METHOD", accessMethod.getType());
154+
assertEquals(List.of("heptree"), names(accessMethod));
155+
assertTrue(accessMethod.isIfExists());
156+
assertEquals(CascadeBehavior.CASCADE, accessMethod.getBehavior());
157+
158+
Drop language = parse("DROP PROCEDURAL LANGUAGE plsample", Dialect.POSTGRESQL);
159+
assertEquals(Drop.ObjectType.LANGUAGE, language.getObjectType());
160+
assertTrue(language.isProcedural());
161+
assertEquals(List.of("plsample"), names(language));
162+
language.setProcedural(false);
163+
assertEquals("DROP LANGUAGE plsample", language.toString());
164+
assertFalse(parse("DROP LANGUAGE plsample", Dialect.POSTGRESQL).isProcedural());
165+
166+
Drop operatorClass = parse("DROP OPERATOR CLASS IF EXISTS public.myops USING btree",
167+
Dialect.POSTGRESQL);
168+
assertEquals(Drop.ObjectType.OPERATOR_CLASS, operatorClass.getObjectType());
169+
assertEquals(List.of("public.myops"), names(operatorClass));
170+
assertEquals("btree", operatorClass.getAccessMethod());
171+
assertNull(operatorClass.getParameters());
172+
operatorClass.setAccessMethod("hash");
173+
assertEquals("DROP OPERATOR CLASS IF EXISTS public.myops USING hash",
174+
operatorClass.toString());
175+
assertEquals(Drop.ObjectType.OPERATOR_FAMILY,
176+
parse("DROP OPERATOR FAMILY myfam USING gist", Dialect.POSTGRESQL).getObjectType());
177+
178+
Drop dictionary = parse("DROP TEXT SEARCH DICTIONARY english_stem, public.french_stem",
179+
Dialect.POSTGRESQL);
180+
assertEquals(Drop.ObjectType.TEXT_SEARCH_DICTIONARY, dictionary.getObjectType());
181+
assertEquals(List.of("english_stem", "public.french_stem"), names(dictionary));
182+
183+
assertEquals("DROP OPERATOR FAMILY f USING btree CASCADE",
184+
new Drop().withObjectType(Drop.ObjectType.OPERATOR_FAMILY)
185+
.withName(new Table("f")).withAccessMethod("btree")
186+
.withBehavior(CascadeBehavior.CASCADE).toString());
187+
assertEquals("DROP PROCEDURAL LANGUAGE l", new Drop()
188+
.withObjectType(Drop.ObjectType.LANGUAGE).withProcedural(true)
189+
.withName(new Table("l")).toString());
190+
}
191+
192+
@Test
193+
void multiWordTypeWordsRemainNames() throws JSQLParserException {
194+
assertEquals(List.of("procedural"),
195+
names(parse("DROP LANGUAGE procedural", Dialect.POSTGRESQL)));
196+
assertEquals(List.of("method"),
197+
names(parse("DROP ACCESS METHOD method", Dialect.POSTGRESQL)));
198+
assertEquals(List.of("access"), names(parse("DROP TABLE access", Dialect.POSTGRESQL)));
199+
assertEquals(List.of("family"),
200+
names(parse("DROP OPERATOR CLASS family USING btree", Dialect.POSTGRESQL)));
201+
}
202+
203+
@ParameterizedTest
204+
@ValueSource(strings = {"DROP OPERATOR CLASS myops", "DROP OPERATOR FAMILY f1, f2 USING btree",
205+
"DROP OPERATOR CLASS myops USING", "DROP ACCESS METHOD",
206+
"DROP ACCESS METHOD IF EXISTS", "DROP TEXT SEARCH INDEX x", "DROP TEXT SEARCH",
207+
"DROP PROCEDURAL LANGUAGE", "DROP ACCESS METHOD heptree USING btree"})
208+
void rejectsIncompleteMultiWordTypes(String sql) {
209+
assertThrows(JSQLParserException.class, () -> parse(sql, Dialect.POSTGRESQL), sql);
210+
}
211+
125212
@ParameterizedTest
126213
@ValueSource(strings = {"DROP EVENT IF EXISTS ev1", "DROP EVENT db1.ev1", "DROP TABLESPACE ts1",
127214
"DROP TABLESPACE ts1 ENGINE = InnoDB", "DROP UNDO TABLESPACE ts1",
@@ -187,6 +274,11 @@ void rejectsMisplacedClauses() {
187274
}
188275
}
189276

277+
private static List<String> names(Drop drop) {
278+
return drop.getNames().stream().map(Table::getFullyQualifiedName)
279+
.collect(Collectors.toList());
280+
}
281+
190282
private static Drop parse(String sql, Dialect dialect) throws JSQLParserException {
191283
return (Drop) CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect));
192284
}

0 commit comments

Comments
 (0)