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 bd9ca3e2f..6e22f6eb7 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java @@ -18,6 +18,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; import java.util.stream.Collectors; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.statement.ReferentialAction; @@ -883,6 +884,16 @@ protected void toStringConstraintAlter(StringBuilder b) { } protected void toStringAlterColumn(StringBuilder b) { + appendAlterColumn(b, b::append); + } + + /** Renders ALTER COLUMN default/visibility actions with their common tail. */ + public void appendColumnActionTo(StringBuilder b, Consumer expressionPrinter) { + appendAlterColumn(b, expressionPrinter); + appendCommonTail(b); + } + + private void appendAlterColumn(StringBuilder b, Consumer expressionPrinter) { b.append("ALTER "); if (hasColumn) { b.append("COLUMN "); @@ -890,7 +901,12 @@ protected void toStringAlterColumn(StringBuilder b) { if (columnDropDefaultList != null && !columnDropDefaultList.isEmpty()) { b.append(PlainSelect.getStringList(columnDropDefaultList)); } else if (columnSetDefaultList != null && !columnSetDefaultList.isEmpty()) { - b.append(PlainSelect.getStringList(columnSetDefaultList)); + for (int i = 0; i < columnSetDefaultList.size(); i++) { + if (i > 0) { + b.append(", "); + } + columnSetDefaultList.get(i).appendTo(b, expressionPrinter); + } } else { b.append(PlainSelect.getStringList(columnSetVisibilityList)); } @@ -1575,7 +1591,8 @@ public String toString() { public static final class ColumnSetDefault implements Serializable { private final String columnName; - private final String defaultValue; + private String defaultValue; + private Expression defaultExpression; public ColumnSetDefault(String columnName, String defaultValue) { this.columnName = columnName; @@ -1586,13 +1603,40 @@ public String getColumnName() { return 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); + result.setDefaultExpression(expression); + return result; + } + + public Expression getDefaultExpression() { + return defaultExpression; + } + + public void setDefaultExpression(Expression defaultExpression) { + this.defaultExpression = defaultExpression; + this.defaultValue = null; + } + public String getDefaultValue() { - return defaultValue; + 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); + } } @Override public String toString() { - return columnName + " SET DEFAULT " + defaultValue; + StringBuilder sql = new StringBuilder(); + appendTo(sql, sql::append); + return sql.toString(); } } diff --git a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java index aabd31ee3..7c637744c 100644 --- a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java +++ b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java @@ -44,6 +44,10 @@ 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.getColumnSetDefaultList() != null) { + action.getColumnSetDefaultList() + .forEach(column -> accept(column.getDefaultExpression(), expressions)); + } if (action.getColDataTypeList() != null) { action.getColDataTypeList().forEach(column -> visit(column, expressions, tables)); } 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 6922525d9..aa71e37a8 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java @@ -60,6 +60,13 @@ private void deParseAction(AlterExpression action) { deParseTail(action); return; } + if (action.getOperation() == net.sf.jsqlparser.statement.alter.AlterOperation.ALTER + && action.getColumnSetDefaultList() != null + && !action.getColumnSetDefaultList().isEmpty()) { + action.appendColumnActionTo(builder, + expression -> expression.accept(expressionVisitor, null)); + return; + } if (action.getColDataTypeList() == null || action.getColDataTypeList().size() != 1 || action.getColDataTypeList().get(0).getUsingExpression() == null) { builder.append(action); diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/AlterValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/AlterValidator.java index 20f1d8024..7702630e0 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/AlterValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/AlterValidator.java @@ -40,14 +40,8 @@ public void validate(Alter alter) { } public void validate(Alter alter, AlterExpression e) { - if (e.getColDataTypeList() != null) { - e.getColDataTypeList().forEach(column -> TableDefinitionTraversal.visit(column, - this::validateOptionalExpression, this::validateOptionalFromItem)); - } - if (e.getIndex() != null) { - TableDefinitionTraversal.visit(e.getIndex(), this::validateOptionalExpression, - this::validateOptionalFromItem); - } + TableDefinitionTraversal.visit(e, this::validateOptionalExpression, + this::validateOptionalFromItem); for (ValidationCapability c : getCapabilities()) { validateOptionalColumnName(c, e.getColumnOldName()); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 52afbaddf..e74d6eee1 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -14552,7 +14552,7 @@ AlterExpression.ColumnSetDefault AlterExpressionColumnSetDefault(): { columnName = RelObjectName() defaultValue = Expression() { - return new AlterExpression.ColumnSetDefault(columnName, defaultValue.toString()); + return AlterExpression.ColumnSetDefault.fromExpression(columnName, defaultValue); } } diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterColumnDefaultExpressionTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterColumnDefaultExpressionTest.java new file mode 100644 index 000000000..07cbcc897 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterColumnDefaultExpressionTest.java @@ -0,0 +1,101 @@ +/*- + * #%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.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnSetDefault; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +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 AlterColumnDefaultExpressionTest { + @ParameterizedTest + @ValueSource(strings = {"(1 + 2)", "NULL", "CURRENT_TIMESTAMP", + "nextval('app.counter'::regclass)", "'value'::text", "-1"}) + void parsedDefaultRetainsExpressionAndLegacyText(String value) throws JSQLParserException { + Alter alter = parse(value); + ColumnSetDefault column = + alter.getAlterExpressions().get(0).getColumnSetDefaultList().get(0); + assertNotNull(column.getDefaultExpression()); + assertEquals(column.getDefaultExpression().toString(), column.getDefaultValue()); + StringBuilder buffer = new StringBuilder(); + alter.accept(new StatementDeParser(buffer), null); + assertEquals(alter.toString(), buffer.toString()); + assertEquals(alter.toString(), CCJSqlParserUtil.parse(buffer.toString()).toString()); + } + + @Test + void visitorReachesDefaultsInEachAlterActionWithContext() throws JSQLParserException { + List values = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(LongValue value, S context) { + assertEquals("context", context); + values.add(value.getValue()); + return null; + } + }; + CCJSqlParserUtil.parse("ALTER TABLE t ALTER COLUMN a SET DEFAULT (1 + 2), " + + "ALTER COLUMN b SET DEFAULT 3") + .accept(new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)), + "context"); + assertThat(values).containsExactly(1L, 2L, 3L); + } + + @Test + void customDeparserAndAstEditsUseTheStructuredDefault() throws JSQLParserException { + Alter alter = parse("(1 + 2)"); + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 100); + } + }; + alter.accept(new StatementDeParser(expressions, new SelectDeParser(), output), null); + assertEquals("ALTER TABLE t ALTER COLUMN a SET DEFAULT (101 + 102)", output.toString()); + ColumnSetDefault column = + alter.getAlterExpressions().get(0).getColumnSetDefaultList().get(0); + column.setDefaultExpression(new LongValue(42)); + assertEquals("42", column.getDefaultValue()); + assertEquals("ALTER TABLE t ALTER COLUMN a SET DEFAULT 42", alter.toString()); + } + + @Test + void legacyStringConstructorRemainsOpaqueAndAcceptsNull() { + ColumnSetDefault column = new ColumnSetDefault("a", "vendor_default()"); + assertNull(column.getDefaultExpression()); + assertEquals("a SET DEFAULT vendor_default()", column.toString()); + assertEquals("a SET DEFAULT null", new ColumnSetDefault("a", null).toString()); + column.setDefaultExpression(new LongValue(1)); + assertEquals("1", column.getDefaultValue()); + column.setDefaultExpression(null); + assertNull(column.getDefaultValue()); + } + + private static Alter parse(String value) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse("ALTER TABLE t ALTER COLUMN a SET DEFAULT " + value); + } +}