diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/TableOption.java b/src/main/java/net/sf/jsqlparser/statement/create/table/TableOption.java index f886f703f..8dffe815c 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/TableOption.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/TableOption.java @@ -21,7 +21,7 @@ public class TableOption implements Serializable { public enum Kind { - ENGINE, CHARACTER_SET, COLLATE, COMMENT, AUTO_INCREMENT, STATS_AUTO_RECALC, STATS_PERSISTENT, STATS_SAMPLE_PAGES, UNION, ENCRYPTION, PASSWORD, DATA_DIRECTORY, INDEX_DIRECTORY, OTHER + ENGINE, CHARACTER_SET, COLLATE, COMMENT, AUTO_INCREMENT, STATS_AUTO_RECALC, STATS_PERSISTENT, STATS_SAMPLE_PAGES, UNION, ENCRYPTION, PASSWORD, DATA_DIRECTORY, INDEX_DIRECTORY, ENGINE_ATTRIBUTE, SECONDARY_ENGINE_ATTRIBUTE, OTHER } private Kind kind = Kind.OTHER; diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 6c42e69e6..9e2f3084c 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1555,9 +1555,13 @@ public class CCJSqlParser extends AbstractJSqlParser { || "STATS_SAMPLE_PAGES".equalsIgnoreCase(name); } + private boolean isMySqlEngineAttributeAhead() { + return isKeywordAhead("ENGINE_ATTRIBUTE") || isKeywordAhead("SECONDARY_ENGINE_ATTRIBUTE"); + } + private boolean isMySqlScalarTableOptionAhead() { int kind = getToken(1).kind; - return kind == K_ENCRYPTION || kind == K_PASSWORD || kind == K_AUTO_INCREMENT + return isMySqlEngineAttributeAhead() || kind == K_ENCRYPTION || kind == K_PASSWORD || kind == K_AUTO_INCREMENT || (kind == K_DATA || kind == K_INDEX) && getToken(2).kind == K_DIRECTORY; } @@ -15237,6 +15241,9 @@ TableOption MySqlTableOption(): { "Expected DEFAULT, 0, or 1 for " + name); } | + LOOKAHEAD({ isMySqlScalarTableOptionAhead() }) option=MySqlScalarTableOption() + { return option; } + | tk= { name = tk.image; kind = TableOption.Kind.ENGINE; @@ -15271,9 +15278,6 @@ TableOption MySqlTableOption(): { } [ "=" { useEquals = true; } ] value=MySqlTableOptionValue() - | - LOOKAHEAD({ isMySqlScalarTableOptionAhead() }) option=MySqlScalarTableOption() - { return option; } ) { option = new TableOption(kind, name, value, useEquals); @@ -15301,6 +15305,10 @@ TableOption MySqlScalarTableOption(): | { name = "PASSWORD"; kind = TableOption.Kind.PASSWORD; } | { name = "DATA DIRECTORY"; kind = TableOption.Kind.DATA_DIRECTORY; } | { name = "INDEX DIRECTORY"; kind = TableOption.Kind.INDEX_DIRECTORY; } + | LOOKAHEAD({ isMySqlEngineAttributeAhead() }) token= { + name = token.image.toUpperCase(Locale.ROOT); + kind = TableOption.Kind.valueOf(name); + } ) [ "=" { useEquals = true; } ] token= { value = token.image; } ) @@ -16676,7 +16684,8 @@ void IndexOptionList(List list) : {} { ( - LOOKAHEAD(2) IndexOption(list) + LOOKAHEAD(2, { !(getToken(1).kind == K_USING && getToken(2).kind == K_INDEX) }) + IndexOption(list) )* } @@ -16697,9 +16706,13 @@ void IndexOption(List list) : Token tk2 = null; String sk1 = null; boolean useEqual = false; + TableOption attribute; } { ( + LOOKAHEAD({ isMySqlEngineAttributeAhead() }) attribute=MySqlScalarTableOption() + { list.addAll(attribute.getTokens()); } + | tk1= ["=" { useEqual = true; } ] tk2= { list.add("KEY_BLOCK_SIZE" + (useEqual ? " = " : "") + tk2.image); diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/IndexOptionBoundaryTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/IndexOptionBoundaryTest.java new file mode 100644 index 000000000..dd7dfd9cb --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/IndexOptionBoundaryTest.java @@ -0,0 +1,75 @@ +/*- + * #%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.alter; + +import static org.junit.jupiter.api.Assertions.*; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.create.table.TableOption; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class IndexOptionBoundaryTest { + @ParameterizedTest + @ValueSource(strings = {"UNIQUE", "PRIMARY KEY"}) + void tablespaceBelongsToConstraintOptions(String key) throws JSQLParserException { + Alter alter = (Alter) parse("ALTER TABLE t ADD CONSTRAINT k " + key + + " (id) USING INDEX TABLESPACE pg_default DEFERRABLE", Dialect.POSTGRESQL); + Index index = alter.getAlterExpressions().get(0).getIndex(); + assertEquals("pg_default", index.getTableSpace()); + assertNull(index.getUsing()); + assertTrue(index.getConstraintAttributes().getDeferrable()); + index.setTableSpace("other_space"); + assertEquals("ALTER TABLE t ADD CONSTRAINT k " + key + + " (id) USING INDEX TABLESPACE other_space DEFERRABLE", alter.toString()); + assertRoundTrip(alter, Dialect.POSTGRESQL); + } + + @ParameterizedTest + @ValueSource(strings = {"SECONDARY_ENGINE_ATTRIBUTE = '{}'", + "SECONDARY_ENGINE_ATTRIBUTE '{\"key\": \"value\"}'", + "SECONDARY_ENGINE_ATTRIBUTE = '{}' VISIBLE COMMENT 'index'"}) + void mysqlAddIndexAttributes(String options) throws JSQLParserException { + Alter alter = (Alter) parse("ALTER TABLE t ADD INDEX ix (id) " + options, Dialect.MYSQL); + assertEquals("ix", alter.getAlterExpressions().get(0).getIndex().getName()); + assertEquals("ALTER TABLE t ADD INDEX ix (id) " + options, alter.toString()); + assertRoundTrip(alter, Dialect.MYSQL); + } + + @Test + void sharedTableAttributeRemainsMutable() throws JSQLParserException { + Alter alter = + (Alter) parse("ALTER TABLE t SECONDARY_ENGINE_ATTRIBUTE = '{}'", Dialect.MYSQL); + AlterExpressionTableOption action = + (AlterExpressionTableOption) alter.getAlterExpressions().get(0); + TableOption option = action.getStructuredTableOption(); + assertEquals(TableOption.Kind.SECONDARY_ENGINE_ATTRIBUTE, option.getKind()); + option.setValue("'{\"key\":1}'"); + assertEquals("ALTER TABLE t SECONDARY_ENGINE_ATTRIBUTE = '{\"key\":1}'", alter.toString()); + assertRoundTrip(alter, Dialect.MYSQL); + } + + private static Statement parse(String sql, Dialect dialect) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(dialect)); + } + + private static void assertRoundTrip(Statement statement, Dialect dialect) + throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(statement.toString(), parse(sql.toString(), dialect).toString()); + } +}