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 57b0fdb4f0..9cefb47d1b 100644 --- a/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java +++ b/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java @@ -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() { @@ -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; @@ -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; } @@ -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; @@ -477,6 +492,9 @@ public StringBuilder appendTo(StringBuilder builder, Consumer tablePrinte if (undo) { builder.append("UNDO "); } + if (publicSynonym) { + builder.append("PUBLIC "); + } builder.append(type); if (concurrently) { builder.append(" CONCURRENTLY"); @@ -487,7 +505,7 @@ public StringBuilder appendTo(StringBuilder builder, Consumer
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); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 2138c7eb68..5165685be6 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -3319,15 +3319,24 @@ public class CCJSqlParser extends AbstractJSqlParser { } } - /** - * 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. */ @@ -19920,6 +19929,8 @@ Drop Drop(): | tk= | + [ { drop.setPublicSynonym(true); } ] tk= + | tk= | tk= @@ -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) }) - [ ] "(" ")" { drop.setForce(true); } + LOOKAHEAD({ DropLookahead.isForceAhead(drop, getToken(1), getToken(2), getToken(3)) }) + ( | [ ] "(" ")" ) { drop.setForce(true); } ] [ // MySQL DROP [UNDO] TABLESPACE name [ ENGINE [=] engine_name ] @@ -19979,7 +19987,7 @@ Drop Drop(): | ( name = DropObjectName() { - if (isDropOwnerTable(drop, name)) { + if (DropLookahead.isOwnerTable(drop, name)) { requireNoDropOwnerTable(indexTable); indexTable = name; indexTablePosition = dropArgs.size(); diff --git a/src/test/java/net/sf/jsqlparser/statement/drop/OracleDropSynonymTest.java b/src/test/java/net/sf/jsqlparser/statement/drop/OracleDropSynonymTest.java new file mode 100644 index 0000000000..237ed6671f --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/drop/OracleDropSynonymTest.java @@ -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()); + } +}