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 6e22f6eb7..4de53927b 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java @@ -1115,9 +1115,19 @@ protected void toStringPartition(StringBuilder b) { * Handles the general case for ADD, MODIFY, CHANGE, DROP (column), COMMENT, row-level security, * and all field-based dispatch (columns, constraints, FK, UK, PK, index). */ + protected void toStringGeneral(StringBuilder b) { + toStringGeneral(b, b::append); + } + + /** Appends a column-definition action, including its common tail. */ + public void appendColumnDefinitionsTo(StringBuilder b, Consumer expressionPrinter) { + toStringGeneral(b, column -> column.appendTo(b, expressionPrinter)); + appendCommonTail(b); + } + @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity", "PMD.ExcessiveMethodLength"}) - protected void toStringGeneral(StringBuilder b) { + private void toStringGeneral(StringBuilder b, Consumer columnPrinter) { if (operation == AlterOperation.COMMENT_WITH_EQUAL_SIGN) { b.append("COMMENT =").append(" "); } else if (operation == AlterOperation.ENABLE_ROW_LEVEL_SECURITY) { @@ -1170,7 +1180,12 @@ protected void toStringGeneral(StringBuilder b) { if (useBrackets && colDataTypeList.size() == 1) { b.append(" ( "); } - b.append(PlainSelect.getStringList(colDataTypeList)); + for (int i = 0; i < colDataTypeList.size(); i++) { + if (i > 0) { + b.append(", "); + } + columnPrinter.accept(colDataTypeList.get(i)); + } if (useBrackets && colDataTypeList.size() == 1) { b.append(" ) "); } @@ -1490,13 +1505,25 @@ public ColumnDataType( @Override public String toString() { + StringBuilder builder = new StringBuilder(); + appendTo(builder, builder::append); + return builder.toString(); + } + + @Override + public void appendTo(StringBuilder builder, Consumer expressionPrinter) { + builder.append(getColumnName()); if (identityAlterations != null) { - return getColumnName() + " " - + PlainSelect.getStringList(identityAlterations, false, false); + builder.append(' ') + .append(PlainSelect.getStringList(identityAlterations, false, false)); + return; + } + builder.append(withType ? " TYPE " : getColDataType() == null ? "" : " "); + appendDataTypeAndSpecTo(builder, expressionPrinter); + if (usingExpression != null) { + builder.append(" USING "); + expressionPrinter.accept(usingExpression); } - return getColumnName() + (withType ? " TYPE " : getColDataType() == null ? "" : " ") - + toStringDataTypeAndSpec() - + (usingExpression == null ? "" : " USING " + usingExpression); } @Override diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnDefinition.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnDefinition.java index cc0f7e794..25f3de620 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnDefinition.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnDefinition.java @@ -11,6 +11,7 @@ import net.sf.jsqlparser.statement.imprt.ImportColumn; import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.expression.Expression; import java.io.Serializable; import java.util.ArrayList; @@ -19,6 +20,7 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.function.Consumer; /** * Globally used definition class for columns. @@ -54,7 +56,7 @@ public ColumnDefinition(String columnName, ColDataType colDataType, List /** * Returns raw specifications, or a token snapshot when structured options are present. Use the * option API or {@link #addColumnSpecs(Collection)} to append without discarding structured - * references and constraints. + * expressions, references and constraints. DEFAULT values use the expression's SQL rendering. */ public List getColumnSpecs() { if (columnOptions != null) { @@ -73,7 +75,7 @@ public void setColumnSpecs(List list) { } /** - * Returns column options in source order, including structured references and MySQL + * Returns column options in source order, including structured defaults, references and MySQL * {@code SERIAL DEFAULT VALUE}. */ public List getColumnOptions() { @@ -134,17 +136,42 @@ public void setColumnName(String string) { @Override public String toString() { - return (columnName + " " + toStringDataTypeAndSpec()).trim(); + StringBuilder builder = new StringBuilder(); + appendTo(builder, builder::append); + return builder.toString().trim(); + } + + /** Appends a column definition using the supplied printer for structured expressions. */ + public void appendTo(StringBuilder builder, Consumer expressionPrinter) { + builder.append(columnName); + if (colDataType != null || withOptions) { + builder.append(' '); + } + appendDataTypeAndSpecTo(builder, expressionPrinter); } public String toStringDataTypeAndSpec() { - return (colDataType == null ? "" : colDataType) - + (withOptions ? "WITH OPTIONS" : "") - + (columnOptions != null && !columnOptions.isEmpty() - ? " " + PlainSelect.getStringList(columnOptions, false, false) - : columnSpecs != null && !columnSpecs.isEmpty() - ? " " + PlainSelect.getStringList(columnSpecs, false, false) - : ""); + StringBuilder builder = new StringBuilder(); + appendDataTypeAndSpecTo(builder, builder::append); + return builder.toString(); + } + + protected void appendDataTypeAndSpecTo(StringBuilder builder, + Consumer expressionPrinter) { + if (colDataType != null) { + builder.append(colDataType); + } + if (withOptions) { + builder.append("WITH OPTIONS"); + } + if (columnOptions != null) { + for (ColumnOption option : columnOptions) { + builder.append(' '); + option.appendTo(builder, expressionPrinter); + } + } else if (columnSpecs != null && !columnSpecs.isEmpty()) { + builder.append(' ').append(PlainSelect.getStringList(columnSpecs, false, false)); + } } public ColumnDefinition withColumnName(String columnName) { diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnOption.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnOption.java index ccd3c6c4d..563b099dd 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnOption.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnOption.java @@ -13,13 +13,16 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.statement.select.PlainSelect; /** A structured option following a column data type. */ public class ColumnOption implements Serializable { public enum Kind { - SERIAL_DEFAULT_VALUE, REFERENCE, IDENTITY, CONSTRAINT, OTHER + SERIAL_DEFAULT_VALUE, REFERENCE, IDENTITY, CONSTRAINT, DEFAULT, OTHER } private Kind kind = Kind.OTHER; @@ -27,6 +30,24 @@ public enum Kind { private ForeignKeyReference foreignKeyReference; private IdentityDefinition identityDefinition; private Index constraint; + private Expression defaultExpression; + + /** Creates a DEFAULT option. Use a NullValue expression for SQL NULL. */ + public static ColumnOption defaultValue(Expression expression) { + ColumnOption option = new ColumnOption(); + option.kind = Kind.DEFAULT; + option.setDefaultExpression(expression); + return option; + } + + public Expression getDefaultExpression() { + return defaultExpression; + } + + /** Replaces the expression of a DEFAULT option created by {@link #defaultValue(Expression)}. */ + public void setDefaultExpression(Expression expression) { + defaultExpression = Objects.requireNonNull(expression, "defaultExpression"); + } public static ColumnOption identity(IdentityDefinition definition) { ColumnOption option = new ColumnOption(); @@ -78,6 +99,9 @@ public Kind getKind() { } public List getTokens() { + if (kind == Kind.DEFAULT) { + return Arrays.asList("DEFAULT", String.valueOf(defaultExpression)); + } return kind == Kind.OTHER || kind == Kind.SERIAL_DEFAULT_VALUE ? tokens : Collections.singletonList(toString()); } @@ -88,15 +112,30 @@ public ForeignKeyReference getForeignKeyReference() { @Override public String toString() { + StringBuilder builder = new StringBuilder(); + appendTo(builder, builder::append); + return builder.toString(); + } + + /** Appends the option using the supplied printer for structured expressions. */ + public void appendTo(StringBuilder builder, Consumer expressionPrinter) { switch (kind) { + case DEFAULT: + builder.append("DEFAULT "); + expressionPrinter.accept(defaultExpression); + break; case REFERENCE: - return foreignKeyReference.toString(); + builder.append(foreignKeyReference); + break; case IDENTITY: - return identityDefinition.toString(); + builder.append(identityDefinition); + break; case CONSTRAINT: - return constraint.toString(); + constraint.appendTo(builder, expressionPrinter); + break; default: - return PlainSelect.getStringList(tokens, false, false); + builder.append(PlainSelect.getStringList(tokens, false, false)); + break; } } } diff --git a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java index 9bc42b9a6..1b0e76b15 100644 --- a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java +++ b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java @@ -133,6 +133,7 @@ public static void visit(TableElement element, Consumer expressions, ColumnDefinition column = (ColumnDefinition) element; if (column.getColumnOptions() != null) { for (ColumnOption option : column.getColumnOptions()) { + accept(option.getDefaultExpression(), expressions); if (option.getForeignKeyReference() != null) { accept(option.getForeignKeyReference().getTable(), 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 e384b3e99..e162f0ec8 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java @@ -81,23 +81,12 @@ private void deParseAction(AlterExpression action) { expression -> expression.accept(expressionVisitor, null)); return; } - if (action.getColDataTypeList() == null || action.getColDataTypeList().size() != 1 - || action.getColDataTypeList().get(0).getUsingExpression() == null) { + if (action.getColDataTypeList() != null) { + action.appendColumnDefinitionsTo(builder, + expression -> expression.accept(expressionVisitor, null)); + } else { builder.append(action); - return; - } - AlterExpression.ColumnDataType column = action.getColDataTypeList().get(0); - builder.append(action.getOperation()).append(' '); - if (action.hasColumn()) { - builder.append("COLUMN "); - } - if (action.isUsingIfExists()) { - builder.append("IF EXISTS "); } - 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) { 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 3fc899998..d0f386034 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/TableElementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/TableElementDeParser.java @@ -11,7 +11,6 @@ import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.statement.create.table.ColumnDefinition; -import net.sf.jsqlparser.statement.create.table.ColumnOption; import net.sf.jsqlparser.statement.create.table.Index; import net.sf.jsqlparser.statement.create.table.TableElement; @@ -30,30 +29,12 @@ public void deParse(TableElement element) { if (element instanceof Index) { ((Index) element).appendTo(builder, expression -> expression.accept(expressionVisitor, null)); - } else if (element instanceof ColumnDefinition - && ((ColumnDefinition) element).getColumnOptions() != null) { - deParseColumn((ColumnDefinition) element); + } else if (element instanceof ColumnDefinition) { + ((ColumnDefinition) element).appendTo(builder, + expression -> expression.accept(expressionVisitor, null)); } else { builder.append(element); } } - private void deParseColumn(ColumnDefinition column) { - builder.append(column.getColumnName()); - if (column.getColDataType() != null) { - builder.append(' ').append(column.getColDataType()); - } - if (column.isWithOptions()) { - builder.append(" WITH OPTIONS"); - } - for (ColumnOption option : column.getColumnOptions()) { - builder.append(' '); - if (option.getConstraint() != null) { - deParse(option.getConstraint()); - } else { - builder.append(option); - } - } - } - } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 92f99568d..92b5fd0ba 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1702,8 +1702,10 @@ public class CCJSqlParser extends AbstractJSqlParser { case K_MATCH_PHRASE: // MATCH_PHRASE case K_MATCH_PHRASE_PREFIX: // MATCH_PHRASE_PREFIX case K_MATCH_REGEXP: // MATCH_REGEXP - case K_NOT: // NOT IN / NOT BETWEEN / NOT LIKE / NOT ISNULL / NOT SIMILAR return true; + case K_NOT: // NOT IN / NOT BETWEEN / NOT LIKE / NOT ISNULL / NOT SIMILAR + // A column's NOT NULL constraint starts after its DEFAULT expression. + return getToken(2).kind != K_NULL; // Oracle (+) before IN: col(+) IN (...) case OPENING_BRACKET: return getToken(2).image.equals("+"); @@ -12566,6 +12568,7 @@ ColumnOption ColumnDefinitionOption(): { ColumnOption option; IdentityDefinition identity; NamedConstraint constraint; + Expression defaultExpression; } { ( LOOKAHEAD({ isKeywordAhead("GENERATED") @@ -12583,6 +12586,17 @@ ColumnOption ColumnDefinitionOption(): { LOOKAHEAD() reference=ForeignKeyReferenceSpec() { option = ColumnOption.reference(reference); } | + LOOKAHEAD( ) + { option = ColumnOption.raw("DROP", "DEFAULT"); } + | + LOOKAHEAD( ) + { option = ColumnOption.raw("DEFAULT", "ON", "NULL"); } + | + LOOKAHEAD() { option = ColumnOption.raw("DEFAULT"); } + // Db2 permits an omitted default value. Keep these implicit defaults raw. + [ LOOKAHEAD(2, { !(getToken(1).kind == K_NOT && getToken(2).kind == K_NULL) }) + defaultExpression=Expression() { option = ColumnOption.defaultValue(defaultExpression); } ] + | parameter=ColumnDefinitionParameter() { option = ColumnOption.raw(parameter); } ) diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java index d26451fdb..b6446de06 100644 --- a/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java @@ -971,7 +971,7 @@ public void testAlterTableDefaultValueTrueIssue926() throws JSQLParserException // There shall be no COLUMN where there is no COLUMN assertStatementCanBeDeparsedAs(parsed, - "ALTER TABLE my_table ADD some_column BOOLEAN DEFAULT FALSE"); + "ALTER TABLE my_table ADD some_column BOOLEAN DEFAULT false"); } private void assertReferentialActionOnConstraint(Alter parsed, Action onUpdate, diff --git a/src/test/java/net/sf/jsqlparser/statement/create/ColumnDefaultExpressionTest.java b/src/test/java/net/sf/jsqlparser/statement/create/ColumnDefaultExpressionTest.java new file mode 100644 index 000000000..c99e6a11e --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/ColumnDefaultExpressionTest.java @@ -0,0 +1,203 @@ +/*- + * #%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; + +import static net.sf.jsqlparser.util.validation.ValidationTestAsserts.validateNotAllowed; +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 static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.alter.Alter; +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.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 ColumnDefaultExpressionTest { + @ParameterizedTest + @ValueSource(strings = {"1", "-1", "NULL", "true", "CURRENT_TIMESTAMP(6)", + "nextval('app.counter'::regclass)", "'value'::character varying", "ARRAY[1, 2]::int[]", + "(1 + 2 * 3)", "(UUID_TO_BIN(UUID()))", "(JSON_ARRAY())", + "(CURRENT_DATE + INTERVAL 1 YEAR)", "CASE WHEN 1 = 1 THEN 2 ELSE 3 END", + "1 > 0", "1 NOT IN (2, 3)"}) + void createAndAlterKeepDefaultsSeparateFromFollowingConstraints(String value) + throws JSQLParserException { + Expression expected = CCJSqlParserUtil.parseExpression(value); + for (String prefix : Arrays.asList("CREATE TABLE t (a INT DEFAULT ", + "ALTER TABLE t ADD COLUMN a INT DEFAULT ", + "ALTER TABLE t MODIFY COLUMN a INT DEFAULT ")) { + Statement statement = CCJSqlParserUtil.parse(prefix + value + " NOT NULL" + + (prefix.startsWith("CREATE") ? ", b INT)" : ", ADD COLUMN b INT")); + ColumnDefinition column = firstColumn(statement); + ColumnOption option = defaultOption(column); + assertEquals(expected.getClass(), option.getDefaultExpression().getClass()); + assertEquals(expected.toString(), option.getDefaultExpression().toString()); + assertThat(column.getColumnSpecs()).containsExactly("DEFAULT", expected.toString(), + "NOT", "NULL"); + assertRoundTrip(statement); + } + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE TABLE t (a INT DEFAULT (1 + 2), b INT DEFAULT 3)", + "ALTER TABLE t ADD (a INT DEFAULT (1 + 2), b INT DEFAULT 3)", + "ALTER TABLE t MODIFY (a INT DEFAULT (1 + 2), b INT DEFAULT 3)", + "ALTER TABLE t ADD a INT DEFAULT (1 + 2), MODIFY b INT DEFAULT 3"}) + void visitorReachesEachDefaultOnceWithContext(String sql) 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(sql) + .accept(new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)), + "context"); + assertThat(values).containsExactly(1L, 2L, 3L); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE TABLE t (a INT DEFAULT (1 + 2) NOT NULL)", + "CREATE TABLE t OF row_type (PRIMARY KEY (a), a WITH OPTIONS DEFAULT (1 + 2))", + "ALTER TABLE t ADD COLUMN IF NOT EXISTS a INT DEFAULT (1 + 2)", + "ALTER TABLE t ADD (a INT DEFAULT 1, b INT DEFAULT 2)", + "ALTER TABLE t MODIFY (a INT DEFAULT 1, b INT DEFAULT 2)", + "ALTER TABLE t MODIFY (a INT DEFAULT (1 + 2))", + "ALTER TABLE t CHANGE COLUMN old_a a INT DEFAULT (1 + 2) AFTER id", + "ALTER TABLE t MODIFY COLUMN a INT DEFAULT (1 + 2) FIRST", + "ALTER TABLE t ALTER COLUMN a TYPE INT USING (1 + 2)"}) + void sharedRenderingUsesCustomExpressionPrinterAndPreservesAlterLayout(String sql) + throws JSQLParserException { + Statement statement = parse(sql); + 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(statement.toString().replace("1", "101").replace("2", "102"), + output.toString()); + assertRoundTrip(statement); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLE t (a INT DEFAULT (1 + 2) NOT NULL)", + "ALTER TABLE t ADD COLUMN a INT DEFAULT (1 + 2) NOT NULL"}) + void replacingDefaultUpdatesLegacyTokensAndBothRenderers(String sql) + throws JSQLParserException { + Statement statement = parse(sql); + ColumnDefinition column = firstColumn(statement); + defaultOption(column).setDefaultExpression(new LongValue(42)); + assertThat(column.getColumnSpecs()).containsExactly("DEFAULT", "42", "NOT", "NULL"); + assertEquals(sql.replace("(1 + 2)", "42"), statement.toString()); + assertRoundTrip(statement); + column.addColumnSpecs("UNIQUE"); + assertNotNull(defaultOption(column).getDefaultExpression()); + assertRoundTrip(statement); + column.setColumnSpecs(Arrays.asList("DEFAULT", "9")); + assertNull(column.getColumnOptions()); + assertEquals(sql.replace("(1 + 2) NOT NULL", "9"), statement.toString()); + assertRoundTrip(statement); + } + + @Test + void identityAndSerialDefaultsKeepTheirOwnOptionKinds() throws JSQLParserException { + ColumnDefinition identity = firstColumn(CCJSqlParserUtil.parse( + "CREATE TABLE t (a INT GENERATED BY DEFAULT AS IDENTITY)")); + assertEquals(ColumnOption.Kind.IDENTITY, identity.getColumnOptions().get(0).getKind()); + assertNull(identity.getColumnOptions().get(0).getDefaultExpression()); + ColumnDefinition serial = firstColumn(CCJSqlParserUtil.parse( + "CREATE TABLE t (a INT SERIAL DEFAULT VALUE)")); + assertEquals(ColumnOption.Kind.SERIAL_DEFAULT_VALUE, + serial.getColumnOptions().get(0).getKind()); + assertNull(serial.getColumnOptions().get(0).getDefaultExpression()); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE TABLE t (a TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP)", + "CREATE TABLE t (a INT DEFAULT ON NULL 1 NOT NULL)", + "CREATE TABLE t (a INT WITH DEFAULT)", + "CREATE TABLE t (a INT DEFAULT NOT NULL)", + "CREATE TABLE t (a INT NOT NULL WITH DEFAULT, b INT DEFAULT)", + "CREATE TABLE t (a INT CONSTRAINT df DEFAULT 1 REFERENCES parent(id))", + "ALTER TABLE t MODIFY (COLUMN a DROP DEFAULT, COLUMN b DROP DEFAULT)"}) + void neighboringAndLegacyColumnOptionsStillRoundTrip(String sql) throws JSQLParserException { + assertRoundTrip(CCJSqlParserUtil.parse(sql)); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLE t (a INT DEFAULT (1 + 2", + "CREATE TABLE t (a INT DEFAULT (1 +))", + "ALTER TABLE t ADD a INT DEFAULT (1 +), ADD b INT"}) + void rejectsIncompleteDefaultExpressions(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLE t (a INT DEFAULT ?)", + "ALTER TABLE t ADD COLUMN a INT DEFAULT ?"}) + void validationChecksStructuredDefaultExpressions(String sql) { + validateNotAllowed(sql, 1, 1, FeaturesAllowed.DDL, Feature.jdbcParameter); + } + + private static Statement parse(String sql) throws JSQLParserException { + return sql.contains(" OF ") + ? CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.POSTGRESQL)) + : CCJSqlParserUtil.parse(sql); + } + + private static ColumnDefinition firstColumn(Statement statement) { + return statement instanceof CreateTable + ? ((CreateTable) statement).getColumnDefinitions().get(0) + : ((Alter) statement).getAlterExpressions().get(0).getColDataTypeList().get(0); + } + + private static ColumnOption defaultOption(ColumnDefinition column) { + return column.getColumnOptions().stream() + .filter(option -> option.getKind() == ColumnOption.Kind.DEFAULT) + .findFirst().orElseThrow(AssertionError::new); + } + + private static void assertRoundTrip(Statement statement) throws JSQLParserException { + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output), null); + assertEquals(statement.toString(), output.toString()); + assertEquals(statement.toString(), parse(output.toString()).toString()); + } +}