use std::fmt;
use crate::core::error::{StorageError, TemporalError};
use crate::core::hlc::HybridTimestamp;
pub type Timestamp = HybridTimestamp;
pub const TIMESTAMP_MAX: Timestamp = HybridTimestamp::new_unchecked(i64::MAX, 0);
pub const MAX_VALID_TIMESTAMP: i64 = i64::MAX - 1000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TimeRange {
start: Timestamp,
end: Timestamp,
}
impl TimeRange {
#[inline]
pub fn new(start: Timestamp, end: Timestamp) -> Result<Self, TemporalError> {
if start > end {
return Err(TemporalError::InvalidTimeRange { start, end });
}
if start.wallclock() > MAX_VALID_TIMESTAMP && start != TIMESTAMP_MAX {
return Err(TemporalError::InvalidTimestamp {
timestamp: start,
reason: format!(
"Start timestamp exceeds MAX_VALID_TIMESTAMP ({})",
MAX_VALID_TIMESTAMP
),
});
}
if end.wallclock() > MAX_VALID_TIMESTAMP && end != TIMESTAMP_MAX {
return Err(TemporalError::InvalidTimestamp {
timestamp: end,
reason: format!(
"End timestamp exceeds MAX_VALID_TIMESTAMP ({})",
MAX_VALID_TIMESTAMP
),
});
}
Ok(TimeRange { start, end })
}
#[inline]
pub fn from(start: Timestamp) -> Self {
if start.wallclock() > MAX_VALID_TIMESTAMP && start != TIMESTAMP_MAX {
panic!(
"Start timestamp exceeds MAX_VALID_TIMESTAMP ({})",
MAX_VALID_TIMESTAMP
);
}
TimeRange {
start,
end: TIMESTAMP_MAX,
}
}
#[inline]
pub fn between(start: Timestamp, end: Timestamp) -> Result<Self, TemporalError> {
Self::new(start, end)
}
#[inline]
pub fn at(timestamp: Timestamp) -> Self {
if timestamp.wallclock() > MAX_VALID_TIMESTAMP && timestamp != TIMESTAMP_MAX {
panic!(
"Timestamp exceeds MAX_VALID_TIMESTAMP ({})",
MAX_VALID_TIMESTAMP
);
}
TimeRange {
start: timestamp,
end: timestamp,
}
}
#[inline]
pub const fn start(&self) -> Timestamp {
self.start
}
#[inline]
pub const fn end(&self) -> Timestamp {
self.end
}
#[inline]
pub fn is_current(&self) -> bool {
self.end == TIMESTAMP_MAX
}
#[inline]
pub fn is_closed(&self) -> bool {
self.end < TIMESTAMP_MAX
}
#[inline]
pub fn contains(&self, timestamp: Timestamp) -> bool {
timestamp >= self.start && timestamp < self.end
}
#[inline]
pub fn contains_or_after(&self, timestamp: Timestamp) -> bool {
timestamp >= self.start
}
#[inline]
pub fn is_empty(&self) -> bool {
self.start == self.end
}
#[inline]
pub fn overlaps(&self, other: &TimeRange) -> bool {
if self.is_empty() || other.is_empty() {
return false;
}
self.start < other.end && other.start < self.end
}
#[inline]
pub fn contains_range(&self, other: &TimeRange) -> bool {
self.start <= other.start && other.end <= self.end
}
#[inline]
pub fn close_at(self, end: Timestamp) -> Result<Self, TemporalError> {
if end < self.start {
return Err(TemporalError::InvalidTimeRange {
start: self.start,
end,
});
}
if end.wallclock() > MAX_VALID_TIMESTAMP && end != TIMESTAMP_MAX {
return Err(TemporalError::InvalidTimestamp {
timestamp: end,
reason: format!(
"End timestamp exceeds MAX_VALID_TIMESTAMP ({})",
MAX_VALID_TIMESTAMP
),
});
}
Ok(TimeRange {
start: self.start,
end,
})
}
#[inline]
pub fn duration_micros(&self) -> Option<i64> {
if self.is_current() {
None
} else {
self.end
.wallclock()
.checked_sub(self.start.wallclock())
.or(Some(i64::MAX))
}
}
pub fn serialize(&self) -> Vec<u8> {
let mut buffer = Vec::with_capacity(24);
self.serialize_into(&mut buffer);
buffer
}
pub fn serialize_into(&self, buffer: &mut Vec<u8>) {
self.start.serialize_into(buffer);
self.end.serialize_into(buffer);
}
pub fn deserialize(bytes: &[u8]) -> Result<(Self, usize), StorageError> {
if bytes.len() < 24 {
return Err(StorageError::CorruptedData(format!(
"Buffer too short for TimeRange: {} bytes (need 24)",
bytes.len()
)));
}
let (start, _) = HybridTimestamp::deserialize(&bytes[0..12])?;
let (end, _) = HybridTimestamp::deserialize(&bytes[12..24])?;
if start > end {
return Err(StorageError::CorruptedData(format!(
"Deserialized TimeRange invalid: start {} > end {}",
start, end
)));
}
Ok((TimeRange { start, end }, 24))
}
}
impl fmt::Display for TimeRange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_current() {
write!(f, "[{}, current)", self.start)
} else {
write!(f, "[{}, {})", self.start, self.end)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct BiTemporalInterval {
valid_time: TimeRange,
transaction_time: TimeRange,
}
impl BiTemporalInterval {
#[inline]
pub const fn new(valid_time: TimeRange, transaction_time: TimeRange) -> Self {
BiTemporalInterval {
valid_time,
transaction_time,
}
}
#[inline]
pub fn current(timestamp: Timestamp) -> Self {
let range = TimeRange::from(timestamp);
BiTemporalInterval {
valid_time: range,
transaction_time: range,
}
}
#[inline]
pub fn now(valid_start: Timestamp, tx_timestamp: Timestamp) -> Self {
BiTemporalInterval {
valid_time: TimeRange::from(valid_start),
transaction_time: TimeRange::from(tx_timestamp),
}
}
#[inline]
pub fn with_valid_time(valid_from: Timestamp, tx_time: Timestamp) -> Self {
BiTemporalInterval {
valid_time: TimeRange::from(valid_from),
transaction_time: TimeRange::from(tx_time),
}
}
#[inline]
pub const fn valid_time(&self) -> TimeRange {
self.valid_time
}
#[inline]
pub const fn transaction_time(&self) -> TimeRange {
self.transaction_time
}
#[inline]
pub fn is_currently_valid(&self) -> bool {
self.valid_time.is_current()
}
#[inline]
pub fn is_currently_recorded(&self) -> bool {
self.transaction_time.is_current()
}
#[inline]
pub fn is_current(&self) -> bool {
self.is_currently_valid() && self.is_currently_recorded()
}
#[inline]
pub fn is_valid_at(&self, timestamp: Timestamp) -> bool {
self.valid_time.contains(timestamp)
}
#[inline]
pub fn is_recorded_at(&self, timestamp: Timestamp) -> bool {
self.transaction_time.contains(timestamp)
}
#[inline]
pub fn is_visible_at(&self, valid_time: Timestamp, tx_time: Timestamp) -> bool {
self.valid_time.contains(valid_time) && self.transaction_time.contains(tx_time)
}
#[inline]
pub fn close_valid_time(self, end: Timestamp) -> Result<Self, TemporalError> {
Ok(BiTemporalInterval {
valid_time: self.valid_time.close_at(end)?,
transaction_time: self.transaction_time,
})
}
#[inline]
pub fn close_transaction_time(self, end: Timestamp) -> Result<Self, TemporalError> {
Ok(BiTemporalInterval {
valid_time: self.valid_time,
transaction_time: self.transaction_time.close_at(end)?,
})
}
#[inline]
pub fn close_both(
self,
valid_end: Timestamp,
tx_end: Timestamp,
) -> Result<Self, TemporalError> {
Ok(BiTemporalInterval {
valid_time: self.valid_time.close_at(valid_end)?,
transaction_time: self.transaction_time.close_at(tx_end)?,
})
}
pub fn serialize(&self) -> Vec<u8> {
let mut buffer = Vec::with_capacity(48);
self.serialize_into(&mut buffer);
buffer
}
pub fn serialize_into(&self, buffer: &mut Vec<u8>) {
self.valid_time.serialize_into(buffer);
self.transaction_time.serialize_into(buffer);
}
pub fn deserialize(bytes: &[u8]) -> Result<(Self, usize), StorageError> {
if bytes.len() < 48 {
return Err(StorageError::CorruptedData(format!(
"Buffer too short for BiTemporalInterval: {} bytes (need 48)",
bytes.len()
)));
}
let (valid_time, _) = TimeRange::deserialize(&bytes[0..24])?;
let (transaction_time, _) = TimeRange::deserialize(&bytes[24..48])?;
Ok((
BiTemporalInterval {
valid_time,
transaction_time,
},
48,
))
}
}
impl fmt::Display for BiTemporalInterval {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"BiTemporal[valid: {}, tx: {}]",
self.valid_time, self.transaction_time
)
}
}
pub mod time {
use super::*;
use std::time::{SystemTime, UNIX_EPOCH};
pub fn now() -> Timestamp {
#[cfg(feature = "simulation")]
if let Some(micros) = crate::simulation::clock::thread_local_now() {
return HybridTimestamp::new_unchecked(micros, 0);
}
try_now().expect("System clock is before Unix epoch")
}
pub fn try_now() -> Result<Timestamp, crate::core::error::TemporalError> {
let wallclock = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| crate::core::error::TemporalError::TemporalParadox {
reason: "System clock is before Unix epoch".to_string(),
})?
.as_micros() as i64;
Ok(HybridTimestamp::new_unchecked(wallclock, 0))
}
pub fn to_iso8601(timestamp: Timestamp) -> String {
if timestamp == TIMESTAMP_MAX {
return "current".to_string();
}
let wallclock = timestamp.wallclock();
let secs = wallclock / 1_000_000;
let nanos = ((wallclock % 1_000_000) * 1000) as u32;
let datetime = UNIX_EPOCH + std::time::Duration::new(secs as u64, nanos);
format!("{:?}", datetime) }
#[inline]
pub const fn from_secs(secs: i64) -> Timestamp {
HybridTimestamp::new_unchecked(secs * 1_000_000, 0)
}
#[inline]
pub const fn from_millis(millis: i64) -> Timestamp {
HybridTimestamp::new_unchecked(millis * 1_000, 0)
}
#[inline]
pub const fn to_secs(timestamp: Timestamp) -> i64 {
timestamp.wallclock() / 1_000_000
}
#[inline]
pub const fn to_millis(timestamp: Timestamp) -> i64 {
timestamp.wallclock() / 1_000
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_time_range_creation() {
let range = TimeRange::new(100.into(), 200.into()).unwrap();
assert_eq!(range.start(), 100.into());
assert_eq!(range.end(), 200.into());
assert!(!range.is_current());
assert!(range.is_closed());
}
#[test]
fn test_time_range_current() {
let range = TimeRange::from(100.into());
assert_eq!(range.start(), 100.into());
assert_eq!(range.end(), TIMESTAMP_MAX);
assert!(range.is_current());
assert!(!range.is_closed());
}
#[test]
fn test_time_range_contains() {
let range = TimeRange::new(100.into(), 200.into()).unwrap();
assert!(!range.contains(99.into()));
assert!(range.contains(100.into()));
assert!(range.contains(150.into()));
assert!(range.contains(199.into()));
assert!(!range.contains(200.into())); }
#[test]
fn test_time_range_overlaps() {
let r1 = TimeRange::new(100.into(), 200.into()).unwrap();
let r2 = TimeRange::new(150.into(), 250.into()).unwrap();
let r3 = TimeRange::new(200.into(), 300.into()).unwrap();
let r4 = TimeRange::new(50.into(), 75.into()).unwrap();
assert!(r1.overlaps(&r2));
assert!(r2.overlaps(&r1));
assert!(!r1.overlaps(&r3)); assert!(!r1.overlaps(&r4));
}
#[test]
fn test_time_range_overlaps_touching_repro() {
let r1 = TimeRange::new(100.into(), 200.into()).unwrap();
let r2 = TimeRange::new(200.into(), 300.into()).unwrap();
assert!(!r1.overlaps(&r2));
assert!(!r2.overlaps(&r1));
}
#[test]
fn test_time_range_contains_range() {
let outer = TimeRange::new(100.into(), 300.into()).unwrap();
let inner = TimeRange::new(150.into(), 250.into()).unwrap();
let overlapping = TimeRange::new(150.into(), 350.into()).unwrap();
assert!(outer.contains_range(&inner));
assert!(!inner.contains_range(&outer));
assert!(!outer.contains_range(&overlapping));
}
#[test]
fn test_time_range_close_at() {
let open = TimeRange::from(100.into());
let closed = open.close_at(200.into()).unwrap();
assert!(open.is_current());
assert!(!closed.is_current());
assert_eq!(closed.start(), 100.into());
assert_eq!(closed.end(), 200.into());
}
#[test]
fn test_time_range_close_at_invalid() {
let open = TimeRange::from(100.into());
let result = open.close_at(50.into());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
TemporalError::InvalidTimeRange { .. }
));
}
#[test]
fn test_time_range_duration() {
let range = TimeRange::new(100.into(), 500.into()).unwrap();
assert_eq!(range.duration_micros(), Some(400.into()));
let open = TimeRange::from(100.into());
assert_eq!(open.duration_micros(), None);
}
#[test]
fn test_bitemporal_current() {
let interval = BiTemporalInterval::current(1000.into());
assert!(interval.is_currently_valid());
assert!(interval.is_currently_recorded());
assert!(interval.is_current());
}
#[test]
fn test_bitemporal_now() {
let interval = BiTemporalInterval::now(1000.into(), 2000.into());
assert_eq!(interval.valid_time().start(), 1000.into());
assert_eq!(interval.transaction_time().start(), 2000.into());
assert!(interval.is_currently_valid());
assert!(interval.is_currently_recorded());
}
#[test]
fn test_bitemporal_visibility() {
let interval = BiTemporalInterval::new(
TimeRange::new(1000.into(), 2000.into()).unwrap(), TimeRange::new(3000.into(), 4000.into()).unwrap(), );
assert!(interval.is_visible_at(1500.into(), 3500.into()));
assert!(!interval.is_visible_at(500.into(), 3500.into())); assert!(!interval.is_visible_at(1500.into(), 2500.into())); assert!(!interval.is_visible_at(2500.into(), 3500.into())); assert!(!interval.is_visible_at(1500.into(), 4500.into()));
assert!(!interval.is_visible_at(1500.into(), 2000.into()));
assert!(!interval.is_visible_at(500.into(), 3500.into()));
}
#[test]
fn test_bitemporal_close() {
let interval = BiTemporalInterval::now(1000.into(), 2000.into());
let closed_valid = interval.close_valid_time(1500.into()).unwrap();
assert!(!closed_valid.is_currently_valid());
assert!(closed_valid.is_currently_recorded());
assert_eq!(closed_valid.valid_time().end(), 1500.into());
let closed_tx = interval.close_transaction_time(2500.into()).unwrap();
assert!(closed_tx.is_currently_valid());
assert!(!closed_tx.is_currently_recorded());
assert_eq!(closed_tx.transaction_time().end(), 2500.into());
let closed_both = interval.close_both(1500.into(), 2500.into()).unwrap();
assert!(!closed_both.is_currently_valid());
assert!(!closed_both.is_currently_recorded());
assert_eq!(closed_both.valid_time().end(), 1500.into());
assert_eq!(closed_both.transaction_time().end(), 2500.into());
}
#[test]
fn test_time_helpers() {
let secs = 1234567890i64;
let timestamp = time::from_secs(secs);
assert_eq!(time::to_secs(timestamp), secs);
let millis = 1234567890123i64;
let timestamp = time::from_millis(millis);
assert_eq!(time::to_millis(timestamp), millis);
}
#[test]
fn test_time_now() {
let timestamp = time::now();
assert!(timestamp > time::from_secs(1577836800));
assert!(timestamp < time::from_secs(4102444800));
}
#[test]
fn test_time_range_invalid_returns_error() {
let result = TimeRange::new(200.into(), 100.into());
assert!(result.is_err());
if let Err(crate::core::error::TemporalError::InvalidTimeRange { start, end }) = result {
assert_eq!(start, 200.into());
assert_eq!(end, 100.into());
} else {
panic!("Expected InvalidTimeRange error");
}
}
#[test]
fn test_time_range_valid_returns_ok() {
let result = TimeRange::new(100.into(), 200.into());
assert!(result.is_ok());
let range = result.unwrap();
assert_eq!(range.start(), 100.into());
assert_eq!(range.end(), 200.into());
}
#[test]
fn test_time_range_equal_start_end_returns_ok() {
let result = TimeRange::new(100.into(), 100.into());
assert!(result.is_ok());
let range = result.unwrap();
assert_eq!(range.start(), 100.into());
assert_eq!(range.end(), 100.into());
}
#[test]
fn test_timerange_serialize_roundtrip() {
let ranges = [
TimeRange::new(100.into(), 200.into()).unwrap(),
TimeRange::from(1000.into()),
TimeRange::at(500.into()),
TimeRange::new(i64::MIN.into(), i64::MAX.into()).unwrap(),
TimeRange::new(0.into(), 0.into()).unwrap(),
];
for range in ranges {
let bytes = range.serialize();
assert_eq!(bytes.len(), 24); let (deserialized, consumed) = TimeRange::deserialize(&bytes).unwrap();
assert_eq!(deserialized, range);
assert_eq!(consumed, 24);
}
}
#[test]
fn test_timerange_serialize_into() {
let range = TimeRange::new(100.into(), 200.into()).unwrap();
let mut buffer = Vec::new();
range.serialize_into(&mut buffer);
assert_eq!(buffer.len(), 24); let (deserialized, _) = TimeRange::deserialize(&buffer).unwrap();
assert_eq!(deserialized, range);
}
#[test]
fn test_timerange_deserialize_truncated() {
let result = TimeRange::deserialize(&[0; 23]); assert!(result.is_err());
}
#[test]
fn test_bitemporal_serialize_roundtrip() {
let intervals = [
BiTemporalInterval::current(1000.into()),
BiTemporalInterval::now(500.into(), 600.into()),
BiTemporalInterval::new(
TimeRange::new(100.into(), 200.into()).unwrap(),
TimeRange::new(300.into(), 400.into()).unwrap(),
),
BiTemporalInterval::new(TimeRange::from(0.into()), TimeRange::from(0.into())),
];
for interval in intervals {
let bytes = interval.serialize();
assert_eq!(bytes.len(), 48); let (deserialized, consumed) = BiTemporalInterval::deserialize(&bytes).unwrap();
assert_eq!(deserialized, interval);
assert_eq!(consumed, 48);
}
}
#[test]
fn test_bitemporal_serialize_into() {
let interval = BiTemporalInterval::new(
TimeRange::new(100.into(), 200.into()).unwrap(),
TimeRange::new(300.into(), 400.into()).unwrap(),
);
let mut buffer = Vec::new();
interval.serialize_into(&mut buffer);
assert_eq!(buffer.len(), 48); let (deserialized, _) = BiTemporalInterval::deserialize(&buffer).unwrap();
assert_eq!(deserialized, interval);
}
#[test]
fn test_bitemporal_deserialize_truncated() {
let result = BiTemporalInterval::deserialize(&[0; 47]); assert!(result.is_err());
}
#[test]
fn test_serialization_endianness() {
let range =
TimeRange::new(0x0102030405060708i64.into(), 0x1112131415161718i64.into()).unwrap();
let bytes = range.serialize();
assert_eq!(bytes.len(), 24);
assert_eq!(bytes[0], 0x08, "start.wallclock LSB");
assert_eq!(bytes[7], 0x01, "start.wallclock MSB");
assert_eq!(&bytes[8..12], &[0, 0, 0, 0], "start.logical should be 0");
assert_eq!(bytes[12], 0x18, "end.wallclock LSB");
assert_eq!(bytes[19], 0x11, "end.wallclock MSB");
assert_eq!(&bytes[20..24], &[0, 0, 0, 0], "end.logical should be 0");
}
#[test]
fn test_timestamp_is_hybrid_timestamp() {
let ts = time::now();
assert!(ts.wallclock() > 0);
assert_eq!(ts.logical(), 0);
}
#[test]
fn test_time_range_with_hybrid_timestamps() {
use crate::core::hlc::HybridTimestamp;
let ts1 = HybridTimestamp::new(1000, 0).unwrap();
let ts2 = HybridTimestamp::new(2000, 0).unwrap();
let range = TimeRange::new(ts1, ts2).unwrap();
assert_eq!(range.start(), ts1);
assert_eq!(range.end(), ts2);
}
#[test]
fn test_time_range_ordering_with_logical_component() {
use crate::core::hlc::HybridTimestamp;
let ts1 = HybridTimestamp::new(1000, 0).unwrap();
let ts2 = HybridTimestamp::new(1000, 1).unwrap();
let ts3 = HybridTimestamp::new(1000, 2).unwrap();
assert!(ts1 < ts2);
assert!(ts2 < ts3);
let range = TimeRange::new(ts1, ts3).unwrap();
assert!(range.contains(ts1));
assert!(range.contains(ts2));
assert!(!range.contains(ts3)); }
#[test]
fn test_bitemporal_with_hybrid_timestamps() {
use crate::core::hlc::HybridTimestamp;
let valid_start = HybridTimestamp::new(1000, 0).unwrap();
let tx_start = HybridTimestamp::new(2000, 0).unwrap();
let interval = BiTemporalInterval::now(valid_start, tx_start);
assert_eq!(interval.valid_time().start(), valid_start);
assert_eq!(interval.transaction_time().start(), tx_start);
}
#[test]
fn test_hybrid_timestamp_serialization_size() {
use crate::core::hlc::HybridTimestamp;
let ts = HybridTimestamp::new(1000, 5).unwrap();
let serialized = ts.serialize();
assert_eq!(serialized.len(), 12);
let range = TimeRange::new(
HybridTimestamp::new(1000, 0).unwrap(),
HybridTimestamp::new(2000, 0).unwrap(),
)
.unwrap();
let serialized = range.serialize();
assert_eq!(serialized.len(), 24);
let interval = BiTemporalInterval::now(
HybridTimestamp::new(1000, 0).unwrap(),
HybridTimestamp::new(2000, 0).unwrap(),
);
let serialized = interval.serialize();
assert_eq!(serialized.len(), 48);
}
#[test]
fn test_timestamp_max_with_hybrid_timestamp() {
assert_eq!(TIMESTAMP_MAX.wallclock(), i64::MAX);
assert_eq!(TIMESTAMP_MAX.logical(), 0);
}
#[test]
fn test_contains_or_after_behavior() {
use crate::core::hlc::HybridTimestamp;
let start = HybridTimestamp::new(100, 0).unwrap();
let end = HybridTimestamp::new(200, 0).unwrap();
let range = TimeRange::new(start, end).unwrap();
assert!(range.contains_or_after(start));
assert!(range.contains_or_after(HybridTimestamp::new(150, 0).unwrap()));
assert!(range.contains_or_after(end)); assert!(range.contains_or_after(HybridTimestamp::new(300, 0).unwrap())); assert!(!range.contains_or_after(HybridTimestamp::new(99, 0).unwrap())); }
#[test]
fn test_close_at_start_time() {
use crate::core::hlc::HybridTimestamp;
let start = HybridTimestamp::new(100, 0).unwrap();
let range = TimeRange::from(start);
let closed = range.close_at(start).unwrap();
assert_eq!(closed.start(), start);
assert_eq!(closed.end(), start);
assert!(closed.is_closed());
}
#[test]
fn test_max_valid_timestamp_boundary() {
use crate::core::hlc::HybridTimestamp;
let max_valid = HybridTimestamp::new(MAX_VALID_TIMESTAMP, 0).unwrap();
let range = TimeRange::new(max_valid, max_valid).unwrap();
assert_eq!(range.start(), max_valid);
let range2 = TimeRange::from(max_valid);
assert_eq!(range2.start(), max_valid);
let almost_max = HybridTimestamp::new(MAX_VALID_TIMESTAMP - 100, 0).unwrap();
let range3 = TimeRange::new(almost_max, max_valid).unwrap();
assert_eq!(range3.end(), max_valid);
}
#[test]
fn test_with_valid_time_creates_separate_dimensions() {
use crate::core::hlc::HybridTimestamp;
let valid_from = HybridTimestamp::new(1000, 0).unwrap();
let tx_time = HybridTimestamp::new(2000, 0).unwrap();
let interval = BiTemporalInterval::with_valid_time(valid_from, tx_time);
assert_eq!(interval.valid_time().start(), valid_from);
assert_eq!(interval.transaction_time().start(), tx_time);
assert_ne!(
interval.valid_time().start(),
interval.transaction_time().start()
);
}
#[test]
fn test_with_valid_time_creates_open_ended_ranges() {
use crate::core::hlc::HybridTimestamp;
let valid_from = HybridTimestamp::new(1000, 0).unwrap();
let tx_time = HybridTimestamp::new(2000, 0).unwrap();
let interval = BiTemporalInterval::with_valid_time(valid_from, tx_time);
assert!(interval.valid_time().is_current());
assert!(interval.transaction_time().is_current());
}
#[test]
fn test_with_valid_time_backdated_visibility() {
use crate::core::hlc::HybridTimestamp;
let jan_1 = HybridTimestamp::new(1_704_067_200_000_000, 0).unwrap(); let feb_1 = HybridTimestamp::new(1_706_745_600_000_000, 0).unwrap(); let jan_15 = HybridTimestamp::new(1_705_276_800_000_000, 0).unwrap();
let interval = BiTemporalInterval::with_valid_time(jan_1, feb_1);
assert!(interval.is_visible_at(jan_15, feb_1));
assert!(!interval.is_visible_at(jan_15, jan_15));
}
#[test]
fn test_contains_range_excludes_partial_overlap_start() {
let start = HybridTimestamp::new(100, 0).unwrap();
let end = HybridTimestamp::new(200, 0).unwrap();
let outer = TimeRange::new(start, end).unwrap();
let inner_start = HybridTimestamp::new(50, 0).unwrap();
let inner_end = HybridTimestamp::new(150, 0).unwrap();
let inner = TimeRange::new(inner_start, inner_end).unwrap();
assert!(
!outer.contains_range(&inner),
"Range starting before outer should not be contained"
);
}
#[test]
fn test_time_range_rejects_timestamps_exceeding_max_valid() {
use crate::core::hlc::HybridTimestamp;
let invalid_ts = HybridTimestamp::new_unchecked(MAX_VALID_TIMESTAMP + 1, 0);
let valid_ts = HybridTimestamp::new(100, 0).unwrap();
let result = TimeRange::new(invalid_ts, invalid_ts);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
TemporalError::InvalidTimestamp { .. }
));
let result = TimeRange::new(valid_ts, invalid_ts);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
TemporalError::InvalidTimestamp { .. }
));
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
fn valid_wallclock() -> impl Strategy<Value = i64> {
0..=(MAX_VALID_TIMESTAMP / 2)
}
fn valid_timestamp() -> impl Strategy<Value = Timestamp> {
(valid_wallclock(), any::<u32>())
.prop_map(|(wc, lc)| HybridTimestamp::new_unchecked(wc, lc))
}
fn three_ordered_wallclocks() -> impl Strategy<Value = (i64, i64, i64)> {
(valid_wallclock(), valid_wallclock(), valid_wallclock()).prop_map(|(a, b, c)| {
let mut vals = [a, b, c];
vals.sort();
(vals[0], vals[1], vals[2])
})
}
proptest! {
#[test]
fn prop_timestamp_ordering_is_transitive(
(wc_a, wc_b, wc_c) in three_ordered_wallclocks()
) {
let a = HybridTimestamp::new_unchecked(wc_a, 0);
let b = HybridTimestamp::new_unchecked(wc_b, 0);
let c = HybridTimestamp::new_unchecked(wc_c, 0);
if a < b && b < c {
prop_assert!(a < c, "Transitivity violated: {:?} < {:?} < {:?} but a >= c", a, b, c);
}
}
#[test]
fn prop_timestamp_ordering_transitive_with_logical(
wc in valid_wallclock(),
lc_a in 0u32..1000,
lc_b in 0u32..1000,
lc_c in 0u32..1000,
) {
let a = HybridTimestamp::new_unchecked(wc, lc_a);
let b = HybridTimestamp::new_unchecked(wc, lc_b);
let c = HybridTimestamp::new_unchecked(wc, lc_c);
if a < b && b < c {
prop_assert!(a < c, "Logical counter transitivity violated");
}
}
#[test]
fn prop_timestamp_ordering_antisymmetric(
a in valid_timestamp(),
b in valid_timestamp(),
) {
if a <= b && b <= a {
prop_assert_eq!(a, b);
}
}
#[test]
fn prop_timestamp_ordering_total(
a in valid_timestamp(),
b in valid_timestamp(),
) {
let lt = a < b;
let eq = a == b;
let gt = a > b;
let exactly_one = (lt as u8 + eq as u8 + gt as u8) == 1;
prop_assert!(exactly_one, "Total ordering violated: lt={}, eq={}, gt={}", lt, eq, gt);
}
#[test]
fn prop_time_now_is_after_epoch(_dummy in 0..10u8) {
let ts = time::now();
let epoch = HybridTimestamp::new_unchecked(0, 0);
prop_assert!(ts > epoch, "time::now() should be after epoch");
}
#[test]
fn prop_time_now_monotonic(_dummy in 0..10u8) {
let t1 = time::now();
let t2 = time::now();
prop_assert!(t2 >= t1, "Sequential now() calls should be non-decreasing: {:?} vs {:?}", t1, t2);
}
#[test]
fn prop_time_range_well_formed(
start_wc in valid_wallclock(),
end_wc in valid_wallclock(),
) {
let start = HybridTimestamp::new_unchecked(start_wc, 0);
let end = HybridTimestamp::new_unchecked(end_wc, 0);
let result = TimeRange::new(start, end);
if start <= end {
let range = result.expect("Valid range should succeed");
prop_assert!(range.start() <= range.end(),
"TimeRange should have start <= end");
} else {
prop_assert!(result.is_err(),
"TimeRange::new should reject start > end");
}
}
#[test]
fn prop_time_range_from_is_current(ts in valid_timestamp()) {
let range = TimeRange::from(ts);
prop_assert!(range.is_current(), "TimeRange::from should be current");
prop_assert_eq!(range.start(), ts);
prop_assert_eq!(range.end(), TIMESTAMP_MAX);
}
#[test]
fn prop_time_range_at_is_point(ts in valid_timestamp()) {
let range = TimeRange::at(ts);
prop_assert_eq!(range.start(), range.end());
prop_assert_eq!(range.start(), ts);
}
#[test]
fn prop_time_range_half_open(
(wc_start, wc_end) in (0i64..1_000_000, 1i64..1_000_000)
.prop_map(|(s, delta)| (s, s + delta))
) {
let start = HybridTimestamp::new_unchecked(wc_start, 0);
let end = HybridTimestamp::new_unchecked(wc_end, 0);
let range = TimeRange::new(start, end).unwrap();
prop_assert!(range.contains(start), "Range should contain its start");
prop_assert!(!range.contains(end), "Range should not contain its end (exclusive)");
}
#[test]
fn prop_time_range_contains_semantics(
wc_start in 0i64..500_000,
wc_end in 500_001i64..1_000_000,
wc_point in 0i64..1_000_000,
) {
let start = HybridTimestamp::new_unchecked(wc_start, 0);
let end = HybridTimestamp::new_unchecked(wc_end, 0);
let point = HybridTimestamp::new_unchecked(wc_point, 0);
let range = TimeRange::new(start, end).unwrap();
let expected = point >= start && point < end;
prop_assert_eq!(range.contains(point), expected,
"contains({:?}) mismatch for range [{:?}, {:?})", point, start, end);
}
#[test]
fn prop_time_range_overlaps_symmetric(
wc_a in 0i64..500_000,
len_a in 1i64..500_000,
wc_b in 0i64..500_000,
len_b in 1i64..500_000,
) {
let start_a = HybridTimestamp::new_unchecked(wc_a, 0);
let end_a = HybridTimestamp::new_unchecked(wc_a + len_a, 0);
let start_b = HybridTimestamp::new_unchecked(wc_b, 0);
let end_b = HybridTimestamp::new_unchecked(wc_b + len_b, 0);
let range_a = TimeRange::new(start_a, end_a).unwrap();
let range_b = TimeRange::new(start_b, end_b).unwrap();
prop_assert_eq!(range_a.overlaps(&range_b), range_b.overlaps(&range_a),
"overlaps should be symmetric");
}
#[test]
fn prop_time_range_contains_self(
wc_start in 0i64..500_000,
wc_end in 500_001i64..1_000_000,
) {
let start = HybridTimestamp::new_unchecked(wc_start, 0);
let end = HybridTimestamp::new_unchecked(wc_end, 0);
let range = TimeRange::new(start, end).unwrap();
prop_assert!(range.contains_range(&range),
"A range should always contain itself");
}
#[test]
fn prop_time_range_serialization_roundtrip(
wc_start in valid_wallclock(),
wc_end in valid_wallclock(),
) {
let (s, e) = if wc_start <= wc_end { (wc_start, wc_end) } else { (wc_end, wc_start) };
let start = HybridTimestamp::new_unchecked(s, 0);
let end = HybridTimestamp::new_unchecked(e, 0);
let range = TimeRange::new(start, end).unwrap();
let bytes = range.serialize();
prop_assert_eq!(bytes.len(), 24);
let (deserialized, consumed) = TimeRange::deserialize(&bytes).unwrap();
prop_assert_eq!(consumed, 24);
prop_assert_eq!(deserialized, range);
}
#[test]
fn prop_bitemporal_current_is_current(ts in valid_timestamp()) {
let interval = BiTemporalInterval::current(ts);
prop_assert!(interval.is_currently_valid());
prop_assert!(interval.is_currently_recorded());
prop_assert!(interval.is_current());
}
#[test]
fn prop_bitemporal_serialization_roundtrip(
wc_vs in valid_wallclock(),
wc_ve in valid_wallclock(),
wc_ts in valid_wallclock(),
wc_te in valid_wallclock(),
) {
let (vs, ve) = if wc_vs <= wc_ve { (wc_vs, wc_ve) } else { (wc_ve, wc_vs) };
let (ts, te) = if wc_ts <= wc_te { (wc_ts, wc_te) } else { (wc_te, wc_ts) };
let vt = TimeRange::new(
HybridTimestamp::new_unchecked(vs, 0),
HybridTimestamp::new_unchecked(ve, 0),
).unwrap();
let tt = TimeRange::new(
HybridTimestamp::new_unchecked(ts, 0),
HybridTimestamp::new_unchecked(te, 0),
).unwrap();
let interval = BiTemporalInterval::new(vt, tt);
let bytes = interval.serialize();
prop_assert_eq!(bytes.len(), 48);
let (deserialized, consumed) = BiTemporalInterval::deserialize(&bytes).unwrap();
prop_assert_eq!(consumed, 48);
prop_assert_eq!(deserialized, interval);
}
#[test]
fn prop_closing_valid_time_makes_not_current(
wc_start in 0i64..500_000,
wc_close in 500_001i64..1_000_000,
) {
let start = HybridTimestamp::new_unchecked(wc_start, 0);
let close = HybridTimestamp::new_unchecked(wc_close, 0);
let interval = BiTemporalInterval::current(start);
prop_assert!(interval.is_currently_valid());
let closed = interval.close_valid_time(close).unwrap();
prop_assert!(!closed.is_currently_valid());
prop_assert_eq!(closed.valid_time().end(), close);
}
#[test]
fn prop_closing_tx_time_makes_not_recorded(
wc_start in 0i64..500_000,
wc_close in 500_001i64..1_000_000,
) {
let start = HybridTimestamp::new_unchecked(wc_start, 0);
let close = HybridTimestamp::new_unchecked(wc_close, 0);
let interval = BiTemporalInterval::current(start);
prop_assert!(interval.is_currently_recorded());
let closed = interval.close_transaction_time(close).unwrap();
prop_assert!(!closed.is_currently_recorded());
prop_assert_eq!(closed.transaction_time().end(), close);
}
#[test]
fn prop_time_range_duration_non_negative(
wc_start in valid_wallclock(),
wc_end in valid_wallclock(),
) {
let (s, e) = if wc_start <= wc_end { (wc_start, wc_end) } else { (wc_end, wc_start) };
let start = HybridTimestamp::new_unchecked(s, 0);
let end = HybridTimestamp::new_unchecked(e, 0);
let range = TimeRange::new(start, end).unwrap();
if let Some(duration) = range.duration_micros() {
prop_assert!(duration >= 0,
"Duration should be non-negative, got {}", duration);
}
}
#[test]
fn prop_time_secs_roundtrip(secs in 0i64..1_000_000_000) {
let ts = time::from_secs(secs);
let result = time::to_secs(ts);
prop_assert_eq!(result, secs);
}
#[test]
fn prop_time_millis_roundtrip(millis in 0i64..1_000_000_000_000) {
let ts = time::from_millis(millis);
let result = time::to_millis(ts);
prop_assert_eq!(result, millis);
}
}
}
#[cfg(test)]
mod sentry_tests {
use super::*;
#[test]
fn test_sentry_bitemporal_is_current_mixed_state() {
let valid_start = 1000.into();
let valid_end = 2000.into();
let tx_start = 3000.into();
let interval = BiTemporalInterval::new(
TimeRange::new(valid_start, valid_end).unwrap(), TimeRange::from(tx_start), );
assert!(!interval.is_currently_valid());
assert!(interval.is_currently_recorded());
assert!(
!interval.is_current(),
"is_current() should be false if one dimension is closed"
);
}
#[test]
fn test_sentry_iso8601_format_content() {
let secs = 1609459200; let ts = time::from_secs(secs);
let output = time::to_iso8601(ts);
if cfg!(windows) {
let expected = "132539328000000000";
assert!(
output.contains(expected),
"to_iso8601 output should contain expected intervals on Windows. Got: {}",
output
);
} else {
assert!(
output.contains(&secs.to_string()),
"to_iso8601 output should contain the seconds timestamp. Got: {}",
output
);
}
}
#[test]
fn test_sentry_time_range_display_format() {
let start = 100.into();
let end = 200.into();
let closed = TimeRange::new(start, end).unwrap();
let closed_str = format!("{}", closed);
assert!(closed_str.starts_with("["));
assert!(closed_str.ends_with(")"));
assert!(closed_str.contains(", "));
let open = TimeRange::from(start);
let open_str = format!("{}", open);
assert!(open_str.starts_with("["));
assert!(open_str.ends_with(")"));
assert!(open_str.contains("current"));
}
#[test]
fn test_sentry_overlaps_strict_inequality() {
let r1 = TimeRange::new(100.into(), 200.into()).unwrap();
let r2 = TimeRange::new(200.into(), 300.into()).unwrap();
assert!(
!r2.overlaps(&r1),
"Touching ranges should not overlap (checking symmetry)"
);
}
#[test]
fn test_sentry_contains_range_strict_inequality() {
let outer = TimeRange::new(100.into(), 300.into()).unwrap();
let inner = TimeRange::new(200.into(), 300.into()).unwrap();
assert!(
outer.contains_range(&inner),
"Should contain range ending at exact same time"
);
}
#[test]
fn test_sentry_iso8601_precision() {
let secs = 1609459200;
let micros = 123456;
let ts = HybridTimestamp::new_unchecked(secs * 1_000_000 + micros, 0);
let output = time::to_iso8601(ts);
if cfg!(windows) {
assert!(
output.contains("1234560"),
"Output should contain the fractional ticks (1234560): {}",
output
);
} else {
assert!(
output.contains("123456000"),
"Output should contain nanoseconds (123456000): {}",
output
);
}
}
#[test]
fn test_sentry_max_valid_timestamp_value() {
let large_val = i64::MAX - 2000;
let ts = HybridTimestamp::new_unchecked(large_val, 0);
let result = TimeRange::new(ts, ts);
assert!(
result.is_ok(),
"Should accept large timestamp close to i64::MAX"
);
}
#[test]
fn test_sentry_contains_range_boundary_conditions() {
let outer = TimeRange::new(100.into(), 200.into()).unwrap();
let same_start = TimeRange::new(100.into(), 150.into()).unwrap();
assert!(
outer.contains_range(&same_start),
"Should contain range with same start timestamp"
);
let same_end = TimeRange::new(150.into(), 200.into()).unwrap();
assert!(
outer.contains_range(&same_end),
"Should contain range with same end timestamp"
);
let same_range = TimeRange::new(100.into(), 200.into()).unwrap();
assert!(
outer.contains_range(&same_range),
"Should contain itself (reflexive)"
);
}
#[test]
fn test_sentry_max_valid_timestamp_constant() {
assert_eq!(
MAX_VALID_TIMESTAMP,
i64::MAX - 1000,
"MAX_VALID_TIMESTAMP must be exactly i64::MAX - 1000 to preserve sentinel space"
);
}
#[test]
fn test_sentry_point_range_is_empty() {
let point = TimeRange::at(100.into());
assert!(point.is_empty());
assert!(!point.contains(100.into()));
assert_eq!(point.duration_micros(), Some(0));
}
#[test]
fn test_sentry_point_range_no_overlap() {
let point = TimeRange::at(100.into());
let wide = TimeRange::new(0.into(), 200.into()).unwrap();
assert!(
!point.overlaps(&wide),
"Empty range should not overlap anything"
);
assert!(
!wide.overlaps(&point),
"Range should not overlap empty range"
);
assert!(
!point.overlaps(&point),
"Empty range should not overlap itself"
);
}
#[test]
fn test_sentry_duration_micros_overflow() {
let start = HybridTimestamp::new_unchecked(i64::MIN, 0);
let end = HybridTimestamp::new_unchecked(MAX_VALID_TIMESTAMP, 0);
let range =
TimeRange::new(start, end).expect("Should create valid range with extreme start");
assert_eq!(
range.duration_micros(),
Some(i64::MAX),
"Duration should saturate at i64::MAX on overflow"
);
}
#[test]
fn test_sentry_range_is_not_empty_for_valid_interval() {
let range = TimeRange::new(100.into(), 200.into()).unwrap();
assert!(!range.is_empty(), "Range [100, 200) should not be empty");
}
#[test]
fn test_sentry_timerange_at_timestamp_max() {
let range = TimeRange::at(TIMESTAMP_MAX);
assert_eq!(range.start(), TIMESTAMP_MAX);
assert_eq!(range.end(), TIMESTAMP_MAX);
assert!(range.is_empty());
}
#[test]
fn test_sentry_timerange_new_timestamp_max() {
let range = TimeRange::new(TIMESTAMP_MAX, TIMESTAMP_MAX).unwrap();
assert_eq!(range.start(), TIMESTAMP_MAX);
assert_eq!(range.end(), TIMESTAMP_MAX);
}
#[test]
fn test_sentry_timerange_close_at_timestamp_max() {
let range = TimeRange::from(100.into());
let closed = range.close_at(TIMESTAMP_MAX).unwrap();
assert_eq!(closed.end(), TIMESTAMP_MAX);
assert!(closed.is_current());
}
#[test]
fn test_sentry_deserialize_rejects_inverted_range() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&200i64.to_le_bytes());
bytes.extend_from_slice(&0u32.to_le_bytes());
bytes.extend_from_slice(&100i64.to_le_bytes());
bytes.extend_from_slice(&0u32.to_le_bytes());
let result = TimeRange::deserialize(&bytes);
assert!(result.is_err(), "Should reject range where start > end");
let err = result.unwrap_err();
assert!(format!("{}", err).contains("Deserialized TimeRange invalid"));
}
#[test]
#[should_panic(expected = "exceeds MAX_VALID_TIMESTAMP")]
fn test_sentry_timerange_from_exceeds_max_valid() {
let ts = HybridTimestamp::new_unchecked(MAX_VALID_TIMESTAMP + 1, 0);
let _ = TimeRange::from(ts);
}
#[test]
#[should_panic(expected = "exceeds MAX_VALID_TIMESTAMP")]
fn test_sentry_timerange_at_exceeds_max_valid() {
let ts = HybridTimestamp::new_unchecked(MAX_VALID_TIMESTAMP + 1, 0);
let _ = TimeRange::at(ts);
}
#[test]
fn test_sentry_timerange_close_at_exceeds_max_valid() {
let open = TimeRange::from(100.into());
let ts = HybridTimestamp::new_unchecked(MAX_VALID_TIMESTAMP + 1, 0);
let result = open.close_at(ts);
assert!(result.is_err());
}
#[test]
fn test_sentry_overlaps_empty_range_inside_non_empty() {
let wide = TimeRange::new(100.into(), 200.into()).unwrap();
let point = TimeRange::at(150.into());
assert!(
!wide.overlaps(&point),
"Non-empty range should not overlap an empty range inside it"
);
assert!(
!point.overlaps(&wide),
"Empty range should not overlap a non-empty range"
);
}
#[test]
fn test_sentry_bitemporal_deserialize_excess_bytes() {
let interval = BiTemporalInterval::now(100.into(), 200.into());
let mut bytes = interval.serialize();
bytes.push(0xFF);
let (parsed, consumed) = BiTemporalInterval::deserialize(&bytes).unwrap();
assert_eq!(parsed, interval);
assert_eq!(consumed, 48, "Should consume exactly 48 bytes");
}
}