diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPartition.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPartition.java index f1ad1da99d..38cb0856b9 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPartition.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPartition.java @@ -22,6 +22,7 @@ import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.TableMaintenanceStatement; import net.sf.jsqlparser.statement.create.table.PartitionBound; import net.sf.jsqlparser.statement.create.table.PartitionDefinition; import net.sf.jsqlparser.statement.create.table.TablePartitioning; @@ -35,7 +36,9 @@ * MySQL partition names and definitions remain available through the inherited * {@link #getPartitions()} and {@link #getPartitionDefinitions()} methods. This subtype adds * structured access to the complete {@code PARTITION BY} clause, the table used by - * {@code EXCHANGE PARTITION}, and its validation mode. It also models PostgreSQL + * {@code EXCHANGE PARTITION}, and its validation mode, the partition count of + * {@code ADD PARTITION PARTITIONS n} and {@code COALESCE PARTITION n}, and the + * {@code NO_WRITE_TO_BINLOG}/{@code LOCAL} modifier. It also models PostgreSQL * {@code ATTACH/DETACH PARTITION} operations. */ public class AlterExpressionPartition extends AlterExpression { @@ -52,7 +55,8 @@ public enum ExchangeValidationMode { private PartitionBound partitionBound; private DetachMode detachMode; private TablePartitioning partitioning; - private Integer coalescePartitionCount; + private Integer partitionCount; + private TableMaintenanceStatement.Modifier modifier; private Table exchangeTable; private ExchangeValidationMode exchangeValidationMode; @@ -251,18 +255,38 @@ public void setAllPartitions(boolean allPartitions) { } /** - * Returns the number of partitions added by a MySQL {@code COALESCE PARTITION} operation. - * Unlike the legacy primitive accessor, this is {@code null} when no count was supplied. + * Returns the number of partitions added by MySQL {@code ADD PARTITION PARTITIONS n} or removed + * by {@code COALESCE PARTITION n}, or {@code null} when no count was supplied. An + * {@code ADD PARTITION} with a count renders the count instead of partition definitions. + */ + public Integer getPartitionCount() { + return partitionCount; + } + + public void setPartitionCount(Integer partitionCount) { + this.partitionCount = partitionCount; + } + + /** + * Returns the number of partitions removed by a MySQL {@code COALESCE PARTITION} operation. + * This is the same value as {@link #getPartitionCount()}. Unlike the legacy primitive accessor, + * this is {@code null} when no count was supplied. */ public Integer getCoalescePartitionCount() { - return coalescePartitionCount; + return partitionCount; } public void setCoalescePartitionCount(Integer coalescePartitionCount) { - this.coalescePartitionCount = coalescePartitionCount; - if (coalescePartitionCount != null) { - super.setCoalescePartitionNumber(coalescePartitionCount); - } + setPartitionCount(coalescePartitionCount); + } + + /** Returns the MySQL {@code NO_WRITE_TO_BINLOG} or {@code LOCAL} modifier, if any. */ + public TableMaintenanceStatement.Modifier getModifier() { + return modifier; + } + + public void setModifier(TableMaintenanceStatement.Modifier modifier) { + this.modifier = modifier; } @Override @@ -285,10 +309,14 @@ protected boolean usesGeneralBody() { return false; } + @Override + public int getCoalescePartitionNumber() { + return partitionCount != null ? partitionCount : 0; + } + @Override public void setCoalescePartitionNumber(int coalescePartitionNumber) { - super.setCoalescePartitionNumber(coalescePartitionNumber); - this.coalescePartitionCount = coalescePartitionNumber; + setPartitionCount(coalescePartitionNumber); } public Table getExchangeTable() { @@ -384,6 +412,16 @@ public AlterExpressionPartition withCoalescePartitionCount(Integer coalesceParti return this; } + public AlterExpressionPartition withPartitionCount(Integer partitionCount) { + setPartitionCount(partitionCount); + return this; + } + + public AlterExpressionPartition withModifier(TableMaintenanceStatement.Modifier modifier) { + setModifier(modifier); + return this; + } + public AlterExpressionPartition withExchangeTable(Table exchangeTable) { setExchangeTable(exchangeTable); return this; @@ -449,12 +487,33 @@ public void appendTo(StringBuilder b, Consumer expressionPrinter) { private void appendBody(StringBuilder b, Consumer expressionPrinter) { switch (getOperation()) { case ADD_PARTITION: - b.append("ADD PARTITION ") + appendOperationKeywords(b); + if (partitionCount != null) { + b.append(" PARTITIONS ").append(partitionCount); + } else { + b.append(' ').append( + PlainSelect.getStringList(getPartitionDefinitions(), true, true)); + } + break; + case COALESCE_PARTITION: + appendOperationKeywords(b); + b.append(' ').append(getCoalescePartitionNumber()); + break; + case REORGANIZE_PARTITION: + appendOperationKeywords(b); + b.append(' ').append(PlainSelect.getStringList(getPartitionNames())) + .append(" INTO ") .append(PlainSelect.getStringList(getPartitionDefinitions(), true, true)); break; case DROP_PARTITION: - b.append("DROP PARTITION ") - .append(PlainSelect.getStringList(getPartitionNames())); + case TRUNCATE_PARTITION: + case ANALYZE_PARTITION: + case CHECK_PARTITION: + case OPTIMIZE_PARTITION: + case REBUILD_PARTITION: + case REPAIR_PARTITION: + appendOperationKeywords(b); + b.append(' ').append(PlainSelect.getStringList(getPartitionNames())); break; case ATTACH_PARTITION: b.append("ATTACH PARTITION ").append(partitionTable).append(' '); @@ -493,4 +552,13 @@ private void appendBody(StringBuilder b, Consumer expressionPrinter) break; } } + + /** Appends {@code PARTITION} and the binlog modifier, if any. */ + private void appendOperationKeywords(StringBuilder b) { + String operation = getOperation().name(); + b.append(operation, 0, operation.length() - "_PARTITION".length()).append(" PARTITION"); + if (modifier != null) { + b.append(' ').append(modifier); + } + } } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 755d965fc0..74b2f2ecdb 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -2404,6 +2404,20 @@ public class CCJSqlParser extends AbstractJSqlParser { return table == K_TABLE || table == K_TABLES; } + /** + * Whether NO_WRITE_TO_BINLOG or LOCAL modifies a MySQL partition operation. Followed by a + * comma, INTO or the end of the statement, the word remains a partition name. + */ + private boolean isPartitionBinlogModifierAhead() { + int kind = getToken(1).kind; + if (kind != K_NO_WRITE_TO_BINLOG && kind != K_LOCAL) { + return false; + } + Token next = getToken(2); + return next.kind != EOF && next.kind != ST_SEMICOLON && next.kind != K_INTO + && !",".equals(next.image); + } + /** Whether a PostgreSQL REINDEX statement starts here; REINDEX stays an identifier elsewhere. */ private boolean isReindexAhead() { if (getToken(1).kind != K_REINDEX) { @@ -20901,6 +20915,7 @@ void MySqlDropNamedConstraint(AlterExpression alterExp): AlterExpression AlterExpressionPartitionOp(): { AlterExpressionPartition alterExp = new AlterExpressionPartition(); + AlterOperation operation; Token tk; List partitions = null; List partitionDefinition = null; @@ -20909,15 +20924,29 @@ AlterExpression AlterExpressionPartitionOp(): } { ( - LOOKAHEAD(2) { alterExp.setOperation(AlterOperation.TRUNCATE_PARTITION); } + LOOKAHEAD(2) ( + { operation = AlterOperation.TRUNCATE_PARTITION; } + | { operation = AlterOperation.ANALYZE_PARTITION; } + | { operation = AlterOperation.CHECK_PARTITION; } + | { operation = AlterOperation.OPTIMIZE_PARTITION; } + | { operation = AlterOperation.REBUILD_PARTITION; } + | { operation = AlterOperation.REPAIR_PARTITION; } + ) + { alterExp.setOperation(operation); } + [ + LOOKAHEAD({ operation != AlterOperation.TRUNCATE_PARTITION + && operation != AlterOperation.CHECK_PARTITION + && isPartitionBinlogModifierAhead() }) + PartitionBinlogModifier(alterExp) + ] partitions=PartitionNamesList() { alterExp.setPartitions(partitions); } | - LOOKAHEAD(2) tk= { - alterExp.setOperation(AlterOperation.COALESCE_PARTITION); - alterExp.setCoalescePartitionCount(Integer.valueOf(tk.image)); - } + LOOKAHEAD(2) { alterExp.setOperation(AlterOperation.COALESCE_PARTITION); } + [ PartitionBinlogModifier(alterExp) ] + tk= { alterExp.setPartitionCount(Integer.valueOf(tk.image)); } | LOOKAHEAD(2) + [ LOOKAHEAD({ isPartitionBinlogModifierAhead() }) PartitionBinlogModifier(alterExp) ] partitions=PartitionNamesList() partitionDefinition=PartitionDefinitions() { alterExp.setOperation(AlterOperation.REORGANIZE_PARTITION); alterExp.setPartitions(partitions); @@ -20942,21 +20971,6 @@ AlterExpression AlterExpressionPartitionOp(): alterExp.setPartitions(partitions); alterExp.setExchangeTable(exchangeTable); } - | - LOOKAHEAD(2) { alterExp.setOperation(AlterOperation.ANALYZE_PARTITION); } - partitions=PartitionNamesList() { alterExp.setPartitions(partitions); } - | - LOOKAHEAD(2) { alterExp.setOperation(AlterOperation.CHECK_PARTITION); } - partitions=PartitionNamesList() { alterExp.setPartitions(partitions); } - | - LOOKAHEAD(2) { alterExp.setOperation(AlterOperation.OPTIMIZE_PARTITION); } - partitions=PartitionNamesList() { alterExp.setPartitions(partitions); } - | - LOOKAHEAD(2) { alterExp.setOperation(AlterOperation.REBUILD_PARTITION); } - partitions=PartitionNamesList() { alterExp.setPartitions(partitions); } - | - LOOKAHEAD(2) { alterExp.setOperation(AlterOperation.REPAIR_PARTITION); } - partitions=PartitionNamesList() { alterExp.setPartitions(partitions); } | LOOKAHEAD(2) { alterExp.setOperation(AlterOperation.REMOVE_PARTITIONING); } | @@ -20968,6 +20982,42 @@ AlterExpression AlterExpressionPartitionOp(): { return alterExp; } } +/** + * Parses MySQL ADD PARTITION [NO_WRITE_TO_BINLOG | LOCAL] {(definitions) | PARTITIONS n} + * after ADD. + */ +AlterExpressionPartition AddPartition(): +{ + AlterExpressionPartition alterExp = new AlterExpressionPartition(); + List partitionDefinitions; + Token tk; +} +{ + { alterExp.setOperation(AlterOperation.ADD_PARTITION); } + [ PartitionBinlogModifier(alterExp) ] + ( + partitionDefinitions=PartitionDefinitions() { + alterExp.setPartitionDefinitions(partitionDefinitions); + } + | + tk= { alterExp.setPartitionCount(Integer.valueOf(tk.image)); } + ) + { return alterExp; } +} + +/** Parses MySQL's opt_no_write_to_binlog of a partition operation. */ +void PartitionBinlogModifier(AlterExpressionPartition alterExp): +{ + Token tk; +} +{ + ( tk= | tk= ) + { + alterExp.setModifier(TableMaintenanceStatement.Modifier.valueOf( + tk.image.toUpperCase(Locale.ROOT))); + } +} + /** * Parses ADD/ALTER/MODIFY operations within ALTER TABLE. * Handles: PRIMARY KEY, INDEX/KEY, SPATIAL/FULLTEXT, column COMMENT, @@ -20989,7 +21039,6 @@ AlterExpression AlterExpressionAddAlterModify(): AlterExpression.ColumnDataType alterExpressionColumnDataType = null; AlterExpression.ColumnDropNotNull alterExpressionColumnDropNotNull = null; List indexSpec = new ArrayList(); - List partitionDefinition = null; } { ( @@ -21052,15 +21101,10 @@ AlterExpression AlterExpressionAddAlterModify(): sk3=RelObjectName() tk= { alterExp.withColumnName(sk3).withCommentText(tk.image); } ) | - LOOKAHEAD(3) ( - { - alterExp = new AlterExpressionPartition(); - alterExp.setOperation(AlterOperation.ADD_PARTITION); - } - partitionDefinition=PartitionDefinitions() { - alterExp.setPartitionDefinitions(partitionDefinition); - } - ) + LOOKAHEAD( [ | ] + ( "(" | ), + { alterExp.getOperation() == AlterOperation.ADD }) + alterExp=AddPartition() | LOOKAHEAD(3) ( ( LOOKAHEAD(2) diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/MySQLAlterTablePartitionTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/MySQLAlterTablePartitionTest.java index 75dbb7aedc..b4ce3d7e02 100644 --- a/src/test/java/net/sf/jsqlparser/statement/alter/MySQLAlterTablePartitionTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/alter/MySQLAlterTablePartitionTest.java @@ -15,13 +15,16 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; import java.util.stream.Stream; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.TableMaintenanceStatement.Modifier; import net.sf.jsqlparser.statement.create.table.PartitionDefinition; import net.sf.jsqlparser.statement.create.table.TablePartitioning; import org.junit.jupiter.api.Test; @@ -52,7 +55,22 @@ public class MySQLAlterTablePartitionTest { "ALTER TABLE sales OPTIMIZE PARTITION p0", "ALTER TABLE sales REBUILD PARTITION p0", "ALTER TABLE sales REPAIR PARTITION ALL", - "ALTER TABLE sales REMOVE PARTITIONING"); + "ALTER TABLE sales REMOVE PARTITIONING", + "ALTER TABLE clients ADD PARTITION PARTITIONS 6", + "ALTER TABLE clients ADD PARTITION NO_WRITE_TO_BINLOG PARTITIONS 6", + "ALTER TABLE clients ADD PARTITION LOCAL PARTITIONS 6", + "ALTER TABLE sales ADD PARTITION NO_WRITE_TO_BINLOG " + + "(PARTITION p2027 VALUES LESS THAN (2028))", + "ALTER TABLE sales ADD PARTITION LOCAL (PARTITION p2027 VALUES LESS THAN (2028))", + "ALTER TABLE clients COALESCE PARTITION NO_WRITE_TO_BINLOG 2", + "ALTER TABLE clients COALESCE PARTITION LOCAL 2", + "ALTER TABLE sales REORGANIZE PARTITION NO_WRITE_TO_BINLOG p0 INTO " + + "(PARTITION p0a VALUES LESS THAN (50), " + + "PARTITION p0b VALUES LESS THAN (100))", + "ALTER TABLE sales ANALYZE PARTITION NO_WRITE_TO_BINLOG p0, p1", + "ALTER TABLE sales OPTIMIZE PARTITION LOCAL ALL", + "ALTER TABLE sales REBUILD PARTITION NO_WRITE_TO_BINLOG ALL", + "ALTER TABLE sales REPAIR PARTITION LOCAL p0"); private static final List MYSQL84_PARTITIONING_CLAUSES = List.of( "ALTER TABLE sales PARTITION BY HASH (id) PARTITIONS 8", @@ -123,6 +141,90 @@ public void testAllPartitionsAndCoalesceCountAst() throws JSQLParserException { assertEquals(2, coalesce.getCoalescePartitionNumber()); } + @Test + public void testAddPartitionCountAst() throws JSQLParserException { + AlterExpressionPartition expression = + parseExpression("ALTER TABLE clients ADD PARTITION PARTITIONS 6"); + + assertEquals(AlterOperation.ADD_PARTITION, expression.getOperation()); + assertEquals(6, expression.getPartitionCount()); + assertNull(expression.getPartitionDefinitions()); + assertNull(expression.getModifier()); + + Alter alter = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE clients ADD PARTITION LOCAL PARTITIONS 6", + parser -> parser.withDialect(Dialect.MYSQL)); + AlterExpressionPartition local = assertInstanceOf(AlterExpressionPartition.class, + alter.getAlterExpressions().get(0)); + assertEquals(6, local.getPartitionCount()); + assertEquals(Modifier.LOCAL, local.getModifier()); + } + + @Test + public void testBinlogModifierAst() throws JSQLParserException { + AlterExpressionPartition coalesce = + parseExpression("ALTER TABLE clients COALESCE PARTITION NO_WRITE_TO_BINLOG 2"); + assertEquals(Modifier.NO_WRITE_TO_BINLOG, coalesce.getModifier()); + assertEquals(2, coalesce.getCoalescePartitionCount()); + + AlterExpressionPartition analyze = + parseExpression("ALTER TABLE sales ANALYZE PARTITION LOCAL ALL"); + assertEquals(Modifier.LOCAL, analyze.getModifier()); + assertTrue(analyze.isAllPartitions()); + + AlterExpressionPartition definitions = parseExpression( + "ALTER TABLE sales ADD PARTITION NO_WRITE_TO_BINLOG " + + "(PARTITION p2027 VALUES LESS THAN (2028))"); + assertEquals(Modifier.NO_WRITE_TO_BINLOG, definitions.getModifier()); + assertNull(definitions.getPartitionCount()); + assertEquals("p2027", definitions.getPartitionDefinitions().get(0).getPartitionName()); + } + + @Test + public void testLocalRemainsAPartitionNameWithoutFollowingNames() + throws JSQLParserException { + AlterExpressionPartition single = + parseExpression("ALTER TABLE sales ANALYZE PARTITION local"); + assertNull(single.getModifier()); + assertEquals(List.of("local"), single.getPartitionNames()); + + AlterExpressionPartition list = + parseExpression("ALTER TABLE sales ANALYZE PARTITION local, p1"); + assertNull(list.getModifier()); + assertEquals(List.of("local", "p1"), list.getPartitionNames()); + + AlterExpressionPartition reorganize = parseExpression( + "ALTER TABLE sales REORGANIZE PARTITION local INTO " + + "(PARTITION p0 VALUES LESS THAN (100))"); + assertNull(reorganize.getModifier()); + assertEquals(List.of("local"), reorganize.getPartitionNames()); + } + + @Test + public void testAddPartitionFormsRequireAdd() { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse( + "ALTER TABLE sales MODIFY PARTITION (PARTITION p3 VALUES LESS THAN (2002))")); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse( + "ALTER TABLE sales ALTER PARTITION (PARTITION p3 VALUES LESS THAN (2002))")); + } + + @Test + public void testLegacyCoalesceAccessorsShareThePartitionCount() { + AlterExpressionPartition expression = new AlterExpressionPartition() + .withOperation(AlterOperation.COALESCE_PARTITION); + assertEquals("COALESCE PARTITION 0", expression.toString()); + + expression.setCoalescePartitionNumber(3); + assertEquals(3, expression.getPartitionCount()); + assertEquals(3, expression.getCoalescePartitionCount()); + assertEquals("COALESCE PARTITION 3", expression.toString()); + + expression.setPartitionCount(4); + assertEquals(4, expression.getCoalescePartitionNumber()); + expression.setCoalescePartitionCount(null); + assertEquals(0, expression.getCoalescePartitionNumber()); + } + @Test public void testExchangePartitionAst() throws JSQLParserException { AlterExpressionPartition expression = parseExpression( @@ -172,6 +274,12 @@ public void testProgrammaticConstruction() { assertEquals("EXCHANGE PARTITION p0 WITH TABLE archive.sales_2024 WITHOUT VALIDATION", expression.toString()); + + AlterExpressionPartition add = new AlterExpressionPartition() + .withOperation(AlterOperation.ADD_PARTITION) + .withModifier(Modifier.NO_WRITE_TO_BINLOG) + .withPartitionCount(4); + assertEquals("ADD PARTITION NO_WRITE_TO_BINLOG PARTITIONS 4", add.toString()); } private static AlterExpressionPartition parseExpression(String sql)