diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionTableOption.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionTableOption.java index 06a1af136..2aa94b5ed 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionTableOption.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionTableOption.java @@ -9,6 +9,8 @@ */ package net.sf.jsqlparser.statement.alter; +import net.sf.jsqlparser.statement.create.table.TableOption; + /** * Internal subclass for table-level option operations within ALTER TABLE. Handles ENGINE, * ALGORITHM, LOCK, KEY_BLOCK_SIZE, COMMENT, ENCRYPTION, AUTO_INCREMENT (SET_TABLE_OPTION), @@ -16,9 +18,52 @@ */ public class AlterExpressionTableOption extends AlterExpression { + private TableOption structuredTableOption; + + public TableOption getStructuredTableOption() { + return structuredTableOption; + } + + /** Uses the same option model as CREATE TABLE, with legacy getters as live projections. */ + public void setStructuredTableOption(TableOption option) { + structuredTableOption = option; + setOperation(AlterOperation.SET_TABLE_OPTION); + super.setTableOption(null); + super.setUseEqual(false); + } + + @Override + public String getTableOption() { + return structuredTableOption == null ? super.getTableOption() + : structuredTableOption.toString(); + } + + @Override + public void setTableOption(String option) { + structuredTableOption = null; + super.setTableOption(option); + } + + @Override + public boolean getUseEqual() { + return structuredTableOption == null ? super.getUseEqual() + : structuredTableOption.isUseEquals(); + } + + @Override + public void setUseEqual(boolean useEqual) { + if (structuredTableOption != null) { + structuredTableOption.setUseEquals(useEqual); + } + super.setUseEqual(useEqual); + } + @Override protected void appendBody(StringBuilder b) { switch (getOperation()) { + case SET_TABLE_OPTION: + b.append(getTableOption()); + break; case COMMENT: b.append("COMMENT "); b.append(getCommentText()); 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 239e61662..f886f703f 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, OTHER + ENGINE, CHARACTER_SET, COLLATE, COMMENT, AUTO_INCREMENT, STATS_AUTO_RECALC, STATS_PERSISTENT, STATS_SAMPLE_PAGES, UNION, ENCRYPTION, PASSWORD, DATA_DIRECTORY, INDEX_DIRECTORY, 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 0df99f424..4567181d8 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1542,10 +1542,16 @@ public class CCJSqlParser extends AbstractJSqlParser { || "STATS_SAMPLE_PAGES".equalsIgnoreCase(name); } + private boolean isMySqlScalarTableOptionAhead() { + int kind = getToken(1).kind; + return kind == K_ENCRYPTION || kind == K_PASSWORD || kind == K_AUTO_INCREMENT + || (kind == K_DATA || kind == K_INDEX) && getToken(2).kind == K_DIRECTORY; + } + private boolean isMySqlTableOptionAhead() { int kind = getToken(1).kind; if (kind == K_ENGINE || kind == K_COLLATE || kind == K_COMMENT - || kind == K_AUTO_INCREMENT || kind == K_UNION + || isMySqlScalarTableOptionAhead() || kind == K_UNION || isMySqlStatisticsOptionAhead()) { return true; } @@ -15163,12 +15169,8 @@ TableOption MySqlTableOption(): { [ "=" { useEquals = true; } ] value=MySqlTableOptionValue() | - tk= { - name = tk.image; - kind = TableOption.Kind.AUTO_INCREMENT; - } - [ "=" { useEquals = true; } ] - value=MySqlTableOptionValue() + LOOKAHEAD({ isMySqlScalarTableOptionAhead() }) option=MySqlScalarTableOption() + { return option; } ) { option = new TableOption(kind, name, value, useEquals); @@ -15177,6 +15179,41 @@ TableOption MySqlTableOption(): { } } +/** Common CREATE/ALTER options with a quoted string or unsigned integer value. */ +TableOption MySqlScalarTableOption(): +{ + Token token; + String name; + String value; + TableOption.Kind kind; + boolean useEquals = false; +} +{ + ( + { name = "AUTO_INCREMENT"; kind = TableOption.Kind.AUTO_INCREMENT; } + [ "=" { useEquals = true; } ] token= { value = token.image; } + | + ( + { name = "ENCRYPTION"; kind = TableOption.Kind.ENCRYPTION; } + | { name = "PASSWORD"; kind = TableOption.Kind.PASSWORD; } + | { name = "DATA DIRECTORY"; kind = TableOption.Kind.DATA_DIRECTORY; } + | { name = "INDEX DIRECTORY"; kind = TableOption.Kind.INDEX_DIRECTORY; } + ) + [ "=" { useEquals = true; } ] token= { value = token.image; } + ) + { return new TableOption(kind, name, value, useEquals); } +} + +AlterExpressionTableOption AlterScalarTableOption(): +{ + TableOption option; + AlterExpressionTableOption expression = new AlterExpressionTableOption(); +} +{ + option=MySqlScalarTableOption() + { expression.setStructuredTableOption(option); return expression; } +} + String MySqlTableOptionValue(): { Token token; String value = null; @@ -17677,17 +17714,7 @@ AlterExpression AlterExpression(): tk= { alterExp.setCommentText(tk.image); } ) | - ({ alterExp = new AlterExpressionTableOption(); } - {alterExp.setOperation(AlterOperation.SET_TABLE_OPTION);} - ["=" { alterExp.setUseEqual(true);} ] - tk= { - if (alterExp.getUseEqual()) { - alterExp.setTableOption("ENCRYPTION = " + tk.image); - } else { - alterExp.setTableOption("ENCRYPTION " + tk.image); - } - } - ) + LOOKAHEAD({ isMySqlScalarTableOptionAhead() }) alterExp=AlterScalarTableOption() | alterExp = AlterExpressionDiscardOrImport() | @@ -17720,18 +17747,6 @@ AlterExpression AlterExpression(): alterExp.setOperation(AlterOperation.ENABLE_KEYS); } ) - | - ({ alterExp = new AlterExpressionTableOption(); } - {alterExp.setOperation(AlterOperation.SET_TABLE_OPTION);} - ["=" { alterExp.setUseEqual(true);} ] - tk= { - if (alterExp.getUseEqual()) { - alterExp.setTableOption("AUTO_INCREMENT = " + tk.image); - } else { - alterExp.setTableOption("AUTO_INCREMENT " + tk.image); - } - } - ) | alterExp = AlterExpressionPartitionOp() | diff --git a/src/test/java/net/sf/jsqlparser/statement/create/MySqlSharedTableOptionTest.java b/src/test/java/net/sf/jsqlparser/statement/create/MySqlSharedTableOptionTest.java new file mode 100644 index 000000000..012645ca8 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/MySqlSharedTableOptionTest.java @@ -0,0 +1,103 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2019 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.create; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.alter.AlterExpressionTableOption; +import net.sf.jsqlparser.statement.create.table.CreateTable; +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.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class MySqlSharedTableOptionTest { + @ParameterizedTest + @CsvSource(value = {"ENCRYPTION|'N'|ENCRYPTION", "PASSWORD|'ignored'|PASSWORD", + "DATA DIRECTORY|'/tmp/data dir'|DATA_DIRECTORY", + "INDEX DIRECTORY|'/tmp/index dir'|INDEX_DIRECTORY", + "AUTO_INCREMENT|18446744073709551614|AUTO_INCREMENT"}, delimiter = '|', + quoteCharacter = '"') + void sharesCreateAndAlterModels(String name, String value, String kind) + throws JSQLParserException { + for (String equals : new String[] {" ", " = "}) { + String optionSql = name + equals + value; + CreateTable create = + (CreateTable) CCJSqlParserUtil.parse("CREATE TABLE t (id BIGINT) " + optionSql); + Alter alter = (Alter) CCJSqlParserUtil + .parse("ALTER TABLE t " + optionSql + ", ADD COLUMN extra INT"); + AlterExpressionTableOption action = assertInstanceOf(AlterExpressionTableOption.class, + alter.getAlterExpressions().get(0)); + TableOption option = action.getStructuredTableOption(); + assertEquals(TableOption.Kind.valueOf(kind), option.getKind()); + assertEquals(create.getTableOptions().get(0).toString(), option.toString()); + assertEquals(value, option.getValue()); + assertEquals(equals.contains("="), action.getUseEqual()); + assertEquals(optionSql, action.getTableOption()); + assertEquals(2, alter.getAlterExpressions().size()); + roundTrip(create); + roundTrip(alter); + option.setUseEquals(!option.isUseEquals()); + assertEquals(option.isUseEquals(), action.getUseEqual()); + roundTrip(alter); + action.setUseEqual(true); + assertTrue(option.isUseEquals()); + action.setTableOption("ENCRYPTION 'N'"); + assertNull(action.getStructuredTableOption()); + assertEquals("ENCRYPTION 'N'", action.toString()); + } + } + + @Test + void commasAndQuerySourceStayOutsideOptions() throws JSQLParserException { + CreateTable table = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TABLE t (id INT) ENGINE=InnoDB, ENCRYPTION='N', PASSWORD='a,b' AS SELECT 1 AS id"); + assertEquals(3, table.getTableOptions().size()); + assertNotNull(table.getSelect()); + table.getTableOptions().get(1).setValue("'Y'"); + assertTrue(table.getTableOptionsStrings().contains("'Y'")); + roundTrip(table); + table.setTableOptionsStrings(List.of("ENCRYPTION", "=", "'N'")); + assertNull(table.getTableOptions()); + roundTrip(table); + } + + @ParameterizedTest + @ValueSource(strings = {"ENCRYPTION=", "PASSWORD=1", "DATA DIRECTORY='/tmp' INDEX DIRECTORY=", + "AUTO_INCREMENT=-1", "AUTO_INCREMENT='5'", "ENCRYPTION='N',"}) + void rejectsMissingOrWrongCreateValues(String option) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("CREATE TABLE t (id INT) " + option)); + } + + @ParameterizedTest + @ValueSource(strings = {"VECTOR", "VECTOR(0)", "VECTOR(1)", "VECTOR(2048)", "VECTOR(16383)"}) + void preservesMySql9VectorColumnsBeforeTableOptions(String type) throws JSQLParserException { + CreateTable table = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TABLE t (v " + type + ") ENCRYPTION='N'"); + assertEquals(type, table.getColumnDefinitions().get(0).getColDataType().toString() + .replace(" ", "")); + assertEquals(TableOption.Kind.ENCRYPTION, table.getTableOptions().get(0).getKind()); + roundTrip(table); + } + + private static void roundTrip(Statement statement) throws JSQLParserException { + StringBuilder out = new StringBuilder(); + statement.accept(new StatementDeParser(out)); + assertEquals(statement.toString(), out.toString()); + assertEquals(out.toString(), CCJSqlParserUtil.parse(out.toString()).toString()); + } +}