sqlx_exasol/types/
chrono.rs

1use std::{
2    borrow::Cow,
3    ops::{Add, Sub},
4};
5
6use chrono::{DateTime, Local, NaiveDate, NaiveDateTime, TimeZone, Utc};
7use serde::Deserialize;
8use sqlx_core::{
9    decode::Decode,
10    encode::{Encode, IsNull},
11    error::BoxDynError,
12    types::Type,
13};
14
15use crate::{
16    arguments::ExaBuffer,
17    database::Exasol,
18    type_info::{ExaDataType, ExaTypeInfo, IntervalDayToSecond, IntervalYearToMonth},
19    value::ExaValueRef,
20};
21
22const TIMESTAMP_FMT: &str = "%Y-%m-%d %H:%M:%S%.6f";
23
24impl Type<Exasol> for DateTime<Utc> {
25    fn type_info() -> ExaTypeInfo {
26        ExaDataType::Timestamp.into()
27    }
28}
29
30impl Encode<'_, Exasol> for DateTime<Utc> {
31    fn encode_by_ref(&self, buf: &mut ExaBuffer) -> Result<IsNull, BoxDynError> {
32        Encode::<Exasol>::encode(self.naive_utc(), buf)
33    }
34
35    fn produces(&self) -> Option<ExaTypeInfo> {
36        Some(ExaDataType::Timestamp.into())
37    }
38
39    fn size_hint(&self) -> usize {
40        TIMESTAMP_FMT.len()
41    }
42}
43
44impl<'r> Decode<'r, Exasol> for DateTime<Utc> {
45    fn decode(value: ExaValueRef<'r>) -> Result<Self, BoxDynError> {
46        let naive: NaiveDateTime = Decode::<Exasol>::decode(value)?;
47        Ok(DateTime::from_naive_utc_and_offset(naive, Utc))
48    }
49}
50
51impl Type<Exasol> for DateTime<Local> {
52    fn type_info() -> ExaTypeInfo {
53        ExaDataType::TimestampWithLocalTimeZone.into()
54    }
55}
56
57impl Encode<'_, Exasol> for DateTime<Local> {
58    fn encode_by_ref(&self, buf: &mut ExaBuffer) -> Result<IsNull, BoxDynError> {
59        Encode::<Exasol>::encode(self.naive_local(), buf)
60    }
61
62    fn produces(&self) -> Option<ExaTypeInfo> {
63        Some(ExaDataType::TimestampWithLocalTimeZone.into())
64    }
65
66    fn size_hint(&self) -> usize {
67        TIMESTAMP_FMT.len()
68    }
69}
70
71impl<'r> Decode<'r, Exasol> for DateTime<Local> {
72    fn decode(value: ExaValueRef<'r>) -> Result<Self, BoxDynError> {
73        let naive: NaiveDateTime = Decode::<Exasol>::decode(value)?;
74        naive
75            .and_local_timezone(Local)
76            .single()
77            .ok_or("cannot uniquely determine timezone offset")
78            .map_err(From::from)
79    }
80}
81
82impl Type<Exasol> for NaiveDateTime {
83    fn type_info() -> ExaTypeInfo {
84        ExaDataType::Timestamp.into()
85    }
86}
87
88impl Encode<'_, Exasol> for NaiveDateTime {
89    fn encode_by_ref(&self, buf: &mut ExaBuffer) -> Result<IsNull, BoxDynError> {
90        buf.append(format_args!("{}", self.format(TIMESTAMP_FMT)))?;
91        Ok(IsNull::No)
92    }
93
94    fn produces(&self) -> Option<ExaTypeInfo> {
95        Some(ExaDataType::Timestamp.into())
96    }
97
98    fn size_hint(&self) -> usize {
99        TIMESTAMP_FMT.len()
100    }
101}
102
103impl Decode<'_, Exasol> for NaiveDateTime {
104    fn decode(value: ExaValueRef<'_>) -> Result<Self, BoxDynError> {
105        let input = Cow::<str>::deserialize(value.value).map_err(Box::new)?;
106        Self::parse_from_str(&input, TIMESTAMP_FMT)
107            .map_err(Box::new)
108            .map_err(From::from)
109    }
110}
111
112impl Type<Exasol> for chrono::Duration {
113    fn type_info() -> ExaTypeInfo {
114        let ids = IntervalDayToSecond::new(
115            IntervalDayToSecond::MAX_PRECISION,
116            IntervalDayToSecond::MAX_SUPPORTED_FRACTION,
117        );
118        ExaDataType::IntervalDayToSecond(ids).into()
119    }
120
121    fn compatible(ty: &ExaTypeInfo) -> bool {
122        <Self as Type<Exasol>>::type_info().compatible(ty)
123    }
124}
125
126impl Encode<'_, Exasol> for chrono::Duration {
127    fn encode_by_ref(&self, buf: &mut ExaBuffer) -> Result<IsNull, BoxDynError> {
128        buf.append(format_args!(
129            "{} {}:{}:{}.{}",
130            self.num_days(),
131            self.num_hours().abs() % 24,
132            self.num_minutes().abs() % 60,
133            self.num_seconds().abs() % 60,
134            self.num_milliseconds().abs() % 1000
135        ))?;
136
137        Ok(IsNull::No)
138    }
139
140    fn produces(&self) -> Option<ExaTypeInfo> {
141        let precision = self
142            .num_days()
143            .unsigned_abs()
144            .checked_ilog10()
145            .unwrap_or_default()
146            + 1;
147
148        let fraction = (self.num_milliseconds() % 1000)
149            .unsigned_abs()
150            .checked_ilog10()
151            .map(|v| v + 1)
152            .unwrap_or_default();
153
154        Some(ExaDataType::IntervalDayToSecond(IntervalDayToSecond::new(precision, fraction)).into())
155    }
156
157    fn size_hint(&self) -> usize {
158        // 1 sign + max days precision + 1 space + 2 hours + 1 column + 2 minutes + 1 column + 2
159        // seconds + 1 dot + max milliseconds fraction
160        1 + IntervalDayToSecond::MAX_PRECISION as usize
161            + 10
162            + IntervalDayToSecond::MAX_SUPPORTED_FRACTION as usize
163    }
164}
165
166impl<'r> Decode<'r, Exasol> for chrono::Duration {
167    fn decode(value: ExaValueRef<'r>) -> Result<Self, BoxDynError> {
168        let input = Cow::<str>::deserialize(value.value).map_err(Box::new)?;
169        let input_err_fn = || format!("could not parse {input} as INTERVAL DAY TO SECOND");
170
171        let (days, rest) = input.split_once(' ').ok_or_else(input_err_fn)?;
172        let (hours, rest) = rest.split_once(':').ok_or_else(input_err_fn)?;
173        let (minutes, rest) = rest.split_once(':').ok_or_else(input_err_fn)?;
174        let (seconds, millis) = rest.split_once('.').ok_or_else(input_err_fn)?;
175
176        let days: i64 = days.parse().map_err(Box::new)?;
177        let hours: i64 = hours.parse().map_err(Box::new)?;
178        let minutes: i64 = minutes.parse().map_err(Box::new)?;
179        let seconds: i64 = seconds.parse().map_err(Box::new)?;
180        let millis: i64 = millis.parse().map_err(Box::new)?;
181        let sign = if days.is_negative() { -1 } else { 1 };
182
183        let duration = chrono::Duration::try_days(days).ok_or_else(input_err_fn)?
184            + chrono::Duration::try_hours(hours * sign).ok_or_else(input_err_fn)?
185            + chrono::Duration::try_minutes(minutes * sign).ok_or_else(input_err_fn)?
186            + chrono::Duration::try_seconds(seconds * sign).ok_or_else(input_err_fn)?
187            + chrono::Duration::try_milliseconds(millis * sign).ok_or_else(input_err_fn)?;
188
189        Ok(duration)
190    }
191}
192
193impl Type<Exasol> for NaiveDate {
194    fn type_info() -> ExaTypeInfo {
195        ExaDataType::Date.into()
196    }
197}
198
199impl Encode<'_, Exasol> for NaiveDate {
200    fn encode_by_ref(&self, buf: &mut ExaBuffer) -> Result<IsNull, BoxDynError> {
201        buf.append(self)?;
202        Ok(IsNull::No)
203    }
204
205    fn produces(&self) -> Option<ExaTypeInfo> {
206        Some(ExaDataType::Date.into())
207    }
208
209    fn size_hint(&self) -> usize {
210        // 4 year + 1 dash + 2 months + 1 dash + 2 days
211        10
212    }
213}
214
215impl Decode<'_, Exasol> for NaiveDate {
216    fn decode(value: ExaValueRef<'_>) -> Result<Self, BoxDynError> {
217        <Self as Deserialize>::deserialize(value.value).map_err(From::from)
218    }
219}
220
221/// A duration in calendar months, analog to [`chrono::Months`].
222/// Unlike [`chrono::Months`], this type can represent a negative duration.
223#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd)]
224pub struct Months(i32);
225
226impl Months {
227    #[must_use]
228    pub const fn new(num: i32) -> Self {
229        Self(num)
230    }
231
232    #[must_use]
233    pub fn num_months(&self) -> i32 {
234        self.0
235    }
236}
237
238impl Type<Exasol> for Months {
239    fn type_info() -> ExaTypeInfo {
240        let iym = IntervalYearToMonth::new(IntervalYearToMonth::MAX_PRECISION);
241        ExaDataType::IntervalYearToMonth(iym).into()
242    }
243
244    fn compatible(ty: &ExaTypeInfo) -> bool {
245        <Self as Type<Exasol>>::type_info().compatible(ty)
246    }
247}
248
249impl Encode<'_, Exasol> for Months {
250    fn encode_by_ref(&self, buf: &mut ExaBuffer) -> Result<IsNull, BoxDynError> {
251        let years = self.0 / 12;
252        let months = (self.0 % 12).abs();
253        buf.append(format_args!("{years}-{months}"))?;
254
255        Ok(IsNull::No)
256    }
257
258    fn produces(&self) -> Option<ExaTypeInfo> {
259        let num_years = self.0 / 12;
260        let precision = num_years
261            .unsigned_abs()
262            .checked_ilog10()
263            .unwrap_or_default()
264            + 1;
265
266        Some(ExaDataType::IntervalYearToMonth(IntervalYearToMonth::new(precision)).into())
267    }
268
269    fn size_hint(&self) -> usize {
270        // 1 sign + max year precision + 1 dash + 2 months
271        1 + IntervalYearToMonth::MAX_PRECISION as usize + 1 + 2
272    }
273}
274
275impl<'r> Decode<'r, Exasol> for Months {
276    fn decode(value: ExaValueRef<'r>) -> Result<Self, BoxDynError> {
277        let input = Cow::<str>::deserialize(value.value).map_err(Box::new)?;
278        let input_err_fn = || format!("could not parse {input} as INTERVAL YEAR TO MONTH");
279
280        let (years, months) = input.rsplit_once('-').ok_or_else(input_err_fn)?;
281
282        let years = years.parse::<i32>().map_err(Box::new)?;
283        let months = months.parse::<i32>().map_err(Box::new)?;
284
285        // The number of months will always get decoded as being positive.
286        // So the sign of the years determines how to add up the months.
287        let total_months = if years.is_negative() {
288            years * 12 - months
289        } else {
290            years * 12 + months
291        };
292
293        Ok(Months::new(total_months))
294    }
295}
296
297impl From<Months> for chrono::Months {
298    fn from(value: Months) -> Self {
299        chrono::Months::new(value.0.unsigned_abs())
300    }
301}
302
303impl<Tz: TimeZone> Add<Months> for DateTime<Tz> {
304    type Output = DateTime<Tz>;
305
306    fn add(self, rhs: Months) -> Self::Output {
307        if rhs.0.is_negative() {
308            self.checked_sub_months(rhs.into()).unwrap()
309        } else {
310            self.checked_add_months(rhs.into()).unwrap()
311        }
312    }
313}
314
315impl Add<Months> for NaiveDate {
316    type Output = NaiveDate;
317
318    fn add(self, rhs: Months) -> Self::Output {
319        if rhs.0.is_negative() {
320            self.checked_sub_months(rhs.into()).unwrap()
321        } else {
322            self.checked_add_months(rhs.into()).unwrap()
323        }
324    }
325}
326
327impl Add<Months> for NaiveDateTime {
328    type Output = NaiveDateTime;
329
330    fn add(self, rhs: Months) -> Self::Output {
331        if rhs.0.is_negative() {
332            self.checked_sub_months(rhs.into()).unwrap()
333        } else {
334            self.checked_add_months(rhs.into()).unwrap()
335        }
336    }
337}
338
339impl<Tz: TimeZone> Sub<Months> for DateTime<Tz> {
340    type Output = DateTime<Tz>;
341
342    fn sub(self, rhs: Months) -> Self::Output {
343        if rhs.0.is_negative() {
344            self.checked_add_months(rhs.into()).unwrap()
345        } else {
346            self.checked_sub_months(rhs.into()).unwrap()
347        }
348    }
349}
350
351impl Sub<Months> for NaiveDate {
352    type Output = NaiveDate;
353
354    fn sub(self, rhs: Months) -> Self::Output {
355        if rhs.0.is_negative() {
356            self.checked_add_months(rhs.into()).unwrap()
357        } else {
358            self.checked_sub_months(rhs.into()).unwrap()
359        }
360    }
361}
362
363impl Sub<Months> for NaiveDateTime {
364    type Output = NaiveDateTime;
365
366    fn sub(self, rhs: Months) -> Self::Output {
367        if rhs.0.is_negative() {
368            self.checked_add_months(rhs.into()).unwrap()
369        } else {
370            self.checked_sub_months(rhs.into()).unwrap()
371        }
372    }
373}