Skip to content

Commit 381d1a9

Browse files
authored
Complete MySQL index options and share CREATE INDEX rendering (#2567)
* Complete MySQL index algorithm and lock options and share CREATE INDEX rendering * Split shared index rendering into focused helpers * Compare median monotonic durations in bracket growth regression checks
1 parent ba4dce9 commit 381d1a9

5 files changed

Lines changed: 198 additions & 123 deletions

File tree

‎src/main/java/net/sf/jsqlparser/statement/create/index/CreateIndex.java‎

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -137,15 +137,28 @@ public void setTailParameters(List<String> tailParameters) {
137137

138138
@Override
139139
public String toString() {
140-
StringBuilder buffer = new StringBuilder();
140+
return appendTo(new StringBuilder()).toString();
141+
}
142+
143+
/** Shared rendering for the statement model and CreateIndexDeParser. */
144+
public StringBuilder appendTo(StringBuilder buffer) {
145+
appendIndexHeader(buffer);
146+
appendIndexTarget(buffer);
147+
appendIndexColumns(buffer);
148+
appendPostgreSqlTail(buffer);
149+
if (tailParameters != null) {
150+
for (String param : tailParameters) {
151+
buffer.append(" ").append(param);
152+
}
153+
}
154+
return buffer;
155+
}
141156

157+
private void appendIndexHeader(StringBuilder buffer) {
142158
buffer.append("CREATE ");
143-
144159
if (index.getType() != null) {
145-
buffer.append(index.getType());
146-
buffer.append(" ");
160+
buffer.append(index.getType()).append(" ");
147161
}
148-
149162
buffer.append("INDEX ");
150163
if (concurrently) {
151164
buffer.append("CONCURRENTLY ");
@@ -156,42 +169,30 @@ public String toString() {
156169
if (index.getName() != null) {
157170
buffer.append(index.getName()).append(" ");
158171
}
172+
}
159173

174+
private void appendIndexTarget(StringBuilder buffer) {
160175
if (index.getUsing() != null && isIndexTypeBeforeOn()) {
161176
buffer.append("USING ").append(index.getUsing()).append(" ");
162177
}
163-
164178
buffer.append("ON ");
165179
if (only) {
166180
buffer.append("ONLY ");
167181
}
168182
buffer.append(table.getFullyQualifiedName());
169-
170183
if (index.getUsing() != null && !isIndexTypeBeforeOn()) {
171-
buffer.append(" USING ");
172-
buffer.append(index.getUsing());
184+
buffer.append(" USING ").append(index.getUsing());
173185
}
186+
}
174187

188+
private void appendIndexColumns(StringBuilder buffer) {
175189
if (index.getColumnsNames() != null) {
176190
buffer.append(" (");
177-
178-
buffer.append(
179-
index.getColumns().stream()
180-
.map(Index.ColumnParams::toString)
181-
.collect(joining(", ")));
182-
191+
buffer.append(index.getColumns().stream()
192+
.map(Index.ColumnParams::toString)
193+
.collect(joining(", ")));
183194
buffer.append(")");
184-
185-
appendPostgreSqlTail(buffer);
186-
187-
if (tailParameters != null) {
188-
for (String param : tailParameters) {
189-
buffer.append(" ").append(param);
190-
}
191-
}
192195
}
193-
194-
return buffer.toString();
195196
}
196197

197198
private void appendPostgreSqlTail(StringBuilder buffer) {

‎src/main/java/net/sf/jsqlparser/util/deparser/CreateIndexDeParser.java‎

Lines changed: 1 addition & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,7 @@
99
*/
1010
package net.sf.jsqlparser.util.deparser;
1111

12-
import static java.util.stream.Collectors.joining;
13-
1412
import net.sf.jsqlparser.statement.create.index.CreateIndex;
15-
import net.sf.jsqlparser.statement.create.table.Index;
16-
import net.sf.jsqlparser.statement.select.PlainSelect;
1713

1814
public class CreateIndexDeParser extends AbstractDeParser<CreateIndex> {
1915

@@ -23,74 +19,6 @@ public CreateIndexDeParser(StringBuilder buffer) {
2319

2420
@Override
2521
public void deParse(CreateIndex createIndex) {
26-
Index index = createIndex.getIndex();
27-
28-
builder.append("CREATE ");
29-
30-
if (index.getType() != null) {
31-
builder.append(index.getType());
32-
builder.append(" ");
33-
}
34-
35-
builder.append("INDEX ");
36-
if (createIndex.isConcurrently()) {
37-
builder.append("CONCURRENTLY ");
38-
}
39-
if (createIndex.isUsingIfNotExists()) {
40-
builder.append("IF NOT EXISTS ");
41-
}
42-
if (index.getName() != null) {
43-
builder.append(index.getName()).append(" ");
44-
}
45-
46-
String using = index.getUsing();
47-
if (using != null && createIndex.isIndexTypeBeforeOn()) {
48-
builder.append("USING ").append(using).append(" ");
49-
}
50-
51-
builder.append("ON ");
52-
if (createIndex.isOnly()) {
53-
builder.append("ONLY ");
54-
}
55-
builder.append(createIndex.getTable().getFullyQualifiedName());
56-
57-
if (using != null && !createIndex.isIndexTypeBeforeOn()) {
58-
builder.append(" USING ");
59-
builder.append(using);
60-
}
61-
62-
if (index.getColumnsNames() != null) {
63-
builder.append(" (");
64-
builder.append(index.getColumnWithParams().stream()
65-
.map(Index.ColumnParams::toString)
66-
.collect(joining(", ")));
67-
builder.append(")");
68-
}
69-
70-
if (createIndex.getIncludeColumns() != null) {
71-
builder.append(" INCLUDE (")
72-
.append(String.join(", ", createIndex.getIncludeColumns())).append(")");
73-
}
74-
if (createIndex.getNullsDistinct() != null) {
75-
builder.append(" NULLS ")
76-
.append(createIndex.getNullsDistinct() ? "DISTINCT" : "NOT DISTINCT");
77-
}
78-
if (createIndex.getStorageParameters() != null) {
79-
builder.append(" WITH ").append(PlainSelect.getStringList(
80-
createIndex.getStorageParameters(), true, true));
81-
}
82-
if (createIndex.getTableSpace() != null) {
83-
builder.append(" TABLESPACE ").append(createIndex.getTableSpace());
84-
}
85-
if (createIndex.getWhere() != null) {
86-
builder.append(" WHERE ").append(createIndex.getWhere());
87-
}
88-
89-
if (createIndex.getTailParameters() != null) {
90-
for (String param : createIndex.getTailParameters()) {
91-
builder.append(" ").append(param);
92-
}
93-
}
22+
createIndex.appendTo(builder);
9423
}
95-
9624
}

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

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11872,7 +11872,8 @@ CreateIndex CreateIndex():
1187211872
[ LOOKAHEAD(2) <K_TABLESPACE> tableSpace=RelObjectName()
1187311873
{ createIndex.setTableSpace(tableSpace); } ]
1187411874
[ LOOKAHEAD(2) <K_WHERE> where=Expression() { createIndex.setWhere(where); } ]
11875-
( LOOKAHEAD(2) parameter=CreateParameter() { tailParameters.addAll(parameter); } )*
11875+
( LOOKAHEAD(2, { getToken(1).kind != K_LOCK || getToken(2).kind != K_TABLE })
11876+
parameter=CreateIndexTailOption() { tailParameters.addAll(parameter); } )*
1187611877
{
1187711878
index.setColumns(colNames);
1187811879
createIndex.setIndex(index);
@@ -11882,6 +11883,35 @@ CreateIndex CreateIndex():
1188211883
}
1188311884
}
1188411885

11886+
List<String> CreateIndexTailOption():
11887+
{
11888+
List<String> option;
11889+
}
11890+
{
11891+
(
11892+
LOOKAHEAD({ getToken(1).kind == K_ALGORITHM || getToken(1).kind == K_LOCK })
11893+
option=IndexAlgorithmOrLockOption()
11894+
|
11895+
option=CreateParameter()
11896+
)
11897+
{ return option; }
11898+
}
11899+
11900+
// CREATE INDEX and DROP INDEX share MySQL's algorithm_option / lock_option syntax.
11901+
// Keep the legacy token list, including optional equals signs and the original order.
11902+
List<String> IndexAlgorithmOrLockOption():
11903+
{
11904+
List<String> option = new ArrayList<String>();
11905+
Token keyword;
11906+
Token value;
11907+
}
11908+
{
11909+
( keyword=<K_ALGORITHM> | keyword=<K_LOCK> ) { option.add(keyword.image); }
11910+
[ "=" { option.add("="); } ]
11911+
( value=<K_DEFAULT> | value=<K_NONE> | value=<K_EXCLUSIVE> | value=<S_IDENTIFIER> )
11912+
{ option.add(value.image); return option; }
11913+
}
11914+
1188511915
List<String> ColumnDefinitionParameter(): {
1188611916
Token tk = null;
1188711917
List<String> parameter = new ArrayList<String>();
@@ -13680,6 +13710,7 @@ Drop Drop():
1368013710
Table name;
1368113711
List<String> dropArgs = new ArrayList<String>();
1368213712
List<String> funcArgs = null;
13713+
List<String> indexOption;
1368313714
boolean useTemporary = false;
1368413715
}
1368513716
{
@@ -13716,6 +13747,10 @@ Drop Drop():
1371613747
( "," name = Table() { drop.addNames(name); } )*
1371713748
[ LOOKAHEAD(2) funcArgs = FuncArgsList() ]
1371813749
(
13750+
LOOKAHEAD({ getToken(1).kind == K_ALGORITHM
13751+
|| (getToken(1).kind == K_LOCK && getToken(2).kind != K_TABLE) })
13752+
indexOption=IndexAlgorithmOrLockOption() { dropArgs.addAll(indexOption); }
13753+
|
1371913754
(
1372013755
tk=<S_IDENTIFIER> | tk=<K_CASCADE> | tk=<K_RESTRICT>
1372113756
// MySQL DROP INDEX accepts a trailing algorithm_option / lock_option,
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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 net.sf.jsqlparser.JSQLParserException;
13+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
14+
import net.sf.jsqlparser.statement.Statements;
15+
import net.sf.jsqlparser.statement.create.index.CreateIndex;
16+
import net.sf.jsqlparser.statement.drop.Drop;
17+
import net.sf.jsqlparser.statement.lock.LockStatement;
18+
import net.sf.jsqlparser.test.TestUtils;
19+
import net.sf.jsqlparser.util.deparser.CreateIndexDeParser;
20+
import org.junit.jupiter.api.Test;
21+
import org.junit.jupiter.params.ParameterizedTest;
22+
import org.junit.jupiter.params.provider.ValueSource;
23+
24+
import java.util.List;
25+
26+
import static org.junit.jupiter.api.Assertions.*;
27+
28+
class MySQLIndexOptionsTest {
29+
@Test
30+
void parsesAlgorithmAndLockOptionsInEitherOrder() throws Exception {
31+
for (String algorithm : List.of("DEFAULT", "INPLACE", "COPY")) {
32+
for (String lock : List.of("DEFAULT", "NONE", "SHARED", "EXCLUSIVE")) {
33+
for (String equals : List.of(" ", " = ")) {
34+
String algorithmOption = "ALGORITHM" + equals + algorithm;
35+
String lockOption = "LOCK" + equals + lock;
36+
for (String options : List.of(algorithmOption + " " + lockOption,
37+
lockOption + " " + algorithmOption)) {
38+
for (String statement : List.of("CREATE INDEX idx ON t (id) ",
39+
"DROP INDEX idx ON t ")) {
40+
TestUtils.assertSqlCanBeParsedAndDeparsed(statement + options, true);
41+
}
42+
}
43+
}
44+
}
45+
}
46+
}
47+
48+
@Test
49+
void preservesLegacyOptionTokensAndTheirOrder() throws Exception {
50+
CreateIndex create = (CreateIndex) CCJSqlParserUtil.parse(
51+
"CREATE INDEX idx ON t (id) LOCK EXCLUSIVE ALGORITHM = DEFAULT");
52+
assertEquals(List.of("LOCK", "EXCLUSIVE", "ALGORITHM", "=", "DEFAULT"),
53+
create.getTailParameters());
54+
Drop drop = (Drop) CCJSqlParserUtil.parse(
55+
"DROP INDEX idx ON t LOCK = DEFAULT ALGORITHM COPY");
56+
assertEquals(List.of("ON", "t", "LOCK", "=", "DEFAULT", "ALGORITHM", "COPY"),
57+
drop.getParameters());
58+
assertEquals(create.toString(), CCJSqlParserUtil.parse(create.toString()).toString());
59+
assertEquals(drop.toString(), CCJSqlParserUtil.parse(drop.toString()).toString());
60+
}
61+
62+
@ParameterizedTest
63+
@ValueSource(strings = {"CREATE INDEX idx ON t (id)", "DROP INDEX idx ON t"})
64+
void keepsFollowingLockStatementSeparate(String index) throws Exception {
65+
Statements statements = CCJSqlParserUtil.parseStatements(
66+
index + "; LOCK TABLE t IN SHARE MODE");
67+
assertEquals(2, statements.size());
68+
assertInstanceOf(LockStatement.class, statements.get(1));
69+
net.sf.jsqlparser.parser.CCJSqlParser parser = CCJSqlParserUtil.newParser(
70+
index + " LOCK TABLE t IN SHARE MODE");
71+
parser.SingleStatement();
72+
assertEquals(net.sf.jsqlparser.parser.CCJSqlParserConstants.K_LOCK,
73+
parser.getNextToken().kind);
74+
}
75+
76+
@ParameterizedTest
77+
@ValueSource(strings = {"CREATE INDEX idx ON t (id) LOCK", "DROP INDEX idx ON t LOCK =",
78+
"CREATE INDEX idx ON t (id) ALGORITHM =", "DROP INDEX idx ON t ALGORITHM"})
79+
void rejectsMissingOptionValues(String sql) {
80+
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql));
81+
}
82+
83+
@ParameterizedTest
84+
@ValueSource(strings = {
85+
"CREATE INDEX idx ON t (id) LOCK EXCLUSIVE ALGORITHM DEFAULT",
86+
"CREATE INDEX idx USING BTREE ON t (id) KEY_BLOCK_SIZE = 8 COMMENT 'test' INVISIBLE",
87+
"CREATE INDEX idx ON t (id) parallel compress nologging",
88+
"CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS idx ON ONLY t USING btree (id DESC) INCLUDE (value) NULLS NOT DISTINCT WITH (fillfactor = 80) TABLESPACE fast WHERE active"
89+
})
90+
void sharesRenderingForMySqlPostgreSqlAndLegacyTails(String sql) throws Exception {
91+
CreateIndex create = (CreateIndex) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true);
92+
create.getIndex().setName("renamed");
93+
StringBuilder deparsed = new StringBuilder("prefix ");
94+
new CreateIndexDeParser(deparsed).deParse(create);
95+
assertEquals("prefix " + create, deparsed.toString());
96+
assertEquals(create.toString(), CCJSqlParserUtil.parse(create.toString()).toString());
97+
}
98+
}

0 commit comments

Comments
 (0)