From 09cfe812d43399894b90e6a2fa7d376bd866ea00 Mon Sep 17 00:00:00 2001 From: minleejae Date: Thu, 1 Oct 2026 22:44:01 +0900 Subject: [PATCH 1/2] refactor: share access to table and view column defaults Signed-off-by: minleejae --- .../statement/alter/AlterExpression.java | 32 ++- .../statement/alter/ColumnDefaultAction.java | 45 ++++ .../statement/alter/RelationAlterAction.java | 14 +- .../alter/ColumnDefaultActionTest.java | 197 ++++++++++++++++++ 4 files changed, 279 insertions(+), 9 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/statement/alter/ColumnDefaultAction.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/alter/ColumnDefaultActionTest.java 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 aa0fba53b..e598a9e5a 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java @@ -419,6 +419,19 @@ public List getColumnSetDefaultList() { return columnSetDefaultList; } + /** + * Returns this action's active SET DEFAULT entries through the same API used by ALTER VIEW. The + * list is read-only; its entries are the original mutable AST objects, not copies. Use the + * existing action-specific API to add or remove entries. + */ + public List getColumnDefaults() { + if (operation != AlterOperation.ALTER || columnSetDefaultList == null + || columnDropDefaultList != null && !columnDropDefaultList.isEmpty()) { + return Collections.emptyList(); + } + return Collections.unmodifiableList(columnSetDefaultList); + } + public void addColSetVisibility(ColumnSetVisibility columnSetVisibility) { if (columnSetVisibilityList == null) { columnSetVisibilityList = new ArrayList<>(); @@ -1696,8 +1709,8 @@ public String toString() { } } - public static final class ColumnSetDefault implements Serializable { - private final String columnName; + public static final class ColumnSetDefault implements ColumnDefaultAction, Serializable { + private String columnName; private String defaultValue; private Expression defaultExpression; @@ -1706,10 +1719,16 @@ public ColumnSetDefault(String columnName, String defaultValue) { this.defaultValue = defaultValue; } + @Override public String getColumnName() { return columnName; } + @Override + public void setColumnName(String columnName) { + this.columnName = columnName; + } + /** Constructs a structured default without overloading the legacy nullable String API. */ public static ColumnSetDefault fromExpression(String columnName, Expression expression) { ColumnSetDefault result = new ColumnSetDefault(columnName, null); @@ -1717,26 +1736,25 @@ public static ColumnSetDefault fromExpression(String columnName, Expression expr return result; } + @Override public Expression getDefaultExpression() { return defaultExpression; } + @Override public void setDefaultExpression(Expression defaultExpression) { this.defaultExpression = defaultExpression; this.defaultValue = null; } + @Override public String getDefaultValue() { return defaultExpression == null ? defaultValue : defaultExpression.toString(); } public void appendTo(StringBuilder sql, Consumer expressionPrinter) { sql.append(columnName).append(" SET DEFAULT "); - if (defaultExpression == null) { - sql.append(defaultValue); - } else { - expressionPrinter.accept(defaultExpression); - } + appendDefaultValueTo(sql, expressionPrinter); } @Override diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/ColumnDefaultAction.java b/src/main/java/net/sf/jsqlparser/statement/alter/ColumnDefaultAction.java new file mode 100644 index 000000000..46b0c653c --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/ColumnDefaultAction.java @@ -0,0 +1,45 @@ +/*- + * #%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.function.Consumer; +import net.sf.jsqlparser.expression.Expression; + +/** + * Shared access to a column SET DEFAULT action on a table or view. Obtain active actions through + * {@link AlterExpression#getColumnDefaults()}; edits update the existing statement model directly. + */ +public interface ColumnDefaultAction { + String getColumnName(); + + void setColumnName(String columnName); + + /** Returns the parsed default, or null for a legacy opaque SQL value. */ + Expression getDefaultExpression(); + + /** Replaces the default expression, discarding any legacy opaque SQL value. */ + void setDefaultExpression(Expression expression); + + /** Returns the default's SQL text, including legacy opaque values when present. */ + default String getDefaultValue() { + Expression expression = getDefaultExpression(); + return expression == null ? null : expression.toString(); + } + + /** Renders the shared value while preserving the caller's expression visitor. */ + default void appendDefaultValueTo(StringBuilder sql, Consumer expressionPrinter) { + Expression expression = getDefaultExpression(); + if (expression == null) { + sql.append(getDefaultValue()); + } else { + expressionPrinter.accept(expression); + } + } +} 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 d00479dc0..a533c47b1 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; import net.sf.jsqlparser.expression.Expression; @@ -17,7 +18,7 @@ import net.sf.jsqlparser.statement.select.PlainSelect; /** Structured PostgreSQL property actions shared by tables, indexes and views. */ -public class RelationAlterAction extends AlterExpression { +public class RelationAlterAction extends AlterExpression implements ColumnDefaultAction { private EnableState enableState; private TriggerTarget triggerTarget; private Kind kind; @@ -168,14 +169,23 @@ public void setStatisticsDefault(boolean statisticsDefault) { } } + @Override public Expression getDefaultExpression() { return defaultExpression; } + @Override public void setDefaultExpression(Expression defaultExpression) { this.defaultExpression = defaultExpression; } + @Override + public List getColumnDefaults() { + return kind == Kind.ALTER_COLUMN && columnAction == ColumnAction.SET_DEFAULT + ? Collections.singletonList(this) + : Collections.emptyList(); + } + public Table getRelation() { return relation; } @@ -315,7 +325,7 @@ private void appendColumnChange(StringBuilder builder, Consumer expr switch (columnAction) { case SET_DEFAULT: builder.append(" SET DEFAULT "); - expressionPrinter.accept(defaultExpression); + appendDefaultValueTo(builder, expressionPrinter); break; case DROP_DEFAULT: builder.append(" DROP DEFAULT"); diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/ColumnDefaultActionTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/ColumnDefaultActionTest.java new file mode 100644 index 000000000..091a752f0 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/ColumnDefaultActionTest.java @@ -0,0 +1,197 @@ +/*- + * #%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.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +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.expression.NullValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnSetDefault; +import net.sf.jsqlparser.util.TableDefinitionTraversal; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +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 ColumnDefaultActionTest { + @ParameterizedTest + @ValueSource(strings = {"TABLE", "VIEW"}) + void commonAccessMutatesTheExistingModel(String objectType) throws JSQLParserException { + Statement statement = parse("ALTER " + objectType + " t ALTER COLUMN a SET DEFAULT 1"); + AlterExpression action = actions(statement).get(0); + ColumnDefaultAction column = action.getColumnDefaults().get(0); + assertEquals("a", column.getColumnName()); + assertEquals("1", column.getDefaultValue()); + assertSame(action instanceof RelationAlterAction ? action + : action.getColumnSetDefaultList().get(0), column); + + Expression replacement = CCJSqlParserUtil.parseExpression("(2 + 3)"); + column.setDefaultExpression(replacement); + column.setColumnName("renamed"); + assertSame(replacement, column.getDefaultExpression()); + assertEquals("(2 + 3)", column.getDefaultValue()); + assertEquals("ALTER " + objectType + " t ALTER COLUMN renamed SET DEFAULT (2 + 3)", + statement.toString()); + roundTrip(statement); + + List expressions = new ArrayList<>(); + TableDefinitionTraversal.visit(action, expressions::add, ignored -> { + }); + assertEquals(List.of(replacement), expressions); + + if (action instanceof RelationAlterAction) { + ((RelationAlterAction) action).setDefaultExpression(new LongValue(4)); + } else { + action.getColumnSetDefaultList().get(0).setDefaultExpression(new LongValue(4)); + } + assertEquals("4", column.getDefaultValue()); + roundTrip(statement); + } + + @ParameterizedTest + @ValueSource(strings = {"TABLE", "VIEW"}) + void sharedRenderingPreservesCustomExpressionVisitors(String objectType) + throws JSQLParserException { + Statement statement = parse("ALTER " + objectType + " t ALTER COLUMN a SET DEFAULT 0"); + actions(statement).get(0).getColumnDefaults().get(0) + .setDefaultExpression(CCJSqlParserUtil.parseExpression("(1 + 2)")); + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 100); + } + }; + statement.accept(new StatementDeParser(expressions, new SelectDeParser(), output), null); + assertEquals("ALTER " + objectType + " t ALTER COLUMN a SET DEFAULT (101 + 102)", + output.toString()); + assertEquals(output.toString(), parse(output.toString()).toString()); + assertEquals("ALTER " + objectType + " t ALTER COLUMN a SET DEFAULT (1 + 2)", + statement.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"TABLE", "VIEW"}) + void quotedNamesAndExplicitSqlNullRoundTrip(String objectType) throws JSQLParserException { + Statement statement = + parse("ALTER " + objectType + " t ALTER COLUMN \"Old Name\" SET DEFAULT 1"); + ColumnDefaultAction column = actions(statement).get(0).getColumnDefaults().get(0); + column.setColumnName("\"New Name\""); + column.setDefaultExpression(new NullValue()); + assertEquals("NULL", column.getDefaultValue()); + assertEquals("ALTER " + objectType + " t ALTER COLUMN \"New Name\" SET DEFAULT NULL", + statement.toString()); + roundTrip(statement); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER TABLE t ADD COLUMN a INT", + "ALTER TABLE t ALTER COLUMN a DROP DEFAULT", "ALTER VIEW v ALTER COLUMN a DROP DEFAULT", + "ALTER VIEW v RENAME COLUMN a TO b", + "ALTER MATERIALIZED VIEW v ALTER COLUMN a SET STATISTICS 100"}) + void unrelatedActionsDoNotAppearAsDefaults(String sql) throws JSQLParserException { + assertTrue(actions(parse(sql)).get(0).getColumnDefaults().isEmpty()); + } + + @Test + void inactiveRelationPropertiesDoNotAppearAsDefaults() { + RelationAlterAction action = new RelationAlterAction(); + action.setColumnName("a"); + action.setDefaultExpression(new LongValue(1)); + action.setColumnAction(RelationAlterAction.ColumnAction.SET_DEFAULT); + assertTrue(action.getColumnDefaults().isEmpty()); + action.setKind(RelationAlterAction.Kind.ALTER_COLUMN); + assertSame(action, action.getColumnDefaults().get(0)); + action.setColumnAction(RelationAlterAction.ColumnAction.DROP_DEFAULT); + assertTrue(action.getColumnDefaults().isEmpty()); + action.setColumnAction(RelationAlterAction.ColumnAction.SET_DEFAULT); + action.setKind(RelationAlterAction.Kind.RENAME_COLUMN); + assertTrue(action.getColumnDefaults().isEmpty()); + } + + @Test + void opaqueLegacyConstructorIsNotParsedOrPassedToAnExpressionVisitor() { + ColumnSetDefault legacy = new ColumnSetDefault("a", "vendor_specific(?)::opaque"); + ColumnDefaultAction column = legacy; + assertNull(column.getDefaultExpression()); + assertEquals("vendor_specific(?)::opaque", column.getDefaultValue()); + StringBuilder sql = new StringBuilder(); + column.appendDefaultValueTo(sql, ignored -> { + throw new AssertionError("Opaque defaults must not enter the expression visitor"); + }); + assertEquals("vendor_specific(?)::opaque", sql.toString()); + column.setColumnName("b"); + assertEquals("b SET DEFAULT vendor_specific(?)::opaque", legacy.toString()); + column.setDefaultExpression(new LongValue(2)); + assertEquals("b SET DEFAULT 2", legacy.toString()); + column.setDefaultExpression(null); + assertNull(column.getDefaultValue()); + assertEquals("b SET DEFAULT null", legacy.toString()); + assertEquals("a SET DEFAULT null", new ColumnSetDefault("a", null).toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"TABLE", "VIEW"}) + void commonLookupDoesNotExposeStructuralMutations(String objectType) + throws JSQLParserException { + Statement statement = parse("ALTER " + objectType + " t ALTER COLUMN a SET DEFAULT 1"); + List defaults = actions(statement).get(0).getColumnDefaults(); + assertThrows(UnsupportedOperationException.class, defaults::clear); + defaults.get(0).setDefaultExpression(new LongValue(2)); + assertEquals("ALTER " + objectType + " t ALTER COLUMN a SET DEFAULT 2", + statement.toString()); + roundTrip(statement); + } + + @Test + void lookupUsesTheExistingTableListWithoutCopyingItsEntries() { + AlterExpression action = new AlterExpression(); + action.setOperation(AlterOperation.ALTER); + ColumnSetDefault first = new ColumnSetDefault("a", "1"); + action.addColSetDefault(first); + List defaults = action.getColumnDefaults(); + ColumnSetDefault second = ColumnSetDefault.fromExpression("b", new LongValue(2)); + action.addColSetDefault(second); + assertEquals(List.of(first, second), defaults); + action.getColumnSetDefaultList().remove(first); + assertEquals(List.of(second), defaults); + action.setOperation(AlterOperation.DROP); + assertTrue(action.getColumnDefaults().isEmpty()); + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.POSTGRESQL)); + } + + private static List actions(Statement statement) { + return statement instanceof Alter ? ((Alter) statement).getAlterExpressions() + : ((AlterRelation) statement).getActions(); + } + + private static void roundTrip(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()); + } +} From b2614e408d133efeecf50e044b8779d8609f13b5 Mon Sep 17 00:00:00 2001 From: minleejae Date: Thu, 1 Oct 2026 22:52:33 +0900 Subject: [PATCH 2/2] fix: omit inactive column defaults from shared access Signed-off-by: minleejae --- .../statement/alter/AlterExpression.java | 1 + .../util/deparser/AlterDeParser.java | 4 +-- src/site/sphinx/usage.rst | 31 +++++++++++++++++++ .../alter/ColumnDefaultActionTest.java | 22 +++++++++++++ 4 files changed, 55 insertions(+), 3 deletions(-) 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 e598a9e5a..a67dcfe1c 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java @@ -426,6 +426,7 @@ public List getColumnSetDefaultList() { */ public List getColumnDefaults() { if (operation != AlterOperation.ALTER || columnSetDefaultList == null + || constraintType != null && constraintSymbol != null || columnDropDefaultList != null && !columnDropDefaultList.isEmpty()) { return Collections.emptyList(); } 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 8c62bf20b..5ef1e7a7e 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java @@ -84,9 +84,7 @@ private void deParseAction(AlterExpression action) { deParseTail(action); return; } - if (action.getOperation() == net.sf.jsqlparser.statement.alter.AlterOperation.ALTER - && action.getColumnSetDefaultList() != null - && !action.getColumnSetDefaultList().isEmpty()) { + if (!action.getColumnDefaults().isEmpty()) { action.appendColumnActionTo(builder, expression -> expression.accept(expressionVisitor, null)); return; diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index a4a8a304f..c8d1c48a6 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -346,6 +346,37 @@ bare ``RESTART``, which uses the sequence's configured start value. Call ``new IdentityAlteration(Kind.RESTART).withRestartWith(20L)``. +Column defaults on tables and views +----------------------------------- + +``AlterExpression.getColumnDefaults()`` exposes active ``SET DEFAULT`` actions +through ``ColumnDefaultAction`` for both tables and views. The returned list is +read-only, but its entries are the original mutable AST objects: + +.. code-block:: java + + for (String objectType : List.of("TABLE", "VIEW")) { + Statement statement = CCJSqlParserUtil.parse( + "ALTER " + objectType + " t ALTER COLUMN a SET DEFAULT 1", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + List actions = statement instanceof Alter + ? ((Alter) statement).getAlterExpressions() + : ((AlterRelation) statement).getActions(); + for (AlterExpression action : actions) { + for (ColumnDefaultAction column : action.getColumnDefaults()) { + column.setDefaultExpression(new LongValue(42)); + } + } + System.out.println(statement); // SET DEFAULT 42 for either object type + } + +``getColumnName()`` and ``setColumnName()`` access the affected column. +``getDefaultExpression()`` exposes parsed expressions; ``getDefaultValue()`` +returns their SQL text. Existing ``ColumnSetDefault`` constructors still accept +opaque strings, for which ``getDefaultExpression()`` returns ``null``. +Replacing an expression also replaces any opaque value. Unrelated actions +return an empty list. + Structured column attributes ============================ diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/ColumnDefaultActionTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/ColumnDefaultActionTest.java index 091a752f0..535606803 100644 --- a/src/test/java/net/sf/jsqlparser/statement/alter/ColumnDefaultActionTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/alter/ColumnDefaultActionTest.java @@ -129,6 +129,28 @@ void inactiveRelationPropertiesDoNotAppearAsDefaults() { assertTrue(action.getColumnDefaults().isEmpty()); } + @Test + void constraintAlterationTakesPrecedenceOverPreviouslySetDefaults() throws JSQLParserException { + Alter statement = + (Alter) CCJSqlParserUtil.parse("ALTER TABLE t ALTER COLUMN a SET DEFAULT 1"); + AlterExpression action = statement.getAlterExpressions().get(0); + ColumnDefaultAction previousDefault = action.getColumnDefaults().get(0); + action.setConstraintType("CHECK"); + action.setConstraintSymbol("c"); + action.setEnforced(true); + + assertTrue(action.getColumnDefaults().isEmpty()); + assertEquals("ALTER TABLE t ALTER CHECK c ENFORCED", statement.toString()); + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(statement.toString(), CCJSqlParserUtil.parse(sql.toString()).toString()); + + action.setConstraintSymbol(null); + assertSame(previousDefault, action.getColumnDefaults().get(0)); + roundTrip(statement); + } + @Test void opaqueLegacyConstructorIsNotParsedOrPassedToAnExpressionVisitor() { ColumnSetDefault legacy = new ColumnSetDefault("a", "vendor_specific(?)::opaque");