From 375964405254c6aa5e4b63e179a84c3c2bedaddf Mon Sep 17 00:00:00 2001 From: minleejae Date: Fri, 25 Sep 2026 22:45:00 +0900 Subject: [PATCH 1/2] fix: reuse MySQL table option parsing across CREATE and ALTER TABLE Signed-off-by: minleejae --- .../alter/AlterExpressionTableOption.java | 74 ++++++++ .../statement/create/table/TableOption.java | 30 +++- .../util/TableDefinitionTraversal.java | 22 ++- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 156 ++++++++--------- .../alter/MySqlCommonTableOptionsTest.java | 163 ++++++++++++++++++ 5 files changed, 355 insertions(+), 90 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/statement/alter/MySqlCommonTableOptionsTest.java 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 2aa94b5ed..317b2da72 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionTableOption.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionTableOption.java @@ -32,6 +32,76 @@ public void setStructuredTableOption(TableOption option) { super.setUseEqual(false); } + @Override + public AlterOperation getOperation() { + if (structuredTableOption != null) { + switch (structuredTableOption.getKind()) { + case ENGINE: + return AlterOperation.ENGINE; + case KEY_BLOCK_SIZE: + return AlterOperation.KEY_BLOCK_SIZE; + case COMMENT: + return structuredTableOption.isUseEquals() + ? AlterOperation.COMMENT_WITH_EQUAL_SIGN + : AlterOperation.COMMENT; + default: + return AlterOperation.SET_TABLE_OPTION; + } + } + return super.getOperation(); + } + + @Override + public String getEngineOption() { + return structuredTableOption != null + && structuredTableOption.getKind() == TableOption.Kind.ENGINE + ? structuredTableOption.getValue() + : super.getEngineOption(); + } + + @Override + public void setEngineOption(String value) { + if (structuredTableOption != null + && structuredTableOption.getKind() == TableOption.Kind.ENGINE) { + structuredTableOption.setValue(value); + } + super.setEngineOption(value); + } + + @Override + public int getKeyBlockSize() { + return structuredTableOption != null + && structuredTableOption.getKind() == TableOption.Kind.KEY_BLOCK_SIZE + ? Integer.parseInt(structuredTableOption.getValue()) + : super.getKeyBlockSize(); + } + + @Override + public void setKeyBlockSize(int value) { + if (structuredTableOption != null + && structuredTableOption.getKind() == TableOption.Kind.KEY_BLOCK_SIZE) { + structuredTableOption.setValue(Integer.toString(value)); + } + super.setKeyBlockSize(value); + } + + @Override + public String getCommentText() { + return structuredTableOption != null + && structuredTableOption.getKind() == TableOption.Kind.COMMENT + ? structuredTableOption.getValue() + : super.getCommentText(); + } + + @Override + public void setCommentText(String value) { + if (structuredTableOption != null + && structuredTableOption.getKind() == TableOption.Kind.COMMENT) { + structuredTableOption.setValue(value); + } + super.setCommentText(value); + } + @Override public String getTableOption() { return structuredTableOption == null ? super.getTableOption() @@ -60,6 +130,10 @@ public void setUseEqual(boolean useEqual) { @Override protected void appendBody(StringBuilder b) { + if (structuredTableOption != null) { + b.append(structuredTableOption); + return; + } switch (getOperation()) { case SET_TABLE_OPTION: b.append(getTableOption()); 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 920120e5e..f784aa05f 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, SECONDARY_ENGINE, AUTOEXTEND_SIZE, INSERT_METHOD, PACK_KEYS, DELAY_KEY_WRITE, CHECKSUM, CONNECTION, COMPRESSION, STORAGE_PARAMETERS, WITHOUT_OIDS, ENGINE_ATTRIBUTE, SECONDARY_ENGINE_ATTRIBUTE, ROW_FORMAT, FOREIGN_SERVER, OTHER + ENGINE, CHARACTER_SET, COLLATE, COMMENT, AUTO_INCREMENT, STATS_AUTO_RECALC, STATS_PERSISTENT, STATS_SAMPLE_PAGES, UNION, ENCRYPTION, PASSWORD, DATA_DIRECTORY, INDEX_DIRECTORY, SECONDARY_ENGINE, AUTOEXTEND_SIZE, INSERT_METHOD, PACK_KEYS, DELAY_KEY_WRITE, CHECKSUM, CONNECTION, COMPRESSION, STORAGE_PARAMETERS, WITHOUT_OIDS, ENGINE_ATTRIBUTE, SECONDARY_ENGINE_ATTRIBUTE, ROW_FORMAT, AVG_ROW_LENGTH, MAX_ROWS, MIN_ROWS, KEY_BLOCK_SIZE, TABLESPACE, FOREIGN_SERVER, OTHER } private ForeignTableOptions foreignTableOptions; @@ -43,6 +43,19 @@ public static TableOption foreignServer(ForeignTableOptions options) { private List tokens; private List unionTables; private List storageParameters; + private ColumnOption.Storage tablespaceStorage; + + public ColumnOption.Storage getTablespaceStorage() { + return tablespaceStorage; + } + + public void setTablespaceStorage(ColumnOption.Storage storage) { + if (storage != null && storage != ColumnOption.Storage.DISK + && storage != ColumnOption.Storage.MEMORY) { + throw new IllegalArgumentException("TABLESPACE storage must be DISK or MEMORY"); + } + tablespaceStorage = storage; + } public List getStorageParameters() { return storageParameters; @@ -51,6 +64,7 @@ public List getStorageParameters() { public void setStorageParameters(List storageParameters) { this.storageParameters = storageParameters; foreignTableOptions = null; + tablespaceStorage = null; kind = Kind.STORAGE_PARAMETERS; name = "WITH"; value = null; @@ -96,6 +110,9 @@ public Kind getKind() { public void setKind(Kind kind) { this.kind = kind; + if (kind != Kind.TABLESPACE) { + tablespaceStorage = null; + } if (kind != Kind.FOREIGN_SERVER) { foreignTableOptions = null; } @@ -141,6 +158,7 @@ public List
getUnionTables() { public void setUnionTables(List
unionTables) { this.unionTables = unionTables; foreignTableOptions = null; + tablespaceStorage = null; kind = Kind.UNION; name = "UNION"; value = null; @@ -177,12 +195,17 @@ public List getTokens() { if (renderedValue != null) { result.add(renderedValue); } + if (kind == Kind.TABLESPACE && tablespaceStorage != null) { + result.add("STORAGE"); + result.add(tablespaceStorage.name()); + } return Collections.unmodifiableList(result); } public void setTokens(List tokens) { this.tokens = tokens; if (tokens != null) { + tablespaceStorage = null; foreignTableOptions = null; unionTables = null; storageParameters = null; @@ -218,6 +241,9 @@ public String toString() { return PlainSelect.getStringList(tokens, false, false); } String renderedValue = getValue(); - return name + (renderedValue != null ? (useEquals ? " = " : " ") + renderedValue : ""); + return name + (renderedValue != null ? (useEquals ? " = " : " ") + renderedValue : "") + + (kind == Kind.TABLESPACE && tablespaceStorage != null + ? " STORAGE " + tablespaceStorage + : ""); } } diff --git a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java index 0fccab980..ec7ab180e 100644 --- a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java +++ b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java @@ -61,6 +61,10 @@ public static void visit(AlterExpression action, Consumer expression relation.visitExpressions(expressions); relation.visitTables(tables); } + if (action instanceof net.sf.jsqlparser.statement.alter.AlterExpressionTableOption) { + visit(((net.sf.jsqlparser.statement.alter.AlterExpressionTableOption) action) + .getStructuredTableOption(), expressions, tables); + } if (action.getOperation() == AlterOperation.RENAME_TABLE) { accept(action.getNewTable(), tables); } @@ -123,12 +127,7 @@ public static void visit(CreateTable table, Consumer expressions, expressions); } if (table.getTableOptions() != null) { - table.getTableOptions().forEach(option -> { - visitOptions(option.getStorageParameters(), expressions); - if (option.getUnionTables() != null) { - option.getUnionTables().forEach(source -> accept(source, tables)); - } - }); + table.getTableOptions().forEach(option -> visit(option, expressions, tables)); } if (table.getInherits() != null) { table.getInherits().forEach(parent -> accept(parent, tables)); @@ -142,6 +141,17 @@ public static void visit(CreateTable table, Consumer expressions, visit(table.getPartitionBound(), expressions); } + /** Shared CREATE/ALTER table option traversal. */ + public static void visit(net.sf.jsqlparser.statement.create.table.TableOption option, + Consumer expressions, Consumer
tables) { + if (option != null) { + visitOptions(option.getStorageParameters(), expressions); + if (option.getUnionTables() != null) { + option.getUnionTables().forEach(source -> accept(source, tables)); + } + } + } + /** Visits the active partition key and any subpartition key. Raw bounds remain opaque. */ public static void visit(TablePartitioning partitioning, Consumer expressions) { if (partitioning == null) { diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 4b505dab0..e1ba0dfe5 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1567,15 +1567,27 @@ public class CCJSqlParser extends AbstractJSqlParser { "DELAY_KEY_WRITE", "CHECKSUM", "CONNECTION", "COMPRESSION").contains(name); } + private boolean isMySqlNumericTableOptionAhead() { + return getToken(1).kind == K_KEY_BLOCK_SIZE || isKeywordAhead("AVG_ROW_LENGTH") + || isKeywordAhead("MAX_ROWS") || isKeywordAhead("MIN_ROWS"); + } + + private boolean isMySqlTableOptionAction(AlterExpression action) { + return action instanceof AlterExpressionTableOption + && ((AlterExpressionTableOption) action).getStructuredTableOption() != null + || action instanceof AlterExpressionCharset + && action.getConvertType() != AlterExpression.ConvertType.CONVERT_TO; + } + private boolean isMySqlScalarTableOptionAhead() { int kind = getToken(1).kind; - return isMySqlAdditionalTableOptionAhead() || isMySqlEngineAttributeAhead() || kind == K_ENCRYPTION || kind == K_PASSWORD || kind == K_AUTO_INCREMENT + return isMySqlNumericTableOptionAhead() || isMySqlAdditionalTableOptionAhead() || isMySqlEngineAttributeAhead() || 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 + if (kind == K_ENGINE || kind == K_COLLATE || kind == K_COMMENT || kind == K_TABLESPACE || isMySqlScalarTableOptionAhead() || kind == K_UNION || isMySqlStatisticsOptionAhead() || isKeywordAhead("ROW_FORMAT")) { return true; @@ -15530,6 +15542,7 @@ TableOption MySqlTableOption(): { String name = ""; TableOption.Kind kind = null; TableOption option; + ColumnOption.Storage storage = null; } { ( tk= { name = tk.image; kind = TableOption.Kind.UNION; @@ -15559,6 +15572,14 @@ TableOption MySqlTableOption(): { LOOKAHEAD({ isMySqlScalarTableOptionAhead() }) option=MySqlScalarTableOption() { return option; } | + { name = "TABLESPACE"; kind = TableOption.Kind.TABLESPACE; } + [ "=" { useEquals = true; } ] value=RelObjectName() + [ LOOKAHEAD(2) tk= { + requireDdlSyntax("DISK".equalsIgnoreCase(tk.image) || "MEMORY".equalsIgnoreCase(tk.image), + "Expected DISK or MEMORY after STORAGE"); + storage = ColumnOption.Storage.valueOf(tk.image.toUpperCase(Locale.ROOT)); + } ] + | tk= { name = tk.image; kind = TableOption.Kind.ENGINE; @@ -15597,6 +15618,7 @@ TableOption MySqlTableOption(): { { option = new TableOption(kind, name, value, useEquals); if (unionTables != null) { option.setUnionTables(unionTables); } + option.setTablespaceStorage(storage); return option; } } @@ -15613,6 +15635,12 @@ TableOption MySqlScalarTableOption(): } { ( + LOOKAHEAD({ isMySqlNumericTableOptionAhead() }) + ( token= | token= | token= | token= ) { + name = token.image.toUpperCase(Locale.ROOT); kind = TableOption.Kind.valueOf(name); + } + [ "=" { useEquals = true; } ] token= { value = token.image; } + | LOOKAHEAD({ isMySqlAdditionalTableOptionAhead() }) additional=MySqlAdditionalTableOption() { name = additional.getName(); kind = additional.getKind(); value = additional.getValue(); useEquals = additional.isUseEquals(); } @@ -15694,12 +15722,44 @@ String MySqlSizeNumber(): } } -/** Statistics and row format share CREATE's validation and option nodes. */ -AlterExpressionTableOption AlterSharedTableOption(): -{ TableOption option; AlterExpressionTableOption action = new AlterExpressionTableOption(); } +/** Adapts CREATE's option nodes to the existing ALTER option APIs. */ +AlterExpression AlterSharedTableOption(): +{ + TableOption option; + TableOption collation = null; + AlterExpressionCharset charset; + AlterExpressionTableOption action; +} { option=MySqlTableOption() - { action.setStructuredTableOption(option); return action; } + [ LOOKAHEAD({ option.getKind() == TableOption.Kind.CHARACTER_SET && getToken(1).kind == K_COLLATE }) + collation=MySqlTableOption() ] + { + if (option.getKind() == TableOption.Kind.CHARACTER_SET || option.getKind() == TableOption.Kind.COLLATE) { + charset = new AlterExpressionCharset(); + boolean useDefault = option.getName().toUpperCase(Locale.ROOT).startsWith("DEFAULT "); + if (option.getKind() == TableOption.Kind.CHARACTER_SET) { + charset.setOperation(AlterOperation.CONVERT); + charset.setConvertType(useDefault ? AlterExpression.ConvertType.DEFAULT_CHARACTER_SET + : AlterExpression.ConvertType.CHARACTER_SET); + charset.setCharacterSet(option.getValue()); + charset.setHasEqualForCharacterSet(option.isUseEquals()); + if (collation != null) { + charset.setCollation(collation.getValue()); + charset.setHasEqualForCollate(collation.isUseEquals()); + } + } else { + charset.setOperation(AlterOperation.COLLATE); + charset.setCollation(option.getValue()); + charset.setHasEqualForCollate(option.isUseEquals()); + charset.setDefaultCollateSpecified(useDefault); + } + return charset; + } + action = new AlterExpressionTableOption(); + action.setStructuredTableOption(option); + return action; + } } AlterExpressionOrderBy MySqlAlterOrderBy(): @@ -15718,16 +15778,6 @@ OrderByElement MySqlAlterOrderColumn(): { return element; } } -AlterExpressionTableOption AlterScalarTableOption(): -{ - TableOption option; - AlterExpressionTableOption expression = new AlterExpressionTableOption(); -} -{ - option=MySqlScalarTableOption() - { expression.setStructuredTableOption(option); return expression; } -} - String MySqlTableOptionValue(): { Token token; String value = null; @@ -18241,7 +18291,7 @@ AlterExpression AlterExpression(): | LOOKAHEAD( ) alterExp=MySqlAlterOrderBy() | - LOOKAHEAD({ isMySqlStatisticsOptionAhead() || isKeywordAhead("ROW_FORMAT") }) + LOOKAHEAD({ isMySqlTableOptionAhead() }) alterExp=AlterSharedTableOption() | LOOKAHEAD({ isPostgreSqlRelationActionAhead() }) alterExp=PostgreSqlRelationAction() { @@ -18293,13 +18343,6 @@ AlterExpression AlterExpression(): ["=" { alterExp.setUseEqual(true);} ] sk3 = RelObjectName() { alterExp.setAlgorithmOption(sk3); } ) - | - ( - { alterExp = new AlterExpressionTableOption(); } - { alterExp.setOperation(AlterOperation.KEY_BLOCK_SIZE); } - ["=" { alterExp.setUseEqual(true);} ] - tk= { alterExp.setKeyBlockSize(Integer.parseInt(tk.image)); } - ) | ( { alterExp = new AlterExpressionTableOption(); } @@ -18307,13 +18350,6 @@ AlterExpression AlterExpression(): ["=" { alterExp.setUseEqual(true);} ] sk3 = RelObjectName() { alterExp.setLockOption(sk3); } ) - | - ( - { alterExp = new AlterExpressionTableOption(); } - { alterExp.setOperation(AlterOperation.ENGINE); } - ["=" { alterExp.setUseEqual(true);} ] - sk3 = RelObjectName() { alterExp.setEngineOption(sk3); } - ) | LOOKAHEAD(2) alterExp = AlterExpressionRenameOp() | @@ -18323,57 +18359,8 @@ AlterExpression AlterExpression(): alterExp.setConvertType(AlterExpression.ConvertType.CONVERT_TO); } tk= { alterExp.setCharacterSet(tk.image); } - [ tk2= { alterExp.setCollation(tk2.image); }] - ) - | - LOOKAHEAD(3) - ( - { alterExp = new AlterExpressionCharset(); } - - ( - [ "=" { alterExp.setHasEqualForCharacterSet(true); } ] - tk= { - alterExp.setOperation(AlterOperation.CONVERT); - alterExp.setConvertType(AlterExpression.ConvertType.DEFAULT_CHARACTER_SET); - alterExp.setCharacterSet(tk.image); - } - [ [ "=" { alterExp.setHasEqualForCollate(true); } ] - tk2= { alterExp.setCollation(tk2.image); }] - | - - [ "=" { alterExp.setHasEqualForCollate(true); } ] - tk= { - alterExp.setOperation(AlterOperation.COLLATE); - alterExp.setCollation(tk.image); - alterExp.setDefaultCollateSpecified(true); - } - ) + [ LOOKAHEAD(2) tk2= { alterExp.setCollation(tk2.image); }] ) - | - ({ alterExp = new AlterExpressionCharset(); } - [ "=" { alterExp.setHasEqualForCharacterSet(true); } ] - tk= { - alterExp.setOperation(AlterOperation.CONVERT); - alterExp.setConvertType(AlterExpression.ConvertType.CHARACTER_SET); - alterExp.setCharacterSet(tk.image); - } - [ [ "=" { alterExp.setHasEqualForCollate(true); } ] - tk2= { alterExp.setCollation(tk2.image); }] - ) - | - ({ alterExp = new AlterExpressionCharset(); } - { alterExp.setOperation(AlterOperation.COLLATE); } - [ "=" { alterExp.setHasEqualForCollate(true); } ] - tk= { alterExp.setCollation(tk.image); } - ) - | - ({ alterExp = new AlterExpressionTableOption(); } - {alterExp.setOperation(AlterOperation.COMMENT);} - ["=" {alterExp.setOperation(AlterOperation.COMMENT_WITH_EQUAL_SIGN);} ] - tk= { alterExp.setCommentText(tk.image); } - ) - | - LOOKAHEAD({ isMySqlScalarTableOptionAhead() }) alterExp=AlterScalarTableOption() | alterExp = AlterExpressionDiscardOrImport() | @@ -18757,7 +18744,12 @@ Alter AlterTable(boolean foreign): alterExp=AlterExpression() { alter.addAlterExpression(alterExp); } - ("," alterExp=AlterExpression() { alter.addAlterExpression(alterExp); } )* + ( + "," alterExp=AlterExpression() { alter.addAlterExpression(alterExp); } + | LOOKAHEAD({ isMySqlTableOptionAhead() }) + { requireDdlSyntax(isMySqlTableOptionAction(alterExp), "Expected a comma before table options"); } + alterExp=AlterSharedTableOption() { alter.addAlterExpression(alterExp); } + )* { for (AlterExpression action : alter.getAlterExpressions()) { diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/MySqlCommonTableOptionsTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/MySqlCommonTableOptionsTest.java new file mode 100644 index 000000000..3caea9295 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/MySqlCommonTableOptionsTest.java @@ -0,0 +1,163 @@ +/*- + * #%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 java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.create.table.ColumnOption; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.TableOption; +import net.sf.jsqlparser.util.TablesNamesFinder; +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 MySqlCommonTableOptionsTest { + @ParameterizedTest + @ValueSource(strings = {"ENGINE='InnoDB'", "ENGINE=\"InnoDB\"", "AVG_ROW_LENGTH=100", + "MAX_ROWS=18446744073709551615", "MIN_ROWS 0", "KEY_BLOCK_SIZE=8", + "TABLESPACE=innodb_file_per_table", "TABLESPACE `space name` STORAGE DISK", + "TABLESPACE ts STORAGE MEMORY", "UNION=(db.m1,db.m2)", "UNION=()"}) + void sharesTableOptions(String sql) throws JSQLParserException { + CreateTable create = (CreateTable) CCJSqlParserUtil.parse("CREATE TABLE t(id INT) " + sql); + Alter alter = parse(sql + ", ADD COLUMN extra INT"); + AlterExpressionTableOption action = + assertInstanceOf(AlterExpressionTableOption.class, + alter.getAlterExpressions().get(0)); + TableOption option = action.getStructuredTableOption(); + assertNotEquals(TableOption.Kind.OTHER, option.getKind()); + assertEquals(create.getTableOptions().get(0).toString(), option.toString()); + assertEquals(2, alter.getAlterExpressions().size()); + roundTrip(create); + roundTrip(alter); + } + + @Test + void optionPairsKeepActionAndStatementBoundaries() throws JSQLParserException { + List options = List.of("ENGINE='InnoDB'", "ROW_FORMAT=DYNAMIC", + "STATS_PERSISTENT=1", "COMMENT='x,y'", "AVG_ROW_LENGTH=100", + "TABLESPACE=innodb_file_per_table", "UNION=(m1,m2)"); + for (String left : options) { + for (String right : options) { + if (left.equals(right)) { + continue; + } + for (String separator : new String[] {" ", ", "}) { + String sql = "ALTER TABLE t " + left + separator + right + + ", ADD COLUMN z INT, ALGORITHM=DEFAULT, LOCK=DEFAULT"; + Alter alter = (Alter) CCJSqlParserUtil.parse(sql); + assertEquals(5, alter.getAlterExpressions().size(), sql); + assertEquals(AlterOperation.ADD, + alter.getAlterExpressions().get(2).getOperation()); + assertEquals(AlterOperation.ALGORITHM, + alter.getAlterExpressions().get(3).getOperation()); + assertEquals(AlterOperation.LOCK, + alter.getAlterExpressions().get(4).getOperation()); + roundTrip(alter); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size()); + } + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"DEFAULT CHARACTER SET", "CHARACTER SET", "CHAR SET", "CHARSET", + "DEFAULT CHARSET", "DEFAULT CHAR SET"}) + void quotedCharsetAndCollationKeepLegacyAccess(String name) throws JSQLParserException { + Alter alter = parse(name + "='utf8mb4' COLLATE='utf8mb4_bin' ROW_FORMAT=DYNAMIC"); + AlterExpression action = alter.getAlterExpressions().get(0); + assertEquals(AlterOperation.CONVERT, action.getOperation()); + assertEquals("'utf8mb4'", action.getCharacterSet()); + assertEquals("'utf8mb4_bin'", action.getCollation()); + assertEquals(2, alter.getAlterExpressions().size()); + action.setCharacterSet("utf8mb4"); + action.setCollation("utf8mb4_general_ci"); + roundTrip(alter); + } + + @Test + void structuredAndLegacyMutationsUseTheSameValues() throws JSQLParserException { + Alter alter = parse("ENGINE=InnoDB KEY_BLOCK_SIZE=8 COMMENT='old'"); + AlterExpressionTableOption engine = + (AlterExpressionTableOption) alter.getAlterExpressions().get(0); + assertEquals(AlterOperation.ENGINE, engine.getOperation()); + engine.setEngineOption("MyISAM"); + assertEquals("MyISAM", engine.getStructuredTableOption().getValue()); + engine.getStructuredTableOption().setValue("InnoDB"); + assertEquals("InnoDB", engine.getEngineOption()); + AlterExpressionTableOption block = + (AlterExpressionTableOption) alter.getAlterExpressions().get(1); + assertEquals(AlterOperation.KEY_BLOCK_SIZE, block.getOperation()); + block.setKeyBlockSize(4); + assertEquals("4", block.getStructuredTableOption().getValue()); + block.getStructuredTableOption().setValue("16"); + assertEquals(16, block.getKeyBlockSize()); + AlterExpressionTableOption comment = + (AlterExpressionTableOption) alter.getAlterExpressions().get(2); + comment.setCommentText("'new'"); + assertEquals("'new'", comment.getStructuredTableOption().getValue()); + comment.getStructuredTableOption().setUseEquals(false); + assertEquals(AlterOperation.COMMENT, comment.getOperation()); + roundTrip(alter); + } + + @Test + void visitsAndChangesUnionSources() throws JSQLParserException { + Alter alter = parse("UNION=(m1,m2)"); + TableOption option = ((AlterExpressionTableOption) alter.getAlterExpressions().get(0)) + .getStructuredTableOption(); + assertEquals(Set.of("t", "m1", "m2"), new TablesNamesFinder().getTables(alter)); + option.getUnionTables().set(1, new Table("m3")); + assertEquals(Set.of("t", "m1", "m3"), new TablesNamesFinder().getTables(alter)); + assertTrue(alter.toString().contains("UNION = (m1, m3)")); + roundTrip(alter); + } + + @Test + void tablespaceStorageHasStructuredAndLegacyProjections() throws JSQLParserException { + CreateTable create = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TABLE t(id INT) TABLESPACE=ts STORAGE DISK"); + TableOption option = create.getTableOptions().get(0); + assertEquals("ts", option.getValue()); + assertEquals(ColumnOption.Storage.DISK, option.getTablespaceStorage()); + option.setValue("new_ts"); + option.setTablespaceStorage(ColumnOption.Storage.MEMORY); + assertEquals(List.of("TABLESPACE", "=", "new_ts", "STORAGE", "MEMORY"), option.getTokens()); + roundTrip(create); + option.setKind(TableOption.Kind.ENGINE); + assertNull(option.getTablespaceStorage()); + } + + @ParameterizedTest + @ValueSource(strings = {"ENGINE=", "AVG_ROW_LENGTH=-1", "MAX_ROWS='1'", "TABLESPACE=", + "TABLESPACE ts STORAGE UNKNOWN", "ENGINE=InnoDB ADD COLUMN z INT", + "ENGINE=InnoDB LOCK=NONE", "ALGORITHM=DEFAULT ROW_FORMAT=DYNAMIC"}) + void rejectsIncompleteOptionsAndMissingActionCommas(String option) { + assertThrows(JSQLParserException.class, () -> parse(option)); + } + + private static Alter parse(String sql) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse("ALTER TABLE t " + sql); + } + + 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()); + } +} From fabb344a0b25f3f8d6716198aa0004e8ca3c39ad Mon Sep 17 00:00:00 2001 From: minleejae Date: Sat, 26 Sep 2026 09:15:50 +0900 Subject: [PATCH 2/2] fix: declare table option fields before methods Signed-off-by: minleejae --- .../statement/create/table/TableOption.java | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) 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 f784aa05f..20b183f70 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 @@ -20,12 +20,20 @@ /** A structured option following a {@code CREATE TABLE} definition. */ public class TableOption implements Serializable { + private ForeignTableOptions foreignTableOptions; + private Kind kind = Kind.OTHER; + private String name; + private String value; + private boolean useEquals; + private List tokens; + private List
unionTables; + private List storageParameters; + private ColumnOption.Storage tablespaceStorage; + 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, SECONDARY_ENGINE, AUTOEXTEND_SIZE, INSERT_METHOD, PACK_KEYS, DELAY_KEY_WRITE, CHECKSUM, CONNECTION, COMPRESSION, STORAGE_PARAMETERS, WITHOUT_OIDS, ENGINE_ATTRIBUTE, SECONDARY_ENGINE_ATTRIBUTE, ROW_FORMAT, AVG_ROW_LENGTH, MAX_ROWS, MIN_ROWS, KEY_BLOCK_SIZE, TABLESPACE, FOREIGN_SERVER, OTHER } - private ForeignTableOptions foreignTableOptions; - public ForeignTableOptions getForeignTableOptions() { return foreignTableOptions; } @@ -36,15 +44,6 @@ public static TableOption foreignServer(ForeignTableOptions options) { return option; } - private Kind kind = Kind.OTHER; - private String name; - private String value; - private boolean useEquals; - private List tokens; - private List
unionTables; - private List storageParameters; - private ColumnOption.Storage tablespaceStorage; - public ColumnOption.Storage getTablespaceStorage() { return tablespaceStorage; }