From 39bd1f62f5613856860dc3861ccba27c79b43106 Mon Sep 17 00:00:00 2001 From: minleejae <55116920+minleejae@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:17:10 +0900 Subject: [PATCH 1/3] 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 --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 21 +++++++++++++++++-- .../jsqlparser/statement/alter/AlterTest.java | 8 +++++++ .../statement/create/CreateIndexTest.java | 17 +++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 0bdead858..c510d84b0 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -10980,6 +10980,7 @@ List ColumnNamesWithParamsList() : { Index.ColumnParams IndexColumnWithParams(): { String columnName = null; List parameter = null; + List param = null; Expression expression = null; Index.ColumnParams column = null; } @@ -10987,14 +10988,30 @@ Index.ColumnParams IndexColumnWithParams(): { ( columnName=RelObjectName() { parameter = null; } - [ parameter = CreateParameter() ] + // MySQL allows a key part to carry a prefix length and a direction, e.g. "c1(20) DESC", + // so more than one parameter has to be collected here. + ( LOOKAHEAD(2) param = CreateParameter() + { + if (parameter == null) { + parameter = new ArrayList(); + } + parameter.addAll(param); + } + )* { column = new Index.ColumnParams(columnName, parameter); } | "(" expression=Expression() ")" { parameter = null; } - [ LOOKAHEAD(2) parameter = CreateParameter() ] + ( LOOKAHEAD(2) param = CreateParameter() + { + if (parameter == null) { + parameter = new ArrayList(); + } + parameter.addAll(param); + } + )* { column = new Index.ColumnParams(expression, parameter); } diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java index f9a18ec4c..d9344e1b4 100644 --- a/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java @@ -2406,4 +2406,12 @@ public void testAlterTableAddConstraintPrimaryKeyUsingIndexNameAndTablespace() assertSqlCanBeParsedAndDeparsed(sql); } + + @Test + public void testAlterTableAddIndexKeyPartWithPrefixLengthAndDirectionIssue2490() + throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("ALTER TABLE t ADD INDEX i05 (c1 (20) DESC)"); + assertSqlCanBeParsedAndDeparsed("ALTER TABLE t ADD INDEX i33 (c1 (20) ASC)"); + assertSqlCanBeParsedAndDeparsed("ALTER TABLE t ADD UNIQUE INDEX i34 (c1 (10) DESC)"); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTest.java b/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTest.java index 40b7504f7..9db294826 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTest.java @@ -179,4 +179,21 @@ public void testCreateIndexVisibility() throws JSQLParserException { public void testCreateIndexIncludeIssue2459() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed("CREATE INDEX idx_a ON t1 (a) INCLUDE (b, c)"); } + + @Test + public void testCreateIndexKeyPartWithPrefixLengthAndDirectionIssue2490() + throws JSQLParserException { + // MySQL writes the prefix length without a space, JSqlParser deparses it with one. + String statement = "CREATE INDEX i03 ON t (c1(20) DESC)"; + CreateIndex createIndex = (CreateIndex) parserManager.parse(new StringReader(statement)); + + List params = createIndex.getIndex().getColumns().get(0).getParams(); + assertEquals(2, params.size()); + assertEquals("(20)", params.get(0)); + assertEquals("DESC", params.get(1)); + + assertSqlCanBeParsedAndDeparsed("CREATE INDEX i03 ON t (c1 (20) DESC)"); + assertSqlCanBeParsedAndDeparsed("CREATE INDEX i04 ON t (c1 (20) ASC, c2 (10) DESC)"); + assertSqlCanBeParsedAndDeparsed("CREATE UNIQUE INDEX i25 ON t (c1 (10) DESC)"); + } } From 9f72b55af56dbff00666e9ed310f44073c3db9ec Mon Sep 17 00:00:00 2001 From: minleejae <55116920+minleejae@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:54:57 +0900 Subject: [PATCH 2/3] 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 --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 13 +++++++ .../statement/create/CreateIndexTest.java | 38 +++++++++++++++++++ .../jsqlparser/statement/drop/DropTest.java | 17 +++++++++ 3 files changed, 68 insertions(+) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index c510d84b0..63013278f 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -11863,6 +11863,11 @@ List CreateParameter(): | tk= | tk= | tk= | tk= | tk= | tk = | tk = | tk= | tk= | tk= | tk= | tk= | tk= | tk= + // MySQL index_option / algorithm_option / lock_option keywords, e.g. the trailing + // "KEY_BLOCK_SIZE = 8 ALGORITHM = INPLACE LOCK = NONE" of CREATE INDEX, the + // "WITH PARSER" option, and the FULLTEXT / SPATIAL index types of CREATE INDEX. + | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk="=" ) { param.add(tk.image); } @@ -12038,11 +12043,19 @@ Drop Drop(): ( ( tk= | tk= | tk= + // MySQL DROP INDEX accepts a trailing algorithm_option / lock_option, + // e.g. "DROP INDEX i ON t ALGORITHM = INPLACE LOCK = NONE". + | tk= | tk= | tk="=" ) { dropArgs.add(tk.image); } | ( name = Table() { dropArgs.add("ON"); dropArgs.add(name.toString()); } ) + | + // The lock_option of DROP INDEX. LOCK also starts a LOCK TABLE statement, so it is only + // taken as a DROP argument when it cannot be the beginning of the next statement. + LOOKAHEAD({ getToken(1).kind == K_LOCK && getToken(2).kind != K_TABLE }) + tk= { dropArgs.add(tk.image); } )* { if (dropArgs.size() > 0) { diff --git a/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTest.java b/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTest.java index 9db294826..dea34d192 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTest.java @@ -196,4 +196,42 @@ public void testCreateIndexKeyPartWithPrefixLengthAndDirectionIssue2490() assertSqlCanBeParsedAndDeparsed("CREATE INDEX i04 ON t (c1 (20) ASC, c2 (10) DESC)"); assertSqlCanBeParsedAndDeparsed("CREATE UNIQUE INDEX i25 ON t (c1 (10) DESC)"); } + + @Test + public void testCreateIndexKeyBlockSizeIssue2490() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("CREATE INDEX i08 ON t (c1) KEY_BLOCK_SIZE = 8"); + assertSqlCanBeParsedAndDeparsed("CREATE INDEX i09 ON t (c1) KEY_BLOCK_SIZE 8"); + assertSqlCanBeParsedAndDeparsed( + "CREATE INDEX i14 ON t (c1) USING BTREE KEY_BLOCK_SIZE = 8 COMMENT 'combo' INVISIBLE"); + } + + @Test + public void testCreateIndexAlgorithmAndLockOptionsIssue2490() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed( + "CREATE INDEX i10 ON t (c1) ALGORITHM = INPLACE LOCK = NONE"); + assertSqlCanBeParsedAndDeparsed("CREATE INDEX i11 ON t (c1) ALGORITHM INPLACE LOCK NONE"); + assertSqlCanBeParsedAndDeparsed("CREATE INDEX i12 ON t (c1) ALGORITHM = INPLACE"); + assertSqlCanBeParsedAndDeparsed("CREATE INDEX i13 ON t (c1) LOCK = NONE"); + + CreateIndex createIndex = (CreateIndex) parserManager + .parse(new StringReader("CREATE INDEX i10 ON t (c1) ALGORITHM=INPLACE LOCK=NONE")); + assertEquals(List.of("ALGORITHM", "=", "INPLACE", "LOCK", "=", "NONE"), + createIndex.getTailParameters()); + } + + @Test + public void testCreateFullTextAndSpatialIndexIssue2490() throws JSQLParserException { + // These used to fall back to UnsupportedStatement instead of producing a CreateIndex. + CreateIndex fullText = (CreateIndex) parserManager + .parse(new StringReader("CREATE FULLTEXT INDEX i17 ON t (body)")); + assertEquals("FULLTEXT", fullText.getIndex().getType()); + + CreateIndex spatial = (CreateIndex) parserManager + .parse(new StringReader("CREATE SPATIAL INDEX i19 ON t (g)")); + assertEquals("SPATIAL", spatial.getIndex().getType()); + + assertSqlCanBeParsedAndDeparsed("CREATE FULLTEXT INDEX i17 ON t (body)"); + assertSqlCanBeParsedAndDeparsed("CREATE FULLTEXT INDEX i18 ON t (body) WITH PARSER ngram"); + assertSqlCanBeParsedAndDeparsed("CREATE SPATIAL INDEX i19 ON t (g)"); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/drop/DropTest.java b/src/test/java/net/sf/jsqlparser/statement/drop/DropTest.java index a48b2fe7d..a692273f2 100644 --- a/src/test/java/net/sf/jsqlparser/statement/drop/DropTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/drop/DropTest.java @@ -12,8 +12,10 @@ import java.io.StringReader; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.parser.CCJSqlParserManager; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.Statements; import static net.sf.jsqlparser.test.TestUtils.*; import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Test; @@ -152,4 +154,19 @@ void dropTemporaryTableTestIssue1712() throws JSQLParserException { String sqlStr = "drop temporary table if exists tmp_MwYT8N0z"; assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + + @Test + public void testDropIndexAlgorithmAndLockOptionsIssue2490() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("DROP INDEX i15 ON t ALGORITHM = INPLACE LOCK = NONE"); + assertSqlCanBeParsedAndDeparsed("DROP INDEX i16 ON t ALGORITHM INPLACE"); + assertSqlCanBeParsedAndDeparsed("DROP INDEX i17 ON t LOCK = NONE"); + } + + @Test + public void testDropTableFollowedByLockTableIssue2490() throws JSQLParserException { + // LOCK must not be swallowed as a DROP argument when it starts the next statement. + Statements statements = CCJSqlParserUtil.parseStatements( + "DROP TABLE t1; LOCK TABLE t2 IN SHARE MODE;"); + assertEquals(2, statements.size()); + } } From ec61e7f1c261827791c0077d44afbb82e8da3e0d Mon Sep 17 00:00:00 2001 From: minleejae <55116920+minleejae@users.noreply.github.com> Date: Mon, 24 Aug 2026 19:33:02 +0900 Subject: [PATCH 3/3] 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. --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 31 ++++++------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 63013278f..de05f830d 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -10979,41 +10979,30 @@ List ColumnNamesWithParamsList() : { Index.ColumnParams IndexColumnWithParams(): { String columnName = null; + // the options collected for this key part, and the result of a single CreateParameter() + List columnParams = new ArrayList(); List parameter = null; - List param = null; Expression expression = null; Index.ColumnParams column = null; } { ( columnName=RelObjectName() - { parameter = null; } // MySQL allows a key part to carry a prefix length and a direction, e.g. "c1(20) DESC", // so more than one parameter has to be collected here. - ( LOOKAHEAD(2) param = CreateParameter() - { - if (parameter == null) { - parameter = new ArrayList(); - } - parameter.addAll(param); - } - )* + ( LOOKAHEAD(2) parameter = CreateParameter() { columnParams.addAll(parameter); } )* { - column = new Index.ColumnParams(columnName, parameter); + // ColumnParams renders a separating space for a non-null list, so a key part + // without options has to be given null rather than an empty list. + column = new Index.ColumnParams(columnName, + columnParams.isEmpty() ? null : columnParams); } | "(" expression=Expression() ")" - { parameter = null; } - ( LOOKAHEAD(2) param = CreateParameter() - { - if (parameter == null) { - parameter = new ArrayList(); - } - parameter.addAll(param); - } - )* + ( LOOKAHEAD(2) parameter = CreateParameter() { columnParams.addAll(parameter); } )* { - column = new Index.ColumnParams(expression, parameter); + column = new Index.ColumnParams(expression, + columnParams.isEmpty() ? null : columnParams); } ) {