diff --git a/src/backend/mod.rs b/src/backend/mod.rs index b918e854..de97419b 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 4f972e9e..f718509b 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 00000000..7b5aef02 --- /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 6d6bf51e..12df5170 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 00000000..6e4e2e56 --- /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 b884cc5e..a187fc2b 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 00000000..a72ee510 --- /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 00000000..399fa942 --- /dev/null +++ b/src/backend/trigger_builder.rs @@ -0,0 +1,61 @@ +use crate::*; + +pub trait TriggerBuilder: TableRefBuilder + QueryBuilder { + /// 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(); + + 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`]. + 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); + } + + 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/backend/trigger_ref_builder.rs b/src/backend/trigger_ref_builder.rs new file mode 100644 index 00000000..4ca84780 --- /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 15e1a189..2c6355b4 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 00000000..02711069 --- /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(&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(&self, schema_builder: T) -> String; +} diff --git a/src/trigger/drop.rs b/src/trigger/drop.rs new file mode 100644 index 00000000..f51e531b --- /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(&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(&self, schema_builder: T) -> String; +} diff --git a/src/trigger/mod.rs b/src/trigger/mod.rs new file mode 100644 index 00000000..b1df7170 --- /dev/null +++ b/src/trigger/mod.rs @@ -0,0 +1,356 @@ +use crate::{Iden, IntoTableRef, SchemaBuilder, SeaRc, SimpleExpr, 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(&self, table: T) -> DefinedTrigger { + self.configure( + table.into_table_ref(), + TriggerEvent::Insert, + TriggerActionTime::Before, + ) + } + fn after_insert(&self, table: T) -> DefinedTrigger { + self.configure( + table.into_table_ref(), + TriggerEvent::Insert, + TriggerActionTime::After, + ) + } + fn before_update(&self, table: T) -> DefinedTrigger { + self.configure( + table.into_table_ref(), + TriggerEvent::Update, + TriggerActionTime::Before, + ) + } + fn after_update(&self, table: T) -> DefinedTrigger { + self.configure( + table.into_table_ref(), + TriggerEvent::Update, + TriggerActionTime::After, + ) + } + fn before_delete(&self, table: T) -> DefinedTrigger { + self.configure( + table.into_table_ref(), + TriggerEvent::Delete, + TriggerActionTime::Before, + ) + } + fn after_delete(&self, table: T) -> DefinedTrigger { + self.configure( + table.into_table_ref(), + TriggerEvent::Delete, + TriggerActionTime::After, + ) + } +} + +pub type TriggerAction = SimpleExpr; +pub type TriggerActions = Vec; + +#[derive(Default, Debug, Clone)] +pub struct NamedTrigger { + pub(crate) name: TriggerRef, + pub(crate) actions: TriggerActions, +} + +impl NamedTrigger { + pub fn new>(name: T) -> NamedTrigger { + Self { + name: name.into(), + actions: vec![], + } + } +} + +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, + actions: self.actions.clone(), + } + } +} + +#[derive(Default, Debug, Clone)] +pub struct UnnamedTrigger { + pub actions: TriggerActions, +} + +impl UnnamedTrigger { + pub fn new() -> UnnamedTrigger { + Self { actions: vec![] } + } + // an unnamed trigger can become a named one + pub fn name>(&self, name: T) -> NamedTrigger { + NamedTrigger { + name: name.into(), + actions: self.actions.clone(), + } + } +} + +impl Configurable for UnnamedTrigger { + fn configure( + &self, + table: TableRef, + event: TriggerEvent, + time: TriggerActionTime, + ) -> DefinedTrigger { + DefinedTrigger { + name: None, + table: table, + event: event, + time: time, + actions: self.actions.clone(), + } + } +} + +#[derive(Debug, Clone)] +pub struct DefinedTrigger { + pub(crate) name: Option, + pub(crate) table: TableRef, + pub(crate) event: TriggerEvent, + pub(crate) time: TriggerActionTime, + pub(crate) actions: TriggerActions, +} + +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, + Option, + Option, + ), + NamedTrigger( + TriggerRef, + Option, + Option, + Option, + ), + DefinedTrigger( + Option, + 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(&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(&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 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 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 d717774f..f250585f 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 00000000..4a9f4d82 --- /dev/null +++ b/tests/mysql/trigger.rs @@ -0,0 +1,80 @@ +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\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\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`" + ); +} + +#[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") + ); +}