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 a6e1c693e..94bc8393f 100644
--- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPartition.java
+++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPartition.java
@@ -9,13 +9,26 @@
*/
package net.sf.jsqlparser.statement.alter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.create.table.PartitionBound;
+import net.sf.jsqlparser.statement.create.table.PartitionDefinition;
+import net.sf.jsqlparser.statement.create.table.TablePartitioning;
+import net.sf.jsqlparser.statement.select.PlainSelect;
/**
- * Internal subclass for partition maintenance operations within ALTER TABLE. Handles TRUNCATE,
- * COALESCE, REORGANIZE, EXCHANGE, ANALYZE, CHECK, OPTIMIZE, REBUILD, REPAIR PARTITION, PARTITION
- * BY, and REMOVE PARTITIONING.
+ * Structured model for partition operations within {@code ALTER TABLE}.
+ *
+ *
+ * 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 ATTACH/DETACH PARTITION} operations.
*/
public class AlterExpressionPartition extends AlterExpression {
@@ -23,9 +36,17 @@ public enum DetachMode {
CONCURRENTLY, FINALIZE
}
+ public enum ExchangeValidationMode {
+ WITH, WITHOUT
+ }
+
private Table partitionTable;
private PartitionBound partitionBound;
private DetachMode detachMode;
+ private TablePartitioning partitioning;
+ private Integer coalescePartitionCount;
+ private Table exchangeTable;
+ private ExchangeValidationMode exchangeValidationMode;
public Table getPartitionTable() {
return partitionTable;
@@ -51,6 +72,136 @@ public void setDetachMode(DetachMode detachMode) {
this.detachMode = detachMode;
}
+ /**
+ * Returns the full MySQL partitioning clause for a {@link AlterOperation#PARTITION_BY}
+ * operation.
+ */
+ public TablePartitioning getPartitioning() {
+ return partitioning;
+ }
+
+ public void setPartitioning(TablePartitioning partitioning) {
+ this.partitioning = partitioning;
+ if (partitioning != null) {
+ setPartitionType(partitioning.getType() != null
+ ? partitioning.getType().toString()
+ : null);
+ setPartitionExpression(partitioning.getExpression() != null
+ ? partitioning.getExpression()
+ : partitioning.getExpressionList());
+ if (partitioning.getColumns() != null) {
+ List partitionColumns = new ArrayList<>();
+ partitioning.getColumns().forEach(
+ column -> partitionColumns.add(column.getFullyQualifiedName()));
+ setPartitionColumns(partitionColumns);
+ } else {
+ setPartitionColumns(null);
+ }
+ setPartitionDefinitions(partitioning.getPartitionDefinitions());
+ }
+ }
+
+ /**
+ * Convenience alias that names the inherited MySQL partition-name list explicitly.
+ */
+ public List getPartitionNames() {
+ return getPartitions();
+ }
+
+ public void setPartitionNames(List partitionNames) {
+ setPartitions(partitionNames);
+ }
+
+ /**
+ * Returns whether the operation targets every partition rather than named partitions.
+ */
+ public boolean isAllPartitions() {
+ List partitionNames = getPartitionNames();
+ return partitionNames != null && partitionNames.size() == 1
+ && "ALL".equalsIgnoreCase(partitionNames.get(0));
+ }
+
+ public void setAllPartitions(boolean allPartitions) {
+ if (allPartitions) {
+ setPartitionNames(Collections.singletonList("ALL"));
+ } else if (isAllPartitions()) {
+ setPartitionNames(null);
+ }
+ }
+
+ /**
+ * 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.
+ */
+ public Integer getCoalescePartitionCount() {
+ return coalescePartitionCount;
+ }
+
+ public void setCoalescePartitionCount(Integer coalescePartitionCount) {
+ this.coalescePartitionCount = coalescePartitionCount;
+ if (coalescePartitionCount != null) {
+ super.setCoalescePartitionNumber(coalescePartitionCount);
+ }
+ }
+
+ @Override
+ public void setCoalescePartitionNumber(int coalescePartitionNumber) {
+ super.setCoalescePartitionNumber(coalescePartitionNumber);
+ this.coalescePartitionCount = coalescePartitionNumber;
+ }
+
+ public Table getExchangeTable() {
+ return exchangeTable;
+ }
+
+ public void setExchangeTable(Table exchangeTable) {
+ this.exchangeTable = exchangeTable;
+ super.setExchangePartitionTableName(
+ exchangeTable != null ? exchangeTable.getFullyQualifiedName() : null);
+ }
+
+ @Override
+ public void setExchangePartitionTableName(String exchangePartitionTableName) {
+ super.setExchangePartitionTableName(exchangePartitionTableName);
+ this.exchangeTable = exchangePartitionTableName != null
+ ? new Table(exchangePartitionTableName)
+ : null;
+ }
+
+ public ExchangeValidationMode getExchangeValidationMode() {
+ return exchangeValidationMode;
+ }
+
+ public void setExchangeValidationMode(ExchangeValidationMode exchangeValidationMode) {
+ this.exchangeValidationMode = exchangeValidationMode;
+ super.setExchangePartitionWithValidation(
+ exchangeValidationMode == ExchangeValidationMode.WITH);
+ super.setExchangePartitionWithoutValidation(
+ exchangeValidationMode == ExchangeValidationMode.WITHOUT);
+ }
+
+ @Override
+ public void setExchangePartitionWithValidation(boolean exchangePartitionWithValidation) {
+ super.setExchangePartitionWithValidation(exchangePartitionWithValidation);
+ if (exchangePartitionWithValidation) {
+ exchangeValidationMode = ExchangeValidationMode.WITH;
+ super.setExchangePartitionWithoutValidation(false);
+ } else if (exchangeValidationMode == ExchangeValidationMode.WITH) {
+ exchangeValidationMode = null;
+ }
+ }
+
+ @Override
+ public void setExchangePartitionWithoutValidation(boolean exchangePartitionWithoutValidation) {
+ super.setExchangePartitionWithoutValidation(exchangePartitionWithoutValidation);
+ if (exchangePartitionWithoutValidation) {
+ exchangeValidationMode = ExchangeValidationMode.WITHOUT;
+ super.setExchangePartitionWithValidation(false);
+ } else if (exchangeValidationMode == ExchangeValidationMode.WITHOUT) {
+ exchangeValidationMode = null;
+ }
+ }
+
public AlterExpressionPartition withPartitionTable(Table partitionTable) {
setPartitionTable(partitionTable);
return this;
@@ -66,18 +217,121 @@ public AlterExpressionPartition withDetachMode(DetachMode detachMode) {
return this;
}
+ @Override
+ public AlterExpressionPartition withOperation(AlterOperation operation) {
+ setOperation(operation);
+ return this;
+ }
+
+ public AlterExpressionPartition withPartitioning(TablePartitioning partitioning) {
+ setPartitioning(partitioning);
+ return this;
+ }
+
+ public AlterExpressionPartition withPartitionNames(List partitionNames) {
+ setPartitionNames(partitionNames);
+ return this;
+ }
+
+ public AlterExpressionPartition withAllPartitions(boolean allPartitions) {
+ setAllPartitions(allPartitions);
+ return this;
+ }
+
+ public AlterExpressionPartition withCoalescePartitionCount(Integer coalescePartitionCount) {
+ setCoalescePartitionCount(coalescePartitionCount);
+ return this;
+ }
+
+ public AlterExpressionPartition withExchangeTable(Table exchangeTable) {
+ setExchangeTable(exchangeTable);
+ return this;
+ }
+
+ public AlterExpressionPartition withExchangeValidationMode(
+ ExchangeValidationMode exchangeValidationMode) {
+ setExchangeValidationMode(exchangeValidationMode);
+ return this;
+ }
+
+ public AlterExpressionPartition withPartitionDefinitions(
+ List partitionDefinitions) {
+ setPartitionDefinitions(partitionDefinitions);
+ return this;
+ }
+
+ public AlterExpressionPartition addPartitionNames(String... partitionNames) {
+ List collection =
+ Optional.ofNullable(getPartitionNames()).orElseGet(ArrayList::new);
+ Collections.addAll(collection, partitionNames);
+ return withPartitionNames(collection);
+ }
+
+ public AlterExpressionPartition addPartitionNames(Collection partitionNames) {
+ List collection =
+ Optional.ofNullable(getPartitionNames()).orElseGet(ArrayList::new);
+ collection.addAll(partitionNames);
+ return withPartitionNames(collection);
+ }
+
+ public AlterExpressionPartition addPartitionDefinitions(
+ PartitionDefinition... partitionDefinitions) {
+ List collection = Optional.ofNullable(getPartitionDefinitions())
+ .orElseGet(ArrayList::new);
+ Collections.addAll(collection, partitionDefinitions);
+ return withPartitionDefinitions(collection);
+ }
+
+ public AlterExpressionPartition addPartitionDefinitions(
+ Collection extends PartitionDefinition> partitionDefinitions) {
+ List collection = Optional.ofNullable(getPartitionDefinitions())
+ .orElseGet(ArrayList::new);
+ collection.addAll(partitionDefinitions);
+ return withPartitionDefinitions(collection);
+ }
+
@Override
protected void appendBody(StringBuilder b) {
- if (getOperation() == AlterOperation.ATTACH_PARTITION) {
- b.append("ATTACH PARTITION ").append(partitionTable).append(" ")
- .append(partitionBound);
- } else if (getOperation() == AlterOperation.DETACH_PARTITION) {
- b.append("DETACH PARTITION ").append(partitionTable);
- if (detachMode != null) {
- b.append(" ").append(detachMode);
- }
- } else {
- toStringPartition(b);
+ switch (getOperation()) {
+ case ADD_PARTITION:
+ b.append("ADD PARTITION ")
+ .append(PlainSelect.getStringList(getPartitionDefinitions(), true, true));
+ break;
+ case DROP_PARTITION:
+ b.append("DROP PARTITION ")
+ .append(PlainSelect.getStringList(getPartitionNames()));
+ break;
+ case ATTACH_PARTITION:
+ b.append("ATTACH PARTITION ").append(partitionTable).append(" ")
+ .append(partitionBound);
+ break;
+ case DETACH_PARTITION:
+ b.append("DETACH PARTITION ").append(partitionTable);
+ if (detachMode != null) {
+ b.append(" ").append(detachMode);
+ }
+ break;
+ case PARTITION_BY:
+ if (partitioning != null) {
+ b.append(partitioning);
+ } else {
+ toStringPartition(b);
+ }
+ break;
+ case EXCHANGE_PARTITION:
+ if (exchangeTable != null) {
+ b.append("EXCHANGE PARTITION ").append(getPartitionNames().get(0))
+ .append(" WITH TABLE ").append(exchangeTable);
+ if (exchangeValidationMode != null) {
+ b.append(" ").append(exchangeValidationMode).append(" VALIDATION");
+ }
+ } else {
+ toStringPartition(b);
+ }
+ break;
+ default:
+ toStringPartition(b);
+ break;
}
}
}
diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/TablePartitioning.java b/src/main/java/net/sf/jsqlparser/statement/create/table/TablePartitioning.java
index 2c8a9a77b..febc2c127 100644
--- a/src/main/java/net/sf/jsqlparser/statement/create/table/TablePartitioning.java
+++ b/src/main/java/net/sf/jsqlparser/statement/create/table/TablePartitioning.java
@@ -21,7 +21,7 @@
import net.sf.jsqlparser.statement.select.PlainSelect;
/**
- * MySQL table partitioning options used by {@code CREATE TABLE}.
+ * MySQL table partitioning options used by {@code CREATE TABLE} and {@code ALTER TABLE}.
*/
public class TablePartitioning implements Serializable {
diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
index 52994191b..c2816f034 100644
--- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
+++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
@@ -13046,24 +13046,18 @@ void PartitionDefinitionOption(PartitionDefinition partitionDefinition):
List PartitionNamesList() :
{
- Token tk;
+ String partitionName = null;
List partitionNames = new ArrayList();
}
{
+ partitionName = RelObjectName() {
+ partitionNames.add(partitionName);
+ }
(
- tk = {
- partitionNames.add(tk.image);
- }
- |
- tk = {
- partitionNames.add(tk.image);
+ LOOKAHEAD(2) "," partitionName = RelObjectName() {
+ partitionNames.add(partitionName);
}
- (
- LOOKAHEAD(2) "," tk = {
- partitionNames.add(tk.image);
- }
- )*
- )
+ )*
{
return partitionNames;
}
@@ -13230,6 +13224,7 @@ AlterExpression AlterExpressionDrop():
(
(
{
+ alterExp = new AlterExpressionPartition();
alterExp.setOperation(AlterOperation.DROP_PARTITION);
}
partitions=PartitionNamesList() {
@@ -13299,12 +13294,12 @@ AlterExpression AlterExpressionDrop():
*/
AlterExpression AlterExpressionPartitionOp():
{
- AlterExpression alterExp = new AlterExpressionPartition();
+ AlterExpressionPartition alterExp = new AlterExpressionPartition();
Token tk;
List partitions = null;
List partitionDefinition = null;
- List columnNames = null;
- Expression exp = null;
+ Table exchangeTable = null;
+ TablePartitioning partitioning = null;
}
{
(
@@ -13313,7 +13308,7 @@ AlterExpression AlterExpressionPartitionOp():
|
LOOKAHEAD(2) tk= {
alterExp.setOperation(AlterOperation.COALESCE_PARTITION);
- alterExp.setCoalescePartitionNumber(Integer.valueOf(tk.image));
+ alterExp.setCoalescePartitionCount(Integer.valueOf(tk.image));
}
|
LOOKAHEAD(2)
@@ -13324,18 +13319,22 @@ AlterExpression AlterExpressionPartitionOp():
}
|
LOOKAHEAD(2) partitions=PartitionNamesList()
- tk=
+ exchangeTable=Table()
[
LOOKAHEAD(2) (
- { alterExp.setExchangePartitionWithValidation(true); }
+ {
+ alterExp.setExchangeValidationMode(AlterExpressionPartition.ExchangeValidationMode.WITH);
+ }
|
- { alterExp.setExchangePartitionWithoutValidation(false); }
+ {
+ alterExp.setExchangeValidationMode(AlterExpressionPartition.ExchangeValidationMode.WITHOUT);
+ }
)
]
{
alterExp.setOperation(AlterOperation.EXCHANGE_PARTITION);
alterExp.setPartitions(partitions);
- alterExp.setExchangePartitionTableName(tk.image);
+ alterExp.setExchangeTable(exchangeTable);
}
|
LOOKAHEAD(2) { alterExp.setOperation(AlterOperation.ANALYZE_PARTITION); }
@@ -13355,14 +13354,10 @@ AlterExpression AlterExpressionPartitionOp():
|
LOOKAHEAD(2) { alterExp.setOperation(AlterOperation.REMOVE_PARTITIONING); }
|
- LOOKAHEAD(2) { alterExp.setOperation(AlterOperation.PARTITION_BY); }
- { alterExp.setPartitionType("RANGE"); }
- (
- "(" exp=Expression() ")" { alterExp.setPartitionExpression(exp); }
- |
- columnNames=ColumnsNamesList() { alterExp.setPartitionColumns(columnNames); }
- )
- partitionDefinition=PartitionDefinitions() { alterExp.setPartitionDefinitions(partitionDefinition); }
+ LOOKAHEAD(2) partitioning=CreateTablePartitioning() {
+ alterExp.setOperation(AlterOperation.PARTITION_BY);
+ alterExp.setPartitioning(partitioning);
+ }
)
{ return alterExp; }
}
@@ -13464,6 +13459,7 @@ AlterExpression AlterExpressionAddAlterModify():
|
LOOKAHEAD(3) (
{
+ alterExp = new AlterExpressionPartition();
alterExp.setOperation(AlterOperation.ADD_PARTITION);
}
partitionDefinition=PartitionDefinitions() {
diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/MySQLAlterTablePartitionTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/MySQLAlterTablePartitionTest.java
new file mode 100644
index 000000000..75dbb7aed
--- /dev/null
+++ b/src/test/java/net/sf/jsqlparser/statement/alter/MySQLAlterTablePartitionTest.java
@@ -0,0 +1,183 @@
+/*-
+ * #%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 net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+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.assertTrue;
+
+import java.util.List;
+import java.util.stream.Stream;
+import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.schema.Table;
+import net.sf.jsqlparser.statement.create.table.PartitionDefinition;
+import net.sf.jsqlparser.statement.create.table.TablePartitioning;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/** MySQL 8.4 {@code ALTER TABLE} partition syntax verified against the MySQL manual. */
+public class MySQLAlterTablePartitionTest {
+
+ private static final List MYSQL84_PARTITION_OPERATIONS = List.of(
+ "ALTER TABLE sales ADD PARTITION "
+ + "(PARTITION p2027 VALUES LESS THAN (2028) ENGINE = InnoDB)",
+ "ALTER TABLE sales DROP PARTITION p2023, `p2024`",
+ "ALTER TABLE sales DISCARD PARTITION p2024 TABLESPACE",
+ "ALTER TABLE sales IMPORT PARTITION ALL TABLESPACE",
+ "ALTER TABLE sales TRUNCATE PARTITION ALL",
+ "ALTER TABLE sales COALESCE PARTITION 2",
+ "ALTER TABLE sales REORGANIZE PARTITION p0, p1 INTO "
+ + "(PARTITION p_low VALUES LESS THAN (100), "
+ + "PARTITION p_high VALUES LESS THAN MAXVALUE)",
+ "ALTER TABLE sales EXCHANGE PARTITION p0 WITH TABLE archive.sales_2024",
+ "ALTER TABLE sales EXCHANGE PARTITION p0 WITH TABLE archive.sales_2024 "
+ + "WITH VALIDATION",
+ "ALTER TABLE sales EXCHANGE PARTITION p0 WITH TABLE archive.sales_2024 "
+ + "WITHOUT VALIDATION",
+ "ALTER TABLE sales ANALYZE PARTITION p0, p1",
+ "ALTER TABLE sales CHECK PARTITION ALL",
+ "ALTER TABLE sales OPTIMIZE PARTITION p0",
+ "ALTER TABLE sales REBUILD PARTITION p0",
+ "ALTER TABLE sales REPAIR PARTITION ALL",
+ "ALTER TABLE sales REMOVE PARTITIONING");
+
+ private static final List MYSQL84_PARTITIONING_CLAUSES = List.of(
+ "ALTER TABLE sales PARTITION BY HASH (id) PARTITIONS 8",
+ "ALTER TABLE sales PARTITION BY LINEAR KEY ALGORITHM = 1 (id) PARTITIONS 4",
+ "ALTER TABLE sales PARTITION BY RANGE (YEAR(created_at)) "
+ + "SUBPARTITION BY HASH (id) SUBPARTITIONS 2 "
+ + "(PARTITION p2026 VALUES LESS THAN (2027), "
+ + "PARTITION pmax VALUES LESS THAN MAXVALUE)",
+ "ALTER TABLE sales PARTITION BY LIST COLUMNS (region, tier) "
+ + "(PARTITION p_eu VALUES IN (('eu', 1), ('eu', 2)), "
+ + "PARTITION p_us VALUES IN (('us', 1)))");
+
+ private static Stream mysql84PartitionOperations() {
+ return MYSQL84_PARTITION_OPERATIONS.stream();
+ }
+
+ private static Stream mysql84PartitioningClauses() {
+ return MYSQL84_PARTITIONING_CLAUSES.stream();
+ }
+
+ @ParameterizedTest
+ @MethodSource("mysql84PartitionOperations")
+ public void testPartitionOperationsUseStructuredSubtype(String sql)
+ throws JSQLParserException {
+ assertInstanceOf(AlterExpressionPartition.class, parseExpression(sql));
+ assertSqlCanBeParsedAndDeparsed(sql);
+ }
+
+ @ParameterizedTest
+ @MethodSource("mysql84PartitioningClauses")
+ public void testPartitionByUsesCreateTablePartitioningModel(String sql)
+ throws JSQLParserException {
+ AlterExpressionPartition expression = parseExpression(sql);
+ assertEquals(AlterOperation.PARTITION_BY, expression.getOperation());
+ assertNotNull(expression.getPartitioning());
+ assertSqlCanBeParsedAndDeparsed(sql);
+ }
+
+ @Test
+ public void testAddPartitionDefinitionAst() throws JSQLParserException {
+ AlterExpressionPartition expression = parseExpression(
+ "ALTER TABLE sales ADD PARTITION "
+ + "(PARTITION p2027 VALUES LESS THAN (2028) ENGINE = InnoDB "
+ + "COMMENT = 'archive')");
+
+ assertEquals(AlterOperation.ADD_PARTITION, expression.getOperation());
+ PartitionDefinition definition = expression.getPartitionDefinitions().get(0);
+ assertEquals("p2027", definition.getPartitionName());
+ assertEquals(PartitionDefinition.ValueOperator.LESS_THAN,
+ definition.getValueOperator());
+ assertEquals(List.of("2028"), definition.getValues());
+ assertEquals("InnoDB", definition.getStorageEngine());
+ assertEquals("'archive'", definition.getComment());
+ }
+
+ @Test
+ public void testAllPartitionsAndCoalesceCountAst() throws JSQLParserException {
+ AlterExpressionPartition all =
+ parseExpression("ALTER TABLE sales TRUNCATE PARTITION ALL");
+ assertTrue(all.isAllPartitions());
+ assertEquals(List.of("ALL"), all.getPartitionNames());
+ assertNull(all.getCoalescePartitionCount());
+
+ AlterExpressionPartition coalesce =
+ parseExpression("ALTER TABLE sales COALESCE PARTITION 2");
+ assertFalse(coalesce.isAllPartitions());
+ assertEquals(2, coalesce.getCoalescePartitionCount());
+ assertEquals(2, coalesce.getCoalescePartitionNumber());
+ }
+
+ @Test
+ public void testExchangePartitionAst() throws JSQLParserException {
+ AlterExpressionPartition expression = parseExpression(
+ "ALTER TABLE sales EXCHANGE PARTITION `p0` WITH TABLE "
+ + "`archive`.`sales_2024` WITHOUT VALIDATION");
+
+ assertEquals(List.of("`p0`"), expression.getPartitionNames());
+ assertEquals("`archive`.`sales_2024`",
+ expression.getExchangeTable().getFullyQualifiedName());
+ assertEquals(AlterExpressionPartition.ExchangeValidationMode.WITHOUT,
+ expression.getExchangeValidationMode());
+ assertTrue(expression.isExchangePartitionWithoutValidation());
+ assertFalse(expression.isExchangePartitionWithValidation());
+ assertEquals("`archive`.`sales_2024`", expression.getExchangePartitionTableName());
+ assertSqlCanBeParsedAndDeparsed("ALTER TABLE sales " + expression);
+ }
+
+ @Test
+ public void testPartitionByAst() throws JSQLParserException {
+ AlterExpressionPartition expression = parseExpression(
+ "ALTER TABLE sales PARTITION BY LIST COLUMNS (region, tier) "
+ + "(PARTITION p_eu VALUES IN (('eu', 1), ('eu', 2)), "
+ + "PARTITION p_us VALUES IN (('us', 1)))");
+
+ TablePartitioning partitioning = expression.getPartitioning();
+ assertEquals(TablePartitioning.Type.LIST, partitioning.getType());
+ assertTrue(partitioning.isColumnsSyntax());
+ assertEquals("region", partitioning.getColumns().get(0).getColumnName());
+ assertEquals("tier", partitioning.getColumns().get(1).getColumnName());
+ assertEquals(PartitionDefinition.ValueOperator.IN,
+ partitioning.getPartitionDefinitions().get(0).getValueOperator());
+
+ // Legacy accessors remain populated for source compatibility.
+ assertEquals("LIST", expression.getPartitionType());
+ assertEquals(List.of("region", "tier"), expression.getPartitionColumns());
+ assertEquals(partitioning.getPartitionDefinitions(), expression.getPartitionDefinitions());
+ }
+
+ @Test
+ public void testProgrammaticConstruction() {
+ AlterExpressionPartition expression = new AlterExpressionPartition()
+ .withOperation(AlterOperation.EXCHANGE_PARTITION)
+ .addPartitionNames("p0")
+ .withExchangeTable(new Table("archive", "sales_2024"))
+ .withExchangeValidationMode(
+ AlterExpressionPartition.ExchangeValidationMode.WITHOUT);
+
+ assertEquals("EXCHANGE PARTITION p0 WITH TABLE archive.sales_2024 WITHOUT VALIDATION",
+ expression.toString());
+ }
+
+ private static AlterExpressionPartition parseExpression(String sql)
+ throws JSQLParserException {
+ Alter alter = (Alter) CCJSqlParserUtil.parse(sql);
+ return assertInstanceOf(AlterExpressionPartition.class,
+ alter.getAlterExpressions().get(0));
+ }
+}