Skip to content

Commit f04ad35

Browse files
authored
Merge pull request #22 from fmguerreiro/fix/trigger-exec-body-call-args
fix(parser): accept call-site expression args in CREATE TRIGGER EXECUTE FUNCTION
2 parents e698c0c + 748d1d9 commit f04ad35

5 files changed

Lines changed: 138 additions & 48 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
[package]
1919
name = "pgmold-sqlparser"
2020
description = "Fork of sqlparser with additional PostgreSQL features (PARTITION OF, SECURITY DEFINER/INVOKER, SET params, EXCLUDE, TEXT SEARCH, AGGREGATE, FOREIGN TABLE/FDW, PUBLICATION, SUBSCRIPTION, ALTER DOMAIN/TRIGGER/EXTENSION, CAST, CONVERSION, LANGUAGE, RULE, STATISTICS, ACCESS METHOD, EVENT TRIGGER, TRANSFORM, SECURITY LABEL, USER MAPPING, TABLESPACE)"
21-
version = "0.60.13"
21+
version = "0.60.14"
2222
authors = ["Filipe Guerreiro <filipe.m.guerreiro@gmail.com>"]
2323
homepage = "https://github.com/fmguerreiro/datafusion-sqlparser-rs"
2424
documentation = "https://docs.rs/pgmold-sqlparser/"

src/ast/trigger.rs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -161,24 +161,32 @@ impl fmt::Display for TriggerExecBodyType {
161161
}
162162
}
163163
}
164-
/// This keyword immediately precedes the declaration of one or two relation names that provide access to the transition relations of the triggering statement
164+
/// The EXECUTE clause of a CREATE TRIGGER statement.
165+
///
166+
/// Holds call-site information: the function/procedure name and optional
167+
/// positional argument expressions (e.g. string literals passed to
168+
/// `tsvector_update_trigger`). This is deliberately distinct from
169+
/// `FunctionDesc`, which carries CREATE-FUNCTION-style parameter
170+
/// *declarations* (`argname argtype DEFAULT expr`).
165171
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
166172
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
167173
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
168174
pub struct TriggerExecBody {
169175
/// Whether the body is a `FUNCTION` or `PROCEDURE` invocation.
170176
pub exec_type: TriggerExecBodyType,
171-
/// Description of the function/procedure to execute.
172-
pub func_desc: FunctionDesc,
177+
/// The name of the function or procedure to invoke.
178+
pub func_name: ObjectName,
179+
/// Call-site arguments (expressions). `None` means no parentheses were
180+
/// written; `Some(vec![])` means empty parentheses `()`.
181+
pub args: Option<Vec<Expr>>,
173182
}
174183

175184
impl fmt::Display for TriggerExecBody {
176185
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
177-
write!(
178-
f,
179-
"{exec_type} {func_desc}",
180-
exec_type = self.exec_type,
181-
func_desc = self.func_desc
182-
)
186+
write!(f, "{} {}", self.exec_type, self.func_name)?;
187+
if let Some(args) = &self.args {
188+
write!(f, "({})", display_comma_separated(args))?;
189+
}
190+
Ok(())
183191
}
184192
}

src/parser/mod.rs

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6385,18 +6385,42 @@ impl<'a> Parser<'a> {
63856385
}
63866386

63876387
/// Parse the execution body of a trigger (`FUNCTION` or `PROCEDURE`).
6388+
///
6389+
/// Unlike CREATE FUNCTION, trigger EXECUTE clauses take call-site
6390+
/// expressions as arguments (e.g. string literals), not parameter
6391+
/// declarations. We therefore parse the name separately and then
6392+
/// parse each argument as a full expression.
63886393
pub fn parse_trigger_exec_body(&mut self) -> Result<TriggerExecBody, ParserError> {
6394+
let exec_type = match self
6395+
.expect_one_of_keywords(&[Keyword::FUNCTION, Keyword::PROCEDURE])?
6396+
{
6397+
Keyword::FUNCTION => TriggerExecBodyType::Function,
6398+
Keyword::PROCEDURE => TriggerExecBodyType::Procedure,
6399+
unexpected_keyword => {
6400+
return Err(ParserError::ParserError(format!(
6401+
"Internal parser error: unexpected keyword `{unexpected_keyword}` in trigger exec body"
6402+
)))
6403+
}
6404+
};
6405+
6406+
let func_name = self.parse_object_name(false)?;
6407+
6408+
let args = if self.consume_token(&Token::LParen) {
6409+
if self.consume_token(&Token::RParen) {
6410+
Some(vec![])
6411+
} else {
6412+
let exprs = self.parse_comma_separated(Parser::parse_expr)?;
6413+
self.expect_token(&Token::RParen)?;
6414+
Some(exprs)
6415+
}
6416+
} else {
6417+
None
6418+
};
6419+
63896420
Ok(TriggerExecBody {
6390-
exec_type: match self
6391-
.expect_one_of_keywords(&[Keyword::FUNCTION, Keyword::PROCEDURE])?
6392-
{
6393-
Keyword::FUNCTION => TriggerExecBodyType::Function,
6394-
Keyword::PROCEDURE => TriggerExecBodyType::Procedure,
6395-
unexpected_keyword => return Err(ParserError::ParserError(
6396-
format!("Internal parser error: unexpected keyword `{unexpected_keyword}` in trigger exec body"),
6397-
)),
6398-
},
6399-
func_desc: self.parse_function_desc()?,
6421+
exec_type,
6422+
func_name,
6423+
args,
64006424
})
64016425
}
64026426

tests/sqlparser_mysql.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4222,10 +4222,8 @@ fn parse_create_trigger() {
42224222
condition: None,
42234223
exec_body: Some(TriggerExecBody {
42244224
exec_type: TriggerExecBodyType::Function,
4225-
func_desc: FunctionDesc {
4226-
name: ObjectName::from(vec![Ident::new("emp_stamp")]),
4227-
args: Some(vec![]),
4228-
}
4225+
func_name: ObjectName::from(vec![Ident::new("emp_stamp")]),
4226+
args: Some(vec![]),
42294227
}),
42304228
statements_as: false,
42314229
statements: None,

tests/sqlparser_postgres.rs

Lines changed: 84 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6418,10 +6418,8 @@ fn parse_create_simple_before_insert_trigger() {
64186418
condition: None,
64196419
exec_body: Some(TriggerExecBody {
64206420
exec_type: TriggerExecBodyType::Function,
6421-
func_desc: FunctionDesc {
6422-
name: ObjectName::from(vec![Ident::new("check_account_insert")]),
6423-
args: None,
6424-
},
6421+
func_name: ObjectName::from(vec![Ident::new("check_account_insert")]),
6422+
args: None,
64256423
}),
64266424
statements_as: false,
64276425
statements: None,
@@ -6457,10 +6455,8 @@ fn parse_create_after_update_trigger_with_condition() {
64576455
}))),
64586456
exec_body: Some(TriggerExecBody {
64596457
exec_type: TriggerExecBodyType::Function,
6460-
func_desc: FunctionDesc {
6461-
name: ObjectName::from(vec![Ident::new("check_account_update")]),
6462-
args: None,
6463-
},
6458+
func_name: ObjectName::from(vec![Ident::new("check_account_update")]),
6459+
args: None,
64646460
}),
64656461
statements_as: false,
64666462
statements: None,
@@ -6489,10 +6485,8 @@ fn parse_create_instead_of_delete_trigger() {
64896485
condition: None,
64906486
exec_body: Some(TriggerExecBody {
64916487
exec_type: TriggerExecBodyType::Function,
6492-
func_desc: FunctionDesc {
6493-
name: ObjectName::from(vec![Ident::new("check_account_deletes")]),
6494-
args: None,
6495-
},
6488+
func_name: ObjectName::from(vec![Ident::new("check_account_deletes")]),
6489+
args: None,
64966490
}),
64976491
statements_as: false,
64986492
statements: None,
@@ -6525,10 +6519,8 @@ fn parse_create_trigger_with_multiple_events_and_deferrable() {
65256519
condition: None,
65266520
exec_body: Some(TriggerExecBody {
65276521
exec_type: TriggerExecBodyType::Function,
6528-
func_desc: FunctionDesc {
6529-
name: ObjectName::from(vec![Ident::new("check_account_changes")]),
6530-
args: None,
6531-
},
6522+
func_name: ObjectName::from(vec![Ident::new("check_account_changes")]),
6523+
args: None,
65326524
}),
65336525
statements_as: false,
65346526
statements: None,
@@ -6572,10 +6564,80 @@ fn parse_create_trigger_with_referencing() {
65726564
condition: None,
65736565
exec_body: Some(TriggerExecBody {
65746566
exec_type: TriggerExecBodyType::Function,
6575-
func_desc: FunctionDesc {
6576-
name: ObjectName::from(vec![Ident::new("check_account_referencing")]),
6577-
args: None,
6578-
},
6567+
func_name: ObjectName::from(vec![Ident::new("check_account_referencing")]),
6568+
args: None,
6569+
}),
6570+
statements_as: false,
6571+
statements: None,
6572+
characteristics: None,
6573+
});
6574+
6575+
assert_eq!(pg().verified_stmt(sql), expected);
6576+
}
6577+
6578+
#[test]
6579+
fn parse_create_trigger_with_string_literal_args() {
6580+
// Verify that EXECUTE FUNCTION accepts call-site expression arguments
6581+
// (e.g. string literals), not just bare identifiers. This matches the
6582+
// PostgreSQL tsvector_update_trigger calling convention used in pagila.
6583+
let sql = "CREATE TRIGGER film_fulltext_trigger BEFORE INSERT OR UPDATE ON public.film FOR EACH ROW EXECUTE FUNCTION tsvector_update_trigger('fulltext', 'pg_catalog.english', 'title', 'description')";
6584+
let expected = Statement::CreateTrigger(CreateTrigger {
6585+
or_alter: false,
6586+
temporary: false,
6587+
or_replace: false,
6588+
is_constraint: false,
6589+
name: ObjectName::from(vec![Ident::new("film_fulltext_trigger")]),
6590+
period: Some(TriggerPeriod::Before),
6591+
period_before_table: true,
6592+
events: vec![TriggerEvent::Insert, TriggerEvent::Update(vec![])],
6593+
table_name: ObjectName::from(vec![
6594+
Ident::new("public"),
6595+
Ident::new("film"),
6596+
]),
6597+
referenced_table_name: None,
6598+
referencing: vec![],
6599+
trigger_object: Some(TriggerObjectKind::ForEach(TriggerObject::Row)),
6600+
condition: None,
6601+
exec_body: Some(TriggerExecBody {
6602+
exec_type: TriggerExecBodyType::Function,
6603+
func_name: ObjectName::from(vec![Ident::new("tsvector_update_trigger")]),
6604+
args: Some(vec![
6605+
Expr::Value(Value::SingleQuotedString("fulltext".to_string()).with_empty_span()),
6606+
Expr::Value(Value::SingleQuotedString("pg_catalog.english".to_string()).with_empty_span()),
6607+
Expr::Value(Value::SingleQuotedString("title".to_string()).with_empty_span()),
6608+
Expr::Value(Value::SingleQuotedString("description".to_string()).with_empty_span()),
6609+
]),
6610+
}),
6611+
statements_as: false,
6612+
statements: None,
6613+
characteristics: None,
6614+
});
6615+
6616+
assert_eq!(pg().verified_stmt(sql), expected);
6617+
}
6618+
6619+
#[test]
6620+
fn parse_create_trigger_with_empty_args() {
6621+
// EXECUTE FUNCTION fn() — explicit empty parens
6622+
let sql = "CREATE TRIGGER trg BEFORE INSERT ON t FOR EACH ROW EXECUTE FUNCTION my_fn()";
6623+
let expected = Statement::CreateTrigger(CreateTrigger {
6624+
or_alter: false,
6625+
temporary: false,
6626+
or_replace: false,
6627+
is_constraint: false,
6628+
name: ObjectName::from(vec![Ident::new("trg")]),
6629+
period: Some(TriggerPeriod::Before),
6630+
period_before_table: true,
6631+
events: vec![TriggerEvent::Insert],
6632+
table_name: ObjectName::from(vec![Ident::new("t")]),
6633+
referenced_table_name: None,
6634+
referencing: vec![],
6635+
trigger_object: Some(TriggerObjectKind::ForEach(TriggerObject::Row)),
6636+
condition: None,
6637+
exec_body: Some(TriggerExecBody {
6638+
exec_type: TriggerExecBodyType::Function,
6639+
func_name: ObjectName::from(vec![Ident::new("my_fn")]),
6640+
args: Some(vec![]),
65796641
}),
65806642
statements_as: false,
65816643
statements: None,
@@ -6883,10 +6945,8 @@ fn parse_trigger_related_functions() {
68836945
condition: None,
68846946
exec_body: Some(TriggerExecBody {
68856947
exec_type: TriggerExecBodyType::Function,
6886-
func_desc: FunctionDesc {
6887-
name: ObjectName::from(vec![Ident::new("emp_stamp")]),
6888-
args: Some(vec![]),
6889-
}
6948+
func_name: ObjectName::from(vec![Ident::new("emp_stamp")]),
6949+
args: Some(vec![]),
68906950
}),
68916951
statements_as: false,
68926952
statements: None,

0 commit comments

Comments
 (0)