use chrono::{NaiveDateTime, Utc};
use std::sync::Arc;
use switchy_database::{Database, DatabaseValue};
#[allow(unused)]
pub trait DateTimeTestSuite<I: Into<String>> {
type DatabaseType: Database + Send + Sync;
async fn get_database(&self) -> Option<Arc<Self::DatabaseType>>;
fn gen_param(&self, i: u8) -> I;
fn get_unique_suffix(&self) -> String {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
(nanos % 1_000_000_000).to_string()
}
fn get_table_name(&self, test_name: &str, backend: &str) -> String {
format!(
"{backend}_datetime_{test_name}_{}",
self.get_unique_suffix()
)
}
async fn create_test_table(&self, db: &Self::DatabaseType, table_name: &str);
async fn cleanup_test_data(&self, db: &Self::DatabaseType, table_name: &str);
async fn get_timestamp_column(
&self,
db: &Self::DatabaseType,
table_name: &str,
column: &str,
id: i32,
) -> Option<NaiveDateTime>;
async fn get_row_id_by_description(
&self,
db: &Self::DatabaseType,
table_name: &str,
description: &str,
) -> i32;
fn assert_timestamp_near(
&self,
actual: NaiveDateTime,
expected: NaiveDateTime,
tolerance_mins: i64,
) {
let diff = (actual - expected).num_seconds().abs();
assert!(
diff <= tolerance_mins * 60,
"Timestamp {actual} not within {tolerance_mins}m of {expected} (diff: {diff}s)"
);
}
async fn test_now_insert(&self, backend: &str) {
let Some(db) = self.get_database().await else {
println!("Skipping test - database not available");
return;
};
let table_name = self.get_table_name("now_insert", backend);
self.create_test_table(&db, &table_name).await;
let before = Utc::now().naive_utc();
self.insert_with_now(&db, &table_name, "test now insert")
.await;
let after = Utc::now().naive_utc();
let id = self
.get_row_id_by_description(&db, &table_name, "test now insert")
.await;
let created_at = self
.get_timestamp_column(&db, &table_name, "created_at", id)
.await
.expect("Failed to get created_at timestamp");
self.assert_timestamp_near(created_at, before, 5);
self.assert_timestamp_near(created_at, after, 5);
self.cleanup_test_data(&db, &table_name).await;
}
async fn test_now_in_where_clause(&self, backend: &str) {
let Some(db) = self.get_database().await else {
println!("Skipping test - database not available");
return;
};
let table_name = self.get_table_name("now_where", backend);
self.create_test_table(&db, &table_name).await;
self.insert_with_expires_at(
&db,
&table_name,
DatabaseValue::now().plus_days(1).build(),
"future expiry",
)
.await;
self.insert_with_expires_at(
&db,
&table_name,
DatabaseValue::now().minus_days(1).build(),
"past expiry",
)
.await;
let future_rows = db
.query_raw_params(
&format!(
"SELECT * FROM {} WHERE expires_at > {}",
table_name,
self.gen_param(1).into()
),
&[DatabaseValue::Now],
)
.await
.expect("Failed to query future rows");
assert_eq!(
future_rows.len(),
1,
"Should find exactly 1 row with future expiry"
);
let past_rows = db
.query_raw_params(
&format!(
"SELECT * FROM {} WHERE expires_at < {}",
table_name,
self.gen_param(1).into()
),
&[DatabaseValue::Now],
)
.await
.expect("Failed to query past rows");
assert_eq!(
past_rows.len(),
1,
"Should find exactly 1 row with past expiry"
);
self.cleanup_test_data(&db, &table_name).await;
}
async fn test_now_plus_interval(&self, backend: &str) {
let Some(db) = self.get_database().await else {
println!("Skipping test - database not available");
return;
};
let table_name = self.get_table_name("now_plus", backend);
self.create_test_table(&db, &table_name).await;
let before = Utc::now().naive_utc();
self.insert_with_scheduled_for(
&db,
&table_name,
DatabaseValue::now().plus_hours(1).build(),
"scheduled future",
)
.await;
let id = self
.get_row_id_by_description(&db, &table_name, "scheduled future")
.await;
let scheduled_for = self
.get_timestamp_column(&db, &table_name, "scheduled_for", id)
.await
.expect("Failed to get scheduled_for timestamp");
let expected_time = before + chrono::Duration::hours(1);
self.assert_timestamp_near(scheduled_for, expected_time, 5);
self.cleanup_test_data(&db, &table_name).await;
}
async fn test_now_minus_interval(&self, backend: &str) {
let Some(db) = self.get_database().await else {
println!("Skipping test - database not available");
return;
};
let table_name = self.get_table_name("now_minus", backend);
self.create_test_table(&db, &table_name).await;
let before = Utc::now().naive_utc();
self.insert_with_scheduled_for(
&db,
&table_name,
DatabaseValue::now().minus_minutes(30).build(),
"scheduled past",
)
.await;
let id = self
.get_row_id_by_description(&db, &table_name, "scheduled past")
.await;
let scheduled_for = self
.get_timestamp_column(&db, &table_name, "scheduled_for", id)
.await
.expect("Failed to get scheduled_for timestamp");
let expected_time = before - chrono::Duration::minutes(30);
self.assert_timestamp_near(scheduled_for, expected_time, 5);
self.cleanup_test_data(&db, &table_name).await;
}
async fn test_complex_interval_operations(&self, backend: &str) {
let Some(db) = self.get_database().await else {
println!("Skipping test - database not available");
return;
};
let table_name = self.get_table_name("complex_interval", backend);
self.create_test_table(&db, &table_name).await;
let before = Utc::now().naive_utc();
let complex_future = DatabaseValue::now()
.plus_days(1)
.plus_hours(2)
.minus_minutes(15);
self.insert_with_scheduled_for(&db, &table_name, complex_future.build(), "complex future")
.await;
let id = self
.get_row_id_by_description(&db, &table_name, "complex future")
.await;
let scheduled_for = self
.get_timestamp_column(&db, &table_name, "scheduled_for", id)
.await
.expect("Failed to get scheduled_for timestamp");
let expected_time = before + chrono::Duration::days(1) + chrono::Duration::hours(2)
- chrono::Duration::minutes(15);
self.assert_timestamp_near(scheduled_for, expected_time, 5);
self.cleanup_test_data(&db, &table_name).await;
}
async fn test_update_with_now(&self, backend: &str) {
let Some(db) = self.get_database().await else {
println!("Skipping test - database not available");
return;
};
let table_name = self.get_table_name("update_now", backend);
self.create_test_table(&db, &table_name).await;
let initial_time = Utc::now().naive_utc() - chrono::Duration::hours(1);
self.insert_with_expires_at(
&db,
&table_name,
DatabaseValue::DateTime(initial_time),
"update test",
)
.await;
let before_update = Utc::now().naive_utc();
let id = self
.get_row_id_by_description(&db, &table_name, "update test")
.await;
db.exec_raw_params(
&format!(
"UPDATE {} SET expires_at = {} WHERE id = {}",
table_name,
self.gen_param(1).into(),
self.gen_param(2).into()
),
&[DatabaseValue::Now, DatabaseValue::Int64(id as i64)],
)
.await
.expect("Failed to update with NOW()");
let after_update = Utc::now().naive_utc();
let updated_expires_at = self
.get_timestamp_column(&db, &table_name, "expires_at", id)
.await
.expect("Failed to get updated expires_at timestamp");
self.assert_timestamp_near(updated_expires_at, before_update, 5);
self.assert_timestamp_near(updated_expires_at, after_update, 5);
assert!(
(updated_expires_at - initial_time).num_seconds().abs() > 3000, "Updated timestamp should be much different from initial time"
);
self.cleanup_test_data(&db, &table_name).await;
}
async fn test_multiple_now_consistency(&self, backend: &str) {
let Some(db) = self.get_database().await else {
println!("Skipping test - database not available");
return;
};
let table_name = self.get_table_name("multiple_now", backend);
self.create_test_table(&db, &table_name).await;
db.exec_raw_params(
&format!(
"INSERT INTO {} (created_at, expires_at, scheduled_for, description) VALUES ({}, {}, {}, {})",
table_name,
self.gen_param(1).into(),
self.gen_param(2).into(),
self.gen_param(3).into(),
self.gen_param(4).into()
),
&[
DatabaseValue::Now,
DatabaseValue::Now,
DatabaseValue::Now,
DatabaseValue::String("consistency test".to_string()),
],
)
.await
.expect("Failed to insert with multiple NOW() values");
let id = self
.get_row_id_by_description(&db, &table_name, "consistency test")
.await;
let created_at = self
.get_timestamp_column(&db, &table_name, "created_at", id)
.await
.expect("Failed to get created_at");
let expires_at = self
.get_timestamp_column(&db, &table_name, "expires_at", id)
.await
.expect("Failed to get expires_at");
let scheduled_for = self
.get_timestamp_column(&db, &table_name, "scheduled_for", id)
.await
.expect("Failed to get scheduled_for");
self.assert_timestamp_near(created_at, expires_at, 1);
self.assert_timestamp_near(created_at, scheduled_for, 1);
self.assert_timestamp_near(expires_at, scheduled_for, 1);
self.cleanup_test_data(&db, &table_name).await;
}
async fn test_mixed_now_operations(&self, backend: &str) {
let Some(db) = self.get_database().await else {
println!("Skipping test - database not available");
return;
};
let table_name = self.get_table_name("mixed_now", backend);
self.create_test_table(&db, &table_name).await;
let before = Utc::now().naive_utc();
self.insert_with_scheduled_for(
&db,
&table_name,
DatabaseValue::now().plus_minutes(30).build(),
"mixed operations test",
)
.await;
let id = self
.get_row_id_by_description(&db, &table_name, "mixed operations test")
.await;
let scheduled_for = self
.get_timestamp_column(&db, &table_name, "scheduled_for", id)
.await
.expect("Failed to get scheduled_for timestamp");
let expected_time = before + chrono::Duration::minutes(30);
self.assert_timestamp_near(scheduled_for, expected_time, 5);
self.cleanup_test_data(&db, &table_name).await;
}
async fn test_now_plus_days(&self, backend: &str) {
let Some(db) = self.get_database().await else {
println!("Skipping test - database not available");
return;
};
let table_name = self.get_table_name("now_plus_days", backend);
self.create_test_table(&db, &table_name).await;
let before = Utc::now().naive_utc();
self.insert_with_scheduled_for(
&db,
&table_name,
DatabaseValue::now().plus_days(1).build(),
"plus one day",
)
.await;
let id = self
.get_row_id_by_description(&db, &table_name, "plus one day")
.await;
let scheduled_for = self
.get_timestamp_column(&db, &table_name, "scheduled_for", id)
.await
.expect("Failed to get scheduled_for timestamp");
let expected_time = before + chrono::Duration::days(1);
self.assert_timestamp_near(scheduled_for, expected_time, 10);
self.cleanup_test_data(&db, &table_name).await;
}
async fn test_now_minus_days(&self, backend: &str) {
let Some(db) = self.get_database().await else {
println!("Skipping test - database not available");
return;
};
let table_name = self.get_table_name("now_minus_days", backend);
self.create_test_table(&db, &table_name).await;
let before = Utc::now().naive_utc();
self.insert_with_scheduled_for(
&db,
&table_name,
DatabaseValue::now().minus_days(1).build(),
"minus one day",
)
.await;
let id = self
.get_row_id_by_description(&db, &table_name, "minus one day")
.await;
let scheduled_for = self
.get_timestamp_column(&db, &table_name, "scheduled_for", id)
.await
.expect("Failed to get scheduled_for timestamp");
let expected_time = before - chrono::Duration::days(1);
self.assert_timestamp_near(scheduled_for, expected_time, 10);
self.cleanup_test_data(&db, &table_name).await;
}
async fn test_now_plus_hours_minutes_seconds(&self, backend: &str) {
let Some(db) = self.get_database().await else {
println!("Skipping test - database not available");
return;
};
let table_name = self.get_table_name("now_plus_hours_minutes_seconds", backend);
self.create_test_table(&db, &table_name).await;
self.insert_with_scheduled_for(
&db,
&table_name,
DatabaseValue::now()
.plus_hours(2)
.plus_minutes(30)
.plus_seconds(15)
.build(),
"complex time",
)
.await;
self.cleanup_test_data(&db, &table_name).await;
}
async fn test_now_plus_minutes_normalization(&self, backend: &str) {
let Some(db) = self.get_database().await else {
println!("Skipping test - database not available");
return;
};
let table_name = self.get_table_name("now_plus_minutes_normalization", backend);
self.create_test_table(&db, &table_name).await;
self.insert_with_scheduled_for(
&db,
&table_name,
DatabaseValue::now().plus_minutes(90).build(),
"normalized time",
)
.await;
self.cleanup_test_data(&db, &table_name).await;
}
async fn test_now_plus_complex_interval(&self, backend: &str) {
let Some(db) = self.get_database().await else {
println!("Skipping test - database not available");
return;
};
let table_name = self.get_table_name("now_plus_complex_interval", backend);
self.create_test_table(&db, &table_name).await;
let complex_future = DatabaseValue::now()
.plus_days(1)
.plus_hours(2)
.minus_minutes(15);
self.insert_with_scheduled_for(&db, &table_name, complex_future.build(), "complex future")
.await;
self.cleanup_test_data(&db, &table_name).await;
}
async fn test_zero_interval_returns_now(&self, backend: &str) {
let Some(db) = self.get_database().await else {
println!("Skipping test - database not available");
return;
};
let table_name = self.get_table_name("zero_interval_returns_now", backend);
self.create_test_table(&db, &table_name).await;
let before = Utc::now().naive_utc();
self.insert_with_scheduled_for(
&db,
&table_name,
DatabaseValue::now().build(),
"zero interval",
)
.await;
let after = Utc::now().naive_utc();
let id = self
.get_row_id_by_description(&db, &table_name, "zero interval")
.await;
let scheduled_for = self
.get_timestamp_column(&db, &table_name, "scheduled_for", id)
.await
.expect("Failed to get scheduled_for timestamp");
self.assert_timestamp_near(scheduled_for, before, 5);
self.assert_timestamp_near(scheduled_for, after, 5);
self.cleanup_test_data(&db, &table_name).await;
}
async fn test_mixed_parameters(&self, backend: &str) {
let Some(db) = self.get_database().await else {
println!("Skipping test - database not available");
return;
};
let table_name = self.get_table_name("mixed_parameters", backend);
self.create_test_table(&db, &table_name).await;
self.insert_with_scheduled_for(
&db,
&table_name,
DatabaseValue::now().plus_minutes(30).build(),
"mixed operations test",
)
.await;
self.cleanup_test_data(&db, &table_name).await;
}
async fn test_now_consistency_in_transaction(&self, backend: &str) {
let Some(db) = self.get_database().await else {
println!("Skipping test - database not available");
return;
};
let table_name = self.get_table_name("now_consistency_in_transaction", backend);
self.create_test_table(&db, &table_name).await;
self.insert_with_all_timestamps(
&db,
&table_name,
DatabaseValue::Now,
DatabaseValue::Now,
DatabaseValue::Now,
"consistent timestamps",
)
.await;
self.cleanup_test_data(&db, &table_name).await;
}
async fn test_duration_conversion(&self, backend: &str) {
let Some(db) = self.get_database().await else {
println!("Skipping test - database not available");
return;
};
let table_name = self.get_table_name("duration_conversion", backend);
self.create_test_table(&db, &table_name).await;
self.insert_with_scheduled_for(
&db,
&table_name,
DatabaseValue::now()
.plus_days(1)
.plus_hours(1)
.plus_minutes(1)
.build(),
"duration test",
)
.await;
self.cleanup_test_data(&db, &table_name).await;
}
async fn insert_with_now(&self, db: &Self::DatabaseType, table_name: &str, description: &str);
async fn insert_with_expires_at(
&self,
db: &Self::DatabaseType,
table_name: &str,
expires_at: DatabaseValue,
description: &str,
);
async fn insert_with_scheduled_for(
&self,
db: &Self::DatabaseType,
table_name: &str,
scheduled_for: DatabaseValue,
description: &str,
);
async fn insert_with_all_timestamps(
&self,
db: &Self::DatabaseType,
table_name: &str,
created_at: DatabaseValue,
expires_at: DatabaseValue,
scheduled_for: DatabaseValue,
description: &str,
);
}