Skip to content

Commit 9fc38bd

Browse files
authored
fix(parser): support MySQL index options and prefix-length key parts in index DDL (#2505)
* fix(parser): support index key parts with a prefix length and a direction MySQL allows a key part to carry both a prefix length and a sort direction, e.g. "CREATE INDEX i ON t (c1(20) DESC)". IndexColumnWithParams() accepted at most one optional CreateParameter() per key part, so the prefix length consumed it and the following ASC/DESC could not be matched. Collect the parameters in a loop instead. IndexColumnsWithParamsList() is shared by the CREATE INDEX and the ALTER TABLE ADD INDEX paths, so both are fixed. Refs #2490 * fix(parser): support MySQL index options in CREATE INDEX and DROP INDEX Several valid MySQL index DDL statements were rejected because their option keywords are tokens of their own and were therefore not reachable from the option lists that CREATE INDEX and DROP INDEX use: CREATE INDEX i ON t (c1) KEY_BLOCK_SIZE = 8 CREATE INDEX i ON t (c1) ALGORITHM = INPLACE LOCK = NONE CREATE FULLTEXT INDEX i ON t (body) WITH PARSER ngram CREATE SPATIAL INDEX i ON t (g) DROP INDEX i ON t ALGORITHM = INPLACE LOCK = NONE The keywords are added to the existing flat token lists of CreateParameter() and Drop() rather than as new grammar alternatives, so no new choice is introduced and the JavaCC warning count is unchanged. LOCK is the one exception: it also starts a LOCK TABLE statement, so taking it unconditionally as a DROP argument would be ambiguous with the next statement. It is guarded by a semantic lookahead that only accepts it when it is not followed by TABLE. Refs #2490 * refactor(parser): clarify key part option variable names in IndexColumnWithParams Rename the accumulator to columnParams and keep parameter for the result of a single CreateParameter(), matching how the other CreateParameter() loops in the grammar name them. Collect eagerly and pass null only when no option was parsed, because ColumnParams renders a separating space for a non-null list.
1 parent 4658415 commit 9fc38bd

4 files changed

Lines changed: 105 additions & 6 deletions

File tree

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

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10979,24 +10979,30 @@ List<Index.ColumnParams> ColumnNamesWithParamsList() : {
1097910979

1098010980
Index.ColumnParams IndexColumnWithParams(): {
1098110981
String columnName = null;
10982+
// the options collected for this key part, and the result of a single CreateParameter()
10983+
List<String> columnParams = new ArrayList<String>();
1098210984
List<String> parameter = null;
1098310985
Expression expression = null;
1098410986
Index.ColumnParams column = null;
1098510987
}
1098610988
{
1098710989
(
1098810990
columnName=RelObjectName()
10989-
{ parameter = null; }
10990-
[ parameter = CreateParameter() ]
10991+
// MySQL allows a key part to carry a prefix length and a direction, e.g. "c1(20) DESC",
10992+
// so more than one parameter has to be collected here.
10993+
( LOOKAHEAD(2) parameter = CreateParameter() { columnParams.addAll(parameter); } )*
1099110994
{
10992-
column = new Index.ColumnParams(columnName, parameter);
10995+
// ColumnParams renders a separating space for a non-null list, so a key part
10996+
// without options has to be given null rather than an empty list.
10997+
column = new Index.ColumnParams(columnName,
10998+
columnParams.isEmpty() ? null : columnParams);
1099310999
}
1099411000
|
1099511001
"(" expression=Expression() ")"
10996-
{ parameter = null; }
10997-
[ LOOKAHEAD(2) parameter = CreateParameter() ]
11002+
( LOOKAHEAD(2) parameter = CreateParameter() { columnParams.addAll(parameter); } )*
1099811003
{
10999-
column = new Index.ColumnParams(expression, parameter);
11004+
column = new Index.ColumnParams(expression,
11005+
columnParams.isEmpty() ? null : columnParams);
1100011006
}
1100111007
)
1100211008
{
@@ -11846,6 +11852,11 @@ List<String> CreateParameter():
1184611852
| tk=<K_TIME_KEY_EXPR> | tk=<K_RAW> | tk=<K_HASH> | tk=<K_FIRST> | tk=<K_LAST> | tk = <K_SIGNED> | tk = <K_UNSIGNED>
1184711853
| tk=<K_ENGINE> | tk=<K_IDENTITY> | tk=<K_MATERIALIZED> | tk=<K_SAMPLE> | tk=<K_ALWAYS>
1184811854
| tk=<K_VISIBLE> | tk=<K_INVISIBLE>
11855+
// MySQL index_option / algorithm_option / lock_option keywords, e.g. the trailing
11856+
// "KEY_BLOCK_SIZE = 8 ALGORITHM = INPLACE LOCK = NONE" of CREATE INDEX, the
11857+
// "WITH PARSER" option, and the FULLTEXT / SPATIAL index types of CREATE INDEX.
11858+
| tk=<K_KEY_BLOCK_SIZE> | tk=<K_ALGORITHM> | tk=<K_LOCK> | tk=<K_NONE>
11859+
| tk=<K_PARSER> | tk=<K_FULLTEXT> | tk=<K_SPATIAL>
1184911860
| tk="="
1185011861
)
1185111862
{ param.add(tk.image); }
@@ -12021,11 +12032,19 @@ Drop Drop():
1202112032
(
1202212033
(
1202312034
tk=<S_IDENTIFIER> | tk=<K_CASCADE> | tk=<K_RESTRICT>
12035+
// MySQL DROP INDEX accepts a trailing algorithm_option / lock_option,
12036+
// e.g. "DROP INDEX i ON t ALGORITHM = INPLACE LOCK = NONE".
12037+
| tk=<K_ALGORITHM> | tk=<K_NONE> | tk="="
1202412038
) { dropArgs.add(tk.image); }
1202512039
|
1202612040
(
1202712041
<K_ON> name = Table() { dropArgs.add("ON"); dropArgs.add(name.toString()); }
1202812042
)
12043+
|
12044+
// The lock_option of DROP INDEX. LOCK also starts a LOCK TABLE statement, so it is only
12045+
// taken as a DROP argument when it cannot be the beginning of the next statement.
12046+
LOOKAHEAD({ getToken(1).kind == K_LOCK && getToken(2).kind != K_TABLE })
12047+
tk=<K_LOCK> { dropArgs.add(tk.image); }
1202912048
)*
1203012049
{
1203112050
if (dropArgs.size() > 0) {

src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2406,4 +2406,12 @@ public void testAlterTableAddConstraintPrimaryKeyUsingIndexNameAndTablespace()
24062406

24072407
assertSqlCanBeParsedAndDeparsed(sql);
24082408
}
2409+
2410+
@Test
2411+
public void testAlterTableAddIndexKeyPartWithPrefixLengthAndDirectionIssue2490()
2412+
throws JSQLParserException {
2413+
assertSqlCanBeParsedAndDeparsed("ALTER TABLE t ADD INDEX i05 (c1 (20) DESC)");
2414+
assertSqlCanBeParsedAndDeparsed("ALTER TABLE t ADD INDEX i33 (c1 (20) ASC)");
2415+
assertSqlCanBeParsedAndDeparsed("ALTER TABLE t ADD UNIQUE INDEX i34 (c1 (10) DESC)");
2416+
}
24092417
}

src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTest.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,59 @@ public void testCreateIndexVisibility() throws JSQLParserException {
179179
public void testCreateIndexIncludeIssue2459() throws JSQLParserException {
180180
assertSqlCanBeParsedAndDeparsed("CREATE INDEX idx_a ON t1 (a) INCLUDE (b, c)");
181181
}
182+
183+
@Test
184+
public void testCreateIndexKeyPartWithPrefixLengthAndDirectionIssue2490()
185+
throws JSQLParserException {
186+
// MySQL writes the prefix length without a space, JSqlParser deparses it with one.
187+
String statement = "CREATE INDEX i03 ON t (c1(20) DESC)";
188+
CreateIndex createIndex = (CreateIndex) parserManager.parse(new StringReader(statement));
189+
190+
List<String> params = createIndex.getIndex().getColumns().get(0).getParams();
191+
assertEquals(2, params.size());
192+
assertEquals("(20)", params.get(0));
193+
assertEquals("DESC", params.get(1));
194+
195+
assertSqlCanBeParsedAndDeparsed("CREATE INDEX i03 ON t (c1 (20) DESC)");
196+
assertSqlCanBeParsedAndDeparsed("CREATE INDEX i04 ON t (c1 (20) ASC, c2 (10) DESC)");
197+
assertSqlCanBeParsedAndDeparsed("CREATE UNIQUE INDEX i25 ON t (c1 (10) DESC)");
198+
}
199+
200+
@Test
201+
public void testCreateIndexKeyBlockSizeIssue2490() throws JSQLParserException {
202+
assertSqlCanBeParsedAndDeparsed("CREATE INDEX i08 ON t (c1) KEY_BLOCK_SIZE = 8");
203+
assertSqlCanBeParsedAndDeparsed("CREATE INDEX i09 ON t (c1) KEY_BLOCK_SIZE 8");
204+
assertSqlCanBeParsedAndDeparsed(
205+
"CREATE INDEX i14 ON t (c1) USING BTREE KEY_BLOCK_SIZE = 8 COMMENT 'combo' INVISIBLE");
206+
}
207+
208+
@Test
209+
public void testCreateIndexAlgorithmAndLockOptionsIssue2490() throws JSQLParserException {
210+
assertSqlCanBeParsedAndDeparsed(
211+
"CREATE INDEX i10 ON t (c1) ALGORITHM = INPLACE LOCK = NONE");
212+
assertSqlCanBeParsedAndDeparsed("CREATE INDEX i11 ON t (c1) ALGORITHM INPLACE LOCK NONE");
213+
assertSqlCanBeParsedAndDeparsed("CREATE INDEX i12 ON t (c1) ALGORITHM = INPLACE");
214+
assertSqlCanBeParsedAndDeparsed("CREATE INDEX i13 ON t (c1) LOCK = NONE");
215+
216+
CreateIndex createIndex = (CreateIndex) parserManager
217+
.parse(new StringReader("CREATE INDEX i10 ON t (c1) ALGORITHM=INPLACE LOCK=NONE"));
218+
assertEquals(List.of("ALGORITHM", "=", "INPLACE", "LOCK", "=", "NONE"),
219+
createIndex.getTailParameters());
220+
}
221+
222+
@Test
223+
public void testCreateFullTextAndSpatialIndexIssue2490() throws JSQLParserException {
224+
// These used to fall back to UnsupportedStatement instead of producing a CreateIndex.
225+
CreateIndex fullText = (CreateIndex) parserManager
226+
.parse(new StringReader("CREATE FULLTEXT INDEX i17 ON t (body)"));
227+
assertEquals("FULLTEXT", fullText.getIndex().getType());
228+
229+
CreateIndex spatial = (CreateIndex) parserManager
230+
.parse(new StringReader("CREATE SPATIAL INDEX i19 ON t (g)"));
231+
assertEquals("SPATIAL", spatial.getIndex().getType());
232+
233+
assertSqlCanBeParsedAndDeparsed("CREATE FULLTEXT INDEX i17 ON t (body)");
234+
assertSqlCanBeParsedAndDeparsed("CREATE FULLTEXT INDEX i18 ON t (body) WITH PARSER ngram");
235+
assertSqlCanBeParsedAndDeparsed("CREATE SPATIAL INDEX i19 ON t (g)");
236+
}
182237
}

src/test/java/net/sf/jsqlparser/statement/drop/DropTest.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
import java.io.StringReader;
1313
import net.sf.jsqlparser.JSQLParserException;
1414
import net.sf.jsqlparser.parser.CCJSqlParserManager;
15+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
1516
import net.sf.jsqlparser.schema.Table;
1617
import net.sf.jsqlparser.statement.Statement;
18+
import net.sf.jsqlparser.statement.Statements;
1719
import static net.sf.jsqlparser.test.TestUtils.*;
1820
import static org.junit.jupiter.api.Assertions.assertEquals;
1921
import org.junit.jupiter.api.Test;
@@ -152,4 +154,19 @@ void dropTemporaryTableTestIssue1712() throws JSQLParserException {
152154
String sqlStr = "drop temporary table if exists tmp_MwYT8N0z";
153155
assertSqlCanBeParsedAndDeparsed(sqlStr, true);
154156
}
157+
158+
@Test
159+
public void testDropIndexAlgorithmAndLockOptionsIssue2490() throws JSQLParserException {
160+
assertSqlCanBeParsedAndDeparsed("DROP INDEX i15 ON t ALGORITHM = INPLACE LOCK = NONE");
161+
assertSqlCanBeParsedAndDeparsed("DROP INDEX i16 ON t ALGORITHM INPLACE");
162+
assertSqlCanBeParsedAndDeparsed("DROP INDEX i17 ON t LOCK = NONE");
163+
}
164+
165+
@Test
166+
public void testDropTableFollowedByLockTableIssue2490() throws JSQLParserException {
167+
// LOCK must not be swallowed as a DROP argument when it starts the next statement.
168+
Statements statements = CCJSqlParserUtil.parseStatements(
169+
"DROP TABLE t1; LOCK TABLE t2 IN SHARE MODE;");
170+
assertEquals(2, statements.size());
171+
}
155172
}

0 commit comments

Comments
 (0)