use crate::protocol::{ErrorCode, ErrorResponse};
use crate::timestamp::pg_base_date_epoch;
use crate::{FromProtocolValue, ProtocolError, ToProtocolValue};
use byteorder::{BigEndian, ByteOrder};
use bytes::{BufMut, BytesMut};
use chrono::NaiveDate;
use std::backtrace::Backtrace;
use std::io::Error;
pub type DateValue = NaiveDate;
impl ToProtocolValue for DateValue {
fn to_text(&self, buf: &mut BytesMut) -> Result<(), ProtocolError> {
self.to_string().to_text(buf)
}
fn to_binary(&self, buf: &mut BytesMut) -> Result<(), ProtocolError> {
let n = self
.signed_duration_since(pg_base_date_epoch().date())
.num_days();
if n > (i32::MAX as i64) {
return Err(Error::other(format!(
"value too large to store in the binary format (i32), actual: {}",
n
))
.into());
}
buf.put_i32(4);
buf.put_i32(n as i32);
Ok(())
}
}
impl FromProtocolValue for DateValue {
fn from_text(raw: &[u8]) -> Result<Self, ProtocolError> {
let as_str = std::str::from_utf8(raw).map_err(|err| ProtocolError::ErrorResponse {
source: ErrorResponse::error(ErrorCode::ProtocolViolation, err.to_string()),
backtrace: Backtrace::capture(),
})?;
NaiveDate::parse_from_str(as_str, "%Y-%m-%d").map_err(|err| ProtocolError::ErrorResponse {
source: ErrorResponse::error(
ErrorCode::ProtocolViolation,
format!("Unable to parse date from text '{}': {}", as_str, err),
),
backtrace: Backtrace::capture(),
})
}
fn from_binary(raw: &[u8]) -> Result<Self, ProtocolError> {
if raw.len() != 4 {
return Err(ProtocolError::ErrorResponse {
source: ErrorResponse::error(
ErrorCode::ProtocolViolation,
format!(
"Invalid binary date format, expected 4 bytes, got {}",
raw.len()
),
),
backtrace: Backtrace::capture(),
});
}
let days_since_epoch = BigEndian::read_i32(raw);
let base_date = pg_base_date_epoch().date();
base_date
.checked_add_signed(chrono::Duration::days(days_since_epoch as i64))
.ok_or_else(|| ProtocolError::ErrorResponse {
source: ErrorResponse::error(
ErrorCode::ProtocolViolation,
format!(
"Date value {} days from epoch is out of range",
days_since_epoch
),
),
backtrace: Backtrace::capture(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::Format;
#[test]
fn test_date_from_text() {
let date_str = b"2025-08-08";
let result = DateValue::from_protocol(date_str, Format::Text).unwrap();
let expected = DateValue::from_ymd_opt(2025, 8, 8).unwrap();
assert_eq!(result, expected);
}
#[test]
fn test_date_from_binary() {
let date = DateValue::from_ymd_opt(2025, 8, 8).unwrap();
let mut buf = BytesMut::new();
date.to_binary(&mut buf).unwrap();
let binary_data = &buf[4..];
let result = DateValue::from_protocol(binary_data, Format::Binary).unwrap();
assert_eq!(result, date);
}
#[test]
fn test_date_from_text_invalid() {
let invalid_date = b"not-a-date";
let result = DateValue::from_protocol(invalid_date, Format::Text);
assert!(result.is_err());
}
#[test]
fn test_date_before_the_pg_epoch() {
let before_epoch = DateValue::from_ymd_opt(1999, 12, 31).unwrap();
let mut buf = BytesMut::new();
before_epoch.to_binary(&mut buf).unwrap();
let binary_data = &buf[4..];
let result = DateValue::from_protocol(binary_data, Format::Binary).unwrap();
assert_eq!(result, before_epoch);
}
}