Skip to content

Commit dc50c7d

Browse files
authored
Make column option tokens read-only snapshots (#2777)
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent ef9dc08 commit dc50c7d

5 files changed

Lines changed: 322 additions & 11 deletions

File tree

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

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

1212
import java.io.Serializable;
13+
import java.util.ArrayList;
1314
import java.util.Arrays;
1415
import java.util.Collections;
1516
import java.util.List;
@@ -268,9 +269,10 @@ public ConstraintKind getConstraintKind() {
268269
}
269270
}
270271

272+
/** Creates an opaque option with its own copy of the supplied tokens, without parsing them. */
271273
public static ColumnOption raw(List<String> tokens) {
272274
ColumnOption option = new ColumnOption();
273-
option.tokens = tokens;
275+
option.setTokens(tokens);
274276
return option;
275277
}
276278

@@ -295,33 +297,60 @@ public Kind getKind() {
295297
return kind;
296298
}
297299

300+
/**
301+
* Returns an unmodifiable, detached token snapshot for both raw and structured options. An
302+
* unset raw option returns null; an explicitly empty raw option returns an empty snapshot. Use
303+
* {@link #setTokens(List)} to edit an opaque option, or the typed setters and mutable child
304+
* nodes to edit structured options. Later changes to the option do not change this snapshot.
305+
*/
298306
public List<String> getTokens() {
299307
switch (kind) {
300308
case DEFAULT:
301-
return Arrays.asList("DEFAULT", String.valueOf(defaultExpression));
309+
return Collections.unmodifiableList(
310+
Arrays.asList("DEFAULT", String.valueOf(defaultExpression)));
302311
case NULLABILITY:
303-
return nullable ? Collections.singletonList("NULL") : Arrays.asList("NOT", "NULL");
312+
return nullable ? Collections.singletonList("NULL")
313+
: Collections.unmodifiableList(Arrays.asList("NOT", "NULL"));
304314
case COLLATE:
305-
return Arrays.asList("COLLATE", collation);
315+
return Collections.unmodifiableList(Arrays.asList("COLLATE", collation));
306316
case COMMENT:
307-
return Arrays.asList("COMMENT", comment.toString());
317+
return Collections.unmodifiableList(Arrays.asList("COMMENT", comment.toString()));
308318
case ON_UPDATE:
309-
return Arrays.asList("ON", "UPDATE", onUpdateExpression.toString());
319+
return Collections.unmodifiableList(
320+
Arrays.asList("ON", "UPDATE", onUpdateExpression.toString()));
310321
case GENERATED:
311322
return generatedDefinition.getTokens();
312323
case CONSTRAINT:
313324
if ("PRIMARY KEY".equals(constraint.toString())) {
314-
return Arrays.asList("PRIMARY", "KEY");
325+
return Collections.unmodifiableList(Arrays.asList("PRIMARY", "KEY"));
315326
}
316327
return Collections.singletonList(toString());
317328
case OTHER:
318329
case SERIAL_DEFAULT_VALUE:
319-
return tokens;
330+
return tokens == null ? null
331+
: Collections.unmodifiableList(new ArrayList<>(tokens));
320332
default:
321333
return Collections.singletonList(toString());
322334
}
323335
}
324336

337+
/**
338+
* Replaces the tokens of an opaque {@link Kind#OTHER} option with a copy of the supplied list.
339+
* Null unsets the tokens; an empty list retains an explicitly empty option. Tokens are never
340+
* parsed or classified. To replace a structured option, including SERIAL DEFAULT VALUE, with
341+
* raw text, replace its node in {@link ColumnDefinition#getColumnOptions()} using
342+
* {@link #raw(List)} instead.
343+
*
344+
* @throws IllegalStateException if this is a structured option
345+
*/
346+
public void setTokens(List<String> tokens) {
347+
if (kind != Kind.OTHER) {
348+
throw new IllegalStateException(
349+
"Only opaque options accept tokens; use typed setters or replace the option");
350+
}
351+
this.tokens = tokens == null ? null : new ArrayList<>(tokens);
352+
}
353+
325354
public ForeignKeyReference getForeignKeyReference() {
326355
return foreignKeyReference;
327356
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import java.io.Serializable;
1313
import java.util.ArrayList;
14+
import java.util.Collections;
1415
import java.util.List;
1516
import java.util.Objects;
1617
import java.util.function.Consumer;
@@ -55,6 +56,10 @@ public void setStorage(Storage storage) {
5556
this.storage = storage;
5657
}
5758

59+
/**
60+
* Returns an unmodifiable token snapshot. Use the setters or edit {@link #getExpression()} to
61+
* change the definition; later changes do not affect previously returned snapshots.
62+
*/
5863
public List<String> getTokens() {
5964
List<String> tokens = new ArrayList<>();
6065
if (generatedAlways) {
@@ -66,7 +71,7 @@ public List<String> getTokens() {
6671
if (storage != null) {
6772
tokens.add(storage.name());
6873
}
69-
return tokens;
74+
return Collections.unmodifiableList(tokens);
7075
}
7176

7277
public StringBuilder appendTo(StringBuilder builder, Consumer<Expression> expressionPrinter) {

‎src/site/sphinx/usage.rst‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,35 @@ setter receives null. Appending preserves the current nodes and accepts an
745745
immutable input list. An empty append preserves existing options and creates an
746746
empty option list when none was set.
747747

748+
``ColumnOption.getTokens()`` and ``GeneratedColumnDefinition.getTokens()`` also
749+
return unmodifiable, detached snapshots. Reading tokens does not detach the
750+
underlying expressions or other child nodes: their setters and direct edits
751+
remain visible in SQL output, visitors and newly requested snapshots.
752+
753+
For opaque ``Kind.OTHER`` options, copy the tokens, edit the copy, then call
754+
``setTokens``. The setter and both ``ColumnOption.raw`` factories copy their
755+
inputs; subsequent changes to the supplied list or array do not edit the AST.
756+
Null raw tokens remain unset and return null; an empty list returns an empty
757+
snapshot. Raw tokens are never parsed or classified.
758+
759+
.. code-block:: java
760+
761+
ColumnOption raw = ColumnOption.raw("COLUMN_FORMAT", "DYNAMIC");
762+
List<String> tokens = new ArrayList<>(raw.getTokens());
763+
tokens.set(1, "FIXED");
764+
raw.setTokens(tokens); // COLUMN_FORMAT FIXED
765+
766+
Migration: raw token getter edits now throw ``UnsupportedOperationException``,
767+
as do edits to structured token snapshots that previously changed a discarded
768+
copy. ``setTokens`` only accepts opaque options; it throws
769+
``IllegalStateException`` for structured options, including SERIAL DEFAULT
770+
VALUE. Use typed setters for structured edits. To explicitly replace a
771+
structured option with raw text, replace its node in the column's option list:
772+
773+
.. code-block:: java
774+
775+
column.getColumnOptions().set(0, ColumnOption.raw("DEFAULT", "8"));
776+
748777
Inspect logical replication statements
749778
======================================
750779

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,12 @@ void specificationSnapshotsAreReadOnlyForRawAndStructuredOptions(String sql)
5454
if (option.getKind() == ColumnOption.Kind.OTHER) {
5555
assertTrue(column.getColumnOptions().stream()
5656
.allMatch(raw -> raw.getKind() == ColumnOption.Kind.OTHER));
57-
List<String> rawTokens = column.getColumnOptions().stream()
58-
.map(ColumnOption::getTokens).filter(tokens -> tokens.contains("DYNAMIC"))
57+
ColumnOption rawOption = column.getColumnOptions().stream()
58+
.filter(raw -> raw.getTokens().contains("DYNAMIC"))
5959
.findFirst().orElseThrow();
60+
List<String> rawTokens = new ArrayList<>(rawOption.getTokens());
6061
rawTokens.set(rawTokens.indexOf("DYNAMIC"), "FIXED");
62+
rawOption.setTokens(rawTokens);
6163
assertEquals(List.of("COLUMN_FORMAT", "DYNAMIC"), snapshot);
6264
assertEquals(List.of("COLUMN_FORMAT", "FIXED"), column.getColumnSpecs());
6365
} else {

0 commit comments

Comments
 (0)