diff --git a/README.md b/README.md index d39e81ea6..0afa5b6d9 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J | **DDL** | `CREATE …` · `ALTER …` · `DROP …` | | **MySQL table maintenance** | `ANALYZE` · `CHECK` · `OPTIMIZE` · `REPAIR TABLE` | | **PostgreSQL RLS** | `CREATE POLICY` · `ALTER TABLE … ENABLE`/`DISABLE`/`FORCE`/`NO FORCE ROW LEVEL SECURITY` | +| **PostgreSQL administration** | `CREATE`/`ALTER EVENT TRIGGER` · `REINDEX` · `SECURITY LABEL` | | **Informix constraints** | `ALTER TABLE … ADD CONSTRAINT` with trailing constraint names for primary, unique, foreign and check constraints; enable with `parser.withDialect(Dialect.INFORMIX)` | | **Salesforce SOQL** | `INCLUDES` · `EXCLUDES` | 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 0d3d080fb..1439ac97a 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -29,6 +29,7 @@ import net.sf.jsqlparser.statement.TableMaintenanceStatement; import net.sf.jsqlparser.statement.UseStatement; import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.alter.AlterEventTrigger; import net.sf.jsqlparser.statement.alter.sequence.AlterSequence; import net.sf.jsqlparser.statement.analyze.Analyze; import net.sf.jsqlparser.statement.comment.Comment; @@ -40,6 +41,7 @@ import net.sf.jsqlparser.statement.create.sequence.CreateSequence; import net.sf.jsqlparser.statement.create.synonym.CreateSynonym; import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.trigger.CreateEventTrigger; import net.sf.jsqlparser.statement.create.view.AlterView; import net.sf.jsqlparser.statement.create.view.CreateView; import net.sf.jsqlparser.statement.delete.Delete; @@ -48,6 +50,7 @@ import net.sf.jsqlparser.statement.grant.Grant; import net.sf.jsqlparser.statement.merge.Merge; import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement; +import net.sf.jsqlparser.statement.reindex.Reindex; import net.sf.jsqlparser.statement.select.Fetch; import net.sf.jsqlparser.statement.select.First; import net.sf.jsqlparser.statement.select.KSQLWindow; @@ -60,6 +63,7 @@ import net.sf.jsqlparser.statement.select.TableFunction; import net.sf.jsqlparser.statement.select.Top; import net.sf.jsqlparser.statement.select.UnPivot; +import net.sf.jsqlparser.statement.securitylabel.SecurityLabel; import net.sf.jsqlparser.statement.show.*; import net.sf.jsqlparser.statement.truncate.Truncate; import net.sf.jsqlparser.statement.update.Update; @@ -607,6 +611,26 @@ public enum Feature { * SQL "CREATE TRIGGER" statement is allowed */ createTrigger, + /** + * PostgreSQL "CREATE EVENT TRIGGER" and "ALTER EVENT TRIGGER" statements are allowed + * + * @see CreateEventTrigger + * @see AlterEventTrigger + */ + createEventTrigger, alterEventTrigger, + /** + * PostgreSQL "REINDEX" statement is allowed; "REINDEX … CONCURRENTLY" additionally requires + * {@link #reindexConcurrently} + * + * @see Reindex + */ + reindex, reindexConcurrently, + /** + * PostgreSQL "SECURITY LABEL" statement is allowed + * + * @see SecurityLabel + */ + securityLabel, /** * SQL "COMMIT" statement is allowed * diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java index 91b272124..ca3f100a9 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java @@ -36,6 +36,7 @@ import net.sf.jsqlparser.statement.alter.AlterSession; import net.sf.jsqlparser.statement.alter.AlterStatistics; import net.sf.jsqlparser.statement.alter.AlterSubscription; +import net.sf.jsqlparser.statement.alter.AlterEventTrigger; import net.sf.jsqlparser.statement.alter.AlterSystemStatement; import net.sf.jsqlparser.statement.alter.AlterTablespaceMove; import net.sf.jsqlparser.statement.alter.AlterTextSearchConfiguration; @@ -68,6 +69,7 @@ import net.sf.jsqlparser.statement.create.synonym.CreateSynonym; import net.sf.jsqlparser.statement.create.table.CreateTable; import net.sf.jsqlparser.statement.create.textsearch.CreateTextSearchConfiguration; +import net.sf.jsqlparser.statement.create.trigger.CreateEventTrigger; import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; import net.sf.jsqlparser.statement.create.type.CreateType; import net.sf.jsqlparser.statement.create.user.CreateUser; @@ -93,6 +95,8 @@ import net.sf.jsqlparser.statement.oracle.OracleBlock; import net.sf.jsqlparser.statement.oracle.OracleNullStatement; import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement; +import net.sf.jsqlparser.statement.reindex.Reindex; +import net.sf.jsqlparser.statement.securitylabel.SecurityLabel; import net.sf.jsqlparser.statement.role.AlterRole; import net.sf.jsqlparser.statement.role.CreateRole; import net.sf.jsqlparser.statement.select.FromItemVisitor; @@ -1065,6 +1069,29 @@ public Void visit(CreateTrigger statement, S context) { return schemaOnly(); } + @Override + public Void visit(CreateEventTrigger statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterEventTrigger statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(Reindex statement, S context) { + analysis.claimTopLevel(); + // rebuilds index storage from the indexed rows + analysis.certain(StmtFeature.MODIFIES_SCHEMA, StmtFeature.READS_DATA); + return null; + } + + @Override + public Void visit(SecurityLabel statement, S context) { + return schemaOnly(); + } + @Override public Void visit(CreateType statement, S context) { analysis.claimTopLevel(); diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java index bf0bf3e8b..e1592dbbd 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -52,6 +52,7 @@ import net.sf.jsqlparser.statement.alter.Alter; import net.sf.jsqlparser.statement.alter.AlterSession; +import net.sf.jsqlparser.statement.alter.AlterEventTrigger; import net.sf.jsqlparser.statement.alter.AlterSystemStatement; import net.sf.jsqlparser.statement.alter.RenameTableStatement; import net.sf.jsqlparser.statement.alter.sequence.AlterSequence; @@ -66,6 +67,7 @@ import net.sf.jsqlparser.statement.create.sequence.CreateSequence; import net.sf.jsqlparser.statement.create.synonym.CreateSynonym; import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.trigger.CreateEventTrigger; import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; import net.sf.jsqlparser.statement.create.user.CreateUser; import net.sf.jsqlparser.statement.create.view.AlterView; @@ -83,6 +85,8 @@ import net.sf.jsqlparser.statement.lock.LockStatement; import net.sf.jsqlparser.statement.merge.Merge; import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement; +import net.sf.jsqlparser.statement.reindex.Reindex; +import net.sf.jsqlparser.statement.securitylabel.SecurityLabel; import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.statement.show.ShowIndexStatement; import net.sf.jsqlparser.statement.show.ShowTablesStatement; @@ -223,6 +227,38 @@ default void visit(CreateTrigger createTrigger) { this.visit(createTrigger, null); } + default T visit(CreateEventTrigger statement, S context) { + return null; + } + + default void visit(CreateEventTrigger statement) { + this.visit(statement, null); + } + + default T visit(AlterEventTrigger statement, S context) { + return null; + } + + default void visit(AlterEventTrigger statement) { + this.visit(statement, null); + } + + default T visit(Reindex statement, S context) { + return null; + } + + default void visit(Reindex statement) { + this.visit(statement, null); + } + + default T visit(SecurityLabel statement, S context) { + return null; + } + + default void visit(SecurityLabel statement) { + this.visit(statement, null); + } + T visit(CreateView createView, S context); default void visit(CreateView createView) { diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java index 8a80763ca..4a4c86ba0 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java @@ -54,6 +54,7 @@ import net.sf.jsqlparser.statement.alter.Alter; import net.sf.jsqlparser.statement.alter.AlterExpression; import net.sf.jsqlparser.statement.alter.AlterSession; +import net.sf.jsqlparser.statement.alter.AlterEventTrigger; import net.sf.jsqlparser.statement.alter.AlterSystemStatement; import net.sf.jsqlparser.statement.alter.RenameTableStatement; import net.sf.jsqlparser.statement.alter.sequence.AlterSequence; @@ -68,6 +69,7 @@ import net.sf.jsqlparser.statement.create.sequence.CreateSequence; import net.sf.jsqlparser.statement.create.synonym.CreateSynonym; import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.trigger.CreateEventTrigger; import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; import net.sf.jsqlparser.statement.create.user.CreateUser; import net.sf.jsqlparser.statement.create.view.AlterView; @@ -88,6 +90,8 @@ import net.sf.jsqlparser.statement.merge.MergeOperationVisitor; import net.sf.jsqlparser.statement.merge.MergeOperationVisitorAdapter; import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement; +import net.sf.jsqlparser.statement.reindex.Reindex; +import net.sf.jsqlparser.statement.securitylabel.SecurityLabel; import net.sf.jsqlparser.statement.select.FromItemVisitor; import net.sf.jsqlparser.statement.select.FromItemVisitorAdapter; import net.sf.jsqlparser.statement.select.PivotVisitor; @@ -383,6 +387,7 @@ public T visit(CreateIndex createIndex, S context) { @Override public T visit(CreateSchema createSchema, S context) { + createSchema.getStatements().forEach(element -> element.accept(this, context)); return null; } @@ -422,6 +427,32 @@ public T visit(CreateTrigger createTrigger, S context) { return null; } + @Override + public T visit(CreateEventTrigger statement, S context) { + statement.visitExpressions(expression -> expression.accept(expressionVisitor, context)); + return null; + } + + @Override + public T visit(AlterEventTrigger statement, S context) { + return null; + } + + @Override + public T visit(Reindex statement, S context) { + if (statement.getTable() != null) { + statement.getTable().accept(fromItemVisitor, context); + } + return null; + } + + @Override + public T visit(SecurityLabel statement, S context) { + statement.visitRelations(table -> table.accept(fromItemVisitor, context)); + expressionVisitor.visitExpression(statement.getLabel(), context); + return null; + } + @Override public T visit(CreateView createView, S context) { createView.getView().accept(fromItemVisitor, context); diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterEventTrigger.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterEventTrigger.java new file mode 100644 index 000000000..0efb70b9b --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterEventTrigger.java @@ -0,0 +1,131 @@ +/*- + * #%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 net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** + * A PostgreSQL {@code ALTER EVENT TRIGGER} statement. The enable states are shared with the trigger + * and rule actions of {@link RelationAlterAction}. Each non-null setter selects its action and + * discards the values of the other actions. + */ +public class AlterEventTrigger implements Statement { + public enum Action { + STATE, OWNER, RENAME + } + + private String name; + private Action action; + private RelationAlterAction.EnableState enableState; + private String owner; + private String newName; + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Action getAction() { + return action; + } + + /** {@code ENABLE}, {@code ENABLE REPLICA}, {@code ENABLE ALWAYS} or {@code DISABLE}. */ + public RelationAlterAction.EnableState getEnableState() { + return enableState; + } + + public void setEnableState(RelationAlterAction.EnableState enableState) { + if (enableState != null) { + select(Action.STATE); + } + this.enableState = enableState; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + if (owner != null) { + select(Action.OWNER); + } + this.owner = owner; + } + + public String getNewName() { + return newName; + } + + public void setNewName(String newName) { + if (newName != null) { + select(Action.RENAME); + } + this.newName = newName; + } + + private void select(Action action) { + this.action = action; + enableState = null; + owner = null; + newName = null; + } + + public StringBuilder appendTo(StringBuilder sql) { + sql.append("ALTER EVENT TRIGGER ").append(name); + switch (action) { + case STATE: + sql.append(' ').append(enableState.name().replace('_', ' ')); + break; + case OWNER: + sql.append(" OWNER TO ").append(owner); + break; + case RENAME: + sql.append(" RENAME TO ").append(newName); + break; + default: + throw new IllegalStateException("Unknown event trigger action: " + action); + } + return sql; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + public AlterEventTrigger withName(String name) { + setName(name); + return this; + } + + public AlterEventTrigger withEnableState(RelationAlterAction.EnableState enableState) { + setEnableState(enableState); + return this; + } + + public AlterEventTrigger withOwner(String owner) { + setOwner(owner); + return this; + } + + public AlterEventTrigger withNewName(String newName) { + setNewName(newName); + return this; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/schema/CreateSchema.java b/src/main/java/net/sf/jsqlparser/statement/create/schema/CreateSchema.java index f8888132f..6e8d2da99 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/schema/CreateSchema.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/schema/CreateSchema.java @@ -14,6 +14,7 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.function.Consumer; import java.util.stream.Collectors; import net.sf.jsqlparser.statement.Statement; @@ -36,7 +37,7 @@ public T accept(StatementVisitor statementVisitor, S context) { } /** - * Add a statement to the schema definition + * Add a schema element, for example a CREATE TABLE or GRANT statement, to the schema definition * * @param statement The statement to be added * @return true if the operation was successful @@ -109,7 +110,8 @@ public void setSchemaPath(List schemaPath) { } /** - * The statements executed as part of the schema creation + * The schema elements created as part of the schema, in source order. Their names are kept as + * written; unqualified names refer to the new schema. * * @return the statements */ @@ -139,7 +141,32 @@ public CreateSchema setIfNotExists(boolean hasIfNotExists) { return this; } + @Override public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } + + /** + * Appends the schema definition, rendering each schema element with the given writer. + * + * @param builder the target + * @param elementWriter renders one schema element + * @return the target + */ + public StringBuilder appendTo(StringBuilder builder, Consumer elementWriter) { + builder.append(headerSql()); + if (schemaPath != null && !schemaPath.isEmpty()) { + builder.append(" PATH ").append(String.join(", ", schemaPath)); + } + for (Statement statement : statements) { + builder.append(' '); + elementWriter.accept(statement); + } + return builder; + } + + private String headerSql() { String sql = "CREATE SCHEMA"; if (hasIfNotExists) { sql += " IF NOT EXISTS"; diff --git a/src/main/java/net/sf/jsqlparser/statement/create/trigger/CreateEventTrigger.java b/src/main/java/net/sf/jsqlparser/statement/create/trigger/CreateEventTrigger.java new file mode 100644 index 000000000..daa1ace7c --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/trigger/CreateEventTrigger.java @@ -0,0 +1,196 @@ +/*- + * #%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.create.trigger; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** + * A PostgreSQL {@code CREATE EVENT TRIGGER} statement. Unlike {@link CreateTrigger}, it fires on a + * database event such as {@code ddl_command_start} rather than on table rows. The function is + * invoked like a {@link CreateTrigger} routine, with the same {@code FUNCTION} or {@code PROCEDURE} + * spelling, but never takes arguments. + */ +public class CreateEventTrigger implements Statement { + + private String name; + private String event; + private final List filters = new ArrayList<>(); + private CreateTrigger.ExecuteKeyword executeKeyword = CreateTrigger.ExecuteKeyword.FUNCTION; + private Function function; + + /** A {@code WHEN} condition, for example {@code TAG IN ('CREATE TABLE', 'DROP TABLE')}. */ + public static class Filter implements Serializable { + private String variable; + private final List values = new ArrayList<>(); + + public Filter() { + // empty constructor + } + + public Filter(String variable, Collection values) { + this.variable = variable; + setValues(values); + } + + /** The filter variable as written, for example {@code TAG}. */ + public String getVariable() { + return variable; + } + + public void setVariable(String variable) { + this.variable = variable; + } + + /** The accepted values in source order. */ + public List getValues() { + return Collections.unmodifiableList(values); + } + + public void setValues(Collection values) { + this.values.clear(); + if (values != null) { + this.values.addAll(values); + } + } + + public Filter addValues(StringValue... values) { + Collections.addAll(this.values, values); + return this; + } + + public StringBuilder appendTo(StringBuilder sql, Consumer expressionWriter) { + sql.append(variable).append(" IN ("); + for (int i = 0; i < values.size(); i++) { + if (i > 0) { + sql.append(", "); + } + expressionWriter.accept(values.get(i)); + } + return sql.append(')'); + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + return appendTo(sql, sql::append).toString(); + } + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + /** The event name as written, for example {@code ddl_command_start} or {@code sql_drop}. */ + public String getEvent() { + return event; + } + + public void setEvent(String event) { + this.event = event; + } + + /** The {@code WHEN} conditions in source order; all of them must match. */ + public List getFilters() { + return Collections.unmodifiableList(filters); + } + + public void setFilters(Collection filters) { + this.filters.clear(); + if (filters != null) { + this.filters.addAll(filters); + } + } + + public CreateEventTrigger addFilters(Filter... filters) { + Collections.addAll(this.filters, filters); + return this; + } + + public CreateTrigger.ExecuteKeyword getExecuteKeyword() { + return executeKeyword; + } + + public void setExecuteKeyword(CreateTrigger.ExecuteKeyword executeKeyword) { + this.executeKeyword = executeKeyword; + } + + /** The invoked function, rendered with its empty argument list. */ + public Function getFunction() { + return function; + } + + public void setFunction(Function function) { + this.function = function; + } + + /** Visits the filter values and the invoked function. */ + public void visitExpressions(Consumer visitor) { + filters.forEach(filter -> filter.getValues().forEach(visitor)); + if (function != null) { + visitor.accept(function); + } + } + + public StringBuilder appendTo(StringBuilder sql, Consumer expressionWriter) { + sql.append("CREATE EVENT TRIGGER ").append(name).append(" ON ").append(event); + for (int i = 0; i < filters.size(); i++) { + sql.append(i == 0 ? " WHEN " : " AND "); + filters.get(i).appendTo(sql, expressionWriter); + } + sql.append(" EXECUTE ").append(executeKeyword).append(' '); + expressionWriter.accept(function); + return sql; + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + return appendTo(sql, sql::append).toString(); + } + + public CreateEventTrigger withName(String name) { + setName(name); + return this; + } + + public CreateEventTrigger withEvent(String event) { + setEvent(event); + return this; + } + + public CreateEventTrigger withExecuteKeyword(CreateTrigger.ExecuteKeyword executeKeyword) { + setExecuteKeyword(executeKeyword); + return this; + } + + public CreateEventTrigger withFunction(Function function) { + setFunction(function); + return this; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/reindex/Reindex.java b/src/main/java/net/sf/jsqlparser/statement/reindex/Reindex.java new file mode 100644 index 000000000..5402be1d8 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/reindex/Reindex.java @@ -0,0 +1,182 @@ +/*- + * #%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.reindex; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** A PostgreSQL {@code REINDEX} statement. */ +public class Reindex implements Statement { + + public enum Target { + INDEX, TABLE, SCHEMA, DATABASE, SYSTEM + } + + private final List