Skip to main content

pg_srv/values/
date.rs

1use crate::protocol::{ErrorCode, ErrorResponse};
2use crate::timestamp::pg_base_date_epoch;
3use crate::{FromProtocolValue, ProtocolError, ToProtocolValue};
4use byteorder::{BigEndian, ByteOrder};
5use bytes::{BufMut, BytesMut};
6use chrono::NaiveDate;
7use std::backtrace::Backtrace;
8use std::io::Error;
9
10pub type DateValue = NaiveDate;
11
12impl ToProtocolValue for DateValue {
13    // date_out - https://github.com/postgres/postgres/blob/REL_14_4/src/backend/utils/adt/date.c#L176
14    fn to_text(&self, buf: &mut BytesMut) -> Result<(), ProtocolError> {
15        self.to_string().to_text(buf)
16    }
17
18    // date_send - https://github.com/postgres/postgres/blob/REL_14_4/src/backend/utils/adt/date.c#L223
19    fn to_binary(&self, buf: &mut BytesMut) -> Result<(), ProtocolError> {
20        let n = self
21            .signed_duration_since(pg_base_date_epoch().date())
22            .num_days();
23        if n > (i32::MAX as i64) {
24            return Err(Error::other(format!(
25                "value too large to store in the binary format (i32), actual: {}",
26                n
27            ))
28            .into());
29        }
30
31        buf.put_i32(4);
32        buf.put_i32(n as i32);
33
34        Ok(())
35    }
36}
37
38impl FromProtocolValue for DateValue {
39    // date_in - https://github.com/postgres/postgres/blob/REL_14_4/src/backend/utils/adt/date.c#L111
40    fn from_text(raw: &[u8]) -> Result<Self, ProtocolError> {
41        let as_str = std::str::from_utf8(raw).map_err(|err| ProtocolError::ErrorResponse {
42            source: ErrorResponse::error(ErrorCode::ProtocolViolation, err.to_string()),
43            backtrace: Backtrace::capture(),
44        })?;
45
46        // Parse date string in format "YYYY-MM-DD"
47        NaiveDate::parse_from_str(as_str, "%Y-%m-%d").map_err(|err| ProtocolError::ErrorResponse {
48            source: ErrorResponse::error(
49                ErrorCode::ProtocolViolation,
50                format!("Unable to parse date from text '{}': {}", as_str, err),
51            ),
52            backtrace: Backtrace::capture(),
53        })
54    }
55
56    // date_recv - https://github.com/postgres/postgres/blob/REL_14_4/src/backend/utils/adt/date.c#L207
57    fn from_binary(raw: &[u8]) -> Result<Self, ProtocolError> {
58        if raw.len() != 4 {
59            return Err(ProtocolError::ErrorResponse {
60                source: ErrorResponse::error(
61                    ErrorCode::ProtocolViolation,
62                    format!(
63                        "Invalid binary date format, expected 4 bytes, got {}",
64                        raw.len()
65                    ),
66                ),
67                backtrace: Backtrace::capture(),
68            });
69        }
70
71        let days_since_epoch = BigEndian::read_i32(raw);
72        let base_date = pg_base_date_epoch().date();
73
74        base_date
75            .checked_add_signed(chrono::Duration::days(days_since_epoch as i64))
76            .ok_or_else(|| ProtocolError::ErrorResponse {
77                source: ErrorResponse::error(
78                    ErrorCode::ProtocolViolation,
79                    format!(
80                        "Date value {} days from epoch is out of range",
81                        days_since_epoch
82                    ),
83                ),
84                backtrace: Backtrace::capture(),
85            })
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use crate::protocol::Format;
93
94    #[test]
95    fn test_date_from_text() {
96        let date_str = b"2025-08-08";
97        let result = DateValue::from_protocol(date_str, Format::Text).unwrap();
98        let expected = DateValue::from_ymd_opt(2025, 8, 8).unwrap();
99        assert_eq!(result, expected);
100    }
101
102    #[test]
103    fn test_date_from_binary() {
104        // Create a date and encode it
105        let date = DateValue::from_ymd_opt(2025, 8, 8).unwrap();
106        let mut buf = BytesMut::new();
107        date.to_binary(&mut buf).unwrap();
108
109        // Skip the length prefix (4 bytes) to get the actual data
110        let binary_data = &buf[4..];
111
112        // Decode it back
113        let result = DateValue::from_protocol(binary_data, Format::Binary).unwrap();
114        assert_eq!(result, date);
115    }
116
117    #[test]
118    fn test_date_from_text_invalid() {
119        let invalid_date = b"not-a-date";
120        let result = DateValue::from_protocol(invalid_date, Format::Text);
121        assert!(result.is_err());
122    }
123
124    #[test]
125    fn test_date_before_the_pg_epoch() {
126        // Test date before epoch
127        let before_epoch = DateValue::from_ymd_opt(1999, 12, 31).unwrap();
128        let mut buf = BytesMut::new();
129        before_epoch.to_binary(&mut buf).unwrap();
130        let binary_data = &buf[4..];
131        let result = DateValue::from_protocol(binary_data, Format::Binary).unwrap();
132        assert_eq!(result, before_epoch);
133    }
134}