use std::time::SystemTime;
use hexeract_outbox::OutboxEnvelope;
#[cfg(feature = "sqlite")]
use hexeract_outbox::OutboxError;
#[cfg(feature = "postgres")]
use time::OffsetDateTime;
#[cfg(any(feature = "mysql", feature = "sqlite"))]
use time::PrimitiveDateTime;
use uuid::Uuid;
#[cfg(feature = "postgres")]
pub(crate) fn to_system_time(o: OffsetDateTime) -> SystemTime {
SystemTime::from(o)
}
#[cfg(feature = "mysql")]
pub(crate) fn primitive_utc_to_system_time(p: PrimitiveDateTime) -> SystemTime {
SystemTime::from(p.assume_utc())
}
#[cfg(all(feature = "sqlite", test))]
pub(crate) fn format_sqlite_utc(t: SystemTime) -> Result<String, OutboxError> {
let fmt = time::macros::format_description!(
"[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3]Z"
);
OffsetDateTime::from(t)
.format(fmt)
.map_err(|e| OutboxError::Internal(format!("sqlite timestamp format failed: {e}")))
}
#[cfg(feature = "sqlite")]
pub(crate) fn parse_sqlite_utc(s: &str) -> Result<SystemTime, OutboxError> {
let rfc3339_millis = time::macros::format_description!(
"[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3]Z"
);
if let Ok(parsed) = PrimitiveDateTime::parse(s, rfc3339_millis) {
return Ok(SystemTime::from(parsed.assume_utc()));
}
let sqlite_canonical =
time::macros::format_description!("[year]-[month]-[day] [hour]:[minute]:[second]");
let parsed = PrimitiveDateTime::parse(s, sqlite_canonical).map_err(|e| {
OutboxError::Internal(format!("sqlite timestamp parse failed for {s:?}: {e}"))
})?;
Ok(SystemTime::from(parsed.assume_utc()))
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn assemble_envelope(
event_id: Uuid,
event_type: String,
payload: Vec<u8>,
subject_id: Option<Uuid>,
created_at: SystemTime,
attempts: u32,
last_error: Option<String>,
next_retry_at: Option<SystemTime>,
) -> OutboxEnvelope {
OutboxEnvelope::restore(
event_id,
event_type,
payload,
subject_id,
created_at,
attempts,
last_error,
next_retry_at,
None,
)
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[cfg(feature = "postgres")]
#[test]
fn system_time_round_trips_through_offset_date_time() {
let original = SystemTime::UNIX_EPOCH + Duration::new(1_750_000_000, 123_456_789);
let restored = to_system_time(OffsetDateTime::from(original));
assert_eq!(restored, original);
}
#[cfg(feature = "sqlite")]
#[test]
fn system_time_round_trips_through_sqlite_text() {
let original = SystemTime::UNIX_EPOCH + Duration::new(1_750_000_000, 123_000_000);
let text = format_sqlite_utc(original).unwrap();
assert!(text.contains('T'));
assert!(text.ends_with('Z'));
let restored = parse_sqlite_utc(&text).unwrap();
assert_eq!(restored, original);
}
#[cfg(feature = "sqlite")]
#[test]
fn sqlite_text_truncates_sub_millisecond_precision() {
let original = SystemTime::UNIX_EPOCH + Duration::new(1_750_000_000, 123_900_000);
let truncated = SystemTime::UNIX_EPOCH + Duration::new(1_750_000_000, 123_000_000);
let restored = parse_sqlite_utc(&format_sqlite_utc(original).unwrap()).unwrap();
assert_ne!(
restored, original,
"sub-millisecond precision must not survive a SQLite round-trip"
);
assert_eq!(
restored, truncated,
"the sub-millisecond tail must be truncated, not rounded"
);
}
#[cfg(feature = "sqlite")]
#[test]
fn parse_sqlite_utc_accepts_canonical_datetime_now_form() {
let parsed = parse_sqlite_utc("2024-01-01 12:00:00").expect("canonical form must parse");
let expected = SystemTime::UNIX_EPOCH + Duration::from_secs(1_704_110_400);
assert_eq!(parsed, expected);
}
#[cfg(feature = "sqlite")]
#[test]
fn parse_sqlite_utc_rejects_garbage() {
assert!(parse_sqlite_utc("not a timestamp").is_err());
}
#[cfg(feature = "mysql")]
#[test]
fn system_time_round_trips_through_primitive_utc() {
let original = SystemTime::UNIX_EPOCH + Duration::new(1_750_000_000, 123_456_000);
let odt = OffsetDateTime::from(original);
let primitive = PrimitiveDateTime::new(odt.date(), odt.time());
let restored = primitive_utc_to_system_time(primitive);
assert_eq!(restored, original);
}
#[test]
fn assemble_envelope_maps_columns_and_leaves_undelivered() {
let event_id = Uuid::from_u128(1);
let subject = Uuid::from_u128(2);
let created = SystemTime::UNIX_EPOCH + Duration::from_secs(10);
let envelope = assemble_envelope(
event_id,
"users.registered".to_owned(),
b"{\"user_id\":\"x\"}".to_vec(),
Some(subject),
created,
3,
Some("boom".to_owned()),
None,
);
assert_eq!(envelope.event_id, event_id);
assert_eq!(envelope.event_type, "users.registered");
assert_eq!(envelope.payload, b"{\"user_id\":\"x\"}".to_vec());
assert_eq!(envelope.subject_id, Some(subject));
assert_eq!(envelope.created_at, created);
assert_eq!(envelope.attempts, 3);
assert_eq!(envelope.last_error.as_deref(), Some("boom"));
assert!(envelope.next_retry_at.is_none());
assert!(envelope.delivered_at.is_none());
}
}