Skip to main content

surrealdb_types/value/
datetime.rs

1use std::fmt::{self, Display, Formatter};
2use std::ops::Deref;
3use std::str::FromStr;
4
5use chrono::offset::LocalResult;
6use chrono::{DateTime, SecondsFormat, TimeZone, Utc};
7use serde::{Deserialize, Serialize};
8use surrealdb_types_derive::write_sql;
9
10use crate::sql::{SqlFormat, ToSql};
11use crate::utils::escape::QuoteStr;
12
13/// Represents a datetime value in SurrealDB
14///
15/// A datetime represents a specific point in time, stored as UTC.
16/// This type wraps the `chrono::DateTime<Utc>` type.
17
18#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
19pub struct Datetime(pub(crate) DateTime<Utc>);
20
21impl Default for Datetime {
22	fn default() -> Self {
23		Self(Utc::now())
24	}
25}
26
27impl Datetime {
28	/// The minimum UTC datetime
29	pub const MIN_UTC: Self = Datetime(DateTime::<Utc>::MIN_UTC);
30	/// The maximum UTC datetime
31	pub const MAX_UTC: Self = Datetime(DateTime::<Utc>::MAX_UTC);
32
33	/// Returns the current UTC datetime
34	pub fn now() -> Self {
35		Self(Utc::now())
36	}
37
38	/// Convert into the inner `DateTime<Utc>`
39	pub fn into_inner(self) -> DateTime<Utc> {
40		self.0
41	}
42
43	/// Create a new datetime from a timestamp.
44	pub fn from_timestamp(seconds: i64, nanos: u32) -> Option<Self> {
45		match Utc.timestamp_opt(seconds, nanos) {
46			LocalResult::Single(v) => Some(Self(v)),
47			LocalResult::Ambiguous(_, _) => None,
48			LocalResult::None => None,
49		}
50	}
51}
52
53impl FromStr for Datetime {
54	type Err = anyhow::Error;
55	fn from_str(s: &str) -> Result<Self, Self::Err> {
56		Ok(Self(DateTime::parse_from_rfc3339(s)?.to_utc()))
57	}
58}
59
60impl From<DateTime<Utc>> for Datetime {
61	fn from(v: DateTime<Utc>) -> Self {
62		Self(v)
63	}
64}
65
66impl From<Datetime> for DateTime<Utc> {
67	fn from(x: Datetime) -> Self {
68		x.0
69	}
70}
71
72impl Display for Datetime {
73	fn fmt(&self, f: &mut Formatter) -> fmt::Result {
74		self.0.to_rfc3339_opts(SecondsFormat::AutoSi, true).fmt(f)
75	}
76}
77
78impl ToSql for Datetime {
79	fn fmt_sql(&self, f: &mut String, fmt: SqlFormat) {
80		use crate as surrealdb_types;
81		write_sql!(f, fmt, "d{}", QuoteStr(&self.to_string()));
82	}
83}
84
85impl TryFrom<(i64, u32)> for Datetime {
86	type Error = anyhow::Error;
87
88	fn try_from(v: (i64, u32)) -> Result<Self, Self::Error> {
89		match Utc.timestamp_opt(v.0, v.1) {
90			LocalResult::Single(v) => Ok(Self(v)),
91			err => match err {
92				LocalResult::Single(v) => Ok(Self(v)),
93				LocalResult::Ambiguous(_, _) => {
94					Err(anyhow::anyhow!("Ambiguous timestamp: {}, {}", v.0, v.1))
95				}
96				LocalResult::None => Err(anyhow::anyhow!("Invalid timestamp: {}, {}", v.0, v.1)),
97			},
98		}
99	}
100}
101
102impl Deref for Datetime {
103	type Target = DateTime<Utc>;
104	fn deref(&self) -> &Self::Target {
105		&self.0
106	}
107}
108
109#[cfg(feature = "arbitrary")]
110mod arb {
111	use arbitrary::Arbitrary;
112	use chrono::{FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Offset, Timelike};
113
114	use super::*;
115
116	impl<'a> Arbitrary<'a> for Datetime {
117		fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
118			let date = u.arbitrary::<NaiveDate>()?;
119			let time = u.arbitrary::<NaiveTime>()?;
120			// Arbitrary was able to create times with 60 seconds instead of the 59 second limit.
121			let time = time.with_second(time.second() % 60).expect("0 to 59 is a valid second");
122			let time = time
123				.with_nanosecond(time.nanosecond() % 1_000_000_000)
124				.expect("0 to 999_999_999 is a valid nanosecond");
125
126			let offset = if u.arbitrary()? {
127				Utc.fix()
128			} else {
129				let hour = u.int_in_range(0..=23)?;
130				let minute = u.int_in_range(0..=59)?;
131				if u.arbitrary()? {
132					FixedOffset::west_opt(hour * 3600 + minute * 60)
133						.expect("valid because range was ensured")
134				} else {
135					FixedOffset::east_opt(hour * 3600 + minute * 60)
136						.expect("valid because range was ensured")
137				}
138			};
139
140			let datetime = NaiveDateTime::new(date, time);
141
142			let Some(x) = offset.from_local_datetime(&datetime).earliest() else {
143				return Err(arbitrary::Error::IncorrectFormat);
144			};
145
146			Ok(Datetime(x.with_timezone(&Utc)))
147		}
148	}
149}