Skip to main content

qail_pg/types/
temporal.rs

1//! Timestamp type conversions for PostgreSQL.
2//!
3//! PostgreSQL timestamps are stored as microseconds since 2000-01-01 00:00:00 UTC.
4
5use super::{FromPg, ToPg, TypeError};
6use crate::protocol::types::oid;
7
8/// PostgreSQL epoch: 2000-01-01 00:00:00 UTC
9/// Difference from Unix epoch (1970-01-01) in microseconds
10const PG_EPOCH_OFFSET_USEC: i64 = 946_684_800_000_000;
11const USEC_PER_DAY: i64 = 86_400_000_000;
12
13/// Timestamp without timezone (microseconds since 2000-01-01)
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct Timestamp {
16    /// Microseconds since PostgreSQL epoch (2000-01-01 00:00:00)
17    pub usec: i64,
18}
19
20impl Timestamp {
21    /// Create from microseconds since PostgreSQL epoch
22    pub fn from_pg_usec(usec: i64) -> Self {
23        Self { usec }
24    }
25
26    /// Create from Unix timestamp (seconds since 1970-01-01)
27    pub fn from_unix_secs(secs: i64) -> Self {
28        Self {
29            usec: secs * 1_000_000 - PG_EPOCH_OFFSET_USEC,
30        }
31    }
32
33    /// Convert to Unix timestamp (seconds since 1970-01-01)
34    pub fn to_unix_secs(&self) -> i64 {
35        (self.usec + PG_EPOCH_OFFSET_USEC) / 1_000_000
36    }
37
38    /// Convert to Unix timestamp with microseconds
39    pub fn to_unix_usec(&self) -> i64 {
40        self.usec + PG_EPOCH_OFFSET_USEC
41    }
42}
43
44impl FromPg for Timestamp {
45    fn from_pg(bytes: &[u8], oid_val: u32, format: i16) -> Result<Self, TypeError> {
46        if oid_val != oid::TIMESTAMP && oid_val != oid::TIMESTAMPTZ {
47            return Err(TypeError::UnexpectedOid {
48                expected: "timestamp",
49                got: oid_val,
50            });
51        }
52
53        if format == 1 {
54            // Binary: 8 bytes, microseconds since 2000-01-01
55            if bytes.len() != 8 {
56                return Err(TypeError::InvalidData(
57                    "Expected 8 bytes for timestamp".to_string(),
58                ));
59            }
60            let usec = i64::from_be_bytes([
61                bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
62            ]);
63            Ok(Timestamp::from_pg_usec(usec))
64        } else {
65            // Text format: parse ISO 8601
66            let s =
67                std::str::from_utf8(bytes).map_err(|e| TypeError::InvalidData(e.to_string()))?;
68            parse_timestamp_text(s)
69        }
70    }
71}
72
73impl ToPg for Timestamp {
74    fn to_pg(&self) -> (Vec<u8>, u32, i16) {
75        (self.usec.to_be_bytes().to_vec(), oid::TIMESTAMP, 1)
76    }
77}
78
79#[cfg(feature = "chrono")]
80impl FromPg for chrono::DateTime<chrono::Utc> {
81    fn from_pg(bytes: &[u8], oid_val: u32, format: i16) -> Result<Self, TypeError> {
82        if oid_val != oid::TIMESTAMP && oid_val != oid::TIMESTAMPTZ {
83            return Err(TypeError::UnexpectedOid {
84                expected: "timestamp",
85                got: oid_val,
86            });
87        }
88
89        if format == 1 {
90            if bytes.len() != 8 {
91                return Err(TypeError::InvalidData(
92                    "Expected 8 bytes for timestamp".to_string(),
93                ));
94            }
95            let pg_usec = i64::from_be_bytes([
96                bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
97            ]);
98            let unix_usec = pg_usec.saturating_add(PG_EPOCH_OFFSET_USEC);
99            chrono::DateTime::<chrono::Utc>::from_timestamp_micros(unix_usec).ok_or_else(|| {
100                TypeError::InvalidData(format!("Timestamp out of range: {}", unix_usec))
101            })
102        } else {
103            let s =
104                std::str::from_utf8(bytes).map_err(|e| TypeError::InvalidData(e.to_string()))?;
105
106            if oid_val == oid::TIMESTAMPTZ {
107                chrono::DateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f%#z")
108                    .or_else(|_| chrono::DateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f%#z"))
109                    .or_else(|_| chrono::DateTime::parse_from_rfc3339(s))
110                    .map(|dt| dt.with_timezone(&chrono::Utc))
111                    .map_err(|e| TypeError::InvalidData(format!("Invalid timestamptz: {}", e)))
112            } else {
113                chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f")
114                    .or_else(|_| chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f"))
115                    .map(|naive| {
116                        chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(
117                            naive,
118                            chrono::Utc,
119                        )
120                    })
121                    .map_err(|e| TypeError::InvalidData(format!("Invalid timestamp: {}", e)))
122            }
123        }
124    }
125}
126
127#[cfg(feature = "chrono")]
128impl ToPg for chrono::DateTime<chrono::Utc> {
129    fn to_pg(&self) -> (Vec<u8>, u32, i16) {
130        let unix_usec = self.timestamp_micros();
131        let pg_usec = unix_usec.saturating_sub(PG_EPOCH_OFFSET_USEC);
132        (pg_usec.to_be_bytes().to_vec(), oid::TIMESTAMPTZ, 1)
133    }
134}
135
136/// Parse PostgreSQL text timestamp format
137fn parse_timestamp_text(s: &str) -> Result<Timestamp, TypeError> {
138    // Format: "2024-12-25 17:30:00" or "2024-12-25 17:30:00.123456"
139    // This is a simplified parser - production would use chrono or time crate
140
141    let parts: Vec<&str> = s.splitn(2, &[' ', 'T'][..]).collect();
142    if parts.len() != 2 {
143        return Err(TypeError::InvalidData(format!("Invalid timestamp: {}", s)));
144    }
145
146    let (year, month, day) = parse_date_components(parts[0])?;
147    let (time_str, timezone_offset_usec) = split_timezone_suffix(parts[1])?;
148    let (hour, minute, second, usec) = parse_time_components(time_str)?;
149    let days_since_epoch = days_from_ymd_checked(year, month, day)?;
150
151    let total_usec = days_since_epoch as i64 * 86_400_000_000
152        + hour as i64 * 3_600_000_000
153        + minute as i64 * 60_000_000
154        + second as i64 * 1_000_000
155        + usec;
156
157    Ok(Timestamp::from_pg_usec(total_usec - timezone_offset_usec))
158}
159
160fn parse_date_components(s: &str) -> Result<(i32, i32, i32), TypeError> {
161    let parts: Vec<&str> = s.split('-').collect();
162    if parts.len() != 3 {
163        return Err(TypeError::InvalidData(format!("Invalid date: {}", s)));
164    }
165    let year = parse_i32_part(parts[0], "year")?;
166    let month = parse_i32_part(parts[1], "month")?;
167    let day = parse_i32_part(parts[2], "day")?;
168    validate_ymd(year, month, day)?;
169    Ok((year, month, day))
170}
171
172fn split_timezone_suffix(s: &str) -> Result<(&str, i64), TypeError> {
173    let s = s.trim_end();
174    if let Some(stripped) = s.strip_suffix('Z') {
175        return Ok((stripped, 0));
176    }
177    if let Some(idx) = s
178        .char_indices()
179        .skip(1)
180        .find_map(|(idx, c)| (c == '+' || c == '-').then_some(idx))
181    {
182        let offset = parse_timezone_offset_usec(&s[idx..]).ok_or_else(|| {
183            TypeError::InvalidData(format!("Invalid timezone offset: {}", &s[idx..]))
184        })?;
185        Ok((&s[..idx], offset))
186    } else {
187        Ok((s, 0))
188    }
189}
190
191fn parse_timezone_offset_usec(s: &str) -> Option<i64> {
192    let sign = match s.as_bytes().first()? {
193        b'+' => 1_i64,
194        b'-' => -1_i64,
195        _ => return None,
196    };
197    let raw = &s[1..];
198    if raw.is_empty() {
199        return None;
200    }
201
202    let (hours, minutes) = if let Some((hours, minutes)) = raw.split_once(':') {
203        (hours, minutes)
204    } else if raw.len() == 4 {
205        (&raw[..2], &raw[2..])
206    } else {
207        (raw, "0")
208    };
209
210    if hours.is_empty() || minutes.is_empty() {
211        return None;
212    }
213    let hours = hours.parse::<i64>().ok()?;
214    let minutes = minutes.parse::<i64>().ok()?;
215    if !(0..=23).contains(&hours) || !(0..=59).contains(&minutes) {
216        return None;
217    }
218
219    Some(sign * ((hours * 3_600 + minutes * 60) * 1_000_000))
220}
221
222fn parse_time_components(s: &str) -> Result<(i32, i32, i32, i64), TypeError> {
223    let parts: Vec<&str> = s.split(':').collect();
224    if !(2..=3).contains(&parts.len()) {
225        return Err(TypeError::InvalidData(format!("Invalid time: {}", s)));
226    }
227
228    let hour = parse_i32_part(parts[0], "hour")?;
229    let minute = parse_i32_part(parts[1], "minute")?;
230    let (second, usec) = if let Some(second_part) = parts.get(2) {
231        parse_second_usec(second_part)?
232    } else {
233        (0, 0)
234    };
235
236    validate_time_components(hour, minute, second, usec)?;
237    Ok((hour, minute, second, usec))
238}
239
240fn parse_second_usec(s: &str) -> Result<(i32, i64), TypeError> {
241    let (second, fraction) = match s.split_once('.') {
242        Some((second, fraction)) => (second, Some(fraction)),
243        None => (s, None),
244    };
245    let second = parse_i32_part(second, "second")?;
246    let usec = match fraction {
247        Some(fraction) => parse_usec_fraction(fraction)?,
248        None => 0,
249    };
250    Ok((second, usec))
251}
252
253fn parse_usec_fraction(s: &str) -> Result<i64, TypeError> {
254    if s.is_empty() || s.len() > 6 || !s.bytes().all(|b| b.is_ascii_digit()) {
255        return Err(TypeError::InvalidData(
256            "Invalid microsecond fraction".to_string(),
257        ));
258    }
259    let padded = format!("{:0<6}", s);
260    padded
261        .parse::<i64>()
262        .map_err(|_| TypeError::InvalidData("Invalid microsecond fraction".to_string()))
263}
264
265fn parse_i32_part(s: &str, label: &str) -> Result<i32, TypeError> {
266    if s.is_empty() {
267        return Err(TypeError::InvalidData(format!("Invalid {}", label)));
268    }
269    s.parse()
270        .map_err(|_| TypeError::InvalidData(format!("Invalid {}", label)))
271}
272
273fn validate_ymd(year: i32, month: i32, day: i32) -> Result<(), TypeError> {
274    if !(1..=12).contains(&month) {
275        return Err(TypeError::InvalidData("Invalid month".to_string()));
276    }
277    let max_day = days_in_month(year, month);
278    if !(1..=max_day).contains(&day) {
279        return Err(TypeError::InvalidData("Invalid day".to_string()));
280    }
281    Ok(())
282}
283
284fn validate_time_components(
285    hour: i32,
286    minute: i32,
287    second: i32,
288    usec: i64,
289) -> Result<(), TypeError> {
290    if !(0..=23).contains(&hour) {
291        return Err(TypeError::InvalidData("Invalid hour".to_string()));
292    }
293    if !(0..=59).contains(&minute) {
294        return Err(TypeError::InvalidData("Invalid minute".to_string()));
295    }
296    if !(0..=59).contains(&second) {
297        return Err(TypeError::InvalidData("Invalid second".to_string()));
298    }
299    if !(0..=999_999).contains(&usec) {
300        return Err(TypeError::InvalidData("Invalid microsecond".to_string()));
301    }
302    Ok(())
303}
304
305fn validate_time_usec(usec: i64) -> Result<(), TypeError> {
306    if !(0..USEC_PER_DAY).contains(&usec) {
307        return Err(TypeError::InvalidData(format!(
308            "Time out of range: {} microseconds",
309            usec
310        )));
311    }
312    Ok(())
313}
314
315fn days_in_month(year: i32, month: i32) -> i32 {
316    match month {
317        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
318        4 | 6 | 9 | 11 => 30,
319        2 if is_leap_year(year) => 29,
320        2 => 28,
321        _ => 0,
322    }
323}
324
325/// Calculate days since 2000-01-01.
326fn days_from_ymd_checked(year: i32, month: i32, day: i32) -> Result<i32, TypeError> {
327    validate_ymd(year, month, day)?;
328    let epoch_days = days_from_civil(2000, 1, 1);
329    let days = days_from_civil(year, month, day)
330        .checked_sub(epoch_days)
331        .ok_or_else(|| TypeError::InvalidData("Date out of range".to_string()))?;
332    i32::try_from(days).map_err(|_| TypeError::InvalidData("Date out of range".to_string()))
333}
334
335fn days_from_civil(year: i32, month: i32, day: i32) -> i64 {
336    let mut year = year as i64;
337    let month = month as i64;
338    let day = day as i64;
339    year -= (month <= 2) as i64;
340    let era = if year >= 0 { year } else { year - 399 } / 400;
341    let year_of_era = year - era * 400;
342    let month_adjusted = month + if month > 2 { -3 } else { 9 };
343    let day_of_year = (153 * month_adjusted + 2) / 5 + day - 1;
344    let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
345    era * 146_097 + day_of_era
346}
347
348fn is_leap_year(year: i32) -> bool {
349    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
350}
351
352/// Date type (days since 2000-01-01)
353#[derive(Debug, Clone, Copy, PartialEq, Eq)]
354pub struct Date {
355    /// Days since PostgreSQL epoch (2000-01-01). Negative values represent dates before the epoch.
356    pub days: i32,
357}
358
359impl FromPg for Date {
360    fn from_pg(bytes: &[u8], oid_val: u32, format: i16) -> Result<Self, TypeError> {
361        if oid_val != oid::DATE {
362            return Err(TypeError::UnexpectedOid {
363                expected: "date",
364                got: oid_val,
365            });
366        }
367
368        if format == 1 {
369            // Binary: 4 bytes, days since 2000-01-01
370            if bytes.len() != 4 {
371                return Err(TypeError::InvalidData(
372                    "Expected 4 bytes for date".to_string(),
373                ));
374            }
375            let days = i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
376            Ok(Date { days })
377        } else {
378            // Text format: YYYY-MM-DD
379            let s =
380                std::str::from_utf8(bytes).map_err(|e| TypeError::InvalidData(e.to_string()))?;
381            let (year, month, day) = parse_date_components(s)?;
382            Ok(Date {
383                days: days_from_ymd_checked(year, month, day)?,
384            })
385        }
386    }
387}
388
389impl ToPg for Date {
390    fn to_pg(&self) -> (Vec<u8>, u32, i16) {
391        (self.days.to_be_bytes().to_vec(), oid::DATE, 1)
392    }
393}
394
395/// Time type (microseconds since midnight)
396#[derive(Debug, Clone, Copy, PartialEq, Eq)]
397pub struct Time {
398    /// Microseconds since midnight
399    pub usec: i64,
400}
401
402impl Time {
403    /// Create from hours, minutes, seconds, microseconds.
404    ///
405    /// # Arguments
406    ///
407    /// * `hour` — Hour component (0–23).
408    /// * `minute` — Minute component (0–59).
409    /// * `second` — Second component (0–59).
410    /// * `usec` — Microseconds within the current second.
411    pub fn new(hour: u8, minute: u8, second: u8, usec: u32) -> Self {
412        Self {
413            usec: hour as i64 * 3_600_000_000
414                + minute as i64 * 60_000_000
415                + second as i64 * 1_000_000
416                + usec as i64,
417        }
418    }
419
420    /// Get hours component (0-23)
421    pub fn hour(&self) -> u8 {
422        ((self.usec / 3_600_000_000) % 24) as u8
423    }
424
425    /// Get minutes component (0-59)
426    pub fn minute(&self) -> u8 {
427        ((self.usec / 60_000_000) % 60) as u8
428    }
429
430    /// Get seconds component (0-59)
431    pub fn second(&self) -> u8 {
432        ((self.usec / 1_000_000) % 60) as u8
433    }
434
435    /// Get microseconds component (0-999999)
436    pub fn microsecond(&self) -> u32 {
437        (self.usec % 1_000_000) as u32
438    }
439}
440
441impl FromPg for Time {
442    fn from_pg(bytes: &[u8], oid_val: u32, format: i16) -> Result<Self, TypeError> {
443        if oid_val != oid::TIME {
444            return Err(TypeError::UnexpectedOid {
445                expected: "time",
446                got: oid_val,
447            });
448        }
449
450        if format == 1 {
451            // Binary: 8 bytes, microseconds since midnight
452            if bytes.len() != 8 {
453                return Err(TypeError::InvalidData(
454                    "Expected 8 bytes for time".to_string(),
455                ));
456            }
457            let usec = i64::from_be_bytes([
458                bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
459            ]);
460            validate_time_usec(usec)?;
461            Ok(Time { usec })
462        } else {
463            // Text format: HH:MM:SS or HH:MM:SS.ffffff
464            let s =
465                std::str::from_utf8(bytes).map_err(|e| TypeError::InvalidData(e.to_string()))?;
466            parse_time_text(s)
467        }
468    }
469}
470
471impl ToPg for Time {
472    fn to_pg(&self) -> (Vec<u8>, u32, i16) {
473        (self.usec.to_be_bytes().to_vec(), oid::TIME, 1)
474    }
475}
476
477/// Parse PostgreSQL text time format
478fn parse_time_text(s: &str) -> Result<Time, TypeError> {
479    let (hour, minute, second, usec) = parse_time_components(s)?;
480
481    Ok(Time {
482        usec: hour as i64 * 3_600_000_000
483            + minute as i64 * 60_000_000
484            + second as i64 * 1_000_000
485            + usec,
486    })
487}
488
489#[cfg(test)]
490mod tests {
491    use super::*;
492    #[cfg(feature = "chrono")]
493    use chrono::{Datelike, Timelike};
494
495    #[test]
496    fn test_timestamp_unix_conversion() {
497        // 2024-01-01 00:00:00 UTC
498        let ts = Timestamp::from_unix_secs(1704067200);
499        let back = ts.to_unix_secs();
500        assert_eq!(back, 1704067200);
501    }
502
503    #[test]
504    fn test_timestamp_from_pg_binary() {
505        // Some arbitrary timestamp in binary
506        let usec: i64 = 789_012_345_678_900; // ~25 years after 2000
507        let bytes = usec.to_be_bytes();
508        let ts = Timestamp::from_pg(&bytes, oid::TIMESTAMP, 1).unwrap();
509        assert_eq!(ts.usec, usec);
510    }
511
512    #[test]
513    fn test_date_from_pg_binary() {
514        // 2024-01-01 = 8766 days since 2000-01-01
515        let days: i32 = 8766;
516        let bytes = days.to_be_bytes();
517        let date = Date::from_pg(&bytes, oid::DATE, 1).unwrap();
518        assert_eq!(date.days, days);
519    }
520
521    #[test]
522    fn test_time_from_pg_binary() {
523        // 12:30:45.123456 = 45045123456 microseconds
524        let usec: i64 = 12 * 3_600_000_000 + 30 * 60_000_000 + 45 * 1_000_000 + 123456;
525        let bytes = usec.to_be_bytes();
526        let time = Time::from_pg(&bytes, oid::TIME, 1).unwrap();
527        assert_eq!(time.hour(), 12);
528        assert_eq!(time.minute(), 30);
529        assert_eq!(time.second(), 45);
530        assert_eq!(time.microsecond(), 123456);
531    }
532
533    #[test]
534    fn test_time_from_pg_binary_rejects_out_of_range_values() {
535        assert!(Time::from_pg(&(-1i64).to_be_bytes(), oid::TIME, 1).is_err());
536        assert!(Time::from_pg(&USEC_PER_DAY.to_be_bytes(), oid::TIME, 1).is_err());
537    }
538
539    #[test]
540    fn test_time_from_pg_text() {
541        let time = parse_time_text("14:30:00").unwrap();
542        assert_eq!(time.hour(), 14);
543        assert_eq!(time.minute(), 30);
544        assert_eq!(time.second(), 0);
545    }
546
547    #[test]
548    fn test_timestamp_from_pg_text_preserves_time_components() {
549        let ts = parse_timestamp_text("2024-12-25 17:30:45.123456").unwrap();
550        let expected_days = days_from_ymd_checked(2024, 12, 25).unwrap() as i64;
551        let expected_usec = expected_days * 86_400_000_000
552            + 17 * 3_600_000_000
553            + 30 * 60_000_000
554            + 45 * 1_000_000
555            + 123_456;
556        assert_eq!(ts.usec, expected_usec);
557    }
558
559    #[test]
560    fn test_timestamp_from_pg_text_rejects_invalid_components() {
561        assert!(parse_timestamp_text("2024-12-25 xx:30:00").is_err());
562        assert!(parse_timestamp_text("2024-12-25 17:bad:00").is_err());
563        assert!(parse_timestamp_text("2024-12-25 17:30:bad").is_err());
564        assert!(parse_timestamp_text("2024-13-25 17:30:00").is_err());
565        assert!(parse_timestamp_text("2024-02-30 17:30:00").is_err());
566        assert!(parse_timestamp_text("2024-12-25 17:30:00+bad").is_err());
567        assert!(parse_timestamp_text("2024-12-25 17:30:00+25:00").is_err());
568    }
569
570    #[test]
571    fn test_timestamp_from_pg_text_ignores_timezone_suffix_without_trimming_time() {
572        let ts = parse_timestamp_text("2024-12-25 17:30:45+00").unwrap();
573        let expected_days = days_from_ymd_checked(2024, 12, 25).unwrap() as i64;
574        let expected_usec =
575            expected_days * 86_400_000_000 + 17 * 3_600_000_000 + 30 * 60_000_000 + 45 * 1_000_000;
576        assert_eq!(ts.usec, expected_usec);
577    }
578
579    #[test]
580    fn test_timestamp_from_pg_text_applies_timezone_offset() {
581        let ts = parse_timestamp_text("2024-12-25 17:30:45+02:30").unwrap();
582        let expected_days = days_from_ymd_checked(2024, 12, 25).unwrap() as i64;
583        let expected_usec = expected_days * 86_400_000_000 + 15 * 3_600_000_000 + 45 * 1_000_000;
584        assert_eq!(ts.usec, expected_usec);
585
586        let negative = parse_timestamp_text("2024-12-25 17:30:45-0330").unwrap();
587        let negative_expected =
588            expected_days * 86_400_000_000 + 21 * 3_600_000_000 + 45 * 1_000_000;
589        assert_eq!(negative.usec, negative_expected);
590    }
591
592    #[test]
593    fn test_date_from_pg_text_rejects_invalid_components() {
594        assert!(Date::from_pg(b"2024-13-01", oid::DATE, 0).is_err());
595        assert!(Date::from_pg(b"2024-aa-01", oid::DATE, 0).is_err());
596        assert!(Date::from_pg(b"2024-02-30", oid::DATE, 0).is_err());
597    }
598
599    #[test]
600    fn test_time_from_pg_text_rejects_invalid_components() {
601        assert!(parse_time_text("24:00:00").is_err());
602        assert!(parse_time_text("14:60:00").is_err());
603        assert!(parse_time_text("14:30:bad").is_err());
604        assert!(parse_time_text("14:30:00.bad").is_err());
605        assert!(parse_time_text("14:30:00.1234567").is_err());
606    }
607
608    #[cfg(feature = "chrono")]
609    #[test]
610    fn test_chrono_datetime_from_pg_binary() {
611        // PostgreSQL binary timestamp at Unix epoch.
612        let pg_usec = -PG_EPOCH_OFFSET_USEC;
613        let bytes = pg_usec.to_be_bytes();
614        let dt = chrono::DateTime::<chrono::Utc>::from_pg(&bytes, oid::TIMESTAMPTZ, 1).unwrap();
615        assert_eq!(dt.timestamp(), 0);
616    }
617
618    #[cfg(feature = "chrono")]
619    #[test]
620    fn test_chrono_datetime_from_pg_text_timestamptz() {
621        let dt = chrono::DateTime::<chrono::Utc>::from_pg(
622            b"2024-12-25 17:30:00+00",
623            oid::TIMESTAMPTZ,
624            0,
625        )
626        .unwrap();
627        assert_eq!(dt.year(), 2024);
628        assert_eq!(dt.month(), 12);
629        assert_eq!(dt.day(), 25);
630        assert_eq!(dt.hour(), 17);
631        assert_eq!(dt.minute(), 30);
632    }
633
634    #[cfg(feature = "chrono")]
635    #[test]
636    fn test_chrono_datetime_to_pg_binary() {
637        let dt =
638            chrono::DateTime::<chrono::Utc>::from_timestamp(1_704_067_200, 123_456_000).unwrap();
639        let (bytes, oid_val, format) = dt.to_pg();
640        assert_eq!(oid_val, oid::TIMESTAMPTZ);
641        assert_eq!(format, 1);
642        assert_eq!(bytes.len(), 8);
643    }
644}