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 fn to_text(&self, buf: &mut BytesMut) -> Result<(), ProtocolError> {
15 self.to_string().to_text(buf)
16 }
17
18 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 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 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 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 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 let binary_data = &buf[4..];
111
112 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 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}