From c44417be0668c9773252cc819278e0f03b6b875e Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 2 Oct 2026 18:15:09 +0200 Subject: [PATCH 1/8] Support PostgreSQL `ALTER DEFAULT PRIVILEGES` --- src/ast/dcl.rs | 146 ++++++++++++++++++++++++++++++++++++ src/ast/mod.rs | 8 +- src/ast/spans.rs | 2 + src/keywords.rs | 1 + src/parser/mod.rs | 108 ++++++++++++++++++++++++-- tests/sqlparser_postgres.rs | 29 +++++++ 6 files changed, 285 insertions(+), 9 deletions(-) diff --git a/src/ast/dcl.rs b/src/ast/dcl.rs index dea7767620..1ce5e083ed 100644 --- a/src/ast/dcl.rs +++ b/src/ast/dcl.rs @@ -687,3 +687,149 @@ impl fmt::Display for RoleGrantOptionValue { }) } } + +/// `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 da24d047a4..2f277fbb56 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, GrantRole, ResetConfig, Revoke, RevokeRole, + AlterDefaultPrivileges, AlterDefaultPrivilegesOperation, AlterRoleOperation, CreateRole, + DefaultPrivilegesObjectType, Grant, GrantRole, ResetConfig, Revoke, RevokeRole, RoleGrantOption, RoleGrantOptionValue, RoleKeyword, RoleOption, SecondaryRoles, SetConfigValue, Use, }; @@ -4689,6 +4690,10 @@ pub enum Statement { /// ``` RevokeRole(RevokeRole), /// ```sql + /// ALTER DEFAULT PRIVILEGES + /// ``` + AlterDefaultPrivileges(AlterDefaultPrivileges), + /// ```sql /// DEALLOCATE [ PREPARE ] { name | ALL } /// ``` /// @@ -6195,6 +6200,7 @@ impl fmt::Display for Statement { Statement::Revoke(revoke) => write!(f, "{revoke}"), Statement::GrantRole(grant) => write!(f, "{grant}"), Statement::RevokeRole(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 704b583b8b..0675ccc3cb 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -304,6 +304,7 @@ impl Spanned for Values { /// - [Statement::Revoke] /// - [Statement::GrantRole] /// - [Statement::RevokeRole] +/// - [Statement::AlterDefaultPrivileges] /// - [Statement::Deallocate] /// - [Statement::Execute] /// - [Statement::Prepare] @@ -473,6 +474,7 @@ impl Spanned for Statement { Statement::Revoke { .. } => Span::empty(), Statement::GrantRole { .. } => Span::empty(), Statement::RevokeRole { .. } => 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 561919b6e1..3879161385 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -1130,6 +1130,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 a66961e0ef..7262c96e93 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11791,6 +11791,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 index = self.index; self.advance_token(); let object_type = match &self.get_current_token().token { @@ -18688,14 +18692,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]) { @@ -19269,6 +19266,101 @@ impl<'a> Parser<'a> { matched } + /// 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 7fb4bb23fe..ab3c783d21 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10859,6 +10859,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 8e5262c876e64c83e206b39de9d2b8a2af6abab5 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Mon, 19 Oct 2026 20:19:57 +0200 Subject: [PATCH 2/8] Accept the default privileges options in any order and ON LARGE OBJECTS --- src/ast/dcl.rs | 44 +++++++++++++++++++++---------- src/ast/mod.rs | 6 ++--- src/parser/mod.rs | 52 ++++++++++++++++++++++++------------- tests/sqlparser_postgres.rs | 46 ++++++++++++++++++++++++++++++++ 4 files changed, 114 insertions(+), 34 deletions(-) diff --git a/src/ast/dcl.rs b/src/ast/dcl.rs index 1ce5e083ed..f9318661a5 100644 --- a/src/ast/dcl.rs +++ b/src/ast/dcl.rs @@ -688,6 +688,30 @@ impl fmt::Display for RoleGrantOptionValue { } } +/// 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. /// @@ -696,10 +720,8 @@ impl fmt::Display for RoleGrantOptionValue { #[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, } @@ -707,15 +729,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) } @@ -819,6 +834,8 @@ pub enum DefaultPrivilegesObjectType { Types, /// `SCHEMAS` Schemas, + /// `LARGE OBJECTS` + LargeObjects, } impl fmt::Display for DefaultPrivilegesObjectType { @@ -830,6 +847,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 2f277fbb56..e3f566d499 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -59,9 +59,9 @@ pub use self::data_type::{ }; pub use self::dcl::{ AlterDefaultPrivileges, AlterDefaultPrivilegesOperation, AlterRoleOperation, CreateRole, - DefaultPrivilegesObjectType, Grant, GrantRole, ResetConfig, Revoke, RevokeRole, - RoleGrantOption, RoleGrantOptionValue, RoleKeyword, RoleOption, SecondaryRoles, SetConfigValue, - Use, + DefaultPrivilegesObjectType, DefaultPrivilegesOption, Grant, GrantRole, ResetConfig, Revoke, + RevokeRole, RoleGrantOption, RoleGrantOptionValue, RoleKeyword, 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 7262c96e93..4de7e0b999 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -19273,18 +19273,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 @@ -19323,11 +19340,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. @@ -19344,6 +19357,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), @@ -19357,7 +19373,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 ab3c783d21..be35210294 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10882,6 +10882,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 057b9cdb9ed248523c8ea7a97a05fb62737d6fdc Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Tue, 20 Oct 2026 13:04:59 +0200 Subject: [PATCH 3/8] Track default-privilege source spans and cover parser boundaries --- src/ast/spans.rs | 78 +++++++++++++++++-------- tests/sqlparser_postgres.rs | 110 ++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 23 deletions(-) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 0675ccc3cb..093d3edcc7 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -28,27 +28,33 @@ use core::iter; use crate::tokenizer::Span; use super::{ - comments, dcl::SecondaryRoles, value::ValueWithSpan, AccessExpr, AlterColumnOperation, - AlterIndexOperation, AlterTableOperation, AlterTrigger, AlterTriggerOperation, Analyze, Array, - Assignment, AssignmentTarget, AttachedToken, BeginEndStatements, CaseStatement, CloseCursor, - ClusteredIndex, ColumnDef, ColumnOption, ColumnOptionDef, ConditionalStatementBlock, - ConditionalStatements, ConflictTarget, ConnectByKind, ConstraintCharacteristics, CopySource, - CreateIndex, CreateRule, CreateRuleAction, CreateTable, CreateTableOptions, Cte, Delete, - DoUpdate, ExceptSelectItem, ExcludeConstraintElement, ExcludeSelectItem, Expr, ExprWithAlias, - Fetch, ForValues, FromTable, Function, FunctionArg, 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, - MergeInsertKind, MergeUpdateExpr, MergeUpdateKind, NamedParenthesizedList, - NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, - OnInsert, OpenStatement, OperateFunctionArg, OrderBy, OrderByExpr, OrderByKind, OutputClause, - Parens, Partition, PartitionBoundValue, PivotValueSource, ProjectionSelect, Query, - RaiseStatement, RaiseStatementValue, ReferentialAction, RenameSelectItem, ReplaceSelectElement, - ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, Setting, SqlOption, Statement, - Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, - TableObject, TableOptionsClustered, TableWithJoins, Update, UpdateTableFromKind, Use, Values, - ViewColumnDef, WhileStatement, WildcardAdditionalOptions, With, WithFill, + comments, + dcl::{ + AlterDefaultPrivileges, AlterDefaultPrivilegesOperation, DefaultPrivilegesOption, + SecondaryRoles, + }, + value::ValueWithSpan, + AccessExpr, AlterColumnOperation, AlterIndexOperation, AlterTableOperation, AlterTrigger, + AlterTriggerOperation, Analyze, Array, Assignment, AssignmentTarget, AttachedToken, + BeginEndStatements, CaseStatement, CloseCursor, ClusteredIndex, ColumnDef, ColumnOption, + ColumnOptionDef, ConditionalStatementBlock, ConditionalStatements, ConflictTarget, + ConnectByKind, ConstraintCharacteristics, CopySource, CreateIndex, CreateRule, + CreateRuleAction, CreateTable, CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, + ExcludeConstraintElement, ExcludeSelectItem, Expr, ExprWithAlias, Fetch, ForValues, FromTable, + Function, FunctionArg, 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, MergeInsertKind, MergeUpdateExpr, + MergeUpdateKind, NamedParenthesizedList, NamedWindowDefinition, ObjectName, ObjectNamePart, + Offset, OnConflict, OnConflictAction, OnInsert, OpenStatement, OperateFunctionArg, OrderBy, + OrderByExpr, OrderByKind, OutputClause, Parens, Partition, PartitionBoundValue, + PivotValueSource, ProjectionSelect, Query, RaiseStatement, RaiseStatementValue, + ReferentialAction, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, + SelectInto, SelectItem, SetExpr, Setting, SqlOption, Statement, Subscript, SymbolDefinition, + TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, TableObject, + TableOptionsClustered, TableWithJoins, Update, UpdateTableFromKind, Use, Values, ViewColumnDef, + WhileStatement, WildcardAdditionalOptions, With, WithFill, }; /// Given an iterator of spans, return the [Span::union] of all spans. @@ -304,7 +310,6 @@ impl Spanned for Values { /// - [Statement::Revoke] /// - [Statement::GrantRole] /// - [Statement::RevokeRole] -/// - [Statement::AlterDefaultPrivileges] /// - [Statement::Deallocate] /// - [Statement::Execute] /// - [Statement::Prepare] @@ -474,7 +479,7 @@ impl Spanned for Statement { Statement::Revoke { .. } => Span::empty(), Statement::GrantRole { .. } => Span::empty(), Statement::RevokeRole { .. } => Span::empty(), - Statement::AlterDefaultPrivileges { .. } => Span::empty(), + Statement::AlterDefaultPrivileges(privileges) => privileges.span(), Statement::Deallocate { .. } => Span::empty(), Statement::Execute { .. } => Span::empty(), Statement::Prepare { .. } => Span::empty(), @@ -557,6 +562,33 @@ impl Spanned for AlterTriggerOperation { } } +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 be35210294..76f4a68ce6 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -11746,3 +11746,113 @@ fn parse_drop_group_is_dialect_gated() { )) ); } + +#[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)) + ); +} From 7a8a174ee71b3b723e9264edc049a0e1237e0142 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Wed, 21 Oct 2026 09:53:35 +0200 Subject: [PATCH 4/8] 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 76f4a68ce6..c5830f889a 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10859,6 +10859,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 ac5614f0020ec6e67af63d61df1cd478a05c3d8f Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 24 Oct 2026 00:27:16 +0200 Subject: [PATCH 5/8] Place regression tests beside related syntax cases --- tests/sqlparser_postgres.rs | 370 ++++++++++++++++++------------------ 1 file changed, 186 insertions(+), 184 deletions(-) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index c5830f889a..b173651304 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5164,6 +5164,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() { pg().verified_stmt(r#"CREATE TABLE "foo" ("bar" "int")"#); @@ -10862,81 +11048,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() { @@ -11748,112 +11859,3 @@ fn parse_drop_group_is_dialect_gated() { ); } -#[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)) - ); -} From 808da2e729da136beac810fce7f7289c8f560db7 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 24 Oct 2026 12:06:13 +0200 Subject: [PATCH 6/8] Simplify default privilege spans and tests --- src/ast/dcl.rs | 32 +++++++++++++++- src/ast/spans.rs | 75 +++++++++++-------------------------- tests/sqlparser_postgres.rs | 38 +------------------ 3 files changed, 52 insertions(+), 93 deletions(-) diff --git a/src/ast/dcl.rs b/src/ast/dcl.rs index f9318661a5..3fc6f0c89b 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; @@ -736,6 +736,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 093d3edcc7..a1904184c0 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -28,33 +28,27 @@ use core::iter; use crate::tokenizer::Span; use super::{ - comments, - dcl::{ - AlterDefaultPrivileges, AlterDefaultPrivilegesOperation, DefaultPrivilegesOption, - SecondaryRoles, - }, - value::ValueWithSpan, - AccessExpr, AlterColumnOperation, AlterIndexOperation, AlterTableOperation, AlterTrigger, - AlterTriggerOperation, Analyze, Array, Assignment, AssignmentTarget, AttachedToken, - BeginEndStatements, CaseStatement, CloseCursor, ClusteredIndex, ColumnDef, ColumnOption, - ColumnOptionDef, ConditionalStatementBlock, ConditionalStatements, ConflictTarget, - ConnectByKind, ConstraintCharacteristics, CopySource, CreateIndex, CreateRule, - CreateRuleAction, CreateTable, CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, - ExcludeConstraintElement, ExcludeSelectItem, Expr, ExprWithAlias, Fetch, ForValues, FromTable, - Function, FunctionArg, 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, MergeInsertKind, MergeUpdateExpr, - MergeUpdateKind, NamedParenthesizedList, NamedWindowDefinition, ObjectName, ObjectNamePart, - Offset, OnConflict, OnConflictAction, OnInsert, OpenStatement, OperateFunctionArg, OrderBy, - OrderByExpr, OrderByKind, OutputClause, Parens, Partition, PartitionBoundValue, - PivotValueSource, ProjectionSelect, Query, RaiseStatement, RaiseStatementValue, - ReferentialAction, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, - SelectInto, SelectItem, SetExpr, Setting, SqlOption, Statement, Subscript, SymbolDefinition, - TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, TableObject, - TableOptionsClustered, TableWithJoins, Update, UpdateTableFromKind, Use, Values, ViewColumnDef, - WhileStatement, WildcardAdditionalOptions, With, WithFill, + comments, dcl::SecondaryRoles, value::ValueWithSpan, AccessExpr, AlterColumnOperation, + AlterIndexOperation, AlterTableOperation, AlterTrigger, AlterTriggerOperation, Analyze, Array, + Assignment, AssignmentTarget, AttachedToken, BeginEndStatements, CaseStatement, CloseCursor, + ClusteredIndex, ColumnDef, ColumnOption, ColumnOptionDef, ConditionalStatementBlock, + ConditionalStatements, ConflictTarget, ConnectByKind, ConstraintCharacteristics, CopySource, + CreateIndex, CreateRule, CreateRuleAction, CreateTable, CreateTableOptions, Cte, Delete, + DoUpdate, ExceptSelectItem, ExcludeConstraintElement, ExcludeSelectItem, Expr, ExprWithAlias, + Fetch, ForValues, FromTable, Function, FunctionArg, 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, + MergeInsertKind, MergeUpdateExpr, MergeUpdateKind, NamedParenthesizedList, + NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, + OnInsert, OpenStatement, OperateFunctionArg, OrderBy, OrderByExpr, OrderByKind, OutputClause, + Parens, Partition, PartitionBoundValue, PivotValueSource, ProjectionSelect, Query, + RaiseStatement, RaiseStatementValue, ReferentialAction, RenameSelectItem, ReplaceSelectElement, + ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, Setting, SqlOption, Statement, + Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, + TableObject, TableOptionsClustered, TableWithJoins, Update, UpdateTableFromKind, Use, Values, + ViewColumnDef, WhileStatement, WildcardAdditionalOptions, With, WithFill, }; /// Given an iterator of spans, return the [Span::union] of all spans. @@ -562,33 +556,6 @@ impl Spanned for AlterTriggerOperation { } } -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 b173651304..f741f443c6 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5202,14 +5202,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( @@ -5283,39 +5280,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 8035262b63986cde6c4d1c40a264617f699249a0 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 24 Oct 2026 13:43:28 +0200 Subject: [PATCH 7/8] 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 From 339301beb20bfd539241d143c6b35936d98c1370 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 30 Oct 2026 20:10:04 +0100 Subject: [PATCH 8/8] Test ALTER DEFAULT PRIVILEGES display write errors --- tests/sqlparser_postgres.rs | 40 +++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index f741f443c6..7e0897fc09 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5314,6 +5314,44 @@ fn parse_alter_default_privileges_spans() { ); } +#[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 [ + "ALTER DEFAULT PRIVILEGES", + "FOR ROLE ", + "GRANT ", + " WITH GRANT OPTION", + ] { + let mut writer = RejectingWriter(rejected); + assert_eq!(write!(writer, "{grant}"), Err(fmt::Error), "{rejected}"); + } + + let revoke = pg().verified_stmt( + "ALTER DEFAULT PRIVILEGES REVOKE GRANT OPTION FOR SELECT ON TABLES FROM g CASCADE", + ); + for rejected in ["REVOKE ", "GRANT OPTION FOR ", " FROM ", "CASCADE"] { + let mut writer = RejectingWriter(rejected); + assert_eq!(write!(writer, "{revoke}"), Err(fmt::Error), "{rejected}"); + } +} + #[test] fn parse_delimited_identifiers() { pg().verified_stmt(r#"CREATE TABLE "foo" ("bar" "int")"#); @@ -11012,7 +11050,6 @@ fn parse_geometric_typed_literal_error_propagates() { ); } - #[test] fn parse_create_aggregate_rejects_output_arguments_and_defaults() { for (args, expected) in [ @@ -11822,4 +11859,3 @@ fn parse_drop_group_is_dialect_gated() { )) ); } -