From 28e132cd0f93c1a4223100d62f9d4b02a07dd7a7 Mon Sep 17 00:00:00 2001 From: Minjae Lee Date: Thu, 10 Sep 2026 18:43:40 +0900 Subject: [PATCH 1/3] Complete MySQL index algorithm and lock options and share CREATE INDEX rendering --- .../statement/create/index/CreateIndex.java | 19 ++-- .../util/deparser/CreateIndexDeParser.java | 74 +------------- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 37 ++++++- .../create/MySQLIndexOptionsTest.java | 98 +++++++++++++++++++ 4 files changed, 145 insertions(+), 83 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/statement/create/MySQLIndexOptionsTest.java diff --git a/src/main/java/net/sf/jsqlparser/statement/create/index/CreateIndex.java b/src/main/java/net/sf/jsqlparser/statement/create/index/CreateIndex.java index c8156b5e8..6787220b1 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/index/CreateIndex.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/index/CreateIndex.java @@ -137,8 +137,11 @@ public void setTailParameters(List tailParameters) { @Override public String toString() { - StringBuilder buffer = new StringBuilder(); + return appendTo(new StringBuilder()).toString(); + } + /** Shared rendering for the statement model and CreateIndexDeParser. */ + public StringBuilder appendTo(StringBuilder buffer) { buffer.append("CREATE "); if (index.getType() != null) { @@ -182,16 +185,14 @@ public String toString() { buffer.append(")"); - appendPostgreSqlTail(buffer); - - if (tailParameters != null) { - for (String param : tailParameters) { - buffer.append(" ").append(param); - } + } + appendPostgreSqlTail(buffer); + if (tailParameters != null) { + for (String param : tailParameters) { + buffer.append(" ").append(param); } } - - return buffer.toString(); + return buffer; } private void appendPostgreSqlTail(StringBuilder buffer) { diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/CreateIndexDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/CreateIndexDeParser.java index b5c806197..0b985ac24 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/CreateIndexDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/CreateIndexDeParser.java @@ -9,11 +9,7 @@ */ package net.sf.jsqlparser.util.deparser; -import static java.util.stream.Collectors.joining; - import net.sf.jsqlparser.statement.create.index.CreateIndex; -import net.sf.jsqlparser.statement.create.table.Index; -import net.sf.jsqlparser.statement.select.PlainSelect; public class CreateIndexDeParser extends AbstractDeParser { @@ -23,74 +19,6 @@ public CreateIndexDeParser(StringBuilder buffer) { @Override public void deParse(CreateIndex createIndex) { - Index index = createIndex.getIndex(); - - builder.append("CREATE "); - - if (index.getType() != null) { - builder.append(index.getType()); - builder.append(" "); - } - - builder.append("INDEX "); - if (createIndex.isConcurrently()) { - builder.append("CONCURRENTLY "); - } - if (createIndex.isUsingIfNotExists()) { - builder.append("IF NOT EXISTS "); - } - if (index.getName() != null) { - builder.append(index.getName()).append(" "); - } - - String using = index.getUsing(); - if (using != null && createIndex.isIndexTypeBeforeOn()) { - builder.append("USING ").append(using).append(" "); - } - - builder.append("ON "); - if (createIndex.isOnly()) { - builder.append("ONLY "); - } - builder.append(createIndex.getTable().getFullyQualifiedName()); - - if (using != null && !createIndex.isIndexTypeBeforeOn()) { - builder.append(" USING "); - builder.append(using); - } - - if (index.getColumnsNames() != null) { - builder.append(" ("); - builder.append(index.getColumnWithParams().stream() - .map(Index.ColumnParams::toString) - .collect(joining(", "))); - builder.append(")"); - } - - if (createIndex.getIncludeColumns() != null) { - builder.append(" INCLUDE (") - .append(String.join(", ", createIndex.getIncludeColumns())).append(")"); - } - if (createIndex.getNullsDistinct() != null) { - builder.append(" NULLS ") - .append(createIndex.getNullsDistinct() ? "DISTINCT" : "NOT DISTINCT"); - } - if (createIndex.getStorageParameters() != null) { - builder.append(" WITH ").append(PlainSelect.getStringList( - createIndex.getStorageParameters(), true, true)); - } - if (createIndex.getTableSpace() != null) { - builder.append(" TABLESPACE ").append(createIndex.getTableSpace()); - } - if (createIndex.getWhere() != null) { - builder.append(" WHERE ").append(createIndex.getWhere()); - } - - if (createIndex.getTailParameters() != null) { - for (String param : createIndex.getTailParameters()) { - builder.append(" ").append(param); - } - } + createIndex.appendTo(builder); } - } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index fbff17c98..b883c375e 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -11725,7 +11725,8 @@ CreateIndex CreateIndex(): [ LOOKAHEAD(2) tableSpace=RelObjectName() { createIndex.setTableSpace(tableSpace); } ] [ LOOKAHEAD(2) where=Expression() { createIndex.setWhere(where); } ] - ( LOOKAHEAD(2) parameter=CreateParameter() { tailParameters.addAll(parameter); } )* + ( LOOKAHEAD(2, { getToken(1).kind != K_LOCK || getToken(2).kind != K_TABLE }) + parameter=CreateIndexTailOption() { tailParameters.addAll(parameter); } )* { index.setColumns(colNames); createIndex.setIndex(index); @@ -11735,6 +11736,35 @@ CreateIndex CreateIndex(): } } +List CreateIndexTailOption(): +{ + List option; +} +{ + ( + LOOKAHEAD({ getToken(1).kind == K_ALGORITHM || getToken(1).kind == K_LOCK }) + option=IndexAlgorithmOrLockOption() + | + option=CreateParameter() + ) + { return option; } +} + +// CREATE INDEX and DROP INDEX share MySQL's algorithm_option / lock_option syntax. +// Keep the legacy token list, including optional equals signs and the original order. +List IndexAlgorithmOrLockOption(): +{ + List option = new ArrayList(); + Token keyword; + Token value; +} +{ + ( keyword= | keyword= ) { option.add(keyword.image); } + [ "=" { option.add("="); } ] + ( value= | value= | value= | value= ) + { option.add(value.image); return option; } +} + List ColumnDefinitionParameter(): { Token tk = null; List parameter = new ArrayList(); @@ -13533,6 +13563,7 @@ Drop Drop(): Table name; List dropArgs = new ArrayList(); List funcArgs = null; + List indexOption; boolean useTemporary = false; } { @@ -13569,6 +13600,10 @@ Drop Drop(): ( "," name = Table() { drop.addNames(name); } )* [ LOOKAHEAD(2) funcArgs = FuncArgsList() ] ( + LOOKAHEAD({ getToken(1).kind == K_ALGORITHM + || (getToken(1).kind == K_LOCK && getToken(2).kind != K_TABLE) }) + indexOption=IndexAlgorithmOrLockOption() { dropArgs.addAll(indexOption); } + | ( tk= | tk= | tk= // MySQL DROP INDEX accepts a trailing algorithm_option / lock_option, diff --git a/src/test/java/net/sf/jsqlparser/statement/create/MySQLIndexOptionsTest.java b/src/test/java/net/sf/jsqlparser/statement/create/MySQLIndexOptionsTest.java new file mode 100644 index 000000000..efb30097f --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/MySQLIndexOptionsTest.java @@ -0,0 +1,98 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.create; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.create.index.CreateIndex; +import net.sf.jsqlparser.statement.drop.Drop; +import net.sf.jsqlparser.statement.lock.LockStatement; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.deparser.CreateIndexDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class MySQLIndexOptionsTest { + @Test + void parsesAlgorithmAndLockOptionsInEitherOrder() throws Exception { + for (String algorithm : List.of("DEFAULT", "INPLACE", "COPY")) { + for (String lock : List.of("DEFAULT", "NONE", "SHARED", "EXCLUSIVE")) { + for (String equals : List.of(" ", " = ")) { + String algorithmOption = "ALGORITHM" + equals + algorithm; + String lockOption = "LOCK" + equals + lock; + for (String options : List.of(algorithmOption + " " + lockOption, + lockOption + " " + algorithmOption)) { + for (String statement : List.of("CREATE INDEX idx ON t (id) ", + "DROP INDEX idx ON t ")) { + TestUtils.assertSqlCanBeParsedAndDeparsed(statement + options, true); + } + } + } + } + } + } + + @Test + void preservesLegacyOptionTokensAndTheirOrder() throws Exception { + CreateIndex create = (CreateIndex) CCJSqlParserUtil.parse( + "CREATE INDEX idx ON t (id) LOCK EXCLUSIVE ALGORITHM = DEFAULT"); + assertEquals(List.of("LOCK", "EXCLUSIVE", "ALGORITHM", "=", "DEFAULT"), + create.getTailParameters()); + Drop drop = (Drop) CCJSqlParserUtil.parse( + "DROP INDEX idx ON t LOCK = DEFAULT ALGORITHM COPY"); + assertEquals(List.of("ON", "t", "LOCK", "=", "DEFAULT", "ALGORITHM", "COPY"), + drop.getParameters()); + assertEquals(create.toString(), CCJSqlParserUtil.parse(create.toString()).toString()); + assertEquals(drop.toString(), CCJSqlParserUtil.parse(drop.toString()).toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE INDEX idx ON t (id)", "DROP INDEX idx ON t"}) + void keepsFollowingLockStatementSeparate(String index) throws Exception { + Statements statements = CCJSqlParserUtil.parseStatements( + index + "; LOCK TABLE t IN SHARE MODE"); + assertEquals(2, statements.size()); + assertInstanceOf(LockStatement.class, statements.get(1)); + net.sf.jsqlparser.parser.CCJSqlParser parser = CCJSqlParserUtil.newParser( + index + " LOCK TABLE t IN SHARE MODE"); + parser.SingleStatement(); + assertEquals(net.sf.jsqlparser.parser.CCJSqlParserConstants.K_LOCK, + parser.getNextToken().kind); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE INDEX idx ON t (id) LOCK", "DROP INDEX idx ON t LOCK =", + "CREATE INDEX idx ON t (id) ALGORITHM =", "DROP INDEX idx ON t ALGORITHM"}) + void rejectsMissingOptionValues(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE INDEX idx ON t (id) LOCK EXCLUSIVE ALGORITHM DEFAULT", + "CREATE INDEX idx USING BTREE ON t (id) KEY_BLOCK_SIZE = 8 COMMENT 'test' INVISIBLE", + "CREATE INDEX idx ON t (id) parallel compress nologging", + "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" + }) + void sharesRenderingForMySqlPostgreSqlAndLegacyTails(String sql) throws Exception { + CreateIndex create = (CreateIndex) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true); + create.getIndex().setName("renamed"); + StringBuilder deparsed = new StringBuilder("prefix "); + new CreateIndexDeParser(deparsed).deParse(create); + assertEquals("prefix " + create, deparsed.toString()); + assertEquals(create.toString(), CCJSqlParserUtil.parse(create.toString()).toString()); + } +} From 932db10ba8221e6a02256a43f9cddfe37c871de7 Mon Sep 17 00:00:00 2001 From: Minjae Lee Date: Thu, 10 Sep 2026 19:00:46 +0900 Subject: [PATCH 2/3] Split shared index rendering into focused helpers --- .../statement/create/index/CreateIndex.java | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/statement/create/index/CreateIndex.java b/src/main/java/net/sf/jsqlparser/statement/create/index/CreateIndex.java index 6787220b1..194e78fc5 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/index/CreateIndex.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/index/CreateIndex.java @@ -142,13 +142,23 @@ public String toString() { /** Shared rendering for the statement model and CreateIndexDeParser. */ public StringBuilder appendTo(StringBuilder buffer) { - buffer.append("CREATE "); + appendIndexHeader(buffer); + appendIndexTarget(buffer); + appendIndexColumns(buffer); + appendPostgreSqlTail(buffer); + if (tailParameters != null) { + for (String param : tailParameters) { + buffer.append(" ").append(param); + } + } + return buffer; + } + private void appendIndexHeader(StringBuilder buffer) { + buffer.append("CREATE "); if (index.getType() != null) { - buffer.append(index.getType()); - buffer.append(" "); + buffer.append(index.getType()).append(" "); } - buffer.append("INDEX "); if (concurrently) { buffer.append("CONCURRENTLY "); @@ -159,40 +169,30 @@ public StringBuilder appendTo(StringBuilder buffer) { if (index.getName() != null) { buffer.append(index.getName()).append(" "); } + } + private void appendIndexTarget(StringBuilder buffer) { if (index.getUsing() != null && isIndexTypeBeforeOn()) { buffer.append("USING ").append(index.getUsing()).append(" "); } - buffer.append("ON "); if (only) { buffer.append("ONLY "); } buffer.append(table.getFullyQualifiedName()); - if (index.getUsing() != null && !isIndexTypeBeforeOn()) { - buffer.append(" USING "); - buffer.append(index.getUsing()); + buffer.append(" USING ").append(index.getUsing()); } + } + private void appendIndexColumns(StringBuilder buffer) { if (index.getColumnsNames() != null) { buffer.append(" ("); - - buffer.append( - index.getColumns().stream() - .map(Index.ColumnParams::toString) - .collect(joining(", "))); - + buffer.append(index.getColumns().stream() + .map(Index.ColumnParams::toString) + .collect(joining(", "))); buffer.append(")"); - - } - appendPostgreSqlTail(buffer); - if (tailParameters != null) { - for (String param : tailParameters) { - buffer.append(" ").append(param); - } } - return buffer; } private void appendPostgreSqlTail(StringBuilder buffer) { From 4433e3c09dfbcaa8e00775f4d048c3ce99fc2ad8 Mon Sep 17 00:00:00 2001 From: Minjae Lee Date: Thu, 10 Sep 2026 19:32:22 +0900 Subject: [PATCH 3/3] Compare median monotonic durations in bracket growth regression checks --- .../select/NestedBracketsPerformanceTest.java | 61 +++++++++++-------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/src/test/java/net/sf/jsqlparser/statement/select/NestedBracketsPerformanceTest.java b/src/test/java/net/sf/jsqlparser/statement/select/NestedBracketsPerformanceTest.java index fc5bd785d..37a4ef87b 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/NestedBracketsPerformanceTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/NestedBracketsPerformanceTest.java @@ -15,6 +15,8 @@ import org.junit.jupiter.api.Timeout; import java.util.logging.Logger; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; import org.junit.jupiter.api.Assertions; @@ -139,22 +141,23 @@ public void testRecursiveBracketExpressionIssue1019_2() throws JSQLParserExcepti doIncreaseOfParseTimeTesting("IF(1=1, $1, 2)", "1", 20); } - @Test void testIssue2422() throws JSQLParserException { + @Test + void testIssue2422() throws JSQLParserException { String sqlStr = "SELECT\n" - + "\t\t\t\t ((((position('-' IN (\n" - + "\t\t\t\t CASE WHEN ((\n" - + "\t\t\t\t CASE WHEN (5 < 0) THEN\n" - + "\t\t\t\t 'yes'\n" - + "\t\t\t\t ELSE\n" - + "\t\t\t\t 'no'\n" - + "\t\t\t\t END) = 'yes') THEN\n" - + "\t\t\t\t SUBSTRING('2012-january-18', (((LENGTH('2012-january-18')) + (5)) + (1)), ABS((0) - (5)))\n" - + "\t\t\t\t ELSE\n" - + "\t\t\t\t SUBSTRING('2012-january-18', ((5) + (1)))\n" - + "\t\t\t\t END)) - 1) + (1)) - (5)) + (0))\n" - + "\t\t\t\tFROM\n" - + "\t\t\t\t testtable"; + + "\t\t\t\t ((((position('-' IN (\n" + + "\t\t\t\t CASE WHEN ((\n" + + "\t\t\t\t CASE WHEN (5 < 0) THEN\n" + + "\t\t\t\t 'yes'\n" + + "\t\t\t\t ELSE\n" + + "\t\t\t\t 'no'\n" + + "\t\t\t\t END) = 'yes') THEN\n" + + "\t\t\t\t SUBSTRING('2012-january-18', (((LENGTH('2012-january-18')) + (5)) + (1)), ABS((0) - (5)))\n" + + "\t\t\t\t ELSE\n" + + "\t\t\t\t SUBSTRING('2012-january-18', ((5) + (1)))\n" + + "\t\t\t\t END)) - 1) + (1)) - (5)) + (0))\n" + + "\t\t\t\tFROM\n" + + "\t\t\t\t testtable"; assertSqlCanBeParsedAndDeparsed(sqlStr); } @@ -204,18 +207,17 @@ public void testIncreaseOfParseTime() throws JSQLParserException { private void doIncreaseOfParseTimeTesting(String template, String finalExpression, int maxDepth) throws JSQLParserException { - long oldDurationTime = 2000; + long oldDurationTime = TimeUnit.SECONDS.toNanos(2); int countProblematic = 0; for (int i = 0; i < maxDepth; i++) { String sql = "SELECT " + buildRecursiveBracketExpression(template, finalExpression, i) + " FROM mytbl"; - long startTime = System.currentTimeMillis(); - assertSqlCanBeParsedAndDeparsed(sql, true, parser -> parser.withTimeOut(12000)); - long durationTime = System.currentTimeMillis() - startTime; + long durationTime = medianParseDuration(sql); if (i > 0) { - System.out.println("old duration " + oldDurationTime + " new duration time " - + durationTime + " for " + sql); + System.out.println("old duration " + TimeUnit.NANOSECONDS.toMicros(oldDurationTime) + + " us, new duration " + TimeUnit.NANOSECONDS.toMicros(durationTime) + + " us for " + sql); } if (oldDurationTime * 10 < durationTime) { countProblematic++; @@ -228,6 +230,19 @@ private void doIncreaseOfParseTimeTesting(String template, String finalExpressio } } + private long medianParseDuration(String sql) throws JSQLParserException { + // A single millisecond sample can turn scheduler or GC jitter into a tenfold increase. + // Keep the growth limit, but compare representative durations from a monotonic clock. + long[] durations = new long[5]; + for (int sample = 0; sample < durations.length; sample++) { + long startTime = System.nanoTime(); + assertSqlCanBeParsedAndDeparsed(sql, true, parser -> parser.withTimeOut(12000)); + durations[sample] = System.nanoTime() - startTime; + } + Arrays.sort(durations); + return durations[durations.length / 2]; + } + @Test @Timeout(2000) public void testRecursiveBracketExpression() { @@ -552,8 +567,7 @@ JSQLParserException.class, new Executable() { public void execute() throws Throwable { TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); } - } - ); + }); } @Test @@ -566,8 +580,7 @@ JSQLParserException.class, new Executable() { public void execute() throws Throwable { TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); } - } - ); + }); } @Test