diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java index 70b82c6220..fea4497b29 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java @@ -85,6 +85,7 @@ import net.sf.jsqlparser.statement.delete.Delete; import net.sf.jsqlparser.statement.drop.Drop; import net.sf.jsqlparser.statement.drop.DropPolicy; +import net.sf.jsqlparser.statement.drop.DropUserMapping; import net.sf.jsqlparser.statement.execute.Execute; import net.sf.jsqlparser.statement.export.Export; import net.sf.jsqlparser.statement.export.ExportDataStatement; @@ -1365,6 +1366,11 @@ public Void visit(AlterUserMapping statement, S context) { return schemaOnly(); } + @Override + public Void visit(DropUserMapping statement, S context) { + return schemaOnly(); + } + @Override public Void visit(CreateRule statement, S context) { return schemaOnly(); diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java index 5cdbcf5665..c802107c0b 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -27,6 +27,7 @@ import net.sf.jsqlparser.statement.alter.AlterTrigger; import net.sf.jsqlparser.statement.alter.AlterPolicy; import net.sf.jsqlparser.statement.drop.DropPolicy; +import net.sf.jsqlparser.statement.drop.DropUserMapping; import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; import net.sf.jsqlparser.statement.alter.AlterStatistics; import net.sf.jsqlparser.statement.alter.AlterRelation; @@ -945,6 +946,14 @@ default void visit(AlterUserMapping statement) { visit(statement, null); } + default T visit(DropUserMapping statement, S context) { + return null; + } + + default void visit(DropUserMapping statement) { + visit(statement, null); + } + default T visit(CreateAccessMethod statement, S context) { return null; } diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java index 3a561589e5..5367cc9a0c 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java @@ -15,6 +15,7 @@ import net.sf.jsqlparser.statement.alter.AlterRoutine; import net.sf.jsqlparser.statement.alter.AlterTrigger; import net.sf.jsqlparser.statement.drop.DropPolicy; +import net.sf.jsqlparser.statement.drop.DropUserMapping; import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; import net.sf.jsqlparser.statement.create.fdw.CreateForeignDataWrapper; import net.sf.jsqlparser.statement.alter.AlterForeignDataWrapper; @@ -1087,6 +1088,11 @@ public T visit(AlterUserMapping statement, S context) { return visitForeignDataStatement(statement, context); } + @Override + public T visit(DropUserMapping statement, S context) { + return null; + } + @Override public T visit(CreateRule statement, S context) { statement.visitTables(table -> table.accept(fromItemVisitor, context)); 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 9cefb47d1b..ddd56863dc 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, 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, OTHER; /** Targets are {@link RoutineReference}s, each with an optional argument signature. */ public boolean hasRoutineTargets() { diff --git a/src/main/java/net/sf/jsqlparser/statement/drop/DropUserMapping.java b/src/main/java/net/sf/jsqlparser/statement/drop/DropUserMapping.java new file mode 100644 index 0000000000..a0324e94b8 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/drop/DropUserMapping.java @@ -0,0 +1,66 @@ +/*- + * #%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 net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** + * PostgreSQL {@code DROP USER MAPPING [IF EXISTS] FOR user SERVER server}. A user mapping has no + * name of its own, so it is identified by the user and the server, as in + * {@link net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping}. + */ +public class DropUserMapping implements Statement { + private String user; + private String server; + private boolean ifExists; + + public String getUser() { + return user; + } + + public void setUser(String value) { + user = value; + } + + public String getServer() { + return server; + } + + public void setServer(String value) { + server = value; + } + + public boolean isIfExists() { + return ifExists; + } + + public void setIfExists(boolean value) { + ifExists = value; + } + + public StringBuilder appendTo(StringBuilder sql) { + sql.append("DROP USER MAPPING "); + if (ifExists) { + sql.append("IF EXISTS "); + } + return sql.append("FOR ").append(user).append(" SERVER ").append(server); + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 4f9b44f912..6f3841e8ec 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -176,6 +176,7 @@ import net.sf.jsqlparser.statement.delete.ParenthesedDelete; import net.sf.jsqlparser.statement.drop.Drop; import net.sf.jsqlparser.statement.drop.DropPolicy; +import net.sf.jsqlparser.statement.drop.DropUserMapping; import net.sf.jsqlparser.statement.execute.Execute; import net.sf.jsqlparser.statement.execute.ExecuteArgument; import net.sf.jsqlparser.statement.export.Export; @@ -3247,6 +3248,12 @@ public Void visit(AlterUserMapping statement, S context) { return null; } + @Override + public Void visit(DropUserMapping statement, S context) { + // User and server names do not refer to tables. + return null; + } + @Override public Void visit(CreateAccessMethod statement, S context) { return null; diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java index 9b34ba440b..6a3e88c7cd 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -27,6 +27,7 @@ import net.sf.jsqlparser.statement.alter.AlterTrigger; import net.sf.jsqlparser.statement.alter.AlterPolicy; import net.sf.jsqlparser.statement.drop.DropPolicy; +import net.sf.jsqlparser.statement.drop.DropUserMapping; import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; import net.sf.jsqlparser.statement.alter.AlterStatistics; import net.sf.jsqlparser.statement.alter.AlterRelation; @@ -1003,6 +1004,11 @@ public StringBuilder visit(AlterUserMapping statement, S context) { expression -> expression.accept(expressionDeParser, context)); } + @Override + public StringBuilder visit(DropUserMapping statement, S context) { + return statement.appendTo(builder); + } + @Override public StringBuilder visit(CreateAccessMethod statement, S context) { return statement.appendTo(builder); diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java index e6d14ec931..e15bded789 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java @@ -13,6 +13,7 @@ import net.sf.jsqlparser.statement.alter.AlterTablespace; import net.sf.jsqlparser.statement.alter.AlterPolicy; import net.sf.jsqlparser.statement.drop.DropPolicy; +import net.sf.jsqlparser.statement.drop.DropUserMapping; import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; import net.sf.jsqlparser.statement.create.accessmethod.CreateAccessMethod; @@ -1292,6 +1293,12 @@ public Void visit(AlterUserMapping statement, S context) { return null; } + @Override + public Void visit(DropUserMapping statement, S context) { + validateFeature(Feature.drop); + return null; + } + @Override public Void visit(CreateAccessMethod statement, S context) { validateFeature(Feature.createAccessMethod); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index f2bf3b59cb..f0dbab5b46 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -4868,6 +4868,11 @@ Statement SingleStatement() : | LOOKAHEAD( ) stm = DropPolicy() | + LOOKAHEAD({ getToken(1).kind == K_DROP && getToken(2).kind == K_USER + && "MAPPING".equalsIgnoreCase(getToken(3).image) + && (getToken(4).kind == K_FOR || getToken(4).kind == K_IF) }) + stm = DropUserMapping() + | stm = Drop() | LOOKAHEAD({ getToken(1).kind == K_REASSIGN && "OWNED".equalsIgnoreCase(getToken(2).image) }) @@ -19994,7 +19999,7 @@ Drop Drop(): List indexOptions = new ArrayList(); int indexOptionPosition = 0; boolean useTemporary = false; - boolean foreign = false; + String foreignType = null; boolean eventTrigger = false; CascadeBehavior behavior = null; String engine; @@ -20003,7 +20008,11 @@ Drop Drop(): [ { drop.setMaterialized(true);} ] ( - tk= { foreign = true; } + ( + { foreignType = "FOREIGN TABLE"; } + | + ContextualKeyword("WRAPPER") { foreignType = "FOREIGN DATA WRAPPER"; } + ) | // PostgreSQL DROP OWNED BY role [, ...] LOOKAHEAD({ isKeywordAhead("OWNED") && getToken(2).kind == K_BY }) tk= @@ -20051,7 +20060,7 @@ Drop Drop(): | tk= ) - { drop.setType(foreign ? "FOREIGN TABLE" : eventTrigger ? "EVENT TRIGGER" : tk.image); } + { drop.setType(foreignType != null ? foreignType : eventTrigger ? "EVENT TRIGGER" : tk.image); } [ LOOKAHEAD({ drop.getObjectType() == Drop.ObjectType.INDEX && getToken(1).kind == K_CONCURRENTLY }) { drop.setConcurrently(true); } ] @@ -25252,6 +25261,18 @@ CreateUserMapping CreateUserMapping(): { return statement; } } +DropUserMapping DropUserMapping(): +{ + DropUserMapping statement = new DropUserMapping(); String name; +} +{ + ContextualKeyword("MAPPING") + [ LOOKAHEAD(2) { statement.setIfExists(true); } ] + name=RelObjectName() { statement.setUser(name); } + ContextualKeyword("SERVER") name=RelObjectName() { statement.setServer(name); } + { return statement; } +} + AlterUserMapping AlterUserMapping(): { AlterUserMapping statement = new AlterUserMapping(); String name; List options; diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlForeignDataObjectsTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlForeignDataObjectsTest.java index 83f2f07de6..e7eef7d45a 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlForeignDataObjectsTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlForeignDataObjectsTest.java @@ -12,11 +12,14 @@ import static org.junit.jupiter.api.Assertions.*; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.NullValue; import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.CascadeBehavior; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.StatementVisitorAdapter; import net.sf.jsqlparser.statement.alter.AlterForeignDataWrapper; @@ -27,6 +30,8 @@ import net.sf.jsqlparser.statement.create.server.CreateServer; import net.sf.jsqlparser.statement.create.table.ForeignDataOption; import net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping; +import net.sf.jsqlparser.statement.drop.Drop; +import net.sf.jsqlparser.statement.drop.DropUserMapping; import net.sf.jsqlparser.statement.foreign.ForeignDataStatement; import net.sf.jsqlparser.util.TablesNamesFinder; import net.sf.jsqlparser.util.deparser.StatementDeParser; @@ -70,6 +75,50 @@ void foreignObjectsShareOptionsAndKeepBoundaries(String sql) throws JSQLParserEx assertFalse(new Validation(List.of(FeaturesAllowed.SELECT), sql).validate().isEmpty()); } + @ParameterizedTest + @ValueSource(strings = {"DROP FOREIGN DATA WRAPPER w", + "DROP FOREIGN DATA WRAPPER IF EXISTS w1, w2 CASCADE", + "DROP FOREIGN DATA WRAPPER w RESTRICT", + "DROP USER MAPPING FOR CURRENT_USER SERVER s", + "DROP USER MAPPING IF EXISTS FOR PUBLIC SERVER s", + "DROP USER MAPPING FOR USER SERVER s", + "DROP USER MAPPING FOR \"user name\" SERVER \"server name\""}) + void dropsForeignDataObjects(String sql) throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql); + assertEquals(sql, statement.toString()); + assertTrue(new TablesNamesFinder().getTables(statement).isEmpty()); + roundTrip(statement); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size()); + assertTrue(new Validation(List.of(FeaturesAllowed.DDL), sql).validate().isEmpty()); + assertFalse(new Validation(List.of(FeaturesAllowed.SELECT), sql).validate().isEmpty()); + } + + @Test + void dropStatementsKeepObjectKindAndMappingKey() throws JSQLParserException { + Drop wrapper = (Drop) CCJSqlParserUtil.parse( + "DROP FOREIGN DATA WRAPPER IF EXISTS w1, w2 CASCADE"); + assertEquals(Drop.ObjectType.FOREIGN_DATA_WRAPPER, wrapper.getObjectType()); + assertEquals(List.of("w1", "w2"), + wrapper.getNames().stream().map(Table::getFullyQualifiedName) + .collect(Collectors.toList())); + assertTrue(wrapper.isIfExists()); + assertEquals(CascadeBehavior.CASCADE, wrapper.getBehavior()); + + DropUserMapping mapping = (DropUserMapping) CCJSqlParserUtil.parse( + "DROP USER MAPPING IF EXISTS FOR CURRENT_USER SERVER s"); + assertTrue(mapping.isIfExists()); + assertEquals("CURRENT_USER", mapping.getUser()); + assertEquals("s", mapping.getServer()); + mapping.setIfExists(false); + mapping.setServer("s2"); + assertEquals("DROP USER MAPPING FOR CURRENT_USER SERVER s2", mapping.toString()); + roundTrip(mapping); + + // MAPPING followed by neither FOR nor IF is still a user name + Drop user = (Drop) CCJSqlParserUtil.parse("DROP USER mapping"); + assertEquals(Drop.ObjectType.USER, user.getObjectType()); + } + @Test void omittedAndRemovedHandlersStayDistinct() throws JSQLParserException { CreateForeignDataWrapper create = (CreateForeignDataWrapper) CCJSqlParserUtil.parse( @@ -210,7 +259,9 @@ public String visit(CreateUserMapping statement, S context) { "ALTER SERVER s OPTIONS(SET x)", "ALTER SERVER s OPTIONS(DROP x 'y')", "ALTER SERVER s OPTIONS()", "ALTER SERVER s OPTIONS(host = 'localhost')", "CREATE SERVER s TYPE NULL FOREIGN DATA WRAPPER w", "ALTER SERVER s VERSION 18", - "ALTER SERVER s RENAME TO s2 OPTIONS(x 'y')"}) + "ALTER SERVER s RENAME TO s2 OPTIONS(x 'y')", + "DROP USER MAPPING FOR u", "DROP USER MAPPING FOR u SERVER s CASCADE", + "DROP USER MAPPING IF EXISTS u SERVER s"}) void rejectsWrongObjectAndOptionForms(String sql) { assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); }