clickhouse_arrow/native/values/
date.rs

1use std::num::TryFromIntError;
2use std::sync::Arc;
3
4use chrono::{Duration, FixedOffset, NaiveDate, TimeZone, Utc};
5use chrono_tz::{Tz, UTC};
6
7use crate::{Error, FromSql, Result, ToSql, Type, Value, unexpected_type};
8
9/// Wrapper type for `ClickHouse` `Date` type.
10#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Debug, Default)]
11pub struct Date(pub u16);
12
13#[expect(clippy::cast_sign_loss)]
14#[expect(clippy::cast_possible_truncation)]
15impl Date {
16    /// # Panics
17    ///
18    /// Panics if the number of days is out of range for a `u16`.
19    pub fn from_days(days: i32) -> Self {
20        assert!(!(days < 0 || days > i32::from(u16::MAX)), "Date out of range for u16: {days}");
21        Date(days as u16)
22    }
23
24    /// # Panics
25    ///
26    /// Panics if the number of milliseconds is out of range for a `u16`.
27    pub fn from_millis(ms: i64) -> Self {
28        let days = ms / 86_400_000; // Milliseconds per day
29        assert!(!(days < 0 || days > i64::from(u16::MAX)), "Date out of range for u16: {days}");
30        Date(days as u16)
31    }
32}
33
34#[cfg(feature = "serde")]
35impl serde::Serialize for Date {
36    fn serialize<S: serde::Serializer>(
37        &self,
38        serializer: S,
39    ) -> std::result::Result<S::Ok, S::Error> {
40        let date: NaiveDate = (*self).into();
41        date.serialize(serializer)
42    }
43}
44
45#[cfg(feature = "serde")]
46impl<'de> serde::Deserialize<'de> for Date {
47    fn deserialize<D: serde::Deserializer<'de>>(
48        deserializer: D,
49    ) -> std::result::Result<Self, D::Error> {
50        let date: NaiveDate = NaiveDate::deserialize(deserializer)?;
51        Ok(date.into())
52    }
53}
54
55impl ToSql for Date {
56    fn to_sql(self, _type_hint: Option<&Type>) -> Result<Value> { Ok(Value::Date(self)) }
57}
58
59impl FromSql for Date {
60    fn from_sql(type_: &Type, value: Value) -> Result<Self> {
61        if !matches!(type_, Type::Date) {
62            return Err(unexpected_type(type_));
63        }
64        match value {
65            Value::Date(x) => Ok(x),
66            _ => unimplemented!(),
67        }
68    }
69}
70
71impl From<NaiveDate> for Date {
72    fn from(other: NaiveDate) -> Self {
73        #[expect(clippy::cast_possible_truncation)]
74        #[expect(clippy::cast_sign_loss)]
75        Self(other.signed_duration_since(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).num_days()
76            as u16)
77    }
78}
79
80impl From<Date> for NaiveDate {
81    fn from(date: Date) -> Self {
82        NaiveDate::from_ymd_opt(1970, 1, 1).unwrap() + Duration::days(i64::from(date.0))
83    }
84}
85
86/// Wrapper type for `ClickHouse` `Date32` type.
87#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Debug, Default)]
88pub struct Date32(pub i32);
89
90#[expect(clippy::cast_possible_truncation)]
91impl Date32 {
92    /// Creates a `Date32` from days since 1900-01-01.
93    pub fn from_days(days: i32) -> Self { Date32(days) }
94
95    /// Creates a `Date32` from milliseconds since 1970-01-01, adjusting to 1900-01-01 epoch.
96    pub fn from_millis(ms: i64) -> Self {
97        const DAYS_1900_TO_1970: i64 = 25_567; // Days from 1900-01-01 to 1970-01-01
98        let days = ms / 86_400_000; // Milliseconds per day
99        Date32((days - DAYS_1900_TO_1970) as i32)
100    }
101}
102
103#[cfg(feature = "serde")]
104impl serde::Serialize for Date32 {
105    fn serialize<S: serde::Serializer>(
106        &self,
107        serializer: S,
108    ) -> std::result::Result<S::Ok, S::Error> {
109        let date: NaiveDate = (*self).into();
110        date.serialize(serializer)
111    }
112}
113
114#[cfg(feature = "serde")]
115impl<'de> serde::Deserialize<'de> for Date32 {
116    fn deserialize<D: serde::Deserializer<'de>>(
117        deserializer: D,
118    ) -> std::result::Result<Self, D::Error> {
119        let date: NaiveDate = NaiveDate::deserialize(deserializer)?;
120        Ok(date.into())
121    }
122}
123
124impl ToSql for Date32 {
125    fn to_sql(self, _type_hint: Option<&Type>) -> Result<Value> { Ok(Value::Date32(self)) }
126}
127
128impl FromSql for Date32 {
129    fn from_sql(type_: &Type, value: Value) -> Result<Self> {
130        if !matches!(type_, Type::Date32) {
131            return Err(unexpected_type(type_));
132        }
133        match value {
134            Value::Date32(x) => Ok(x),
135            _ => unimplemented!(),
136        }
137    }
138}
139
140impl From<NaiveDate> for Date32 {
141    fn from(other: NaiveDate) -> Self {
142        let days =
143            other.signed_duration_since(NaiveDate::from_ymd_opt(1900, 1, 1).unwrap()).num_days();
144        #[expect(clippy::cast_possible_truncation)]
145        Date32(days as i32)
146    }
147}
148
149impl From<Date32> for NaiveDate {
150    fn from(date: Date32) -> Self {
151        NaiveDate::from_ymd_opt(1900, 1, 1).unwrap() + Duration::days(i64::from(date.0))
152    }
153}
154
155/// Wrapper type for `ClickHouse` `DateTime` type.
156#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
157pub struct DateTime(pub Tz, pub u32);
158
159#[expect(clippy::cast_sign_loss)]
160#[expect(clippy::cast_possible_truncation)]
161impl DateTime {
162    /// Infallible from implemenation for [`chrono::DateTime<Tz>`].
163    ///
164    /// IMPORTANT: This will truncate a value that is too large for the target type.
165    /// Use `TryFrom` for the fallible version.
166    #[must_use]
167    pub fn from_chrono_infallible(other: chrono::DateTime<Tz>) -> Self {
168        // Get the timestamp and convert directly to u32, letting the error bubble up
169        let timestamp = other.timestamp() as u32;
170        Self(other.timezone(), timestamp)
171    }
172
173    /// Infallible from implemenation for [`chrono::DateTime<Utc>`].
174    ///
175    /// IMPORTANT: This will truncate a value that is too large for the target type.
176    /// Use `TryFrom` for the fallible version.
177    #[must_use]
178    pub fn from_chrono_infallible_utc(other: chrono::DateTime<Utc>) -> Self {
179        // Get the timestamp and convert directly to u32, letting the error bubble up
180        let timestamp = other.timestamp() as u32;
181        Self(UTC, timestamp)
182    }
183
184    /// # Panics
185    ///
186    /// Panics if the number of seconds is out of range for a `u32`.
187    pub fn from_seconds(seconds: i64, tz: Option<Arc<str>>) -> Self {
188        let tz = tz.map_or(UTC, |s| s.parse::<Tz>().unwrap());
189        assert!(
190            !(seconds < 0 || seconds > i64::from(u32::MAX)),
191            "DateTime out of range for u32: {seconds}"
192        );
193        DateTime(tz, seconds as u32)
194    }
195
196    /// # Panics
197    ///
198    /// Panics if the number of milliseconds is out of range for a `u32`.
199    pub fn from_millis(ms: i64, tz: Option<Arc<str>>) -> Self {
200        let seconds = ms / 1000;
201        assert!(
202            !(seconds < 0 || seconds > i64::from(u32::MAX)),
203            "DateTime out of range for u32: {seconds}"
204        );
205        DateTime(tz.map_or(UTC, |s| s.parse::<Tz>().unwrap()), seconds as u32)
206    }
207
208    /// # Panics
209    ///
210    /// Panics if the number of microseconds is out of range for a `u32`.
211    pub fn from_micros(us: i64, tz: Option<Arc<str>>) -> Self {
212        let seconds = us / 1_000_000;
213        assert!(
214            !(seconds < 0 || seconds > i64::from(u32::MAX)),
215            "DateTime out of range for u32: {seconds}"
216        );
217        DateTime(tz.map_or(UTC, |s| s.parse::<Tz>().unwrap()), seconds as u32)
218    }
219
220    /// # Panics
221    ///
222    /// Panics if the number of nanoseconds is out of range for a `u32`.
223    pub fn from_nanos(ns: i64, tz: Option<Arc<str>>) -> Self {
224        let seconds = ns / 1_000_000_000;
225        assert!(
226            !(seconds < 0 || seconds > i64::from(u32::MAX)),
227            "DateTime out of range for u32: {seconds}"
228        );
229        DateTime(tz.map_or(UTC, |s| s.parse::<Tz>().unwrap()), seconds as u32)
230    }
231}
232
233#[cfg(feature = "serde")]
234impl serde::Serialize for DateTime {
235    fn serialize<S: serde::Serializer>(
236        &self,
237        serializer: S,
238    ) -> std::result::Result<S::Ok, S::Error> {
239        let date: chrono::DateTime<Tz> = (*self)
240            .try_into()
241            .map_err(|e: TryFromIntError| serde::ser::Error::custom(e.to_string()))?;
242        date.to_rfc3339().serialize(serializer)
243    }
244}
245
246#[cfg(feature = "serde")]
247impl<'de> serde::Deserialize<'de> for DateTime {
248    fn deserialize<D: serde::Deserializer<'de>>(
249        deserializer: D,
250    ) -> std::result::Result<Self, D::Error> {
251        let raw: String = String::deserialize(deserializer)?;
252        let date: chrono::DateTime<FixedOffset> =
253            chrono::DateTime::<FixedOffset>::parse_from_rfc3339(&raw)
254                .map_err(|e: chrono::ParseError| serde::de::Error::custom(e.to_string()))?;
255
256        date.try_into().map_err(|e: TryFromIntError| serde::de::Error::custom(e.to_string()))
257    }
258}
259
260impl ToSql for DateTime {
261    fn to_sql(self, _type_hint: Option<&Type>) -> Result<Value> { Ok(Value::DateTime(self)) }
262}
263
264impl FromSql for DateTime {
265    fn from_sql(type_: &Type, value: Value) -> Result<Self> {
266        if !matches!(type_, Type::DateTime(_)) {
267            return Err(unexpected_type(type_));
268        }
269        match value {
270            Value::DateTime(x) => Ok(x),
271            _ => unimplemented!(),
272        }
273    }
274}
275
276impl Default for DateTime {
277    fn default() -> Self { Self(UTC, 0) }
278}
279
280impl TryFrom<DateTime> for chrono::DateTime<Tz> {
281    type Error = TryFromIntError;
282
283    fn try_from(date: DateTime) -> Result<Self, TryFromIntError> {
284        Ok(date.0.timestamp_opt(date.1.into(), 0).unwrap())
285    }
286}
287
288impl TryFrom<DateTime> for chrono::DateTime<FixedOffset> {
289    type Error = TryFromIntError;
290
291    fn try_from(date: DateTime) -> Result<Self, TryFromIntError> {
292        Ok(date.0.timestamp_opt(date.1.into(), 0).unwrap().fixed_offset())
293    }
294}
295
296impl TryFrom<DateTime> for chrono::DateTime<Utc> {
297    type Error = TryFromIntError;
298
299    fn try_from(date: DateTime) -> Result<Self, TryFromIntError> {
300        Ok(date.0.timestamp_opt(date.1.into(), 0).unwrap().with_timezone(&Utc))
301    }
302}
303
304impl TryFrom<chrono::DateTime<Tz>> for DateTime {
305    type Error = TryFromIntError;
306
307    fn try_from(other: chrono::DateTime<Tz>) -> Result<Self, TryFromIntError> {
308        // Get the timestamp and convert directly to u32, letting the error bubble up
309        let timestamp = u32::try_from(other.timestamp())?;
310        Ok(Self(other.timezone(), timestamp))
311    }
312}
313
314impl TryFrom<chrono::DateTime<FixedOffset>> for DateTime {
315    type Error = TryFromIntError;
316
317    fn try_from(other: chrono::DateTime<FixedOffset>) -> Result<Self, TryFromIntError> {
318        Tz::UTC.from_utc_datetime(&other.naive_utc()).with_timezone(&other.timezone()).try_into()
319    }
320}
321
322impl TryFrom<chrono::DateTime<Utc>> for DateTime {
323    type Error = TryFromIntError;
324
325    fn try_from(other: chrono::DateTime<Utc>) -> Result<Self, TryFromIntError> {
326        Ok(Self(UTC, other.timestamp().try_into()?))
327    }
328}
329
330/// Wrapper type for `ClickHouse` `DateTime64` type.
331#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
332pub struct DateTime64<const PRECISION: usize>(pub Tz, pub u64);
333
334/// Wrapper type for `ClickHouse` `DateTime64` type with dynamic precision.
335#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
336pub struct DynDateTime64(pub Tz, pub u64, pub usize);
337
338// TODO: Remove all panics, return error
339#[expect(clippy::cast_sign_loss)]
340impl DynDateTime64 {
341    /// # Panics
342    ///
343    /// Panics if seconds is negative.
344    pub fn from_seconds(seconds: i64, tz: Option<Arc<str>>) -> Self {
345        let tz = tz.map_or(UTC, |s| s.parse::<Tz>().unwrap());
346        assert!(seconds >= 0, "DynDateTime64 does not support negative seconds: {seconds}");
347        DynDateTime64(tz, seconds as u64, 0) // Precision 0 for seconds
348    }
349
350    /// # Panics
351    ///
352    /// Panics if milliseconds is negative.
353    pub fn from_millis(ms: i64, tz: Option<Arc<str>>) -> Self {
354        let tz = tz.map_or(UTC, |s| s.parse::<Tz>().unwrap());
355        assert!(ms >= 0, "DynDateTime64 does not support negative milliseconds: {ms}");
356        DynDateTime64(tz, ms as u64, 3) // Precision 3 for milliseconds
357    }
358
359    /// # Panics
360    ///
361    /// Panics if micros is negative.
362    pub fn from_micros(us: i64, tz: Option<Arc<str>>) -> Self {
363        let tz = tz.map_or(UTC, |s| s.parse::<Tz>().unwrap());
364        assert!(us >= 0, "DynDateTime64 does not support negative microseconds: {us}");
365        DynDateTime64(tz, us as u64, 6) // Precision 6 for microseconds
366    }
367
368    /// # Panics
369    ///
370    /// Panics if nanos is negative.
371    pub fn from_nanos(ns: i64, tz: Option<Arc<str>>) -> Self {
372        let tz = tz.map_or(UTC, |s| s.parse::<Tz>().unwrap());
373        assert!(ns >= 0, "DynDateTime64 does not support negative nanoseconds: {ns}");
374        DynDateTime64(tz, ns as u64, 9) // Precision 9, adjust for ClickHouse
375    }
376}
377
378impl<const PRECISION: usize> From<DateTime64<PRECISION>> for DynDateTime64 {
379    fn from(value: DateTime64<PRECISION>) -> Self { Self(value.0, value.1, PRECISION) }
380}
381
382#[cfg(feature = "serde")]
383impl serde::Serialize for DynDateTime64 {
384    fn serialize<S: serde::Serializer>(
385        &self,
386        serializer: S,
387    ) -> std::result::Result<S::Ok, S::Error> {
388        let date: chrono::DateTime<Tz> = (*self)
389            .try_into()
390            .map_err(|e: TryFromIntError| serde::ser::Error::custom(e.to_string()))?;
391        date.to_rfc3339().serialize(serializer)
392    }
393}
394
395#[cfg(feature = "serde")]
396impl<'de> serde::Deserialize<'de> for DynDateTime64 {
397    fn deserialize<D: serde::Deserializer<'de>>(
398        deserializer: D,
399    ) -> std::result::Result<Self, D::Error> {
400        let raw: String = String::deserialize(deserializer)?;
401        let date: chrono::DateTime<Utc> = Utc.from_utc_datetime(
402            &chrono::DateTime::<FixedOffset>::parse_from_rfc3339(&raw)
403                .map_err(|e: chrono::ParseError| serde::de::Error::custom(e.to_string()))?
404                .naive_utc(),
405        );
406
407        DynDateTime64::try_from_utc(date, 6)
408            .map_err(|e: TryFromIntError| serde::de::Error::custom(e.to_string()))
409    }
410}
411
412#[cfg(feature = "serde")]
413impl<const PRECISION: usize> serde::Serialize for DateTime64<PRECISION> {
414    fn serialize<S: serde::Serializer>(
415        &self,
416        serializer: S,
417    ) -> std::result::Result<S::Ok, S::Error> {
418        let date: chrono::DateTime<Tz> = (*self)
419            .try_into()
420            .map_err(|e: TryFromIntError| serde::ser::Error::custom(e.to_string()))?;
421        date.to_rfc3339().serialize(serializer)
422    }
423}
424
425#[cfg(feature = "serde")]
426impl<'de, const PRECISION: usize> serde::Deserialize<'de> for DateTime64<PRECISION> {
427    fn deserialize<D: serde::Deserializer<'de>>(
428        deserializer: D,
429    ) -> std::result::Result<Self, D::Error> {
430        let raw: String = String::deserialize(deserializer)?;
431        let date: chrono::DateTime<Utc> = Utc.from_utc_datetime(
432            &chrono::DateTime::<FixedOffset>::parse_from_rfc3339(&raw)
433                .map_err(|e: chrono::ParseError| serde::de::Error::custom(e.to_string()))?
434                .naive_utc(),
435        );
436
437        date.try_into().map_err(|e: TryFromIntError| serde::de::Error::custom(e.to_string()))
438    }
439}
440
441impl<const PRECISION: usize> ToSql for DateTime64<PRECISION> {
442    fn to_sql(self, _type_hint: Option<&Type>) -> Result<Value> {
443        Ok(Value::DateTime64(self.into()))
444    }
445}
446
447impl<const PRECISION: usize> FromSql for DateTime64<PRECISION> {
448    fn from_sql(type_: &Type, value: Value) -> Result<Self> {
449        if !matches!(type_, Type::DateTime64(x, _) if *x == PRECISION) {
450            return Err(unexpected_type(type_));
451        }
452        match value {
453            Value::DateTime64(datetime) => Ok(Self(datetime.0, datetime.1)),
454            _ => unimplemented!(),
455        }
456    }
457}
458
459impl<const PRECISION: usize> Default for DateTime64<PRECISION> {
460    fn default() -> Self { Self(UTC, 0) }
461}
462
463impl ToSql for chrono::DateTime<Utc> {
464    fn to_sql(self, _type_hint: Option<&Type>) -> Result<Value> {
465        Ok(Value::DateTime64(DynDateTime64(
466            UTC,
467            self.timestamp_micros().try_into().map_err(|e| {
468                Error::DeserializeError(format!("failed to convert DateTime64: {e:?}"))
469            })?,
470            6,
471        )))
472    }
473}
474
475impl FromSql for chrono::DateTime<Utc> {
476    fn from_sql(type_: &Type, value: Value) -> Result<Self> {
477        if !matches!(type_, Type::DateTime64(_, _) | Type::DateTime(_)) {
478            return Err(unexpected_type(type_));
479        }
480        match value {
481            Value::DateTime64(datetime) => {
482                #[expect(clippy::cast_possible_truncation)]
483                let datetime_2 = datetime.2 as u32;
484                let seconds = datetime.1 / 10u64.pow(datetime_2);
485                let units = datetime.1 % 10u64.pow(datetime_2);
486                let units_ns = units * 10u64.pow(9 - datetime_2);
487                let (seconds, units_ns): (i64, u32) =
488                    seconds.try_into().and_then(|k| Ok((k, units_ns.try_into()?))).map_err(
489                        |e| Error::DeserializeError(format!("failed to convert DateTime: {e:?}")),
490                    )?;
491                Ok(datetime.0.timestamp_opt(seconds, units_ns).unwrap().with_timezone(&Utc))
492            }
493            Value::DateTime(date) => Ok(date.try_into().map_err(|e| {
494                Error::DeserializeError(format!("failed to convert DateTime: {e:?}"))
495            })?),
496            _ => unimplemented!(),
497        }
498    }
499}
500
501impl<const PRECISION: usize> TryFrom<DateTime64<PRECISION>> for chrono::DateTime<Utc> {
502    type Error = TryFromIntError;
503
504    fn try_from(date: DateTime64<PRECISION>) -> Result<Self, TryFromIntError> {
505        #[expect(clippy::cast_possible_truncation)]
506        let precision = PRECISION as u32;
507        let seconds = date.1 / 10u64.pow(precision);
508        let units = date.1 % 10u64.pow(precision);
509        let units_ns = units * 10u64.pow(9 - precision);
510        Ok(date
511            .0
512            .timestamp_opt(seconds.try_into()?, units_ns.try_into()?)
513            .unwrap()
514            .with_timezone(&Utc))
515    }
516}
517
518impl TryFrom<DynDateTime64> for chrono::DateTime<Utc> {
519    type Error = TryFromIntError;
520
521    fn try_from(date: DynDateTime64) -> Result<Self, TryFromIntError> {
522        #[expect(clippy::cast_possible_truncation)]
523        let date_2 = date.2 as u32;
524        let seconds = date.1 / 10u64.pow(date_2);
525        let units = date.1 % 10u64.pow(date_2);
526        let units_ns = units * 10u64.pow(9 - date_2);
527        Ok(date
528            .0
529            .timestamp_opt(seconds.try_into()?, units_ns.try_into()?)
530            .unwrap()
531            .with_timezone(&Utc))
532    }
533}
534
535impl ToSql for chrono::DateTime<Tz> {
536    fn to_sql(self, _type_hint: Option<&Type>) -> Result<Value> {
537        Ok(Value::DateTime64(DynDateTime64(
538            self.timezone(),
539            self.timestamp_micros().try_into().map_err(|e| {
540                Error::DeserializeError(format!("failed to convert DateTime64: {e:?}"))
541            })?,
542            6,
543        )))
544    }
545}
546
547impl FromSql for chrono::DateTime<Tz> {
548    fn from_sql(type_: &Type, value: Value) -> Result<Self> {
549        if !matches!(type_, Type::DateTime64(_, _) | Type::DateTime(_)) {
550            return Err(unexpected_type(type_));
551        }
552        match value {
553            Value::DateTime64(datetime) => {
554                #[expect(clippy::cast_possible_truncation)]
555                let datetime_2 = datetime.2 as u32;
556                let seconds = datetime.1 / 10u64.pow(datetime_2);
557                let units = datetime.1 % 10u64.pow(datetime_2);
558                let units_ns = units * 10u64.pow(9 - datetime_2);
559                let (seconds, units_ns): (i64, u32) =
560                    seconds.try_into().and_then(|k| Ok((k, units_ns.try_into()?))).map_err(
561                        |e| Error::DeserializeError(format!("failed to convert DateTime: {e:?}")),
562                    )?;
563                Ok(datetime.0.timestamp_opt(seconds, units_ns).unwrap())
564            }
565            Value::DateTime(date) => Ok(date.try_into().map_err(|e| {
566                Error::DeserializeError(format!("failed to convert DateTime: {e:?}"))
567            })?),
568            _ => unimplemented!(),
569        }
570    }
571}
572
573impl<const PRECISION: usize> TryFrom<chrono::DateTime<Utc>> for DateTime64<PRECISION> {
574    type Error = TryFromIntError;
575
576    fn try_from(other: chrono::DateTime<Utc>) -> Result<Self, TryFromIntError> {
577        #[expect(clippy::cast_possible_truncation)]
578        let precision = PRECISION as u32;
579        let seconds: u64 = other.timestamp().try_into()?;
580        let sub_seconds: u64 = u64::from(other.timestamp_subsec_nanos());
581        let total = seconds * 10u64.pow(precision) + sub_seconds / 10u64.pow(9 - precision);
582        Ok(Self(UTC, total))
583    }
584}
585
586impl DynDateTime64 {
587    /// # Errors
588    ///
589    /// Returns an error if the timestamp cannot be converted to a u64.
590    pub fn try_from_utc(
591        other: chrono::DateTime<Utc>,
592        precision: usize,
593    ) -> Result<Self, TryFromIntError> {
594        #[expect(clippy::cast_possible_truncation)]
595        let precision_u32 = precision as u32;
596        let seconds: u64 = other.timestamp().try_into()?;
597        let sub_seconds: u64 = u64::from(other.timestamp_subsec_nanos());
598        let total = seconds * 10u64.pow(precision_u32) + sub_seconds / 10u64.pow(9 - precision_u32);
599        Ok(Self(UTC, total, precision))
600    }
601}
602
603impl<const PRECISION: usize> TryFrom<DateTime64<PRECISION>> for chrono::DateTime<Tz> {
604    type Error = TryFromIntError;
605
606    fn try_from(date: DateTime64<PRECISION>) -> Result<Self, TryFromIntError> {
607        #[expect(clippy::cast_possible_truncation)]
608        let precision = PRECISION as u32;
609        let seconds = date.1 / 10u64.pow(precision);
610        let units = date.1 % 10u64.pow(precision);
611        let units_ns = units * 10u64.pow(9 - precision);
612        Ok(date.0.timestamp_opt(seconds.try_into()?, units_ns.try_into()?).unwrap())
613    }
614}
615
616impl TryFrom<DynDateTime64> for chrono::DateTime<Tz> {
617    type Error = TryFromIntError;
618
619    fn try_from(date: DynDateTime64) -> Result<Self, TryFromIntError> {
620        #[expect(clippy::cast_possible_truncation)]
621        let date_2 = date.2 as u32;
622        let seconds = date.1 / 10u64.pow(date_2);
623        let units = date.1 % 10u64.pow(date_2);
624        let units_ns = units * 10u64.pow(9 - date_2);
625        Ok(date.0.timestamp_opt(seconds.try_into()?, units_ns.try_into()?).unwrap())
626    }
627}
628
629impl<const PRECISION: usize> TryFrom<chrono::DateTime<Tz>> for DateTime64<PRECISION> {
630    type Error = TryFromIntError;
631
632    fn try_from(other: chrono::DateTime<Tz>) -> Result<Self, TryFromIntError> {
633        let seconds: u64 = other.timestamp().try_into()?;
634        let sub_seconds: u64 = u64::from(other.timestamp_subsec_nanos());
635        #[expect(clippy::cast_possible_truncation)]
636        let total =
637            seconds * 10u64.pow(PRECISION as u32) + sub_seconds / 10u64.pow(9 - PRECISION as u32);
638        Ok(Self(other.timezone(), total))
639    }
640}
641
642impl DynDateTime64 {
643    /// # Errors
644    ///
645    /// Returns an error if the timestamp cannot be converted to a u64.
646    pub fn try_from_tz(
647        other: chrono::DateTime<Tz>,
648        precision: usize,
649    ) -> Result<Self, TryFromIntError> {
650        let seconds: u64 = other.timestamp().try_into()?;
651        let sub_seconds: u64 = u64::from(other.timestamp_subsec_nanos());
652        #[expect(clippy::cast_possible_truncation)]
653        let total =
654            seconds * 10u64.pow(precision as u32) + sub_seconds / 10u64.pow(9 - precision as u32);
655        Ok(Self(other.timezone(), total, precision))
656    }
657}
658
659impl<const PRECISION: usize> TryFrom<DateTime64<PRECISION>> for chrono::DateTime<FixedOffset> {
660    type Error = TryFromIntError;
661
662    fn try_from(date: DateTime64<PRECISION>) -> Result<Self, TryFromIntError> {
663        #[expect(clippy::cast_possible_truncation)]
664        let precision = PRECISION as u32;
665        let seconds = date.1 / 10u64.pow(precision);
666        let units = date.1 % 10u64.pow(precision);
667        let units_ns = units * 10u64.pow(9 - precision);
668        Ok(date.0.timestamp_opt(seconds.try_into()?, units_ns.try_into()?).unwrap().fixed_offset())
669    }
670}
671
672impl TryFrom<DynDateTime64> for chrono::DateTime<FixedOffset> {
673    type Error = TryFromIntError;
674
675    fn try_from(date: DynDateTime64) -> Result<Self, TryFromIntError> {
676        #[expect(clippy::cast_possible_truncation)]
677        let date_2 = date.2 as u32;
678        let seconds = date.1 / 10u64.pow(date_2);
679        let units = date.1 % 10u64.pow(date_2);
680        let units_ns = units * 10u64.pow(9 - date_2);
681        Ok(date.0.timestamp_opt(seconds.try_into()?, units_ns.try_into()?).unwrap().fixed_offset())
682    }
683}
684
685#[cfg(test)]
686mod chrono_tests {
687    use chrono::TimeZone;
688    use chrono_tz::UTC;
689
690    use super::*;
691
692    #[test]
693    fn test_naivedate() {
694        for i in 0..30000u16 {
695            let date = Date(i);
696            let chrono_date: NaiveDate = date.into();
697            let new_date = Date::from(chrono_date);
698            assert_eq!(new_date, date);
699        }
700    }
701
702    #[test]
703    fn test_datetime() {
704        for i in (0..30000u32).map(|x| x * 10000) {
705            let date = DateTime(UTC, i);
706            let chrono_date: chrono::DateTime<Tz> = date.try_into().unwrap();
707            let new_date = DateTime::try_from(chrono_date).unwrap();
708            assert_eq!(new_date, date);
709        }
710    }
711
712    #[test]
713    fn test_datetime64() {
714        for i in (0..30000u64).map(|x| x * 10000) {
715            let date = DateTime64::<6>(UTC, i);
716            let chrono_date: chrono::DateTime<Tz> = date.try_into().unwrap();
717            let new_date = DateTime64::try_from(chrono_date).unwrap();
718            assert_eq!(new_date, date);
719        }
720    }
721
722    #[test]
723    fn test_datetime64_precision() {
724        for i in (0..30000u64).map(|x| x * 10000) {
725            let date = DateTime64::<6>(UTC, i);
726            let date_value = date.to_sql(None).unwrap();
727            assert_eq!(date_value, Value::DateTime64(DynDateTime64(UTC, i, 6)));
728            let chrono_date: chrono::DateTime<Utc> =
729                FromSql::from_sql(&Type::DateTime64(6, UTC), date_value).unwrap();
730            let new_date = DateTime64::try_from(chrono_date).unwrap();
731            assert_eq!(new_date, date);
732        }
733    }
734
735    #[test]
736    fn test_datetime64_precision2() {
737        for i in (0..300u64).map(|x| x * 1_000_000) {
738            #[expect(clippy::cast_possible_wrap)]
739            #[expect(clippy::cast_possible_truncation)]
740            let chrono_time = Utc.timestamp_opt(i as i64, i as u32).unwrap();
741            let date = chrono_time.to_sql(None).unwrap();
742            let out_time: chrono::DateTime<Utc> =
743                FromSql::from_sql(&Type::DateTime64(9, UTC), date.clone()).unwrap();
744            assert_eq!(chrono_time, out_time);
745            let date = match date {
746                Value::DateTime64(mut datetime) => {
747                    datetime.2 -= 3;
748                    datetime.1 /= 1000;
749                    Value::DateTime64(datetime)
750                }
751                _ => unimplemented!(),
752            };
753            let out_time: chrono::DateTime<Utc> =
754                FromSql::from_sql(&Type::DateTime64(9, UTC), date.clone()).unwrap();
755
756            assert_eq!(chrono_time, out_time);
757        }
758    }
759
760    #[test]
761    fn test_from_seconds() {
762        let dt = DynDateTime64::from_seconds(1000, Some(Arc::from("UTC")));
763        assert_eq!(dt.0, Tz::UTC);
764        assert_eq!(dt.1, 1000);
765        assert_eq!(dt.2, 0);
766    }
767
768    #[test]
769    fn test_from_millis() {
770        let dt = DynDateTime64::from_millis(1000, Some(Arc::from("UTC")));
771        assert_eq!(dt.0, Tz::UTC);
772        assert_eq!(dt.1, 1000); // 1000 ms
773        assert_eq!(dt.2, 3);
774    }
775
776    #[test]
777    fn test_from_micros() {
778        let dt = DynDateTime64::from_micros(1_000_000, Some(Arc::from("UTC")));
779        assert_eq!(dt.0, Tz::UTC);
780        assert_eq!(dt.1, 1_000_000); // 1,000,000 µs
781        assert_eq!(dt.2, 6);
782    }
783
784    #[test]
785    fn test_from_nanos() {
786        let dt = DynDateTime64::from_nanos(1_000_000_000, Some(Arc::from("UTC")));
787        assert_eq!(dt.0, Tz::UTC);
788        assert_eq!(dt.1, 1_000_000_000); // 1,000,000,000 ns
789        assert_eq!(dt.2, 9);
790    }
791
792    #[test]
793    fn test_from_seconds_zero() {
794        let dt = DynDateTime64::from_seconds(0, None);
795        assert_eq!(dt.0, Tz::UTC);
796        assert_eq!(dt.1, 0);
797        assert_eq!(dt.2, 0);
798    }
799
800    #[test]
801    #[should_panic(expected = "DynDateTime64 does not support negative seconds: -1")]
802    fn test_from_seconds_negative() {
803        let _ = DynDateTime64::from_seconds(-1, Some(Arc::from("UTC")));
804    }
805
806    #[test]
807    #[should_panic(expected = "DynDateTime64 does not support negative milliseconds: -1000")]
808    fn test_from_millis_negative() {
809        let _ = DynDateTime64::from_millis(-1000, Some(Arc::from("UTC")));
810    }
811
812    #[test]
813    #[should_panic(expected = "DynDateTime64 does not support negative microseconds: -1000000")]
814    fn test_from_micros_negative() {
815        let _ = DynDateTime64::from_micros(-1_000_000, Some(Arc::from("UTC")));
816    }
817
818    #[test]
819    #[should_panic(expected = "DynDateTime64 does not support negative nanoseconds: -1000000000")]
820    fn test_from_nanos_negative() {
821        let _ = DynDateTime64::from_nanos(-1_000_000_000, Some(Arc::from("UTC")));
822    }
823
824    #[test]
825    fn test_from_millis_custom_tz() {
826        let dt = DynDateTime64::from_millis(1000, Some(Arc::from("America/New_York")));
827        assert_eq!(dt.0, Tz::America__New_York);
828        assert_eq!(dt.1, 1000);
829        assert_eq!(dt.2, 3);
830    }
831
832    #[test]
833    #[allow(deprecated)]
834    fn test_consistency_with_convert_for_str() {
835        let test_date = "2022-04-22 00:00:00";
836
837        let dt = chrono::NaiveDateTime::parse_from_str(test_date, "%Y-%m-%d %H:%M:%S").unwrap();
838
839        let chrono_date =
840            chrono::DateTime::<Tz>::from_utc(dt, chrono_tz::UTC.offset_from_utc_datetime(&dt));
841
842        #[expect(clippy::cast_possible_truncation)]
843        #[expect(clippy::cast_sign_loss)]
844        let date = DateTime(UTC, dt.timestamp() as u32);
845
846        let new_chrono_date: chrono::DateTime<Tz> = date.try_into().unwrap();
847
848        assert_eq!(new_chrono_date, chrono_date);
849    }
850}