Skip to content

Commit 066f581

Browse files
authored
Unify column option storage and mutation contracts (#2769)
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 8b1f029 commit 066f581

8 files changed

Lines changed: 306 additions & 61 deletions

File tree

‎src/main/java/net/sf/jsqlparser/statement/create/table/ColumnDefinition.java‎

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
package net.sf.jsqlparser.statement.create.table;
1111

1212
import net.sf.jsqlparser.statement.imprt.ImportColumn;
13-
import net.sf.jsqlparser.statement.select.PlainSelect;
1413
import net.sf.jsqlparser.expression.Expression;
1514

1615
import java.io.Serializable;
@@ -19,7 +18,7 @@
1918
import java.util.Collection;
2019
import java.util.Collections;
2120
import java.util.List;
22-
import java.util.Optional;
21+
import java.util.Objects;
2322
import java.util.function.Consumer;
2423

2524
/**
@@ -29,7 +28,6 @@ public class ColumnDefinition implements ImportColumn, TableElement, Serializabl
2928

3029
private String columnName;
3130
private ColDataType colDataType;
32-
private List<String> columnSpecs;
3331
private List<ColumnOption> columnOptions;
3432
private boolean withOptions;
3533

@@ -50,42 +48,54 @@ public ColumnDefinition(String columnName, ColDataType colDataType) {
5048

5149
public ColumnDefinition(String columnName, ColDataType colDataType, List<String> columnSpecs) {
5250
this(columnName, colDataType);
53-
this.columnSpecs = columnSpecs;
51+
columnOptions = rawOptions(columnSpecs);
5452
}
5553

5654
/**
57-
* Returns raw specifications, or a token snapshot when structured options are present. Use the
58-
* option API or {@link #addColumnSpecs(Collection)} to append without discarding structured
59-
* expressions, references and constraints. DEFAULT values use the expression's SQL rendering.
55+
* Returns an unmodifiable token snapshot for both raw and structured options, or null when
56+
* options are unset. Use {@link #getColumnOptions()} to edit the nodes, or
57+
* {@link #addColumnSpecs(Collection)} to append without discarding structured expressions,
58+
* references and constraints. DEFAULT values use the expression's SQL rendering.
6059
*/
6160
public List<String> getColumnSpecs() {
6261
if (columnOptions != null) {
6362
List<String> tokens = new ArrayList<>();
6463
for (ColumnOption option : columnOptions) {
6564
tokens.addAll(option.getTokens());
6665
}
67-
return tokens;
66+
return Collections.unmodifiableList(tokens);
6867
}
69-
return columnSpecs;
68+
return null;
7069
}
7170

71+
/**
72+
* Replaces every option with opaque tokens, without parsing them. The supplied list is copied;
73+
* null clears the options, while an empty list stores an explicitly empty option list.
74+
*/
7275
public void setColumnSpecs(List<String> list) {
73-
columnSpecs = list;
74-
columnOptions = null;
76+
columnOptions = rawOptions(list);
77+
}
78+
79+
private static List<ColumnOption> rawOptions(List<String> list) {
80+
List<ColumnOption> options = list == null ? null : new ArrayList<>();
81+
if (list != null && !list.isEmpty()) {
82+
options.add(ColumnOption.raw(new ArrayList<>(list)));
83+
}
84+
return options;
7585
}
7686

7787
/**
78-
* Returns column options in source order, including defaults, references, generated columns,
79-
* nullability, collation, comments, visibility and MySQL attributes. Unrecognized options
80-
* remain raw. Structured option keywords use canonical capitalization when rendered.
88+
* Returns the mutable option list in source order, or null when options are unset. Opaque
89+
* specifications are included as {@link ColumnOption.Kind#OTHER} nodes. Structured option
90+
* keywords use canonical capitalization when rendered.
8191
*/
8292
public List<ColumnOption> getColumnOptions() {
8393
return columnOptions;
8494
}
8595

96+
/** Copies the list membership while retaining the supplied mutable option nodes. */
8697
public void setColumnOptions(List<ColumnOption> columnOptions) {
87-
this.columnOptions = columnOptions;
88-
this.columnSpecs = null;
98+
this.columnOptions = columnOptions == null ? null : new ArrayList<>(columnOptions);
8999
}
90100

91101
public boolean isSerialDefaultValue() {
@@ -110,13 +120,12 @@ public ColumnDefinition withColumnOptions(List<ColumnOption> columnOptions) {
110120
}
111121

112122
public ColumnDefinition addColumnOptions(ColumnOption... columnOptions) {
113-
List<ColumnOption> collection =
114-
Optional.ofNullable(getColumnOptions()).orElseGet(ArrayList::new);
115-
if (this.columnOptions == null && columnSpecs != null && !columnSpecs.isEmpty()) {
116-
collection.add(ColumnOption.raw(new ArrayList<>(columnSpecs)));
123+
Objects.requireNonNull(columnOptions, "columnOptions");
124+
if (this.columnOptions == null) {
125+
this.columnOptions = new ArrayList<>();
117126
}
118-
Collections.addAll(collection, columnOptions);
119-
return withColumnOptions(collection);
127+
Collections.addAll(this.columnOptions, columnOptions);
128+
return this;
120129
}
121130

122131
public ColDataType getColDataType() {
@@ -170,8 +179,6 @@ protected void appendDataTypeAndSpecTo(StringBuilder builder,
170179
builder.append(' ');
171180
option.appendTo(builder, expressionPrinter);
172181
}
173-
} else if (columnSpecs != null && !columnSpecs.isEmpty()) {
174-
builder.append(' ').append(PlainSelect.getStringList(columnSpecs, false, false));
175182
}
176183
}
177184

@@ -194,15 +201,15 @@ public ColumnDefinition addColumnSpecs(String... columnSpecs) {
194201
return addColumnSpecs(Arrays.asList(columnSpecs));
195202
}
196203

204+
/** Appends copied opaque tokens after the existing nodes, preserving their structure. */
197205
public ColumnDefinition addColumnSpecs(Collection<String> columnSpecs) {
198-
if (columnOptions != null) {
199-
if (!columnSpecs.isEmpty()) {
200-
columnOptions.add(ColumnOption.raw(new ArrayList<>(columnSpecs)));
201-
}
202-
return this;
206+
Objects.requireNonNull(columnSpecs, "columnSpecs");
207+
if (columnOptions == null) {
208+
columnOptions = new ArrayList<>();
203209
}
204-
List<String> collection = Optional.ofNullable(getColumnSpecs()).orElseGet(ArrayList::new);
205-
collection.addAll(columnSpecs);
206-
return this.withColumnSpecs(collection);
210+
if (!columnSpecs.isEmpty()) {
211+
columnOptions.add(ColumnOption.raw(new ArrayList<>(columnSpecs)));
212+
}
213+
return this;
207214
}
208215
}

‎src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt‎

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2080,15 +2080,6 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
20802080
}
20812081
}
20822082

2083-
private static boolean hasStructuredColumnOption(List<ColumnOption> options) {
2084-
for (ColumnOption option : options) {
2085-
if (option.getKind() != ColumnOption.Kind.OTHER) {
2086-
return true;
2087-
}
2088-
}
2089-
return false;
2090-
}
2091-
20922083
/** Builds names from lexer-separated components without reinterpreting PostgreSQL dots. */
20932084
protected Table createTable(List<String> names) {
20942085
return Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) && names.size() == 1
@@ -14538,23 +14529,19 @@ ColumnDefinition ColumnDefinition(): {
1453814529
ColumnDefinition coldef;
1453914530
String columnName;
1454014531
ColDataType colDataType;
14541-
List<String> columnSpecs = new ArrayList<String>();
1454214532
List<ColumnOption> columnOptions = new ArrayList<ColumnOption>();
1454314533
ColumnOption option;
1454414534
} {
1454514535
columnName=RelObjectName()
1454614536
colDataType=ColDataType()
1454714537
( LOOKAHEAD(2) option=ColumnDefinitionOption() {
1454814538
columnOptions.add(option);
14549-
columnSpecs.addAll(option.getTokens());
1455014539
} )*
1455114540
{
1455214541
coldef = new ColumnDefinition();
1455314542
coldef.setColumnName(columnName);
1455414543
coldef.setColDataType(colDataType);
14555-
if (columnSpecs.size() > 0)
14556-
coldef.setColumnSpecs(columnSpecs);
14557-
if (hasStructuredColumnOption(columnOptions))
14544+
if (!columnOptions.isEmpty())
1455814545
coldef.setColumnOptions(columnOptions);
1455914546
return coldef;
1456014547
}
@@ -17271,7 +17258,6 @@ AlterExpression.ColumnDataType AlterExpressionColumnDataType():
1727117258
boolean withType = false;
1727217259
boolean useSetData = false;
1727317260
ColDataType dataType = null;
17274-
List<String> columnSpecs = new ArrayList<String>();
1727517261
List<ColumnOption> columnOptions = new ArrayList<ColumnOption>();
1727617262
ColumnOption option = null;
1727717263
AlterExpression.ColumnDataType result;
@@ -17293,19 +17279,16 @@ AlterExpression.ColumnDataType AlterExpressionColumnDataType():
1729317279
( LOOKAHEAD(2) dataType = ColDataType() )?
1729417280
( LOOKAHEAD(2, { !(withType && getToken(1).kind == K_USING) }) option = ColumnDefinitionOption() {
1729517281
columnOptions.add(option);
17296-
columnSpecs.addAll(option.getTokens());
1729717282
} )*
1729817283
[ LOOKAHEAD({ withType && getToken(1).kind == K_USING }) <K_USING> usingExpression=Expression() ]
1729917284
)
1730017285
{
1730117286
requireDdlSyntax(!withType || dataType != null, "TYPE requires a data type");
17302-
result = new AlterExpression.ColumnDataType(columnName, withType, dataType, columnSpecs);
17287+
result = new AlterExpression.ColumnDataType(columnName, withType, dataType, null);
1730317288
result.setUseSetData(useSetData);
1730417289
result.setUsingExpression(usingExpression);
1730517290
result.setIdentityAlterations(identityAlterations);
17306-
if (hasStructuredColumnOption(columnOptions)) {
17307-
result.setColumnOptions(columnOptions);
17308-
}
17291+
result.setColumnOptions(columnOptions);
1730917292
return result;
1731017293
}
1731117294
}

‎src/site/sphinx/usage.rst‎

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -652,11 +652,36 @@ Charset remains in ``ColDataType.getCharacterSet()``; column collation is in its
652652
COLLATE option. Consumers can map both directly without reconstructing tokens.
653653
StringValue comment bodies retain their SQL escape representation.
654654

655-
The legacy ``getColumnSpecs()`` returns a token snapshot when options are
656-
structured; expression fragments may occupy one token and structured keywords
657-
use canonical capitalization. Mutate the option objects to change the AST.
658-
``addColumnSpecs`` preserves existing options; ``setColumnSpecs`` explicitly
659-
replaces them with raw specifications.
655+
``ColumnDefinition`` stores all options in one ordered list, including opaque
656+
``ColumnOption.Kind.OTHER`` nodes. ``getColumnOptions()`` returns that mutable
657+
list. ``getColumnSpecs()`` returns an **unmodifiable snapshot** for both raw and
658+
structured options; expression fragments may occupy one token and structured
659+
keywords use canonical capitalization. Unset options return null; an explicitly
660+
empty list returns an empty snapshot.
661+
662+
.. code-block:: java
663+
664+
CreateTable table = (CreateTable) CCJSqlParserUtil.parse(
665+
"CREATE TABLE t (id INT DEFAULT 1)");
666+
ColumnDefinition column = table.getColumnDefinitions().get(0);
667+
column.getColumnOptions().get(0).setDefaultExpression(new LongValue(2));
668+
column.addColumnSpecs("NOT", "NULL"); // keeps the structured DEFAULT node
669+
// CREATE TABLE t (id INT DEFAULT 2 NOT NULL)
670+
671+
Migration: calls such as ``getColumnSpecs().add(...)`` or ``.set(...)`` now throw
672+
``UnsupportedOperationException`` instead of sometimes changing the AST and
673+
sometimes editing a discarded copy. Use ``addColumnSpecs`` to append raw tokens,
674+
edit the option nodes directly, or pass a modified token-list copy to
675+
``setColumnSpecs`` to replace every option with opaque text. Raw tokens are never
676+
automatically parsed. Consequently ``getColumnOptions()`` is also non-null for
677+
raw specifications; inspect ``Kind`` to distinguish them.
678+
679+
The constructor and ``setColumnSpecs`` copy supplied tokens. ``setColumnOptions``
680+
copies list membership but retains the mutable child nodes; later changes to the
681+
caller's list do not change the column. Both getters return null after either
682+
setter receives null. Appending preserves the current nodes and accepts an
683+
immutable input list. An empty append preserves existing options and creates an
684+
empty option list when none was set.
660685

661686
Inspect logical replication statements
662687
======================================

‎src/test/java/net/sf/jsqlparser/statement/create/ColumnAttributeTest.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ void mutationsUpdateLegacyTokensAndBothRenderers() throws Exception {
156156
column(statement).addColumnSpecs("CUSTOM");
157157
assertEquals(ColumnOption.Kind.OTHER, options.get(options.size() - 1).getKind());
158158
column(statement).setColumnSpecs(List.of("NOT", "NULL"));
159-
assertNull(column(statement).getColumnOptions());
159+
assertEquals(ColumnOption.Kind.OTHER,
160+
column(statement).getColumnOptions().get(0).getKind());
160161
assertEquals("CREATE TABLE t (c INT NOT NULL)", statement.toString());
161162
}
162163

‎src/test/java/net/sf/jsqlparser/statement/create/ColumnDefaultExpressionTest.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ void replacingDefaultUpdatesLegacyTokensAndBothRenderers(String sql)
130130
assertNotNull(defaultOption(column).getDefaultExpression());
131131
assertRoundTrip(statement);
132132
column.setColumnSpecs(Arrays.asList("DEFAULT", "9"));
133-
assertNull(column.getColumnOptions());
133+
assertEquals(ColumnOption.Kind.OTHER, column.getColumnOptions().get(0).getKind());
134134
assertEquals(sql.replace("(1 + 2) NOT NULL", "9"), statement.toString());
135135
assertRoundTrip(statement);
136136
}

0 commit comments

Comments
 (0)