Skip to content

Commit d8b308d

Browse files
authored
feat: support Oracle DROP SYNONYM (#2855)
1 parent dedc3a8 commit d8b308d

3 files changed

Lines changed: 153 additions & 18 deletions

File tree

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

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
public class Drop implements Statement {
3232

3333
public enum ObjectType {
34-
DATABASE, EVENT, FUNCTION, INDEX, PROCEDURE, SCHEMA, SEQUENCE, SERVER, TABLE, TABLESPACE, TRIGGER, VIEW, FOREIGN_TABLE, AGGREGATE, DOMAIN, EVENT_TRIGGER, GROUP, OWNED, ROLE, ROUTINE, RULE, TYPE, USER, OTHER;
34+
DATABASE, EVENT, FUNCTION, INDEX, PROCEDURE, SCHEMA, SEQUENCE, SERVER, TABLE, TABLESPACE, TRIGGER, VIEW, FOREIGN_TABLE, AGGREGATE, DOMAIN, EVENT_TRIGGER, GROUP, OWNED, ROLE, ROUTINE, RULE, TYPE, USER, SYNONYM, OTHER;
3535

3636
/** Targets are {@link RoutineReference}s, each with an optional argument signature. */
3737
public boolean hasRoutineTargets() {
@@ -66,6 +66,7 @@ public boolean hasOwnerTable() {
6666
private boolean concurrently;
6767
private boolean undo;
6868
private boolean force;
69+
private boolean publicSynonym;
6970
private String engine;
7071
private boolean engineUseEquals;
7172

@@ -339,7 +340,7 @@ public Drop withUndo(boolean undo) {
339340
return this;
340341
}
341342

342-
/** PostgreSQL DROP DATABASE ... WITH (FORCE). */
343+
/** PostgreSQL DROP DATABASE ... WITH (FORCE), or Oracle DROP SYNONYM ... FORCE. */
343344
public boolean isForce() {
344345
return force;
345346
}
@@ -353,6 +354,20 @@ public Drop withForce(boolean force) {
353354
return this;
354355
}
355356

357+
/** Whether the synonym belongs to the PUBLIC namespace. */
358+
public boolean isPublicSynonym() {
359+
return publicSynonym;
360+
}
361+
362+
public void setPublicSynonym(boolean publicSynonym) {
363+
this.publicSynonym = publicSynonym;
364+
}
365+
366+
public Drop withPublicSynonym(boolean publicSynonym) {
367+
setPublicSynonym(publicSynonym);
368+
return this;
369+
}
370+
356371
/** The storage engine of MySQL DROP TABLESPACE ... ENGINE, or null when it is omitted. */
357372
public String getEngine() {
358373
return engine;
@@ -477,6 +492,9 @@ public StringBuilder appendTo(StringBuilder builder, Consumer<Table> tablePrinte
477492
if (undo) {
478493
builder.append("UNDO ");
479494
}
495+
if (publicSynonym) {
496+
builder.append("PUBLIC ");
497+
}
480498
builder.append(type);
481499
if (concurrently) {
482500
builder.append(" CONCURRENTLY");
@@ -487,7 +505,7 @@ public StringBuilder appendTo(StringBuilder builder, Consumer<Table> tablePrinte
487505
builder.append(ifExists ? " IF EXISTS " : " ");
488506
appendTargets(builder, tablePrinter);
489507
if (force) {
490-
builder.append(" WITH (FORCE)");
508+
builder.append(objectType == ObjectType.SYNONYM ? " FORCE" : " WITH (FORCE)");
491509
}
492510
if (engine != null) {
493511
builder.append(engineUseEquals ? " ENGINE = " : " ENGINE ").append(engine);

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

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3351,15 +3351,24 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
33513351
}
33523352
}
33533353

3354-
/**
3355-
* Whether ON names the table of a dropped index, trigger or rule. SQL Server DDL triggers
3356-
* name a scope instead: DROP TRIGGER t ON DATABASE or ON ALL SERVER.
3357-
*/
3358-
private static boolean isDropOwnerTable(Drop drop, Table name) {
3359-
String text = name.getFullyQualifiedName();
3360-
boolean triggerScope = drop.getObjectType() == Drop.ObjectType.TRIGGER
3361-
&& ("DATABASE".equalsIgnoreCase(text) || "ALL".equalsIgnoreCase(text));
3362-
return drop.getObjectType().hasOwnerTable() && !triggerScope;
3354+
/** Keeps DROP lookahead branches out of the generated parser's nearly full constant pool. */
3355+
private static final class DropLookahead {
3356+
private DropLookahead() {}
3357+
3358+
/** SQL Server DDL triggers name a scope rather than an owning table. */
3359+
static boolean isOwnerTable(Drop drop, Table name) {
3360+
String text = name.getFullyQualifiedName();
3361+
boolean triggerScope = drop.getObjectType() == Drop.ObjectType.TRIGGER
3362+
&& ("DATABASE".equalsIgnoreCase(text) || "ALL".equalsIgnoreCase(text));
3363+
return drop.getObjectType().hasOwnerTable() && !triggerScope;
3364+
}
3365+
3366+
static boolean isForceAhead(Drop drop, Token first, Token second, Token third) {
3367+
return drop.getObjectType() == Drop.ObjectType.SYNONYM && first.kind == K_FORCE
3368+
|| drop.getObjectType() == Drop.ObjectType.DATABASE
3369+
&& (first.kind == K_WITH && "(".equals(second.image) && third.kind == K_FORCE
3370+
|| "(".equals(first.image) && second.kind == K_FORCE);
3371+
}
33633372
}
33643373

33653374
/** Builds names from lexer-separated components without reinterpreting PostgreSQL dots. */
@@ -19948,6 +19957,8 @@ Drop Drop():
1994819957
|
1994919958
tk=<K_SCHEMA>
1995019959
|
19960+
[ <K_PUBLIC> { drop.setPublicSynonym(true); } ] tk=<K_SYNONYM>
19961+
|
1995119962
tk=<K_SEQUENCE>
1995219963
|
1995319964
tk=<K_TRIGGER>
@@ -19979,11 +19990,8 @@ Drop Drop():
1997919990

1998019991
DropTargets(drop)
1998119992
[
19982-
// PostgreSQL DROP DATABASE name [ [ WITH ] ( FORCE ) ]
19983-
LOOKAHEAD({ drop.getObjectType() == Drop.ObjectType.DATABASE
19984-
&& (getToken(1).kind == K_WITH && "(".equals(getToken(2).image) && getToken(3).kind == K_FORCE
19985-
|| "(".equals(getToken(1).image) && getToken(2).kind == K_FORCE) })
19986-
[ <K_WITH> ] "(" <K_FORCE> ")" { drop.setForce(true); }
19993+
LOOKAHEAD({ DropLookahead.isForceAhead(drop, getToken(1), getToken(2), getToken(3)) })
19994+
( <K_FORCE> | [ <K_WITH> ] "(" <K_FORCE> ")" ) { drop.setForce(true); }
1998719995
]
1998819996
[
1998919997
// MySQL DROP [UNDO] TABLESPACE name [ ENGINE [=] engine_name ]
@@ -20007,7 +20015,7 @@ Drop Drop():
2000720015
|
2000820016
(
2000920017
<K_ON> name = DropObjectName() {
20010-
if (isDropOwnerTable(drop, name)) {
20018+
if (DropLookahead.isOwnerTable(drop, name)) {
2001120019
requireNoDropOwnerTable(indexTable);
2001220020
indexTable = name;
2001320021
indexTablePosition = dropArgs.size();
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.drop;
11+
12+
import static org.junit.jupiter.api.Assertions.assertEquals;
13+
import static org.junit.jupiter.api.Assertions.assertThrows;
14+
import static org.junit.jupiter.api.Assertions.assertTrue;
15+
16+
import net.sf.jsqlparser.JSQLParserException;
17+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
18+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
19+
import net.sf.jsqlparser.schema.Table;
20+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
21+
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.params.ParameterizedTest;
23+
import org.junit.jupiter.params.provider.ValueSource;
24+
25+
class OracleDropSynonymTest {
26+
@ParameterizedTest
27+
@ValueSource(strings = {"sample_syn", "\"Sample.Synonym\""})
28+
void preservesSynonymModifiers(String name) throws JSQLParserException {
29+
for (String scope : new String[] {"", "PUBLIC "}) {
30+
for (String existence : new String[] {"", "IF EXISTS "}) {
31+
for (String force : new String[] {"", " FORCE"}) {
32+
String sql = "DROP " + scope + "SYNONYM " + existence + name + force;
33+
for (Dialect dialect : new Dialect[] {null, Dialect.ORACLE}) {
34+
Drop drop = parse(sql, dialect);
35+
assertEquals(Drop.ObjectType.SYNONYM, drop.getObjectType());
36+
assertEquals("SYNONYM", drop.getType());
37+
assertEquals(!scope.isEmpty(), drop.isPublicSynonym());
38+
assertEquals(!existence.isEmpty(), drop.isIfExists());
39+
assertEquals(!force.isEmpty(), drop.isForce());
40+
assertEquals(1, drop.getNames().size());
41+
assertEquals(name, drop.getName().getName());
42+
assertEquals(sql, drop.toString());
43+
roundTrip(drop, dialect);
44+
assertEquals(2,
45+
CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size());
46+
}
47+
}
48+
}
49+
}
50+
}
51+
52+
@Test
53+
void preservesQualifiedNamesAndSupportsModelChanges() throws JSQLParserException {
54+
Drop drop = parse("DROP SYNONYM IF EXISTS \"hr.schema\".\"sample.syn\" FORCE",
55+
Dialect.ORACLE);
56+
assertEquals("\"hr.schema\"", drop.getName().getSchemaName());
57+
assertEquals("\"sample.syn\"", drop.getName().getName());
58+
drop.getName().setName("replacement");
59+
assertEquals("DROP SYNONYM IF EXISTS \"hr.schema\".replacement FORCE", drop.toString());
60+
roundTrip(drop, Dialect.ORACLE);
61+
62+
Drop created = new Drop().withObjectType(Drop.ObjectType.SYNONYM)
63+
.withPublicSynonym(true).withIfExists(true).withName(new Table("s"))
64+
.withForce(true);
65+
assertEquals("DROP PUBLIC SYNONYM IF EXISTS s FORCE", created.toString());
66+
roundTrip(created, Dialect.ORACLE);
67+
created.setPublicSynonym(false);
68+
created.setForce(false);
69+
assertEquals("DROP SYNONYM IF EXISTS s", created.toString());
70+
roundTrip(created, null);
71+
}
72+
73+
@Test
74+
void keepsPostgreSqlForceSyntax() throws JSQLParserException {
75+
Drop database = parse("DROP DATABASE d WITH (FORCE)", Dialect.POSTGRESQL);
76+
assertTrue(database.isForce());
77+
assertEquals("DROP DATABASE d WITH (FORCE)", database.toString());
78+
roundTrip(database, Dialect.POSTGRESQL);
79+
}
80+
81+
@ParameterizedTest
82+
@ValueSource(strings = {"DROP PUBLIC TABLE t", "DROP SYNONYM",
83+
"DROP PUBLIC SYNONYM IF EXISTS",
84+
"DROP SYNONYM s FORCE FORCE", "DROP SYNONYM s WITH (FORCE)",
85+
"DROP TABLE t FORCE"})
86+
void rejectsMalformedSynonymClauses(String sql) {
87+
assertThrows(JSQLParserException.class, () -> parse(sql, Dialect.ORACLE));
88+
}
89+
90+
private static Drop parse(String sql, Dialect dialect) throws JSQLParserException {
91+
return (Drop) CCJSqlParserUtil.parse(sql, parser -> {
92+
if (dialect != null) {
93+
parser.withDialect(dialect);
94+
}
95+
});
96+
}
97+
98+
private static void roundTrip(Drop drop, Dialect dialect) throws JSQLParserException {
99+
StringBuilder output = new StringBuilder();
100+
drop.accept(new StatementDeParser(output), null);
101+
assertEquals(drop.toString(), output.toString());
102+
Drop reparsed = parse(output.toString(), dialect);
103+
assertEquals(drop.getObjectType(), reparsed.getObjectType());
104+
assertEquals(drop.isPublicSynonym(), reparsed.isPublicSynonym());
105+
assertEquals(drop.isIfExists(), reparsed.isIfExists());
106+
assertEquals(drop.isForce(), reparsed.isForce());
107+
assertEquals(output.toString(), reparsed.toString());
108+
}
109+
}

0 commit comments

Comments
 (0)