From 182d320453dd42ca53d6e06384307ae7d3e47b7c Mon Sep 17 00:00:00 2001 From: minleejae Date: Sat, 12 Sep 2026 12:32:23 +0900 Subject: [PATCH 1/2] fix: traverse and deparse structured table partitions --- .../alter/AlterExpressionPartition.java | 21 +++- .../create/table/PartitionBound.java | 31 ++++- .../create/table/TablePartitioning.java | 28 +++-- .../util/TableDefinitionTraversal.java | 62 ++++++++++ .../util/deparser/AlterDeParser.java | 6 + .../util/deparser/CreateTableDeParser.java | 10 +- .../create/PartitionTraversalTest.java | 110 ++++++++++++++++++ 7 files changed, 250 insertions(+), 18 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/statement/create/PartitionTraversalTest.java 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 94bc8393fc..56e21e3e07 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPartition.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPartition.java @@ -14,6 +14,8 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.create.table.PartitionBound; import net.sf.jsqlparser.statement.create.table.PartitionDefinition; @@ -292,6 +294,15 @@ public AlterExpressionPartition addPartitionDefinitions( @Override protected void appendBody(StringBuilder b) { + appendBody(b, b::append); + } + + public void appendTo(StringBuilder b, Consumer expressionPrinter) { + appendBody(b, expressionPrinter); + appendCommonTail(b); + } + + private void appendBody(StringBuilder b, Consumer expressionPrinter) { switch (getOperation()) { case ADD_PARTITION: b.append("ADD PARTITION ") @@ -302,8 +313,12 @@ protected void appendBody(StringBuilder b) { .append(PlainSelect.getStringList(getPartitionNames())); break; case ATTACH_PARTITION: - b.append("ATTACH PARTITION ").append(partitionTable).append(" ") - .append(partitionBound); + b.append("ATTACH PARTITION ").append(partitionTable).append(' '); + if (partitionBound == null) { + b.append("null"); + } else { + partitionBound.appendTo(b, expressionPrinter); + } break; case DETACH_PARTITION: b.append("DETACH PARTITION ").append(partitionTable); @@ -313,7 +328,7 @@ protected void appendBody(StringBuilder b) { break; case PARTITION_BY: if (partitioning != null) { - b.append(partitioning); + partitioning.appendTo(b, expressionPrinter); } else { toStringPartition(b); } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionBound.java b/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionBound.java index 9baa2afe1e..29d4897332 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionBound.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionBound.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.statement.create.table; import java.io.Serializable; +import java.util.function.Consumer; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; @@ -113,17 +114,37 @@ public PartitionBound withRemainder(Expression remainder) { @Override public String toString() { + StringBuilder sql = new StringBuilder(); + appendTo(sql, sql::append); + return sql.toString(); + } + + public void appendTo(StringBuilder sql, Consumer expressionPrinter) { switch (type) { case RANGE: - return "FOR VALUES FROM (" + fromExpressions + ") TO (" + toExpressions + ")"; + sql.append("FOR VALUES FROM ("); + expressionPrinter.accept(fromExpressions); + sql.append(") TO ("); + expressionPrinter.accept(toExpressions); + sql.append(')'); + break; case LIST: - return "FOR VALUES IN (" + inExpressions + ")"; + sql.append("FOR VALUES IN ("); + expressionPrinter.accept(inExpressions); + sql.append(')'); + break; case HASH: - return "FOR VALUES WITH (MODULUS " + modulus + ", REMAINDER " + remainder + ")"; + sql.append("FOR VALUES WITH (MODULUS "); + expressionPrinter.accept(modulus); + sql.append(", REMAINDER "); + expressionPrinter.accept(remainder); + sql.append(')'); + break; case DEFAULT: - return "DEFAULT"; + sql.append("DEFAULT"); + break; default: - return ""; + 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 febc2c1272..70b127dbba 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 @@ -15,6 +15,7 @@ import java.util.Collections; import java.util.List; 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; @@ -256,14 +257,20 @@ public TablePartitioning addPartitionOptions(Collection partitionOptions @Override public String toString() { - StringBuilder builder = new StringBuilder("PARTITION BY "); - appendMethod(builder); + StringBuilder builder = new StringBuilder(); + appendTo(builder, builder::append); + return builder.toString(); + } + + public void appendTo(StringBuilder builder, Consumer expressionPrinter) { + builder.append("PARTITION BY "); + appendMethod(builder, expressionPrinter); if (partitions != null) { builder.append(" PARTITIONS ").append(partitions); } if (subPartitioning != null) { builder.append(" SUBPARTITION BY "); - subPartitioning.appendMethod(builder); + subPartitioning.appendMethod(builder, expressionPrinter); if (subPartitioning.getPartitions() != null) { builder.append(" SUBPARTITIONS ").append(subPartitioning.getPartitions()); } @@ -275,10 +282,9 @@ public String toString() { if (partitionOptions != null && !partitionOptions.isEmpty()) { builder.append(" ").append(PlainSelect.getStringList(partitionOptions, false, false)); } - return builder.toString(); } - private void appendMethod(StringBuilder builder) { + private void appendMethod(StringBuilder builder, Consumer expressionPrinter) { if (linear) { builder.append("LINEAR "); } @@ -291,11 +297,17 @@ private void appendMethod(StringBuilder builder) { builder.append(" COLUMNS"); } if (expression != null) { - builder.append(" (").append(expression).append(")"); + builder.append(" ("); + expressionPrinter.accept(expression); + builder.append(')'); } else if (expressionList != null) { - builder.append(" (").append(expressionList).append(")"); + builder.append(" ("); + expressionPrinter.accept(expressionList); + builder.append(')'); } else if (columns != null) { - builder.append(" ").append(PlainSelect.getStringList(columns, true, true)); + builder.append(" ("); + expressionPrinter.accept(columns); + builder.append(')'); } } } diff --git a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java index aabd31ee3a..7706938cdd 100644 --- a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java +++ b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java @@ -15,6 +15,7 @@ import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.LikeClause; import net.sf.jsqlparser.statement.alter.AlterExpression; +import net.sf.jsqlparser.statement.alter.AlterExpressionPartition; import net.sf.jsqlparser.statement.alter.AlterExpressionPrimaryKey; import net.sf.jsqlparser.statement.create.index.CreateIndex; import net.sf.jsqlparser.statement.create.table.CheckConstraint; @@ -26,6 +27,8 @@ import net.sf.jsqlparser.statement.create.table.ForeignKeyIndex; import net.sf.jsqlparser.statement.create.table.Index; import net.sf.jsqlparser.statement.create.table.TableElement; +import net.sf.jsqlparser.statement.create.table.TablePartitioning; +import net.sf.jsqlparser.statement.create.table.PartitionBound; /** Traverses structured table definitions without interpreting legacy raw column options. */ public final class TableDefinitionTraversal { @@ -50,6 +53,26 @@ public static void visit(AlterExpression action, Consumer expression if (action.getIndex() != null) { visit(action.getIndex(), expressions, tables); } + if (action instanceof AlterExpressionPartition) { + AlterExpressionPartition partition = (AlterExpressionPartition) action; + switch (partition.getOperation()) { + case ATTACH_PARTITION: + accept(partition.getPartitionTable(), tables); + visit(partition.getPartitionBound(), expressions); + break; + case DETACH_PARTITION: + accept(partition.getPartitionTable(), tables); + break; + case EXCHANGE_PARTITION: + accept(partition.getExchangeTable(), tables); + break; + case PARTITION_BY: + visit(partition.getPartitioning(), expressions); + break; + default: + break; + } + } if (action instanceof AlterExpressionPrimaryKey) { AlterExpressionPrimaryKey primaryKey = (AlterExpressionPrimaryKey) action; if (primaryKey.isUsingHash()) { @@ -72,6 +95,45 @@ public static void visit(CreateTable table, Consumer expressions, } accept(table.getTrailingLikeTable(), tables); accept(table.getPartitionOf(), tables); + visit(table.getPartitioning(), expressions); + visit(table.getPartitionBound(), expressions); + } + + /** Visits the active partition key and any subpartition key. Raw bounds remain opaque. */ + public static void visit(TablePartitioning partitioning, Consumer expressions) { + if (partitioning == null) { + return; + } + if (partitioning.getExpression() != null) { + accept(partitioning.getExpression(), expressions); + } else if (partitioning.getExpressionList() != null) { + accept(partitioning.getExpressionList(), expressions); + } else { + accept(partitioning.getColumns(), expressions); + } + visit(partitioning.getSubPartitioning(), expressions); + } + + /** Visits expressions belonging to the selected PostgreSQL bound type. */ + public static void visit(PartitionBound bound, Consumer expressions) { + if (bound == null || bound.getType() == null) { + return; + } + switch (bound.getType()) { + case RANGE: + accept(bound.getFromExpressions(), expressions); + accept(bound.getToExpressions(), expressions); + break; + case LIST: + accept(bound.getInExpressions(), expressions); + break; + case HASH: + accept(bound.getModulus(), expressions); + accept(bound.getRemainder(), expressions); + break; + default: + break; + } } public static void visit(TableElement element, Consumer expressions, diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java index 6922525d9a..80323f910c 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java @@ -12,6 +12,7 @@ import net.sf.jsqlparser.statement.alter.Alter; import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.statement.alter.AlterExpression; +import net.sf.jsqlparser.statement.alter.AlterExpressionPartition; import net.sf.jsqlparser.statement.alter.AlterExpressionPrimaryKey; import net.sf.jsqlparser.statement.create.table.DefaultConstraint; import net.sf.jsqlparser.statement.select.PlainSelect; @@ -49,6 +50,11 @@ public void deParse(Alter alter) { } private void deParseAction(AlterExpression action) { + if (action instanceof AlterExpressionPartition) { + ((AlterExpressionPartition) action).appendTo(builder, + expression -> expression.accept(expressionVisitor, null)); + return; + } if (action instanceof AlterExpressionPrimaryKey) { ((AlterExpressionPrimaryKey) action).appendTo(builder, expression -> expression.accept(expressionVisitor, null)); diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/CreateTableDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/CreateTableDeParser.java index 8655346eaf..639bdc40b5 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/CreateTableDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/CreateTableDeParser.java @@ -105,7 +105,10 @@ public void deParse(CreateTable createTable) { } if (createTable.getPartitionBound() != null) { - builder.append(' ').append(createTable.getPartitionBound()); + builder.append(' '); + createTable.getPartitionBound().appendTo(builder, + expression -> expression.accept(statementDeParser.getExpressionDeParser(), + null)); } params = PlainSelect.getStringList(createTable.getTableOptionsStrings(), false, false); @@ -113,7 +116,10 @@ public void deParse(CreateTable createTable) { builder.append(' ').append(params); } if (createTable.getPartitioning() != null) { - builder.append(' ').append(createTable.getPartitioning()); + builder.append(' '); + createTable.getPartitioning().appendTo(builder, + expression -> expression.accept(statementDeParser.getExpressionDeParser(), + null)); } if (createTable.getRowMovement() != null) { diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PartitionTraversalTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PartitionTraversalTest.java new file mode 100644 index 0000000000..2f074c6478 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PartitionTraversalTest.java @@ -0,0 +1,110 @@ +/*- + * #%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.create; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +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 PartitionTraversalTest { + @ParameterizedTest + @ValueSource(strings = { + "ALTER TABLE parent ATTACH PARTITION child FOR VALUES FROM (0) TO (10)", + "ALTER TABLE parent ATTACH PARTITION child DEFAULT", + "ALTER TABLE parent DETACH PARTITION child CONCURRENTLY", + "ALTER TABLE parent EXCHANGE PARTITION p0 WITH TABLE child WITHOUT VALIDATION" + }) + void finderIncludesPartitionTables(String sql) throws JSQLParserException { + assertThat(TablesNamesFinder.findTables(sql)).containsExactlyInAnyOrder("parent", "child"); + } + + @Test + void visitorReceivesPartitionKeysBoundsAndContext() throws JSQLParserException { + List visited = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(Column column, S context) { + assertEquals("context", context); + visited.add(column.getColumnName()); + return null; + } + + @Override + public Void visit(LongValue value, S context) { + assertEquals("context", context); + visited.add(value.toString()); + return null; + } + }; + StatementVisitorAdapter visitor = + new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)); + CCJSqlParserUtil.parse("CREATE TABLE t (id INT) PARTITION BY RANGE (id) " + + "SUBPARTITION BY HASH (id + 1) SUBPARTITIONS 2") + .accept(visitor, "context"); + CCJSqlParserUtil + .parse("ALTER TABLE parent ATTACH PARTITION child FOR VALUES FROM (0) TO (10)") + .accept(visitor, "context"); + assertThat(visited).containsExactly("id", "id", "1", "0", "10"); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE TABLE t (id INT) PARTITION BY HASH (id + 1) PARTITIONS 2", + "ALTER TABLE t PARTITION BY RANGE COLUMNS (id)", + "CREATE TABLE child PARTITION OF parent FOR VALUES FROM (0) TO (10)", + "ALTER TABLE parent ATTACH PARTITION child FOR VALUES IN (1, 2)", + "ALTER TABLE parent ATTACH PARTITION child FOR VALUES WITH (MODULUS 4, REMAINDER 1)", + "ALTER TABLE parent ATTACH PARTITION child DEFAULT" + }) + void deparserUsesPartitionExpressionVisitor(String sql) throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql); + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(Column column, S context) { + return getBuilder().append("mapped_").append(column.getColumnName()); + } + + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 100); + } + }; + statement.accept(new StatementDeParser(expressions, new SelectDeParser(), output), null); + String expected = statement.toString() + .replace("HASH (id + 1)", "HASH (mapped_id + 101)") + .replace("COLUMNS (id)", "COLUMNS (mapped_id)") + .replace("FROM (0) TO (10)", "FROM (100) TO (110)") + .replace("IN (1, 2)", "IN (101, 102)") + .replace("MODULUS 4, REMAINDER 1", "MODULUS 104, REMAINDER 101"); + assertEquals(expected, output.toString()); + StringBuilder plain = new StringBuilder(); + statement.accept(new StatementDeParser(plain), null); + assertEquals(statement.toString(), plain.toString()); + assertEquals(expected, CCJSqlParserUtil.parse(expected).toString()); + } +} From 75bbcafd266371cf8a76e3ce508760d07822c7f2 Mon Sep 17 00:00:00 2001 From: minleejae Date: Sat, 12 Sep 2026 12:44:00 +0900 Subject: [PATCH 2/2] fix: preserve unbounded partition markers during expression rewrites --- .../create/table/PartitionBound.java | 69 ++++++++++++++++++- .../util/TableDefinitionTraversal.java | 19 +---- .../create/PartitionTraversalTest.java | 18 +++++ 3 files changed, 87 insertions(+), 19 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionBound.java b/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionBound.java index 29d4897332..0fe2a24ed0 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionBound.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionBound.java @@ -12,6 +12,7 @@ import java.io.Serializable; import java.util.function.Consumer; import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; /** A PostgreSQL declarative-partition bound. */ @@ -123,9 +124,9 @@ public void appendTo(StringBuilder sql, Consumer expressionPrinter) switch (type) { case RANGE: sql.append("FOR VALUES FROM ("); - expressionPrinter.accept(fromExpressions); + appendRangeValues(sql, fromExpressions, expressionPrinter); sql.append(") TO ("); - expressionPrinter.accept(toExpressions); + appendRangeValues(sql, toExpressions, expressionPrinter); sql.append(')'); break; case LIST: @@ -147,4 +148,68 @@ public void appendTo(StringBuilder sql, Consumer expressionPrinter) break; } } + + /** Visits active bound expressions, excluding the MINVALUE/MAXVALUE range markers. */ + public void visitExpressions(Consumer expressions) { + if (type == null) { + return; + } + switch (type) { + case RANGE: + visitRangeValues(fromExpressions, expressions); + visitRangeValues(toExpressions, expressions); + break; + case LIST: + if (inExpressions != null) { + expressions.accept(inExpressions); + } + break; + case HASH: + if (modulus != null) { + expressions.accept(modulus); + } + if (remainder != null) { + expressions.accept(remainder); + } + break; + default: + break; + } + } + + private static void visitRangeValues(ExpressionList values, + Consumer expressions) { + if (values != null) { + values.stream().filter(value -> !isRangeMarker(value)).forEach(expressions); + } + } + + private static void appendRangeValues(StringBuilder sql, ExpressionList values, + Consumer expressions) { + if (values == null) { + sql.append("null"); + return; + } + for (int i = 0; i < values.size(); i++) { + if (i > 0) { + sql.append(", "); + } + Expression value = values.get(i); + if (isRangeMarker(value)) { + sql.append(value); + } else { + expressions.accept(value); + } + } + } + + private static boolean isRangeMarker(Expression expression) { + if (!(expression instanceof Column)) { + return false; + } + Column column = (Column) expression; + return column.getTable() == null && ("MINVALUE".equalsIgnoreCase(column.getColumnName()) + || "MAXVALUE".equalsIgnoreCase(column.getColumnName())); + } + } diff --git a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java index 7706938cdd..0711ca1d27 100644 --- a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java +++ b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java @@ -116,23 +116,8 @@ public static void visit(TablePartitioning partitioning, Consumer ex /** Visits expressions belonging to the selected PostgreSQL bound type. */ public static void visit(PartitionBound bound, Consumer expressions) { - if (bound == null || bound.getType() == null) { - return; - } - switch (bound.getType()) { - case RANGE: - accept(bound.getFromExpressions(), expressions); - accept(bound.getToExpressions(), expressions); - break; - case LIST: - accept(bound.getInExpressions(), expressions); - break; - case HASH: - accept(bound.getModulus(), expressions); - accept(bound.getRemainder(), expressions); - break; - default: - break; + if (bound != null) { + bound.visitExpressions(expressions); } } diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PartitionTraversalTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PartitionTraversalTest.java index 2f074c6478..065ec7f495 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/PartitionTraversalTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/PartitionTraversalTest.java @@ -71,11 +71,29 @@ public Void visit(LongValue value, S context) { assertThat(visited).containsExactly("id", "id", "1", "0", "10"); } + @Test + void rangeMarkersAreNotVisitedAsColumnReferences() throws JSQLParserException { + List columns = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(Column column, S context) { + columns.add(column.getColumnName()); + return null; + } + }; + CCJSqlParserUtil.parse("ALTER TABLE parent ATTACH PARTITION child " + + "FOR VALUES FROM (MINVALUE) TO (MAXVALUE)") + .accept(new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)), + null); + assertThat(columns).isEmpty(); + } + @ParameterizedTest @ValueSource(strings = { "CREATE TABLE t (id INT) PARTITION BY HASH (id + 1) PARTITIONS 2", "ALTER TABLE t PARTITION BY RANGE COLUMNS (id)", "CREATE TABLE child PARTITION OF parent FOR VALUES FROM (0) TO (10)", + "CREATE TABLE child PARTITION OF parent FOR VALUES FROM (MINVALUE) TO (MAXVALUE)", "ALTER TABLE parent ATTACH PARTITION child FOR VALUES IN (1, 2)", "ALTER TABLE parent ATTACH PARTITION child FOR VALUES WITH (MODULUS 4, REMAINDER 1)", "ALTER TABLE parent ATTACH PARTITION child DEFAULT"