From 5d8413a08f78abdd06598d7a45e37a46dcaa2458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Wed, 4 Feb 2026 19:17:41 +0100 Subject: [PATCH 1/5] feat: implement To/FromDbValue for `chrono::DateTime` --- cot/src/db/fields.rs | 79 +++++++++++++----------------- cot/src/db/fields/chrono_fields.rs | 61 +++++++++++++++++++++++ 2 files changed, 94 insertions(+), 46 deletions(-) create mode 100644 cot/src/db/fields/chrono_fields.rs diff --git a/cot/src/db/fields.rs b/cot/src/db/fields.rs index e273b8d8..cb23f418 100644 --- a/cot/src/db/fields.rs +++ b/cot/src/db/fields.rs @@ -11,66 +11,81 @@ use crate::db::{ LimitedString, Model, PrimaryKey, Result, SqlxValueRef, ToDbFieldValue, ToDbValue, }; +mod chrono_fields; mod chrono_wrapper; macro_rules! impl_from_sqlite_default { () => { #[cfg(feature = "sqlite")] - fn from_sqlite(value: SqliteValueRef<'_>) -> Result { + fn from_sqlite( + value: crate::db::impl_sqlite::SqliteValueRef<'_>, + ) -> crate::db::Result { value.get::() } }; ($wrapper_ty:ty) => { #[cfg(feature = "sqlite")] - fn from_sqlite(value: SqliteValueRef<'_>) -> Result { + fn from_sqlite( + value: crate::db::impl_sqlite::SqliteValueRef<'_>, + ) -> crate::db::Result { <$wrapper_ty as FromDbValue>::from_sqlite(value).map(|val| val.into()) } }; ($wrapper_ty:ty, option) => { #[cfg(feature = "sqlite")] - fn from_sqlite(value: SqliteValueRef<'_>) -> Result { + fn from_sqlite( + value: crate::db::impl_sqlite::SqliteValueRef<'_>, + ) -> crate::db::Result { <$wrapper_ty as FromDbValue>::from_sqlite(value).map(|val| val.map(|val| val.into())) } }; } +use impl_from_sqlite_default; macro_rules! impl_from_postgres_default { () => { #[cfg(feature = "postgres")] - fn from_postgres(value: PostgresValueRef<'_>) -> Result { + fn from_postgres( + value: crate::db::impl_postgres::PostgresValueRef<'_>, + ) -> crate::db::Result { value.get::() } }; ($wrapper_ty:ty) => { #[cfg(feature = "postgres")] - fn from_postgres(value: PostgresValueRef<'_>) -> Result { + fn from_postgres( + value: crate::db::impl_postgres::PostgresValueRef<'_>, + ) -> crate::db::Result { <$wrapper_ty as FromDbValue>::from_postgres(value).map(|val| val.into()) } }; ($wrapper_ty:ty, option) => { #[cfg(feature = "postgres")] - fn from_postgres(value: PostgresValueRef<'_>) -> Result { + fn from_postgres( + value: crate::db::impl_postgres::PostgresValueRef<'_>, + ) -> crate::db::Result { <$wrapper_ty as FromDbValue>::from_postgres(value).map(|val| val.map(|val| val.into())) } }; } +use impl_from_postgres_default; macro_rules! impl_from_mysql_default { () => { #[cfg(feature = "mysql")] - fn from_mysql(value: MySqlValueRef<'_>) -> Result { + fn from_mysql(value: crate::db::impl_mysql::MySqlValueRef<'_>) -> Result { value.get::() } }; ($wrapper_ty:ty) => { #[cfg(feature = "mysql")] - fn from_mysql(value: MySqlValueRef<'_>) -> Result { + fn from_mysql(value: crate::db::impl_mysql::MySqlValueRef<'_>) -> Result { <$wrapper_ty as FromDbValue>::from_mysql(value).map(|val| val.into()) } }; ($wrapper_ty:ty, option) => { #[cfg(feature = "mysql")] - fn from_mysql(value: MySqlValueRef<'_>) -> Result { + fn from_mysql(value: crate::db::impl_mysql::MySqlValueRef<'_>) -> Result { <$wrapper_ty as FromDbValue>::from_mysql(value).map(|val| val.map(|val| val.into())) } }; @@ -78,28 +93,28 @@ macro_rules! impl_from_mysql_default { macro_rules! impl_to_db_value_default { ($ty:ty) => { - impl ToDbValue for $ty { - fn to_db_value(&self) -> DbValue { + impl crate::db::ToDbValue for $ty { + fn to_db_value(&self) -> crate::db::DbValue { self.clone().into() } } - impl ToDbValue for Option<$ty> { - fn to_db_value(&self) -> DbValue { + impl crate::db::ToDbValue for Option<$ty> { + fn to_db_value(&self) -> crate::db::DbValue { self.clone().into() } } }; ($ty:ty, $wrapper_ty:ty) => { - impl ToDbValue for $ty { - fn to_db_value(&self) -> DbValue { + impl crate::db::ToDbValue for $ty { + fn to_db_value(&self) -> crate::db::DbValue { Into::<$wrapper_ty>::into(self.clone()).to_db_value() } } - impl ToDbValue for Option<$ty> { - fn to_db_value(&self) -> DbValue { + impl crate::db::ToDbValue for Option<$ty> { + fn to_db_value(&self) -> crate::db::DbValue { self.clone() .map(|val| Into::<$wrapper_ty>::into(val)) .to_db_value() @@ -107,6 +122,7 @@ macro_rules! impl_to_db_value_default { } }; } +use impl_to_db_value_default; macro_rules! impl_db_field { ($ty:ty, $column_type:ident) => { @@ -231,35 +247,6 @@ impl ToDbValue for &str { } } -impl DatabaseField for chrono::DateTime { - const TYPE: ColumnType = ColumnType::DateTimeWithTimeZone; -} - -impl FromDbValue for chrono::DateTime { - impl_from_sqlite_default!(); - - impl_from_postgres_default!(); - - #[cfg(feature = "mysql")] - fn from_mysql(value: MySqlValueRef<'_>) -> Result { - Ok(value.get::>()?.fixed_offset()) - } -} -impl FromDbValue for Option> { - impl_from_sqlite_default!(); - - impl_from_postgres_default!(); - - #[cfg(feature = "mysql")] - fn from_mysql(value: MySqlValueRef<'_>) -> Result { - Ok(value - .get::>>()? - .map(|dt| dt.fixed_offset())) - } -} - -impl_to_db_value_default!(chrono::DateTime); - impl ToDbValue for Option<&str> { fn to_db_value(&self) -> DbValue { self.map(ToString::to_string).into() diff --git a/cot/src/db/fields/chrono_fields.rs b/cot/src/db/fields/chrono_fields.rs new file mode 100644 index 00000000..a6156897 --- /dev/null +++ b/cot/src/db/fields/chrono_fields.rs @@ -0,0 +1,61 @@ +use crate::db::fields::{ + impl_from_postgres_default, impl_from_sqlite_default, impl_to_db_value_default, +}; +use crate::db::impl_mysql::MySqlValueRef; +use crate::db::{ColumnType, DatabaseField, FromDbValue, Result, SqlxValueRef}; + +impl DatabaseField for chrono::DateTime { + const TYPE: ColumnType = ColumnType::DateTimeWithTimeZone; +} + +impl FromDbValue for chrono::DateTime { + impl_from_sqlite_default!(); + + impl_from_postgres_default!(); + + #[cfg(feature = "mysql")] + fn from_mysql(value: MySqlValueRef<'_>) -> Result { + Ok(value.get::>()?.fixed_offset()) + } +} +impl FromDbValue for Option> { + impl_from_sqlite_default!(); + + impl_from_postgres_default!(); + + #[cfg(feature = "mysql")] + fn from_mysql(value: MySqlValueRef<'_>) -> Result { + Ok(value + .get::>>()? + .map(|dt| dt.fixed_offset())) + } +} + +impl_to_db_value_default!(chrono::DateTime); + +impl DatabaseField for chrono::DateTime { + const TYPE: ColumnType = ColumnType::DateTimeWithTimeZone; +} + +impl FromDbValue for chrono::DateTime { + impl_from_sqlite_default!(); + + impl_from_postgres_default!(); + + #[cfg(feature = "mysql")] + fn from_mysql(value: MySqlValueRef<'_>) -> Result { + value.get::() + } +} +impl FromDbValue for Option> { + impl_from_sqlite_default!(); + + impl_from_postgres_default!(); + + #[cfg(feature = "mysql")] + fn from_mysql(value: MySqlValueRef<'_>) -> Result { + value.get::>>() + } +} + +impl_to_db_value_default!(chrono::DateTime); From f2bd882679ef7afcb80fc292586dffc4ee26a068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Wed, 4 Feb 2026 19:33:58 +0100 Subject: [PATCH 2/5] fix --- cot/src/db/fields.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cot/src/db/fields.rs b/cot/src/db/fields.rs index cb23f418..50e8acfd 100644 --- a/cot/src/db/fields.rs +++ b/cot/src/db/fields.rs @@ -20,6 +20,7 @@ macro_rules! impl_from_sqlite_default { fn from_sqlite( value: crate::db::impl_sqlite::SqliteValueRef<'_>, ) -> crate::db::Result { + use crate::db::SqlxValueRef; value.get::() } }; @@ -48,6 +49,7 @@ macro_rules! impl_from_postgres_default { fn from_postgres( value: crate::db::impl_postgres::PostgresValueRef<'_>, ) -> crate::db::Result { + use crate::db::SqlxValueRef; value.get::() } }; @@ -74,6 +76,7 @@ macro_rules! impl_from_mysql_default { () => { #[cfg(feature = "mysql")] fn from_mysql(value: crate::db::impl_mysql::MySqlValueRef<'_>) -> Result { + use crate::db::SqlxValueRef; value.get::() } }; From 764dbceb094a5c2f922be4a6625870bfb52ac073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Wed, 4 Feb 2026 19:43:24 +0100 Subject: [PATCH 3/5] fix yet again --- cot/src/db/fields/chrono_fields.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/cot/src/db/fields/chrono_fields.rs b/cot/src/db/fields/chrono_fields.rs index a6156897..45718632 100644 --- a/cot/src/db/fields/chrono_fields.rs +++ b/cot/src/db/fields/chrono_fields.rs @@ -1,6 +1,7 @@ use crate::db::fields::{ impl_from_postgres_default, impl_from_sqlite_default, impl_to_db_value_default, }; +#[cfg(feature = "mysql")] use crate::db::impl_mysql::MySqlValueRef; use crate::db::{ColumnType, DatabaseField, FromDbValue, Result, SqlxValueRef}; From 8cd5ddfe5f174dee15701b897c91f7c955d105b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Wed, 4 Feb 2026 20:06:39 +0100 Subject: [PATCH 4/5] tests --- cot/src/db/fields/chrono_fields.rs | 99 ++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/cot/src/db/fields/chrono_fields.rs b/cot/src/db/fields/chrono_fields.rs index 45718632..e5b7e5b7 100644 --- a/cot/src/db/fields/chrono_fields.rs +++ b/cot/src/db/fields/chrono_fields.rs @@ -60,3 +60,102 @@ impl FromDbValue for Option> { } impl_to_db_value_default!(chrono::DateTime); + +#[cfg(test)] +mod tests { + use chrono::{DateTime, FixedOffset, Utc}; + + use crate::db::{ColumnType, DatabaseField, DbValue, ToDbValue}; + + #[test] + fn test_datetime_fixed_offset_column_type() { + assert_eq!( + as DatabaseField>::TYPE, + ColumnType::DateTimeWithTimeZone + ); + assert!(! as DatabaseField>::NULLABLE); + } + + #[test] + fn test_datetime_utc_column_type() { + assert_eq!( + as DatabaseField>::TYPE, + ColumnType::DateTimeWithTimeZone + ); + assert!(! as DatabaseField>::NULLABLE); + } + + #[test] + fn test_option_datetime_column_type() { + assert_eq!( + > as DatabaseField>::TYPE, + ColumnType::DateTimeWithTimeZone + ); + assert!(> as DatabaseField>::NULLABLE); + + assert_eq!( + > as DatabaseField>::TYPE, + ColumnType::DateTimeWithTimeZone + ); + assert!(> as DatabaseField>::NULLABLE); + } + + #[test] + fn test_datetime_fixed_offset_to_db_value() { + let dt = DateTime::parse_from_rfc3339("2023-01-01T12:00:00+01:00").unwrap(); + let db_value = dt.to_db_value(); + + match db_value { + DbValue::ChronoDateTimeWithTimeZone(Some(v)) => assert_eq!(*v, dt), + _ => panic!( + "Expected DbValue::ChronoDateTimeWithTimeZone, got {:?}", + db_value + ), + } + } + + #[test] + fn test_datetime_utc_to_db_value() { + let dt = Utc::now(); + let db_value = dt.to_db_value(); + + match db_value { + DbValue::ChronoDateTimeUtc(Some(v)) => assert_eq!(*v, dt), + _ => panic!("Expected DbValue::ChronoDateTimeUtc, got {:?}", db_value), + } + } + + #[test] + fn test_option_datetime_to_db_value() { + let dt = DateTime::parse_from_rfc3339("2023-01-01T12:00:00+01:00").unwrap(); + let some_dt = Some(dt); + let none_dt: Option> = None; + + match some_dt.to_db_value() { + DbValue::ChronoDateTimeWithTimeZone(Some(v)) => assert_eq!(*v, dt), + _ => panic!( + "Expected DbValue::ChronoDateTimeWithTimeZone(Some), got {:?}", + some_dt.to_db_value() + ), + } + + assert_eq!( + none_dt.to_db_value(), + DbValue::ChronoDateTimeWithTimeZone(None) + ); + + let dt_utc = Utc::now(); + let some_dt_utc = Some(dt_utc); + let none_dt_utc: Option> = None; + + match some_dt_utc.to_db_value() { + DbValue::ChronoDateTimeUtc(Some(v)) => assert_eq!(*v, dt_utc), + _ => panic!( + "Expected DbValue::ChronoDateTimeUtc(Some), got {:?}", + some_dt_utc.to_db_value() + ), + } + + assert_eq!(none_dt_utc.to_db_value(), DbValue::ChronoDateTimeUtc(None)); + } +} From fb2523d2f76013cd8e3be063decb7866f10dcde5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Wed, 4 Feb 2026 20:13:07 +0100 Subject: [PATCH 5/5] fix clippy --- cot/src/db/fields/chrono_fields.rs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/cot/src/db/fields/chrono_fields.rs b/cot/src/db/fields/chrono_fields.rs index e5b7e5b7..48bdb06f 100644 --- a/cot/src/db/fields/chrono_fields.rs +++ b/cot/src/db/fields/chrono_fields.rs @@ -73,7 +73,9 @@ mod tests { as DatabaseField>::TYPE, ColumnType::DateTimeWithTimeZone ); - assert!(! as DatabaseField>::NULLABLE); + const { + assert!(! as DatabaseField>::NULLABLE); + } } #[test] @@ -82,7 +84,9 @@ mod tests { as DatabaseField>::TYPE, ColumnType::DateTimeWithTimeZone ); - assert!(! as DatabaseField>::NULLABLE); + const { + assert!(! as DatabaseField>::NULLABLE); + } } #[test] @@ -91,13 +95,17 @@ mod tests { > as DatabaseField>::TYPE, ColumnType::DateTimeWithTimeZone ); - assert!(> as DatabaseField>::NULLABLE); + const { + assert!(> as DatabaseField>::NULLABLE); + } assert_eq!( > as DatabaseField>::TYPE, ColumnType::DateTimeWithTimeZone ); - assert!(> as DatabaseField>::NULLABLE); + const { + assert!(> as DatabaseField>::NULLABLE); + } } #[test] @@ -107,10 +115,7 @@ mod tests { match db_value { DbValue::ChronoDateTimeWithTimeZone(Some(v)) => assert_eq!(*v, dt), - _ => panic!( - "Expected DbValue::ChronoDateTimeWithTimeZone, got {:?}", - db_value - ), + _ => panic!("Expected DbValue::ChronoDateTimeWithTimeZone, got {db_value:?}"), } } @@ -121,7 +126,7 @@ mod tests { match db_value { DbValue::ChronoDateTimeUtc(Some(v)) => assert_eq!(*v, dt), - _ => panic!("Expected DbValue::ChronoDateTimeUtc, got {:?}", db_value), + _ => panic!("Expected DbValue::ChronoDateTimeUtc, got {db_value:?}"), } }