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 7778b0ac3..cc0f7e794 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 @@ -14,6 +14,7 @@ import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -50,6 +51,11 @@ public ColumnDefinition(String columnName, ColDataType colDataType, List this.columnSpecs = 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 + * references and constraints. + */ public List getColumnSpecs() { if (columnOptions != null) { List tokens = new ArrayList<>(); @@ -76,6 +82,7 @@ public List getColumnOptions() { public void setColumnOptions(List columnOptions) { this.columnOptions = columnOptions; + this.columnSpecs = null; } public boolean isSerialDefaultValue() { @@ -102,6 +109,9 @@ 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))); + } Collections.addAll(collection, columnOptions); return withColumnOptions(collection); } @@ -153,12 +163,16 @@ public ColumnDefinition withColumnSpecs(List columnSpecs) { } public ColumnDefinition addColumnSpecs(String... columnSpecs) { - List collection = Optional.ofNullable(getColumnSpecs()).orElseGet(ArrayList::new); - Collections.addAll(collection, columnSpecs); - return this.withColumnSpecs(collection); + return addColumnSpecs(Arrays.asList(columnSpecs)); } public ColumnDefinition addColumnSpecs(Collection columnSpecs) { + if (columnOptions != null) { + if (!columnSpecs.isEmpty()) { + columnOptions.add(ColumnOption.raw(new ArrayList<>(columnSpecs))); + } + return this; + } List collection = Optional.ofNullable(getColumnSpecs()).orElseGet(ArrayList::new); collection.addAll(columnSpecs); return this.withColumnSpecs(collection); diff --git a/src/test/java/net/sf/jsqlparser/statement/create/ColumnOptionMutationTest.java b/src/test/java/net/sf/jsqlparser/statement/create/ColumnOptionMutationTest.java new file mode 100644 index 000000000..3b6233e66 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/ColumnOptionMutationTest.java @@ -0,0 +1,91 @@ +/*- + * #%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.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +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.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; + +class ColumnOptionMutationTest { + @Test + void clearingIdentityDoesNotRestoreRawSpecifications() throws JSQLParserException { + CreateTable table = parse("id INT GENERATED ALWAYS AS IDENTITY"); + ColumnDefinition column = table.getColumnDefinitions().get(0); + column.getColumnOptions().clear(); + assertTrue(column.getColumnSpecs().isEmpty()); + assertSql(table, "CREATE TABLE t (id INT)"); + } + + @Test + void legacyAppendPreservesReferenceAndTraversal() throws JSQLParserException { + CreateTable table = parse("id INT REFERENCES parent(id)"); + ColumnDefinition column = table.getColumnDefinitions().get(0); + ForeignKeyReference reference = column.getForeignKeyReference(); + column.addColumnSpecs("NOT", "NULL"); + column.addColumnSpecs(Arrays.asList("DEFAULT", "1")); + assertSame(reference, column.getForeignKeyReference()); + assertTrue(new TablesNamesFinder().getTables(table).contains("parent")); + assertSql(table, "CREATE TABLE t (id INT REFERENCES parent(id) NOT NULL DEFAULT 1)"); + } + + @Test + void addingStructuredOptionPreservesExistingRawSpecifications() throws JSQLParserException { + CreateTable table = parse("id INT NOT NULL"); + ColumnDefinition column = table.getColumnDefinitions().get(0); + assertNull(column.getColumnOptions()); + ForeignKeyReference reference = parse("id INT REFERENCES parent(id)") + .getColumnDefinitions().get(0).getForeignKeyReference(); + column.addColumnOptions(ColumnOption.reference(reference)); + assertSql(table, "CREATE TABLE t (id INT NOT NULL REFERENCES parent(id))"); + assertTrue(new TablesNamesFinder().getTables(table).contains("parent")); + } + + @Test + void replacingOptionsWithNullClearsPreviousSpecifications() throws JSQLParserException { + CreateTable table = parse("id INT GENERATED ALWAYS AS IDENTITY"); + ColumnDefinition column = table.getColumnDefinitions().get(0); + column.setColumnOptions(null); + assertNull(column.getColumnSpecs()); + assertSql(table, "CREATE TABLE t (id INT)"); + } + + @Test + void legacySetterStillReplacesStructuredOptions() throws JSQLParserException { + CreateTable table = parse("id INT REFERENCES parent(id)"); + ColumnDefinition column = table.getColumnDefinitions().get(0); + column.setColumnSpecs(Arrays.asList("NOT", "NULL")); + assertNull(column.getForeignKeyReference()); + assertSql(table, "CREATE TABLE t (id INT NOT NULL)"); + } + + private static CreateTable parse(String definition) throws JSQLParserException { + return (CreateTable) CCJSqlParserUtil.parse("CREATE TABLE t (" + definition + ")"); + } + + private static void assertSql(CreateTable table, String expected) throws JSQLParserException { + assertEquals(expected, table.toString()); + StringBuilder buffer = new StringBuilder(); + table.accept(new StatementDeParser(buffer), null); + assertEquals(expected, buffer.toString()); + assertEquals(expected, CCJSqlParserUtil.parse(expected).toString()); + } +}