diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/DefaultConstraint.java b/src/main/java/net/sf/jsqlparser/statement/create/table/DefaultConstraint.java new file mode 100644 index 000000000..fd6daa82a --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/DefaultConstraint.java @@ -0,0 +1,95 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.create.table; + +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.schema.Column; + +/** A SQL Server table-level DEFAULT expression FOR a column, with an optional constraint name. */ +public class DefaultConstraint extends NamedConstraint { + private Expression expression; + private Column column; + private boolean withValues; + + public DefaultConstraint() { + setType("DEFAULT"); + setKind(Kind.DEFAULT); + } + + public Expression getExpression() { + return expression; + } + + public void setExpression(Expression expression) { + this.expression = expression; + } + + public Column getColumn() { + return column; + } + + public void setColumn(Column column) { + this.column = column; + } + + public boolean isWithValues() { + return withValues; + } + + public void setWithValues(boolean withValues) { + this.withValues = withValues; + } + + public DefaultConstraint withExpression(Expression expression) { + setExpression(expression); + return this; + } + + public DefaultConstraint withColumn(Column column) { + setColumn(column); + return this; + } + + public DefaultConstraint withWithValues(boolean withValues) { + setWithValues(withValues); + return this; + } + + @Override + public DefaultConstraint withName(String name) { + setName(name); + return this; + } + + /** Shares rendering with deparsers while allowing both the value and column to be visited. */ + public void appendTo(StringBuilder builder, Consumer expressionWriter) { + if (expression == null || column == null) { + throw new IllegalStateException("DEFAULT requires an expression and a target column"); + } + if (getName() != null) { + builder.append("CONSTRAINT ").append(getName()).append(' '); + } + builder.append("DEFAULT "); + expressionWriter.accept(expression); + builder.append(" FOR "); + expressionWriter.accept(column); + if (withValues) { + builder.append(" WITH VALUES"); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + appendTo(builder, builder::append); + return builder.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java b/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java index 0d602b5fc..c57ad8389 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java @@ -24,7 +24,7 @@ public class Index implements TableElement, Serializable { public enum Kind { - PRIMARY_KEY, UNIQUE, INDEX, FULLTEXT, SPATIAL, FOREIGN_KEY, CHECK, EXCLUDE, OTHER + PRIMARY_KEY, UNIQUE, INDEX, FULLTEXT, SPATIAL, FOREIGN_KEY, CHECK, EXCLUDE, DEFAULT, OTHER } private final List name = new ArrayList<>(); @@ -196,6 +196,8 @@ public void setType(String string) { kind = Kind.CHECK; } else if (normalized.startsWith("EXCLUDE")) { kind = Kind.EXCLUDE; + } else if (normalized.equals("DEFAULT")) { + kind = Kind.DEFAULT; } else if (normalized.contains("INDEX") || normalized.contains("KEY")) { kind = Kind.INDEX; } diff --git a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java index 9f2e82cd9..0392d731d 100644 --- a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java +++ b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java @@ -19,6 +19,7 @@ import net.sf.jsqlparser.statement.create.table.ColumnDefinition; import net.sf.jsqlparser.statement.create.table.ColumnOption; import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.DefaultConstraint; import net.sf.jsqlparser.statement.create.table.ExcludeConstraint; import net.sf.jsqlparser.statement.create.table.ForeignKeyIndex; import net.sf.jsqlparser.statement.create.table.Index; @@ -75,6 +76,11 @@ public static void visit(TableElement element, Consumer expressions, if (index instanceof CheckConstraint) { accept(((CheckConstraint) index).getExpression(), expressions); } + if (index instanceof DefaultConstraint) { + DefaultConstraint constraint = (DefaultConstraint) index; + accept(constraint.getExpression(), expressions); + accept(constraint.getColumn(), expressions); + } if (index instanceof ExcludeConstraint) { accept(((ExcludeConstraint) index).getExpression(), expressions); } 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 7b2b1560f..2cd0507cb 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java @@ -12,6 +12,7 @@ import net.sf.jsqlparser.statement.alter.Alter; import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.statement.alter.AlterExpression; +import net.sf.jsqlparser.statement.create.table.DefaultConstraint; import net.sf.jsqlparser.statement.select.PlainSelect; import java.util.Iterator; @@ -47,6 +48,12 @@ public void deParse(Alter alter) { } private void deParseAction(AlterExpression action) { + if (action.getIndex() instanceof DefaultConstraint) { + builder.append(action.getOperation()).append(' '); + new TableElementDeParser(builder, expressionVisitor).deParse(action.getIndex()); + deParseTail(action); + return; + } if (action.getColDataTypeList() == null || action.getColDataTypeList().size() != 1 || action.getColDataTypeList().get(0).getUsingExpression() == null) { builder.append(action); @@ -63,10 +70,17 @@ private void deParseAction(AlterExpression action) { builder.append(column.getColumnName()).append(column.isWithType() ? " TYPE " : " ") .append(column.toStringDataTypeAndSpec()).append(" USING "); column.getUsingExpression().accept(expressionVisitor, null); + deParseTail(action); + } + + private void deParseTail(AlterExpression action) { if (action.getParameters() != null && !action.getParameters().isEmpty()) { builder.append(' ') .append(PlainSelect.getStringList(action.getParameters(), false, false)); } + if (action.getIndex() != null && action.getIndex().getCommentText() != null) { + builder.append(" COMMENT ").append(action.getIndex().getCommentText()); + } } } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/TableElementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/TableElementDeParser.java index 62c7f98c6..709997ed3 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/TableElementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/TableElementDeParser.java @@ -14,6 +14,7 @@ import net.sf.jsqlparser.statement.create.table.CheckConstraint; import net.sf.jsqlparser.statement.create.table.ColumnDefinition; import net.sf.jsqlparser.statement.create.table.ColumnOption; +import net.sf.jsqlparser.statement.create.table.DefaultConstraint; import net.sf.jsqlparser.statement.create.table.ExcludeConstraint; import net.sf.jsqlparser.statement.create.table.Index; import net.sf.jsqlparser.statement.create.table.TableElement; @@ -30,7 +31,10 @@ public TableElementDeParser(StringBuilder builder, @Override public void deParse(TableElement element) { - if (element instanceof ExcludeConstraint) { + if (element instanceof DefaultConstraint) { + ((DefaultConstraint) element).appendTo(builder, + expression -> expression.accept(expressionVisitor, null)); + } else if (element instanceof ExcludeConstraint) { deParseExclude((ExcludeConstraint) element); } else if (element instanceof CheckConstraint) { deParseCheck((CheckConstraint) element); 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 87b990578..20f1d8024 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 @@ -19,6 +19,7 @@ import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnDropNotNull; import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnSetNotNull; import net.sf.jsqlparser.statement.alter.AlterOperation; +import net.sf.jsqlparser.statement.create.table.DefaultConstraint; import net.sf.jsqlparser.util.TableDefinitionTraversal; import net.sf.jsqlparser.util.validation.ValidationCapability; import net.sf.jsqlparser.util.validation.ValidationUtil; @@ -87,7 +88,10 @@ public void validate(Alter alter, AlterExpression e) { validateOptionalColumnNames(c, e.getUkColumns(), NamedObject.uniqueConstraint); } - if (e.getIndex() != null) { + if (e.getIndex() instanceof DefaultConstraint) { + validateOptionalName(c, NamedObject.constraint, e.getIndex().getName(), null, false, + NamedObject.table); + } else if (e.getIndex() != null) { validateName(c, NamedObject.index, e.getIndex().getName()); if (e.getIndex().getColumns() != null) { validateOptionalColumnNames(c, diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index ebb26f492..f723c635b 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -14137,6 +14137,23 @@ AlterExpression AlterExpressionDiscardOrImport(): } +/** + * Parses SQL Server's [CONSTRAINT name] DEFAULT expression FOR column [WITH VALUES]. + */ +DefaultConstraint DefaultConstraintSpec(): +{ + DefaultConstraint constraint = new DefaultConstraint(); + String name; + Expression expression; +} +{ + [ name=RelObjectName() { constraint.setName(name); } ] + expression=Expression() { constraint.setExpression(expression); } + name=RelObjectName() { constraint.setColumn(new Column(name)); } + [ { constraint.setWithValues(true); } ] + { return constraint; } +} + /** * Parses ADD/ALTER CONSTRAINT clause within AlterExpression. * Handles: CONSTRAINT [UNIQUE [KEY|INDEX]] name columns @@ -14422,6 +14439,13 @@ AlterExpression AlterExpressionAddAlterModify(): ) ( + LOOKAHEAD({ getToken(1).kind == K_DEFAULT || getToken(1).kind == K_CONSTRAINT + && getToken(3).kind == K_DEFAULT }) index=DefaultConstraintSpec() { + requireDdlSyntax(alterExp.getOperation() == AlterOperation.ADD, + "Table-level DEFAULT constraints require ADD"); + alterExp.setIndex(index); + } + | LOOKAHEAD({ alterExp.getOperation() == AlterOperation.ALTER && getToken(1).kind == K_INDEX }) tk= sk3=RelObjectName() IndexOptionList(indexSpec) { diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/SqlServerDefaultConstraintTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/SqlServerDefaultConstraintTest.java new file mode 100644 index 000000000..3516b92f3 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/SqlServerDefaultConstraintTest.java @@ -0,0 +1,164 @@ +/*- + * #%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 java.util.Set; +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.Column; +import net.sf.jsqlparser.statement.create.table.DefaultConstraint; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.AlterDeParser; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.validation.validator.AlterValidator; +import net.sf.jsqlparser.util.validation.ValidationContext; +import net.sf.jsqlparser.util.validation.metadata.DatabaseMetaDataValidation; +import net.sf.jsqlparser.util.validation.metadata.Named; +import net.sf.jsqlparser.util.validation.metadata.NamedObject; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class SqlServerDefaultConstraintTest { + @ParameterizedTest + @ValueSource(strings = { + "ALTER TABLE dbo.virtual_production_record ADD CONSTRAINT DF_virtual_production_record__d DEFAULT ((0)) FOR _d", + "ALTER TABLE [dbo].[t] ADD CONSTRAINT [DF_value] DEFAULT ((0)) FOR [value] WITH VALUES", + "ALTER TABLE t ADD DEFAULT 0 FOR c", + "ALTER TABLE t ADD DEFAULT NULL FOR c", + "ALTER TABLE t ADD DEFAULT -1 FOR c WITH VALUES", + "ALTER TABLE t ADD DEFAULT N'it''s fine' FOR c", + "ALTER TABLE t ADD CONSTRAINT df DEFAULT (GETDATE()) FOR created_at", + "ALTER TABLE t ADD DEFAULT (NEXT VALUE FOR dbo.seq) FOR id", + "ALTER TABLE t ADD DEFAULT (1 + 2) FOR c", + "ALTER TABLE t ADD CONSTRAINT df DEFAULT CAST(0 AS INT) FOR c", + "ALTER TABLE t ADD DEFAULT 0 FOR a, ADD DEFAULT 1 FOR b", + "ALTER TABLE t ADD DEFAULT 0 FOR c, ADD CONSTRAINT ck CHECK (c >= 0)" + }) + void roundTrip(String sql) throws JSQLParserException { + Alter alter = (Alter) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true, + parser -> parser.withSquareBracketQuotation(true)); + assertEquals(alter.toString(), parse(sql).toString()); + assertInstanceOf(DefaultConstraint.class, alter.getAlterExpressions().get(0).getIndex()); + } + + @ParameterizedTest + @ValueSource(strings = { + "ALTER TABLE t ADD DEFAULT FOR c", + "ALTER TABLE t ADD DEFAULT 0", + "ALTER TABLE t ADD CONSTRAINT df DEFAULT 0 FOR", + "ALTER TABLE t ADD CONSTRAINT DEFAULT 0 FOR c", + "ALTER TABLE t ADD DEFAULT 0 FOR c WITH", + "ALTER TABLE t ADD DEFAULT 0 FOR c WITH VALUES WITH VALUES", + "ALTER TABLE t ALTER CONSTRAINT df DEFAULT 0 FOR c", + "ALTER TABLE t MODIFY DEFAULT 0 FOR c" + }) + void rejectMalformedSyntax(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + @Test + void inspectAndModifyStructuredConstraint() throws JSQLParserException { + Alter alter = parse("ALTER TABLE t ADD CONSTRAINT df DEFAULT 0 FOR c WITH VALUES"); + AlterExpression action = alter.getAlterExpressions().get(0); + DefaultConstraint constraint = (DefaultConstraint) action.getIndex(); + assertEquals(AlterOperation.ADD, action.getOperation()); + assertEquals(Index.Kind.DEFAULT, constraint.getKind()); + assertEquals("DEFAULT", constraint.getType()); + assertEquals("df", constraint.getName()); + assertEquals("c", constraint.getColumn().getColumnName()); + assertEquals(0, ((LongValue) constraint.getExpression()).getValue()); + assertTrue(constraint.isWithValues()); + constraint.withName((String) null).withExpression(new LongValue(2)) + .withColumn(new Column("other_column")).withWithValues(false); + assertEquals("ALTER TABLE t ADD DEFAULT 2 FOR other_column", alter.toString()); + TestUtils.assertSqlCanBeParsedAndDeparsed(alter.toString()); + } + + @Test + void deparserVisitsDefaultAndTargetColumn() throws JSQLParserException { + Alter alter = parse("ALTER TABLE t ADD CONSTRAINT df DEFAULT ((0)) FOR c WITH VALUES"); + StringBuilder sql = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append('1'); + } + + @Override + public StringBuilder visit(Column column, S context) { + return getBuilder().append("new_").append(column.getColumnName()); + } + }; + expressions.setBuilder(sql); + new AlterDeParser(sql, expressions).deParse(alter); + assertEquals("ALTER TABLE t ADD CONSTRAINT df DEFAULT ((1)) FOR new_c WITH VALUES", + sql.toString()); + } + + @Test + void validationTraversesStructuredValueAndColumn() throws JSQLParserException { + Alter alter = parse("ALTER TABLE dbo.t ADD DEFAULT (GETDATE()) FOR created_at"); + List visited = new ArrayList<>(); + AlterValidator validator = new AlterValidator() { + @Override + public void validateOptionalExpression(Expression expression) { + visited.add(expression.toString()); + } + }; + validator.setContext(new ValidationContext().setCapabilities(List.of())); + validator.validate(alter); + assertEquals(List.of("(GETDATE())", "created_at"), visited); + assertEquals(Set.of("dbo.t"), new TablesNamesFinder().getTables(alter)); + } + + @Test + void existingColumnDefaultsAndCreateTableRemainSupported() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE t (c INT DEFAULT ((0)), CONSTRAINT pk PRIMARY KEY CLUSTERED (c))"); + TestUtils.assertSqlCanBeParsedAndDeparsed("ALTER TABLE t ALTER COLUMN c SET DEFAULT 0"); + TestUtils.assertSqlCanBeParsedAndDeparsed("ALTER TABLE t ALTER COLUMN c DROP DEFAULT"); + assertEquals(Index.Kind.DEFAULT, new Index().withType("DEFAULT").getKind()); + } + + @ParameterizedTest + @ValueSource(strings = {"", "CONSTRAINT df "}) + void validateOptionalConstraintNameAsNewConstraint(String prefix) throws JSQLParserException { + List visited = new ArrayList<>(); + DatabaseMetaDataValidation metadata = named -> { + visited.add(named); + return named.getNamedObject() != NamedObject.constraint; + }; + AlterValidator validator = new AlterValidator(); + validator.setContext(new ValidationContext().setCapabilities(List.of(metadata))); + validator.validate(parse("ALTER TABLE t ADD " + prefix + "DEFAULT 0 FOR c")); + assertTrue(validator.getValidationErrors().isEmpty()); + assertTrue(visited.stream().anyMatch(n -> n.getNamedObject() == NamedObject.column + && n.getFqn().equals("c"))); + assertEquals(prefix.isEmpty() ? 0 : 1, + visited.stream().filter(n -> n.getNamedObject() == NamedObject.constraint).count()); + assertTrue(visited.stream().noneMatch(n -> n.getNamedObject() == NamedObject.index)); + } + + private Alter parse(String sql) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.SQLSERVER).withSquareBracketQuotation(true) + .withUnsupportedStatements(false)); + } +}