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..fbb5ef6357 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -11115,3 +11115,55 @@ 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", + "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", + ), + ] { + assert_eq!( + pg().parse_sql_statements(sql).unwrap_err().to_string(), + format!("sql parser error: {expected}"), + "{sql}" + ); + } +}