From d65dd04387ae1207cba730d881e88ce23780b68d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Stefan=20Gr=C3=B6nke?= <stefan@gronke.net>
Date: Fri, 4 Oct 2024 12:24:36 +0200
Subject: [PATCH 1/2] draft triggers create and drop

---
 src/backend/mod.rs                 |   6 +-
 src/backend/mysql/mod.rs           |   1 +
 src/backend/mysql/trigger.rs       |   3 +
 src/backend/postgres/mod.rs        |   1 +
 src/backend/postgres/trigger.rs    |   3 +
 src/backend/sqlite/mod.rs          |   1 +
 src/backend/sqlite/trigger.rs      |   3 +
 src/backend/trigger_builder.rs     |  48 ++++
 src/backend/trigger_ref_builder.rs |  12 +
 src/lib.rs                         |   2 +
 src/trigger/create.rs              |  40 ++++
 src/trigger/drop.rs                |  99 ++++++++
 src/trigger/mod.rs                 | 353 +++++++++++++++++++++++++++++
 tests/mysql/mod.rs                 |   1 +
 tests/mysql/trigger.rs             |  60 +++++
 15 files changed, 632 insertions(+), 1 deletion(-)
 create mode 100644 src/backend/mysql/trigger.rs
 create mode 100644 src/backend/postgres/trigger.rs
 create mode 100644 src/backend/sqlite/trigger.rs
 create mode 100644 src/backend/trigger_builder.rs
 create mode 100644 src/backend/trigger_ref_builder.rs
 create mode 100644 src/trigger/create.rs
 create mode 100644 src/trigger/drop.rs
 create mode 100644 src/trigger/mod.rs
 create mode 100644 tests/mysql/trigger.rs

diff --git a/src/backend/mod.rs b/src/backend/mod.rs
index b918e8544..de97419b1 100644
--- a/src/backend/mod.rs
+++ b/src/backend/mod.rs
@@ -24,16 +24,20 @@ mod index_builder;
 mod query_builder;
 mod table_builder;
 mod table_ref_builder;
+mod trigger_builder;
+// mod trigger_ref_builder;
 
 pub use self::foreign_key_builder::*;
 pub use self::index_builder::*;
 pub use self::query_builder::*;
 pub use self::table_builder::*;
 pub use self::table_ref_builder::*;
+pub use self::trigger_builder::*;
+// pub use self::trigger_ref_builder::*;
 
 pub trait GenericBuilder: QueryBuilder + SchemaBuilder {}
 
-pub trait SchemaBuilder: TableBuilder + IndexBuilder + ForeignKeyBuilder {}
+pub trait SchemaBuilder: TableBuilder + IndexBuilder + ForeignKeyBuilder + TriggerBuilder {}
 
 pub trait QuotedBuilder {
     /// The type of quote the builder uses.
diff --git a/src/backend/mysql/mod.rs b/src/backend/mysql/mod.rs
index 4f972e9ed..f718509b2 100644
--- a/src/backend/mysql/mod.rs
+++ b/src/backend/mysql/mod.rs
@@ -2,6 +2,7 @@ pub(crate) mod foreign_key;
 pub(crate) mod index;
 pub(crate) mod query;
 pub(crate) mod table;
+pub(crate) mod trigger;
 
 use super::*;
 
diff --git a/src/backend/mysql/trigger.rs b/src/backend/mysql/trigger.rs
new file mode 100644
index 000000000..7b5aef026
--- /dev/null
+++ b/src/backend/mysql/trigger.rs
@@ -0,0 +1,3 @@
+use super::*;
+
+impl TriggerBuilder for MysqlQueryBuilder {}
diff --git a/src/backend/postgres/mod.rs b/src/backend/postgres/mod.rs
index 6d6bf51e9..12df51709 100644
--- a/src/backend/postgres/mod.rs
+++ b/src/backend/postgres/mod.rs
@@ -3,6 +3,7 @@ pub(crate) mod foreign_key;
 pub(crate) mod index;
 pub(crate) mod query;
 pub(crate) mod table;
+pub(crate) mod trigger;
 pub(crate) mod types;
 
 use super::*;
diff --git a/src/backend/postgres/trigger.rs b/src/backend/postgres/trigger.rs
new file mode 100644
index 000000000..6e4e2e561
--- /dev/null
+++ b/src/backend/postgres/trigger.rs
@@ -0,0 +1,3 @@
+use super::*;
+
+impl TriggerBuilder for PostgresQueryBuilder {}
diff --git a/src/backend/sqlite/mod.rs b/src/backend/sqlite/mod.rs
index b884cc5ed..a187fc2b0 100644
--- a/src/backend/sqlite/mod.rs
+++ b/src/backend/sqlite/mod.rs
@@ -2,6 +2,7 @@ pub(crate) mod foreign_key;
 pub(crate) mod index;
 pub(crate) mod query;
 pub(crate) mod table;
+pub(crate) mod trigger;
 
 use super::*;
 
diff --git a/src/backend/sqlite/trigger.rs b/src/backend/sqlite/trigger.rs
new file mode 100644
index 000000000..a72ee5104
--- /dev/null
+++ b/src/backend/sqlite/trigger.rs
@@ -0,0 +1,3 @@
+use super::*;
+
+impl TriggerBuilder for SqliteQueryBuilder {}
diff --git a/src/backend/trigger_builder.rs b/src/backend/trigger_builder.rs
new file mode 100644
index 000000000..7a66377f6
--- /dev/null
+++ b/src/backend/trigger_builder.rs
@@ -0,0 +1,48 @@
+use crate::*;
+
+pub trait TriggerBuilder: TableRefBuilder {
+    /// Translate [`TriggerCreateStatement`] into SQL statement.
+    fn prepare_trigger_create_statement(
+        &self,
+        create: &TriggerCreateStatement,
+        sql: &mut dyn SqlWriter,
+    ) {
+        write!(sql, "CREATE TRIGGER ").unwrap();
+        self.prepare_create_trigger_if_not_exists(create, sql);
+
+        let trigger_ref = match &create.trigger.name {
+            Some(value) => value,
+            // auto-generate trigger name
+            _ => &create.trigger.trigger_ref(),
+        };
+        let trigger_ref: TableRef = trigger_ref.into();
+        self.prepare_table_ref_iden(&trigger_ref, sql);
+        write!(sql, " {} {} ON ", create.trigger.time, create.trigger.event).unwrap();
+        self.prepare_table_ref_iden(&create.trigger.table, sql);
+        write!(sql, " FOR EACH ROW\nBEGIN\n").unwrap();
+
+        write!(sql, "\nEND").unwrap();
+    }
+
+    /// Translate IF NOT EXISTS expression in [`TriggerCreateStatement`].
+    fn prepare_create_trigger_if_not_exists(
+        &self,
+        create: &TriggerCreateStatement,
+        sql: &mut dyn SqlWriter,
+    ) {
+        if create.if_not_exists {
+            write!(sql, "IF NOT EXISTS ").unwrap();
+        }
+    }
+
+    // /// Translate [`TriggerRef`] into SQL statement.
+    // fn prepare_table_ref(&self, trigger_ref: &TableRef, sql: &mut dyn SqlWriter) {
+    //     self.prepare_table_ref_iden(trigger_ref, sql)
+    // }
+
+    /// Translate [`TriggerDropStatement`] into SQL statement.
+    fn prepare_trigger_drop_statement(&self, drop: &TriggerDropStatement, sql: &mut dyn SqlWriter) {
+        write!(sql, "DROP TRIGGER ").unwrap();
+        self.prepare_table_ref_iden(&drop.name.clone().into(), sql);
+    }
+}
diff --git a/src/backend/trigger_ref_builder.rs b/src/backend/trigger_ref_builder.rs
new file mode 100644
index 000000000..4ca847800
--- /dev/null
+++ b/src/backend/trigger_ref_builder.rs
@@ -0,0 +1,12 @@
+use crate::*;
+
+pub trait TriggerRefBuilder: QuotedBuilder {
+    /// Translate [`TriggerRef`] that without values into SQL statement.
+    fn prepare_trigger_ref_iden(&self, table_ref: &TriggerRef, sql: &mut dyn SqlWriter) {
+        match table_ref {
+            TriggerRef::Trigger(iden) => {
+                iden.prepare(sql.as_writer(), self.quote());
+            }
+        }
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 15e1a189c..2c6355b4d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -824,6 +824,7 @@ pub mod query;
 pub mod schema;
 pub mod table;
 pub mod token;
+pub mod trigger;
 pub mod types;
 pub mod value;
 
@@ -841,6 +842,7 @@ pub use query::*;
 pub use schema::*;
 pub use table::*;
 pub use token::*;
+pub use trigger::*;
 pub use types::*;
 pub use value::*;
 
diff --git a/src/trigger/create.rs b/src/trigger/create.rs
new file mode 100644
index 000000000..02711069f
--- /dev/null
+++ b/src/trigger/create.rs
@@ -0,0 +1,40 @@
+use super::DefinedTrigger;
+use crate::{backend::SchemaBuilder, SchemaStatementBuilder};
+use inherent::inherent;
+
+#[derive(Debug, Clone)]
+pub struct TriggerCreateStatement {
+    pub(crate) trigger: DefinedTrigger,
+    pub(crate) if_not_exists: bool,
+}
+
+impl TriggerCreateStatement {
+    pub fn new(trigger: DefinedTrigger) -> Self {
+        TriggerCreateStatement {
+            trigger,
+            if_not_exists: false,
+        }
+    }
+
+    pub fn if_not_exists(&mut self) -> &mut Self {
+        self.if_not_exists = true;
+        self
+    }
+}
+
+#[inherent]
+impl SchemaStatementBuilder for TriggerCreateStatement {
+    pub fn build<T: SchemaBuilder>(&self, schema_builder: T) -> String {
+        let mut sql = String::with_capacity(256);
+        schema_builder.prepare_trigger_create_statement(self, &mut sql);
+        sql
+    }
+
+    pub fn build_any(&self, schema_builder: &dyn SchemaBuilder) -> String {
+        let mut sql = String::with_capacity(256);
+        schema_builder.prepare_trigger_create_statement(self, &mut sql);
+        sql
+    }
+
+    pub fn to_string<T: SchemaBuilder>(&self, schema_builder: T) -> String;
+}
diff --git a/src/trigger/drop.rs b/src/trigger/drop.rs
new file mode 100644
index 000000000..f51e531bc
--- /dev/null
+++ b/src/trigger/drop.rs
@@ -0,0 +1,99 @@
+use super::TriggerRef;
+use crate::{backend::SchemaBuilder, SchemaStatementBuilder};
+use inherent::inherent;
+
+/// Drop a trigger
+///
+/// # Examples
+///
+/// ```
+/// use sea_query::{tests_cfg::*, *};
+///
+/// let trigger = NamedTrigger::new("my_trigger")
+///     .to_owned();
+///
+/// let drop_stmt = trigger.drop();
+///
+/// assert_eq!(
+///     drop_stmt.to_string(MysqlQueryBuilder),
+///     r#"DROP TRIGGER `my_trigger`"#
+/// );
+/// assert_eq!(
+///     drop_stmt.to_string(PostgresQueryBuilder),
+///     r#"DROP TRIGGER "my_trigger""#
+/// );
+/// assert_eq!(
+///     drop_stmt.to_string(SqliteQueryBuilder),
+///     r#"DROP TRIGGER "my_trigger""#
+/// );
+/// ```
+///
+/// # Trigger names can be derived from table name, action and action time
+///
+/// ```
+/// use sea_query::{tests_cfg::*, *};
+///
+/// let trigger = UnnamedTrigger::new()
+///     .before_insert(Glyph::Table);
+///
+/// let drop_stmt = trigger.drop();
+///
+/// assert_eq!(
+///     drop_stmt.to_string(MysqlQueryBuilder),
+///     r#"DROP TRIGGER `t_glyph_before_insert`"#
+/// );
+/// assert_eq!(
+///     drop_stmt.to_string(PostgresQueryBuilder),
+///     r#"DROP TRIGGER "t_glyph_before_insert""#
+/// );
+/// assert_eq!(
+///     drop_stmt.to_string(SqliteQueryBuilder),
+///     r#"DROP TRIGGER "t_glyph_before_insert""#
+/// );
+///
+/// ```
+#[derive(Debug, Clone)]
+pub struct TriggerDropStatement {
+    pub(crate) name: TriggerRef,
+    pub(crate) if_exists: bool,
+}
+
+impl TriggerDropStatement {
+    /// Construct drop table statement
+    pub fn new(name: TriggerRef) -> Self {
+        Self {
+            name: name,
+            if_exists: false,
+        }
+    }
+
+    /// Drop table if exists
+    pub fn if_exists(&mut self) -> &mut Self {
+        self.if_exists = true;
+        self
+    }
+
+    pub fn take(&mut self) -> Self {
+        Self {
+            name: std::mem::take(&mut self.name),
+            if_exists: self.if_exists,
+        }
+    }
+}
+
+#[inherent]
+impl SchemaStatementBuilder for TriggerDropStatement {
+    pub fn build<T: SchemaBuilder>(&self, schema_builder: T) -> String {
+        let mut sql = String::with_capacity(256);
+        schema_builder.prepare_trigger_drop_statement(self, &mut sql);
+        sql
+    }
+
+    pub fn build_any(&self, schema_builder: &dyn SchemaBuilder) -> String {
+        let mut sql = String::with_capacity(256);
+        schema_builder.prepare_trigger_drop_statement(self, &mut sql);
+        sql
+    }
+
+    pub fn to_string<T: SchemaBuilder>(&self, schema_builder: T) -> String;
+}
diff --git a/src/trigger/mod.rs b/src/trigger/mod.rs
new file mode 100644
index 000000000..5c6b56662
--- /dev/null
+++ b/src/trigger/mod.rs
@@ -0,0 +1,353 @@
+use crate::{Iden, IntoTableRef, SchemaBuilder, SeaRc, TableRef};
+use std::fmt;
+
+mod create;
+mod drop;
+
+pub use create::*;
+pub use drop::*;
+
+pub trait Referencable {
+    fn trigger_ref(&self) -> TriggerRef;
+    fn trigger_name(&self) -> String;
+}
+pub trait Droppable: Referencable {
+    fn drop(&self) -> TriggerDropStatement {
+        TriggerDropStatement::new(self.trigger_ref())
+    }
+}
+
+pub trait Creatable: Referencable {
+    fn create(&self) -> TriggerCreateStatement;
+}
+
+pub trait Configurable {
+    fn configure(
+        &self,
+        table: TableRef,
+        event: TriggerEvent,
+        time: TriggerActionTime,
+    ) -> DefinedTrigger;
+    fn before_insert<T: IntoTableRef>(&self, table: T) -> DefinedTrigger {
+        self.configure(
+            table.into_table_ref(),
+            TriggerEvent::Insert,
+            TriggerActionTime::Before,
+        )
+    }
+    fn after_insert<T: IntoTableRef>(&self, table: T) -> DefinedTrigger {
+        self.configure(
+            table.into_table_ref(),
+            TriggerEvent::Insert,
+            TriggerActionTime::After,
+        )
+    }
+    fn before_update<T: IntoTableRef>(&self, table: T) -> DefinedTrigger {
+        self.configure(
+            table.into_table_ref(),
+            TriggerEvent::Update,
+            TriggerActionTime::Before,
+        )
+    }
+    fn after_update<T: IntoTableRef>(&self, table: T) -> DefinedTrigger {
+        self.configure(
+            table.into_table_ref(),
+            TriggerEvent::Update,
+            TriggerActionTime::After,
+        )
+    }
+    fn before_delete<T: IntoTableRef>(&self, table: T) -> DefinedTrigger {
+        self.configure(
+            table.into_table_ref(),
+            TriggerEvent::Delete,
+            TriggerActionTime::Before,
+        )
+    }
+    fn after_delete<T: IntoTableRef>(&self, table: T) -> DefinedTrigger {
+        self.configure(
+            table.into_table_ref(),
+            TriggerEvent::Delete,
+            TriggerActionTime::After,
+        )
+    }
+}
+
+#[derive(Default, Debug, Clone)]
+pub struct NamedTrigger {
+    pub(crate) name: TriggerRef
+}
+
+impl NamedTrigger {
+    pub fn new<T: Into<TriggerRef>>(name: T) -> NamedTrigger {
+        Self {
+            name: name.into()
+        }
+    }
+}
+
+impl Referencable for NamedTrigger {
+    fn trigger_ref(&self) -> TriggerRef {
+        self.name.clone()
+    }
+    fn trigger_name(&self) -> String {
+        self.name.to_string()
+    }
+}
+
+impl Droppable for NamedTrigger {}
+impl Configurable for NamedTrigger {
+    fn configure(
+        &self,
+        table: TableRef,
+        event: TriggerEvent,
+        time: TriggerActionTime,
+    ) -> DefinedTrigger {
+        DefinedTrigger {
+            name: Some(self.name.clone()),
+            table: table,
+            event: event,
+            time: time,
+        }
+    }
+}
+
+#[derive(Default, Debug, Clone)]
+pub struct UnnamedTrigger {
+    pub(crate) table: Option<TableRef>,
+    pub(crate) event: Option<TriggerEvent>,
+    pub(crate) time: Option<TriggerActionTime>,
+}
+
+impl UnnamedTrigger {
+    pub fn new() -> UnnamedTrigger {
+        Self {
+            table: None,
+            event: None,
+            time: None,
+        }
+    }
+    // an unnamed trigger can become a named one
+    pub fn name<T: Into<TriggerRef>>(&self, name: T) -> NamedTrigger {
+        NamedTrigger {
+            name: name.into()
+        }
+    }
+}
+
+impl Configurable for UnnamedTrigger {
+    fn configure(
+        &self,
+        table: TableRef,
+        event: TriggerEvent,
+        time: TriggerActionTime,
+    ) -> DefinedTrigger {
+        DefinedTrigger {
+            name: None,
+            table: table,
+            event: event,
+            time: time,
+        }
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct DefinedTrigger {
+    pub(crate) name: Option<TriggerRef>,
+    pub(crate) table: TableRef,
+    pub(crate) event: TriggerEvent,
+    pub(crate) time: TriggerActionTime,
+}
+
+impl Referencable for DefinedTrigger {
+    fn trigger_ref(&self) -> TriggerRef {
+        match &self.name {
+            Some(name) => name.clone(),
+            _ => TriggerRef {
+                name: self.trigger_name(),
+            },
+        }
+    }
+    fn trigger_name(&self) -> String {
+        match &self.name {
+            Some(name) => name.to_string(),
+            _ => format!(
+                "t_{}_{}_{}",
+                self.table.to_string().to_lowercase(),
+                self.time.to_string().to_lowercase(),
+                self.event.to_string().to_lowercase(),
+            ),
+        }
+    }
+}
+
+impl Creatable for DefinedTrigger {
+    fn create(&self) -> TriggerCreateStatement {
+        TriggerCreateStatement {
+            trigger: self.clone(),
+            if_not_exists: false,
+        }
+    }
+}
+impl Droppable for DefinedTrigger {}
+
+#[derive(Debug, Clone)]
+pub enum Trigger {
+    UnnamedTrigger(
+        Option<TableRef>,
+        Option<TriggerEvent>,
+        Option<TriggerActionTime>,
+    ),
+    NamedTrigger(
+        TriggerRef,
+        Option<TableRef>,
+        Option<TriggerEvent>,
+        Option<TriggerActionTime>,
+    ),
+    DefinedTrigger(
+        Option<TriggerRef>,
+        TableRef,
+        TriggerEvent,
+        TriggerActionTime,
+    ),
+}
+
+impl Default for Trigger {
+    fn default() -> Self {
+        Trigger::UnnamedTrigger(None, None, None)
+    }
+}
+
+impl Trigger {
+    pub fn new() -> Trigger {
+        Trigger::UnnamedTrigger(None, None, None)
+    }
+
+    pub fn with_name(name: TriggerRef) -> Trigger {
+        Trigger::NamedTrigger(name, None, None, None)
+    }
+}
+
+/// All available types of trigger statement
+#[derive(Debug, Clone)]
+pub enum TriggerStatement {
+    Create(TriggerCreateStatement),
+    Drop(TriggerDropStatement),
+}
+
+#[derive(Debug, Clone)]
+pub enum TriggerEvent {
+    Insert,
+    Update,
+    Delete,
+}
+
+impl fmt::Display for TriggerEvent {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(
+            f,
+            "{}",
+            match self {
+                Self::Insert => "INSERT",
+                Self::Update => "UPDATE",
+                Self::Delete => "DELETE",
+            }
+        )
+    }
+}
+
+#[derive(Debug, Clone)]
+pub enum TriggerActionTime {
+    Before,
+    After,
+}
+
+impl fmt::Display for TriggerActionTime {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(
+            f,
+            "{}",
+            match self {
+                Self::Before => "BEFORE",
+                Self::After => "AFTER",
+            }
+        )
+    }
+}
+
+impl TriggerStatement {
+    /// Build corresponding SQL statement for certain database backend and return SQL string
+    pub fn build<T: SchemaBuilder>(&self, trigger_builder: T) -> String {
+        match self {
+            Self::Create(stat) => stat.build(trigger_builder),
+            Self::Drop(stat) => stat.build(trigger_builder),
+        }
+    }
+
+    /// Build corresponding SQL statement for certain database backend and return SQL string
+    pub fn build_any(&self, trigger_builder: &dyn SchemaBuilder) -> String {
+        match self {
+            Self::Create(stat) => stat.build_any(trigger_builder),
+            Self::Drop(stat) => stat.build_any(trigger_builder),
+        }
+    }
+
+    /// Build corresponding SQL statement for certain database backend and return SQL string
+    pub fn to_string<T: SchemaBuilder>(&self, trigger_builder: T) -> String {
+        match self {
+            Self::Create(stat) => stat.to_string(trigger_builder),
+            Self::Drop(stat) => stat.to_string(trigger_builder),
+        }
+    }
+}
+
+#[derive(Default, Debug, Clone)]
+pub struct TriggerRef {
+    name: String,
+}
+
+impl Iden for TriggerRef {
+    fn unquoted(&self, s: &mut dyn fmt::Write) {
+        s.write_str(&self.name).unwrap();
+    }
+}
+
+impl From<String> for TriggerRef {
+    fn from(value: String) -> Self {
+        Self { name: value }
+    }
+}
+
+impl From<&str> for TriggerRef {
+    fn from(value: &str) -> Self {
+        Self {
+            name: value.to_string(),
+        }
+    }
+}
+
+impl Into<TableRef> for TriggerRef {
+    fn into(self) -> TableRef {
+        TableRef::Table(SeaRc::new(self))
+    }
+}
+
+impl From<&TriggerRef> for TableRef {
+    fn from(value: &TriggerRef) -> Self {
+        TableRef::Table(SeaRc::new(value.clone()))
+    }
+}
+
+impl fmt::Display for TableRef {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(
+            f,
+            "{}",
+            match self {
+                TableRef::Table(iden) => {
+                    iden.to_string()
+                }
+                _ => "bar".to_string(),
+            }
+        )
+    }
+}
diff --git a/tests/mysql/mod.rs b/tests/mysql/mod.rs
index d717774f1..f250585f1 100644
--- a/tests/mysql/mod.rs
+++ b/tests/mysql/mod.rs
@@ -4,6 +4,7 @@ mod foreign_key;
 mod index;
 mod query;
 mod table;
+mod trigger;
 
 #[path = "../common.rs"]
 mod common;
diff --git a/tests/mysql/trigger.rs b/tests/mysql/trigger.rs
new file mode 100644
index 000000000..8df8b1042
--- /dev/null
+++ b/tests/mysql/trigger.rs
@@ -0,0 +1,60 @@
+use super::*;
+use pretty_assertions::assert_eq;
+
+#[test]
+fn unnamed_trigger_can_receive_name() {
+    let unnamed_trigger = UnnamedTrigger::new();
+    let named_trigger = unnamed_trigger.name("my_trigger");
+    assert_eq!(named_trigger.trigger_name().to_string(), "my_trigger");
+}
+
+#[test]
+fn create_unnamed_trigger() {
+    assert_eq!(
+        UnnamedTrigger::new()
+            .before_insert(Glyph::Table)
+            .create()
+            .to_string(MysqlQueryBuilder),
+        [
+            "CREATE TRIGGER `t_glyph_before_insert`",
+            "BEFORE INSERT ON `glyph`",
+            "FOR EACH ROW\nBEGIN\n\nEND",
+        ]
+        .join(" ")
+    );
+}
+
+#[test]
+fn create_named_trigger() {
+    assert_eq!(
+        UnnamedTrigger::new()
+            .name("my_trigger")
+            .before_insert(Glyph::Table)
+            .create()
+            .to_string(MysqlQueryBuilder),
+        [
+            "CREATE TRIGGER `my_trigger`",
+            "BEFORE INSERT ON `glyph`",
+            "FOR EACH ROW\nBEGIN\n\nEND",
+        ]
+        .join(" ")
+    );
+}
+
+#[test]
+fn drop_named_trigger() {
+    let trigger = NamedTrigger::new("my_trigger");
+    assert_eq!(
+        trigger.drop().to_string(MysqlQueryBuilder),
+        "DROP TRIGGER `my_trigger`"
+    );
+}
+
+#[test]
+fn drop_unnamed_trigger() {
+    let trigger = UnnamedTrigger::new().before_delete(Glyph::Table);
+    assert_eq!(
+        trigger.drop().to_string(MysqlQueryBuilder),
+        "DROP TRIGGER `t_glyph_before_delete`"
+    );
+}

From 5ee24755732f66c02bb1f3ad1ee932bba91fbe48 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Stefan=20Gr=C3=B6nke?= <stefan@gronke.net>
Date: Sat, 5 Oct 2024 10:22:04 +0200
Subject: [PATCH 2/2] prepare trigger actions

---
 src/backend/trigger_builder.rs | 17 +++++++++++++++--
 src/trigger/mod.rs             | 27 +++++++++++++++------------
 tests/mysql/trigger.rs         | 24 ++++++++++++++++++++++--
 3 files changed, 52 insertions(+), 16 deletions(-)

diff --git a/src/backend/trigger_builder.rs b/src/backend/trigger_builder.rs
index 7a66377f6..399fa9421 100644
--- a/src/backend/trigger_builder.rs
+++ b/src/backend/trigger_builder.rs
@@ -1,6 +1,6 @@
 use crate::*;
 
-pub trait TriggerBuilder: TableRefBuilder {
+pub trait TriggerBuilder: TableRefBuilder + QueryBuilder {
     /// Translate [`TriggerCreateStatement`] into SQL statement.
     fn prepare_trigger_create_statement(
         &self,
@@ -21,7 +21,16 @@ pub trait TriggerBuilder: TableRefBuilder {
         self.prepare_table_ref_iden(&create.trigger.table, sql);
         write!(sql, " FOR EACH ROW\nBEGIN\n").unwrap();
 
-        write!(sql, "\nEND").unwrap();
+        self.prepare_trigger_actions(&create.trigger.actions, sql);
+
+        write!(sql, "END").unwrap();
+    }
+
+    fn prepare_trigger_actions(&self, actions: &TriggerActions, sql: &mut dyn SqlWriter) {
+        for action in actions {
+            self.prepare_simple_expr_common(&action, sql);
+            write!(sql, ";\n").unwrap();
+        }
     }
 
     /// Translate IF NOT EXISTS expression in [`TriggerCreateStatement`].
@@ -45,4 +54,8 @@ pub trait TriggerBuilder: TableRefBuilder {
         write!(sql, "DROP TRIGGER ").unwrap();
         self.prepare_table_ref_iden(&drop.name.clone().into(), sql);
     }
+
+    fn prepare_simple_expr_yeah(&self, simple_expr: &SimpleExpr, sql: &mut dyn SqlWriter) {
+        self.prepare_simple_expr_common(simple_expr, sql);
+    }
 }
diff --git a/src/trigger/mod.rs b/src/trigger/mod.rs
index 5c6b56662..b1df71701 100644
--- a/src/trigger/mod.rs
+++ b/src/trigger/mod.rs
@@ -1,4 +1,4 @@
-use crate::{Iden, IntoTableRef, SchemaBuilder, SeaRc, TableRef};
+use crate::{Iden, IntoTableRef, SchemaBuilder, SeaRc, SimpleExpr, TableRef};
 use std::fmt;
 
 mod create;
@@ -72,15 +72,20 @@ pub trait Configurable {
     }
 }
 
+pub type TriggerAction = SimpleExpr;
+pub type TriggerActions = Vec<TriggerAction>;
+
 #[derive(Default, Debug, Clone)]
 pub struct NamedTrigger {
-    pub(crate) name: TriggerRef
+    pub(crate) name: TriggerRef,
+    pub(crate) actions: TriggerActions,
 }
 
 impl NamedTrigger {
     pub fn new<T: Into<TriggerRef>>(name: T) -> NamedTrigger {
         Self {
-            name: name.into()
+            name: name.into(),
+            actions: vec![],
         }
     }
 }
@@ -107,29 +112,25 @@ impl Configurable for NamedTrigger {
             table: table,
             event: event,
             time: time,
+            actions: self.actions.clone(),
         }
     }
 }
 
 #[derive(Default, Debug, Clone)]
 pub struct UnnamedTrigger {
-    pub(crate) table: Option<TableRef>,
-    pub(crate) event: Option<TriggerEvent>,
-    pub(crate) time: Option<TriggerActionTime>,
+    pub actions: TriggerActions,
 }
 
 impl UnnamedTrigger {
     pub fn new() -> UnnamedTrigger {
-        Self {
-            table: None,
-            event: None,
-            time: None,
-        }
+        Self { actions: vec![] }
     }
     // an unnamed trigger can become a named one
     pub fn name<T: Into<TriggerRef>>(&self, name: T) -> NamedTrigger {
         NamedTrigger {
-            name: name.into()
+            name: name.into(),
+            actions: self.actions.clone(),
         }
     }
 }
@@ -146,6 +147,7 @@ impl Configurable for UnnamedTrigger {
             table: table,
             event: event,
             time: time,
+            actions: self.actions.clone(),
         }
     }
 }
@@ -156,6 +158,7 @@ pub struct DefinedTrigger {
     pub(crate) table: TableRef,
     pub(crate) event: TriggerEvent,
     pub(crate) time: TriggerActionTime,
+    pub(crate) actions: TriggerActions,
 }
 
 impl Referencable for DefinedTrigger {
diff --git a/tests/mysql/trigger.rs b/tests/mysql/trigger.rs
index 8df8b1042..4a9f4d821 100644
--- a/tests/mysql/trigger.rs
+++ b/tests/mysql/trigger.rs
@@ -18,7 +18,7 @@ fn create_unnamed_trigger() {
         [
             "CREATE TRIGGER `t_glyph_before_insert`",
             "BEFORE INSERT ON `glyph`",
-            "FOR EACH ROW\nBEGIN\n\nEND",
+            "FOR EACH ROW\nBEGIN\nEND",
         ]
         .join(" ")
     );
@@ -35,7 +35,7 @@ fn create_named_trigger() {
         [
             "CREATE TRIGGER `my_trigger`",
             "BEFORE INSERT ON `glyph`",
-            "FOR EACH ROW\nBEGIN\n\nEND",
+            "FOR EACH ROW\nBEGIN\nEND",
         ]
         .join(" ")
     );
@@ -58,3 +58,23 @@ fn drop_unnamed_trigger() {
         "DROP TRIGGER `t_glyph_before_delete`"
     );
 }
+
+#[test]
+fn trigger_actions() {
+    let mut trigger = UnnamedTrigger::new();
+    trigger.actions.push(Expr::col(Glyph::Id).eq(1));
+
+    assert_eq!(
+        trigger
+            .before_insert(Glyph::Table)
+            .create()
+            .to_string(MysqlQueryBuilder),
+        [
+            "CREATE TRIGGER `t_glyph_before_insert` BEFORE INSERT ON `glyph` FOR EACH ROW",
+            "BEGIN",
+            "`id` = 1;",
+            "END"
+        ]
+        .join("\n")
+    );
+}