Skip to content

Commit eb27c78

Browse files
authored
fix: preserve ordered table elements through legacy list edits (#2607)
1 parent 537c8c1 commit eb27c78

4 files changed

Lines changed: 272 additions & 17 deletions

File tree

src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,19 @@ public void setUnlogged(boolean unlogged) {
7070
}
7171

7272
/**
73-
* @return a list of {@link ColumnDefinition}s of this table.
73+
* @return a list of {@link ColumnDefinition}s of this table. When ordered table elements are
74+
* present, this is a mutable view of the column definitions in that list.
7475
*/
7576
public List<ColumnDefinition> getColumnDefinitions() {
7677
return columnDefinitions;
7778
}
7879

7980
public void setColumnDefinitions(List<ColumnDefinition> list) {
80-
columnDefinitions = list;
81-
tableElements = null;
81+
if (tableElements == null) {
82+
columnDefinitions = list;
83+
} else {
84+
TableElementList.replace(tableElements, ColumnDefinition.class, list);
85+
}
8286
}
8387

8488
public List<String> getColumns() {
@@ -139,15 +143,19 @@ public void setCreateOptionsStrings(List<String> createOptionsStrings) {
139143
/**
140144
* @return a list of {@link Index}es (for example "PRIMARY KEY") of this table.<br>
141145
* Indexes created with column definitions (as in mycol INT PRIMARY KEY) are not
142-
* inserted into this list.
146+
* inserted into this list. When ordered table elements are present, this is a mutable
147+
* view of their indexes.
143148
*/
144149
public List<Index> getIndexes() {
145150
return indexes;
146151
}
147152

148153
public void setIndexes(List<Index> list) {
149-
indexes = list;
150-
tableElements = null;
154+
if (tableElements == null) {
155+
indexes = list;
156+
} else {
157+
TableElementList.replace(tableElements, Index.class, list);
158+
}
151159
}
152160

153161
/**
@@ -164,15 +172,8 @@ public void setTableElements(List<TableElement> tableElements) {
164172
indexes = null;
165173
return;
166174
}
167-
columnDefinitions = new ArrayList<>();
168-
indexes = new ArrayList<>();
169-
for (TableElement element : tableElements) {
170-
if (element instanceof ColumnDefinition) {
171-
columnDefinitions.add((ColumnDefinition) element);
172-
} else if (element instanceof Index) {
173-
indexes.add((Index) element);
174-
}
175-
}
175+
columnDefinitions = new TableElementList<>(tableElements, ColumnDefinition.class);
176+
indexes = new TableElementList<>(tableElements, Index.class);
176177
}
177178

178179
/** Returns table elements of a requested AST type while preserving their declaration order. */
@@ -336,7 +337,7 @@ private void appendColumnDefinitions(StringBuilder b) {
336337
b.append(" ");
337338
b.append(PlainSelect.getStringList(columns, true, true));
338339
}
339-
if (tableElements != null && !tableElements.isEmpty()) {
340+
if (tableElements != null) {
340341
b.append(" (");
341342
b.append(PlainSelect.getStringList(tableElements, true, false));
342343
b.append(")");
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.create.table;
11+
12+
import java.io.Serializable;
13+
import java.util.AbstractList;
14+
import java.util.ArrayList;
15+
import java.util.Collections;
16+
import java.util.Iterator;
17+
import java.util.List;
18+
import java.util.ListIterator;
19+
import java.util.NoSuchElementException;
20+
import java.util.Objects;
21+
22+
/** A mutable, filtered view preserving the other declarations in a table definition. */
23+
final class TableElementList<E extends TableElement> extends AbstractList<E>
24+
implements Serializable {
25+
private final List<TableElement> elements;
26+
private final Class<E> type;
27+
28+
TableElementList(List<TableElement> elements, Class<E> type) {
29+
this.elements = elements;
30+
this.type = type;
31+
}
32+
33+
@Override
34+
public int size() {
35+
int count = 0;
36+
for (TableElement element : elements) {
37+
if (type.isInstance(element)) {
38+
count++;
39+
}
40+
}
41+
return count;
42+
}
43+
44+
@Override
45+
public Iterator<E> iterator() {
46+
return new Iterator<E>() {
47+
private int cursor;
48+
private int last = -1;
49+
50+
@Override
51+
public boolean hasNext() {
52+
while (cursor < elements.size() && !type.isInstance(elements.get(cursor))) {
53+
cursor++;
54+
}
55+
return cursor < elements.size();
56+
}
57+
58+
@Override
59+
public E next() {
60+
if (!hasNext()) {
61+
throw new NoSuchElementException();
62+
}
63+
last = cursor++;
64+
return type.cast(elements.get(last));
65+
}
66+
67+
@Override
68+
public void remove() {
69+
if (last < 0) {
70+
throw new IllegalStateException();
71+
}
72+
elements.remove(last);
73+
cursor--;
74+
last = -1;
75+
modCount++;
76+
}
77+
};
78+
}
79+
80+
private int elementIndex(int index, boolean insertion) {
81+
if (index < 0) {
82+
throw new IndexOutOfBoundsException(Integer.toString(index));
83+
}
84+
int count = 0;
85+
int end = elements.size();
86+
for (int i = 0; i < elements.size(); i++) {
87+
if (type.isInstance(elements.get(i))) {
88+
if (count++ == index) {
89+
return i;
90+
}
91+
end = i + 1;
92+
}
93+
}
94+
if (insertion && index == count) {
95+
return end;
96+
}
97+
throw new IndexOutOfBoundsException(Integer.toString(index));
98+
}
99+
100+
@Override
101+
public E get(int index) {
102+
return type.cast(elements.get(elementIndex(index, false)));
103+
}
104+
105+
@Override
106+
public E set(int index, E element) {
107+
return type.cast(elements.set(elementIndex(index, false), Objects.requireNonNull(element)));
108+
}
109+
110+
@Override
111+
public void add(int index, E element) {
112+
elements.add(elementIndex(index, true), Objects.requireNonNull(element));
113+
modCount++;
114+
}
115+
116+
@Override
117+
public E remove(int index) {
118+
E removed = type.cast(elements.remove(elementIndex(index, false)));
119+
modCount++;
120+
return removed;
121+
}
122+
123+
static <E extends TableElement> void replace(List<TableElement> elements, Class<E> type,
124+
List<E> replacements) {
125+
// The replacement may itself be a view of elements.
126+
Iterator<E> replacement = (replacements == null ? Collections.<E>emptyList()
127+
: new ArrayList<>(replacements)).iterator();
128+
ListIterator<TableElement> iterator = elements.listIterator();
129+
while (iterator.hasNext()) {
130+
if (type.isInstance(iterator.next())) {
131+
if (replacement.hasNext()) {
132+
iterator.set(replacement.next());
133+
} else {
134+
iterator.remove();
135+
}
136+
}
137+
}
138+
replacement.forEachRemaining(iterator::add);
139+
}
140+
}

src/main/java/net/sf/jsqlparser/util/deparser/CreateTableDeParser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public void deParse(CreateTable createTable) {
7272
}
7373
builder.append(")");
7474
}
75-
if (createTable.getTableElements() != null && !createTable.getTableElements().isEmpty()) {
75+
if (createTable.getTableElements() != null) {
7676
builder.append(" (");
7777
for (Iterator<TableElement> iter = createTable.getTableElements().iterator(); iter
7878
.hasNext();) {
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.create;
11+
12+
import static org.junit.jupiter.api.Assertions.assertEquals;
13+
import static org.junit.jupiter.api.Assertions.assertThrows;
14+
import static org.junit.jupiter.api.Assertions.assertTrue;
15+
16+
import java.util.ArrayList;
17+
import java.util.Collections;
18+
import java.util.List;
19+
import net.sf.jsqlparser.JSQLParserException;
20+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
21+
import net.sf.jsqlparser.statement.LikeClause;
22+
import net.sf.jsqlparser.statement.create.table.ColDataType;
23+
import net.sf.jsqlparser.statement.create.table.ColumnDefinition;
24+
import net.sf.jsqlparser.statement.create.table.CreateTable;
25+
import net.sf.jsqlparser.util.TablesNamesFinder;
26+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
27+
import org.junit.jupiter.api.Test;
28+
29+
class TableElementMutationTest {
30+
private static final String SQL = "CREATE TABLE t (LIKE parent INCLUDING DEFAULTS, "
31+
+ "a INT, CONSTRAINT c CHECK (a > 0), b INT)";
32+
33+
@Test
34+
void legacyListEditsUpdateOrderedElements() throws JSQLParserException {
35+
CreateTable table = parse();
36+
table.getColumnDefinitions().remove(1);
37+
table.getIndexes().clear();
38+
table.getColumnDefinitions().add(0, column("first"));
39+
table.getColumnDefinitions().set(1, column("last"));
40+
assertSql(table, "CREATE TABLE t (LIKE parent INCLUDING DEFAULTS, first INT, last INT)");
41+
assertEquals(3, table.getTableElements().size());
42+
assertTrue(new TablesNamesFinder().getTables(table).contains("parent"));
43+
}
44+
45+
@Test
46+
void orderedListEditsRemainVisibleThroughExistingViews() throws JSQLParserException {
47+
CreateTable table = parse();
48+
List<ColumnDefinition> columns = table.getColumnDefinitions();
49+
table.getTableElements().remove(1);
50+
assertEquals("b", columns.get(0).getColumnName());
51+
table.getTableElements().add(column("extra"));
52+
assertEquals(2, columns.size());
53+
columns.clear();
54+
assertEquals(2, table.getTableElements().size());
55+
assertEquals(1, table.getIndexes().size());
56+
assertEquals(1, table.getTableElements(LikeClause.class).size());
57+
}
58+
59+
@Test
60+
void replacingColumnsPreservesOtherElementsAndTheirPositions() throws JSQLParserException {
61+
CreateTable table = parse();
62+
table.setColumnDefinitions(new ArrayList<>(table.getColumnDefinitions()));
63+
assertSql(table, SQL);
64+
table.setColumnDefinitions(Collections.singletonList(column("replacement")));
65+
assertSql(table, "CREATE TABLE t (LIKE parent INCLUDING DEFAULTS, replacement INT, "
66+
+ "CONSTRAINT c CHECK (a > 0))");
67+
table.setIndexes(null);
68+
table.setColumnDefinitions(null);
69+
assertSql(table, "CREATE TABLE t (LIKE parent INCLUDING DEFAULTS)");
70+
}
71+
72+
@Test
73+
void fluentAddersCanPassTheirOwnViewsToSetters() throws JSQLParserException {
74+
CreateTable table = parse();
75+
table.addColumnDefinitions(column("extra"));
76+
assertEquals(3, table.getColumnDefinitions().size());
77+
assertEquals(1, table.getTableElements(LikeClause.class).size());
78+
table.setIndexes(table.getIndexes());
79+
assertEquals(1, table.getIndexes().size());
80+
assertSql(table, SQL.substring(0, SQL.length() - 1) + ", extra INT)");
81+
}
82+
83+
@Test
84+
void filteredListHonorsIndexBounds() throws JSQLParserException {
85+
List<ColumnDefinition> columns = parse().getColumnDefinitions();
86+
assertThrows(IndexOutOfBoundsException.class, () -> columns.get(-1));
87+
assertThrows(IndexOutOfBoundsException.class, () -> columns.get(2));
88+
assertThrows(IndexOutOfBoundsException.class, () -> columns.add(3, column("bad")));
89+
}
90+
91+
@Test
92+
void clearingAllElementsKeepsBothRenderersConsistent() throws JSQLParserException {
93+
CreateTable table = parse();
94+
table.getTableElements().clear();
95+
assertSql(table, "CREATE TABLE t ()");
96+
assertTrue(table.getColumnDefinitions().isEmpty());
97+
assertTrue(table.getIndexes().isEmpty());
98+
}
99+
100+
private static CreateTable parse() throws JSQLParserException {
101+
return (CreateTable) CCJSqlParserUtil.parse(SQL);
102+
}
103+
104+
private static ColumnDefinition column(String name) {
105+
return new ColumnDefinition(name, new ColDataType("INT"));
106+
}
107+
108+
private static void assertSql(CreateTable table, String expected) {
109+
assertEquals(expected, table.toString());
110+
StringBuilder buffer = new StringBuilder();
111+
table.accept(new StatementDeParser(buffer), null);
112+
assertEquals(expected, buffer.toString());
113+
}
114+
}

0 commit comments

Comments
 (0)