From b1ad1734e9bd1558741980259a8f663c8faa3369 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Thu, 29 Oct 2026 11:21:14 +0100 Subject: [PATCH 1/2] PostgreSQL: Parse `ROUTINE` and `ALL PROCEDURES | ROUTINES IN SCHEMA` grant targets --- src/ast/mod.rs | 34 ++++++++++++++++++++++++ src/keywords.rs | 3 +++ src/parser/mod.rs | 22 ++++++++++++++++ tests/sqlparser_common.rs | 54 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c06e120aee..fd523daa56 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -7779,6 +7779,16 @@ pub enum GrantObjects { /// The target schema names. schemas: Vec, }, + /// Grant privileges on `ALL PROCEDURES IN SCHEMA [, ...]` + AllProceduresInSchema { + /// The target schema names. + schemas: Vec, + }, + /// Grant privileges on `ALL ROUTINES IN SCHEMA [, ...]` + AllRoutinesInSchema { + /// The target schema names. + schemas: Vec, + }, /// Grant privileges on `FUTURE SCHEMAS IN DATABASE [, ...]` FutureSchemasInDatabase { /// The target database names. @@ -7847,6 +7857,13 @@ pub enum GrantObjects { /// For example: /// `GRANT EXECUTE ON FUNCTION foo(p_id uuid), bar() TO role1` Functions(Vec), + /// Grant privileges on routines, which name functions and procedures alike. + /// + /// For example: + /// `GRANT EXECUTE ON ROUTINE foo(p_id uuid), bar() TO role1` + /// + /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-grant.html) + Routines(Vec), } impl fmt::Display for GrantObjects { @@ -7915,6 +7932,20 @@ impl fmt::Display for GrantObjects { display_comma_separated(schemas) ) } + GrantObjects::AllProceduresInSchema { schemas } => { + write!( + f, + "ALL PROCEDURES IN SCHEMA {}", + display_comma_separated(schemas) + ) + } + GrantObjects::AllRoutinesInSchema { schemas } => { + write!( + f, + "ALL ROUTINES IN SCHEMA {}", + display_comma_separated(schemas) + ) + } GrantObjects::FutureSchemasInDatabase { databases } => { write!( f, @@ -7984,6 +8015,9 @@ impl fmt::Display for GrantObjects { GrantObjects::Functions(functions) => { write!(f, "FUNCTION {}", display_comma_separated(functions)) } + GrantObjects::Routines(routines) => { + write!(f, "ROUTINE {}", display_comma_separated(routines)) + } } } } diff --git a/src/keywords.rs b/src/keywords.rs index b35644befb..561919b6e1 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -845,6 +845,7 @@ define_keywords!( PRIVILEGES, PROC, PROCEDURE, + PROCEDURES, PROCESSLIST, PROFILE, PROGRAM, @@ -934,6 +935,8 @@ define_keywords!( ROLLBACK, ROLLUP, ROOT, + ROUTINE, + ROUTINES, ROW, ROWGROUPSIZE, ROWID, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6f82dfaac8..d9de76a565 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -18722,6 +18722,24 @@ impl<'a> Parser<'a> { Some(GrantObjects::AllFunctionsInSchema { schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, }) + } else if self.parse_keywords(&[ + Keyword::ALL, + Keyword::PROCEDURES, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::AllProceduresInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) + } else if self.parse_keywords(&[ + Keyword::ALL, + Keyword::ROUTINES, + Keyword::IN, + Keyword::SCHEMA, + ]) { + Some(GrantObjects::AllRoutinesInSchema { + schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?, + }) } else if self.parse_keywords(&[ Keyword::FUTURE, Keyword::SCHEMAS, @@ -18815,6 +18833,10 @@ impl<'a> Parser<'a> { Some(GrantObjects::Procedures( self.parse_comma_separated(Parser::parse_function_desc)?, )) + } else if self.parse_keyword(Keyword::ROUTINE) { + Some(GrantObjects::Routines( + self.parse_comma_separated(Parser::parse_function_desc)?, + )) } else { let object_type = self.parse_one_of_keywords(&[ Keyword::SEQUENCE, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 379e37a660..658dfeca86 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -22744,3 +22744,57 @@ fn display_create_procedure_parameter_list_propagates_write_errors() { let mut writer = RejectingWriter(" ("); assert_eq!(write!(writer, "{statement}"), Err(fmt::Error)); } + +#[test] +fn parse_grant_routines() { + let objects = |sql: &str| match verified_stmt(sql) { + Statement::Grant(grant) => grant.objects, + Statement::Revoke(revoke) => revoke.objects, + stmt => panic!("unexpected statement: {stmt:?}"), + }; + let name = |name: &str| ObjectName::from(vec![Ident::new(name)]); + + assert_eq!( + objects("GRANT EXECUTE ON ROUTINE f(INT) TO bob"), + Some(GrantObjects::Routines(vec![FunctionDesc { + name: name("f"), + args: Some(vec![OperateFunctionArg::unnamed(DataType::Int(None))]), + }])) + ); + assert_eq!( + objects("REVOKE ALL ON ROUTINE f, g() FROM bob"), + Some(GrantObjects::Routines(vec![ + FunctionDesc { + name: name("f"), + args: None, + }, + FunctionDesc { + name: name("g"), + args: Some(vec![]), + }, + ])) + ); + assert_eq!( + objects("GRANT EXECUTE ON ALL PROCEDURES IN SCHEMA s TO bob"), + Some(GrantObjects::AllProceduresInSchema { + schemas: vec![name("s")], + }) + ); + assert_eq!( + objects("REVOKE EXECUTE ON ALL ROUTINES IN SCHEMA s, t FROM bob"), + Some(GrantObjects::AllRoutinesInSchema { + schemas: vec![name("s"), name("t")], + }) + ); + + verified_stmt("REVOKE ALL ON ROUTINE public.f(p_id UUID), g() FROM bob"); + verified_stmt("REVOKE EXECUTE ON ALL PROCEDURES IN SCHEMA s, t FROM bob"); + verified_stmt("GRANT EXECUTE ON ALL ROUTINES IN SCHEMA s TO bob"); + + assert_eq!( + all_dialects() + .parse_sql_statements("GRANT EXECUTE ON ROUTINE f(INT") + .unwrap_err(), + ParserError::ParserError("Expected: ), found: EOF".to_string()) + ); +} From 88c587b4913c1ab7e0fe6c5b76317c3912faecf6 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 30 Oct 2026 15:04:33 +0100 Subject: [PATCH 2/2] Read object-type keywords before TO, FROM, comma or dot as table names --- .../postgres_2761_grant_on_routine_table.sql | 1 + src/parser/mod.rs | 52 ++++++++++------ tests/sqlparser_common.rs | 60 +++++++++++++++++-- 3 files changed, 91 insertions(+), 22 deletions(-) create mode 100644 fuzz/fuzz_seeds/postgres_2761_grant_on_routine_table.sql diff --git a/fuzz/fuzz_seeds/postgres_2761_grant_on_routine_table.sql b/fuzz/fuzz_seeds/postgres_2761_grant_on_routine_table.sql new file mode 100644 index 0000000000..e7bdcc6595 --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_2761_grant_on_routine_table.sql @@ -0,0 +1 @@ +GRANT SELECT ON routine, function.t TO r diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d9de76a565..0f6b07e74a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -18666,6 +18666,20 @@ impl<'a> Parser<'a> { Ok(values) } + /// Whether the keyword at the next token introduces a grant object type. A table named after + /// the keyword is followed by `TO`, `FROM`, `,` or `.`, as in `GRANT SELECT ON function TO r`. + fn peek_grant_object_type(&self) -> bool { + !matches!( + &self.peek_nth_token_ref(1).token, + Token::Comma + | Token::Period + | Token::Word(Word { + keyword: Keyword::TO | Keyword::FROM, + .. + }) + ) + } + /// Parse privileges and optional target objects for GRANT/DENY/REVOKE statements. pub fn parse_grant_deny_revoke_privileges_objects( &mut self, @@ -18825,33 +18839,37 @@ impl<'a> Parser<'a> { Some(GrantObjects::ExternalVolumes( self.parse_comma_separated(|p| p.parse_object_name(false))?, )) - } else if self.parse_keyword(Keyword::FUNCTION) { + } else if self.peek_grant_object_type() && self.parse_keyword(Keyword::FUNCTION) { Some(GrantObjects::Functions( self.parse_comma_separated(Parser::parse_function_desc)?, )) - } else if self.parse_keyword(Keyword::PROCEDURE) { + } else if self.peek_grant_object_type() && self.parse_keyword(Keyword::PROCEDURE) { Some(GrantObjects::Procedures( self.parse_comma_separated(Parser::parse_function_desc)?, )) - } else if self.parse_keyword(Keyword::ROUTINE) { + } else if self.peek_grant_object_type() && self.parse_keyword(Keyword::ROUTINE) { Some(GrantObjects::Routines( self.parse_comma_separated(Parser::parse_function_desc)?, )) } else { - let object_type = self.parse_one_of_keywords(&[ - Keyword::SEQUENCE, - Keyword::DATABASE, - Keyword::SCHEMA, - Keyword::TABLE, - Keyword::VIEW, - Keyword::WAREHOUSE, - Keyword::INTEGRATION, - Keyword::VIEW, - Keyword::WAREHOUSE, - Keyword::INTEGRATION, - Keyword::USER, - Keyword::CONNECTION, - ]); + let object_type = if self.peek_grant_object_type() { + self.parse_one_of_keywords(&[ + Keyword::SEQUENCE, + Keyword::DATABASE, + Keyword::SCHEMA, + Keyword::TABLE, + Keyword::VIEW, + Keyword::WAREHOUSE, + Keyword::INTEGRATION, + Keyword::VIEW, + Keyword::WAREHOUSE, + Keyword::INTEGRATION, + Keyword::USER, + Keyword::CONNECTION, + ]) + } else { + None + }; let objects = self.parse_comma_separated(|p| p.parse_object_name_inner(false, true)); match object_type { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 658dfeca86..d3a3e75698 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -22790,11 +22790,61 @@ fn parse_grant_routines() { verified_stmt("REVOKE ALL ON ROUTINE public.f(p_id UUID), g() FROM bob"); verified_stmt("REVOKE EXECUTE ON ALL PROCEDURES IN SCHEMA s, t FROM bob"); verified_stmt("GRANT EXECUTE ON ALL ROUTINES IN SCHEMA s TO bob"); - assert_eq!( - all_dialects() - .parse_sql_statements("GRANT EXECUTE ON ROUTINE f(INT") - .unwrap_err(), - ParserError::ParserError("Expected: ), found: EOF".to_string()) + objects("GRANT EXECUTE ON ROUTINE routine TO bob"), + Some(GrantObjects::Routines(vec![FunctionDesc { + name: name("routine"), + args: None, + }])) ); + + for (sql, err) in [ + ("GRANT EXECUTE ON ROUTINE f(INT", "Expected: ), found: EOF"), + ( + "GRANT EXECUTE ON ALL PROCEDURES IN SCHEMA", + "Expected: identifier, found: EOF", + ), + ( + "GRANT EXECUTE ON ALL ROUTINES IN SCHEMA", + "Expected: identifier, found: EOF", + ), + ] { + assert_eq!( + all_dialects().parse_sql_statements(sql), + Err(ParserError::ParserError(err.to_string())), + "{sql}" + ); + } +} + +#[test] +fn parse_grant_on_tables_named_after_object_types() { + for name in [ + "function", + "procedure", + "routine", + "sequence", + "database", + "schema", + "view", + "warehouse", + "integration", + "user", + "connection", + ] { + let sql = format!("GRANT SELECT ON {name} TO r"); + match verified_stmt(&sql) { + Statement::Grant(grant) => assert_eq!( + grant.objects, + Some(GrantObjects::Tables(vec![ObjectName::from(vec![ + Ident::new(name) + ])])), + "{sql}" + ), + stmt => panic!("unexpected statement: {stmt:?}"), + } + one_statement_parses_to(&format!("GRANT SELECT ON TABLE {name} TO r"), &sql); + verified_stmt(&format!("REVOKE SELECT ON {name}, t FROM r")); + verified_stmt(&format!("GRANT SELECT ON {name}.t TO r")); + } }