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