diff --git a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java index 2328c8e26..e09ae8553 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -507,6 +507,8 @@ public enum Feature { * @see CreateSchema */ createSchema, + /** SQL ALTER SCHEMA rename and owner changes. */ + alterSchema, /** * SQL "CREATE VIEW" statement is allowed * diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java index 93cb54c75..3b477e274 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.statement; +import net.sf.jsqlparser.statement.alter.schema.AlterSchema; import net.sf.jsqlparser.statement.oracle.OracleBlock; import net.sf.jsqlparser.statement.oracle.OracleAssignment; import net.sf.jsqlparser.statement.oracle.OracleNullStatement; @@ -534,6 +535,14 @@ default void visit(CreateRole statement) { visit(statement, null); } + default T visit(AlterSchema statement, S context) { + return null; + } + + default void visit(AlterSchema statement) { + visit(statement, null); + } + default T visit(AlterRole statement, S context) { return null; } diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/schema/AlterSchema.java b/src/main/java/net/sf/jsqlparser/statement/alter/schema/AlterSchema.java new file mode 100644 index 000000000..44dd63ae8 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/schema/AlterSchema.java @@ -0,0 +1,79 @@ +/*- + * #%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.alter.schema; + +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** PostgreSQL schema name or ownership change. Names retain SQL identifier quoting. */ +public class AlterSchema implements Statement { + public enum Action { + RENAME, OWNER + } + + private String schemaName; + private Action action; + private String newName; + private String owner; + + public String getSchemaName() { + return schemaName; + } + + public void setSchemaName(String schemaName) { + this.schemaName = schemaName; + } + + public Action getAction() { + return action; + } + + public void setAction(Action action) { + this.action = action; + } + + public String getNewName() { + return newName; + } + + public void setNewName(String newName) { + this.newName = newName; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("ALTER SCHEMA ").append(schemaName); + if (action == Action.RENAME) { + builder.append(" RENAME TO ").append(newName); + } else if (action == Action.OWNER) { + builder.append(" OWNER TO ").append(owner); + } else { + throw new IllegalStateException("Expected a schema alteration action"); + } + return builder; + } + + @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 58ed96435..70d777275 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.util; +import net.sf.jsqlparser.statement.alter.schema.AlterSchema; import net.sf.jsqlparser.statement.select.MatchRecognize; import net.sf.jsqlparser.expression.RowPatternFunction; @@ -2783,6 +2784,12 @@ public Void visit(CreateRole statement, S context) { return null; } + @Override + public Void visit(AlterSchema statement, S context) { + // Schema and owner names do not refer to tables. + return null; + } + @Override public Void visit(AlterRole 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 b4ce34096..56f75187f 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.util.deparser; +import net.sf.jsqlparser.statement.alter.schema.AlterSchema; import net.sf.jsqlparser.statement.oracle.OracleBlock; import net.sf.jsqlparser.statement.oracle.OracleAssignment; import net.sf.jsqlparser.statement.oracle.OracleNullStatement; @@ -708,6 +709,11 @@ public StringBuilder visit(CreateRole statement, S context) { return builder; } + @Override + public StringBuilder visit(AlterSchema statement, S context) { + return statement.appendTo(builder); + } + @Override public StringBuilder visit(AlterRole statement, S context) { statement.appendTo(builder, e -> e.accept(expressionDeParser, context)); diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java index e1202fd12..08e137e54 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java @@ -113,7 +113,7 @@ public class FeaturesAllowed implements FeatureSetValidation, ModifyableFeatureS */ public static final FeaturesAllowed ALTER = new FeaturesAllowed("ALTER", Feature.alterTable, Feature.alterSequence, - Feature.alterView, Feature.alterIndex) + Feature.alterView, Feature.alterIndex, Feature.alterSchema) .unmodifyable(); /** * all "DROP" {@link Feature}'s diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java index 5663a4031..e5a9b43f1 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java @@ -103,7 +103,7 @@ public enum PostgresqlVersion implements Version { Feature.createDomain, Feature.alterDomain, Feature.createExtension, Feature.alterExtension, // https://www.postgresql.org/docs/current/sql-altersequence.html Feature.alterSequence, // https://www.postgresql.org/docs/current/sql-createschema.html - Feature.createSchema, // https://www.postgresql.org/docs/current/sql-createindex.html + Feature.createSchema, Feature.alterSchema, // https://www.postgresql.org/docs/current/sql-createindex.html Feature.createIndex, // https://www.postgresql.org/docs/current/sql-createtable.html Feature.createTable, Feature.createTableUnlogged, Feature.createTableCreateOptionStrings, Feature.createTableTableOptionStrings, 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 1567a975e..0b8d2765d 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 @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.util.validation.validator; +import net.sf.jsqlparser.statement.alter.schema.AlterSchema; import net.sf.jsqlparser.statement.oracle.OracleBlock; import net.sf.jsqlparser.statement.oracle.OracleAssignment; import net.sf.jsqlparser.statement.oracle.OracleNullStatement; @@ -882,6 +883,12 @@ public Void visit(CreateRole statement, S context) { return null; } + @Override + public Void visit(AlterSchema statement, S context) { + validateFeature(Feature.alterSchema); + return null; + } + @Override public Void visit(AlterRole statement, S context) { validateFeature(Feature.alterRole); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 6fd8d0375..d1823fe4e 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -49,6 +49,7 @@ import net.sf.jsqlparser.schema.*; import net.sf.jsqlparser.statement.*; import net.sf.jsqlparser.statement.analyze.*; import net.sf.jsqlparser.statement.alter.*; +import net.sf.jsqlparser.statement.alter.schema.AlterSchema; import net.sf.jsqlparser.statement.alter.sequence.*; import net.sf.jsqlparser.statement.comment.*; import net.sf.jsqlparser.statement.create.database.*; @@ -17656,6 +17657,8 @@ Statement Alter(): | statement = AlterDefaultPrivileges() | + LOOKAHEAD() statement = AlterSchema() + | LOOKAHEAD() statement = AlterType() | LOOKAHEAD() statement = AlterDomain() @@ -18095,6 +18098,23 @@ CreateRole CreateRole(): { requireAccessSyntax(!role.isUseWith() || !role.getOptions().isEmpty(), "WITH requires role attributes"); return role; } } +AlterSchema AlterSchema(): +{ + AlterSchema statement = new AlterSchema(); + String name; +} +{ + name=RelObjectName() { statement.setSchemaName(name); } + ( + name=RelObjectName() + { statement.setAction(AlterSchema.Action.RENAME); statement.setNewName(name); } + | + ContextualKeyword("OWNER") name=RelObjectName() + { statement.setAction(AlterSchema.Action.OWNER); statement.setOwner(name); } + ) + { return statement; } +} + AlterRole AlterRole(): { AlterRole role = new AlterRole(); String name; RoleOption option; Token token; Expression value; diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterSchemaTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterSchemaTest.java new file mode 100644 index 000000000..45a9eaa3c --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterSchemaTest.java @@ -0,0 +1,98 @@ +/*- + * #%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.alter; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.alter.schema.AlterSchema; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import net.sf.jsqlparser.util.validation.feature.PostgresqlVersion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class AlterSchemaTest { + @ParameterizedTest + @ValueSource(strings = {"ALTER SCHEMA analytics RENAME TO reporting", + "ALTER SCHEMA \"a.b\" RENAME TO \"next.schema\"", + "ALTER SCHEMA s OWNER TO app", "ALTER SCHEMA s OWNER TO CURRENT_USER", + "ALTER SCHEMA s OWNER TO CURRENT_ROLE", "ALTER SCHEMA s OWNER TO SESSION_USER", + "ALTER SCHEMA \"Case\" OWNER TO \"role.with.dot\""}) + void parsesAndRendersSchemaActions(String sql) throws Exception { + AlterSchema schema = parse(sql); + assertEquals(sql, schema.toString()); + assertTrue(new TablesNamesFinder().getTables(schema).isEmpty()); + roundTrip(schema); + assertTrue(new Validation(List.of(PostgresqlVersion.V14), sql).validate().isEmpty()); + } + + @Test + void supportsAstMutationAndVisitorDispatch() throws Exception { + AlterSchema schema = parse("ALTER SCHEMA s RENAME TO renamed"); + assertEquals(AlterSchema.Action.RENAME, schema.getAction()); + assertEquals("renamed", schema.getNewName()); + schema.setSchemaName("\"schema.name\""); + schema.setNewName("replacement"); + roundTrip(schema); + schema.setAction(AlterSchema.Action.OWNER); + schema.setOwner("CURRENT_ROLE"); + assertEquals("ALTER SCHEMA \"schema.name\" OWNER TO CURRENT_ROLE", schema.toString()); + roundTrip(schema); + Object context = new Object(); + assertEquals("visited", schema.accept(new StatementVisitorAdapter() { + @Override + public String visit(AlterSchema statement, S supplied) { + assertSame(schema, statement); + assertSame(context, supplied); + return "visited"; + } + }, context)); + } + + @Test + void respectsStatementBoundariesAndValidationCapabilities() throws Exception { + Statements statements = CCJSqlParserUtil.parseStatements( + "ALTER SCHEMA s OWNER TO CURRENT_USER; SELECT 1;"); + assertEquals(2, statements.size()); + assertInstanceOf(AlterSchema.class, statements.get(0)); + assertFalse(new Validation(List.of(FeaturesAllowed.SELECT), + "ALTER SCHEMA s RENAME TO r").validate().isEmpty()); + assertTrue(new Validation(List.of(new FeaturesAllowed().add(Feature.alterSchema)), + "ALTER SCHEMA s RENAME TO r").validate().isEmpty()); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER SCHEMA s RENAME", "ALTER SCHEMA s OWNER TO", + "ALTER SCHEMA s RENAME TO 'not an identifier'", "ALTER SCHEMA s OWNER TO a.b", + "ALTER SCHEMA s OWNER TO a, b", "ALTER SCHEMA a.b RENAME TO c"}) + void rejectsIncompleteActionsAndQualifiedSchemaOrRoleNames(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + private static AlterSchema parse(String sql) throws JSQLParserException { + return (AlterSchema) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void roundTrip(AlterSchema schema) throws Exception { + StringBuilder out = new StringBuilder(); + schema.accept(new StatementDeParser(out), null); + assertEquals(schema.toString(), out.toString()); + assertEquals(schema.toString(), parse(out.toString()).toString()); + } +}