From 3d6ffcc8689f9fdf83bde56d4285fa007b2cbb02 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 2 Oct 2026 18:15:09 +0200 Subject: [PATCH 1/7] Support PostgreSQL `ALTER DEFAULT PRIVILEGES` --- src/ast/dcl.rs | 146 ++++++++++++++++++++++++++++++++++++ src/ast/mod.rs | 8 +- src/ast/spans.rs | 2 + src/keywords.rs | 2 + src/parser/mod.rs | 109 +++++++++++++++++++++++++-- tests/sqlparser_postgres.rs | 29 +++++++ 6 files changed, 287 insertions(+), 9 deletions(-) diff --git a/src/ast/dcl.rs b/src/ast/dcl.rs index 96b340714c..5e6dcfc3f4 100644 --- a/src/ast/dcl.rs +++ b/src/ast/dcl.rs @@ -535,3 +535,149 @@ impl From for crate::ast::Statement { crate::ast::Statement::Revoke(v) } } + +/// `ALTER DEFAULT PRIVILEGES`, which applies to objects created later rather +/// than to existing ones. +/// +/// [PostgreSQL](https://www.postgresql.org/docs/current/sql-alterdefaultprivileges.html) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct AlterDefaultPrivileges { + /// `FOR ROLE [, ...]`, empty when the clause is absent. + pub for_roles: Vec, + /// `IN SCHEMA [, ...]`, empty when the clause is absent. + pub in_schemas: Vec, + /// The abbreviated `GRANT` or `REVOKE` that follows. + pub operation: AlterDefaultPrivilegesOperation, +} + +impl fmt::Display for AlterDefaultPrivileges { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("ALTER DEFAULT PRIVILEGES")?; + if !self.for_roles.is_empty() { + write!(f, " FOR ROLE {}", display_comma_separated(&self.for_roles))?; + } + if !self.in_schemas.is_empty() { + write!( + f, + " IN SCHEMA {}", + display_comma_separated(&self.in_schemas) + )?; + } + write!(f, " {}", self.operation) + } +} + +impl From for crate::ast::Statement { + fn from(v: AlterDefaultPrivileges) -> Self { + crate::ast::Statement::AlterDefaultPrivileges(v) + } +} + +/// The abbreviated `GRANT` or `REVOKE` of an [`AlterDefaultPrivileges`], naming +/// an object type rather than named objects and so unable to reuse [`Grant`]. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterDefaultPrivilegesOperation { + /// `GRANT ON TO [WITH GRANT OPTION]` + Grant { + /// The privileges being granted. + privileges: Privileges, + /// The kind of future object the privileges apply to. + object_type: DefaultPrivilegesObjectType, + /// The roles receiving the privileges. + grantees: Vec, + /// Whether `WITH GRANT OPTION` is present. + with_grant_option: bool, + }, + /// `REVOKE [GRANT OPTION FOR] ON FROM [CASCADE | RESTRICT]` + Revoke { + /// Whether `GRANT OPTION FOR` is present. + grant_option_for: bool, + /// The privileges being revoked. + privileges: Privileges, + /// The kind of future object the privileges apply to. + object_type: DefaultPrivilegesObjectType, + /// The roles losing the privileges. + grantees: Vec, + /// Optional `CASCADE`/`RESTRICT` behaviour. + cascade: Option, + }, +} + +impl fmt::Display for AlterDefaultPrivilegesOperation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AlterDefaultPrivilegesOperation::Grant { + privileges, + object_type, + grantees, + with_grant_option, + } => { + write!( + f, + "GRANT {privileges} ON {object_type} TO {}", + display_comma_separated(grantees) + )?; + if *with_grant_option { + f.write_str(" WITH GRANT OPTION")?; + } + } + AlterDefaultPrivilegesOperation::Revoke { + grant_option_for, + privileges, + object_type, + grantees, + cascade, + } => { + f.write_str("REVOKE ")?; + if *grant_option_for { + f.write_str("GRANT OPTION FOR ")?; + } + write!( + f, + "{privileges} ON {object_type} FROM {}", + display_comma_separated(grantees) + )?; + if let Some(cascade) = cascade { + write!(f, " {cascade}")?; + } + } + } + Ok(()) + } +} + +/// The kind of future object an [`AlterDefaultPrivileges`] applies to. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum DefaultPrivilegesObjectType { + /// `TABLES`, which also covers views and foreign tables. + Tables, + /// `SEQUENCES` + Sequences, + /// `FUNCTIONS` + Functions, + /// `ROUTINES`, a synonym of `FUNCTIONS`. + Routines, + /// `TYPES` + Types, + /// `SCHEMAS` + Schemas, +} + +impl fmt::Display for DefaultPrivilegesObjectType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + DefaultPrivilegesObjectType::Tables => "TABLES", + DefaultPrivilegesObjectType::Sequences => "SEQUENCES", + DefaultPrivilegesObjectType::Functions => "FUNCTIONS", + DefaultPrivilegesObjectType::Routines => "ROUTINES", + DefaultPrivilegesObjectType::Types => "TYPES", + DefaultPrivilegesObjectType::Schemas => "SCHEMAS", + }) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8e798524c9..9f2dbcf9c4 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -58,7 +58,8 @@ pub use self::data_type::{ ExactNumberInfo, IntervalFields, MapBracketKind, StructBracketKind, TimezoneInfo, TypeModifier, }; pub use self::dcl::{ - AlterRoleOperation, CreateRole, Grant, ResetConfig, Revoke, RoleOption, SecondaryRoles, + AlterDefaultPrivileges, AlterDefaultPrivilegesOperation, AlterRoleOperation, CreateRole, + DefaultPrivilegesObjectType, Grant, ResetConfig, Revoke, RoleOption, SecondaryRoles, SetConfigValue, Use, }; pub use self::ddl::{ @@ -4644,6 +4645,10 @@ pub enum Statement { /// ``` Revoke(Revoke), /// ```sql + /// ALTER DEFAULT PRIVILEGES + /// ``` + AlterDefaultPrivileges(AlterDefaultPrivileges), + /// ```sql /// DEALLOCATE [ PREPARE ] { name | ALL } /// ``` /// @@ -6147,6 +6152,7 @@ impl fmt::Display for Statement { Statement::Grant(grant) => write!(f, "{grant}"), Statement::Deny(s) => write!(f, "{s}"), Statement::Revoke(revoke) => write!(f, "{revoke}"), + Statement::AlterDefaultPrivileges(stmt) => write!(f, "{stmt}"), Statement::Deallocate { name, prepare } => write!( f, "DEALLOCATE {prepare}{name}", diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 7638d7d6c0..fd27ee5786 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -301,6 +301,7 @@ impl Spanned for Values { /// - [Statement::Assert] /// - [Statement::Grant] /// - [Statement::Revoke] +/// - [Statement::AlterDefaultPrivileges] /// - [Statement::Deallocate] /// - [Statement::Execute] /// - [Statement::Prepare] @@ -467,6 +468,7 @@ impl Spanned for Statement { Statement::Grant { .. } => Span::empty(), Statement::Deny { .. } => Span::empty(), Statement::Revoke { .. } => Span::empty(), + Statement::AlterDefaultPrivileges { .. } => Span::empty(), Statement::Deallocate { .. } => Span::empty(), Statement::Execute { .. } => Span::empty(), Statement::Prepare { .. } => Span::empty(), diff --git a/src/keywords.rs b/src/keywords.rs index 8a6c980cab..ce985834b3 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -932,6 +932,7 @@ define_keywords!( ROLLBACK, ROLLUP, ROOT, + ROUTINES, ROW, ROWGROUPSIZE, ROWID, @@ -1125,6 +1126,7 @@ define_keywords!( TSVECTOR, TUPLE, TYPE, + TYPES, TYPMOD_IN, TYPMOD_OUT, UBIGINT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3efb22a349..d7fe0ce7f7 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11641,6 +11641,10 @@ impl<'a> Parser<'a> { return self.parse_alter_text_search().map(Into::into); } + if self.parse_keywords(&[Keyword::DEFAULT, Keyword::PRIVILEGES]) { + return self.parse_alter_default_privileges().map(Into::into); + } + let object_type = self.expect_one_of_keywords(&[ Keyword::VIEW, Keyword::TYPE, @@ -18465,14 +18469,7 @@ impl<'a> Parser<'a> { pub fn parse_grant_deny_revoke_privileges_objects( &mut self, ) -> Result<(Privileges, Option), ParserError> { - let privileges = if self.parse_keyword(Keyword::ALL) { - Privileges::All { - with_privileges_keyword: self.parse_keyword(Keyword::PRIVILEGES), - } - } else { - let actions = self.parse_actions_list()?; - Privileges::Actions(actions) - }; + let privileges = self.parse_privileges()?; let objects = if self.parse_keyword(Keyword::ON) { if self.parse_keywords(&[Keyword::ALL, Keyword::TABLES, Keyword::IN, Keyword::SCHEMA]) { @@ -19004,6 +19001,102 @@ impl<'a> Parser<'a> { }) } + /// Parse `ALTER DEFAULT PRIVILEGES`, whose keywords are already consumed. + /// + /// See [PostgreSQL docs](https://www.postgresql.org/docs/current/sql-alterdefaultprivileges.html). + pub fn parse_alter_default_privileges( + &mut self, + ) -> Result { + // `FOR USER` is a synonym of `FOR ROLE` and is normalised to it. + let for_roles = if self.parse_keyword(Keyword::FOR) { + self.expect_one_of_keywords(&[Keyword::ROLE, Keyword::USER])?; + self.parse_comma_separated(|p| p.parse_identifier())? + } else { + vec![] + }; + + let in_schemas = if self.parse_keywords(&[Keyword::IN, Keyword::SCHEMA]) { + self.parse_comma_separated(|p| p.parse_object_name(false))? + } else { + vec![] + }; + + let is_grant = if self.parse_keyword(Keyword::GRANT) { + true + } else if self.parse_keyword(Keyword::REVOKE) { + false + } else { + return self.expected_ref("GRANT or REVOKE", self.peek_token_ref()); + }; + + let grant_option_for = + !is_grant && self.parse_keywords(&[Keyword::GRANT, Keyword::OPTION, Keyword::FOR]); + let privileges = self.parse_privileges()?; + self.expect_keyword_is(Keyword::ON)?; + let object_type = self.parse_default_privileges_object_type()?; + self.expect_keyword_is(if is_grant { Keyword::TO } else { Keyword::FROM })?; + let grantees = self.parse_grantees()?; + + let operation = if is_grant { + AlterDefaultPrivilegesOperation::Grant { + privileges, + object_type, + grantees, + with_grant_option: self.parse_keywords(&[ + Keyword::WITH, + Keyword::GRANT, + Keyword::OPTION, + ]), + } + } else { + AlterDefaultPrivilegesOperation::Revoke { + grant_option_for, + privileges, + object_type, + grantees, + cascade: self.parse_cascade_option(), + } + }; + + Ok(AlterDefaultPrivileges { + for_roles, + in_schemas, + operation, + }) + } + + /// Parse `ALL [PRIVILEGES]` or a comma separated privilege list. + fn parse_privileges(&mut self) -> Result { + if self.parse_keyword(Keyword::ALL) { + Ok(Privileges::All { + with_privileges_keyword: self.parse_keyword(Keyword::PRIVILEGES), + }) + } else { + Ok(Privileges::Actions(self.parse_actions_list()?)) + } + } + + fn parse_default_privileges_object_type( + &mut self, + ) -> Result { + for (keyword, object_type) in [ + (Keyword::TABLES, DefaultPrivilegesObjectType::Tables), + (Keyword::SEQUENCES, DefaultPrivilegesObjectType::Sequences), + (Keyword::FUNCTIONS, DefaultPrivilegesObjectType::Functions), + (Keyword::ROUTINES, DefaultPrivilegesObjectType::Routines), + (Keyword::TYPES, DefaultPrivilegesObjectType::Types), + (Keyword::SCHEMAS, DefaultPrivilegesObjectType::Schemas), + ] { + if self.parse_keyword(keyword) { + return Ok(object_type); + } + } + self.expected_ref( + "TABLES, SEQUENCES, FUNCTIONS, ROUTINES, TYPES or SCHEMAS", + self.peek_token_ref(), + ) + } + /// Parse an REPLACE statement pub fn parse_replace( &mut self, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index aa9252125b..c1c7ed25b5 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10845,6 +10845,35 @@ fn parse_geometric_typed_literal_error_propagates() { pg().parse_sql_statements("SELECT circle( FROM t") .unwrap_err(), ParserError::ParserError("Expected: a value, found: (".to_string()) +} + +#[test] +fn parse_alter_default_privileges() { + // What `pg_dump -s` emits for a default ACL. + pg().verified_stmt("ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT SELECT ON TABLES TO app_user"); + pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES TO app"); + pg().verified_stmt("ALTER DEFAULT PRIVILEGES FOR ROLE a, b IN SCHEMA s1, s2 GRANT SELECT, INSERT ON TABLES TO app WITH GRANT OPTION"); + pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT ALL PRIVILEGES ON SEQUENCES TO app"); + pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT EXECUTE ON FUNCTIONS TO app"); + pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT EXECUTE ON ROUTINES TO app"); + pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT USAGE ON TYPES TO app"); + pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT USAGE, CREATE ON SCHEMAS TO app"); + pg().verified_stmt("ALTER DEFAULT PRIVILEGES REVOKE SELECT ON TABLES FROM app"); + pg().verified_stmt( + "ALTER DEFAULT PRIVILEGES REVOKE GRANT OPTION FOR ALL ON TABLES FROM app CASCADE", + ); + // `FOR USER` normalises to `FOR ROLE`. + pg().one_statement_parses_to( + "ALTER DEFAULT PRIVILEGES FOR USER bob GRANT SELECT ON TABLES TO app", + "ALTER DEFAULT PRIVILEGES FOR ROLE bob GRANT SELECT ON TABLES TO app", + ); + + // `TABLES` is a keyword only here, so a table named `tables` still parses. + pg().verified_stmt("GRANT SELECT ON TABLES TO app"); + assert_eq!( + pg().parse_sql_statements("ALTER DEFAULT PRIVILEGES FOR ROLE a") + .unwrap_err(), + ParserError::ParserError("Expected: GRANT or REVOKE, found: EOF".to_string()), ); } From 43ebf18091c470a87ffa564a883d985610d1c54d Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Mon, 19 Oct 2026 20:19:57 +0200 Subject: [PATCH 2/7] Accept the default privileges options in any order and ON LARGE OBJECTS --- src/ast/dcl.rs | 44 +++++++++++++++++++++---------- src/ast/mod.rs | 4 +-- src/parser/mod.rs | 52 ++++++++++++++++++++++++------------- tests/sqlparser_postgres.rs | 46 ++++++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 33 deletions(-) diff --git a/src/ast/dcl.rs b/src/ast/dcl.rs index 5e6dcfc3f4..096c6eca29 100644 --- a/src/ast/dcl.rs +++ b/src/ast/dcl.rs @@ -536,6 +536,30 @@ impl From for crate::ast::Statement { } } +/// The options of [`AlterDefaultPrivileges`], kept in the order written. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum DefaultPrivilegesOption { + /// `FOR ROLE [, ...]`. + ForRole(Vec), + /// `IN SCHEMA [, ...]`. + InSchema(Vec), +} + +impl fmt::Display for DefaultPrivilegesOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + DefaultPrivilegesOption::ForRole(roles) => { + write!(f, "FOR ROLE {}", display_comma_separated(roles)) + } + DefaultPrivilegesOption::InSchema(schemas) => { + write!(f, "IN SCHEMA {}", display_comma_separated(schemas)) + } + } + } +} + /// `ALTER DEFAULT PRIVILEGES`, which applies to objects created later rather /// than to existing ones. /// @@ -544,10 +568,8 @@ impl From for crate::ast::Statement { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct AlterDefaultPrivileges { - /// `FOR ROLE [, ...]`, empty when the clause is absent. - pub for_roles: Vec, - /// `IN SCHEMA [, ...]`, empty when the clause is absent. - pub in_schemas: Vec, + /// The `FOR ROLE` and `IN SCHEMA` options, in written order. + pub options: Vec, /// The abbreviated `GRANT` or `REVOKE` that follows. pub operation: AlterDefaultPrivilegesOperation, } @@ -555,15 +577,8 @@ pub struct AlterDefaultPrivileges { impl fmt::Display for AlterDefaultPrivileges { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("ALTER DEFAULT PRIVILEGES")?; - if !self.for_roles.is_empty() { - write!(f, " FOR ROLE {}", display_comma_separated(&self.for_roles))?; - } - if !self.in_schemas.is_empty() { - write!( - f, - " IN SCHEMA {}", - display_comma_separated(&self.in_schemas) - )?; + for option in &self.options { + write!(f, " {option}")?; } write!(f, " {}", self.operation) } @@ -667,6 +682,8 @@ pub enum DefaultPrivilegesObjectType { Types, /// `SCHEMAS` Schemas, + /// `LARGE OBJECTS` + LargeObjects, } impl fmt::Display for DefaultPrivilegesObjectType { @@ -678,6 +695,7 @@ impl fmt::Display for DefaultPrivilegesObjectType { DefaultPrivilegesObjectType::Routines => "ROUTINES", DefaultPrivilegesObjectType::Types => "TYPES", DefaultPrivilegesObjectType::Schemas => "SCHEMAS", + DefaultPrivilegesObjectType::LargeObjects => "LARGE OBJECTS", }) } } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 9f2dbcf9c4..d66b839c44 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -59,8 +59,8 @@ pub use self::data_type::{ }; pub use self::dcl::{ AlterDefaultPrivileges, AlterDefaultPrivilegesOperation, AlterRoleOperation, CreateRole, - DefaultPrivilegesObjectType, Grant, ResetConfig, Revoke, RoleOption, SecondaryRoles, - SetConfigValue, Use, + DefaultPrivilegesObjectType, DefaultPrivilegesOption, Grant, ResetConfig, Revoke, RoleOption, + SecondaryRoles, SetConfigValue, Use, }; pub use self::ddl::{ AggregateModifyKind, Alignment, AlterCollation, AlterCollationOperation, AlterColumnOperation, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d7fe0ce7f7..64756c91ca 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -19008,18 +19008,35 @@ impl<'a> Parser<'a> { &mut self, ) -> Result { // `FOR USER` is a synonym of `FOR ROLE` and is normalised to it. - let for_roles = if self.parse_keyword(Keyword::FOR) { - self.expect_one_of_keywords(&[Keyword::ROLE, Keyword::USER])?; - self.parse_comma_separated(|p| p.parse_identifier())? - } else { - vec![] - }; - - let in_schemas = if self.parse_keywords(&[Keyword::IN, Keyword::SCHEMA]) { - self.parse_comma_separated(|p| p.parse_object_name(false))? - } else { - vec![] - }; + let mut options = Vec::new(); + let mut for_roles = false; + let mut in_schemas = false; + loop { + if self.parse_keyword(Keyword::FOR) { + if for_roles { + return Err(ParserError::ParserError( + "FOR ROLE specified more than once".to_string(), + )); + } + self.expect_one_of_keywords(&[Keyword::ROLE, Keyword::USER])?; + for_roles = true; + options.push(DefaultPrivilegesOption::ForRole( + self.parse_comma_separated(|p| p.parse_identifier())?, + )); + } else if self.parse_keywords(&[Keyword::IN, Keyword::SCHEMA]) { + if in_schemas { + return Err(ParserError::ParserError( + "IN SCHEMA specified more than once".to_string(), + )); + } + in_schemas = true; + options.push(DefaultPrivilegesOption::InSchema( + self.parse_comma_separated(|p| p.parse_object_name(false))?, + )); + } else { + break; + } + } let is_grant = if self.parse_keyword(Keyword::GRANT) { true @@ -19058,11 +19075,7 @@ impl<'a> Parser<'a> { } }; - Ok(AlterDefaultPrivileges { - for_roles, - in_schemas, - operation, - }) + Ok(AlterDefaultPrivileges { options, operation }) } /// Parse `ALL [PRIVILEGES]` or a comma separated privilege list. @@ -19079,6 +19092,9 @@ impl<'a> Parser<'a> { fn parse_default_privileges_object_type( &mut self, ) -> Result { + if self.parse_keywords(&[Keyword::LARGE, Keyword::OBJECTS]) { + return Ok(DefaultPrivilegesObjectType::LargeObjects); + } for (keyword, object_type) in [ (Keyword::TABLES, DefaultPrivilegesObjectType::Tables), (Keyword::SEQUENCES, DefaultPrivilegesObjectType::Sequences), @@ -19092,7 +19108,7 @@ impl<'a> Parser<'a> { } } self.expected_ref( - "TABLES, SEQUENCES, FUNCTIONS, ROUTINES, TYPES or SCHEMAS", + "TABLES, SEQUENCES, FUNCTIONS, ROUTINES, TYPES, SCHEMAS or LARGE OBJECTS", self.peek_token_ref(), ) } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index c1c7ed25b5..7bd27160f4 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10868,6 +10868,52 @@ fn parse_alter_default_privileges() { "ALTER DEFAULT PRIVILEGES FOR ROLE bob GRANT SELECT ON TABLES TO app", ); + // Options keep the order written. + for (sql, options) in [ + ( + "ALTER DEFAULT PRIVILEGES IN SCHEMA s FOR ROLE r GRANT SELECT ON TABLES TO app", + vec![ + DefaultPrivilegesOption::InSchema(vec![ObjectName::from(Ident::new("s"))]), + DefaultPrivilegesOption::ForRole(vec![Ident::new("r")]), + ], + ), + ( + "ALTER DEFAULT PRIVILEGES FOR ROLE r IN SCHEMA s GRANT SELECT ON TABLES TO app", + vec![ + DefaultPrivilegesOption::ForRole(vec![Ident::new("r")]), + DefaultPrivilegesOption::InSchema(vec![ObjectName::from(Ident::new("s"))]), + ], + ), + ] { + let mut stmts = pg().parse_sql_statements(sql).unwrap(); + match stmts.pop().unwrap() { + Statement::AlterDefaultPrivileges(stmt) => assert_eq!(stmt.options, options, "{sql}"), + _ => unreachable!(), + } + } + // Reversed option order round-trips unchanged. + pg().verified_stmt("ALTER DEFAULT PRIVILEGES IN SCHEMA public FOR ROLE postgres GRANT SELECT ON TABLES TO app_user"); + // `LARGE OBJECTS` is a valid object type for both `GRANT` and `REVOKE`. + pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT SELECT, UPDATE ON LARGE OBJECTS TO app"); + pg().verified_stmt( + "ALTER DEFAULT PRIVILEGES REVOKE GRANT OPTION FOR SELECT ON LARGE OBJECTS FROM app CASCADE", + ); + // Repeated options are rejected. + assert_eq!( + pg().parse_sql_statements( + "ALTER DEFAULT PRIVILEGES FOR ROLE a FOR ROLE b GRANT SELECT ON TABLES TO app" + ) + .unwrap_err(), + ParserError::ParserError("FOR ROLE specified more than once".to_string()), + ); + assert_eq!( + pg().parse_sql_statements( + "ALTER DEFAULT PRIVILEGES IN SCHEMA s IN SCHEMA t GRANT SELECT ON TABLES TO app" + ) + .unwrap_err(), + ParserError::ParserError("IN SCHEMA specified more than once".to_string()), + ); + // `TABLES` is a keyword only here, so a table named `tables` still parses. pg().verified_stmt("GRANT SELECT ON TABLES TO app"); assert_eq!( From 5c642178d0cdf6eb5b5c8e26f0abba18a55cee5e Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Tue, 20 Oct 2026 13:04:59 +0200 Subject: [PATCH 3/7] Track default-privilege source spans and cover parser boundaries --- src/ast/spans.rs | 40 +++++++++++-- tests/sqlparser_postgres.rs | 109 ++++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 4 deletions(-) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index fd27ee5786..fea6495851 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -28,14 +28,19 @@ use core::iter; use crate::tokenizer::Span; use super::{ - comments, dcl::SecondaryRoles, value::ValueWithSpan, AccessExpr, AlterColumnOperation, + comments, + dcl::{ + AlterDefaultPrivileges, AlterDefaultPrivilegesOperation, DefaultPrivilegesOption, + SecondaryRoles, + }, + value::ValueWithSpan, AccessExpr, AlterColumnOperation, AlterIndexOperation, AlterTableOperation, Analyze, Array, Assignment, AssignmentTarget, AttachedToken, BeginEndStatements, CaseStatement, CloseCursor, ClusteredIndex, ColumnDef, ColumnOption, ColumnOptionDef, ConditionalStatementBlock, ConditionalStatements, ConflictTarget, ConnectByKind, ConstraintCharacteristics, CopySource, CreateIndex, CreateTable, CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, ExcludeConstraintElement, ExcludeSelectItem, Expr, ExprWithAlias, Fetch, ForValues, FromTable, Function, FunctionArg, - FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, FunctionArguments, GroupByExpr, + FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, FunctionArguments, GranteeName, GroupByExpr, HavingBound, IfStatement, IlikeSelectItem, InTarget, IndexColumn, Insert, Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, LimitClause, MatchRecognizePattern, Measure, Merge, MergeAction, MergeClause, MergeInsertExpr, @@ -301,7 +306,6 @@ impl Spanned for Values { /// - [Statement::Assert] /// - [Statement::Grant] /// - [Statement::Revoke] -/// - [Statement::AlterDefaultPrivileges] /// - [Statement::Deallocate] /// - [Statement::Execute] /// - [Statement::Prepare] @@ -468,7 +472,7 @@ impl Spanned for Statement { Statement::Grant { .. } => Span::empty(), Statement::Deny { .. } => Span::empty(), Statement::Revoke { .. } => Span::empty(), - Statement::AlterDefaultPrivileges { .. } => Span::empty(), + Statement::AlterDefaultPrivileges(privileges) => privileges.span(), Statement::Deallocate { .. } => Span::empty(), Statement::Execute { .. } => Span::empty(), Statement::Prepare { .. } => Span::empty(), @@ -531,6 +535,34 @@ impl Spanned for Statement { } } +impl Spanned for AlterDefaultPrivileges { + fn span(&self) -> Span { + let options = self.options.iter().map(|option| match option { + DefaultPrivilegesOption::ForRole(roles) => { + union_spans(roles.iter().map(|role| role.span)) + } + DefaultPrivilegesOption::InSchema(schemas) => { + union_spans(schemas.iter().map(Spanned::span)) + } + }); + let grantees = match &self.operation { + AlterDefaultPrivilegesOperation::Grant { grantees, .. } + | AlterDefaultPrivilegesOperation::Revoke { grantees, .. } => grantees, + }; + union_spans( + options.chain( + grantees + .iter() + .filter_map(|grantee| grantee.name.as_ref()) + .map(|name| match name { + GranteeName::ObjectName(name) => name.span(), + GranteeName::UserHost { user, host } => user.span.union(&host.span), + }), + ), + ) + } +} + impl Spanned for Use { fn span(&self) -> Span { match self { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7bd27160f4..4d91af8393 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10950,3 +10950,112 @@ fn parse_create_aggregate_rejects_output_arguments_and_defaults() { ); } } + +fn parse_alter_default_privileges_rejects_malformed_statements() { + for (sql, expected) in [ + ( + "ALTER DEFAULT PRIVILEGES FOR ALL GRANT SELECT ON TABLES TO app", + "Expected: one of ROLE or USER, found: ALL", + ), + ( + "ALTER DEFAULT PRIVILEGES FOR ROLE", + "Expected: identifier, found: EOF", + ), + ( + "ALTER DEFAULT PRIVILEGES IN SCHEMA", + "Expected: identifier, found: EOF", + ), + ( + "ALTER DEFAULT PRIVILEGES GRANT ON TABLES TO app", + "Expected: a privilege keyword, found: ON", + ), + ( + "ALTER DEFAULT PRIVILEGES GRANT SELECT TO app", + "Expected: ON, found: TO", + ), + ( + "ALTER DEFAULT PRIVILEGES GRANT SELECT ON INDEXES TO app", + "Expected: TABLES, SEQUENCES, FUNCTIONS, ROUTINES, TYPES, SCHEMAS or LARGE OBJECTS, found: INDEXES", + ), + ( + "ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES", + "Expected: TO, found: EOF", + ), + ( + "ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES TO", + "Expected: identifier, found: EOF", + ), + ] { + assert_eq!( + pg().parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError(expected.to_string()), + ); + } +} + +#[test] +fn display_alter_default_privileges_propagates_write_errors() { + use core::fmt::{self, Write}; + + struct RejectingWriter(&'static str); + + impl Write for RejectingWriter { + fn write_str(&mut self, value: &str) -> fmt::Result { + if value.contains(self.0) { + Err(fmt::Error) + } else { + Ok(()) + } + } + } + + let grant = pg().verified_stmt( + "ALTER DEFAULT PRIVILEGES FOR ROLE r IN SCHEMA s GRANT SELECT ON TABLES TO g WITH GRANT OPTION", + ); + for rejected in ["D", "r", "g", "W"] { + let mut writer = RejectingWriter(rejected); + assert_eq!(write!(writer, "{grant}"), Err(fmt::Error)); + } + + let revoke = pg().verified_stmt( + "ALTER DEFAULT PRIVILEGES REVOKE GRANT OPTION FOR SELECT ON TABLES FROM g CASCADE", + ); + for rejected in ["K", "N", "g", "CASCADE"] { + let mut writer = RejectingWriter(rejected); + assert_eq!(write!(writer, "{revoke}"), Err(fmt::Error)); + } +} + +#[test] +fn parse_alter_default_privileges_spans() { + for (sql, start, end) in [ + ( + "ALTER DEFAULT PRIVILEGES FOR ROLE r IN SCHEMA s GRANT SELECT ON TABLES TO g", + 35, + 76, + ), + ( + "ALTER DEFAULT PRIVILEGES REVOKE SELECT ON TABLES FROM g", + 55, + 56, + ), + ( + "ALTER DEFAULT PRIVILEGES FOR ROLE r GRANT SELECT ON TABLES TO PUBLIC", + 35, + 36, + ), + ] { + let statements = Parser::parse_sql(&PostgreSqlDialect {}, sql).unwrap(); + assert_eq!( + statements[0].span(), + Span::new(Location::new(1, start), Location::new(1, end)), + "span for {sql}" + ); + } + let sql = "ALTER DEFAULT PRIVILEGES REVOKE SELECT ON TABLES FROM 'bob'@'localhost'"; + let statements = Parser::parse_sql(&sqlparser::dialect::GenericDialect {}, sql).unwrap(); + assert_eq!( + statements[0].span(), + Span::new(Location::new(1, 55), Location::new(1, 72)) + ); +} From 6f3024595be01dc782b5de52b4c15dfd8ec5f089 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Wed, 21 Oct 2026 09:53:35 +0200 Subject: [PATCH 4/7] Complete typed-literal regression assertion after rebase --- tests/sqlparser_postgres.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 4d91af8393..8019c39e33 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10845,6 +10845,7 @@ fn parse_geometric_typed_literal_error_propagates() { pg().parse_sql_statements("SELECT circle( FROM t") .unwrap_err(), ParserError::ParserError("Expected: a value, found: (".to_string()) + ); } #[test] From f38e5ee7898b69d28e877c176dded2f81d50f4ec Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 24 Oct 2026 00:27:16 +0200 Subject: [PATCH 5/7] Place regression tests beside related syntax cases --- tests/sqlparser_postgres.rs | 371 ++++++++++++++++++------------------ 1 file changed, 186 insertions(+), 185 deletions(-) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 8019c39e33..2c15fa7d17 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5102,6 +5102,192 @@ fn parse_alter_user() { ); } +#[test] +fn parse_alter_default_privileges() { + // What `pg_dump -s` emits for a default ACL. + pg().verified_stmt("ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT SELECT ON TABLES TO app_user"); + pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES TO app"); + pg().verified_stmt("ALTER DEFAULT PRIVILEGES FOR ROLE a, b IN SCHEMA s1, s2 GRANT SELECT, INSERT ON TABLES TO app WITH GRANT OPTION"); + pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT ALL PRIVILEGES ON SEQUENCES TO app"); + pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT EXECUTE ON FUNCTIONS TO app"); + pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT EXECUTE ON ROUTINES TO app"); + pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT USAGE ON TYPES TO app"); + pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT USAGE, CREATE ON SCHEMAS TO app"); + pg().verified_stmt("ALTER DEFAULT PRIVILEGES REVOKE SELECT ON TABLES FROM app"); + pg().verified_stmt( + "ALTER DEFAULT PRIVILEGES REVOKE GRANT OPTION FOR ALL ON TABLES FROM app CASCADE", + ); + // `FOR USER` normalises to `FOR ROLE`. + pg().one_statement_parses_to( + "ALTER DEFAULT PRIVILEGES FOR USER bob GRANT SELECT ON TABLES TO app", + "ALTER DEFAULT PRIVILEGES FOR ROLE bob GRANT SELECT ON TABLES TO app", + ); + + // Options keep the order written. + for (sql, options) in [ + ( + "ALTER DEFAULT PRIVILEGES IN SCHEMA s FOR ROLE r GRANT SELECT ON TABLES TO app", + vec![ + DefaultPrivilegesOption::InSchema(vec![ObjectName::from(Ident::new("s"))]), + DefaultPrivilegesOption::ForRole(vec![Ident::new("r")]), + ], + ), + ( + "ALTER DEFAULT PRIVILEGES FOR ROLE r IN SCHEMA s GRANT SELECT ON TABLES TO app", + vec![ + DefaultPrivilegesOption::ForRole(vec![Ident::new("r")]), + DefaultPrivilegesOption::InSchema(vec![ObjectName::from(Ident::new("s"))]), + ], + ), + ] { + let mut stmts = pg().parse_sql_statements(sql).unwrap(); + match stmts.pop().unwrap() { + Statement::AlterDefaultPrivileges(stmt) => assert_eq!(stmt.options, options, "{sql}"), + _ => unreachable!(), + } + } + // Reversed option order round-trips unchanged. + pg().verified_stmt("ALTER DEFAULT PRIVILEGES IN SCHEMA public FOR ROLE postgres GRANT SELECT ON TABLES TO app_user"); + // `LARGE OBJECTS` is a valid object type for both `GRANT` and `REVOKE`. + pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT SELECT, UPDATE ON LARGE OBJECTS TO app"); + pg().verified_stmt( + "ALTER DEFAULT PRIVILEGES REVOKE GRANT OPTION FOR SELECT ON LARGE OBJECTS FROM app CASCADE", + ); + // Repeated options are rejected. + assert_eq!( + pg().parse_sql_statements( + "ALTER DEFAULT PRIVILEGES FOR ROLE a FOR ROLE b GRANT SELECT ON TABLES TO app" + ) + .unwrap_err(), + ParserError::ParserError("FOR ROLE specified more than once".to_string()), + ); + assert_eq!( + pg().parse_sql_statements( + "ALTER DEFAULT PRIVILEGES IN SCHEMA s IN SCHEMA t GRANT SELECT ON TABLES TO app" + ) + .unwrap_err(), + ParserError::ParserError("IN SCHEMA specified more than once".to_string()), + ); + + // `TABLES` is a keyword only here, so a table named `tables` still parses. + pg().verified_stmt("GRANT SELECT ON TABLES TO app"); + assert_eq!( + pg().parse_sql_statements("ALTER DEFAULT PRIVILEGES FOR ROLE a") + .unwrap_err(), + ParserError::ParserError("Expected: GRANT or REVOKE, found: EOF".to_string()), + ); +} + +#[test] +fn parse_alter_default_privileges_rejects_malformed_statements() { + for (sql, expected) in [ + ( + "ALTER DEFAULT PRIVILEGES FOR ALL GRANT SELECT ON TABLES TO app", + "Expected: one of ROLE or USER, found: ALL", + ), + ( + "ALTER DEFAULT PRIVILEGES FOR ROLE", + "Expected: identifier, found: EOF", + ), + ( + "ALTER DEFAULT PRIVILEGES IN SCHEMA", + "Expected: identifier, found: EOF", + ), + ( + "ALTER DEFAULT PRIVILEGES GRANT ON TABLES TO app", + "Expected: a privilege keyword, found: ON", + ), + ( + "ALTER DEFAULT PRIVILEGES GRANT SELECT TO app", + "Expected: ON, found: TO", + ), + ( + "ALTER DEFAULT PRIVILEGES GRANT SELECT ON INDEXES TO app", + "Expected: TABLES, SEQUENCES, FUNCTIONS, ROUTINES, TYPES, SCHEMAS or LARGE OBJECTS, found: INDEXES", + ), + ( + "ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES", + "Expected: TO, found: EOF", + ), + ( + "ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES TO", + "Expected: identifier, found: EOF", + ), + ] { + assert_eq!( + pg().parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError(expected.to_string()), + ); + } +} + +#[test] +fn display_alter_default_privileges_propagates_write_errors() { + use core::fmt::{self, Write}; + + struct RejectingWriter(&'static str); + + impl Write for RejectingWriter { + fn write_str(&mut self, value: &str) -> fmt::Result { + if value.contains(self.0) { + Err(fmt::Error) + } else { + Ok(()) + } + } + } + + let grant = pg().verified_stmt( + "ALTER DEFAULT PRIVILEGES FOR ROLE r IN SCHEMA s GRANT SELECT ON TABLES TO g WITH GRANT OPTION", + ); + for rejected in ["D", "r", "g", "W"] { + let mut writer = RejectingWriter(rejected); + assert_eq!(write!(writer, "{grant}"), Err(fmt::Error)); + } + + let revoke = pg().verified_stmt( + "ALTER DEFAULT PRIVILEGES REVOKE GRANT OPTION FOR SELECT ON TABLES FROM g CASCADE", + ); + for rejected in ["K", "N", "g", "CASCADE"] { + let mut writer = RejectingWriter(rejected); + assert_eq!(write!(writer, "{revoke}"), Err(fmt::Error)); + } +} + +#[test] +fn parse_alter_default_privileges_spans() { + for (sql, start, end) in [ + ( + "ALTER DEFAULT PRIVILEGES FOR ROLE r IN SCHEMA s GRANT SELECT ON TABLES TO g", + 35, + 76, + ), + ( + "ALTER DEFAULT PRIVILEGES REVOKE SELECT ON TABLES FROM g", + 55, + 56, + ), + ( + "ALTER DEFAULT PRIVILEGES FOR ROLE r GRANT SELECT ON TABLES TO PUBLIC", + 35, + 36, + ), + ] { + let statements = Parser::parse_sql(&PostgreSqlDialect {}, sql).unwrap(); + assert_eq!( + statements[0].span(), + Span::new(Location::new(1, start), Location::new(1, end)), + "span for {sql}" + ); + } + let sql = "ALTER DEFAULT PRIVILEGES REVOKE SELECT ON TABLES FROM 'bob'@'localhost'"; + let statements = Parser::parse_sql(&sqlparser::dialect::GenericDialect {}, sql).unwrap(); + assert_eq!( + statements[0].span(), + Span::new(Location::new(1, 55), Location::new(1, 72)) + ); +} + #[test] fn parse_delimited_identifiers() { // check that quoted identifiers in any position remain quoted after serialization @@ -10848,82 +11034,6 @@ fn parse_geometric_typed_literal_error_propagates() { ); } -#[test] -fn parse_alter_default_privileges() { - // What `pg_dump -s` emits for a default ACL. - pg().verified_stmt("ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT SELECT ON TABLES TO app_user"); - pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES TO app"); - pg().verified_stmt("ALTER DEFAULT PRIVILEGES FOR ROLE a, b IN SCHEMA s1, s2 GRANT SELECT, INSERT ON TABLES TO app WITH GRANT OPTION"); - pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT ALL PRIVILEGES ON SEQUENCES TO app"); - pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT EXECUTE ON FUNCTIONS TO app"); - pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT EXECUTE ON ROUTINES TO app"); - pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT USAGE ON TYPES TO app"); - pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT USAGE, CREATE ON SCHEMAS TO app"); - pg().verified_stmt("ALTER DEFAULT PRIVILEGES REVOKE SELECT ON TABLES FROM app"); - pg().verified_stmt( - "ALTER DEFAULT PRIVILEGES REVOKE GRANT OPTION FOR ALL ON TABLES FROM app CASCADE", - ); - // `FOR USER` normalises to `FOR ROLE`. - pg().one_statement_parses_to( - "ALTER DEFAULT PRIVILEGES FOR USER bob GRANT SELECT ON TABLES TO app", - "ALTER DEFAULT PRIVILEGES FOR ROLE bob GRANT SELECT ON TABLES TO app", - ); - - // Options keep the order written. - for (sql, options) in [ - ( - "ALTER DEFAULT PRIVILEGES IN SCHEMA s FOR ROLE r GRANT SELECT ON TABLES TO app", - vec![ - DefaultPrivilegesOption::InSchema(vec![ObjectName::from(Ident::new("s"))]), - DefaultPrivilegesOption::ForRole(vec![Ident::new("r")]), - ], - ), - ( - "ALTER DEFAULT PRIVILEGES FOR ROLE r IN SCHEMA s GRANT SELECT ON TABLES TO app", - vec![ - DefaultPrivilegesOption::ForRole(vec![Ident::new("r")]), - DefaultPrivilegesOption::InSchema(vec![ObjectName::from(Ident::new("s"))]), - ], - ), - ] { - let mut stmts = pg().parse_sql_statements(sql).unwrap(); - match stmts.pop().unwrap() { - Statement::AlterDefaultPrivileges(stmt) => assert_eq!(stmt.options, options, "{sql}"), - _ => unreachable!(), - } - } - // Reversed option order round-trips unchanged. - pg().verified_stmt("ALTER DEFAULT PRIVILEGES IN SCHEMA public FOR ROLE postgres GRANT SELECT ON TABLES TO app_user"); - // `LARGE OBJECTS` is a valid object type for both `GRANT` and `REVOKE`. - pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT SELECT, UPDATE ON LARGE OBJECTS TO app"); - pg().verified_stmt( - "ALTER DEFAULT PRIVILEGES REVOKE GRANT OPTION FOR SELECT ON LARGE OBJECTS FROM app CASCADE", - ); - // Repeated options are rejected. - assert_eq!( - pg().parse_sql_statements( - "ALTER DEFAULT PRIVILEGES FOR ROLE a FOR ROLE b GRANT SELECT ON TABLES TO app" - ) - .unwrap_err(), - ParserError::ParserError("FOR ROLE specified more than once".to_string()), - ); - assert_eq!( - pg().parse_sql_statements( - "ALTER DEFAULT PRIVILEGES IN SCHEMA s IN SCHEMA t GRANT SELECT ON TABLES TO app" - ) - .unwrap_err(), - ParserError::ParserError("IN SCHEMA specified more than once".to_string()), - ); - - // `TABLES` is a keyword only here, so a table named `tables` still parses. - pg().verified_stmt("GRANT SELECT ON TABLES TO app"); - assert_eq!( - pg().parse_sql_statements("ALTER DEFAULT PRIVILEGES FOR ROLE a") - .unwrap_err(), - ParserError::ParserError("Expected: GRANT or REVOKE, found: EOF".to_string()), - ); -} - #[test] fn parse_create_aggregate_rejects_output_arguments_and_defaults() { for (args, expected) in [ @@ -10951,112 +11061,3 @@ fn parse_create_aggregate_rejects_output_arguments_and_defaults() { ); } } - -fn parse_alter_default_privileges_rejects_malformed_statements() { - for (sql, expected) in [ - ( - "ALTER DEFAULT PRIVILEGES FOR ALL GRANT SELECT ON TABLES TO app", - "Expected: one of ROLE or USER, found: ALL", - ), - ( - "ALTER DEFAULT PRIVILEGES FOR ROLE", - "Expected: identifier, found: EOF", - ), - ( - "ALTER DEFAULT PRIVILEGES IN SCHEMA", - "Expected: identifier, found: EOF", - ), - ( - "ALTER DEFAULT PRIVILEGES GRANT ON TABLES TO app", - "Expected: a privilege keyword, found: ON", - ), - ( - "ALTER DEFAULT PRIVILEGES GRANT SELECT TO app", - "Expected: ON, found: TO", - ), - ( - "ALTER DEFAULT PRIVILEGES GRANT SELECT ON INDEXES TO app", - "Expected: TABLES, SEQUENCES, FUNCTIONS, ROUTINES, TYPES, SCHEMAS or LARGE OBJECTS, found: INDEXES", - ), - ( - "ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES", - "Expected: TO, found: EOF", - ), - ( - "ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES TO", - "Expected: identifier, found: EOF", - ), - ] { - assert_eq!( - pg().parse_sql_statements(sql).unwrap_err(), - ParserError::ParserError(expected.to_string()), - ); - } -} - -#[test] -fn display_alter_default_privileges_propagates_write_errors() { - use core::fmt::{self, Write}; - - struct RejectingWriter(&'static str); - - impl Write for RejectingWriter { - fn write_str(&mut self, value: &str) -> fmt::Result { - if value.contains(self.0) { - Err(fmt::Error) - } else { - Ok(()) - } - } - } - - let grant = pg().verified_stmt( - "ALTER DEFAULT PRIVILEGES FOR ROLE r IN SCHEMA s GRANT SELECT ON TABLES TO g WITH GRANT OPTION", - ); - for rejected in ["D", "r", "g", "W"] { - let mut writer = RejectingWriter(rejected); - assert_eq!(write!(writer, "{grant}"), Err(fmt::Error)); - } - - let revoke = pg().verified_stmt( - "ALTER DEFAULT PRIVILEGES REVOKE GRANT OPTION FOR SELECT ON TABLES FROM g CASCADE", - ); - for rejected in ["K", "N", "g", "CASCADE"] { - let mut writer = RejectingWriter(rejected); - assert_eq!(write!(writer, "{revoke}"), Err(fmt::Error)); - } -} - -#[test] -fn parse_alter_default_privileges_spans() { - for (sql, start, end) in [ - ( - "ALTER DEFAULT PRIVILEGES FOR ROLE r IN SCHEMA s GRANT SELECT ON TABLES TO g", - 35, - 76, - ), - ( - "ALTER DEFAULT PRIVILEGES REVOKE SELECT ON TABLES FROM g", - 55, - 56, - ), - ( - "ALTER DEFAULT PRIVILEGES FOR ROLE r GRANT SELECT ON TABLES TO PUBLIC", - 35, - 36, - ), - ] { - let statements = Parser::parse_sql(&PostgreSqlDialect {}, sql).unwrap(); - assert_eq!( - statements[0].span(), - Span::new(Location::new(1, start), Location::new(1, end)), - "span for {sql}" - ); - } - let sql = "ALTER DEFAULT PRIVILEGES REVOKE SELECT ON TABLES FROM 'bob'@'localhost'"; - let statements = Parser::parse_sql(&sqlparser::dialect::GenericDialect {}, sql).unwrap(); - assert_eq!( - statements[0].span(), - Span::new(Location::new(1, 55), Location::new(1, 72)) - ); -} From 15d63d52498d8e2a621a1ad40d0d3badd713d10e Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 24 Oct 2026 12:06:13 +0200 Subject: [PATCH 6/7] Simplify default privilege spans and tests --- src/ast/dcl.rs | 32 +++++++++++++++++++++++++++++-- src/ast/spans.rs | 37 ++---------------------------------- tests/sqlparser_postgres.rs | 38 +------------------------------------ 3 files changed, 33 insertions(+), 74 deletions(-) diff --git a/src/ast/dcl.rs b/src/ast/dcl.rs index 096c6eca29..dbdfb775ae 100644 --- a/src/ast/dcl.rs +++ b/src/ast/dcl.rs @@ -30,8 +30,8 @@ use sqlparser_derive::{Visit, VisitMut}; use super::{display_comma_separated, Expr, Ident, Password, Spanned}; use crate::ast::{ - display_separated, CascadeOption, CurrentGrantsKind, GrantObjects, Grantee, ObjectName, - Privileges, + display_separated, CascadeOption, CurrentGrantsKind, GrantObjects, Grantee, GranteeName, + ObjectName, Privileges, }; use crate::tokenizer::Span; @@ -584,6 +584,34 @@ impl fmt::Display for AlterDefaultPrivileges { } } +impl Spanned for AlterDefaultPrivileges { + fn span(&self) -> Span { + let options = self.options.iter().map(|option| match option { + DefaultPrivilegesOption::ForRole(roles) => { + Span::union_iter(roles.iter().map(|role| role.span)) + } + DefaultPrivilegesOption::InSchema(schemas) => { + Span::union_iter(schemas.iter().map(Spanned::span)) + } + }); + let grantees = match &self.operation { + AlterDefaultPrivilegesOperation::Grant { grantees, .. } + | AlterDefaultPrivilegesOperation::Revoke { grantees, .. } => grantees, + }; + Span::union_iter( + options.chain( + grantees + .iter() + .filter_map(|grantee| grantee.name.as_ref()) + .map(|name| match name { + GranteeName::ObjectName(name) => name.span(), + GranteeName::UserHost { user, host } => user.span.union(&host.span), + }), + ), + ) + } +} + impl From for crate::ast::Statement { fn from(v: AlterDefaultPrivileges) -> Self { crate::ast::Statement::AlterDefaultPrivileges(v) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index fea6495851..fdafb7b2a6 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -28,19 +28,14 @@ use core::iter; use crate::tokenizer::Span; use super::{ - comments, - dcl::{ - AlterDefaultPrivileges, AlterDefaultPrivilegesOperation, DefaultPrivilegesOption, - SecondaryRoles, - }, - value::ValueWithSpan, AccessExpr, AlterColumnOperation, + comments, dcl::SecondaryRoles, value::ValueWithSpan, AccessExpr, AlterColumnOperation, AlterIndexOperation, AlterTableOperation, Analyze, Array, Assignment, AssignmentTarget, AttachedToken, BeginEndStatements, CaseStatement, CloseCursor, ClusteredIndex, ColumnDef, ColumnOption, ColumnOptionDef, ConditionalStatementBlock, ConditionalStatements, ConflictTarget, ConnectByKind, ConstraintCharacteristics, CopySource, CreateIndex, CreateTable, CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, ExcludeConstraintElement, ExcludeSelectItem, Expr, ExprWithAlias, Fetch, ForValues, FromTable, Function, FunctionArg, - FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, FunctionArguments, GranteeName, GroupByExpr, + FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, FunctionArguments, GroupByExpr, HavingBound, IfStatement, IlikeSelectItem, InTarget, IndexColumn, Insert, Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, LimitClause, MatchRecognizePattern, Measure, Merge, MergeAction, MergeClause, MergeInsertExpr, @@ -535,34 +530,6 @@ impl Spanned for Statement { } } -impl Spanned for AlterDefaultPrivileges { - fn span(&self) -> Span { - let options = self.options.iter().map(|option| match option { - DefaultPrivilegesOption::ForRole(roles) => { - union_spans(roles.iter().map(|role| role.span)) - } - DefaultPrivilegesOption::InSchema(schemas) => { - union_spans(schemas.iter().map(Spanned::span)) - } - }); - let grantees = match &self.operation { - AlterDefaultPrivilegesOperation::Grant { grantees, .. } - | AlterDefaultPrivilegesOperation::Revoke { grantees, .. } => grantees, - }; - union_spans( - options.chain( - grantees - .iter() - .filter_map(|grantee| grantee.name.as_ref()) - .map(|name| match name { - GranteeName::ObjectName(name) => name.span(), - GranteeName::UserHost { user, host } => user.span.union(&host.span), - }), - ), - ) - } -} - impl Spanned for Use { fn span(&self) -> Span { match self { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 2c15fa7d17..156274bb91 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5140,14 +5140,11 @@ fn parse_alter_default_privileges() { ], ), ] { - let mut stmts = pg().parse_sql_statements(sql).unwrap(); - match stmts.pop().unwrap() { + match pg().verified_stmt(sql) { Statement::AlterDefaultPrivileges(stmt) => assert_eq!(stmt.options, options, "{sql}"), _ => unreachable!(), } } - // Reversed option order round-trips unchanged. - pg().verified_stmt("ALTER DEFAULT PRIVILEGES IN SCHEMA public FOR ROLE postgres GRANT SELECT ON TABLES TO app_user"); // `LARGE OBJECTS` is a valid object type for both `GRANT` and `REVOKE`. pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT SELECT, UPDATE ON LARGE OBJECTS TO app"); pg().verified_stmt( @@ -5221,39 +5218,6 @@ fn parse_alter_default_privileges_rejects_malformed_statements() { } } -#[test] -fn display_alter_default_privileges_propagates_write_errors() { - use core::fmt::{self, Write}; - - struct RejectingWriter(&'static str); - - impl Write for RejectingWriter { - fn write_str(&mut self, value: &str) -> fmt::Result { - if value.contains(self.0) { - Err(fmt::Error) - } else { - Ok(()) - } - } - } - - let grant = pg().verified_stmt( - "ALTER DEFAULT PRIVILEGES FOR ROLE r IN SCHEMA s GRANT SELECT ON TABLES TO g WITH GRANT OPTION", - ); - for rejected in ["D", "r", "g", "W"] { - let mut writer = RejectingWriter(rejected); - assert_eq!(write!(writer, "{grant}"), Err(fmt::Error)); - } - - let revoke = pg().verified_stmt( - "ALTER DEFAULT PRIVILEGES REVOKE GRANT OPTION FOR SELECT ON TABLES FROM g CASCADE", - ); - for rejected in ["K", "N", "g", "CASCADE"] { - let mut writer = RejectingWriter(rejected); - assert_eq!(write!(writer, "{revoke}"), Err(fmt::Error)); - } -} - #[test] fn parse_alter_default_privileges_spans() { for (sql, start, end) in [ From 986c5276ef2dbc870079e6423ee1eceb8da24ef5 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 24 Oct 2026 13:43:28 +0200 Subject: [PATCH 7/7] Add fuzz seeds for pg alter default privileges --- .../postgres_2417_alter_default_privileges.sql | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 fuzz/fuzz_seeds/postgres_2417_alter_default_privileges.sql diff --git a/fuzz/fuzz_seeds/postgres_2417_alter_default_privileges.sql b/fuzz/fuzz_seeds/postgres_2417_alter_default_privileges.sql new file mode 100644 index 0000000000..a1c2036315 --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_2417_alter_default_privileges.sql @@ -0,0 +1,10 @@ +ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT SELECT ON TABLES TO app_user; +ALTER DEFAULT PRIVILEGES FOR ROLE a, b IN SCHEMA s1, s2 GRANT SELECT, INSERT ON TABLES TO app WITH GRANT OPTION; +ALTER DEFAULT PRIVILEGES FOR USER bob GRANT ALL PRIVILEGES ON SEQUENCES TO app; +ALTER DEFAULT PRIVILEGES GRANT EXECUTE ON FUNCTIONS TO app; +ALTER DEFAULT PRIVILEGES GRANT USAGE ON ROUTINES TO app; +ALTER DEFAULT PRIVILEGES GRANT USAGE, CREATE ON TYPES TO app; +ALTER DEFAULT PRIVILEGES GRANT SELECT ON SCHEMAS TO app; +ALTER DEFAULT PRIVILEGES GRANT SELECT, UPDATE ON LARGE OBJECTS TO app; +ALTER DEFAULT PRIVILEGES REVOKE SELECT ON TABLES FROM app; +ALTER DEFAULT PRIVILEGES REVOKE GRANT OPTION FOR ALL ON TABLES FROM app CASCADE