diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java index aa0fba53b..0f394bcab 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java @@ -1128,7 +1128,7 @@ protected void toStringPartition(StringBuilder b) { b.append("REORGANIZE PARTITION ") .append(PlainSelect.getStringList(partitions)) .append(" INTO (") - .append(partitionDefinitions.stream() + .append(getPartitionDefinitions().stream() .map(PartitionDefinition::toString) .collect(Collectors.joining(", "))) .append(")"); @@ -1168,7 +1168,7 @@ protected void toStringPartition(StringBuilder b) { } else if (partitionColumns != null && !partitionColumns.isEmpty()) { b.append("COLUMNS(").append(String.join(", ", partitionColumns)).append(") "); } - b.append("(").append(partitionDefinitions.stream() + b.append("(").append(getPartitionDefinitions().stream() .map(PartitionDefinition::toString) .collect(Collectors.joining(", "))) .append(")"); 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 56e21e3e0..6322d22af 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,17 @@ */ package net.sf.jsqlparser.statement.alter; +import java.util.AbstractList; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Optional; import java.util.function.Consumer; import net.sf.jsqlparser.expression.Expression; +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.create.table.PartitionBound; import net.sf.jsqlparser.statement.create.table.PartitionDefinition; @@ -82,24 +86,137 @@ public TablePartitioning getPartitioning() { return partitioning; } + /** + * Replaces the complete partitioning clause. Legacy partition-key accessors delegate to this + * object while it is present. Passing {@code null} clears the clause and its legacy values. + */ public void setPartitioning(TablePartitioning partitioning) { this.partitioning = partitioning; - if (partitioning != null) { - setPartitionType(partitioning.getType() != null - ? partitioning.getType().toString() + super.setPartitionType(null); + super.setPartitionExpression(null); + super.setPartitionColumns(null); + super.setPartitionDefinitions(null); + } + + @Override + public String getPartitionType() { + if (partitioning == null) { + return super.getPartitionType(); + } + return partitioning.getType() != null ? partitioning.getType().toString() : null; + } + + @Override + public void setPartitionType(String partitionType) { + if (partitioning == null) { + super.setPartitionType(partitionType); + } else { + partitioning.setType(partitionType != null + ? TablePartitioning.Type.valueOf(partitionType.toUpperCase(Locale.ROOT)) : 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); + } + } + + @Override + public Expression getPartitionExpression() { + if (partitioning == null) { + return super.getPartitionExpression(); + } + return partitioning.getExpression() != null ? partitioning.getExpression() + : partitioning.getExpressionList(); + } + + @Override + @SuppressWarnings("unchecked") + public void setPartitionExpression(Expression partitionExpression) { + if (partitioning == null) { + super.setPartitionExpression(partitionExpression); + } else if (partitionExpression instanceof ExpressionList) { + partitioning.setExpressionList((ExpressionList) partitionExpression); + partitioning.setColumnsSyntax(false); + } else { + partitioning.setExpression(partitionExpression); + if (partitionExpression == null) { + partitioning.setExpressionList(null); } else { - setPartitionColumns(null); + partitioning.setColumnsSyntax(false); + } + } + } + + /** + * Returns a mutable view of the structured column names when a partitioning clause is set. + */ + @Override + public List getPartitionColumns() { + if (partitioning == null) { + return super.getPartitionColumns(); + } + ExpressionList columns = partitioning.getColumns(); + if (columns == null) { + return null; + } + return new AbstractList() { + @Override + public String get(int index) { + return columns.get(index).getFullyQualifiedName(); + } + + @Override + public int size() { + return columns.size(); + } + + @Override + public String set(int index, String column) { + return columns.set(index, columnWithName(column)).getFullyQualifiedName(); + } + + @Override + public void add(int index, String column) { + columns.add(index, columnWithName(column)); } - setPartitionDefinitions(partitioning.getPartitionDefinitions()); + + @Override + public String remove(int index) { + return columns.remove(index).getFullyQualifiedName(); + } + }; + } + + @Override + public void setPartitionColumns(List partitionColumns) { + if (partitioning == null) { + super.setPartitionColumns(partitionColumns); + } else if (partitionColumns == null) { + partitioning.setColumns(null); + } else { + ExpressionList columns = new ExpressionList<>(); + partitionColumns.forEach(column -> columns.add(columnWithName(column))); + partitioning.setColumns(columns); + partitioning.setColumnsSyntax(partitioning.getType() == TablePartitioning.Type.RANGE + || partitioning.getType() == TablePartitioning.Type.LIST); + } + } + + private static Column columnWithName(String name) { + Column column = new Column(); + column.setName(name, false); + return column; + } + + @Override + public List getPartitionDefinitions() { + return partitioning != null ? partitioning.getPartitionDefinitions() + : super.getPartitionDefinitions(); + } + + @Override + public void setPartitionDefinitions(List partitionDefinitions) { + if (partitioning == null) { + super.setPartitionDefinitions(partitionDefinitions); + } else { + partitioning.setPartitionDefinitions(partitionDefinitions); } } diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterPartitionMutationTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterPartitionMutationTest.java new file mode 100644 index 000000000..37319554d --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterPartitionMutationTest.java @@ -0,0 +1,238 @@ +/*- + * #%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.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.create.table.PartitionDefinition; +import net.sf.jsqlparser.statement.create.table.TablePartitioning; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; + +class AlterPartitionMutationTest { + + @Test + void legacyExpressionSetterChangesParsedPartitioning() throws JSQLParserException { + Alter alter = parse("ALTER TABLE sales PARTITION BY HASH (id) PARTITIONS 8"); + AlterExpression legacy = action(alter); + Column replacement = new Column("other"); + legacy.setPartitionExpression(replacement); + + assertSame(replacement, action(alter).getPartitioning().getExpression()); + assertRoundTrip(alter, "ALTER TABLE sales PARTITION BY HASH (other) PARTITIONS 8"); + } + + @Test + void legacyColumnsReplaceExpressionsWithAppropriateSyntax() throws JSQLParserException { + Alter alter = parse("ALTER TABLE sales PARTITION BY LINEAR HASH (id) PARTITIONS 8"); + AlterExpression legacy = action(alter); + legacy.setPartitionType("key"); + legacy.setPartitionColumns(List.of("other")); + + assertNull(legacy.getPartitionExpression()); + assertFalse(action(alter).getPartitioning().isColumnsSyntax()); + assertRoundTrip(alter, "ALTER TABLE sales PARTITION BY LINEAR KEY (other) PARTITIONS 8"); + } + + @Test + void legacyRangeColumnsAndExpressionSettersReplaceOneAnother() throws JSQLParserException { + Alter alter = parse("ALTER TABLE sales PARTITION BY RANGE (id) " + + "(PARTITION p0 VALUES LESS THAN (100), PARTITION pmax VALUES LESS THAN MAXVALUE)"); + AlterExpression legacy = action(alter); + legacy.setPartitionColumns(List.of("other")); + + assertNull(legacy.getPartitionExpression()); + assertTrue(action(alter).getPartitioning().isColumnsSyntax()); + assertRoundTrip(alter, "ALTER TABLE sales PARTITION BY RANGE COLUMNS (other) " + + "(PARTITION p0 VALUES LESS THAN (100), PARTITION pmax VALUES LESS THAN MAXVALUE)"); + + legacy.setPartitionExpression(new Column("id")); + assertNull(legacy.getPartitionColumns()); + assertFalse(action(alter).getPartitioning().isColumnsSyntax()); + assertRoundTrip(alter, "ALTER TABLE sales PARTITION BY RANGE (id) " + + "(PARTITION p0 VALUES LESS THAN (100), PARTITION pmax VALUES LESS THAN MAXVALUE)"); + } + + @Test + void legacyColumnNamesAreAMutableViewOfStructuredColumns() throws JSQLParserException { + Alter alter = parse("ALTER TABLE sales PARTITION BY KEY (`id`, other) PARTITIONS 8"); + AlterExpressionPartition action = action(alter); + List names = action.getPartitionColumns(); + assertEquals("`id`", names.set(0, "`new_id`")); + names.add("extra"); + assertEquals("other", names.remove(1)); + action.getPartitioning().getColumns().get(1).setColumnName("last_id"); + + assertEquals(List.of("`new_id`", "last_id"), names); + assertRoundTrip(alter, + "ALTER TABLE sales PARTITION BY KEY (`new_id`, last_id) PARTITIONS 8"); + names.clear(); + assertTrue(action.getPartitioning().getColumns().isEmpty()); + names.add("id"); + assertRoundTrip(alter, "ALTER TABLE sales PARTITION BY KEY (id) PARTITIONS 8"); + } + + @Test + void structuredMutationsAreVisibleThroughLegacyAccessors() throws JSQLParserException { + Alter alter = parse("ALTER TABLE sales PARTITION BY HASH (id) PARTITIONS 8"); + AlterExpressionPartition action = action(alter); + TablePartitioning partitioning = action.getPartitioning(); + partitioning.setType(TablePartitioning.Type.KEY); + partitioning.setColumns(new ExpressionList<>(new Column("other"))); + + assertEquals("KEY", action.getPartitionType()); + assertEquals(List.of("other"), action.getPartitionColumns()); + assertNull(action.getPartitionExpression()); + assertRoundTrip(alter, "ALTER TABLE sales PARTITION BY KEY (other) PARTITIONS 8"); + + partitioning.setType(TablePartitioning.Type.HASH); + Column id = new Column("id"); + partitioning.setExpression(id); + assertSame(id, action.getPartitionExpression()); + assertNull(action.getPartitionColumns()); + assertRoundTrip(alter, "ALTER TABLE sales PARTITION BY HASH (id) PARTITIONS 8"); + } + + @Test + void partitionDefinitionsShareTheStructuredList() throws JSQLParserException { + Alter alter = parse("ALTER TABLE sales PARTITION BY RANGE (id) " + + "(PARTITION p0 VALUES LESS THAN (100))"); + AlterExpressionPartition action = action(alter); + List replacements = new ArrayList<>(action(parse( + "ALTER TABLE sales ADD PARTITION (PARTITION p1 VALUES LESS THAN (200))")) + .getPartitionDefinitions()); + action.setPartitionDefinitions(replacements); + assertSame(replacements, action.getPartitioning().getPartitionDefinitions()); + action.addPartitionDefinitions(action(parse("ALTER TABLE sales ADD PARTITION " + + "(PARTITION pmax VALUES LESS THAN MAXVALUE)")).getPartitionDefinitions()); + assertRoundTrip(alter, "ALTER TABLE sales PARTITION BY RANGE (id) " + + "(PARTITION p1 VALUES LESS THAN (200), PARTITION pmax VALUES LESS THAN MAXVALUE)"); + + action.getPartitioning().setPartitionDefinitions(null); + assertNull(action.getPartitionDefinitions()); + action.setPartitionDefinitions(replacements); + action.getPartitionDefinitions().remove(0); + assertRoundTrip(alter, "ALTER TABLE sales PARTITION BY RANGE (id) " + + "(PARTITION pmax VALUES LESS THAN MAXVALUE)"); + } + + @Test + void reorganizeUsesTheSameDefinitionsAsTheStructuredClause() throws JSQLParserException { + Alter alter = parse("ALTER TABLE sales PARTITION BY RANGE (id) " + + "(PARTITION p0 VALUES LESS THAN (100))"); + AlterExpressionPartition action = action(alter); + action.setOperation(AlterOperation.REORGANIZE_PARTITION); + action.setPartitionNames(List.of("p_old")); + action.getPartitioning().setPartitionDefinitions(action(parse( + "ALTER TABLE sales ADD PARTITION (PARTITION p1 VALUES LESS THAN (200))")) + .getPartitionDefinitions()); + assertRoundTrip(alter, "ALTER TABLE sales REORGANIZE PARTITION p_old INTO " + + "(PARTITION p1 VALUES LESS THAN (200))"); + } + + @Test + void clearingLegacyExpressionAlsoClearsAKeyListWithoutResurrectingOldKeys() { + AlterExpressionPartition action = new AlterExpressionPartition() + .withPartitioning(new TablePartitioning(TablePartitioning.Type.RANGE)); + ExpressionList keys = + new ExpressionList<>(new Column("id"), new Column("other")); + action.setPartitionExpression(keys); + assertSame(keys, action.getPartitioning().getExpressionList()); + assertSame(keys, action.getPartitionExpression()); + action.setPartitionColumns(null); + assertSame(keys, action.getPartitionExpression()); + action.setPartitionExpression(null); + assertNull(action.getPartitioning().getExpressionList()); + assertNull(action.getPartitionExpression()); + + action.setPartitionColumns(List.of("id")); + action.setPartitionExpression(null); + assertEquals(List.of("id"), action.getPartitionColumns()); + action.setPartitionColumns(null); + assertNull(action.getPartitionColumns()); + } + + @Test + void replacingAndClearingPartitioningDoesNotLeaveLegacySnapshots() throws JSQLParserException { + Alter alter = parse("ALTER TABLE sales PARTITION BY RANGE COLUMNS (id) " + + "(PARTITION p0 VALUES LESS THAN (100))"); + AlterExpressionPartition action = action(alter); + TablePartitioning replacement = new TablePartitioning(TablePartitioning.Type.HASH) + .withExpression(new Column("other")).withPartitions(4L); + action.setPartitioning(replacement); + assertNull(action.getPartitionColumns()); + assertNull(action.getPartitionDefinitions()); + assertRoundTrip(alter, "ALTER TABLE sales PARTITION BY HASH (other) PARTITIONS 4"); + + action.setPartitioning(null); + assertNull(action.getPartitionType()); + assertNull(action.getPartitionExpression()); + assertNull(action.getPartitionColumns()); + assertNull(action.getPartitionDefinitions()); + action.setPartitionType("RANGE"); + action.setPartitionExpression(new Column("id")); + action.setPartitionDefinitions(action(parse("ALTER TABLE sales ADD PARTITION " + + "(PARTITION pmax VALUES LESS THAN MAXVALUE)")).getPartitionDefinitions()); + assertRoundTrip(alter, "ALTER TABLE sales PARTITION BY RANGE (id) " + + "(PARTITION pmax VALUES LESS THAN MAXVALUE)"); + } + + @Test + void quotedDotsRemainPartOfTheColumnNameInLegacySettersAndListEdits() + throws JSQLParserException { + Alter alter = parse("ALTER TABLE sales PARTITION BY KEY (id) PARTITIONS 8"); + AlterExpressionPartition action = action(alter); + action.setPartitionColumns(List.of("`event.date`")); + Column column = action.getPartitioning().getColumns().get(0); + assertNull(column.getTable()); + assertEquals("`event.date`", column.getColumnName()); + assertRendering(alter, "ALTER TABLE sales PARTITION BY KEY (`event.date`) PARTITIONS 8"); + + List names = action.getPartitionColumns(); + assertEquals("`event.date`", names.set(0, "`renamed.value`")); + names.add("`other.column`"); + assertEquals(List.of("`renamed.value`", "`other.column`"), names); + action.getPartitioning().getColumns().forEach(item -> assertNull(item.getTable())); + assertRendering(alter, "ALTER TABLE sales PARTITION BY KEY " + + "(`renamed.value`, `other.column`) PARTITIONS 8"); + } + + private static Alter parse(String sql) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.MYSQL)); + } + + private static AlterExpressionPartition action(Alter alter) { + return (AlterExpressionPartition) alter.getAlterExpressions().get(0); + } + + private static void assertRoundTrip(Alter alter, String expected) throws JSQLParserException { + assertRendering(alter, expected); + assertEquals(expected, parse(alter.toString()).toString()); + } + + private static void assertRendering(Alter alter, String expected) { + assertEquals(expected, alter.toString()); + StringBuilder output = new StringBuilder(); + alter.accept(new StatementDeParser(output), null); + assertEquals(expected, output.toString()); + } +}