From 8369e81ea4bfb9202ac5a1ea8c7dcab91375f685 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Mon, 8 Dec 2025 15:02:57 +0900 Subject: [PATCH 01/10] Add PostgreSQL PARTITION OF syntax support (#2042). --- src/ast/ddl.rs | 90 ++++++++++++- src/ast/helpers/stmt_create_table.rs | 24 +++- src/ast/mod.rs | 13 +- src/ast/spans.rs | 2 + src/keywords.rs | 2 + src/parser/mod.rs | 72 ++++++++++ tests/sqlparser_duckdb.rs | 2 + tests/sqlparser_mssql.rs | 4 + tests/sqlparser_postgres.rs | 190 +++++++++++++++++++++++++++ 9 files changed, 390 insertions(+), 9 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 4e042a365..325cbab90 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -2697,6 +2697,14 @@ pub struct CreateTable { /// /// pub inherits: Option>, + /// PostgreSQL `PARTITION OF` clause to create a partition of a parent table. + /// Contains the parent table name. + /// + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub partition_of: Option, + /// PostgreSQL partition bound specification for PARTITION OF. + /// + pub for_values: Option, /// SQLite "STRICT" clause. /// if the "STRICT" table-option keyword is added to the end, after the closing ")", /// then strict typing rules apply to that table. @@ -2792,6 +2800,9 @@ impl fmt::Display for CreateTable { dynamic = if self.dynamic { "DYNAMIC " } else { "" }, name = self.name, )?; + if let Some(partition_of) = &self.partition_of { + write!(f, " PARTITION OF {partition_of}")?; + } if let Some(on_cluster) = &self.on_cluster { write!(f, " ON CLUSTER {on_cluster}")?; } @@ -2806,12 +2817,19 @@ impl fmt::Display for CreateTable { Indent(DisplayCommaSeparated(&self.constraints)).fmt(f)?; NewLine.fmt(f)?; f.write_str(")")?; - } else if self.query.is_none() && self.like.is_none() && self.clone.is_none() { + } else if self.query.is_none() + && self.like.is_none() + && self.clone.is_none() + && self.partition_of.is_none() + { // PostgreSQL allows `CREATE TABLE t ();`, but requires empty parens f.write_str(" ()")?; } else if let Some(CreateTableLikeKind::Parenthesized(like_in_columns_list)) = &self.like { write!(f, " ({like_in_columns_list})")?; } + if let Some(for_values) = &self.for_values { + write!(f, " {for_values}")?; + } // Hive table comment should be after column definitions, please refer to: // [Hive](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-CreateTable) @@ -3053,6 +3071,76 @@ impl fmt::Display for CreateTable { } } +/// PostgreSQL partition bound specification for PARTITION OF. +/// +/// Specifies partition bounds for a child partition table. +/// +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createtable.html) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum ForValues { + /// `FOR VALUES IN (expr, ...)` + In(Vec), + /// `FOR VALUES FROM (expr|MINVALUE|MAXVALUE, ...) TO (expr|MINVALUE|MAXVALUE, ...)` + From { + from: Vec, + to: Vec, + }, + /// `FOR VALUES WITH (MODULUS n, REMAINDER r)` + With { modulus: u64, remainder: u64 }, + /// `DEFAULT` + Default, +} + +impl fmt::Display for ForValues { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ForValues::In(values) => { + write!(f, "FOR VALUES IN ({})", display_comma_separated(values)) + } + ForValues::From { from, to } => { + write!( + f, + "FOR VALUES FROM ({}) TO ({})", + display_comma_separated(from), + display_comma_separated(to) + ) + } + ForValues::With { modulus, remainder } => { + write!( + f, + "FOR VALUES WITH (MODULUS {modulus}, REMAINDER {remainder})" + ) + } + ForValues::Default => write!(f, "DEFAULT"), + } + } +} + +/// A value in a partition bound specification. +/// +/// Used in RANGE partition bounds where values can be expressions, +/// MINVALUE (negative infinity), or MAXVALUE (positive infinity). +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum PartitionBoundValue { + Expr(Expr), + MinValue, + MaxValue, +} + +impl fmt::Display for PartitionBoundValue { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + PartitionBoundValue::Expr(expr) => write!(f, "{expr}"), + PartitionBoundValue::MinValue => write!(f, "MINVALUE"), + PartitionBoundValue::MaxValue => write!(f, "MAXVALUE"), + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index fe950c909..62dbbbcba 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -26,8 +26,8 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::ast::{ ClusteredBy, ColumnDef, CommentDef, CreateTable, CreateTableLikeKind, CreateTableOptions, Expr, - FileFormat, HiveDistributionStyle, HiveFormat, Ident, InitializeKind, ObjectName, OnCommit, - OneOrManyWithParens, Query, RefreshModeKind, RowAccessPolicy, Statement, + FileFormat, ForValues, HiveDistributionStyle, HiveFormat, Ident, InitializeKind, ObjectName, + OnCommit, OneOrManyWithParens, Query, RefreshModeKind, RowAccessPolicy, Statement, StorageSerializationPolicy, TableConstraint, TableVersion, Tag, WrappedCollection, }; @@ -94,6 +94,8 @@ pub struct CreateTableBuilder { pub cluster_by: Option>>, pub clustered_by: Option, pub inherits: Option>, + pub partition_of: Option, + pub for_values: Option, pub strict: bool, pub copy_grants: bool, pub enable_schema_evolution: Option, @@ -150,6 +152,8 @@ impl CreateTableBuilder { cluster_by: None, clustered_by: None, inherits: None, + partition_of: None, + for_values: None, strict: false, copy_grants: false, enable_schema_evolution: None, @@ -317,6 +321,16 @@ impl CreateTableBuilder { self } + pub fn partition_of(mut self, partition_of: Option) -> Self { + self.partition_of = partition_of; + self + } + + pub fn for_values(mut self, for_values: Option) -> Self { + self.for_values = for_values; + self + } + pub fn strict(mut self, strict: bool) -> Self { self.strict = strict; self @@ -463,6 +477,8 @@ impl CreateTableBuilder { cluster_by: self.cluster_by, clustered_by: self.clustered_by, inherits: self.inherits, + partition_of: self.partition_of, + for_values: self.for_values, strict: self.strict, copy_grants: self.copy_grants, enable_schema_evolution: self.enable_schema_evolution, @@ -527,6 +543,8 @@ impl TryFrom for CreateTableBuilder { cluster_by, clustered_by, inherits, + partition_of, + for_values, strict, copy_grants, enable_schema_evolution, @@ -577,6 +595,8 @@ impl TryFrom for CreateTableBuilder { cluster_by, clustered_by, inherits, + partition_of, + for_values, strict, iceberg, copy_grants, diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 467678602..2c170e7f3 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -69,12 +69,13 @@ pub use self::ddl::{ CreateExtension, CreateFunction, CreateIndex, CreateOperator, CreateOperatorClass, CreateOperatorFamily, CreateTable, CreateTrigger, CreateView, Deduplicate, DeferrableInitial, DropBehavior, DropExtension, DropFunction, DropOperator, DropOperatorClass, DropOperatorFamily, - DropOperatorSignature, DropTrigger, GeneratedAs, GeneratedExpressionMode, IdentityParameters, - IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, - IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, Msck, NullsDistinctOption, - OperatorArgTypes, OperatorClassItem, OperatorFamilyDropItem, OperatorFamilyItem, - OperatorOption, OperatorPurpose, Owner, Partition, ProcedureParam, ReferentialAction, - RenameTableNameKind, ReplicaIdentity, TagsColumnOption, TriggerObjectKind, Truncate, + DropOperatorSignature, DropTrigger, ForValues, GeneratedAs, GeneratedExpressionMode, + IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, + IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, Msck, + NullsDistinctOption, OperatorArgTypes, OperatorClassItem, OperatorFamilyDropItem, + OperatorFamilyItem, OperatorOption, OperatorPurpose, Owner, Partition, PartitionBoundValue, + ProcedureParam, ReferentialAction, RenameTableNameKind, ReplicaIdentity, TagsColumnOption, + TriggerObjectKind, Truncate, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeInternalLength, UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, ViewColumnDef, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index d4e843157..d120a3f04 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -554,6 +554,8 @@ impl Spanned for CreateTable { cluster_by: _, // todo, BigQuery specific clustered_by: _, // todo, Hive specific inherits: _, // todo, PostgreSQL specific + partition_of: _, // todo, PostgreSQL specific + for_values: _, // todo, PostgreSQL specific strict: _, // bool copy_grants: _, // bool enable_schema_evolution: _, // bool diff --git a/src/keywords.rs b/src/keywords.rs index f06842ec6..87c77379c 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -637,6 +637,7 @@ define_keywords!( MODIFIES, MODIFY, MODULE, + MODULUS, MONITOR, MONTH, MONTHS, @@ -837,6 +838,7 @@ define_keywords!( RELAY, RELEASE, RELEASES, + REMAINDER, REMOTE, REMOVE, REMOVEQUOTES, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d1c4fe05b..6ebf5c62c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7887,6 +7887,15 @@ impl<'a> Parser<'a> { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let table_name = self.parse_object_name(allow_unquoted_hyphen)?; + // PostgreSQL PARTITION OF for child partition tables + let partition_of = if dialect_of!(self is PostgreSqlDialect | GenericDialect) + && self.parse_keywords(&[Keyword::PARTITION, Keyword::OF]) + { + Some(self.parse_object_name(allow_unquoted_hyphen)?) + } else { + None + }; + // Clickhouse has `ON CLUSTER 'cluster'` syntax for DDLs let on_cluster = self.parse_optional_on_cluster()?; @@ -7911,6 +7920,13 @@ impl<'a> Parser<'a> { None }; + // PostgreSQL PARTITION OF: partition bound specification + let for_values = if partition_of.is_some() { + Some(self.parse_partition_for_values()?) + } else { + None + }; + // SQLite supports `WITHOUT ROWID` at the end of `CREATE TABLE` let without_rowid = self.parse_keywords(&[Keyword::WITHOUT, Keyword::ROWID]); @@ -7988,6 +8004,8 @@ impl<'a> Parser<'a> { .partition_by(create_table_config.partition_by) .cluster_by(create_table_config.cluster_by) .inherits(create_table_config.inherits) + .partition_of(partition_of) + .for_values(for_values) .table_options(create_table_config.table_options) .primary_key(primary_key) .strict(strict) @@ -8047,6 +8065,60 @@ impl<'a> Parser<'a> { } } + /// Parse PostgreSQL partition bound specification for PARTITION OF. + /// + /// Parses: `FOR VALUES partition_bound_spec | DEFAULT` + /// + /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-createtable.html) + fn parse_partition_for_values(&mut self) -> Result { + if self.parse_keyword(Keyword::DEFAULT) { + return Ok(ForValues::Default); + } + + self.expect_keywords(&[Keyword::FOR, Keyword::VALUES])?; + + if self.parse_keyword(Keyword::IN) { + // FOR VALUES IN (expr, ...) + self.expect_token(&Token::LParen)?; + let values = self.parse_comma_separated(Parser::parse_expr)?; + self.expect_token(&Token::RParen)?; + Ok(ForValues::In(values)) + } else if self.parse_keyword(Keyword::FROM) { + // FOR VALUES FROM (...) TO (...) + self.expect_token(&Token::LParen)?; + let from = self.parse_comma_separated(Parser::parse_partition_bound_value)?; + self.expect_token(&Token::RParen)?; + self.expect_keyword(Keyword::TO)?; + self.expect_token(&Token::LParen)?; + let to = self.parse_comma_separated(Parser::parse_partition_bound_value)?; + self.expect_token(&Token::RParen)?; + Ok(ForValues::From { from, to }) + } else if self.parse_keyword(Keyword::WITH) { + // FOR VALUES WITH (MODULUS n, REMAINDER r) + self.expect_token(&Token::LParen)?; + self.expect_keyword(Keyword::MODULUS)?; + let modulus = self.parse_literal_uint()?; + self.expect_token(&Token::Comma)?; + self.expect_keyword(Keyword::REMAINDER)?; + let remainder = self.parse_literal_uint()?; + self.expect_token(&Token::RParen)?; + Ok(ForValues::With { modulus, remainder }) + } else { + self.expected("IN, FROM, or WITH after FOR VALUES", self.peek_token()) + } + } + + /// Parse a single partition bound value (MINVALUE, MAXVALUE, or expression). + fn parse_partition_bound_value(&mut self) -> Result { + if self.parse_keyword(Keyword::MINVALUE) { + Ok(PartitionBoundValue::MinValue) + } else if self.parse_keyword(Keyword::MAXVALUE) { + Ok(PartitionBoundValue::MaxValue) + } else { + Ok(PartitionBoundValue::Expr(self.parse_expr()?)) + } + } + /// Parse configuration like inheritance, partitioning, clustering information during the table creation. /// /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_2) diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 73a1afe26..4a2f29e15 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -755,6 +755,8 @@ fn test_duckdb_union_datatype() { cluster_by: Default::default(), clustered_by: Default::default(), inherits: Default::default(), + partition_of: Default::default(), + for_values: Default::default(), strict: Default::default(), copy_grants: Default::default(), enable_schema_evolution: Default::default(), diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 70e0aab49..0eee96e69 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1897,6 +1897,8 @@ fn parse_create_table_with_valid_options() { cluster_by: None, clustered_by: None, inherits: None, + partition_of: None, + for_values: None, strict: false, iceberg: false, copy_grants: false, @@ -2064,6 +2066,8 @@ fn parse_create_table_with_identity_column() { cluster_by: None, clustered_by: None, inherits: None, + partition_of: None, + for_values: None, strict: false, copy_grants: false, enable_schema_evolution: None, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 9f4564ef2..717112d1b 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6130,6 +6130,8 @@ fn parse_trigger_related_functions() { cluster_by: None, clustered_by: None, inherits: None, + partition_of: None, + for_values: None, strict: false, copy_grants: false, enable_schema_evolution: None, @@ -7914,3 +7916,191 @@ fn parse_create_operator_class() { ) .is_err()); } + +#[test] +fn parse_create_table_partition_of_range() { + // RANGE partition with FROM ... TO + let sql = "CREATE TABLE measurement_y2006m02 PARTITION OF measurement FOR VALUES FROM ('2006-02-01') TO ('2006-03-01')"; + match pg_and_generic().verified_stmt(sql) { + Statement::CreateTable(create_table) => { + assert_eq!("measurement_y2006m02", create_table.name.to_string()); + assert_eq!( + Some(ObjectName::from(vec![Ident::new("measurement")])), + create_table.partition_of + ); + match create_table.for_values { + Some(ForValues::From { from, to }) => { + assert_eq!(1, from.len()); + assert_eq!(1, to.len()); + match &from[0] { + PartitionBoundValue::Expr(Expr::Value(v)) => { + assert_eq!("'2006-02-01'", v.to_string()); + } + _ => panic!("Expected Expr value in from"), + } + match &to[0] { + PartitionBoundValue::Expr(Expr::Value(v)) => { + assert_eq!("'2006-03-01'", v.to_string()); + } + _ => panic!("Expected Expr value in to"), + } + } + _ => panic!("Expected ForValues::From"), + } + } + _ => panic!("Expected CreateTable"), + } +} + +#[test] +fn parse_create_table_partition_of_range_with_minvalue_maxvalue() { + // RANGE partition with MINVALUE/MAXVALUE + let sql = + "CREATE TABLE orders_old PARTITION OF orders FOR VALUES FROM (MINVALUE) TO ('2020-01-01')"; + match pg_and_generic().verified_stmt(sql) { + Statement::CreateTable(create_table) => { + assert_eq!("orders_old", create_table.name.to_string()); + assert_eq!( + Some(ObjectName::from(vec![Ident::new("orders")])), + create_table.partition_of + ); + match create_table.for_values { + Some(ForValues::From { from, to }) => { + assert_eq!(PartitionBoundValue::MinValue, from[0]); + match &to[0] { + PartitionBoundValue::Expr(Expr::Value(v)) => { + assert_eq!("'2020-01-01'", v.to_string()); + } + _ => panic!("Expected Expr value in to"), + } + } + _ => panic!("Expected ForValues::From"), + } + } + _ => panic!("Expected CreateTable"), + } + + // With MAXVALUE + let sql = + "CREATE TABLE orders_new PARTITION OF orders FOR VALUES FROM ('2024-01-01') TO (MAXVALUE)"; + match pg_and_generic().verified_stmt(sql) { + Statement::CreateTable(create_table) => match create_table.for_values { + Some(ForValues::From { from, to }) => { + match &from[0] { + PartitionBoundValue::Expr(Expr::Value(v)) => { + assert_eq!("'2024-01-01'", v.to_string()); + } + _ => panic!("Expected Expr value in from"), + } + assert_eq!(PartitionBoundValue::MaxValue, to[0]); + } + _ => panic!("Expected ForValues::From"), + }, + _ => panic!("Expected CreateTable"), + } +} + +#[test] +fn parse_create_table_partition_of_list() { + // LIST partition + let sql = "CREATE TABLE orders_us PARTITION OF orders FOR VALUES IN ('US', 'CA', 'MX')"; + match pg_and_generic().verified_stmt(sql) { + Statement::CreateTable(create_table) => { + assert_eq!("orders_us", create_table.name.to_string()); + assert_eq!( + Some(ObjectName::from(vec![Ident::new("orders")])), + create_table.partition_of + ); + match create_table.for_values { + Some(ForValues::In(values)) => { + assert_eq!(3, values.len()); + } + _ => panic!("Expected ForValues::In"), + } + } + _ => panic!("Expected CreateTable"), + } +} + +#[test] +fn parse_create_table_partition_of_hash() { + // HASH partition + let sql = "CREATE TABLE orders_p0 PARTITION OF orders FOR VALUES WITH (MODULUS 4, REMAINDER 0)"; + match pg_and_generic().verified_stmt(sql) { + Statement::CreateTable(create_table) => { + assert_eq!("orders_p0", create_table.name.to_string()); + assert_eq!( + Some(ObjectName::from(vec![Ident::new("orders")])), + create_table.partition_of + ); + match create_table.for_values { + Some(ForValues::With { modulus, remainder }) => { + assert_eq!(4, modulus); + assert_eq!(0, remainder); + } + _ => panic!("Expected ForValues::With"), + } + } + _ => panic!("Expected CreateTable"), + } +} + +#[test] +fn parse_create_table_partition_of_default() { + // DEFAULT partition + let sql = "CREATE TABLE orders_default PARTITION OF orders DEFAULT"; + match pg_and_generic().verified_stmt(sql) { + Statement::CreateTable(create_table) => { + assert_eq!("orders_default", create_table.name.to_string()); + assert_eq!( + Some(ObjectName::from(vec![Ident::new("orders")])), + create_table.partition_of + ); + assert_eq!(Some(ForValues::Default), create_table.for_values); + } + _ => panic!("Expected CreateTable"), + } +} + +#[test] +fn parse_create_table_partition_of_multicolumn_range() { + // Multi-column RANGE partition + let sql = "CREATE TABLE sales_2023_q1 PARTITION OF sales FOR VALUES FROM ('2023-01-01', 1) TO ('2023-04-01', 1)"; + match pg_and_generic().verified_stmt(sql) { + Statement::CreateTable(create_table) => { + assert_eq!("sales_2023_q1", create_table.name.to_string()); + match create_table.for_values { + Some(ForValues::From { from, to }) => { + assert_eq!(2, from.len()); + assert_eq!(2, to.len()); + } + _ => panic!("Expected ForValues::From"), + } + } + _ => panic!("Expected CreateTable"), + } +} + +#[test] +fn parse_create_table_partition_of_with_constraint() { + // With table constraint (not column constraint which has different syntax in PARTITION OF) + let sql = "CREATE TABLE orders_2023 PARTITION OF orders (\ +CONSTRAINT check_date CHECK (order_date >= '2023-01-01')\ +) FOR VALUES FROM ('2023-01-01') TO ('2024-01-01')"; + match pg_and_generic().verified_stmt(sql) { + Statement::CreateTable(create_table) => { + assert_eq!("orders_2023", create_table.name.to_string()); + assert_eq!( + Some(ObjectName::from(vec![Ident::new("orders")])), + create_table.partition_of + ); + // Check that table constraint was parsed + assert_eq!(1, create_table.constraints.len()); + match create_table.for_values { + Some(ForValues::From { .. }) => {} + _ => panic!("Expected ForValues::From"), + } + } + _ => panic!("Expected CreateTable"), + } +} From f126c9395c7334362b8d459c235e54fe0b048251 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Tue, 16 Dec 2025 10:31:05 +0900 Subject: [PATCH 02/10] Remove PostgreSQL dialect check for PARTITION OF. --- src/parser/mod.rs | 4 +--- tests/sqlparser_postgres.rs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6ebf5c62c..216f544eb 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7888,9 +7888,7 @@ impl<'a> Parser<'a> { let table_name = self.parse_object_name(allow_unquoted_hyphen)?; // PostgreSQL PARTITION OF for child partition tables - let partition_of = if dialect_of!(self is PostgreSqlDialect | GenericDialect) - && self.parse_keywords(&[Keyword::PARTITION, Keyword::OF]) - { + let partition_of = if self.parse_keywords(&[Keyword::PARTITION, Keyword::OF]) { Some(self.parse_object_name(allow_unquoted_hyphen)?) } else { None diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 717112d1b..5fbd35b27 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -8104,3 +8104,34 @@ CONSTRAINT check_date CHECK (order_date >= '2023-01-01')\ _ => panic!("Expected CreateTable"), } } + +#[test] +fn parse_create_table_partition_of_works_without_dialect_check() { + use sqlparser::dialect::{GenericDialect, MySqlDialect, SQLiteDialect}; + use sqlparser::test_utils::TestedDialects; + + let sql = "CREATE TABLE measurement_y2006m02 PARTITION OF measurement FOR VALUES FROM ('2006-02-01') TO ('2006-03-01')"; + let dialects = TestedDialects::new(vec![ + Box::new(GenericDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(MySqlDialect {}), + Box::new(SQLiteDialect {}), + ]); + match dialects.verified_stmt(sql) { + Statement::CreateTable(create_table) => { + assert_eq!("measurement_y2006m02", create_table.name.to_string()); + assert_eq!( + Some(ObjectName::from(vec![Ident::new("measurement")])), + create_table.partition_of + ); + match create_table.for_values { + Some(ForValues::From { from, to }) => { + assert_eq!(1, from.len()); + assert_eq!(1, to.len()); + } + _ => panic!("Expected ForValues::From"), + } + } + _ => panic!("Expected CreateTable"), + } +} From 4fc212fb918ede2a086731ec66179556ef73d48e Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Tue, 16 Dec 2025 10:34:44 +0900 Subject: [PATCH 03/10] Fix doc comments for PARTITION OF support. --- src/ast/ddl.rs | 2 +- src/parser/mod.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 325cbab90..a2b79e554 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -3071,7 +3071,7 @@ impl fmt::Display for CreateTable { } } -/// PostgreSQL partition bound specification for PARTITION OF. +/// PostgreSQL partition bound specification for `PARTITION OF`. /// /// Specifies partition bounds for a child partition table. /// diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 216f544eb..83b8d3164 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8063,7 +8063,7 @@ impl<'a> Parser<'a> { } } - /// Parse PostgreSQL partition bound specification for PARTITION OF. + /// Parse [ForValues] of a `PARTITION OF` clause. /// /// Parses: `FOR VALUES partition_bound_spec | DEFAULT` /// @@ -8106,7 +8106,7 @@ impl<'a> Parser<'a> { } } - /// Parse a single partition bound value (MINVALUE, MAXVALUE, or expression). + /// Parse a single [PartitionBoundValue]. fn parse_partition_bound_value(&mut self) -> Result { if self.parse_keyword(Keyword::MINVALUE) { Ok(PartitionBoundValue::MinValue) From 7f7e6ba665b131c40ba3cce38502d50f91c7c3e2 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Fri, 19 Dec 2025 12:02:24 +0900 Subject: [PATCH 04/10] Remove redundant PARTITION OF dialect test. --- tests/sqlparser_postgres.rs | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 5fbd35b27..717112d1b 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -8104,34 +8104,3 @@ CONSTRAINT check_date CHECK (order_date >= '2023-01-01')\ _ => panic!("Expected CreateTable"), } } - -#[test] -fn parse_create_table_partition_of_works_without_dialect_check() { - use sqlparser::dialect::{GenericDialect, MySqlDialect, SQLiteDialect}; - use sqlparser::test_utils::TestedDialects; - - let sql = "CREATE TABLE measurement_y2006m02 PARTITION OF measurement FOR VALUES FROM ('2006-02-01') TO ('2006-03-01')"; - let dialects = TestedDialects::new(vec![ - Box::new(GenericDialect {}), - Box::new(PostgreSqlDialect {}), - Box::new(MySqlDialect {}), - Box::new(SQLiteDialect {}), - ]); - match dialects.verified_stmt(sql) { - Statement::CreateTable(create_table) => { - assert_eq!("measurement_y2006m02", create_table.name.to_string()); - assert_eq!( - Some(ObjectName::from(vec![Ident::new("measurement")])), - create_table.partition_of - ); - match create_table.for_values { - Some(ForValues::From { from, to }) => { - assert_eq!(1, from.len()); - assert_eq!(1, to.len()); - } - _ => panic!("Expected ForValues::From"), - } - } - _ => panic!("Expected CreateTable"), - } -} From f6611715e014f5b23c0da4926b44ba4c4f88e0f5 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Fri, 19 Dec 2025 12:42:16 +0900 Subject: [PATCH 05/10] Improve PARTITION OF error handling and add tests. - Add explicit check for FOR/DEFAULT after PARTITION OF with clear error message - Document intentional removal of dialect check for multi-dialect tool support - Add negative test cases for malformed PARTITION OF syntax --- src/parser/mod.rs | 12 +++++++++++- tests/sqlparser_postgres.rs | 39 +++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 83b8d3164..d85f96af0 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7888,6 +7888,9 @@ impl<'a> Parser<'a> { let table_name = self.parse_object_name(allow_unquoted_hyphen)?; // PostgreSQL PARTITION OF for child partition tables + // Note: This is a PostgreSQL-specific feature, but the dialect check was intentionally + // removed to allow GenericDialect and other dialects to parse this syntax. This enables + // multi-dialect SQL tools to work with PostgreSQL-specific DDL statements. let partition_of = if self.parse_keywords(&[Keyword::PARTITION, Keyword::OF]) { Some(self.parse_object_name(allow_unquoted_hyphen)?) } else { @@ -7920,7 +7923,14 @@ impl<'a> Parser<'a> { // PostgreSQL PARTITION OF: partition bound specification let for_values = if partition_of.is_some() { - Some(self.parse_partition_for_values()?) + if self.peek_keyword(Keyword::FOR) || self.peek_keyword(Keyword::DEFAULT) { + Some(self.parse_partition_for_values()?) + } else { + return self.expected( + "FOR VALUES or DEFAULT after PARTITION OF", + self.peek_token(), + ); + } } else { None }; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 717112d1b..3669196df 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -8104,3 +8104,42 @@ CONSTRAINT check_date CHECK (order_date >= '2023-01-01')\ _ => panic!("Expected CreateTable"), } } + +#[test] +fn parse_create_table_partition_of_errors() { + let sql = "CREATE TABLE p PARTITION OF parent"; + let result = pg_and_generic().parse_sql_statements(sql); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!( + err.contains("FOR VALUES or DEFAULT"), + "Expected error about FOR VALUES, got: {err}" + ); + + let sql = "CREATE TABLE p PARTITION OF parent WITH (fillfactor = 70)"; + let result = pg_and_generic().parse_sql_statements(sql); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!( + err.contains("FOR VALUES or DEFAULT"), + "Expected error about FOR VALUES, got: {err}" + ); + + let sql = "CREATE TABLE p PARTITION OF parent FOR VALUES RANGE (1, 10)"; + let result = pg_and_generic().parse_sql_statements(sql); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!( + err.contains("IN, FROM, or WITH"), + "Expected error about invalid keyword after FOR VALUES, got: {err}" + ); + + let sql = "CREATE TABLE p PARTITION OF parent FOR VALUES FROM (1)"; + let result = pg_and_generic().parse_sql_statements(sql); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!( + err.contains("TO"), + "Expected error about missing TO clause, got: {err}" + ); +} From c9b5fde29a7ea0b4d6cd069d2c3d1b1afacea85f Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Tue, 23 Dec 2025 09:45:32 +0900 Subject: [PATCH 06/10] Add validation for empty value lists in PARTITION OF syntax. --- src/parser/mod.rs | 9 +++++++++ tests/sqlparser_postgres.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d85f96af0..9a88cc479 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8088,16 +8088,25 @@ impl<'a> Parser<'a> { if self.parse_keyword(Keyword::IN) { // FOR VALUES IN (expr, ...) self.expect_token(&Token::LParen)?; + if self.peek_token() == Token::RParen { + return self.expected("at least one value", self.peek_token()); + } let values = self.parse_comma_separated(Parser::parse_expr)?; self.expect_token(&Token::RParen)?; Ok(ForValues::In(values)) } else if self.parse_keyword(Keyword::FROM) { // FOR VALUES FROM (...) TO (...) self.expect_token(&Token::LParen)?; + if self.peek_token() == Token::RParen { + return self.expected("at least one value", self.peek_token()); + } let from = self.parse_comma_separated(Parser::parse_partition_bound_value)?; self.expect_token(&Token::RParen)?; self.expect_keyword(Keyword::TO)?; self.expect_token(&Token::LParen)?; + if self.peek_token() == Token::RParen { + return self.expected("at least one value", self.peek_token()); + } let to = self.parse_comma_separated(Parser::parse_partition_bound_value)?; self.expect_token(&Token::RParen)?; Ok(ForValues::From { from, to }) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 3669196df..6974d07ae 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -8142,4 +8142,31 @@ fn parse_create_table_partition_of_errors() { err.contains("TO"), "Expected error about missing TO clause, got: {err}" ); + + let sql = "CREATE TABLE p PARTITION OF parent FOR VALUES IN ()"; + let result = pg_and_generic().parse_sql_statements(sql); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!( + err.contains("at least one value"), + "Expected error about empty value list in IN clause, got: {err}" + ); + + let sql = "CREATE TABLE p PARTITION OF parent FOR VALUES FROM () TO (10)"; + let result = pg_and_generic().parse_sql_statements(sql); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!( + err.contains("at least one value"), + "Expected error about empty FROM list, got: {err}" + ); + + let sql = "CREATE TABLE p PARTITION OF parent FOR VALUES FROM (1) TO ()"; + let result = pg_and_generic().parse_sql_statements(sql); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!( + err.contains("at least one value"), + "Expected error about empty TO list, got: {err}" + ); } From cf55b4c86ebadcddc90f77b673519d839eb5a85a Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Tue, 23 Dec 2025 09:50:12 +0900 Subject: [PATCH 07/10] Document PARTITION OF mutual exclusivity with AS SELECT and LIKE. --- src/parser/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9a88cc479..eb47d9e4a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7891,6 +7891,12 @@ impl<'a> Parser<'a> { // Note: This is a PostgreSQL-specific feature, but the dialect check was intentionally // removed to allow GenericDialect and other dialects to parse this syntax. This enables // multi-dialect SQL tools to work with PostgreSQL-specific DDL statements. + // + // PARTITION OF can be combined with other table definition clauses in the AST, + // though PostgreSQL itself prohibits PARTITION OF with AS SELECT or LIKE clauses. + // The parser accepts these combinations for flexibility; semantic validation + // is left to downstream tools. + // Child partitions can have their own constraints and indexes. let partition_of = if self.parse_keywords(&[Keyword::PARTITION, Keyword::OF]) { Some(self.parse_object_name(allow_unquoted_hyphen)?) } else { From b00722f442d52a5f1879f5c6fe9aec1328612fa6 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Tue, 23 Dec 2025 10:24:06 +0900 Subject: [PATCH 08/10] Add span support for PARTITION OF fields. --- src/ast/spans.rs | 50 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index d120a3f04..323abd442 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -34,19 +34,20 @@ use super::{ ColumnOption, ColumnOptionDef, ConditionalStatementBlock, ConditionalStatements, ConflictTarget, ConnectBy, ConstraintCharacteristics, CopySource, CreateIndex, CreateTable, CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, ExcludeSelectItem, Expr, - ExprWithAlias, Fetch, FromTable, Function, FunctionArg, FunctionArgExpr, + ExprWithAlias, Fetch, ForValues, FromTable, Function, FunctionArg, FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, FunctionArguments, GroupByExpr, HavingBound, IfStatement, IlikeSelectItem, IndexColumn, Insert, Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, LimitClause, MatchRecognizePattern, Measure, Merge, MergeAction, MergeClause, MergeInsertExpr, MergeInsertKind, MergeUpdateExpr, NamedParenthesizedList, NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, OnInsert, OpenStatement, OrderBy, - OrderByExpr, OrderByKind, OutputClause, Partition, PivotValueSource, ProjectionSelect, Query, - RaiseStatement, RaiseStatementValue, ReferentialAction, RenameSelectItem, ReplaceSelectElement, - ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript, - SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, TableObject, - TableOptionsClustered, TableWithJoins, Update, UpdateTableFromKind, Use, Value, Values, - ViewColumnDef, WhileStatement, WildcardAdditionalOptions, With, WithFill, + OrderByExpr, OrderByKind, OutputClause, Partition, PartitionBoundValue, PivotValueSource, + ProjectionSelect, Query, RaiseStatement, RaiseStatementValue, ReferentialAction, + RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, + SetExpr, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, + TableConstraint, TableFactor, TableObject, TableOptionsClustered, TableWithJoins, Update, + UpdateTableFromKind, Use, Value, Values, ViewColumnDef, WhileStatement, + WildcardAdditionalOptions, With, WithFill, }; /// Given an iterator of spans, return the [Span::union] of all spans. @@ -554,8 +555,8 @@ impl Spanned for CreateTable { cluster_by: _, // todo, BigQuery specific clustered_by: _, // todo, Hive specific inherits: _, // todo, PostgreSQL specific - partition_of: _, // todo, PostgreSQL specific - for_values: _, // todo, PostgreSQL specific + partition_of, + for_values, strict: _, // bool copy_grants: _, // bool enable_schema_evolution: _, // bool @@ -586,7 +587,9 @@ impl Spanned for CreateTable { .chain(columns.iter().map(|i| i.span())) .chain(constraints.iter().map(|i| i.span())) .chain(query.iter().map(|i| i.span())) - .chain(clone.iter().map(|i| i.span())), + .chain(clone.iter().map(|i| i.span())) + .chain(partition_of.iter().map(|i| i.span())) + .chain(for_values.iter().map(|i| i.span())), ) } } @@ -624,6 +627,33 @@ impl Spanned for TableConstraint { } } +impl Spanned for PartitionBoundValue { + fn span(&self) -> Span { + match self { + PartitionBoundValue::Expr(expr) => expr.span(), + // MINVALUE and MAXVALUE are keywords without tracked spans + PartitionBoundValue::MinValue => Span::empty(), + PartitionBoundValue::MaxValue => Span::empty(), + } + } +} + +impl Spanned for ForValues { + fn span(&self) -> Span { + match self { + ForValues::In(exprs) => union_spans(exprs.iter().map(|e| e.span())), + ForValues::From { from, to } => union_spans( + from.iter() + .map(|v| v.span()) + .chain(to.iter().map(|v| v.span())), + ), + // WITH (MODULUS n, REMAINDER r) - u64 values have no spans + ForValues::With { .. } => Span::empty(), + ForValues::Default => Span::empty(), + } + } +} + impl Spanned for CreateIndex { fn span(&self) -> Span { let CreateIndex { From 3afcdb44e8e507a7ac7fc540e87cbd80291a2d8c Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Thu, 25 Dec 2025 14:32:08 +0900 Subject: [PATCH 09/10] Add SET configuration_parameter parsing for PostgreSQL functions. --- src/parser/mod.rs | 19 +++++++- tests/sqlparser_postgres.rs | 88 +++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index eb47d9e4a..b1770b7ff 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5260,6 +5260,7 @@ impl<'a> Parser<'a> { function_body: Option, called_on_null: Option, parallel: Option, + options: Vec, } let mut body = Body::default(); loop { @@ -5326,6 +5327,18 @@ impl<'a> Parser<'a> { } else { return self.expected("one of UNSAFE | RESTRICTED | SAFE", self.peek_token()); } + } else if self.parse_keyword(Keyword::SET) { + let name = self.parse_identifier()?; + self.expect_token(&Token::Eq)?; + let first_value = self.parse_expr()?; + let value = if self.consume_token(&Token::Comma) { + let mut values = vec![first_value]; + values.extend(self.parse_comma_separated(Parser::parse_expr)?); + Expr::Tuple(values) + } else { + first_value + }; + body.options.push(SqlOption::KeyValue { key: name, value }); } else if self.parse_keyword(Keyword::RETURN) { ensure_not_set(&body.function_body, "RETURN")?; body.function_body = Some(CreateFunctionBody::Return(self.parse_expr()?)); @@ -5349,7 +5362,11 @@ impl<'a> Parser<'a> { if_not_exists: false, using: None, determinism_specifier: None, - options: None, + options: if body.options.is_empty() { + None + } else { + Some(body.options) + }, remote_connection: None, })) } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 6974d07ae..65183e5d2 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4586,6 +4586,94 @@ fn parse_create_function_c_with_module_pathname() { ); } +#[test] +fn parse_create_function_with_set_config() { + let sql = r#"CREATE FUNCTION auth.hook(event jsonb) +RETURNS jsonb +LANGUAGE plpgsql +SET search_path = auth, pg_temp, public +AS $$ BEGIN RETURN event; END; $$"#; + + let statements = pg().parse_sql_statements(sql).unwrap(); + assert_eq!(statements.len(), 1); + match &statements[0] { + Statement::CreateFunction(CreateFunction { name, options, .. }) => { + assert_eq!(name.to_string(), "auth.hook"); + let opts = options.as_ref().expect("should have options"); + assert_eq!(opts.len(), 1, "Should have one SET option"); + + // Verify the SET option was captured + match &opts[0] { + SqlOption::KeyValue { key, value } => { + assert_eq!(key.to_string(), "search_path"); + // Value should be a comma-separated list of identifiers + match value { + Expr::Tuple(tuple) => { + assert_eq!(tuple.len(), 3); + // Verify tuple contains expected identifiers + for (i, expected) in ["auth", "pg_temp", "public"].iter().enumerate() { + match &tuple[i] { + Expr::Identifier(ident) => { + assert_eq!(ident.to_string(), *expected); + } + _ => panic!("Expected identifier in tuple position {}", i), + } + } + } + _ => panic!("Expected Tuple expression for comma-separated values, got: {:?}", value), + } + } + _ => panic!("Expected KeyValue option"), + } + } + _ => panic!("Expected CreateFunction"), + } + + // Test with quoted string value + let sql2 = r#"CREATE FUNCTION test_func() RETURNS void LANGUAGE plpgsql SET work_mem = '64MB' AS $$ BEGIN END; $$"#; + let statements2 = pg().parse_sql_statements(sql2).unwrap(); + match &statements2[0] { + Statement::CreateFunction(CreateFunction { options, .. }) => { + let opts = options.as_ref().expect("should have options"); + match &opts[0] { + SqlOption::KeyValue { key, value } => { + assert_eq!(key.to_string(), "work_mem"); + match value { + Expr::Value(ValueWithSpan { value: Value::SingleQuotedString(s), .. }) => { + assert_eq!(s, "64MB"); + } + _ => panic!("Expected SingleQuotedString, got: {:?}", value), + } + } + _ => panic!("Expected KeyValue option"), + } + } + _ => panic!("Expected CreateFunction"), + } + + // Test with boolean value + let sql3 = r#"CREATE FUNCTION test_func2() RETURNS void LANGUAGE plpgsql SET enable_seqscan = false AS $$ BEGIN END; $$"#; + let statements3 = pg().parse_sql_statements(sql3).unwrap(); + match &statements3[0] { + Statement::CreateFunction(CreateFunction { options, .. }) => { + let opts = options.as_ref().expect("should have options"); + match &opts[0] { + SqlOption::KeyValue { key, value } => { + assert_eq!(key.to_string(), "enable_seqscan"); + match value { + Expr::Value(ValueWithSpan { value: Value::Boolean(b), .. }) => { + assert_eq!(*b, false); + } + _ => panic!("Expected Boolean, got: {:?}", value), + } + } + _ => panic!("Expected KeyValue option"), + } + } + _ => panic!("Expected CreateFunction"), + } +} + #[test] fn parse_drop_function() { let sql = "DROP FUNCTION IF EXISTS test_func"; From d85fe748dfafcd7b25f803d141d03f971cf488f9 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Thu, 25 Dec 2025 14:44:58 +0900 Subject: [PATCH 10/10] Add SECURITY DEFINER/INVOKER parsing for PostgreSQL functions. --- src/ast/ddl.rs | 9 +++++- src/ast/mod.rs | 20 ++++++++++++ src/parser/mod.rs | 14 +++++++++ tests/sqlparser_bigquery.rs | 1 + tests/sqlparser_mssql.rs | 2 ++ tests/sqlparser_postgres.rs | 61 +++++++++++++++++++++++++++++++++++++ 6 files changed, 106 insertions(+), 1 deletion(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index a2b79e554..6e95b6d61 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -44,7 +44,7 @@ use crate::ast::{ ArgMode, AttachedToken, CommentDef, ConditionalStatements, CreateFunctionBody, CreateFunctionUsing, CreateTableLikeKind, CreateTableOptions, CreateViewParams, DataType, Expr, FileFormat, FunctionBehavior, FunctionCalledOnNull, FunctionDesc, FunctionDeterminismSpecifier, - FunctionParallel, HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, + FunctionParallel, FunctionSecurity, HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, HiveSetLocation, Ident, InitializeKind, MySQLColumnPosition, ObjectName, OnCommit, OneOrManyWithParens, OperateFunctionArg, OrderByExpr, ProjectionSelect, Query, RefreshModeKind, RowAccessPolicy, SequenceOptions, Spanned, SqlOption, StorageSerializationPolicy, TableVersion, @@ -3226,6 +3226,10 @@ pub struct CreateFunction { /// /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-createfunction.html) pub parallel: Option, + /// SECURITY DEFINER | SECURITY INVOKER + /// + /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-createfunction.html) + pub security: Option, /// USING ... (Hive only) pub using: Option, /// Language used in a UDF definition. @@ -3292,6 +3296,9 @@ impl fmt::Display for CreateFunction { if let Some(parallel) = &self.parallel { write!(f, " {parallel}")?; } + if let Some(security) = &self.security { + write!(f, " {security}")?; + } if let Some(remote_connection) = &self.remote_connection { write!(f, " REMOTE WITH CONNECTION {remote_connection}")?; } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 2c170e7f3..600b10252 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -8822,6 +8822,26 @@ impl fmt::Display for FunctionParallel { } } +/// SECURITY DEFINER | SECURITY INVOKER +/// +/// [PostgreSQL](https://www.postgresql.org/docs/current/sql-createfunction.html) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum FunctionSecurity { + Definer, + Invoker, +} + +impl fmt::Display for FunctionSecurity { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + FunctionSecurity::Definer => write!(f, "SECURITY DEFINER"), + FunctionSecurity::Invoker => write!(f, "SECURITY INVOKER"), + } + } +} + /// [BigQuery] Determinism specifier used in a UDF definition. /// /// [BigQuery]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_11 diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b1770b7ff..0291bc6c5 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5260,6 +5260,7 @@ impl<'a> Parser<'a> { function_body: Option, called_on_null: Option, parallel: Option, + security: Option, options: Vec, } let mut body = Body::default(); @@ -5339,6 +5340,15 @@ impl<'a> Parser<'a> { first_value }; body.options.push(SqlOption::KeyValue { key: name, value }); + } else if self.parse_keyword(Keyword::SECURITY) { + ensure_not_set(&body.security, "SECURITY DEFINER | SECURITY INVOKER")?; + if self.parse_keyword(Keyword::DEFINER) { + body.security = Some(FunctionSecurity::Definer); + } else if self.parse_keyword(Keyword::INVOKER) { + body.security = Some(FunctionSecurity::Invoker); + } else { + return self.expected("DEFINER or INVOKER", self.peek_token()); + } } else if self.parse_keyword(Keyword::RETURN) { ensure_not_set(&body.function_body, "RETURN")?; body.function_body = Some(CreateFunctionBody::Return(self.parse_expr()?)); @@ -5357,6 +5367,7 @@ impl<'a> Parser<'a> { behavior: body.behavior, called_on_null: body.called_on_null, parallel: body.parallel, + security: body.security, language: body.language, function_body: body.function_body, if_not_exists: false, @@ -5398,6 +5409,7 @@ impl<'a> Parser<'a> { behavior: None, called_on_null: None, parallel: None, + security: None, language: None, determinism_specifier: None, options: None, @@ -5480,6 +5492,7 @@ impl<'a> Parser<'a> { behavior: None, called_on_null: None, parallel: None, + security: None, })) } @@ -5569,6 +5582,7 @@ impl<'a> Parser<'a> { behavior: None, called_on_null: None, parallel: None, + security: None, })) } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 24b9efcaa..8b4f84d49 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -2294,6 +2294,7 @@ fn test_bigquery_create_function() { remote_connection: None, called_on_null: None, parallel: None, + security: None, }) ); diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 0eee96e69..fad960abd 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -266,6 +266,7 @@ fn parse_create_function() { behavior: None, called_on_null: None, parallel: None, + security: None, using: None, language: None, determinism_specifier: None, @@ -439,6 +440,7 @@ fn parse_create_function_parameter_default_values() { behavior: None, called_on_null: None, parallel: None, + security: None, using: None, language: None, determinism_specifier: None, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 65183e5d2..da8d05264 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4294,6 +4294,7 @@ $$"#; behavior: None, called_on_null: None, parallel: None, + security: None, function_body: Some(CreateFunctionBody::AsBeforeOptions { body: Expr::Value( (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF str1 <> str2 THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() @@ -4335,6 +4336,7 @@ $$"#; behavior: None, called_on_null: None, parallel: None, + security: None, function_body: Some(CreateFunctionBody::AsBeforeOptions { body: Expr::Value( (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF int1 <> 0 THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() @@ -4380,6 +4382,7 @@ $$"#; behavior: None, called_on_null: None, parallel: None, + security: None, function_body: Some(CreateFunctionBody::AsBeforeOptions { body: Expr::Value( (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF a <> b THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() @@ -4425,6 +4428,7 @@ $$"#; behavior: None, called_on_null: None, parallel: None, + security: None, function_body: Some(CreateFunctionBody::AsBeforeOptions { body: Expr::Value( (Value::DollarQuotedString(DollarQuotedString {value: "\nBEGIN\n IF int1 <> int2 THEN\n RETURN TRUE;\n ELSE\n RETURN FALSE;\n END IF;\nEND;\n".to_owned(), tag: None})).with_empty_span() @@ -4463,6 +4467,7 @@ $$"#; behavior: None, called_on_null: None, parallel: None, + security: None, function_body: Some(CreateFunctionBody::AsBeforeOptions { body: Expr::Value( (Value::DollarQuotedString(DollarQuotedString { @@ -4504,6 +4509,7 @@ fn parse_create_function() { behavior: Some(FunctionBehavior::Immutable), called_on_null: Some(FunctionCalledOnNull::Strict), parallel: Some(FunctionParallel::Safe), + security: None, function_body: Some(CreateFunctionBody::AsBeforeOptions { body: Expr::Value( (Value::SingleQuotedString("select $1 + $2;".into())).with_empty_span() @@ -4562,6 +4568,7 @@ fn parse_create_function_c_with_module_pathname() { behavior: Some(FunctionBehavior::Immutable), called_on_null: None, parallel: Some(FunctionParallel::Safe), + security: None, function_body: Some(CreateFunctionBody::AsBeforeOptions { body: Expr::Value( (Value::SingleQuotedString("MODULE_PATHNAME".into())).with_empty_span() @@ -4674,6 +4681,59 @@ AS $$ BEGIN RETURN event; END; $$"#; } } +#[test] +fn parse_create_function_with_security_definer() { + let sql = r#"CREATE FUNCTION public.my_func() RETURNS void LANGUAGE sql SECURITY DEFINER AS $$ SELECT 1 $$"#; + + let stmt = pg().verified_stmt(sql); + match stmt { + Statement::CreateFunction(CreateFunction { name, security, .. }) => { + assert_eq!(name.to_string(), "public.my_func"); + assert_eq!(security, Some(FunctionSecurity::Definer)); + } + _ => panic!("Expected CreateFunction"), + } +} + +#[test] +fn parse_create_function_with_security_invoker() { + let sql = r#"CREATE FUNCTION public.my_func() RETURNS void LANGUAGE sql SECURITY INVOKER AS $$ SELECT 1 $$"#; + + let stmt = pg().verified_stmt(sql); + match stmt { + Statement::CreateFunction(CreateFunction { name, security, .. }) => { + assert_eq!(name.to_string(), "public.my_func"); + assert_eq!(security, Some(FunctionSecurity::Invoker)); + } + _ => panic!("Expected CreateFunction"), + } +} + +#[test] +fn parse_create_function_with_security_and_other_attributes() { + pg().one_statement_parses_to( + r#"CREATE FUNCTION test_func() RETURNS integer LANGUAGE plpgsql IMMUTABLE SECURITY DEFINER AS $$ BEGIN RETURN 42; END; $$"#, + r#"CREATE FUNCTION test_func() RETURNS INTEGER LANGUAGE plpgsql IMMUTABLE SECURITY DEFINER AS $$ BEGIN RETURN 42; END; $$"#, + ); + + let stmt = pg().verified_stmt(r#"CREATE FUNCTION test_func() RETURNS INTEGER LANGUAGE plpgsql IMMUTABLE SECURITY DEFINER AS $$ BEGIN RETURN 42; END; $$"#); + match stmt { + Statement::CreateFunction(CreateFunction { + name, + security, + behavior, + language, + .. + }) => { + assert_eq!(name.to_string(), "test_func"); + assert_eq!(security, Some(FunctionSecurity::Definer)); + assert_eq!(behavior, Some(FunctionBehavior::Immutable)); + assert_eq!(language.as_ref().map(|i| i.value.as_str()), Some("plpgsql")); + } + _ => panic!("Expected CreateFunction"), + } +} + #[test] fn parse_drop_function() { let sql = "DROP FUNCTION IF EXISTS test_func"; @@ -6275,6 +6335,7 @@ fn parse_trigger_related_functions() { behavior: None, called_on_null: None, parallel: None, + security: None, using: None, language: Some(Ident::new("plpgsql")), determinism_specifier: None,