From 23214d9bdea6c4bf20201c90e6c74d5c047cc6bf Mon Sep 17 00:00:00 2001 From: Minjae Lee Date: Thu, 3 Sep 2026 16:29:51 +0900 Subject: [PATCH 1/2] fix(parser): model PostgreSQL declarative partitions --- .../statement/alter/AlterExpression.java | 2 + .../alter/AlterExpressionPartition.java | 62 ++++++++- .../statement/alter/AlterOperation.java | 2 +- .../statement/create/table/CreateTable.java | 34 +++++ .../create/table/PartitionBound.java | 129 ++++++++++++++++++ .../create/table/TablePartitioning.java | 21 +++ .../util/deparser/CreateTableDeParser.java | 8 ++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 115 +++++++++++++++- .../jsqlparser/statement/alter/AlterTest.java | 42 ++++++ .../statement/create/CreateTableTest.java | 57 ++++++++ 10 files changed, 464 insertions(+), 8 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/table/PartitionBound.java 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 7e2fea943..4055eb8b1 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java @@ -830,6 +830,8 @@ protected boolean isPartitionOperation() { switch (operation) { case DISCARD_PARTITION: case IMPORT_PARTITION: + case ATTACH_PARTITION: + case DETACH_PARTITION: case TRUNCATE_PARTITION: case COALESCE_PARTITION: case REORGANIZE_PARTITION: 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 dd9e37b6f..a6e1c693e 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPartition.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPartition.java @@ -9,6 +9,9 @@ */ package net.sf.jsqlparser.statement.alter; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.create.table.PartitionBound; + /** * Internal subclass for partition maintenance operations within ALTER TABLE. Handles TRUNCATE, * COALESCE, REORGANIZE, EXCHANGE, ANALYZE, CHECK, OPTIMIZE, REBUILD, REPAIR PARTITION, PARTITION @@ -16,8 +19,65 @@ */ public class AlterExpressionPartition extends AlterExpression { + public enum DetachMode { + CONCURRENTLY, FINALIZE + } + + private Table partitionTable; + private PartitionBound partitionBound; + private DetachMode detachMode; + + public Table getPartitionTable() { + return partitionTable; + } + + public void setPartitionTable(Table partitionTable) { + this.partitionTable = partitionTable; + } + + public PartitionBound getPartitionBound() { + return partitionBound; + } + + public void setPartitionBound(PartitionBound partitionBound) { + this.partitionBound = partitionBound; + } + + public DetachMode getDetachMode() { + return detachMode; + } + + public void setDetachMode(DetachMode detachMode) { + this.detachMode = detachMode; + } + + public AlterExpressionPartition withPartitionTable(Table partitionTable) { + setPartitionTable(partitionTable); + return this; + } + + public AlterExpressionPartition withPartitionBound(PartitionBound partitionBound) { + setPartitionBound(partitionBound); + return this; + } + + public AlterExpressionPartition withDetachMode(DetachMode detachMode) { + setDetachMode(detachMode); + return this; + } + @Override protected void appendBody(StringBuilder b) { - toStringPartition(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); + } } } diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java index a33a98536..9f7da8a8c 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java @@ -12,7 +12,7 @@ import java.util.Locale; public enum AlterOperation { - ADD, ALTER, DROP, DROP_PRIMARY_KEY, DROP_UNIQUE, DROP_FOREIGN_KEY, MODIFY, CHANGE, CONVERT, COLLATE, ALGORITHM, RENAME, RENAME_TABLE, RENAME_INDEX, RENAME_KEY, RENAME_CONSTRAINT, COMMENT, COMMENT_WITH_EQUAL_SIGN, UNSPECIFIC, ADD_PARTITION, DROP_PARTITION, DISCARD_PARTITION, IMPORT_PARTITION, TRUNCATE_PARTITION, COALESCE_PARTITION, REORGANIZE_PARTITION, EXCHANGE_PARTITION, ANALYZE_PARTITION, CHECK_PARTITION, OPTIMIZE_PARTITION, REBUILD_PARTITION, REPAIR_PARTITION, REMOVE_PARTITIONING, PARTITION_BY, SET_TABLE_OPTION, ENGINE, FORCE, KEY_BLOCK_SIZE, LOCK, DISCARD_TABLESPACE, IMPORT_TABLESPACE, DISABLE_KEYS, ENABLE_KEYS, ENABLE_ROW_LEVEL_SECURITY, DISABLE_ROW_LEVEL_SECURITY, FORCE_ROW_LEVEL_SECURITY, NO_FORCE_ROW_LEVEL_SECURITY; + ADD, ALTER, DROP, DROP_PRIMARY_KEY, DROP_UNIQUE, DROP_FOREIGN_KEY, MODIFY, CHANGE, CONVERT, COLLATE, ALGORITHM, RENAME, RENAME_TABLE, RENAME_INDEX, RENAME_KEY, RENAME_CONSTRAINT, COMMENT, COMMENT_WITH_EQUAL_SIGN, UNSPECIFIC, ADD_PARTITION, DROP_PARTITION, ATTACH_PARTITION, DETACH_PARTITION, DISCARD_PARTITION, IMPORT_PARTITION, TRUNCATE_PARTITION, COALESCE_PARTITION, REORGANIZE_PARTITION, EXCHANGE_PARTITION, ANALYZE_PARTITION, CHECK_PARTITION, OPTIMIZE_PARTITION, REBUILD_PARTITION, REPAIR_PARTITION, REMOVE_PARTITIONING, PARTITION_BY, SET_TABLE_OPTION, ENGINE, FORCE, KEY_BLOCK_SIZE, LOCK, DISCARD_TABLESPACE, IMPORT_TABLESPACE, DISABLE_KEYS, ENABLE_KEYS, ENABLE_ROW_LEVEL_SECURITY, DISABLE_ROW_LEVEL_SECURITY, FORCE_ROW_LEVEL_SECURITY, NO_FORCE_ROW_LEVEL_SECURITY; public static AlterOperation from(String operation) { return Enum.valueOf(AlterOperation.class, operation.toUpperCase(Locale.ROOT)); diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java b/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java index acbe0b718..e5c8b6806 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java @@ -37,6 +37,8 @@ public class CreateTable implements Statement { private boolean ifNotExists = false; private boolean orReplace = false; private TablePartitioning partitioning; + private Table partitionOf; + private PartitionBound partitionBound; private RowMovement rowMovement; @@ -157,6 +159,22 @@ public void setPartitioning(TablePartitioning partitioning) { this.partitioning = partitioning; } + public Table getPartitionOf() { + return partitionOf; + } + + public void setPartitionOf(Table partitionOf) { + this.partitionOf = partitionOf; + } + + public PartitionBound getPartitionBound() { + return partitionBound; + } + + public void setPartitionBound(PartitionBound partitionBound) { + this.partitionBound = partitionBound; + } + public boolean isSelectParenthesis() { return selectParenthesis; } @@ -179,6 +197,9 @@ public String toString() { StringBuilder b = new StringBuilder(); appendCreateClause(b); appendColumnDefinitions(b); + if (partitionBound != null) { + b.append(" ").append(partitionBound); + } appendTableOptions(b); appendTableProperties(b); return b.toString(); @@ -202,6 +223,9 @@ private void appendCreateClause(StringBuilder b) { b.append("IF NOT EXISTS "); } b.append(table); + if (partitionOf != null) { + b.append(" PARTITION OF ").append(partitionOf); + } } private void appendColumnDefinitions(StringBuilder b) { @@ -289,6 +313,16 @@ public CreateTable withPartitioning(TablePartitioning partitioning) { return this; } + public CreateTable withPartitionOf(Table partitionOf) { + setPartitionOf(partitionOf); + return this; + } + + public CreateTable withPartitionBound(PartitionBound partitionBound) { + setPartitionBound(partitionBound); + return this; + } + public CreateTable withRowMovement(RowMovement rowMovement) { this.setRowMovement(rowMovement); return this; 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 new file mode 100644 index 000000000..9baa2afe1 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionBound.java @@ -0,0 +1,129 @@ +/*- + * #%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.table; + +import java.io.Serializable; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; + +/** A PostgreSQL declarative-partition bound. */ +public class PartitionBound implements Serializable { + + public enum Type { + RANGE, LIST, HASH, DEFAULT + } + + private Type type; + private ExpressionList fromExpressions; + private ExpressionList toExpressions; + private ExpressionList inExpressions; + private Expression modulus; + private Expression remainder; + + public PartitionBound() {} + + public PartitionBound(Type type) { + this.type = type; + } + + public Type getType() { + return type; + } + + public void setType(Type type) { + this.type = type; + } + + public ExpressionList getFromExpressions() { + return fromExpressions; + } + + public void setFromExpressions(ExpressionList fromExpressions) { + this.fromExpressions = fromExpressions; + } + + public ExpressionList getToExpressions() { + return toExpressions; + } + + public void setToExpressions(ExpressionList toExpressions) { + this.toExpressions = toExpressions; + } + + public ExpressionList getInExpressions() { + return inExpressions; + } + + public void setInExpressions(ExpressionList inExpressions) { + this.inExpressions = inExpressions; + } + + public Expression getModulus() { + return modulus; + } + + public void setModulus(Expression modulus) { + this.modulus = modulus; + } + + public Expression getRemainder() { + return remainder; + } + + public void setRemainder(Expression remainder) { + this.remainder = remainder; + } + + public PartitionBound withType(Type type) { + setType(type); + return this; + } + + public PartitionBound withFromExpressions(ExpressionList fromExpressions) { + setFromExpressions(fromExpressions); + return this; + } + + public PartitionBound withToExpressions(ExpressionList toExpressions) { + setToExpressions(toExpressions); + return this; + } + + public PartitionBound withInExpressions(ExpressionList inExpressions) { + setInExpressions(inExpressions); + return this; + } + + public PartitionBound withModulus(Expression modulus) { + setModulus(modulus); + return this; + } + + public PartitionBound withRemainder(Expression remainder) { + setRemainder(remainder); + return this; + } + + @Override + public String toString() { + switch (type) { + case RANGE: + return "FOR VALUES FROM (" + fromExpressions + ") TO (" + toExpressions + ")"; + case LIST: + return "FOR VALUES IN (" + inExpressions + ")"; + case HASH: + return "FOR VALUES WITH (MODULUS " + modulus + ", REMAINDER " + remainder + ")"; + case DEFAULT: + return "DEFAULT"; + default: + return ""; + } + } +} 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 fa0ce42c0..2c8a9a77b 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 @@ -33,6 +33,7 @@ public enum Type { private boolean linear; private boolean columnsSyntax; private Expression expression; + private ExpressionList expressionList; private ExpressionList columns; private Integer algorithm; private boolean algorithmUseEquals; @@ -79,6 +80,19 @@ public void setExpression(Expression expression) { this.expression = expression; } + /** + * Returns the ordered partition key expressions when more than one key was declared. + * + * @return partition key expressions, or {@code null} for the legacy single-expression form + */ + public ExpressionList getExpressionList() { + return expressionList; + } + + public void setExpressionList(ExpressionList expressionList) { + this.expressionList = expressionList; + } + public ExpressionList getColumns() { return columns; } @@ -155,6 +169,11 @@ public TablePartitioning withExpression(Expression expression) { return this; } + public TablePartitioning withExpressionList(ExpressionList expressionList) { + setExpressionList(expressionList); + return this; + } + public TablePartitioning withColumns(ExpressionList columns) { setColumns(columns); return this; @@ -273,6 +292,8 @@ private void appendMethod(StringBuilder builder) { } if (expression != null) { builder.append(" (").append(expression).append(")"); + } else if (expressionList != null) { + builder.append(" (").append(expressionList).append(")"); } else if (columns != null) { builder.append(" ").append(PlainSelect.getStringList(columns, true, true)); } 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 391b08889..0c0cbabeb 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/CreateTableDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/CreateTableDeParser.java @@ -52,6 +52,10 @@ public void deParse(CreateTable createTable) { builder.append("IF NOT EXISTS "); } builder.append(createTable.getTable().getFullyQualifiedName()); + if (createTable.getPartitionOf() != null) { + builder.append(" PARTITION OF ") + .append(createTable.getPartitionOf().getFullyQualifiedName()); + } if (createTable.getColumns() != null && !createTable.getColumns().isEmpty()) { builder.append(" ("); @@ -92,6 +96,10 @@ public void deParse(CreateTable createTable) { builder.append(")"); } + if (createTable.getPartitionBound() != null) { + builder.append(' ').append(createTable.getPartitionBound()); + } + params = PlainSelect.getStringList(createTable.getTableOptionsStrings(), false, false); if (!"".equals(params)) { builder.append(' ').append(params); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 8ed4a18d5..c8c8f2b17 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1460,6 +1460,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1521,6 +1522,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1564,6 +1566,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1640,6 +1643,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1708,6 +1712,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -11558,6 +11563,8 @@ CreateTable CreateTable(boolean isUsingOrReplace): String tableColumn = null; List columns = new ArrayList(); TablePartitioning partitioning = null; + Table partitionOfTable = null; + PartitionBound partitionBound = null; } { { createTable.setOrReplace(isUsingOrReplace);} @@ -11571,6 +11578,8 @@ CreateTable CreateTable(boolean isUsingOrReplace): [ LOOKAHEAD(2) { createTable.setIfNotExists(true); }] table=Table() + [ LOOKAHEAD(2) partitionOfTable=Table() + { createTable.setPartitionOf(partitionOfTable); } ] [ LOOKAHEAD(2) ( LOOKAHEAD(3) ( "(" tableColumn=RelObjectName() { columns.add(tableColumn); } ("," tableColumn=RelObjectName() { columns.add(tableColumn); } )* ")" @@ -11599,6 +11608,9 @@ CreateTable CreateTable(boolean isUsingOrReplace): ) ) ] + [ LOOKAHEAD({ partitionOfTable != null }) + partitionBound=PostgreSqlPartitionBound() + { createTable.setPartitionBound(partitionBound); } ] ( LOOKAHEAD(2, { getToken(1).kind != K_AS && !(getToken(1).kind == K_PARTITION && getToken(2).kind == K_BY) }) parameter=CreateParameter() { tableOptions.addAll(parameter); } )* @@ -12586,7 +12598,7 @@ TablePartitioning CreateTablePartitioning(): { TablePartitioning partitioning = null; TablePartitioning subPartitioning = null; - Expression expression = null; + ExpressionList expressionList = null; ExpressionList columns = null; List partitionDefinitions = null; List parameter = null; @@ -12600,7 +12612,13 @@ TablePartitioning CreateTablePartitioning(): | { partitioning = new TablePartitioning(TablePartitioning.Type.RANGE); } ( - "(" expression=Expression() ")" { partitioning.setExpression(expression); } + "(" expressionList=SimpleExpressionList() ")" { + if (expressionList.size() == 1) { + partitioning.setExpression(expressionList.get(0)); + } else { + partitioning.setExpressionList(expressionList); + } + } | "(" columns=ColumnList() ")" { @@ -12611,7 +12629,13 @@ TablePartitioning CreateTablePartitioning(): | { partitioning = new TablePartitioning(TablePartitioning.Type.LIST); } ( - "(" expression=Expression() ")" { partitioning.setExpression(expression); } + "(" expressionList=SimpleExpressionList() ")" { + if (expressionList.size() == 1) { + partitioning.setExpression(expressionList.get(0)); + } else { + partitioning.setExpressionList(expressionList); + } + } | "(" columns=ColumnList() ")" { @@ -12642,10 +12666,49 @@ TablePartitioning CreateTablePartitioning(): } } +PartitionBound PostgreSqlPartitionBound(): +{ + PartitionBound bound = null; + ExpressionList expressions = null; + Expression modulus = null; + Expression remainder = null; +} +{ + ( + { bound = new PartitionBound(PartitionBound.Type.DEFAULT); } + | + + ( + "(" expressions=SimpleExpressionList() ")" + { + bound = new PartitionBound(PartitionBound.Type.RANGE); + bound.setFromExpressions(expressions); + } + "(" expressions=SimpleExpressionList() ")" + { bound.setToExpressions(expressions); } + | + "(" expressions=SimpleExpressionList() ")" + { + bound = new PartitionBound(PartitionBound.Type.LIST); + bound.setInExpressions(expressions); + } + | + "(" modulus=Expression() "," + remainder=Expression() ")" + { + bound = new PartitionBound(PartitionBound.Type.HASH); + bound.setModulus(modulus); + bound.setRemainder(remainder); + } + ) + ) + { return bound; } +} + TablePartitioning HashOrKeyPartitioning(): { TablePartitioning partitioning = new TablePartitioning(); - Expression expression = null; + ExpressionList expressionList = null; ExpressionList columns = null; Token tk = null; boolean useEquals = false; @@ -12653,10 +12716,14 @@ TablePartitioning HashOrKeyPartitioning(): { [ { partitioning.setLinear(true); } ] ( - "(" expression=Expression() ")" + "(" expressionList=SimpleExpressionList() ")" { partitioning.setType(TablePartitioning.Type.HASH); - partitioning.setExpression(expression); + if (expressionList.size() == 1) { + partitioning.setExpression(expressionList.get(0)); + } else { + partitioning.setExpressionList(expressionList); + } } | @@ -13412,6 +13479,40 @@ AlterExpression AlterExpressionRenameOp(): { return alterExp; } } +AlterExpressionPartition AlterExpressionPostgreSqlPartitionOp(): +{ + AlterExpressionPartition alterExp = new AlterExpressionPartition(); + Table partitionTable = null; + PartitionBound partitionBound = null; +} +{ + ( + partitionTable=Table() + partitionBound=PostgreSqlPartitionBound() + { + alterExp.setOperation(AlterOperation.ATTACH_PARTITION); + alterExp.setPartitionTable(partitionTable); + alterExp.setPartitionBound(partitionBound); + } + | + partitionTable=Table() + { + alterExp.setOperation(AlterOperation.DETACH_PARTITION); + alterExp.setPartitionTable(partitionTable); + } + [ + { + alterExp.setDetachMode(AlterExpressionPartition.DetachMode.CONCURRENTLY); + } + | + { + alterExp.setDetachMode(AlterExpressionPartition.DetachMode.FINALIZE); + } + ] + ) + { return alterExp; } +} + /** * Dispatcher production for all ALTER TABLE expression types. * Delegates to focused sub-productions for each operation category. @@ -13562,6 +13663,8 @@ AlterExpression AlterExpression(): ) | alterExp = AlterExpressionDiscardOrImport() + | + alterExp = AlterExpressionPostgreSqlPartitionOp() | LOOKAHEAD(4) ( { alterExp = new AlterExpression(); } diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java index 66e947ac0..771bb8ec5 100644 --- a/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java @@ -28,6 +28,7 @@ import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.expression.operators.relational.NotEqualsTo; import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.ReferentialAction; @@ -42,6 +43,7 @@ import net.sf.jsqlparser.statement.create.table.Index.ColumnParams; import net.sf.jsqlparser.statement.create.table.NamedConstraint; import net.sf.jsqlparser.statement.create.table.PartitionDefinition; +import net.sf.jsqlparser.statement.create.table.PartitionBound; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -2444,4 +2446,44 @@ public void testAlterTableAddMultiValuedIndexIssue2490() throws JSQLParserExcept assertSqlCanBeParsedAndDeparsed( "ALTER TABLE t ADD INDEX i31 ((CAST(data -> '$.zips' AS UNSIGNED ARRAY)))"); } + + @Test + public void testPostgreSqlAttachPartitionIssue2523() throws JSQLParserException { + String sql = "ALTER TABLE measurement ATTACH PARTITION measurement_y2026m01 " + + "FOR VALUES FROM ('2026-01-01') TO ('2026-02-01')"; + Alter alter = (Alter) assertSqlCanBeParsedAndDeparsed( + sql, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + AlterExpressionPartition expression = + (AlterExpressionPartition) alter.getAlterExpressions().get(0); + + assertEquals(AlterOperation.ATTACH_PARTITION, expression.getOperation()); + assertEquals("measurement_y2026m01", expression.getPartitionTable().getName()); + assertEquals(PartitionBound.Type.RANGE, expression.getPartitionBound().getType()); + } + + @Test + public void testPostgreSqlDetachPartitionModesIssue2523() throws JSQLParserException { + String concurrently = "ALTER TABLE measurement DETACH PARTITION " + + "measurement_y2026m01 CONCURRENTLY"; + Alter concurrentAlter = (Alter) assertSqlCanBeParsedAndDeparsed( + concurrently, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + AlterExpressionPartition concurrentExpression = + (AlterExpressionPartition) concurrentAlter.getAlterExpressions().get(0); + assertEquals(AlterOperation.DETACH_PARTITION, concurrentExpression.getOperation()); + assertEquals(AlterExpressionPartition.DetachMode.CONCURRENTLY, + concurrentExpression.getDetachMode()); + + String finalizeSql = + "ALTER TABLE measurement DETACH PARTITION measurement_y2026m01 FINALIZE"; + Alter finalizeAlter = (Alter) assertSqlCanBeParsedAndDeparsed( + finalizeSql, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + AlterExpressionPartition finalizeExpression = + (AlterExpressionPartition) finalizeAlter.getAlterExpressions().get(0); + assertEquals(AlterExpressionPartition.DetachMode.FINALIZE, + finalizeExpression.getDetachMode()); + + assertSqlCanBeParsedAndDeparsed( + "ALTER TABLE measurement DETACH PARTITION measurement_y2026m01", + true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/create/CreateTableTest.java b/src/test/java/net/sf/jsqlparser/statement/create/CreateTableTest.java index a58caea80..407ca582b 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/CreateTableTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/CreateTableTest.java @@ -29,6 +29,7 @@ import net.sf.jsqlparser.expression.operators.relational.GreaterThan; import net.sf.jsqlparser.parser.CCJSqlParserManager; import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.create.table.ColDataType; @@ -36,7 +37,9 @@ import net.sf.jsqlparser.statement.create.table.CreateTable; import net.sf.jsqlparser.statement.create.table.ExcludeConstraint; import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.create.table.PartitionBound; import net.sf.jsqlparser.statement.create.table.RowMovementMode; +import net.sf.jsqlparser.statement.create.table.TablePartitioning; import net.sf.jsqlparser.test.TestException; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; @@ -1247,4 +1250,58 @@ void testUniqueIndexIssue1893() throws JSQLParserException { // A plain INDEX must still parse unchanged. assertSqlCanBeParsedAndDeparsed("CREATE TABLE t (a int, INDEX idx (a))", true); } + + @Test + void testPostgreSqlDeclarativePartitionParentsIssue2522() throws JSQLParserException { + String sql = "CREATE TABLE pg_range_parent (tenant_id int, " + + "happened_at timestamptz) PARTITION BY RANGE (tenant_id, happened_at)"; + CreateTable createTable = (CreateTable) assertSqlCanBeParsedAndDeparsed( + sql, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + TablePartitioning partitioning = createTable.getPartitioning(); + + assertEquals(TablePartitioning.Type.RANGE, partitioning.getType()); + assertEquals(List.of("tenant_id", "happened_at"), partitioning.getExpressionList() + .stream().map(Object::toString).toList()); + + assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE pg_list_parent (region text) PARTITION BY LIST (region)", + true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE pg_hash_parent (tenant_id int) PARTITION BY HASH (tenant_id)", + true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + } + + @Test + void testPostgreSqlDeclarativePartitionChildrenIssue2522() throws JSQLParserException { + String rangeSql = "CREATE TABLE pg_range_p1 PARTITION OF pg_range_parent " + + "FOR VALUES FROM (1, MINVALUE) TO (2, MAXVALUE)"; + CreateTable range = (CreateTable) assertSqlCanBeParsedAndDeparsed( + rangeSql, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals("pg_range_parent", range.getPartitionOf().getName()); + assertEquals(PartitionBound.Type.RANGE, range.getPartitionBound().getType()); + assertEquals(List.of("1", "MINVALUE"), range.getPartitionBound().getFromExpressions() + .stream().map(Object::toString).toList()); + assertEquals(List.of("2", "MAXVALUE"), range.getPartitionBound().getToExpressions() + .stream().map(Object::toString).toList()); + + String listSql = "CREATE TABLE pg_list_eu_us PARTITION OF pg_list_parent " + + "FOR VALUES IN ('eu', 'us')"; + CreateTable list = (CreateTable) assertSqlCanBeParsedAndDeparsed( + listSql, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals(PartitionBound.Type.LIST, list.getPartitionBound().getType()); + + String hashSql = "CREATE TABLE pg_hash_p0 PARTITION OF pg_hash_parent " + + "FOR VALUES WITH (MODULUS 4, REMAINDER 0)"; + CreateTable hash = (CreateTable) assertSqlCanBeParsedAndDeparsed( + hashSql, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals(PartitionBound.Type.HASH, hash.getPartitionBound().getType()); + assertEquals("4", hash.getPartitionBound().getModulus().toString()); + assertEquals("0", hash.getPartitionBound().getRemainder().toString()); + + String defaultSql = + "CREATE TABLE pg_list_default PARTITION OF pg_list_parent DEFAULT"; + CreateTable defaultPartition = (CreateTable) assertSqlCanBeParsedAndDeparsed( + defaultSql, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals(PartitionBound.Type.DEFAULT, defaultPartition.getPartitionBound().getType()); + } } From 2e20844bfaecf51511c92a8c9ac0ce23c008ebc0 Mon Sep 17 00:00:00 2001 From: Minjae Lee Date: Thu, 3 Sep 2026 17:18:19 +0900 Subject: [PATCH 2/2] fix(ci): keep partition tests compatible with Java 11 --- .../sf/jsqlparser/statement/create/CreateTableTest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/test/java/net/sf/jsqlparser/statement/create/CreateTableTest.java b/src/test/java/net/sf/jsqlparser/statement/create/CreateTableTest.java index 407ca582b..845567bbe 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/CreateTableTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/CreateTableTest.java @@ -24,6 +24,7 @@ import java.util.Iterator; import java.util.List; import java.util.StringTokenizer; +import java.util.stream.Collectors; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.operators.relational.GreaterThan; @@ -1261,7 +1262,7 @@ void testPostgreSqlDeclarativePartitionParentsIssue2522() throws JSQLParserExcep assertEquals(TablePartitioning.Type.RANGE, partitioning.getType()); assertEquals(List.of("tenant_id", "happened_at"), partitioning.getExpressionList() - .stream().map(Object::toString).toList()); + .stream().map(Object::toString).collect(Collectors.toList())); assertSqlCanBeParsedAndDeparsed( "CREATE TABLE pg_list_parent (region text) PARTITION BY LIST (region)", @@ -1280,9 +1281,9 @@ void testPostgreSqlDeclarativePartitionChildrenIssue2522() throws JSQLParserExce assertEquals("pg_range_parent", range.getPartitionOf().getName()); assertEquals(PartitionBound.Type.RANGE, range.getPartitionBound().getType()); assertEquals(List.of("1", "MINVALUE"), range.getPartitionBound().getFromExpressions() - .stream().map(Object::toString).toList()); + .stream().map(Object::toString).collect(Collectors.toList())); assertEquals(List.of("2", "MAXVALUE"), range.getPartitionBound().getToExpressions() - .stream().map(Object::toString).toList()); + .stream().map(Object::toString).collect(Collectors.toList())); String listSql = "CREATE TABLE pg_list_eu_us PARTITION OF pg_list_parent " + "FOR VALUES IN ('eu', 'us')";