From 98b80ea76f5d12b64d77c7967cb2439f686438b0 Mon Sep 17 00:00:00 2001 From: minleejae Date: Sun, 11 Oct 2026 18:49:20 +0900 Subject: [PATCH] Unify column option storage and mutation contracts Signed-off-by: minleejae --- .../create/table/ColumnDefinition.java | 71 +++--- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 23 +- src/site/sphinx/usage.rst | 35 ++- .../statement/create/ColumnAttributeTest.java | 3 +- .../create/ColumnDefaultExpressionTest.java | 2 +- .../create/ColumnDefinitionStateTest.java | 225 ++++++++++++++++++ .../create/ColumnOptionMutationTest.java | 2 +- .../PostgreSqlColumnTypeOptionsTest.java | 6 +- 8 files changed, 306 insertions(+), 61 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/statement/create/ColumnDefinitionStateTest.java 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 ccede4eb8..bf3d26f58 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 @@ -10,7 +10,6 @@ package net.sf.jsqlparser.statement.create.table; import net.sf.jsqlparser.statement.imprt.ImportColumn; -import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.expression.Expression; import java.io.Serializable; @@ -19,7 +18,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Optional; +import java.util.Objects; import java.util.function.Consumer; /** @@ -29,7 +28,6 @@ public class ColumnDefinition implements ImportColumn, TableElement, Serializabl private String columnName; private ColDataType colDataType; - private List columnSpecs; private List columnOptions; private boolean withOptions; @@ -50,13 +48,14 @@ public ColumnDefinition(String columnName, ColDataType colDataType) { public ColumnDefinition(String columnName, ColDataType colDataType, List columnSpecs) { this(columnName, colDataType); - this.columnSpecs = columnSpecs; + columnOptions = rawOptions(columnSpecs); } /** - * 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 - * expressions, references and constraints. DEFAULT values use the expression's SQL rendering. + * Returns an unmodifiable token snapshot for both raw and structured options, or null when + * options are unset. Use {@link #getColumnOptions()} to edit the nodes, or + * {@link #addColumnSpecs(Collection)} to append without discarding structured expressions, + * references and constraints. DEFAULT values use the expression's SQL rendering. */ public List getColumnSpecs() { if (columnOptions != null) { @@ -64,28 +63,39 @@ public List getColumnSpecs() { for (ColumnOption option : columnOptions) { tokens.addAll(option.getTokens()); } - return tokens; + return Collections.unmodifiableList(tokens); } - return columnSpecs; + return null; } + /** + * Replaces every option with opaque tokens, without parsing them. The supplied list is copied; + * null clears the options, while an empty list stores an explicitly empty option list. + */ public void setColumnSpecs(List list) { - columnSpecs = list; - columnOptions = null; + columnOptions = rawOptions(list); + } + + private static List rawOptions(List list) { + List options = list == null ? null : new ArrayList<>(); + if (list != null && !list.isEmpty()) { + options.add(ColumnOption.raw(new ArrayList<>(list))); + } + return options; } /** - * Returns column options in source order, including defaults, references, generated columns, - * nullability, collation, comments, visibility and MySQL attributes. Unrecognized options - * remain raw. Structured option keywords use canonical capitalization when rendered. + * Returns the mutable option list in source order, or null when options are unset. Opaque + * specifications are included as {@link ColumnOption.Kind#OTHER} nodes. Structured option + * keywords use canonical capitalization when rendered. */ public List getColumnOptions() { return columnOptions; } + /** Copies the list membership while retaining the supplied mutable option nodes. */ public void setColumnOptions(List columnOptions) { - this.columnOptions = columnOptions; - this.columnSpecs = null; + this.columnOptions = columnOptions == null ? null : new ArrayList<>(columnOptions); } public boolean isSerialDefaultValue() { @@ -110,13 +120,12 @@ public ColumnDefinition withColumnOptions(List columnOptions) { } public ColumnDefinition addColumnOptions(ColumnOption... columnOptions) { - List collection = - Optional.ofNullable(getColumnOptions()).orElseGet(ArrayList::new); - if (this.columnOptions == null && columnSpecs != null && !columnSpecs.isEmpty()) { - collection.add(ColumnOption.raw(new ArrayList<>(columnSpecs))); + Objects.requireNonNull(columnOptions, "columnOptions"); + if (this.columnOptions == null) { + this.columnOptions = new ArrayList<>(); } - Collections.addAll(collection, columnOptions); - return withColumnOptions(collection); + Collections.addAll(this.columnOptions, columnOptions); + return this; } public ColDataType getColDataType() { @@ -170,8 +179,6 @@ protected void appendDataTypeAndSpecTo(StringBuilder builder, builder.append(' '); option.appendTo(builder, expressionPrinter); } - } else if (columnSpecs != null && !columnSpecs.isEmpty()) { - builder.append(' ').append(PlainSelect.getStringList(columnSpecs, false, false)); } } @@ -194,15 +201,15 @@ public ColumnDefinition addColumnSpecs(String... columnSpecs) { return addColumnSpecs(Arrays.asList(columnSpecs)); } + /** Appends copied opaque tokens after the existing nodes, preserving their structure. */ public ColumnDefinition addColumnSpecs(Collection columnSpecs) { - if (columnOptions != null) { - if (!columnSpecs.isEmpty()) { - columnOptions.add(ColumnOption.raw(new ArrayList<>(columnSpecs))); - } - return this; + Objects.requireNonNull(columnSpecs, "columnSpecs"); + if (columnOptions == null) { + columnOptions = new ArrayList<>(); } - List collection = Optional.ofNullable(getColumnSpecs()).orElseGet(ArrayList::new); - collection.addAll(columnSpecs); - return this.withColumnSpecs(collection); + if (!columnSpecs.isEmpty()) { + columnOptions.add(ColumnOption.raw(new ArrayList<>(columnSpecs))); + } + return this; } } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 6a82cb3bb..be014a730 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -2095,15 +2095,6 @@ public class CCJSqlParser extends AbstractJSqlParser { } } - private static boolean hasStructuredColumnOption(List options) { - for (ColumnOption option : options) { - if (option.getKind() != ColumnOption.Kind.OTHER) { - return true; - } - } - return false; - } - /** Builds names from lexer-separated components without reinterpreting PostgreSQL dots. */ protected Table createTable(List names) { return Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) && names.size() == 1 @@ -14553,7 +14544,6 @@ ColumnDefinition ColumnDefinition(): { ColumnDefinition coldef; String columnName; ColDataType colDataType; - List columnSpecs = new ArrayList(); List columnOptions = new ArrayList(); ColumnOption option; } { @@ -14561,15 +14551,12 @@ ColumnDefinition ColumnDefinition(): { colDataType=ColDataType() ( LOOKAHEAD(2) option=ColumnDefinitionOption() { columnOptions.add(option); - columnSpecs.addAll(option.getTokens()); } )* { coldef = new ColumnDefinition(); coldef.setColumnName(columnName); coldef.setColDataType(colDataType); - if (columnSpecs.size() > 0) - coldef.setColumnSpecs(columnSpecs); - if (hasStructuredColumnOption(columnOptions)) + if (!columnOptions.isEmpty()) coldef.setColumnOptions(columnOptions); return coldef; } @@ -17286,7 +17273,6 @@ AlterExpression.ColumnDataType AlterExpressionColumnDataType(): boolean withType = false; boolean useSetData = false; ColDataType dataType = null; - List columnSpecs = new ArrayList(); List columnOptions = new ArrayList(); ColumnOption option = null; AlterExpression.ColumnDataType result; @@ -17308,19 +17294,16 @@ AlterExpression.ColumnDataType AlterExpressionColumnDataType(): ( LOOKAHEAD(2) dataType = ColDataType() )? ( LOOKAHEAD(2, { !(withType && getToken(1).kind == K_USING) }) option = ColumnDefinitionOption() { columnOptions.add(option); - columnSpecs.addAll(option.getTokens()); } )* [ LOOKAHEAD({ withType && getToken(1).kind == K_USING }) usingExpression=Expression() ] ) { requireDdlSyntax(!withType || dataType != null, "TYPE requires a data type"); - result = new AlterExpression.ColumnDataType(columnName, withType, dataType, columnSpecs); + result = new AlterExpression.ColumnDataType(columnName, withType, dataType, null); result.setUseSetData(useSetData); result.setUsingExpression(usingExpression); result.setIdentityAlterations(identityAlterations); - if (hasStructuredColumnOption(columnOptions)) { - result.setColumnOptions(columnOptions); - } + result.setColumnOptions(columnOptions); return result; } } diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index 218d6dcf7..feca29bf2 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -537,11 +537,36 @@ Charset remains in ``ColDataType.getCharacterSet()``; column collation is in its COLLATE option. Consumers can map both directly without reconstructing tokens. StringValue comment bodies retain their SQL escape representation. -The legacy ``getColumnSpecs()`` returns a token snapshot when options are -structured; expression fragments may occupy one token and structured keywords -use canonical capitalization. Mutate the option objects to change the AST. -``addColumnSpecs`` preserves existing options; ``setColumnSpecs`` explicitly -replaces them with raw specifications. +``ColumnDefinition`` stores all options in one ordered list, including opaque +``ColumnOption.Kind.OTHER`` nodes. ``getColumnOptions()`` returns that mutable +list. ``getColumnSpecs()`` returns an **unmodifiable snapshot** for both raw and +structured options; expression fragments may occupy one token and structured +keywords use canonical capitalization. Unset options return null; an explicitly +empty list returns an empty snapshot. + +.. code-block:: java + + CreateTable table = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TABLE t (id INT DEFAULT 1)"); + ColumnDefinition column = table.getColumnDefinitions().get(0); + column.getColumnOptions().get(0).setDefaultExpression(new LongValue(2)); + column.addColumnSpecs("NOT", "NULL"); // keeps the structured DEFAULT node + // CREATE TABLE t (id INT DEFAULT 2 NOT NULL) + +Migration: calls such as ``getColumnSpecs().add(...)`` or ``.set(...)`` now throw +``UnsupportedOperationException`` instead of sometimes changing the AST and +sometimes editing a discarded copy. Use ``addColumnSpecs`` to append raw tokens, +edit the option nodes directly, or pass a modified token-list copy to +``setColumnSpecs`` to replace every option with opaque text. Raw tokens are never +automatically parsed. Consequently ``getColumnOptions()`` is also non-null for +raw specifications; inspect ``Kind`` to distinguish them. + +The constructor and ``setColumnSpecs`` copy supplied tokens. ``setColumnOptions`` +copies list membership but retains the mutable child nodes; later changes to the +caller's list do not change the column. Both getters return null after either +setter receives null. Appending preserves the current nodes and accepts an +immutable input list. An empty append preserves existing options and creates an +empty option list when none was set. Inspect logical replication statements ====================================== diff --git a/src/test/java/net/sf/jsqlparser/statement/create/ColumnAttributeTest.java b/src/test/java/net/sf/jsqlparser/statement/create/ColumnAttributeTest.java index faf18fdbf..fc5864f37 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/ColumnAttributeTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/ColumnAttributeTest.java @@ -156,7 +156,8 @@ void mutationsUpdateLegacyTokensAndBothRenderers() throws Exception { column(statement).addColumnSpecs("CUSTOM"); assertEquals(ColumnOption.Kind.OTHER, options.get(options.size() - 1).getKind()); column(statement).setColumnSpecs(List.of("NOT", "NULL")); - assertNull(column(statement).getColumnOptions()); + assertEquals(ColumnOption.Kind.OTHER, + column(statement).getColumnOptions().get(0).getKind()); assertEquals("CREATE TABLE t (c INT NOT NULL)", statement.toString()); } diff --git a/src/test/java/net/sf/jsqlparser/statement/create/ColumnDefaultExpressionTest.java b/src/test/java/net/sf/jsqlparser/statement/create/ColumnDefaultExpressionTest.java index c99e6a11e..4afc42d80 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/ColumnDefaultExpressionTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/ColumnDefaultExpressionTest.java @@ -130,7 +130,7 @@ void replacingDefaultUpdatesLegacyTokensAndBothRenderers(String sql) assertNotNull(defaultOption(column).getDefaultExpression()); assertRoundTrip(statement); column.setColumnSpecs(Arrays.asList("DEFAULT", "9")); - assertNull(column.getColumnOptions()); + assertEquals(ColumnOption.Kind.OTHER, column.getColumnOptions().get(0).getKind()); assertEquals(sql.replace("(1 + 2) NOT NULL", "9"), statement.toString()); assertRoundTrip(statement); } diff --git a/src/test/java/net/sf/jsqlparser/statement/create/ColumnDefinitionStateTest.java b/src/test/java/net/sf/jsqlparser/statement/create/ColumnDefinitionStateTest.java new file mode 100644 index 000000000..1e23f0d8e --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/ColumnDefinitionStateTest.java @@ -0,0 +1,225 @@ +/*- + * #%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 org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.Collection; +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.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnDataType; +import net.sf.jsqlparser.statement.create.table.ColDataType; +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.ForeignKeyReference; +import net.sf.jsqlparser.util.TableDefinitionTraversal; +import net.sf.jsqlparser.util.TablesNamesFinder; +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 ColumnDefinitionStateTest { + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLE t (id INT COLUMN_FORMAT DYNAMIC)", + "ALTER TABLE t ADD COLUMN id INT COLUMN_FORMAT DYNAMIC", + "CREATE TABLE t (id INT DEFAULT 1)", "ALTER TABLE t ADD COLUMN id INT DEFAULT 1"}) + void specificationSnapshotsAreReadOnlyForRawAndStructuredOptions(String sql) + throws JSQLParserException { + Statement statement = parse(sql); + ColumnDefinition column = column(statement); + List snapshot = column.getColumnSpecs(); + assertThrows(UnsupportedOperationException.class, () -> snapshot.add("NOT NULL")); + assertThrows(UnsupportedOperationException.class, () -> snapshot.set(0, "changed")); + assertThrows(UnsupportedOperationException.class, snapshot::clear); + ColumnOption option = column.getColumnOptions().get(0); + if (option.getKind() == ColumnOption.Kind.OTHER) { + assertTrue(column.getColumnOptions().stream() + .allMatch(raw -> raw.getKind() == ColumnOption.Kind.OTHER)); + List rawTokens = column.getColumnOptions().stream() + .map(ColumnOption::getTokens).filter(tokens -> tokens.contains("DYNAMIC")) + .findFirst().orElseThrow(); + rawTokens.set(rawTokens.indexOf("DYNAMIC"), "FIXED"); + assertEquals(List.of("COLUMN_FORMAT", "DYNAMIC"), snapshot); + assertEquals(List.of("COLUMN_FORMAT", "FIXED"), column.getColumnSpecs()); + } else { + option.setDefaultExpression(new LongValue(2)); + assertEquals(List.of("DEFAULT", "1"), snapshot); + assertEquals(List.of("DEFAULT", "2"), column.getColumnSpecs()); + } + roundTrip(statement); + } + + @Test + void constructorAndRawSettersCopyTokensWithoutParsingThem() { + List input = new ArrayList<>(List.of("DEFAULT", "1")); + ColumnDefinition column = new ColumnDefinition("id", new ColDataType("INT"), input); + input.set(1, "99"); + assertEquals("id INT DEFAULT 1", column.toString()); + assertEquals(ColumnOption.Kind.OTHER, column.getColumnOptions().get(0).getKind()); + assertNull(column.getColumnOptions().get(0).getDefaultExpression()); + List appended = new ArrayList<>(List.of("NOT", "NULL")); + column.addColumnSpecs(appended); + appended.clear(); + assertEquals("id INT DEFAULT 1 NOT NULL", column.toString()); + column.setColumnSpecs(input); + input.clear(); + assertEquals("id INT DEFAULT 99", column.toString()); + assertEquals(1, column.getColumnOptions().size()); + column.setColumnSpecs(column.getColumnSpecs()); + assertEquals("id INT DEFAULT 99", column.toString()); + } + + @Test + void constructorDoesNotDispatchToOverriddenSpecificationSetter() { + ColumnDefinition column = new ColumnDefinition("id", new ColDataType("INT"), + List.of("NOT", "NULL")) { + @Override + public void setColumnSpecs(List specs) { + fail("The constructor must not call an overridable setter"); + } + }; + assertEquals(List.of("NOT", "NULL"), column.getColumnSpecs()); + assertEquals("id INT NOT NULL", column.toString()); + } + + @Test + void optionSettersCopyMembershipAndKeepTheOriginalMutableNodes() { + ColumnOption defaultOption = ColumnOption.defaultValue(new LongValue(1)); + List input = new ArrayList<>(List.of(defaultOption)); + ColumnDefinition column = new ColumnDefinition("id", new ColDataType("INT")) + .withColumnOptions(input); + input.clear(); + assertSame(defaultOption, column.getColumnOptions().get(0)); + List liveOptions = column.getColumnOptions(); + defaultOption.setDefaultExpression(new LongValue(2)); + column.addColumnSpecs("NOT", "NULL"); + column.addColumnOptions(ColumnOption.comment( + new net.sf.jsqlparser.expression.StringValue("description"))); + assertSame(liveOptions, column.getColumnOptions()); + assertEquals(3, liveOptions.size()); + assertEquals("id INT DEFAULT 2 NOT NULL COMMENT 'description'", column.toString()); + column.setColumnOptions(List.of(defaultOption)); + column.addColumnSpecs("NOT", "NULL"); + assertEquals("id INT DEFAULT 2 NOT NULL", column.toString()); + column.getColumnOptions().remove(0); + assertEquals(List.of("NOT", "NULL"), column.getColumnSpecs()); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLE t (id INT DEFAULT 1 REFERENCES parent(id))", + "ALTER TABLE t ADD COLUMN id INT DEFAULT 1 REFERENCES parent(id)"}) + void rawAppendPreservesStructuredChildrenVisitorsAndCustomRendering(String sql) + throws JSQLParserException { + Statement statement = parse(sql); + ColumnDefinition column = column(statement); + ColumnOption defaultOption = column.getColumnOptions().get(0); + ForeignKeyReference reference = column.getForeignKeyReference(); + column.addColumnSpecs("NOT", "NULL"); + assertSame(defaultOption, column.getColumnOptions().get(0)); + assertSame(reference, column.getForeignKeyReference()); + List visited = new ArrayList<>(); + TableDefinitionTraversal.visit(column, visited::add, ignored -> { + }); + assertEquals(List.of(defaultOption.getDefaultExpression()), visited); + assertEquals(Set.of("t", "parent"), new TablesNamesFinder().getTables(statement)); + StringBuilder rewritten = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(7); + } + }; + statement.accept(new StatementDeParser(expressions, new SelectDeParser(), rewritten), null); + assertEquals(statement.toString().replace("DEFAULT 1", "DEFAULT 7"), rewritten.toString()); + assertEquals(rewritten.toString(), parse(rewritten.toString()).toString()); + roundTrip(statement); + + column.setColumnSpecs(List.of("DEFAULT", "2")); + assertNull(column.getForeignKeyReference()); + visited.clear(); + TableDefinitionTraversal.visit(column, visited::add, ignored -> { + }); + assertTrue(visited.isEmpty()); + assertEquals(Set.of("t"), new TablesNamesFinder().getTables(statement)); + defaultOption.setDefaultExpression(new LongValue(9)); + assertEquals(List.of("DEFAULT", "2"), column.getColumnSpecs()); + roundTrip(statement); + } + + @Test + void nullEmptyAndAppendContractsRemainDistinct() { + ColumnDefinition column = new ColumnDefinition("id", new ColDataType("INT")); + assertNull(column.getColumnSpecs()); + assertNull(column.getColumnOptions()); + assertThrows(NullPointerException.class, + () -> column.addColumnSpecs((Collection) null)); + assertThrows(NullPointerException.class, + () -> column.addColumnOptions((ColumnOption[]) null)); + assertNull(column.getColumnOptions()); + column.setColumnSpecs(List.of()); + assertTrue(column.getColumnSpecs().isEmpty()); + assertTrue(column.getColumnOptions().isEmpty()); + assertEquals("id INT", column.toString()); + column.setColumnOptions(null); + assertNull(column.getColumnSpecs()); + column.addColumnSpecs(List.of()); + assertTrue(column.getColumnSpecs().isEmpty()); + column.setColumnSpecs(null); + assertNull(column.getColumnOptions()); + column.setColumnOptions(List.of()); + assertTrue(column.getColumnSpecs().isEmpty()); + assertEquals("INT", column.toStringDataTypeAndSpec()); + } + + @Test + void parserKeepsExistingAbsentAndEmptySpecificationBoundaries() throws JSQLParserException { + ColumnDefinition create = column(parse("CREATE TABLE t (id INT)")); + ColumnDefinition alter = column(parse("ALTER TABLE t ADD COLUMN id INT")); + assertNull(create.getColumnSpecs()); + assertNull(create.getColumnOptions()); + assertTrue(alter.getColumnSpecs().isEmpty()); + assertTrue(alter.getColumnOptions().isEmpty()); + ColumnDataType dataType = new ColumnDataType(false); + assertSame(dataType, dataType.withColumnName("id").withColDataType(new ColDataType("INT")) + .withColumnSpecs(List.of("NOT", "NULL"))); + assertSame(dataType, dataType.addColumnSpecs("DEFAULT", "1")); + assertSame(dataType, dataType.addColumnSpecs(List.of("UNIQUE"))); + assertEquals("id INT NOT NULL DEFAULT 1 UNIQUE", dataType.toString()); + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.MYSQL)); + } + + private static ColumnDefinition column(Statement statement) { + return statement instanceof CreateTable + ? ((CreateTable) statement).getColumnDefinitions().get(0) + : ((Alter) statement).getAlterExpressions().get(0).getColDataTypeList().get(0); + } + + private static void roundTrip(Statement statement) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql)); + assertEquals(statement.toString(), sql.toString()); + assertEquals(sql.toString(), parse(sql.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/ColumnOptionMutationTest.java b/src/test/java/net/sf/jsqlparser/statement/create/ColumnOptionMutationTest.java index bceb012c4..3257f9313 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/ColumnOptionMutationTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/ColumnOptionMutationTest.java @@ -52,7 +52,7 @@ void addingStructuredOptionPreservesExistingRawSpecifications() throws JSQLParse CreateTable table = parse("id INT"); ColumnDefinition column = table.getColumnDefinitions().get(0); column.setColumnSpecs(Arrays.asList("NOT", "NULL")); - assertNull(column.getColumnOptions()); + assertEquals(ColumnOption.Kind.OTHER, column.getColumnOptions().get(0).getKind()); ForeignKeyReference reference = parse("id INT REFERENCES parent(id)") .getColumnDefinitions().get(0).getForeignKeyReference(); column.addColumnOptions(ColumnOption.reference(reference)); diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlColumnTypeOptionsTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlColumnTypeOptionsTest.java index 1ee7ca302..5946470e7 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlColumnTypeOptionsTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlColumnTypeOptionsTest.java @@ -36,7 +36,11 @@ void arrayKeywordBelongsToDataType(String type) throws JSQLParserException { assertTrue(dataType.isUsingArrayKeyword()); assertEquals(dimensions.isEmpty() ? Arrays.asList((Integer) null) : List.of(4), dataType.getArrayData()); - assertNull(column.getColumnOptions()); + if (alter) { + assertTrue(column.getColumnOptions().isEmpty()); + } else { + assertNull(column.getColumnOptions()); + } dataType.setArrayData(List.of(8)); assertTrue(statement.toString().contains(" ARRAY[8]")); assertRoundTrip(statement);