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 94bc8393f..56e21e3e0 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 9baa2afe1..0fe2a24ed 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,7 +10,9 @@ 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.schema.Column; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; /** A PostgreSQL declarative-partition bound. */ @@ -113,17 +115,101 @@ 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 ("); + appendRangeValues(sql, fromExpressions, expressionPrinter); + sql.append(") TO ("); + appendRangeValues(sql, toExpressions, expressionPrinter); + 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: + 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: - return ""; + 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/statement/create/table/TablePartitioning.java b/src/main/java/net/sf/jsqlparser/statement/create/table/TablePartitioning.java index febc2c127..70b127dbb 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 aabd31ee3..0711ca1d2 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,30 @@ 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.visitExpressions(expressions); + } } 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 6922525d9..80323f910 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 8655346ea..639bdc40b 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 000000000..065ec7f49 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PartitionTraversalTest.java @@ -0,0 +1,128 @@ +/*- + * #%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"); + } + + @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" + }) + 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()); + } +}