use std::collections::BTreeMap;
use pg_query::NodeEnum;
use pg_query::protobuf::{AlterEventTrigStmt, CreateEventTrigStmt};
use crate::identifier::Identifier;
use crate::ir::event_trigger::{EventTrigger, EventTriggerEnabled, EventTriggerEvent};
use crate::parse::builder::shared;
use crate::parse::error::{ParseError, SourceLocation};
pub(crate) fn parse_create_event_trigger(
stmt: &CreateEventTrigStmt,
default_schema: Option<&Identifier>,
source_loc: SourceLocation,
existing: &mut BTreeMap<Identifier, EventTrigger>,
) -> Result<(), ParseError> {
let name = Identifier::from_unquoted(&stmt.trigname)
.map_err(|e| ParseError::InvalidIdentifier(stmt.trigname.clone(), e.to_string()))?;
if existing.contains_key(&name) {
return Err(ParseError::DuplicateEventTrigger(name, source_loc));
}
let event = EventTriggerEvent::from_sql(&stmt.eventname.to_lowercase()).ok_or_else(|| {
ParseError::UnknownEventTriggerEvent(
stmt.eventname.clone(),
name.clone(),
source_loc.clone(),
)
})?;
let tag_filter = extract_tag_filter(&stmt.whenclause, &name, &source_loc)?;
let function = shared::qname_from_string_list(&stmt.funcname, default_schema, &source_loc)?;
existing.insert(
name.clone(),
EventTrigger {
name,
event,
tag_filter,
function,
enabled: EventTriggerEnabled::Enabled,
owner: None,
comment: None,
},
);
Ok(())
}
pub(crate) fn parse_alter_event_trigger(
stmt: &AlterEventTrigStmt,
source_loc: SourceLocation,
existing: &mut BTreeMap<Identifier, EventTrigger>,
) -> Result<(), ParseError> {
let name = Identifier::from_unquoted(&stmt.trigname)
.map_err(|e| ParseError::InvalidIdentifier(stmt.trigname.clone(), e.to_string()))?;
let et = existing.get_mut(&name).ok_or_else(|| {
ParseError::AlterEventTriggerBeforeCreate(name.clone(), source_loc.clone())
})?;
let code = stmt.tgenabled.chars().next().ok_or_else(|| {
ParseError::UnknownEventTriggerEnabled(
stmt.tgenabled.clone(),
name.clone(),
source_loc.clone(),
)
})?;
let enabled = EventTriggerEnabled::from_pg_char(code).ok_or_else(|| {
ParseError::UnknownEventTriggerEnabled(stmt.tgenabled.clone(), name.clone(), source_loc)
})?;
et.enabled = enabled;
Ok(())
}
pub(crate) fn apply_event_trigger_comment(
stmt: &pg_query::protobuf::CommentStmt,
location: &SourceLocation,
existing: &mut BTreeMap<Identifier, EventTrigger>,
) -> Result<(), ParseError> {
let name = string_object_name(stmt.object.as_deref(), location)?;
let comment = if stmt.comment.is_empty() {
None
} else {
Some(stmt.comment.clone())
};
let et = existing.get_mut(&name).ok_or_else(|| {
ParseError::CommentOnEventTriggerBeforeCreate(name.clone(), location.clone())
})?;
et.comment = comment;
Ok(())
}
pub(crate) fn apply_event_trigger_owner(
stmt: &pg_query::protobuf::AlterOwnerStmt,
location: &SourceLocation,
existing: &mut BTreeMap<Identifier, EventTrigger>,
) -> Result<(), ParseError> {
let name = string_object_name(stmt.object.as_deref(), location)?;
let new_owner = crate::parse::builder::owner_stmt::extract_new_owner(stmt, location)?;
let et = existing
.get_mut(&name)
.ok_or_else(|| ParseError::EventTriggerOwnerBeforeCreate(name.clone(), location.clone()))?;
et.owner = Some(new_owner);
Ok(())
}
fn extract_tag_filter(
whenclause: &[pg_query::protobuf::Node],
name: &Identifier,
loc: &SourceLocation,
) -> Result<Vec<String>, ParseError> {
let mut tags: Vec<String> = Vec::new();
for node in whenclause {
let Some(NodeEnum::DefElem(def)) = node.node.as_ref() else {
return Err(ParseError::EventTriggerWhenClauseMalformed(
name.clone(),
loc.clone(),
));
};
if def.defname != "tag" {
return Err(ParseError::EventTriggerWhenClauseMalformed(
name.clone(),
loc.clone(),
));
}
let Some(NodeEnum::List(list)) = def.arg.as_ref().and_then(|a| a.node.as_ref()) else {
return Err(ParseError::EventTriggerWhenClauseMalformed(
name.clone(),
loc.clone(),
));
};
for item in &list.items {
match item.node.as_ref() {
Some(NodeEnum::String(s)) => tags.push(s.sval.clone()),
_ => {
return Err(ParseError::EventTriggerWhenClauseMalformed(
name.clone(),
loc.clone(),
));
}
}
}
}
Ok(tags)
}
fn string_object_name(
object: Option<&pg_query::protobuf::Node>,
loc: &SourceLocation,
) -> Result<Identifier, ParseError> {
let node = object
.and_then(|o| o.node.as_ref())
.ok_or_else(|| ParseError::Structural {
location: loc.clone(),
message: "EVENT TRIGGER reference missing object name".into(),
})?;
match node {
NodeEnum::String(s) => Identifier::from_unquoted(&s.sval)
.map_err(|e| ParseError::InvalidIdentifier(s.sval.clone(), e.to_string())),
other => Err(ParseError::Structural {
location: loc.clone(),
message: format!(
"expected String node for event-trigger name, got {:?}",
std::mem::discriminant(other)
),
}),
}
}
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
use tempfile::tempdir;
use super::*;
use crate::ir::catalog::Catalog;
use crate::parse::parse_directory;
fn write(dir: &Path, rel: &str, contents: &str) {
let p = dir.join(rel);
if let Some(parent) = p.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(p, contents).unwrap();
}
fn parse_source(sql: &str) -> Result<Catalog, ParseError> {
let tmp = tempdir().expect("tempdir");
write(tmp.path(), "schema.sql", sql);
parse_directory(tmp.path(), &[])
}
fn loc() -> SourceLocation {
SourceLocation::new(PathBuf::from("test.sql"), 1, 1)
}
fn parse_one_create(sql: &str) -> CreateEventTrigStmt {
let parsed = pg_query::parse(sql).expect("pg_query parse");
let node = parsed
.protobuf
.stmts
.into_iter()
.next()
.and_then(|r| r.stmt)
.and_then(|n| n.node)
.expect("stmt");
let NodeEnum::CreateEventTrigStmt(s) = node else {
panic!("expected CreateEventTrigStmt");
};
s
}
fn parse_one_alter(sql: &str) -> AlterEventTrigStmt {
let parsed = pg_query::parse(sql).expect("pg_query parse");
let node = parsed
.protobuf
.stmts
.into_iter()
.next()
.and_then(|r| r.stmt)
.and_then(|n| n.node)
.expect("stmt");
let NodeEnum::AlterEventTrigStmt(s) = node else {
panic!("expected AlterEventTrigStmt");
};
s
}
#[test]
fn create_simple() {
let stmt = parse_one_create(
"CREATE EVENT TRIGGER e ON ddl_command_start EXECUTE FUNCTION app.f();",
);
let mut acc: BTreeMap<Identifier, EventTrigger> = BTreeMap::new();
parse_create_event_trigger(&stmt, None, loc(), &mut acc).expect("ok");
let et = acc.values().next().unwrap();
assert_eq!(et.name.as_str(), "e");
assert_eq!(et.event, EventTriggerEvent::DdlCommandStart);
assert!(et.tag_filter.is_empty());
assert_eq!(et.function.schema.as_str(), "app");
assert_eq!(et.function.name.as_str(), "f");
assert_eq!(et.enabled, EventTriggerEnabled::Enabled);
assert!(et.owner.is_none());
assert!(et.comment.is_none());
}
#[test]
fn create_unqualified_function_uses_default_schema() {
let stmt = parse_one_create("CREATE EVENT TRIGGER e ON sql_drop EXECUTE FUNCTION f();");
let mut acc: BTreeMap<Identifier, EventTrigger> = BTreeMap::new();
let app = Identifier::from_unquoted("app").unwrap();
parse_create_event_trigger(&stmt, Some(&app), loc(), &mut acc).expect("ok");
let et = acc.values().next().unwrap();
assert_eq!(et.function.schema.as_str(), "app");
assert_eq!(et.function.name.as_str(), "f");
assert_eq!(et.event, EventTriggerEvent::SqlDrop);
}
#[test]
fn create_with_tag_filter() {
let stmt = parse_one_create(
"CREATE EVENT TRIGGER e ON ddl_command_end \
WHEN TAG IN ('CREATE TABLE', 'CREATE INDEX') EXECUTE FUNCTION public.f();",
);
let mut acc: BTreeMap<Identifier, EventTrigger> = BTreeMap::new();
parse_create_event_trigger(&stmt, None, loc(), &mut acc).expect("ok");
let et = acc.values().next().unwrap();
assert_eq!(et.event, EventTriggerEvent::DdlCommandEnd);
assert_eq!(
et.tag_filter,
vec!["CREATE TABLE".to_string(), "CREATE INDEX".to_string()]
);
}
#[test]
fn create_duplicate_name_errors() {
let stmt =
parse_one_create("CREATE EVENT TRIGGER e ON sql_drop EXECUTE FUNCTION public.f();");
let mut acc: BTreeMap<Identifier, EventTrigger> = BTreeMap::new();
parse_create_event_trigger(&stmt, None, loc(), &mut acc).expect("first ok");
let err = parse_create_event_trigger(&stmt, None, loc(), &mut acc).unwrap_err();
assert!(
matches!(err, ParseError::DuplicateEventTrigger(_, _)),
"got: {err:?}"
);
}
#[test]
fn create_unknown_event_errors() {
let stmt = parse_one_create("CREATE EVENT TRIGGER e ON login EXECUTE FUNCTION public.f();");
let mut acc: BTreeMap<Identifier, EventTrigger> = BTreeMap::new();
let err = parse_create_event_trigger(&stmt, None, loc(), &mut acc).unwrap_err();
assert!(
matches!(err, ParseError::UnknownEventTriggerEvent(ref s, _, _) if s == "login"),
"got: {err:?}"
);
}
#[test]
fn alter_disable() {
let mut acc: BTreeMap<Identifier, EventTrigger> = BTreeMap::new();
let create =
parse_one_create("CREATE EVENT TRIGGER e ON sql_drop EXECUTE FUNCTION public.f();");
parse_create_event_trigger(&create, None, loc(), &mut acc).expect("create ok");
let alter = parse_one_alter("ALTER EVENT TRIGGER e DISABLE;");
parse_alter_event_trigger(&alter, loc(), &mut acc).expect("alter ok");
assert_eq!(
acc.values().next().unwrap().enabled,
EventTriggerEnabled::Disabled
);
}
#[test]
fn alter_enable_replica() {
let mut acc: BTreeMap<Identifier, EventTrigger> = BTreeMap::new();
let create =
parse_one_create("CREATE EVENT TRIGGER e ON sql_drop EXECUTE FUNCTION public.f();");
parse_create_event_trigger(&create, None, loc(), &mut acc).expect("create ok");
let alter = parse_one_alter("ALTER EVENT TRIGGER e ENABLE REPLICA;");
parse_alter_event_trigger(&alter, loc(), &mut acc).expect("alter ok");
assert_eq!(
acc.values().next().unwrap().enabled,
EventTriggerEnabled::Replica
);
}
#[test]
fn alter_before_create_errors() {
let alter = parse_one_alter("ALTER EVENT TRIGGER e DISABLE;");
let mut acc: BTreeMap<Identifier, EventTrigger> = BTreeMap::new();
let err = parse_alter_event_trigger(&alter, loc(), &mut acc).unwrap_err();
assert!(
matches!(err, ParseError::AlterEventTriggerBeforeCreate(_, _)),
"got: {err:?}"
);
}
#[test]
fn comment_applies() {
let sql = "
CREATE EVENT TRIGGER e ON sql_drop EXECUTE FUNCTION public.f();
COMMENT ON EVENT TRIGGER e IS 'audit drops';
";
let cat = parse_source(sql).expect("parses");
assert_eq!(cat.event_triggers.len(), 1);
assert_eq!(
cat.event_triggers[0].comment.as_deref(),
Some("audit drops")
);
}
#[test]
fn owner_applies() {
let sql = "
CREATE EVENT TRIGGER e ON sql_drop EXECUTE FUNCTION public.f();
ALTER EVENT TRIGGER e OWNER TO alice;
";
let cat = parse_source(sql).expect("parses");
assert_eq!(cat.event_triggers.len(), 1);
assert_eq!(
cat.event_triggers[0].owner,
Some(Identifier::from_unquoted("alice").unwrap())
);
}
#[test]
fn rename_rejected() {
let sql = "ALTER EVENT TRIGGER e RENAME TO f;";
let err = parse_source(sql).expect_err("should fail");
assert!(
matches!(err, ParseError::EventTriggerRenameNotSupported(_, _)),
"got: {err:?}"
);
}
#[test]
fn parse_directory_folds_create_and_alter() {
let sql = "
CREATE EVENT TRIGGER guard ON ddl_command_start
WHEN TAG IN ('DROP TABLE') EXECUTE FUNCTION public.block();
ALTER EVENT TRIGGER guard ENABLE ALWAYS;
";
let cat = parse_source(sql).expect("parses");
assert_eq!(cat.event_triggers.len(), 1);
let et = &cat.event_triggers[0];
assert_eq!(et.name.as_str(), "guard");
assert_eq!(et.event, EventTriggerEvent::DdlCommandStart);
assert_eq!(et.tag_filter, vec!["DROP TABLE".to_string()]);
assert_eq!(et.function.schema.as_str(), "public");
assert_eq!(et.function.name.as_str(), "block");
assert_eq!(et.enabled, EventTriggerEnabled::Always);
}
}