use crate::{
protocol::{ErrorCode, ErrorResponse},
FromProtocolValue, ProtocolError, ToProtocolValue,
};
use byteorder::{BigEndian, ByteOrder};
use bytes::{BufMut, BytesMut};
use chrono::{
format::{
Fixed, Item,
Numeric::{Day, Hour, Minute, Month, Second, Year},
Pad::Zero,
},
prelude::*,
};
use chrono_tz::Tz;
use std::backtrace::Backtrace;
use std::io::Error;
use std::{
fmt::{self, Debug, Display, Formatter},
io,
};
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct TimestampValue {
unix_nano: i64,
tz: Option<String>,
}
impl TimestampValue {
pub fn new(mut unix_nano: i64, tz: Option<String>) -> TimestampValue {
unix_nano -= unix_nano % 1000;
TimestampValue { unix_nano, tz }
}
pub fn to_naive_datetime(&self) -> NaiveDateTime {
let secs = self.unix_nano / 1_000_000_000;
let nsecs = (self.unix_nano % 1_000_000_000) as u32;
DateTime::from_timestamp(secs, nsecs)
.unwrap_or_else(|| panic!("Invalid timestamp: {}", self.unix_nano))
.naive_utc()
}
pub fn to_fixed_datetime(&self) -> io::Result<DateTime<Tz>> {
assert!(self.tz.is_some());
let tz = self
.tz
.as_ref()
.unwrap()
.parse::<Tz>()
.map_err(|err| io::Error::other(err.to_string()))?;
let ndt = self.to_naive_datetime();
Ok(tz.from_utc_datetime(&ndt))
}
pub fn tz_ref(&self) -> &Option<String> {
&self.tz
}
pub fn get_time_stamp(&self) -> i64 {
self.unix_nano
}
}
impl Debug for TimestampValue {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("TimestampValue")
.field("unix_nano", &self.unix_nano)
.field("tz", &self.tz)
.field("str", &self.to_string())
.finish()
}
}
impl Display for TimestampValue {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let formatted = Utc.timestamp_nanos(self.unix_nano).format_with_items(
[
Item::Numeric(Year, Zero),
Item::Literal("-"),
Item::Numeric(Month, Zero),
Item::Literal("-"),
Item::Numeric(Day, Zero),
Item::Literal("T"),
Item::Numeric(Hour, Zero),
Item::Literal(":"),
Item::Numeric(Minute, Zero),
Item::Literal(":"),
Item::Numeric(Second, Zero),
Item::Fixed(Fixed::Nanosecond3),
]
.iter(),
);
write!(f, "{}", formatted)
}
}
pub(crate) fn pg_base_date_epoch() -> NaiveDateTime {
NaiveDate::from_ymd_opt(2000, 1, 1)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap()
}
impl ToProtocolValue for TimestampValue {
fn to_text(&self, buf: &mut BytesMut) -> Result<(), ProtocolError> {
let ndt = match self.tz_ref() {
None => self.to_naive_datetime(),
Some(_) => self.to_fixed_datetime()?.naive_utc(),
};
let as_str = ndt.format("%Y-%m-%d %H:%M:%S%.6f").to_string();
match self.tz_ref() {
None => as_str.to_text(buf),
Some(_) => (as_str + "+00").to_text(buf),
}
}
fn to_binary(&self, buf: &mut BytesMut) -> Result<(), ProtocolError> {
let ndt = match self.tz_ref() {
None => self.to_naive_datetime(),
Some(_) => self.to_fixed_datetime()?.naive_utc(),
};
let n = ndt
.signed_duration_since(pg_base_date_epoch())
.num_microseconds()
.ok_or(Error::other(
"Unable to extract number of seconds from timestamp",
))?;
buf.put_i32(8);
buf.put_i64(n);
Ok(())
}
}
impl FromProtocolValue for TimestampValue {
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(),
})?;
let parsed_datetime = NaiveDateTime::parse_from_str(as_str, "%Y-%m-%d %H:%M:%S")
.or_else(|_| NaiveDateTime::parse_from_str(as_str, "%Y-%m-%d %H:%M:%S%.f"))
.or_else(|_| NaiveDateTime::parse_from_str(as_str, "%Y-%m-%d %H:%M:%S%.f UTC"))
.or_else(|_| NaiveDateTime::parse_from_str(as_str, "%Y-%m-%dT%H:%M:%S"))
.or_else(|_| NaiveDateTime::parse_from_str(as_str, "%Y-%m-%dT%H:%M:%S%.f"))
.or_else(|_| NaiveDateTime::parse_from_str(as_str, "%Y-%m-%dT%H:%M:%S%.fZ"))
.or_else(|_| {
NaiveDate::parse_from_str(as_str, "%Y-%m-%d").map(|date| {
date.and_hms_opt(0, 0, 0)
.expect("Unable to set time to 00:00:00")
})
})
.map_err(|err| ProtocolError::ErrorResponse {
source: ErrorResponse::error(
ErrorCode::ProtocolViolation,
format!(
"Unable to parse timestamp from text: '{}', error: {}",
as_str, err
),
),
backtrace: Backtrace::capture(),
})?;
let unix_nano = parsed_datetime
.and_utc()
.timestamp_nanos_opt()
.ok_or_else(|| ProtocolError::ErrorResponse {
source: ErrorResponse::error(
ErrorCode::ProtocolViolation,
format!("Timestamp out of range: '{}'", as_str),
),
backtrace: Backtrace::capture(),
})?;
Ok(TimestampValue::new(unix_nano, None))
}
fn from_binary(raw: &[u8]) -> Result<Self, ProtocolError> {
if raw.len() != 8 {
return Err(ProtocolError::ErrorResponse {
source: ErrorResponse::error(
ErrorCode::ProtocolViolation,
format!(
"Invalid binary timestamp length: expected 8 bytes, got {}",
raw.len()
),
),
backtrace: Backtrace::capture(),
});
}
let pg_microseconds = BigEndian::read_i64(raw);
let unix_nano = pg_base_date_epoch()
.and_utc()
.timestamp_nanos_opt()
.expect("Unable to get timestamp nanos for pg_base_date_epoch")
+ (pg_microseconds * 1_000);
Ok(TimestampValue::new(unix_nano, None))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ProtocolError;
#[test]
fn test_timestamp_creation() -> Result<(), ProtocolError> {
let ts = TimestampValue::new(1650890322000000000, None);
assert_eq!(ts.get_time_stamp(), 1650890322000000000);
assert_eq!(ts.tz_ref(), &None);
let ts_with_tz = TimestampValue::new(1650890322000000000, Some("UTC".to_string()));
assert_eq!(ts_with_tz.get_time_stamp(), 1650890322000000000);
assert_eq!(ts_with_tz.tz_ref(), &Some("UTC".to_string()));
Ok(())
}
#[test]
fn test_timestamp_to_string() {
let ts = TimestampValue::new(1650890322000000000, None);
assert!(!ts.to_string().is_empty());
}
#[test]
fn test_timestamp_precision_hack() {
let ts = TimestampValue::new(1650890322123456789, None);
assert_eq!(ts.get_time_stamp(), 1650890322123456000);
}
#[test]
fn test_invalid_timestamp_text() {
assert!(TimestampValue::from_text(b"invalid-date").is_err());
assert!(TimestampValue::from_text(b"2025-13-45 25:70:99").is_err());
assert!(TimestampValue::from_text(b"").is_err());
}
#[test]
fn test_timestamp_from_text_various_formats() {
let ts1 = TimestampValue::from_text(b"2025-08-04 20:15:47").unwrap();
assert_eq!(ts1.to_naive_datetime().to_string(), "2025-08-04 20:15:47");
let ts2 = TimestampValue::from_text(b"2025-08-04 20:16:54.853660").unwrap();
assert_eq!(
ts2.to_naive_datetime()
.format("%Y-%m-%d %H:%M:%S%.6f")
.to_string(),
"2025-08-04 20:16:54.853660"
);
let ts3 = TimestampValue::from_text(b"2025-08-04 20:15:47.953").unwrap();
assert_eq!(
ts3.to_naive_datetime()
.format("%Y-%m-%d %H:%M:%S%.3f")
.to_string(),
"2025-08-04 20:15:47.953"
);
let ts4 = TimestampValue::from_text(b"2025-08-04T20:15:47").unwrap();
assert_eq!(ts4.to_naive_datetime().to_string(), "2025-08-04 20:15:47");
let ts5 = TimestampValue::from_text(b"2025-08-04T20:15:47.953116").unwrap();
assert_eq!(
ts5.to_naive_datetime()
.format("%Y-%m-%d %H:%M:%S%.6f")
.to_string(),
"2025-08-04 20:15:47.953116"
);
}
#[test]
fn test_invalid_timestamp_binary() {
assert!(TimestampValue::from_binary(&[1, 2, 3]).is_err()); assert!(TimestampValue::from_binary(&[]).is_err()); }
}