diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java index e1a0d055e..ca8a3777f 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -9,6 +9,8 @@ */ package net.sf.jsqlparser.statement; +import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.alter.AlterTablespaceMove; import net.sf.jsqlparser.statement.alter.database.AlterDatabase; import net.sf.jsqlparser.statement.alter.schema.AlterSchema; import net.sf.jsqlparser.statement.oracle.OracleBlock; @@ -672,6 +674,14 @@ default void visit(OracleNullStatement statement) { visit(statement, null); } + default T visit(AlterRelation statement, S context) { + return null; + } + + default void visit(AlterRelation statement) { + visit(statement, null); + } + default T visit(AlterDatabase statement, S context) { return null; } @@ -680,4 +690,11 @@ default void visit(AlterDatabase statement) { visit(statement, null); } + default T visit(AlterTablespaceMove statement, S context) { + return null; + } + + default void visit(AlterTablespaceMove statement) { + visit(statement, null); + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java index bca23bbe4..2130d4b40 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java @@ -12,7 +12,7 @@ import java.util.Locale; public enum AlterOperation { - ADD, ALTER, DROP, DROP_PRIMARY_KEY, DROP_UNIQUE, DROP_FOREIGN_KEY, DROP_CHECK, MODIFY, CHANGE, CONVERT, COLLATE, ALGORITHM, RENAME, RENAME_TABLE, RENAME_INDEX, RENAME_KEY, RENAME_CONSTRAINT, COMMENT, COMMENT_WITH_EQUAL_SIGN, UNSPECIFIC, ADD_PARTITION, DROP_PARTITION, ATTACH_PARTITION, DETACH_PARTITION, DISCARD_PARTITION, IMPORT_PARTITION, TRUNCATE_PARTITION, COALESCE_PARTITION, REORGANIZE_PARTITION, EXCHANGE_PARTITION, ANALYZE_PARTITION, CHECK_PARTITION, OPTIMIZE_PARTITION, REBUILD_PARTITION, REPAIR_PARTITION, REMOVE_PARTITIONING, PARTITION_BY, SET_TABLE_OPTION, ENGINE, FORCE, KEY_BLOCK_SIZE, LOCK, DISCARD_TABLESPACE, IMPORT_TABLESPACE, DISABLE_KEYS, ENABLE_KEYS, ENABLE_ROW_LEVEL_SECURITY, DISABLE_ROW_LEVEL_SECURITY, FORCE_ROW_LEVEL_SECURITY, NO_FORCE_ROW_LEVEL_SECURITY, ALTER_PRIMARY_KEY; + ADD, ALTER, DROP, DROP_PRIMARY_KEY, DROP_UNIQUE, DROP_FOREIGN_KEY, DROP_CHECK, MODIFY, CHANGE, CONVERT, COLLATE, ALGORITHM, RENAME, RENAME_TABLE, RENAME_INDEX, RENAME_KEY, RENAME_CONSTRAINT, COMMENT, COMMENT_WITH_EQUAL_SIGN, UNSPECIFIC, ADD_PARTITION, DROP_PARTITION, ATTACH_PARTITION, DETACH_PARTITION, DISCARD_PARTITION, IMPORT_PARTITION, TRUNCATE_PARTITION, COALESCE_PARTITION, REORGANIZE_PARTITION, EXCHANGE_PARTITION, ANALYZE_PARTITION, CHECK_PARTITION, OPTIMIZE_PARTITION, REBUILD_PARTITION, REPAIR_PARTITION, REMOVE_PARTITIONING, PARTITION_BY, SET_TABLE_OPTION, ENGINE, FORCE, KEY_BLOCK_SIZE, LOCK, DISCARD_TABLESPACE, IMPORT_TABLESPACE, DISABLE_KEYS, ENABLE_KEYS, ENABLE_ROW_LEVEL_SECURITY, DISABLE_ROW_LEVEL_SECURITY, FORCE_ROW_LEVEL_SECURITY, NO_FORCE_ROW_LEVEL_SECURITY, ALTER_PRIMARY_KEY, ALTER_RELATION; public static AlterOperation from(String operation) { return Enum.valueOf(AlterOperation.class, operation.toUpperCase(Locale.ROOT)); diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterRelation.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterRelation.java new file mode 100644 index 000000000..0c09943e1 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterRelation.java @@ -0,0 +1,84 @@ +/*- + * #%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 java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** PostgreSQL ALTER INDEX, ALTER VIEW properties and ALTER MATERIALIZED VIEW. */ +public class AlterRelation implements Statement { + public enum ObjectType { + INDEX, VIEW, MATERIALIZED_VIEW + } + + private ObjectType objectType; + private Table relation; + private boolean ifExists; + private final List actions = new ArrayList<>(); + + public ObjectType getObjectType() { + return objectType; + } + + public void setObjectType(ObjectType objectType) { + this.objectType = objectType; + } + + public Table getRelation() { + return relation; + } + + public void setRelation(Table relation) { + this.relation = relation; + } + + public boolean isIfExists() { + return ifExists; + } + + public void setIfExists(boolean ifExists) { + this.ifExists = ifExists; + } + + public List getActions() { + return actions; + } + + public StringBuilder appendTo(StringBuilder builder, Consumer expressionPrinter) { + builder.append("ALTER ").append(objectType.name().replace('_', ' ')).append(' '); + if (ifExists) { + builder.append("IF EXISTS "); + } + builder.append(relation).append(' '); + for (int i = 0; i < actions.size(); i++) { + if (i > 0) { + builder.append(", "); + } + actions.get(i).appendTo(builder, expressionPrinter); + } + return builder; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterTablespaceMove.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterTablespaceMove.java new file mode 100644 index 000000000..789315a31 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterTablespaceMove.java @@ -0,0 +1,92 @@ +/*- + * #%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 java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.select.PlainSelect; + +/** Moves all matching relations from one PostgreSQL tablespace to another. */ +public class AlterTablespaceMove implements Statement { + public enum ObjectType { + TABLE, INDEX, MATERIALIZED_VIEW + } + + private ObjectType objectType; + private String sourceTablespace; + private String targetTablespace; + private List owners = new ArrayList<>(); + private boolean noWait; + + public ObjectType getObjectType() { + return objectType; + } + + public void setObjectType(ObjectType objectType) { + this.objectType = objectType; + } + + public String getSourceTablespace() { + return sourceTablespace; + } + + public void setSourceTablespace(String sourceTablespace) { + this.sourceTablespace = sourceTablespace; + } + + public String getTargetTablespace() { + return targetTablespace; + } + + public void setTargetTablespace(String targetTablespace) { + this.targetTablespace = targetTablespace; + } + + public List getOwners() { + return owners; + } + + public void setOwners(List owners) { + this.owners = owners; + } + + public boolean isNoWait() { + return noWait; + } + + public void setNoWait(boolean noWait) { + this.noWait = noWait; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("ALTER ").append(objectType.name().replace('_', ' ')) + .append(" ALL IN TABLESPACE ").append(sourceTablespace); + if (owners != null && !owners.isEmpty()) { + builder.append(" OWNED BY ").append(PlainSelect.getStringList(owners)); + } + builder.append(" SET TABLESPACE ").append(targetTablespace); + if (noWait) { + builder.append(" NOWAIT"); + } + 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/statement/alter/RelationAlterAction.java b/src/main/java/net/sf/jsqlparser/statement/alter/RelationAlterAction.java new file mode 100644 index 000000000..a8584c828 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/RelationAlterAction.java @@ -0,0 +1,340 @@ +/*- + * #%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 java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.select.PlainSelect; + +/** Structured PostgreSQL property actions shared by tables, indexes and views. */ +public class RelationAlterAction extends AlterExpression { + private TriggerState triggerState; + private TriggerTarget triggerTarget; + private Kind kind; + private ColumnAction columnAction; + private ReplicaIdentity replicaIdentity; + private String newName; + private String value; + private Integer columnNumber; + private Long statistics; + private boolean statisticsDefault; + private Expression defaultExpression; + private Table relation; + private boolean noInherit; + private boolean noDependency; + private List options; + private List resetOptions; + + public enum Kind { + RENAME, RENAME_COLUMN, OWNER, SET_SCHEMA, SET_TABLESPACE, SET_ACCESS_METHOD, SET_OPTIONS, RESET_OPTIONS, ALTER_COLUMN, ATTACH_PARTITION, DEPENDS_ON_EXTENSION, VALIDATE_CONSTRAINT, INHERIT, ALTER_CONSTRAINT_INHERIT, REPLICA_IDENTITY, CLUSTER_ON, SET_WITHOUT_CLUSTER, SET_WITHOUT_OIDS, SET_LOGGED, SET_UNLOGGED, OF, NOT_OF, TRIGGER_STATE + } + + public enum ColumnAction { + SET_DEFAULT, DROP_DEFAULT, SET_STATISTICS, SET_STORAGE, SET_COMPRESSION, DROP_EXPRESSION, SET_OPTIONS, RESET_OPTIONS + } + + public enum ReplicaIdentity { + DEFAULT, FULL, NOTHING, USING_INDEX + } + + public enum TriggerState { + ENABLE, DISABLE, ENABLE_ALWAYS, ENABLE_REPLICA + } + public enum TriggerTarget { + NAME, ALL, USER + } + + + public TriggerState getTriggerState() { + return triggerState; + } + + public void setTriggerState(TriggerState triggerState) { + this.triggerState = triggerState; + } + + public TriggerTarget getTriggerTarget() { + return triggerTarget; + } + + public void setTriggerTarget(TriggerTarget triggerTarget) { + this.triggerTarget = triggerTarget; + } + + + public RelationAlterAction() { + setOperation(AlterOperation.ALTER_RELATION); + } + + public Kind getKind() { + return kind; + } + + public void setKind(Kind kind) { + this.kind = kind; + } + + public ColumnAction getColumnAction() { + return columnAction; + } + + public void setColumnAction(ColumnAction columnAction) { + this.columnAction = columnAction; + } + + public ReplicaIdentity getReplicaIdentity() { + return replicaIdentity; + } + + public void setReplicaIdentity(ReplicaIdentity replicaIdentity) { + this.replicaIdentity = replicaIdentity; + } + + public String getNewName() { + return newName; + } + + public void setNewName(String newName) { + this.newName = newName; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public Integer getColumnNumber() { + return columnNumber; + } + + public void setColumnNumber(Integer columnNumber) { + this.columnNumber = columnNumber; + } + + public Long getStatistics() { + return statistics; + } + + public void setStatistics(Long statistics) { + this.statistics = statistics; + statisticsDefault = false; + } + + public boolean isStatisticsDefault() { + return statisticsDefault; + } + + public void setStatisticsDefault(boolean statisticsDefault) { + this.statisticsDefault = statisticsDefault; + if (statisticsDefault) { + statistics = null; + } + } + + public Expression getDefaultExpression() { + return defaultExpression; + } + + public void setDefaultExpression(Expression defaultExpression) { + this.defaultExpression = defaultExpression; + } + + public Table getRelation() { + return relation; + } + + public void setRelation(Table relation) { + this.relation = relation; + } + + public boolean isNoInherit() { + return noInherit; + } + + public void setNoInherit(boolean noInherit) { + this.noInherit = noInherit; + } + + public boolean isNoDependency() { + return noDependency; + } + + public void setNoDependency(boolean noDependency) { + this.noDependency = noDependency; + } + + public List getOptions() { + return options; + } + + public void setOptions(List options) { + this.options = options; + } + + public List getResetOptions() { + return resetOptions; + } + + public void setResetOptions(List resetOptions) { + this.resetOptions = resetOptions; + } + + @Override + protected void appendBody(StringBuilder builder) { + appendDefinition(builder, builder::append); + } + + public StringBuilder appendTo(StringBuilder builder, Consumer expressionPrinter) { + appendDefinition(builder, expressionPrinter); + appendCommonTail(builder); + return builder; + } + + private void appendDefinition(StringBuilder builder, Consumer expressionPrinter) { + switch (kind) { + case CLUSTER_ON: + builder.append("CLUSTER ON ").append(value); + break; + case SET_WITHOUT_CLUSTER: + case SET_WITHOUT_OIDS: + case SET_LOGGED: + case SET_UNLOGGED: + case NOT_OF: + builder.append(kind.name().replace('_', ' ')); + break; + case OF: + builder.append("OF ").append(value); + break; + case TRIGGER_STATE: + builder.append(triggerState.name().replace('_', ' ')).append(" TRIGGER ") + .append(triggerTarget == TriggerTarget.NAME ? value : triggerTarget); + break; + case RENAME: + builder.append("RENAME TO ").append(newName); + break; + case RENAME_COLUMN: + builder.append(hasColumn() ? "RENAME COLUMN " : "RENAME ") + .append(getColumnName()).append(" TO ").append(newName); + break; + case OWNER: + builder.append("OWNER TO ").append(value); + break; + case SET_SCHEMA: + builder.append("SET SCHEMA ").append(value); + break; + case SET_TABLESPACE: + builder.append("SET TABLESPACE ").append(value); + break; + case SET_ACCESS_METHOD: + builder.append("SET ACCESS METHOD ").append(value); + break; + case SET_OPTIONS: + builder.append("SET "); + Index.Option.appendListTo(builder, options, expressionPrinter); + break; + case RESET_OPTIONS: + builder.append("RESET ") + .append(PlainSelect.getStringList(resetOptions, true, true)); + break; + case ALTER_COLUMN: + appendColumnChange(builder, expressionPrinter); + break; + case ATTACH_PARTITION: + builder.append("ATTACH PARTITION ").append(relation); + break; + case DEPENDS_ON_EXTENSION: + builder.append(noDependency ? "NO DEPENDS ON EXTENSION " : "DEPENDS ON EXTENSION ") + .append(value); + break; + case VALIDATE_CONSTRAINT: + builder.append("VALIDATE CONSTRAINT ").append(getConstraintName()); + break; + case INHERIT: + builder.append(noInherit ? "NO INHERIT " : "INHERIT ").append(relation); + break; + case ALTER_CONSTRAINT_INHERIT: + builder.append("ALTER CONSTRAINT ").append(getConstraintName()) + .append(noInherit ? " NO INHERIT" : " INHERIT"); + break; + case REPLICA_IDENTITY: + builder.append("REPLICA IDENTITY "); + if (replicaIdentity == ReplicaIdentity.USING_INDEX) { + builder.append("USING INDEX ").append(value); + } else { + builder.append(replicaIdentity); + } + break; + default: + throw new IllegalStateException("Unsupported relation alteration: " + kind); + } + } + + private void appendColumnChange(StringBuilder builder, Consumer expressionPrinter) { + builder.append(hasColumn() ? "ALTER COLUMN " : "ALTER ") + .append(columnNumber != null ? columnNumber : getColumnName()); + switch (columnAction) { + case SET_DEFAULT: + builder.append(" SET DEFAULT "); + expressionPrinter.accept(defaultExpression); + break; + case DROP_DEFAULT: + builder.append(" DROP DEFAULT"); + break; + case SET_STATISTICS: + builder.append(" SET STATISTICS ") + .append(statisticsDefault ? "DEFAULT" : statistics); + break; + case SET_OPTIONS: + builder.append(" SET "); + Index.Option.appendListTo(builder, options, expressionPrinter); + break; + case RESET_OPTIONS: + builder.append(" RESET ") + .append(PlainSelect.getStringList(resetOptions, true, true)); + break; + case SET_STORAGE: + builder.append(" SET STORAGE ").append(value); + break; + case SET_COMPRESSION: + builder.append(" SET COMPRESSION ").append(value); + break; + case DROP_EXPRESSION: + builder.append(" DROP EXPRESSION"); + if (isUsingIfExists()) { + builder.append(" IF EXISTS"); + } + break; + default: + throw new IllegalStateException("Unsupported column alteration: " + columnAction); + } + } + + public void visitExpressions(Consumer visitor) { + if (kind == Kind.ALTER_COLUMN && columnAction == ColumnAction.SET_DEFAULT) { + visitor.accept(defaultExpression); + } else if ((kind == Kind.SET_OPTIONS || kind == Kind.ALTER_COLUMN + && columnAction == ColumnAction.SET_OPTIONS) && options != null) { + options.stream().map(Index.Option::getValue).filter(java.util.Objects::nonNull) + .forEach(visitor); + } + } + + public void visitTables(Consumer visitor) { + if (kind == Kind.INHERIT && relation != null) { + visitor.accept(relation); + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/TableOption.java b/src/main/java/net/sf/jsqlparser/statement/create/table/TableOption.java index 1f2104b9f..fc0e1f1db 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/TableOption.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/TableOption.java @@ -21,8 +21,7 @@ public class TableOption implements Serializable { public enum Kind { - ENGINE, CHARACTER_SET, COLLATE, COMMENT, AUTO_INCREMENT, STATS_AUTO_RECALC, STATS_PERSISTENT, STATS_SAMPLE_PAGES, UNION, ENCRYPTION, PASSWORD, DATA_DIRECTORY, INDEX_DIRECTORY, - STORAGE_PARAMETERS, WITHOUT_OIDS, ENGINE_ATTRIBUTE, SECONDARY_ENGINE_ATTRIBUTE, OTHER + ENGINE, CHARACTER_SET, COLLATE, COMMENT, AUTO_INCREMENT, STATS_AUTO_RECALC, STATS_PERSISTENT, STATS_SAMPLE_PAGES, UNION, ENCRYPTION, PASSWORD, DATA_DIRECTORY, INDEX_DIRECTORY, STORAGE_PARAMETERS, WITHOUT_OIDS, ENGINE_ATTRIBUTE, SECONDARY_ENGINE_ATTRIBUTE, OTHER } private Kind kind = Kind.OTHER; diff --git a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java index d3fc323c1..cd71cb5f0 100644 --- a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java +++ b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java @@ -49,6 +49,12 @@ public static void visit(CreateIndex createIndex, Consumer expressio /** Visits the structured definitions and expressions belonging to a single ALTER action. */ public static void visit(AlterExpression action, Consumer expressions, Consumer
tables) { + if (action instanceof net.sf.jsqlparser.statement.alter.RelationAlterAction) { + net.sf.jsqlparser.statement.alter.RelationAlterAction relation = + (net.sf.jsqlparser.statement.alter.RelationAlterAction) action; + relation.visitExpressions(expressions); + relation.visitTables(tables); + } if (action.getOperation() == AlterOperation.RENAME_TABLE) { accept(action.getNewTable(), tables); } diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index a223ce783..aead296b0 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -9,6 +9,8 @@ */ package net.sf.jsqlparser.util; +import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.alter.AlterTablespaceMove; import net.sf.jsqlparser.statement.alter.database.AlterDatabase; import net.sf.jsqlparser.statement.alter.schema.AlterSchema; import net.sf.jsqlparser.statement.select.MatchRecognize; @@ -2916,9 +2918,25 @@ public Void visit(OracleNullStatement statement, S context) { return null; } + @Override + public Void visit(AlterRelation statement, S context) { + if (statement.getObjectType() != AlterRelation.ObjectType.INDEX) { + visit(statement.getRelation(), context); + } + statement.getActions().forEach(action -> { + action.visitExpressions(expression -> expression.accept(this, context)); + action.visitTables(table -> visit(table, context)); + }); + return null; + } + @Override public Void visit(AlterDatabase statement, S context) { return null; } + @Override + public Void visit(AlterTablespaceMove statement, S context) { + return null; + } } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java index e162f0ec8..f3cc37c2c 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java @@ -14,6 +14,7 @@ import net.sf.jsqlparser.statement.alter.AlterExpression; import net.sf.jsqlparser.statement.alter.AlterExpressionPartition; import net.sf.jsqlparser.statement.alter.AlterExpressionPrimaryKey; +import net.sf.jsqlparser.statement.alter.RelationAlterAction; import net.sf.jsqlparser.statement.alter.AlterOperation; import net.sf.jsqlparser.statement.select.PlainSelect; import java.util.Iterator; @@ -50,6 +51,11 @@ public void deParse(Alter alter) { } private void deParseAction(AlterExpression action) { + if (action instanceof RelationAlterAction) { + ((RelationAlterAction) action).appendTo(builder, + expression -> expression.accept(expressionVisitor, null)); + return; + } if (action instanceof AlterExpressionPartition) { ((AlterExpressionPartition) action).appendTo(builder, expression -> expression.accept(expressionVisitor, 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 83a40d6fd..f50ecd351 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,8 @@ */ package net.sf.jsqlparser.util.deparser; +import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.alter.AlterTablespaceMove; import net.sf.jsqlparser.statement.alter.database.AlterDatabase; import net.sf.jsqlparser.statement.alter.schema.AlterSchema; import net.sf.jsqlparser.statement.oracle.OracleBlock; @@ -810,9 +812,19 @@ public StringBuilder visit(OracleNullStatement statement, S context) { return builder.append("NULL"); } + @Override + public StringBuilder visit(AlterRelation statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + @Override public StringBuilder visit(AlterDatabase statement, S context) { return statement.appendTo(builder); } + @Override + public StringBuilder visit(AlterTablespaceMove statement, S context) { + return statement.appendTo(builder); + } } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 2c2608ae8..0c921870b 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1813,6 +1813,75 @@ public class CCJSqlParser extends AbstractJSqlParser { } } + private boolean isPostgreSqlRelationActionAhead() { + if (!Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) { return false; } + Token first = getToken(1); + if (first.kind == K_VALIDATE || first.kind == K_RESET || isKeywordAhead("INHERIT") + || isKeywordAhead("REPLICA") || isKeywordAhead("OWNER") || isKeywordAhead("CLUSTER") + || first.kind == K_OF || first.kind == K_NOT && getToken(2).kind == K_OF + || (first.kind == K_ENABLE || first.kind == K_DISABLE) + && (getToken(2).kind == K_TRIGGER || getToken(2).kind == K_ALWAYS + || "REPLICA".equalsIgnoreCase(getToken(2).image))) { return true; } + if (first.kind == K_NO) { return "INHERIT".equalsIgnoreCase(getToken(2).image); } + if (first.kind == K_SET) { + return getToken(2).kind == OPENING_BRACKET || getToken(2).kind == K_SCHEMA + || getToken(2).kind == K_TABLESPACE || "ACCESS".equalsIgnoreCase(getToken(2).image) + || getToken(2).kind == K_WITHOUT || getToken(2).kind == K_UNLOGGED + || "LOGGED".equalsIgnoreCase(getToken(2).image); + } + if (first.kind != K_ALTER) { return false; } + if (getToken(2).kind == K_CONSTRAINT) { + return "INHERIT".equalsIgnoreCase(getToken(4).image) + || getToken(4).kind == K_NO && "INHERIT".equalsIgnoreCase(getToken(5).image); + } + int offset = getToken(2).kind == K_COLUMN ? 4 : 3; + return getToken(offset).kind == K_RESET + || getToken(offset).kind == K_DROP && "EXPRESSION".equalsIgnoreCase(getToken(offset + 1).image) + || getToken(offset).kind == K_SET && (getToken(offset + 1).kind == OPENING_BRACKET + || getToken(offset + 1).kind == K_STORAGE + || "COMPRESSION".equalsIgnoreCase(getToken(offset + 1).image) + || "STATISTICS".equalsIgnoreCase(getToken(offset + 1).image)); + } + + private void validateRelationAction(AlterRelation.ObjectType type, RelationAlterAction action) throws ParseException { + RelationAlterAction.Kind kind = action.getKind(); + EnumSet allowed = EnumSet.of(RelationAlterAction.Kind.RENAME, + RelationAlterAction.Kind.SET_OPTIONS, RelationAlterAction.Kind.RESET_OPTIONS, + RelationAlterAction.Kind.ALTER_COLUMN); + if (type == AlterRelation.ObjectType.INDEX) { + allowed.addAll(EnumSet.of(RelationAlterAction.Kind.SET_TABLESPACE, + RelationAlterAction.Kind.ATTACH_PARTITION, RelationAlterAction.Kind.DEPENDS_ON_EXTENSION)); + } else { + allowed.addAll(EnumSet.of(RelationAlterAction.Kind.RENAME_COLUMN, + RelationAlterAction.Kind.OWNER, RelationAlterAction.Kind.SET_SCHEMA)); + if (type == AlterRelation.ObjectType.MATERIALIZED_VIEW) { + allowed.addAll(EnumSet.of(RelationAlterAction.Kind.SET_TABLESPACE, + RelationAlterAction.Kind.SET_ACCESS_METHOD, RelationAlterAction.Kind.DEPENDS_ON_EXTENSION, + RelationAlterAction.Kind.CLUSTER_ON, RelationAlterAction.Kind.SET_WITHOUT_CLUSTER)); + } + } + requireDdlSyntax(allowed.contains(kind), "Unsupported " + type + " action: " + kind); + if (kind == RelationAlterAction.Kind.ALTER_COLUMN) { + RelationAlterAction.ColumnAction column = action.getColumnAction(); + if (type == AlterRelation.ObjectType.INDEX) { + requireDdlSyntax(action.getColumnNumber() != null + && column == RelationAlterAction.ColumnAction.SET_STATISTICS, + "ALTER INDEX column actions require an ordinal and SET STATISTICS"); + } else { + requireDdlSyntax(action.getColumnNumber() == null, "Expected a column name"); + boolean defaults = column == RelationAlterAction.ColumnAction.SET_DEFAULT + || column == RelationAlterAction.ColumnAction.DROP_DEFAULT; + requireDdlSyntax(type == AlterRelation.ObjectType.VIEW ? defaults + : column == RelationAlterAction.ColumnAction.SET_STATISTICS + || column == RelationAlterAction.ColumnAction.SET_STORAGE + || column == RelationAlterAction.ColumnAction.SET_COMPRESSION + || column == RelationAlterAction.ColumnAction.SET_OPTIONS + || column == RelationAlterAction.ColumnAction.RESET_OPTIONS, + "Unsupported view column action: " + column); + } + } + } + private boolean isIdentityAlterationAhead() { int first = getToken(1).kind; int second = getToken(2).kind; @@ -17977,6 +18046,11 @@ AlterExpression AlterExpression(): { ( + LOOKAHEAD({ isPostgreSqlRelationActionAhead() }) alterExp=PostgreSqlRelationAction() { + requireDdlSyntax(((RelationAlterAction) alterExp).getColumnNumber() == null, + "ALTER TABLE expects a column name, not an ordinal"); + } + | LOOKAHEAD( , { Dialect.COCKROACHDB.name().equals(getAsString(Feature.dialect)) }) alterExp = CockroachAlterPrimaryKey() @@ -18192,11 +18266,17 @@ Statement Alter(): | statement = AlterEvent() | + LOOKAHEAD(( | | ) ) + statement = PostgreSqlTablespaceMove() + | statement = AlterTable() | statement = AlterSession() | - LOOKAHEAD(MySqlViewPrefix() ) statement = AlterView(false) + LOOKAHEAD(MySqlViewPrefix() Table() [ ColumnsNamesList() ] ) statement = AlterView(false) + | + LOOKAHEAD({ getToken(1).kind == K_INDEX || getToken(1).kind == K_MATERIALIZED + || getToken(1).kind == K_VIEW }) statement=PostgreSqlAlterRelation() | statement = AlterSystemStatement() | @@ -18226,6 +18306,211 @@ Statement Alter(): } } +AlterTablespaceMove PostgreSqlTablespaceMove(): +{ + AlterTablespaceMove result = new AlterTablespaceMove(); + String name; +} +{ + ( { result.setObjectType(AlterTablespaceMove.ObjectType.TABLE); } + | { result.setObjectType(AlterTablespaceMove.ObjectType.INDEX); } + | { result.setObjectType(AlterTablespaceMove.ObjectType.MATERIALIZED_VIEW); } ) + name=RelObjectName() { result.setSourceTablespace(name); } + [ ContextualKeyword("OWNED") name=RelObjectName() { result.getOwners().add(name); } + ( "," name=RelObjectName() { result.getOwners().add(name); } )* ] + name=RelObjectName() { result.setTargetTablespace(name); } + [ { result.setNoWait(true); } ] + { return result; } +} + +/** The new relation statements reuse the same actions as structured ALTER TABLE properties. */ +AlterRelation PostgreSqlAlterRelation(): +{ + AlterRelation result = new AlterRelation(); + Table relation; + RelationAlterAction action; +} +{ + ( { result.setObjectType(AlterRelation.ObjectType.INDEX); } + | { result.setObjectType(AlterRelation.ObjectType.VIEW); } + | { result.setObjectType(AlterRelation.ObjectType.MATERIALIZED_VIEW); } ) + [ LOOKAHEAD(2) { result.setIfExists(true); } ] + relation=Table() { result.setRelation(relation); } + action=PostgreSqlRelationAction() { + validateRelationAction(result.getObjectType(), action); result.getActions().add(action); + } + ( "," action=PostgreSqlRelationAction() { + validateRelationAction(result.getObjectType(), action); result.getActions().add(action); + } )* + { + if (result.getActions().size() > 1) { + for (RelationAlterAction item : result.getActions()) { + requireDdlSyntax(!EnumSet.of(RelationAlterAction.Kind.RENAME, + RelationAlterAction.Kind.RENAME_COLUMN, RelationAlterAction.Kind.SET_SCHEMA, + RelationAlterAction.Kind.ATTACH_PARTITION, RelationAlterAction.Kind.DEPENDS_ON_EXTENSION) + .contains(item.getKind()), "This relation action must be a separate statement"); + } + } + return result; + } +} + +RelationAlterAction PostgreSqlRelationAction(): +{ + RelationAlterAction action = new RelationAlterAction(); + String name; + Token token; + Table relation; + Expression expression; + Long statistics; + List options; + List names = new ArrayList(); +} +{ + ( + LOOKAHEAD({ isKeywordAhead("CLUSTER") }) ContextualKeyword("CLUSTER") name=RelObjectName() + { action.setKind(RelationAlterAction.Kind.CLUSTER_ON); action.setValue(name); } + | + name=AccessQualifiedName() + { action.setKind(RelationAlterAction.Kind.OF); action.setValue(name); } + | + { action.setKind(RelationAlterAction.Kind.NOT_OF); } + | + ( { action.setTriggerState(RelationAlterAction.TriggerState.DISABLE); } + | { action.setTriggerState(RelationAlterAction.TriggerState.ENABLE); } + [ { action.setTriggerState(RelationAlterAction.TriggerState.ENABLE_ALWAYS); } + | ContextualKeyword("REPLICA") { action.setTriggerState(RelationAlterAction.TriggerState.ENABLE_REPLICA); } ] ) + + ( LOOKAHEAD(1) { action.setTriggerTarget(RelationAlterAction.TriggerTarget.ALL); } + | LOOKAHEAD(1) { action.setTriggerTarget(RelationAlterAction.TriggerTarget.USER); } + | name=RelObjectName() { action.setTriggerTarget(RelationAlterAction.TriggerTarget.NAME); action.setValue(name); } ) + { + action.setKind(RelationAlterAction.Kind.TRIGGER_STATE); + requireDdlSyntax(action.getTriggerTarget() == RelationAlterAction.TriggerTarget.NAME + || action.getTriggerState() == RelationAlterAction.TriggerState.ENABLE + || action.getTriggerState() == RelationAlterAction.TriggerState.DISABLE, + "ENABLE ALWAYS/REPLICA requires a trigger name"); + } + | + + ( + LOOKAHEAD(1) name=RelObjectName() { + action.setKind(RelationAlterAction.Kind.RENAME); action.setNewName(name); + } + | + [ LOOKAHEAD(1) { action.hasColumn(true); } ] + name=RelObjectName() { action.setColumnName(name); } + name=RelObjectName() { + action.setKind(RelationAlterAction.Kind.RENAME_COLUMN); action.setNewName(name); + } + ) + | + LOOKAHEAD({ isKeywordAhead("OWNER") }) ContextualKeyword("OWNER") name=RelObjectName() + { action.setKind(RelationAlterAction.Kind.OWNER); action.setValue(name); } + | + + ( + + ( LOOKAHEAD({ isKeywordAhead("CLUSTER") }) ContextualKeyword("CLUSTER") + { action.setKind(RelationAlterAction.Kind.SET_WITHOUT_CLUSTER); } + | ContextualKeyword("OIDS") { action.setKind(RelationAlterAction.Kind.SET_WITHOUT_OIDS); } ) + | { action.setKind(RelationAlterAction.Kind.SET_UNLOGGED); } + | LOOKAHEAD({ isKeywordAhead("LOGGED") }) ContextualKeyword("LOGGED") + { action.setKind(RelationAlterAction.Kind.SET_LOGGED); } + | name=RelObjectName() + { action.setKind(RelationAlterAction.Kind.SET_SCHEMA); action.setValue(name); } + | name=RelObjectName() + { action.setKind(RelationAlterAction.Kind.SET_TABLESPACE); action.setValue(name); } + | LOOKAHEAD({ isKeywordAhead("ACCESS") }) ContextualKeyword("ACCESS") ContextualKeyword("METHOD") name=RelObjectName() + { action.setKind(RelationAlterAction.Kind.SET_ACCESS_METHOD); action.setValue(name); } + | options=PostgreSqlIndexOptions() + { action.setKind(RelationAlterAction.Kind.SET_OPTIONS); action.setOptions(options); } + ) + | + "(" name=IndexKeyAttributeName() { names.add(name); } + ( "," name=IndexKeyAttributeName() { names.add(name); } )* ")" + { action.setKind(RelationAlterAction.Kind.RESET_OPTIONS); action.setResetOptions(names); } + | + LOOKAHEAD( ) name=RelObjectName() + [ { action.setNoInherit(true); } ] ContextualKeyword("INHERIT") + { action.setKind(RelationAlterAction.Kind.ALTER_CONSTRAINT_INHERIT); action.setConstraintName(name); } + | + [ LOOKAHEAD(1) { action.hasColumn(true); } ] + ( token= { action.setColumnNumber(Integer.valueOf(token.image)); } + | name=RelObjectName() { action.setColumnName(name); } ) + { action.setKind(RelationAlterAction.Kind.ALTER_COLUMN); } + ( + + ( + expression=Expression() { + action.setColumnAction(RelationAlterAction.ColumnAction.SET_DEFAULT); + action.setDefaultExpression(expression); + } + | + LOOKAHEAD({ isKeywordAhead("STATISTICS") }) ContextualKeyword("STATISTICS") + { action.setColumnAction(RelationAlterAction.ColumnAction.SET_STATISTICS); } + ( { action.setStatisticsDefault(true); } + | statistics=SequenceParameterValue() { + requireDdlSyntax(statistics >= -1 && statistics <= 10000, "Statistics target must be between -1 and 10000"); + action.setStatistics(statistics); + } ) + | + LOOKAHEAD("(") options=PostgreSqlIndexOptions() { + for (Index.Option option : options) { + requireDdlSyntax(option.getValue() != null && option.isUseEquals(), + "Column attributes require a value and an equals sign"); + } + action.setColumnAction(RelationAlterAction.ColumnAction.SET_OPTIONS); action.setOptions(options); + } + | + ( token= | token= | token= ) { + requireDdlSyntax(Arrays.asList("PLAIN", "EXTERNAL", "EXTENDED", "MAIN", "DEFAULT") + .contains(token.image.toUpperCase(Locale.ROOT)), "Invalid column storage strategy"); + action.setColumnAction(RelationAlterAction.ColumnAction.SET_STORAGE); action.setValue(token.image); + } + | + ContextualKeyword("COMPRESSION") name=RelObjectName() + { action.setColumnAction(RelationAlterAction.ColumnAction.SET_COMPRESSION); action.setValue(name); } + ) + | + "(" name=RelObjectName() { names.add(name); } + ( "," name=RelObjectName() { names.add(name); } )* ")" + { action.setColumnAction(RelationAlterAction.ColumnAction.RESET_OPTIONS); action.setResetOptions(names); } + | + + ( + { action.setColumnAction(RelationAlterAction.ColumnAction.DROP_DEFAULT); } + | + ContextualKeyword("EXPRESSION") { action.setColumnAction(RelationAlterAction.ColumnAction.DROP_EXPRESSION); } + [ LOOKAHEAD(2) { action.setUsingIfExists(true); } ] + ) + ) + | + relation=Table() + { action.setKind(RelationAlterAction.Kind.ATTACH_PARTITION); action.setRelation(relation); } + | + LOOKAHEAD({ isKeywordAhead("DEPENDS") || getToken(1).kind == K_NO && "DEPENDS".equalsIgnoreCase(getToken(2).image) }) + [ { action.setNoDependency(true); } ] ContextualKeyword("DEPENDS") ContextualKeyword("EXTENSION") name=RelObjectName() + { action.setKind(RelationAlterAction.Kind.DEPENDS_ON_EXTENSION); action.setValue(name); } + | + name=RelObjectName() + { action.setKind(RelationAlterAction.Kind.VALIDATE_CONSTRAINT); action.setConstraintName(name); } + | + LOOKAHEAD({ isKeywordAhead("REPLICA") }) ContextualKeyword("REPLICA") + { action.setKind(RelationAlterAction.Kind.REPLICA_IDENTITY); } + ( + name=RelObjectName() + { action.setReplicaIdentity(RelationAlterAction.ReplicaIdentity.USING_INDEX); action.setValue(name); } + | ( token= | token= | token= ) + { action.setReplicaIdentity(accessEnum(RelationAlterAction.ReplicaIdentity.class, token.image)); } + ) + | + [ { action.setNoInherit(true); } ] ContextualKeyword("INHERIT") relation=Table() + { action.setKind(RelationAlterAction.Kind.INHERIT); action.setRelation(relation); } + ) + { return action; } +} + Alter AlterTable(): { Alter alter = new Alter(); diff --git a/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java index a7a7b6d4d..5df34b8e3 100644 --- a/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java @@ -17,6 +17,7 @@ import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.alter.AlterRelation; import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.test.TestUtils; @@ -118,11 +119,11 @@ public void testCaptureRestIssue1993() throws JSQLParserException { } @Test - void testAlter() throws JSQLParserException { + void testFormerlyUnsupportedAlterIndex() throws JSQLParserException { String sqlStr = "ALTER INDEX idx_t_fa RENAME TO idx_t_fb"; Statement statement = TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); - assertInstanceOf(UnsupportedStatement.class, statement); + assertInstanceOf(AlterRelation.class, statement); } @Test diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlColumnStatisticsTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlColumnStatisticsTest.java new file mode 100644 index 000000000..eadbfa332 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlColumnStatisticsTest.java @@ -0,0 +1,84 @@ +/*- + * #%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.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +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 PostgreSqlColumnStatisticsTest { + @ParameterizedTest + @ValueSource(strings = {"SET STATISTICS DEFAULT", "SET STATISTICS -1", "SET STATISTICS 10000", + "SET (n_distinct = -0.5)", "SET (n_distinct = -1, n_distinct_inherited = 20)", + "RESET (n_distinct)", "RESET (n_distinct, n_distinct_inherited)"}) + void tablesAndMaterializedViewsShareColumnActions(String action) throws JSQLParserException { + for (String object : List.of("TABLE t", "MATERIALIZED VIEW mv")) { + String sql = "ALTER " + object + " ALTER COLUMN id " + action; + Statement statement = parse(sql); + assertRoundTrip(statement); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1", + p -> p.withDialect(Dialect.POSTGRESQL)).size()); + } + } + + @Test + void defaultTargetsAndAttributeValuesAreMutable() throws JSQLParserException { + AlterRelation index = + (AlterRelation) parse("ALTER INDEX ix ALTER COLUMN 1 SET STATISTICS DEFAULT"); + RelationAlterAction action = index.getActions().get(0); + assertTrue(action.isStatisticsDefault()); + assertNull(action.getStatistics()); + action.setStatistics(200L); + assertFalse(action.isStatisticsDefault()); + assertEquals("ALTER INDEX ix ALTER COLUMN 1 SET STATISTICS 200", index.toString()); + assertRoundTrip(index); + action.setStatisticsDefault(true); + assertRoundTrip(index); + Alter table = (Alter) parse("ALTER TABLE t ALTER COLUMN id SET (n_distinct=10)"); + action = (RelationAlterAction) table.getAlterExpressions().get(0); + action.getOptions().get(0).setValue(new LongValue(20)); + List visited = new ArrayList<>(); + action.visitExpressions(visited::add); + assertEquals(List.of(new LongValue(20)), visited); + assertEquals("ALTER TABLE t ALTER COLUMN id SET (n_distinct = 20)", table.toString()); + assertRoundTrip(table); + } + + @Test + void invalidTargetsAndCrossObjectActionsFail() { + for (String sql : new String[] {"ALTER VIEW v ALTER COLUMN id SET STATISTICS DEFAULT", + "ALTER INDEX ix ALTER COLUMN 1 SET (n_distinct=10)", + "ALTER TABLE t ALTER COLUMN id SET (n_distinct)", + "ALTER TABLE t ALTER COLUMN id SET STATISTICS 10001"}) { + assertThrows(JSQLParserException.class, () -> parse(sql), sql); + } + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void assertRoundTrip(Statement statement) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(statement.toString(), parse(sql.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlRelationAlterTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlRelationAlterTest.java new file mode 100644 index 000000000..c332046d8 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlRelationAlterTest.java @@ -0,0 +1,222 @@ +/*- + * #%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.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.util.TableDefinitionTraversal; +import net.sf.jsqlparser.util.TablesNamesFinder; +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 PostgreSqlRelationAlterTest { + @ParameterizedTest + @ValueSource(strings = { + "ALTER TABLE t CLUSTER ON ix", "ALTER TABLE t SET WITHOUT CLUSTER", + "ALTER TABLE t SET WITHOUT OIDS", "ALTER TABLE t SET LOGGED", + "ALTER TABLE t SET UNLOGGED", + "ALTER TABLE t OF public.typ", "ALTER TABLE t NOT OF", + "ALTER TABLE t ENABLE ALWAYS TRIGGER trg", "ALTER TABLE t ENABLE REPLICA TRIGGER trg", + "ALTER TABLE t DISABLE TRIGGER ALL", "ALTER TABLE t ENABLE TRIGGER USER", + "ALTER TABLE t ENABLE TRIGGER \"ALL\"", + "ALTER MATERIALIZED VIEW mv CLUSTER ON ix", + "ALTER MATERIALIZED VIEW mv SET WITHOUT CLUSTER", + "ALTER INDEX ix RENAME TO ix2", + "ALTER INDEX ix SET TABLESPACE pg_default", + "ALTER INDEX ix SET (fillfactor=80)", + "ALTER INDEX ix RESET (fillfactor)", + "ALTER INDEX ix ALTER COLUMN 1 SET STATISTICS 200", + "ALTER INDEX ix ALTER 1 SET STATISTICS 200", + "ALTER INDEX IF EXISTS ix RENAME TO ix2", + "ALTER INDEX IF EXISTS ix SET TABLESPACE pg_default", + "ALTER INDEX IF EXISTS ix SET (fillfactor=80)", + "ALTER INDEX IF EXISTS ix RESET (fillfactor)", + "ALTER INDEX IF EXISTS ix ALTER COLUMN 1 SET STATISTICS 200", + "ALTER INDEX IF EXISTS ix ALTER 1 SET STATISTICS 200", + "ALTER INDEX ix ATTACH PARTITION child_ix", + "ALTER VIEW v ALTER COLUMN id SET DEFAULT 1", + "ALTER VIEW v ALTER COLUMN id DROP DEFAULT", + "ALTER VIEW v SET (security_invoker=true)", + "ALTER VIEW v RESET (security_invoker)", + "ALTER VIEW v OWNER TO CURRENT_USER", + "ALTER VIEW v RENAME COLUMN id TO new_id", + "ALTER VIEW v RENAME TO v2", + "ALTER VIEW v SET SCHEMA ddl_aux", + "ALTER MATERIALIZED VIEW mv RENAME TO mv2", + "ALTER MATERIALIZED VIEW mv RENAME COLUMN id TO new_id", + "ALTER MATERIALIZED VIEW mv OWNER TO CURRENT_USER", + "ALTER MATERIALIZED VIEW mv SET SCHEMA ddl_aux", + "ALTER MATERIALIZED VIEW mv SET (fillfactor=80)", + "ALTER MATERIALIZED VIEW mv RESET (fillfactor)", + "ALTER MATERIALIZED VIEW mv SET TABLESPACE pg_default", + "ALTER MATERIALIZED VIEW mv SET ACCESS METHOD heap", + "ALTER MATERIALIZED VIEW mv ALTER COLUMN id SET STATISTICS 200", + "ALTER MATERIALIZED VIEW mv ALTER COLUMN label SET STORAGE EXTENDED", + "ALTER TABLE t ALTER COLUMN gen DROP EXPRESSION IF EXISTS", + "ALTER TABLE t ALTER CONSTRAINT ck INHERIT", + "ALTER TABLE t ALTER CONSTRAINT ck NO INHERIT", + "ALTER TABLE t ALTER CONSTRAINT nn INHERIT", + "ALTER TABLE t ALTER CONSTRAINT nn NO INHERIT", + "ALTER TABLE t VALIDATE CONSTRAINT ck", + "ALTER TABLE t SET (toast.autovacuum_enabled=false,fillfactor=70)", + "ALTER TABLE t RESET(toast.autovacuum_enabled,fillfactor)", + "ALTER TABLE t INHERIT p", + "ALTER TABLE t NO INHERIT p", + "ALTER TABLE t REPLICA IDENTITY USING INDEX ix", + "ALTER TABLE t SET ACCESS METHOD heap"}) + void auditedActionsAreStructuredAndRoundTrip(String sql) throws JSQLParserException { + Statement statement = parse(sql); + if (statement instanceof Alter) { + assertInstanceOf(RelationAlterAction.class, + ((Alter) statement).getAlterExpressions().get(0)); + } else { + AlterRelation relation = assertInstanceOf(AlterRelation.class, statement); + assertFalse(relation.getActions().isEmpty()); + } + assertRoundTrip(statement); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1", + p -> p.withDialect(Dialect.POSTGRESQL)).size()); + } + + @Test + void indexNameAndActionParametersCanBeReplaced() throws JSQLParserException { + AlterRelation index = + (AlterRelation) parse("ALTER INDEX IF EXISTS ix ALTER COLUMN 1 SET STATISTICS 200"); + assertEquals(AlterRelation.ObjectType.INDEX, index.getObjectType()); + assertTrue(index.isIfExists()); + assertEquals(1, index.getActions().get(0).getColumnNumber()); + index.setRelation(new Table("new_ix")); + index.getActions().get(0).setStatistics(100L); + assertEquals("ALTER INDEX IF EXISTS new_ix ALTER COLUMN 1 SET STATISTICS 100", + index.toString()); + assertTrue(new TablesNamesFinder().getTables(index).isEmpty()); + assertRoundTrip(index); + assertEquals("INDEX", index.accept(new StatementVisitorAdapter() { + @Override + public String visit(AlterRelation statement, S context) { + return statement.getObjectType().name(); + } + }, null)); + } + + @Test + void tableInheritanceAndGeneratedExpressionFlagsAreEditable() throws JSQLParserException { + Alter inherit = (Alter) parse("ALTER TABLE t NO INHERIT p"); + RelationAlterAction action = (RelationAlterAction) inherit.getAlterExpressions().get(0); + action.setNoInherit(false); + action.setRelation(new Table("parent")); + assertEquals("ALTER TABLE t INHERIT parent", inherit.toString()); + assertEquals(java.util.Set.of("t", "parent"), new TablesNamesFinder().getTables(inherit)); + Alter drop = + (Alter) parse("ALTER TABLE t ALTER COLUMN generated DROP EXPRESSION IF EXISTS"); + action = (RelationAlterAction) drop.getAlterExpressions().get(0); + assertEquals(RelationAlterAction.ColumnAction.DROP_EXPRESSION, action.getColumnAction()); + assertTrue(action.isUsingIfExists()); + action.setUsingIfExists(false); + assertEquals("ALTER TABLE t ALTER COLUMN generated DROP EXPRESSION", drop.toString()); + assertRoundTrip(drop); + } + + @Test + void viewDefaultsAndTableParametersAreVisited() throws JSQLParserException { + AlterRelation view = (AlterRelation) parse("ALTER VIEW v ALTER COLUMN id SET DEFAULT 1"); + RelationAlterAction action = view.getActions().get(0); + action.setDefaultExpression(new LongValue(2)); + assertEquals("ALTER VIEW v ALTER COLUMN id SET DEFAULT 2", view.toString()); + List visited = new ArrayList<>(); + action.visitExpressions(visited::add); + assertEquals(List.of(action.getDefaultExpression()), visited); + assertEquals(java.util.Set.of("v"), new TablesNamesFinder().getTables(view)); + assertRoundTrip(view); + Alter table = + (Alter) parse("ALTER TABLE t SET (toast.autovacuum_enabled=false, fillfactor=70)"); + action = (RelationAlterAction) table.getAlterExpressions().get(0); + assertEquals("toast.autovacuum_enabled", action.getOptions().get(0).getName()); + action.getOptions().get(1).setValue(new LongValue(80)); + visited.clear(); + TableDefinitionTraversal.visit(action, visited::add, ignored -> { + }); + assertEquals(2, visited.size()); + assertTrue(table.toString().endsWith("fillfactor = 80)")); + assertRoundTrip(table); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER INDEX ix OWNER TO me", "ALTER VIEW v SET TABLESPACE pg_default", + "ALTER MATERIALIZED VIEW v ALTER COLUMN id SET DEFAULT 1", + "ALTER INDEX ix ALTER COLUMN id SET STATISTICS 10", + "ALTER TABLE t ALTER COLUMN 1 SET STATISTICS 10", + "ALTER VIEW v RENAME TO v2, SET (security_invoker=true)"}) + void objectBoundariesAreEnforced(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + @Test + void triggerTargetsAndTypeNamesAreMutableWithoutInventingTables() throws JSQLParserException { + Alter statement = (Alter) parse("ALTER TABLE t ENABLE REPLICA TRIGGER trg"); + RelationAlterAction action = (RelationAlterAction) statement.getAlterExpressions().get(0); + assertEquals(RelationAlterAction.TriggerState.ENABLE_REPLICA, action.getTriggerState()); + action.setTriggerState(RelationAlterAction.TriggerState.DISABLE); + action.setTriggerTarget(RelationAlterAction.TriggerTarget.USER); + assertEquals("ALTER TABLE t DISABLE TRIGGER USER", statement.toString()); + assertRoundTrip(statement); + statement = (Alter) parse("ALTER TABLE t OF public.typ"); + action = (RelationAlterAction) statement.getAlterExpressions().get(0); + action.setValue("other.typ"); + assertEquals(java.util.Set.of("t"), new TablesNamesFinder().getTables(statement)); + assertRoundTrip(statement); + for (String sql : List.of("ALTER TABLE t ENABLE ALWAYS TRIGGER ALL", + "ALTER INDEX ix SET LOGGED", "ALTER VIEW v CLUSTER ON ix")) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + } + + @ParameterizedTest + @ValueSource(strings = {"TABLE", "INDEX", "MATERIALIZED VIEW"}) + void bulkTablespaceMovesShareOneModel(String objectType) throws JSQLParserException { + AlterTablespaceMove statement = (AlterTablespaceMove) parse("ALTER " + objectType + + " ALL IN TABLESPACE old_space OWNED BY alice, \"Bob\" SET TABLESPACE new_space NOWAIT"); + assertEquals(objectType.replace(' ', '_'), statement.getObjectType().name()); + assertEquals(List.of("alice", "\"Bob\""), statement.getOwners()); + assertEquals("old_space", statement.getSourceTablespace()); + assertTrue(statement.isNoWait()); + assertTrue(new TablesNamesFinder().getTables(statement).isEmpty()); + statement.setTargetTablespace("pg_default"); + statement.getOwners().clear(); + statement.setNoWait(false); + assertEquals( + "ALTER " + objectType + " ALL IN TABLESPACE old_space SET TABLESPACE pg_default", + statement.toString()); + assertRoundTrip(statement); + assertEquals(2, CCJSqlParserUtil.parseStatements(statement + "; SELECT 1").size()); + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void assertRoundTrip(Statement statement) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(statement.toString(), parse(sql.toString()).toString()); + } +}