From 69c627bd767d6841155faf6a09acbded4661df67 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Wed, 28 Oct 2026 08:44:32 +0100 Subject: [PATCH 1/2] PostgreSQL: Support ALTER TABLE ATTACH PARTITION --- .../postgres_alter_table_attach_partition.sql | 2 + src/ast/ddl.rs | 15 +++++++ src/ast/spans.rs | 4 ++ src/dialect/mod.rs | 8 ++++ src/dialect/postgresql.rs | 5 +++ src/parser/mod.rs | 7 +++ tests/sqlparser_postgres.rs | 44 +++++++++++++++++++ 7 files changed, 85 insertions(+) create mode 100644 fuzz/fuzz_seeds/postgres_alter_table_attach_partition.sql diff --git a/fuzz/fuzz_seeds/postgres_alter_table_attach_partition.sql b/fuzz/fuzz_seeds/postgres_alter_table_attach_partition.sql new file mode 100644 index 0000000000..e78e33d257 --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_alter_table_attach_partition.sql @@ -0,0 +1,2 @@ +ALTER TABLE ONLY public.root ATTACH PARTITION public.part FOR VALUES FROM (0) TO (10); +ALTER TABLE root ATTACH PARTITION part DEFAULT \ No newline at end of file diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 61a4d4002a..10e278483a 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -238,6 +238,15 @@ pub enum AlterTableOperation { /// Partition expression to attach. partition: Partition, }, + /// `ATTACH PARTITION partition_name { FOR VALUES partition_bound_spec | DEFAULT }` + /// + /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-altertable.html) + AttachPartitionOf { + /// The table to attach as a partition. + partition: ObjectName, + /// The partition bound. + for_values: ForValues, + }, /// `DETACH PART|PARTITION ` /// Note: this is a ClickHouse-specific operation, please refer to /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/partition#detach-partitionpart) @@ -930,6 +939,12 @@ impl fmt::Display for AlterTableOperation { AlterTableOperation::AttachPartition { partition } => { write!(f, "ATTACH {partition}") } + AlterTableOperation::AttachPartitionOf { + partition, + for_values, + } => { + write!(f, "ATTACH PARTITION {partition} {for_values}") + } AlterTableOperation::DetachPartition { partition } => { write!(f, "DETACH {partition}") } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 4ed3df2bbc..94725033f7 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1208,6 +1208,10 @@ impl Spanned for AlterTableOperation { drop_behavior: _, } => union_spans(column_names.iter().map(|i| i.span)), AlterTableOperation::AttachPartition { partition } => partition.span(), + AlterTableOperation::AttachPartitionOf { + partition, + for_values, + } => partition.span().union(&for_values.span()), AlterTableOperation::DetachPartition { partition } => partition.span(), AlterTableOperation::FreezePartition { partition, diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 5cfff36965..5f208f4dd4 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1541,6 +1541,14 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports attaching a table as a partition with + /// `ALTER TABLE ... ATTACH PARTITION name { FOR VALUES ... | DEFAULT }`. + /// + /// + fn supports_attach_partition_of(&self) -> bool { + false + } + /// Returns true if the specified keyword is reserved and cannot be /// used as an identifier without special handling like quoting. fn is_reserved_for_identifier(&self, kw: Keyword) -> bool { diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index e8202cfdac..e5c09725b1 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -569,4 +569,9 @@ impl Dialect for PostgreSqlDialect { fn supports_add_column_if_not_exists(&self) -> bool { true } + + /// See + fn supports_attach_partition_of(&self) -> bool { + true + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 53f54a048b..ee8f93807e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11528,6 +11528,13 @@ impl<'a> Parser<'a> { { let new_owner = self.parse_owner()?; AlterTableOperation::OwnerTo { new_owner } + } else if self.dialect.supports_attach_partition_of() + && self.parse_keywords(&[Keyword::ATTACH, Keyword::PARTITION]) + { + AlterTableOperation::AttachPartitionOf { + partition: self.parse_object_name(false)?, + for_values: self.parse_partition_for_values()?, + } } else if dialect_of!(self is ClickHouseDialect|GenericDialect) && self.parse_keyword(Keyword::ATTACH) { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index a21435b00f..3773f477c4 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -11115,3 +11115,47 @@ fn display_column_unique_nulls_distinct_propagates_formatter_errors() { let stmt = pg_and_generic().verified_stmt("CREATE TABLE t (u INT UNIQUE NULLS NOT DISTINCT)"); assert_eq!(write!(RejectingWriter, "{stmt}"), Err(fmt::Error)); } + +#[test] +fn parse_alter_table_attach_partition_of() { + let sql = + "ALTER TABLE ONLY public.root ATTACH PARTITION public.part FOR VALUES FROM (0) TO (10)"; + let Statement::AlterTable(alter) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!( + alter.operations, + vec![AlterTableOperation::AttachPartitionOf { + partition: ObjectName::from(vec![Ident::new("public"), Ident::new("part")]), + for_values: ForValues::From { + from: vec![PartitionBoundValue::Expr(Expr::value(number("0")))], + to: vec![PartitionBoundValue::Expr(Expr::value(number("10")))], + }, + }] + ); + + pg().verified_stmt("ALTER TABLE root ATTACH PARTITION part DEFAULT"); + pg().verified_stmt("ALTER TABLE root ATTACH PARTITION part FOR VALUES IN (1, 2)"); + // As written by pg_dump. + pg().one_statement_parses_to( + "ALTER TABLE ONLY public.hroot ATTACH PARTITION public.part4 FOR VALUES WITH (modulus 4, remainder 0)", + "ALTER TABLE ONLY public.hroot ATTACH PARTITION public.part4 FOR VALUES WITH (MODULUS 4, REMAINDER 0)", + ); + + for (sql, expected) in [ + ( + "ALTER TABLE root ATTACH PARTITION part", + "Expected: FOR, found: EOF", + ), + ( + "ALTER TABLE root ATTACH PARTITION part FOR VALUES", + "Expected: IN, FROM, or WITH after FOR VALUES, found: EOF", + ), + ] { + assert_eq!( + pg().parse_sql_statements(sql).unwrap_err().to_string(), + format!("sql parser error: {expected}"), + "{sql}" + ); + } +} From 11f7fa0fed108bd9490f90259c6796e2372bceb2 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Wed, 28 Oct 2026 09:54:25 +0100 Subject: [PATCH 2/2] Test error paths and formatter errors --- tests/sqlparser_postgres.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 3773f477c4..fbb5ef6357 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -11147,6 +11147,14 @@ fn parse_alter_table_attach_partition_of() { "ALTER TABLE root ATTACH PARTITION part", "Expected: FOR, found: EOF", ), + ( + "ALTER TABLE root ATTACH PARTITION", + "Expected: identifier, found: EOF", + ), + ( + "ALTER TABLE root ATTACH PARTITION part FOR VALUES IN ()", + "Expected: at least one value, found: )", + ), ( "ALTER TABLE root ATTACH PARTITION part FOR VALUES", "Expected: IN, FROM, or WITH after FOR VALUES, found: EOF",