diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterConstraintAttributes.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterConstraintAttributes.java index 911c0a895..c9276af84 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterConstraintAttributes.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterConstraintAttributes.java @@ -9,6 +9,9 @@ */ package net.sf.jsqlparser.statement.alter; +import java.util.Collections; +import java.util.List; + import java.util.Objects; import net.sf.jsqlparser.statement.create.table.ConstraintAttributes; import net.sf.jsqlparser.statement.create.table.ConstraintKind; @@ -30,6 +33,11 @@ public void setAttributes(ConstraintAttributes attributes) { this.attributes = Objects.requireNonNull(attributes, "attributes"); } + @Override + public List getColumnIdentityActions() { + return Collections.emptyList(); + } + @Override public boolean hasActiveTableDefinition() { return false; diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java index 45fa8ad19..eef80c080 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java @@ -444,6 +444,35 @@ public List getColumnDefaults() { return Collections.unmodifiableList(columnSetDefaultList); } + /** + * Returns active ALTER COLUMN identity actions without treating them as data-type changes. The + * list is an unmodifiable membership snapshot; its entries are the original mutable column + * nodes. Request a new list after adding, removing or changing actions. Subclasses that render + * a different body override this method to exclude inactive inherited column definitions. + */ + public List getColumnIdentityActions() { + if (hasExplicitConstraintHeader() || isRenameOperation() || hasAlterColumnValueActions()) { + return Collections.emptyList(); + } + return getColumnIdentityActionsFromDefinitions(); + } + + /** Returns identity actions when this body's general column-definition branch is active. */ + protected List getColumnIdentityActionsFromDefinitions() { + if (operation != AlterOperation.ALTER || colDataTypeList == null + || columnName != null || commentText != null) { + return Collections.emptyList(); + } + List actions = new ArrayList<>(); + for (ColumnDataType column : colDataTypeList) { + if (column.getIdentityAlterations() != null + && !column.getIdentityAlterations().isEmpty()) { + actions.add(column); + } + } + return Collections.unmodifiableList(actions); + } + public void addColSetVisibility(ColumnSetVisibility columnSetVisibility) { if (columnSetVisibilityList == null) { columnSetVisibilityList = new ArrayList<>(); @@ -994,10 +1023,7 @@ protected void appendBody(StringBuilder b) { b.append(optionalSpecifier); } else if (hasExplicitConstraintHeader()) { toStringConstraintAlter(b); - } else if (operation == AlterOperation.ALTER - && (columnDropDefaultList != null && !columnDropDefaultList.isEmpty() - || columnSetDefaultList != null && !columnSetDefaultList.isEmpty() - || columnSetVisibilityList != null && !columnSetVisibilityList.isEmpty())) { + } else if (hasAlterColumnValueActions()) { toStringAlterColumn(b); } else if (isSimpleKeywordOperation()) { toStringSimpleKeyword(b); @@ -1014,6 +1040,13 @@ protected void appendBody(StringBuilder b) { } } + private boolean hasAlterColumnValueActions() { + return operation == AlterOperation.ALTER + && (columnDropDefaultList != null && !columnDropDefaultList.isEmpty() + || columnSetDefaultList != null && !columnSetDefaultList.isEmpty() + || columnSetVisibilityList != null && !columnSetVisibilityList.isEmpty()); + } + /** * Appends the common tail (parameters, index comment) shared by all ALTER expressions. */ @@ -1711,7 +1744,8 @@ public void setHasEqualForCollate(boolean hasEqualForCollate) { this.hasEqualForCollate = hasEqualForCollate; } - public static final class ColumnDataType extends ColumnDefinition { + public static final class ColumnDataType extends ColumnDefinition + implements ColumnIdentityAction { private final boolean withType; private boolean useSetData; @@ -1739,10 +1773,12 @@ public void setUsingExpression(Expression usingExpression) { this.usingExpression = usingExpression; } + @Override public List getIdentityAlterations() { return identityAlterations; } + @Override public void setIdentityAlterations(List identityAlterations) { this.identityAlterations = identityAlterations == null ? null : new ArrayList<>(identityAlterations); diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionCharset.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionCharset.java index 21fa295c4..49511719f 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionCharset.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionCharset.java @@ -9,6 +9,9 @@ */ package net.sf.jsqlparser.statement.alter; +import java.util.Collections; +import java.util.List; + import net.sf.jsqlparser.statement.create.table.ConstraintKind; /** @@ -17,6 +20,11 @@ */ public class AlterExpressionCharset extends AlterExpression { + @Override + public List getColumnIdentityActions() { + return Collections.emptyList(); + } + @Override public boolean hasActiveTableDefinition() { return false; diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionDrop.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionDrop.java index 839b52caf..71d5d91f1 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionDrop.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionDrop.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.statement.alter; +import java.util.List; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.statement.create.table.ConstraintKind; @@ -18,6 +19,11 @@ * DROP INDEX/KEY, DROP PRIMARY KEY, DROP UNIQUE, DROP FOREIGN KEY, and DROP PARTITION. */ public class AlterExpressionDrop extends AlterExpression { + @Override + public List getColumnIdentityActions() { + return getColumnIdentityActionsFromDefinitions(); + } + @Override public boolean hasActiveTableDefinition() { return false; diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionOrderBy.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionOrderBy.java index 7b49c007a..9df2b1fb8 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionOrderBy.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionOrderBy.java @@ -9,6 +9,8 @@ */ package net.sf.jsqlparser.statement.alter; +import java.util.Collections; + import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; @@ -28,6 +30,11 @@ public List getOrderByElements() { return orderByElements; } + @Override + public List getColumnIdentityActions() { + return Collections.emptyList(); + } + @Override public boolean hasActiveTableDefinition() { return false; diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPartition.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPartition.java index ab0d5c84b..ad97b1183 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPartition.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPartition.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.statement.alter; + import java.util.AbstractList; import java.util.ArrayList; import java.util.Collection; @@ -264,6 +265,11 @@ public void setCoalescePartitionCount(Integer coalescePartitionCount) { } } + @Override + public List getColumnIdentityActions() { + return Collections.emptyList(); + } + @Override public boolean hasActiveTableDefinition() { return false; diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPrimaryKey.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPrimaryKey.java index a51370e37..e8e170190 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPrimaryKey.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPrimaryKey.java @@ -9,6 +9,9 @@ */ package net.sf.jsqlparser.statement.alter; +import java.util.Collections; +import java.util.List; + import net.sf.jsqlparser.statement.create.table.IndexOption; import net.sf.jsqlparser.statement.create.table.KeyElement; import java.util.Iterator; @@ -31,6 +34,11 @@ public AlterExpressionPrimaryKey() { setConstraint(new KeyConstraint().withType("PRIMARY KEY")); } + @Override + public List getColumnIdentityActions() { + return Collections.emptyList(); + } + @Override public boolean hasActiveTableDefinition() { return false; diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionRename.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionRename.java index baf7e4e69..5c4176271 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionRename.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionRename.java @@ -9,6 +9,9 @@ */ package net.sf.jsqlparser.statement.alter; +import java.util.Collections; +import java.util.List; + import net.sf.jsqlparser.statement.create.table.ConstraintKind; /** @@ -42,6 +45,11 @@ public void setTargetName(String name) { setColumnName(name); } + @Override + public List getColumnIdentityActions() { + return Collections.emptyList(); + } + @Override public boolean hasActiveTableDefinition() { return false; diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionTableOption.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionTableOption.java index 31d392e7b..811bc898a 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionTableOption.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionTableOption.java @@ -9,6 +9,9 @@ */ package net.sf.jsqlparser.statement.alter; +import java.util.Collections; +import java.util.List; + import net.sf.jsqlparser.statement.create.table.TableOption; import net.sf.jsqlparser.statement.create.table.ConstraintKind; @@ -33,6 +36,11 @@ public void setStructuredTableOption(TableOption option) { super.setUseEqual(false); } + @Override + public List getColumnIdentityActions() { + return Collections.emptyList(); + } + @Override public boolean hasActiveTableDefinition() { return false; diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterForeignDataOptions.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterForeignDataOptions.java index 5e5a1df95..53ded8ff5 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterForeignDataOptions.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterForeignDataOptions.java @@ -9,6 +9,8 @@ */ package net.sf.jsqlparser.statement.alter; +import java.util.Collections; + import java.util.List; import java.util.function.Consumer; import net.sf.jsqlparser.expression.Expression; @@ -40,6 +42,11 @@ public StringBuilder appendTo(StringBuilder builder, Consumer expres return builder; } + @Override + public List getColumnIdentityActions() { + return Collections.emptyList(); + } + @Override public boolean hasActiveTableDefinition() { return false; diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/ColumnIdentityAction.java b/src/main/java/net/sf/jsqlparser/statement/alter/ColumnIdentityAction.java new file mode 100644 index 000000000..4fe49f838 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/ColumnIdentityAction.java @@ -0,0 +1,28 @@ +/*- + * #%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; + +/** + * An ALTER COLUMN target and its ordered identity alterations. Obtain active column actions through + * {@link AlterExpression#getColumnIdentityActions()}; edits update the existing statement model. + */ +public interface ColumnIdentityAction { + String getColumnName(); + + void setColumnName(String columnName); + + /** Returns the mutable, ordered alterations, or null when this column has none. */ + List getIdentityAlterations(); + + /** Replaces the alterations; null restores the column's ordinary definition rendering. */ + void setIdentityAlterations(List identityAlterations); +} diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/IdentityAlteration.java b/src/main/java/net/sf/jsqlparser/statement/alter/IdentityAlteration.java index c5c918fc5..369708073 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/IdentityAlteration.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/IdentityAlteration.java @@ -45,20 +45,60 @@ public void setIdentityDefinition(IdentityDefinition identityDefinition) { this.identityDefinition = identityDefinition; } + /** + * Returns ADD's definition mode or SET GENERATED's mode. An ADD without a definition returns + * null; no definition is created by reading it. + */ public IdentityDefinition.GenerationMode getGenerationMode() { - return generationMode; + return kind == Kind.ADD + ? identityDefinition == null ? null : identityDefinition.getGenerationMode() + : generationMode; } + /** + * Updates ADD's existing definition or SET GENERATED's mode. A non-null ADD mode creates a + * definition when absent; a null mode never creates one. Other kinds retain the value as + * inactive metadata and do not render it. + */ public void setGenerationMode(IdentityDefinition.GenerationMode generationMode) { - this.generationMode = generationMode; + if (kind == Kind.ADD) { + if (identityDefinition != null) { + identityDefinition.setGenerationMode(generationMode); + } else if (generationMode != null) { + identityDefinition = new IdentityDefinition(generationMode); + } + } else { + this.generationMode = generationMode; + } } + /** + * Returns ADD's definition parameters or SET_PARAMETER's parameters. The returned list belongs + * to the active payload and can be edited directly; an omitted list is null. + */ public List getParameters() { - return parameters; + return kind == Kind.ADD + ? identityDefinition == null ? null : identityDefinition.getParameters() + : parameters; } + /** + * Updates ADD's definition parameters or SET_PARAMETER's parameters. A non-null ADD list + * creates a BY DEFAULT definition when absent; a null list never creates one. Other kinds + * retain the value as inactive metadata and do not render it. + */ public void setParameters(List parameters) { - this.parameters = parameters == null ? null : new ArrayList<>(parameters); + if (kind == Kind.ADD) { + if (identityDefinition == null && parameters != null) { + identityDefinition = + new IdentityDefinition(IdentityDefinition.GenerationMode.BY_DEFAULT); + } + if (identityDefinition != null) { + identityDefinition.setParameters(parameters); + } + } else { + this.parameters = parameters == null ? null : new ArrayList<>(parameters); + } } /** Returns the restart value, or null for a bare RESTART using the sequence start value. */ diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/RelationAlterAction.java b/src/main/java/net/sf/jsqlparser/statement/alter/RelationAlterAction.java index f5beb06e9..58d52a635 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/RelationAlterAction.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/RelationAlterAction.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.statement.alter; + import java.util.Collections; import java.util.List; import java.util.function.Consumer; @@ -240,6 +241,11 @@ public void setResetOptions(List resetOptions) { this.resetOptions = resetOptions; } + @Override + public List getColumnIdentityActions() { + return Collections.emptyList(); + } + @Override public boolean hasActiveTableDefinition() { return false; diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index 218d6dcf7..fa8db1623 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -471,7 +471,9 @@ with ``new ExclusionOperator().withSchemaName("pg_catalog").withName("&&") column.setUsingExpression(CCJSqlParserUtil.parseExpression("id * 10")); String updatedSql = alter.toString(); -Identity alterations are available as ``ColumnDataType.getIdentityAlterations()``. Sequence ownership is shared by ``CreateSequence`` and ``AlterSequence`` through ``Sequence.getOwnership()``: ``null`` means omitted, ``isNone()`` means explicit ``OWNED BY NONE``, and ``getColumn()`` identifies an owner. ``TablesNamesFinder`` includes ``LIKE`` sources and sequence owners without treating sequence or type names as tables. See `ALTER TABLE `_ and `ALTER SEQUENCE `_. +Identity alterations are directly accessible through ``AlterExpression.getColumnIdentityActions()``; +the existing ``ColumnDataType.getIdentityAlterations()`` API remains available. +Sequence ownership is shared by ``CreateSequence`` and ``AlterSequence`` through ``Sequence.getOwnership()``: ``null`` means omitted, ``isNone()`` means explicit ``OWNED BY NONE``, and ``getColumn()`` identifies an owner. ``TablesNamesFinder`` includes ``LIKE`` sources and sequence owners without treating sequence or type names as tables. See `ALTER TABLE `_ and `ALTER SEQUENCE `_. For identity columns, ``RESTART 20`` and ``RESTART WITH 20`` produce the same ``IdentityAlteration`` with kind ``RESTART`` and ``getRestartWith() == 20L``. @@ -481,6 +483,52 @@ bare ``RESTART``, which uses the sequence's configured start value. Call ``new IdentityAlteration(Kind.RESTART).withRestartWith(20L)``. +Edit column identity actions +---------------------------- + +``getColumnIdentityActions()`` returns active ``ALTER COLUMN`` identity actions +as ``ColumnIdentityAction`` entries. Each entry exposes the editable column name +and its ordered ``IdentityAlteration`` list, without requiring a data-type cast: + +.. code-block:: java + + Alter alter = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE accounts ALTER COLUMN id " + + "ADD GENERATED BY DEFAULT AS IDENTITY (CACHE 10)", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + ColumnIdentityAction column = alter.getAlterExpressions().get(0) + .getColumnIdentityActions().get(0); + column.setColumnName("account_id"); + IdentityAlteration identity = column.getIdentityAlterations().get(0); + identity.setGenerationMode(IdentityDefinition.GenerationMode.ALWAYS); + identity.getParameters().get(0).setValue(20L); + System.out.println(alter); + // ALTER TABLE accounts ALTER COLUMN account_id + // ADD GENERATED ALWAYS AS IDENTITY (CACHE 20) + +The outer list has unmodifiable membership; its entries are the original AST +nodes. Request a new list after changing action membership or switching the +outer ALTER operation. The inner alteration list is mutable: replace an entry +to change its kind, or use ``setIdentityAlterations`` to replace the ordered +actions for a column. Unrelated actions and inactive inherited payloads are +excluded. An inline identity declaration in ``ADD COLUMN`` remains a +``ColumnOption`` rather than an identity alteration. + +``getGenerationMode``/``setGenerationMode`` work for both ``ADD`` and +``SET_GENERATED``. ``getParameters``/``setParameters`` work for ``ADD`` and +``SET_PARAMETER``. ADD access delegates to its existing ``IdentityDefinition``; +replacing or clearing that definition immediately changes both views. Parameters +are ordered mutable ``Sequence.Parameter`` values, with null meaning omitted. +RESTART and DROP retain their distinct ``getRestartWith`` and ``isIfExists`` +properties. A sequence such as ``SET CACHE 10 RESTART WITH 20`` remains two +ordered alterations. + +For a newly constructed ADD action without a definition, mode and parameter +getters return null. Non-null setters create the definition; setting parameters +first uses BY DEFAULT. Null setters do not create one. This does not change the +SQL grammar or validate whether a column already has an identity in a database. + + Column defaults on tables and views ----------------------------------- diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/ColumnIdentityActionTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/ColumnIdentityActionTest.java new file mode 100644 index 000000000..e20f11186 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/ColumnIdentityActionTest.java @@ -0,0 +1,287 @@ +/*- + * #%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 java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Sequence; +import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnDataType; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.create.table.IdentityDefinition; +import net.sf.jsqlparser.statement.create.table.IdentityDefinition.GenerationMode; +import net.sf.jsqlparser.statement.create.table.Index; +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.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class ColumnIdentityActionTest { + @ParameterizedTest + @ValueSource(strings = {"ADD GENERATED ALWAYS AS IDENTITY (CACHE 10)", + "SET GENERATED BY DEFAULT", "SET CACHE 10", "RESTART WITH 20", + "DROP IDENTITY IF EXISTS"}) + void exposesOriginalColumnAndKeepsEachActionKind(String clause) throws JSQLParserException { + Alter statement = parse("ALTER TABLE t ALTER COLUMN id " + clause); + AlterExpression action = first(statement); + ColumnIdentityAction column = action.getColumnIdentityActions().get(0); + assertSame(action.getColDataTypeList().get(0), column); + assertSame(action.getColDataTypeList().get(0).getIdentityAlterations(), + column.getIdentityAlterations()); + assertNull(action.getColDataTypeList().get(0).getColDataType()); + column.setColumnName("\"New Id\""); + assertEquals("ALTER TABLE t ALTER COLUMN \"New Id\" " + clause, statement.toString()); + roundTrip(statement); + } + + @Test + void addModeAndParametersEditTheDefinitionAndFollowReplacements() throws JSQLParserException { + Alter statement = parse("ALTER TABLE t ALTER COLUMN id " + + "ADD GENERATED BY DEFAULT AS IDENTITY (CACHE 10)"); + IdentityAlteration action = identity(statement); + IdentityDefinition original = action.getIdentityDefinition(); + assertEquals(GenerationMode.BY_DEFAULT, action.getGenerationMode()); + assertSame(original.getParameters(), action.getParameters()); + action.setGenerationMode(GenerationMode.ALWAYS); + action.getParameters().get(0).setValue(20L); + assertEquals(GenerationMode.ALWAYS, original.getGenerationMode()); + assertEquals("ALTER TABLE t ALTER COLUMN id " + + "ADD GENERATED ALWAYS AS IDENTITY (CACHE 20)", statement.toString()); + roundTrip(statement); + + IdentityDefinition replacement = new IdentityDefinition(GenerationMode.BY_DEFAULT); + action.setIdentityDefinition(replacement); + assertEquals(GenerationMode.BY_DEFAULT, action.getGenerationMode()); + assertNull(action.getParameters()); + action.setParameters(List.of(cache(30))); + assertSame(replacement.getParameters(), action.getParameters()); + replacement.setGenerationMode(GenerationMode.ALWAYS); + assertEquals(GenerationMode.ALWAYS, action.getGenerationMode()); + assertEquals("ALTER TABLE t ALTER COLUMN id " + + "ADD GENERATED ALWAYS AS IDENTITY (CACHE 30)", statement.toString()); + roundTrip(statement); + + action.setIdentityDefinition(null); + assertNull(action.getGenerationMode()); + assertNull(action.getParameters()); + action.setGenerationMode(GenerationMode.BY_DEFAULT); + assertNotSame(original, action.getIdentityDefinition()); + assertNotSame(replacement, action.getIdentityDefinition()); + assertNull(action.getParameters()); + roundTrip(statement); + } + + @Test + void addBuilderCreatesOnlyTheRequestedDefinition() { + IdentityAlteration action = new IdentityAlteration(IdentityAlteration.Kind.ADD); + assertNull(action.getGenerationMode()); + assertNull(action.getParameters()); + action.setGenerationMode(null); + action.setParameters(null); + assertNull(action.getIdentityDefinition()); + action.setParameters(List.of(cache(10))); + assertEquals(GenerationMode.BY_DEFAULT, action.getGenerationMode()); + assertEquals("ADD GENERATED BY DEFAULT AS IDENTITY (CACHE 10)", action.toString()); + IdentityDefinition definition = action.getIdentityDefinition(); + action.setGenerationMode(GenerationMode.ALWAYS); + action.setParameters(null); + assertSame(definition, action.getIdentityDefinition()); + assertEquals("ADD GENERATED ALWAYS AS IDENTITY", action.toString()); + action.setGenerationMode(null); + assertNull(definition.getGenerationMode()); + } + + @Test + void orderedSetAndRestartActionsRemainIndependentAndEditable() throws JSQLParserException { + Alter statement = parse("ALTER TABLE t ALTER COLUMN id " + + "SET GENERATED BY DEFAULT SET CACHE 10 RESTART 20"); + ColumnIdentityAction column = first(statement).getColumnIdentityActions().get(0); + List actions = column.getIdentityAlterations(); + assertEquals(List.of(IdentityAlteration.Kind.SET_GENERATED, + IdentityAlteration.Kind.SET_PARAMETER, IdentityAlteration.Kind.RESTART), + actions.stream().map(IdentityAlteration::getKind).collect(Collectors.toList())); + actions.get(0).setGenerationMode(GenerationMode.ALWAYS); + actions.get(1).getParameters().get(0).setValue(30L); + actions.get(2).setRestartWith(null); + assertEquals("ALTER TABLE t ALTER COLUMN id SET GENERATED ALWAYS SET CACHE 30 RESTART", + statement.toString()); + roundTrip(statement); + + IdentityAlteration drop = new IdentityAlteration(IdentityAlteration.Kind.DROP); + drop.setIfExists(true); + column.setIdentityAlterations(List.of(drop)); + assertSame(drop, identity(statement)); + assertNotSame(actions, column.getIdentityAlterations()); + assertEquals("ALTER TABLE t ALTER COLUMN id DROP IDENTITY IF EXISTS", statement.toString()); + roundTrip(statement); + drop.setIfExists(false); + assertEquals("ALTER TABLE t ALTER COLUMN id DROP IDENTITY", statement.toString()); + roundTrip(statement); + } + + @Test + void unrelatedIdentityPropertiesDoNotChangeRestartOrDropRendering() { + for (IdentityAlteration.Kind kind : List.of(IdentityAlteration.Kind.RESTART, + IdentityAlteration.Kind.DROP)) { + IdentityAlteration action = new IdentityAlteration(kind); + String before = action.toString(); + action.setIdentityDefinition(new IdentityDefinition(GenerationMode.ALWAYS)); + action.setGenerationMode(GenerationMode.ALWAYS); + action.setParameters(List.of(cache(50))); + assertEquals(before, action.toString()); + } + } + + @Test + void lookupKeepsColumnMembershipSeparateFromMutableActions() throws JSQLParserException { + Alter statement = parse("ALTER TABLE t ALTER COLUMN id RESTART WITH 20, " + + "ALTER COLUMN other TYPE bigint"); + AlterExpression action = first(statement); + List columns = action.getColumnIdentityActions(); + assertEquals(1, columns.size()); + assertTrue(statement.getAlterExpressions().get(1).getColumnIdentityActions().isEmpty()); + assertThrows(UnsupportedOperationException.class, columns::clear); + ColumnIdentityAction column = columns.get(0); + ColumnDataType stored = action.getColDataTypeList().get(0); + stored.setColDataType(new ColDataType("bigint")); + stored.setIdentityAlterations(null); + assertNull(column.getIdentityAlterations()); + assertTrue(action.getColumnIdentityActions().isEmpty()); + assertEquals(1, columns.size()); + column.setIdentityAlterations( + List.of(new IdentityAlteration(IdentityAlteration.Kind.RESTART))); + assertSame(column, action.getColumnIdentityActions().get(0)); + action.setOperation(AlterOperation.DROP); + assertTrue(action.getColumnIdentityActions().isEmpty()); + action.setOperation(AlterOperation.ALTER); + assertSame(column, action.getColumnIdentityActions().get(0)); + roundTrip(statement); + column.getIdentityAlterations().clear(); + assertTrue(action.getColumnIdentityActions().isEmpty()); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER TABLE t ADD COLUMN id bigint GENERATED ALWAYS AS IDENTITY", + "ALTER TABLE t ALTER COLUMN id TYPE bigint", + "ALTER TABLE t ALTER COLUMN id SET DEFAULT 1", + "ALTER TABLE t DROP COLUMN id"}) + void excludesDeclarationsAndUnrelatedColumnChanges(String sql) throws JSQLParserException { + Alter statement = parse(sql); + assertTrue(first(statement).getColumnIdentityActions().isEmpty()); + roundTrip(statement); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("inactiveColumnBodies") + void excludesInheritedIdentityPayloadWhenAnotherBodyWins(String description, + Consumer change) throws JSQLParserException { + AlterExpression action = first(parse("ALTER TABLE t ALTER COLUMN id RESTART")); + change.accept(action); + assertTrue(action.getColumnIdentityActions().isEmpty(), description); + } + + static Stream inactiveColumnBodies() { + return Stream.of( + inactive("different operation", action -> action.setOperation(AlterOperation.DROP)), + inactive("named column", action -> action.setColumnName("other")), + inactive("comment", action -> action.setCommentText("'text'")), + inactive("constraint header", action -> { + action.setConstraintType("CONSTRAINT"); + action.setConstraintSymbol("c"); + }), + inactive("index rename", action -> action.setOldIndex(new Index().withName("old"))), + inactive("drop default", action -> action.addColDropDefault( + new AlterExpression.ColumnDropDefault("other"))), + inactive("set default", action -> action.addColSetDefault( + new AlterExpression.ColumnSetDefault("other", "1"))), + inactive("visibility", action -> action.addColSetVisibility( + new AlterExpression.ColumnSetVisibility("other", true)))); + } + + private static Arguments inactive(String description, Consumer change) { + return Arguments.of(description, change); + } + + @Test + void changedDropOperationUsesItsGeneralRendererPrecedence() { + AlterExpressionDrop action = new AlterExpressionDrop(); + action.setOperation(AlterOperation.ALTER); + ColumnDataType column = new ColumnDataType(false).withColumnName("id"); + column.setIdentityAlterations( + List.of(new IdentityAlteration(IdentityAlteration.Kind.RESTART))); + action.addColDataType(column); + assertEquals(List.of(column), action.getColumnIdentityActions()); + action.addColSetDefault(new AlterExpression.ColumnSetDefault("other", "1")); + action.setConstraintType("CONSTRAINT"); + action.setConstraintSymbol("c"); + action.setOldIndex(new Index().withName("old")); + assertEquals("ALTER id RESTART", action.toString()); + assertEquals(List.of(column), action.getColumnIdentityActions()); + column.setColumnName("renamed"); + assertEquals("ALTER renamed RESTART", action.toString()); + action.setColumnName("other"); + assertTrue(action.getColumnIdentityActions().isEmpty()); + assertEquals("ALTER other", action.toString()); + action.setColumnName(null); + action.setOperation(AlterOperation.DROP); + assertTrue(action.getColumnIdentityActions().isEmpty()); + } + + @ParameterizedTest + @MethodSource("fixedBodies") + void fixedActionRenderersDoNotExposeInactiveColumnIdentityData(AlterExpression action) { + action.setOperation(AlterOperation.ALTER); + ColumnDataType column = new ColumnDataType(false).withColumnName("id"); + column.setIdentityAlterations( + List.of(new IdentityAlteration(IdentityAlteration.Kind.RESTART))); + action.addColDataType(column); + assertTrue(action.getColumnIdentityActions().isEmpty()); + } + + static Stream fixedBodies() { + return Stream.of(new AlterConstraintAttributes(), new AlterExpressionCharset(), + new AlterExpressionOrderBy(), new AlterExpressionPartition(), + new AlterExpressionPrimaryKey(), new AlterExpressionRename(), + new AlterExpressionTableOption(), new AlterForeignDataOptions(), + new RelationAlterAction()); + } + + private static Sequence.Parameter cache(long value) { + return new Sequence.Parameter(Sequence.ParameterType.CACHE).withValue(value); + } + + private static Alter parse(String sql) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.POSTGRESQL)); + } + + private static AlterExpression first(Alter statement) { + return statement.getAlterExpressions().get(0); + } + + private static IdentityAlteration identity(Alter statement) { + return first(statement).getColumnIdentityActions().get(0).getIdentityAlterations().get(0); + } + + private static void roundTrip(Alter statement) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(sql.toString(), parse(sql.toString()).toString()); + } +}