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
24 changes: 21 additions & 3 deletions src/main/java/net/sf/jsqlparser/statement/drop/Drop.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
public class Drop implements Statement {

public enum ObjectType {
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;
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;

/** Targets are {@link RoutineReference}s, each with an optional argument signature. */
public boolean hasRoutineTargets() {
Expand Down Expand Up @@ -66,6 +66,7 @@ public boolean hasOwnerTable() {
private boolean concurrently;
private boolean undo;
private boolean force;
private boolean publicSynonym;
private String engine;
private boolean engineUseEquals;

Expand Down Expand Up @@ -339,7 +340,7 @@ public Drop withUndo(boolean undo) {
return this;
}

/** PostgreSQL DROP DATABASE ... WITH (FORCE). */
/** PostgreSQL DROP DATABASE ... WITH (FORCE), or Oracle DROP SYNONYM ... FORCE. */
public boolean isForce() {
return force;
}
Expand All @@ -353,6 +354,20 @@ public Drop withForce(boolean force) {
return this;
}

/** Whether the synonym belongs to the PUBLIC namespace. */
public boolean isPublicSynonym() {
return publicSynonym;
}

public void setPublicSynonym(boolean publicSynonym) {
this.publicSynonym = publicSynonym;
}

public Drop withPublicSynonym(boolean publicSynonym) {
setPublicSynonym(publicSynonym);
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 @@ -477,6 +492,9 @@ public StringBuilder appendTo(StringBuilder builder, Consumer<Table> tablePrinte
if (undo) {
builder.append("UNDO ");
}
if (publicSynonym) {
builder.append("PUBLIC ");
}
builder.append(type);
if (concurrently) {
builder.append(" CONCURRENTLY");
Expand All @@ -487,7 +505,7 @@ public StringBuilder appendTo(StringBuilder builder, Consumer<Table> tablePrinte
builder.append(ifExists ? " IF EXISTS " : " ");
appendTargets(builder, tablePrinter);
if (force) {
builder.append(" WITH (FORCE)");
builder.append(objectType == ObjectType.SYNONYM ? " FORCE" : " WITH (FORCE)");
}
if (engine != null) {
builder.append(engineUseEquals ? " ENGINE = " : " ENGINE ").append(engine);
Expand Down
38 changes: 23 additions & 15 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -3319,15 +3319,24 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
}
}

/**
* Whether ON names the table of a dropped index, trigger or rule. SQL Server DDL triggers
* name a scope instead: DROP TRIGGER t ON DATABASE or ON ALL SERVER.
*/
private static boolean isDropOwnerTable(Drop drop, Table name) {
String text = name.getFullyQualifiedName();
boolean triggerScope = drop.getObjectType() == Drop.ObjectType.TRIGGER
&& ("DATABASE".equalsIgnoreCase(text) || "ALL".equalsIgnoreCase(text));
return drop.getObjectType().hasOwnerTable() && !triggerScope;
/** Keeps DROP lookahead branches out of the generated parser's nearly full constant pool. */
private static final class DropLookahead {
private DropLookahead() {}

/** SQL Server DDL triggers name a scope rather than an owning table. */
static boolean isOwnerTable(Drop drop, Table name) {
String text = name.getFullyQualifiedName();
boolean triggerScope = drop.getObjectType() == Drop.ObjectType.TRIGGER
&& ("DATABASE".equalsIgnoreCase(text) || "ALL".equalsIgnoreCase(text));
return drop.getObjectType().hasOwnerTable() && !triggerScope;
}

static boolean isForceAhead(Drop drop, Token first, Token second, Token third) {
return drop.getObjectType() == Drop.ObjectType.SYNONYM && first.kind == K_FORCE
|| drop.getObjectType() == Drop.ObjectType.DATABASE
&& (first.kind == K_WITH && "(".equals(second.image) && third.kind == K_FORCE
|| "(".equals(first.image) && second.kind == K_FORCE);
}
}

/** Builds names from lexer-separated components without reinterpreting PostgreSQL dots. */
Expand Down Expand Up @@ -19920,6 +19929,8 @@ Drop Drop():
|
tk=<K_SCHEMA>
|
[ <K_PUBLIC> { drop.setPublicSynonym(true); } ] tk=<K_SYNONYM>
|
tk=<K_SEQUENCE>
|
tk=<K_TRIGGER>
Expand Down Expand Up @@ -19951,11 +19962,8 @@ Drop Drop():

DropTargets(drop)
[
// PostgreSQL DROP DATABASE name [ [ WITH ] ( FORCE ) ]
LOOKAHEAD({ drop.getObjectType() == Drop.ObjectType.DATABASE
&& (getToken(1).kind == K_WITH && "(".equals(getToken(2).image) && getToken(3).kind == K_FORCE
|| "(".equals(getToken(1).image) && getToken(2).kind == K_FORCE) })
[ <K_WITH> ] "(" <K_FORCE> ")" { drop.setForce(true); }
LOOKAHEAD({ DropLookahead.isForceAhead(drop, getToken(1), getToken(2), getToken(3)) })
( <K_FORCE> | [ <K_WITH> ] "(" <K_FORCE> ")" ) { drop.setForce(true); }
]
[
// MySQL DROP [UNDO] TABLESPACE name [ ENGINE [=] engine_name ]
Expand All @@ -19979,7 +19987,7 @@ Drop Drop():
|
(
<K_ON> name = DropObjectName() {
if (isDropOwnerTable(drop, name)) {
if (DropLookahead.isOwnerTable(drop, name)) {
requireNoDropOwnerTable(indexTable);
indexTable = name;
indexTablePosition = dropArgs.size();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*-
* #%L
* JSQLParser library
* %%
* Copyright (C) 2004 - 2026 JSQLParser
* %%
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
* #L%
*/
package net.sf.jsqlparser.statement.drop;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.util.deparser.StatementDeParser;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class OracleDropSynonymTest {
@ParameterizedTest
@ValueSource(strings = {"sample_syn", "\"Sample.Synonym\""})
void preservesSynonymModifiers(String name) throws JSQLParserException {
for (String scope : new String[] {"", "PUBLIC "}) {
for (String existence : new String[] {"", "IF EXISTS "}) {
for (String force : new String[] {"", " FORCE"}) {
String sql = "DROP " + scope + "SYNONYM " + existence + name + force;
for (Dialect dialect : new Dialect[] {null, Dialect.ORACLE}) {
Drop drop = parse(sql, dialect);
assertEquals(Drop.ObjectType.SYNONYM, drop.getObjectType());
assertEquals("SYNONYM", drop.getType());
assertEquals(!scope.isEmpty(), drop.isPublicSynonym());
assertEquals(!existence.isEmpty(), drop.isIfExists());
assertEquals(!force.isEmpty(), drop.isForce());
assertEquals(1, drop.getNames().size());
assertEquals(name, drop.getName().getName());
assertEquals(sql, drop.toString());
roundTrip(drop, dialect);
assertEquals(2,
CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size());
}
}
}
}
}

@Test
void preservesQualifiedNamesAndSupportsModelChanges() throws JSQLParserException {
Drop drop = parse("DROP SYNONYM IF EXISTS \"hr.schema\".\"sample.syn\" FORCE",
Dialect.ORACLE);
assertEquals("\"hr.schema\"", drop.getName().getSchemaName());
assertEquals("\"sample.syn\"", drop.getName().getName());
drop.getName().setName("replacement");
assertEquals("DROP SYNONYM IF EXISTS \"hr.schema\".replacement FORCE", drop.toString());
roundTrip(drop, Dialect.ORACLE);

Drop created = new Drop().withObjectType(Drop.ObjectType.SYNONYM)
.withPublicSynonym(true).withIfExists(true).withName(new Table("s"))
.withForce(true);
assertEquals("DROP PUBLIC SYNONYM IF EXISTS s FORCE", created.toString());
roundTrip(created, Dialect.ORACLE);
created.setPublicSynonym(false);
created.setForce(false);
assertEquals("DROP SYNONYM IF EXISTS s", created.toString());
roundTrip(created, null);
}

@Test
void keepsPostgreSqlForceSyntax() throws JSQLParserException {
Drop database = parse("DROP DATABASE d WITH (FORCE)", Dialect.POSTGRESQL);
assertTrue(database.isForce());
assertEquals("DROP DATABASE d WITH (FORCE)", database.toString());
roundTrip(database, Dialect.POSTGRESQL);
}

@ParameterizedTest
@ValueSource(strings = {"DROP PUBLIC TABLE t", "DROP SYNONYM",
"DROP PUBLIC SYNONYM IF EXISTS",
"DROP SYNONYM s FORCE FORCE", "DROP SYNONYM s WITH (FORCE)",
"DROP TABLE t FORCE"})
void rejectsMalformedSynonymClauses(String sql) {
assertThrows(JSQLParserException.class, () -> parse(sql, Dialect.ORACLE));
}

private static Drop parse(String sql, Dialect dialect) throws JSQLParserException {
return (Drop) CCJSqlParserUtil.parse(sql, parser -> {
if (dialect != null) {
parser.withDialect(dialect);
}
});
}

private static void roundTrip(Drop drop, Dialect dialect) throws JSQLParserException {
StringBuilder output = new StringBuilder();
drop.accept(new StatementDeParser(output), null);
assertEquals(drop.toString(), output.toString());
Drop reparsed = parse(output.toString(), dialect);
assertEquals(drop.getObjectType(), reparsed.getObjectType());
assertEquals(drop.isPublicSynonym(), reparsed.isPublicSynonym());
assertEquals(drop.isIfExists(), reparsed.isIfExists());
assertEquals(drop.isForce(), reparsed.isForce());
assertEquals(output.toString(), reparsed.toString());
}
}
Loading