diff --git a/README.md b/README.md index 538891b2..3a0dd7c5 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,9 @@ and [Poem](https://github.com/poem-web/poem). - Structured logs/traces using tokio's [tracing](https://docs.rs/tracing/latest/tracing/) crate. Export traces/metrics using OpenTelemetry (requires the `otel` feature). - Health checks to ensure the app's external dependencies are healthy +- Pre-built migrations for common DB tables, e.g. `user` (requires the `db-sql` feature) +- Support for auto-updating timestamp columns, e.g. `updated_at`, when updating DB rows (Postgres only currently) ( + requires the `db-sql` feature) # Getting started diff --git a/src/migration/mod.rs b/src/migration/mod.rs index 7318c08b..83dacc56 100644 --- a/src/migration/mod.rs +++ b/src/migration/mod.rs @@ -5,4 +5,5 @@ pub mod check; pub mod schema; +pub mod timestamp; pub mod user; diff --git a/src/migration/schema.rs b/src/migration/schema.rs index f38788d5..cba01d89 100644 --- a/src/migration/schema.rs +++ b/src/migration/schema.rs @@ -10,20 +10,30 @@ use sea_orm_migration::{prelude::*, schema::*}; /// Timestamp related fields. #[derive(DeriveIden)] #[non_exhaustive] -pub enum Timetamps { +pub enum Timestamps { /// When the row was created. When used with the [timestamps] method, will default to /// the current timestamp (with timezone). CreatedAt, /// When the row was updated. When used with the [timestamps] method, will be initially set to - /// the current timestamp (with timezone). Updates to the row will need to provide this field - /// as well in order for it to be updated. - // Todo: does seaorm automatically update this? + /// the current timestamp (with timezone). + /// + /// To automatically update the value for a row whenever the row is updated, include the + /// [crate::migration::timestamp::m20240723_201404_add_update_timestamp_function::Migration] + /// in your [MigratorTrait] implementation, along with a [MigrationTrait] for your table + /// that add a trigger to update the column. Helper methods are provided for this in + /// the [crate::migration::timestamp] module. Specifically, see: + /// - [crate::migration::timestamp::exec_create_update_timestamp_trigger] + /// - [crate::migration::timestamp::exec_drop_update_timestamp_trigger] + /// + /// Note that the auto-updates mentioned above are currently only supported on Postgres. If + /// an app is using a different DB, it will need to manually update the timestamp when updating + /// a row. UpdatedAt, } /// Create a table if it does not exist yet and add some default columns /// (e.g., create/update timestamps). -pub fn table(name: T) -> TableCreateStatement { +pub fn table(name: T) -> TableCreateStatement { timestamps(Table::create().table(name).if_not_exists().to_owned()) } @@ -31,8 +41,8 @@ pub fn table(name: T) -> TableCreateStatement { /// The default for each column is the current timestamp. pub fn timestamps(mut table: TableCreateStatement) -> TableCreateStatement { table - .col(timestamp_with_time_zone(Timetamps::CreatedAt).default(Expr::current_timestamp())) - .col(timestamp_with_time_zone(Timetamps::UpdatedAt).default(Expr::current_timestamp())) + .col(timestamp_with_time_zone(Timestamps::CreatedAt).default(Expr::current_timestamp())) + .col(timestamp_with_time_zone(Timestamps::UpdatedAt).default(Expr::current_timestamp())) .to_owned() } diff --git a/src/migration/timestamp/m20240723_201404_add_update_timestamp_function.rs b/src/migration/timestamp/m20240723_201404_add_update_timestamp_function.rs new file mode 100644 index 00000000..71c6de83 --- /dev/null +++ b/src/migration/timestamp/m20240723_201404_add_update_timestamp_function.rs @@ -0,0 +1,26 @@ +//! Migration to create a SQL function to update the [Timestamps::UpdatedAt] column for a row +//! with the current timestamp. +//! +//! Note: Currently only supports Postgres. If another DB is used, will do nothing. + +use crate::migration::schema::Timestamps; +use crate::migration::timestamp::{ + exec_create_update_timestamp_function, exec_drop_update_timestamp_function, +}; +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +const COLUMN: Timestamps = Timestamps::UpdatedAt; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + exec_create_update_timestamp_function(manager, COLUMN).await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + exec_drop_update_timestamp_function(manager, COLUMN).await + } +} diff --git a/src/migration/timestamp/mod.rs b/src/migration/timestamp/mod.rs new file mode 100644 index 00000000..25423520 --- /dev/null +++ b/src/migration/timestamp/mod.rs @@ -0,0 +1,450 @@ +//! Utilities and migrations related to timestamp fields. + +use sea_orm::{DbBackend, Statement}; +use sea_orm_migration::prelude::*; + +pub mod m20240723_201404_add_update_timestamp_function; + +/// Wrapper around [create_update_timestamp_function] to execute the returned [Statement], if +/// present. +/// +/// # Examples +/// ```rust +/// use roadster::migration::schema::Timestamps; +/// use roadster::migration::timestamp::exec_create_update_timestamp_function; +/// use sea_orm_migration::prelude::*; +/// +/// #[derive(DeriveMigrationName)] +/// pub struct Migration; +/// +/// const COLUMN: Timestamps = Timestamps::UpdatedAt; +/// +/// #[async_trait::async_trait] +/// impl MigrationTrait for Migration { +/// async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { +/// exec_create_update_timestamp_function(manager, COLUMN).await +/// } +/// # +/// # async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { +/// # todo!() +/// # } +/// } +/// ``` +pub async fn exec_create_update_timestamp_function( + manager: &SchemaManager<'_>, + column: T, +) -> Result<(), DbErr> { + let statement = create_update_timestamp_function(manager, column); + if let Some(statement) = statement { + manager.get_connection().execute(statement).await?; + } + + Ok(()) +} + +/// Create a SQL function to update a timestamp column with the current timestamp. Returns +/// a [Statement] containing the SQL instructions to create the function. +/// +/// Note: Currently only supports Postgres. If another DB is used, will return [None]. +pub fn create_update_timestamp_function( + manager: &SchemaManager<'_>, + column: T, +) -> Option { + let backend = manager.get_database_backend(); + create_update_timestamp_function_for_db_backend(backend, column) +} + +fn create_update_timestamp_function_for_db_backend( + backend: DbBackend, + column: T, +) -> Option { + if let DbBackend::Postgres = backend { + let FnQueryStrings { + column, fn_call, .. + } = FnQueryStrings::new(column); + + let statement = Statement::from_string( + backend, + format!( + r#" +CREATE OR REPLACE FUNCTION {fn_call} RETURNS TRIGGER AS $$ +BEGIN + NEW.{column} = NOW(); + RETURN NEW; +END; +$$ language 'plpgsql'; +"# + ), + ); + Some(statement) + } else { + None + } +} + +/// Wrapper around [drop_update_timestamp_function] to execute the returned [Statement], if +/// present. +/// +/// # Examples +/// ```rust +/// use roadster::migration::schema::Timestamps; +/// use roadster::migration::timestamp::exec_drop_update_timestamp_function; +/// use sea_orm_migration::prelude::*; +/// +/// #[derive(DeriveMigrationName)] +/// pub struct Migration; +/// +/// const COLUMN: Timestamps = Timestamps::UpdatedAt; +/// +/// #[async_trait::async_trait] +/// impl MigrationTrait for Migration { +/// # async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { +/// # todo!() +/// # } +/// # +/// async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { +/// exec_drop_update_timestamp_function(manager, COLUMN).await +/// } +/// } +/// ``` +pub async fn exec_drop_update_timestamp_function( + manager: &SchemaManager<'_>, + column: T, +) -> Result<(), DbErr> { + let statement = drop_update_timestamp_function(manager, column); + if let Some(statement) = statement { + manager.get_connection().execute(statement).await?; + } + + Ok(()) +} + +/// Drop a SQL function that was previously created by [create_update_timestamp_function]. +/// Returns a [Statement] containing the SQL instructions to create the function. +/// +/// Note: Currently only supports Postgres. If another DB is used, will return [None]. +pub fn drop_update_timestamp_function( + manager: &SchemaManager<'_>, + column: T, +) -> Option { + let backend = manager.get_database_backend(); + drop_update_timestamp_function_for_db_backend(backend, column) +} + +fn drop_update_timestamp_function_for_db_backend( + backend: DbBackend, + column: T, +) -> Option { + if let DbBackend::Postgres = backend { + let FnQueryStrings { fn_name, .. } = FnQueryStrings::new(column); + + let statement = + Statement::from_string(backend, format!(r#"DROP FUNCTION IF EXISTS {fn_name};"#)); + Some(statement) + } else { + None + } +} + +/// Wrapper around [create_update_timestamp_trigger] to execute the returned [Statement], if +/// present. +/// +/// # Examples +/// ```rust +/// use roadster::migration::schema::Timestamps; +/// use roadster::migration::timestamp::exec_create_update_timestamp_trigger; +/// use sea_orm_migration::prelude::*; +/// +/// #[derive(DeriveMigrationName)] +/// pub struct Migration; +/// +/// const TABLE: Foo = Foo::Table; +/// const COLUMN: Timestamps = Timestamps::UpdatedAt; +/// +/// #[async_trait::async_trait] +/// impl MigrationTrait for Migration { +/// async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { +/// exec_create_update_timestamp_trigger(manager, TABLE, COLUMN).await +/// } +/// # +/// # async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { +/// # todo!() +/// # } +/// } +/// +/// #[derive(DeriveIden)] +/// pub(crate) enum Foo { +/// Table, +/// // ... +/// } +/// ``` +pub async fn exec_create_update_timestamp_trigger( + manager: &SchemaManager<'_>, + table: T, + column: C, +) -> Result<(), DbErr> { + let statement = create_update_timestamp_trigger(manager, table, column); + if let Some(statement) = statement { + manager.get_connection().execute(statement).await?; + } + + Ok(()) +} + +/// Create a SQL trigger to automatically update a timestamp column of a row whenever the row is +/// updated. Depends on the function created by [create_update_timestamp_function]. +/// Returns a [Statement] containing the SQL instructions to create the trigger. +/// +/// Note: Currently only supports Postgres. If another DB is used, will return [None]. +pub fn create_update_timestamp_trigger( + manager: &SchemaManager<'_>, + table: T, + column: C, +) -> Option { + let backend = manager.get_database_backend(); + create_update_timestamp_trigger_for_db_backend(backend, table, column) +} + +fn create_update_timestamp_trigger_for_db_backend( + backend: DbBackend, + table: T, + column: C, +) -> Option { + if let DbBackend::Postgres = backend { + let TriggerQueryNames { + fn_query_strings: FnQueryStrings { fn_call, .. }, + table, + trigger_name, + } = TriggerQueryNames::new(table, column); + + let statement = Statement::from_string( + backend, + format!( + r#" +CREATE TRIGGER {trigger_name} BEFORE UPDATE +ON {table} +FOR EACH ROW +EXECUTE PROCEDURE {fn_call}; +"# + ), + ); + + Some(statement) + } else { + None + } +} + +/// Wrapper around [drop_update_timestamp_trigger] to execute the returned [Statement], if +/// present. +/// +/// # Examples +/// ```rust +/// use roadster::migration::schema::Timestamps; +/// use roadster::migration::timestamp::exec_drop_update_timestamp_trigger; +/// use sea_orm_migration::prelude::*; +/// +/// #[derive(DeriveMigrationName)] +/// pub struct Migration; +/// +/// const TABLE: Foo = Foo::Table; +/// const COLUMN: Timestamps = Timestamps::UpdatedAt; +/// +/// #[async_trait::async_trait] +/// impl MigrationTrait for Migration { +/// # async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { +/// # todo!() +/// # } +/// # +/// async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { +/// exec_drop_update_timestamp_trigger(manager, TABLE, COLUMN).await +/// } +/// } +/// +/// #[derive(DeriveIden)] +/// pub(crate) enum Foo { +/// Table, +/// // ... +/// } +/// ``` +pub async fn exec_drop_update_timestamp_trigger( + manager: &SchemaManager<'_>, + table: T, + column: C, +) -> Result<(), DbErr> { + let statement = drop_update_timestamp_trigger(manager, table, column); + if let Some(statement) = statement { + manager.get_connection().execute(statement).await?; + } + + Ok(()) +} + +/// Drop a SQL trigger that was previously created by [create_update_timestamp_trigger]. +/// Returns a [Statement] containing the SQL instructions to create the function. +/// +/// Note: Currently only supports Postgres. If another DB is used, will return [None]. +pub fn drop_update_timestamp_trigger( + manager: &SchemaManager<'_>, + table: T, + column: C, +) -> Option { + let backend = manager.get_database_backend(); + drop_update_timestamp_trigger_for_db_backend(backend, table, column) +} + +fn drop_update_timestamp_trigger_for_db_backend( + backend: DbBackend, + table: T, + column: C, +) -> Option { + if let DbBackend::Postgres = backend { + let TriggerQueryNames { + table, + trigger_name, + .. + } = TriggerQueryNames::new(table, column); + + let statement = Statement::from_string( + backend, + format!(r#"DROP TRIGGER IF EXISTS {trigger_name} ON {table};"#), + ); + Some(statement) + } else { + None + } +} + +#[derive(Debug)] +struct FnQueryStrings { + column: String, + fn_name: String, + fn_call: String, +} + +#[derive(Debug)] +struct TriggerQueryNames { + fn_query_strings: FnQueryStrings, + table: String, + trigger_name: String, +} + +impl FnQueryStrings { + fn new(column: C) -> Self { + let column = column.into_iden().to_string(); + let fn_name = update_timestamp_fn_name(&column); + let fn_call = format!("{fn_name}()"); + + Self { + column, + fn_name, + fn_call, + } + } +} + +impl TriggerQueryNames { + fn new(table: T, column: C) -> Self { + let fn_query_strings = FnQueryStrings::new(column); + let table = table.into_iden().to_string(); + let trigger_name = trigger_name(&table, &fn_query_strings.fn_name); + + Self { + fn_query_strings, + table: format!("public.{table}"), + trigger_name, + } + } +} + +fn update_timestamp_fn_name(column: &str) -> String { + format!("update_timestamp_{column}") +} + +fn trigger_name(table: &str, fn_name: &str) -> String { + format!("{table}_{fn_name}") +} + +#[cfg(test)] +mod tests { + use crate::migration::timestamp::{FnQueryStrings, TriggerQueryNames}; + use crate::testing::snapshot::TestCase; + use insta::assert_debug_snapshot; + use rstest::{fixture, rstest}; + use sea_orm::DbBackend; + use sea_orm_migration::prelude::*; + + #[derive(DeriveIden)] + enum Foo { + Table, + UpdatedAt, + } + + #[fixture] + fn case() -> TestCase { + Default::default() + } + + #[rstest] + #[case(DbBackend::Postgres)] + #[case(DbBackend::Postgres)] + #[case(DbBackend::MySql)] + #[cfg_attr(coverage_nightly, coverage(off))] + fn add_update_timestamp_trigger(_case: TestCase, #[case] backend: DbBackend) { + let statement = super::create_update_timestamp_trigger_for_db_backend( + backend, + Foo::Table, + Foo::UpdatedAt, + ); + + assert_debug_snapshot!(statement); + } + + #[rstest] + #[case(DbBackend::Postgres)] + #[case(DbBackend::MySql)] + #[cfg_attr(coverage_nightly, coverage(off))] + fn drop_update_timestamp_trigger(_case: TestCase, #[case] backend: DbBackend) { + let statement = super::drop_update_timestamp_trigger_for_db_backend( + backend, + Foo::Table, + Foo::UpdatedAt, + ); + + assert_debug_snapshot!(statement); + } + + #[rstest] + #[case(DbBackend::Postgres)] + #[case(DbBackend::MySql)] + #[cfg_attr(coverage_nightly, coverage(off))] + fn add_update_timestamp_function(_case: TestCase, #[case] backend: DbBackend) { + let statement = + super::create_update_timestamp_function_for_db_backend(backend, Foo::UpdatedAt); + + assert_debug_snapshot!(statement); + } + + #[rstest] + #[case(DbBackend::Postgres)] + #[case(DbBackend::MySql)] + #[cfg_attr(coverage_nightly, coverage(off))] + fn drop_update_timestamp_function(_case: TestCase, #[case] backend: DbBackend) { + let statement = + super::drop_update_timestamp_function_for_db_backend(backend, Foo::UpdatedAt); + + assert_debug_snapshot!(statement); + } + + #[test] + fn fn_query_strings() { + let fn_query_strings = FnQueryStrings::new(Foo::UpdatedAt); + assert_debug_snapshot!(fn_query_strings); + } + + #[test] + fn trigger_query_strings() { + let trigger_query_strings = TriggerQueryNames::new(Foo::Table, Foo::UpdatedAt); + assert_debug_snapshot!(trigger_query_strings); + } +} diff --git a/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__add_update_timestamp_function@case_1.snap b/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__add_update_timestamp_function@case_1.snap new file mode 100644 index 00000000..2639f3b6 --- /dev/null +++ b/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__add_update_timestamp_function@case_1.snap @@ -0,0 +1,11 @@ +--- +source: src/migration/timestamp/mod.rs +expression: statement +--- +Some( + Statement { + sql: "\nCREATE OR REPLACE FUNCTION update_timestamp_updated_at() RETURNS TRIGGER AS $$\nBEGIN\n NEW.updated_at = NOW();\n RETURN NEW;\nEND;\n$$ language 'plpgsql';\n", + values: None, + db_backend: Postgres, + }, +) diff --git a/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__add_update_timestamp_function@case_2.snap b/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__add_update_timestamp_function@case_2.snap new file mode 100644 index 00000000..50a9c1e4 --- /dev/null +++ b/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__add_update_timestamp_function@case_2.snap @@ -0,0 +1,5 @@ +--- +source: src/migration/timestamp/mod.rs +expression: statement +--- +None diff --git a/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__add_update_timestamp_trigger@case_1.snap b/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__add_update_timestamp_trigger@case_1.snap new file mode 100644 index 00000000..ec299dce --- /dev/null +++ b/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__add_update_timestamp_trigger@case_1.snap @@ -0,0 +1,11 @@ +--- +source: src/migration/timestamp/mod.rs +expression: statement +--- +Some( + Statement { + sql: "\nCREATE TRIGGER foo_update_timestamp_updated_at BEFORE UPDATE\nON public.foo\nFOR EACH ROW\nEXECUTE PROCEDURE update_timestamp_updated_at();\n", + values: None, + db_backend: Postgres, + }, +) diff --git a/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__add_update_timestamp_trigger@case_2.snap b/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__add_update_timestamp_trigger@case_2.snap new file mode 100644 index 00000000..ec299dce --- /dev/null +++ b/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__add_update_timestamp_trigger@case_2.snap @@ -0,0 +1,11 @@ +--- +source: src/migration/timestamp/mod.rs +expression: statement +--- +Some( + Statement { + sql: "\nCREATE TRIGGER foo_update_timestamp_updated_at BEFORE UPDATE\nON public.foo\nFOR EACH ROW\nEXECUTE PROCEDURE update_timestamp_updated_at();\n", + values: None, + db_backend: Postgres, + }, +) diff --git a/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__add_update_timestamp_trigger@case_3.snap b/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__add_update_timestamp_trigger@case_3.snap new file mode 100644 index 00000000..50a9c1e4 --- /dev/null +++ b/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__add_update_timestamp_trigger@case_3.snap @@ -0,0 +1,5 @@ +--- +source: src/migration/timestamp/mod.rs +expression: statement +--- +None diff --git a/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__drop_update_timestamp_function@case_1.snap b/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__drop_update_timestamp_function@case_1.snap new file mode 100644 index 00000000..cb480338 --- /dev/null +++ b/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__drop_update_timestamp_function@case_1.snap @@ -0,0 +1,11 @@ +--- +source: src/migration/timestamp/mod.rs +expression: statement +--- +Some( + Statement { + sql: "DROP FUNCTION IF EXISTS update_timestamp_updated_at;", + values: None, + db_backend: Postgres, + }, +) diff --git a/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__drop_update_timestamp_function@case_2.snap b/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__drop_update_timestamp_function@case_2.snap new file mode 100644 index 00000000..50a9c1e4 --- /dev/null +++ b/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__drop_update_timestamp_function@case_2.snap @@ -0,0 +1,5 @@ +--- +source: src/migration/timestamp/mod.rs +expression: statement +--- +None diff --git a/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__drop_update_timestamp_trigger@case_1.snap b/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__drop_update_timestamp_trigger@case_1.snap new file mode 100644 index 00000000..bb3e85cb --- /dev/null +++ b/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__drop_update_timestamp_trigger@case_1.snap @@ -0,0 +1,11 @@ +--- +source: src/migration/timestamp/mod.rs +expression: statement +--- +Some( + Statement { + sql: "DROP TRIGGER IF EXISTS foo_update_timestamp_updated_at ON public.foo;", + values: None, + db_backend: Postgres, + }, +) diff --git a/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__drop_update_timestamp_trigger@case_2.snap b/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__drop_update_timestamp_trigger@case_2.snap new file mode 100644 index 00000000..50a9c1e4 --- /dev/null +++ b/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__drop_update_timestamp_trigger@case_2.snap @@ -0,0 +1,5 @@ +--- +source: src/migration/timestamp/mod.rs +expression: statement +--- +None diff --git a/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__fn_query_strings.snap b/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__fn_query_strings.snap new file mode 100644 index 00000000..38957ecf --- /dev/null +++ b/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__fn_query_strings.snap @@ -0,0 +1,9 @@ +--- +source: src/migration/timestamp/mod.rs +expression: fn_query_strings +--- +FnQueryStrings { + column: "updated_at", + fn_name: "update_timestamp_updated_at", + fn_call: "update_timestamp_updated_at()", +} diff --git a/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__trigger_query_strings.snap b/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__trigger_query_strings.snap new file mode 100644 index 00000000..aee36544 --- /dev/null +++ b/src/migration/timestamp/snapshots/roadster__migration__timestamp__tests__trigger_query_strings.snap @@ -0,0 +1,13 @@ +--- +source: src/migration/timestamp/mod.rs +expression: trigger_query_strings +--- +TriggerQueryNames { + fn_query_strings: FnQueryStrings { + column: "updated_at", + fn_name: "update_timestamp_updated_at", + fn_call: "update_timestamp_updated_at()", + }, + table: "public.foo", + trigger_name: "foo_update_timestamp_updated_at", +} diff --git a/src/migration/user/m20240714_203550_create_user_table_int_pk.rs b/src/migration/user/m20240714_203550_create_user_table_int_pk.rs index abfa7191..17ac0e47 100644 --- a/src/migration/user/m20240714_203550_create_user_table_int_pk.rs +++ b/src/migration/user/m20240714_203550_create_user_table_int_pk.rs @@ -1,4 +1,4 @@ -//! Migrations to create a basic `user` table that contains the following fields: +//! Migration to create a basic `user` table that contains the following fields: //! //! - Id (BIGINT) //! - Name diff --git a/src/migration/user/m20240714_203551_create_user_table_uuid_pk.rs b/src/migration/user/m20240714_203551_create_user_table_uuid_pk.rs index 1175d053..f39ffcfc 100644 --- a/src/migration/user/m20240714_203551_create_user_table_uuid_pk.rs +++ b/src/migration/user/m20240714_203551_create_user_table_uuid_pk.rs @@ -1,4 +1,4 @@ -//! Migrations to create a basic `user` table that contains the following fields: +//! Migration to create a basic `user` table that contains the following fields: //! //! - Id (UUID) //! - Name diff --git a/src/migration/user/m20240724_005115_user_update_timestamp.rs b/src/migration/user/m20240724_005115_user_update_timestamp.rs new file mode 100644 index 00000000..afc35bf5 --- /dev/null +++ b/src/migration/user/m20240724_005115_user_update_timestamp.rs @@ -0,0 +1,31 @@ +//! Migration to create a SQL trigger to automatically update the [Timestamps::UpdatedAt] column of +//! a row in the `user` table whenever the row is updated. +//! +//! Expects to be run after [crate::migration::timestamp::m20240723_201404_add_update_timestamp_function::Migration], +//! or another equivalent [Migration]. +//! +//! Note: Currently only supports Postgres. If another DB is used, will do nothing. + +use crate::migration::schema::Timestamps; +use crate::migration::timestamp::{ + exec_create_update_timestamp_trigger, exec_drop_update_timestamp_trigger, +}; +use crate::migration::user::User; +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +const TABLE: User = User::Table; +const COLUMN: Timestamps = Timestamps::UpdatedAt; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + exec_create_update_timestamp_trigger(manager, TABLE, COLUMN).await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + exec_drop_update_timestamp_trigger(manager, TABLE, COLUMN).await + } +} diff --git a/src/migration/user/mod.rs b/src/migration/user/mod.rs index 0c3c6e76..d8eaa285 100644 --- a/src/migration/user/mod.rs +++ b/src/migration/user/mod.rs @@ -1,31 +1,13 @@ +use crate::migration::timestamp::m20240723_201404_add_update_timestamp_function; use sea_orm_migration::prelude::*; mod create_table; pub mod m20240714_203550_create_user_table_int_pk; pub mod m20240714_203551_create_user_table_uuid_pk; pub mod m20240723_070533_add_user_account_management_fields; - -/// The collection of migrations defined to create a `user` table. Migrations authored -/// by `roadster` will automatically be added here. -/// -/// Note that the migration uses a `UUID` field for the `id` Primary Key field. If you would like -/// to use a `BIGINT` instead, you can do one of the following: -/// -/// 1. Use the [m20240714_203550_create_user_table_int_pk::Migration] instead -- simply add it to -/// your main [MigratorTrait] implementation before the migrations from [UserMigrator]. -/// 2. Add an `alter table` migration after the migrations from [UserMigrator]. -#[non_exhaustive] -pub struct UserMigrator; - -#[async_trait::async_trait] -impl MigratorTrait for UserMigrator { - fn migrations() -> Vec> { - vec![ - Box::new(m20240714_203551_create_user_table_uuid_pk::Migration), - Box::new(m20240723_070533_add_user_account_management_fields::Migration), - ] - } -} +pub mod m20240724_005115_user_update_timestamp; +#[cfg(test)] +mod tests; /// Contains the identifiers/fields created by all the `user` migrations. #[derive(DeriveIden)] @@ -50,3 +32,27 @@ pub(crate) enum User { /// When the user was deleted. DeletedAt, } + +/// The collection of migrations defined to create a `user` table. Relevant [MigrationTrait]s +/// authored by `roadster` will be added here. +/// +/// Note that the migration uses a `UUID` field for the `id` Primary Key field. If you would like +/// to use a `BIGINT` instead, you can do one of the following: +/// +/// 1. Use the [m20240714_203550_create_user_table_int_pk::Migration] instead -- simply add it to +/// your main [MigratorTrait] implementation before the migrations from [UserMigrator]. +/// 2. Add an `alter table` migration after the migrations from [UserMigrator]. +#[non_exhaustive] +pub struct UserMigrator; + +#[async_trait::async_trait] +impl MigratorTrait for UserMigrator { + fn migrations() -> Vec> { + vec![ + Box::new(m20240714_203551_create_user_table_uuid_pk::Migration), + Box::new(m20240723_070533_add_user_account_management_fields::Migration), + Box::new(m20240723_201404_add_update_timestamp_function::Migration), + Box::new(m20240724_005115_user_update_timestamp::Migration), + ] + } +} diff --git a/src/migration/user/snapshots/roadster__migration__user__tests__user_migrator_migrations.snap b/src/migration/user/snapshots/roadster__migration__user__tests__user_migrator_migrations.snap new file mode 100644 index 00000000..7dbb5a07 --- /dev/null +++ b/src/migration/user/snapshots/roadster__migration__user__tests__user_migrator_migrations.snap @@ -0,0 +1,10 @@ +--- +source: src/migration/user/tests.rs +expression: user_migrations +--- +[ + "m20240714_203551_create_user_table_uuid_pk", + "m20240723_070533_add_user_account_management_fields", + "m20240723_201404_add_update_timestamp_function", + "m20240724_005115_user_update_timestamp", +] diff --git a/src/migration/user/tests.rs b/src/migration/user/tests.rs new file mode 100644 index 00000000..00edaaf7 --- /dev/null +++ b/src/migration/user/tests.rs @@ -0,0 +1,26 @@ +use super::*; +use insta::assert_debug_snapshot; +use itertools::Itertools; + +#[test] +fn user_migrator_migrations() { + let user_migrations = UserMigrator::migrations() + .into_iter() + .map(|migration| migration.name().to_string()) + .collect_vec(); + assert_debug_snapshot!(user_migrations); +} + +#[test] +fn user_migrator_migrations_no_int_pk() { + let user_migrations = UserMigrator::migrations() + .into_iter() + .map(|migration| migration.name().to_string()) + .collect_vec(); + + assert!(!user_migrations.contains( + &m20240714_203550_create_user_table_int_pk::Migration::default() + .name() + .to_string() + )) +}