use super::{FromPg, ToPg, TypeError};
use crate::protocol::types::oid;
const PG_EPOCH_OFFSET_USEC: i64 = 946_684_800_000_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Timestamp {
pub usec: i64,
}
impl Timestamp {
pub fn from_pg_usec(usec: i64) -> Self {
Self { usec }
}
pub fn from_unix_secs(secs: i64) -> Self {
Self {
usec: secs * 1_000_000 - PG_EPOCH_OFFSET_USEC,
}
}
pub fn to_unix_secs(&self) -> i64 {
(self.usec + PG_EPOCH_OFFSET_USEC) / 1_000_000
}
pub fn to_unix_usec(&self) -> i64 {
self.usec + PG_EPOCH_OFFSET_USEC
}
}
impl FromPg for Timestamp {
fn from_pg(bytes: &[u8], oid_val: u32, format: i16) -> Result<Self, TypeError> {
if oid_val != oid::TIMESTAMP && oid_val != oid::TIMESTAMPTZ {
return Err(TypeError::UnexpectedOid {
expected: "timestamp",
got: oid_val,
});
}
if format == 1 {
if bytes.len() != 8 {
return Err(TypeError::InvalidData(
"Expected 8 bytes for timestamp".to_string(),
));
}
let usec = i64::from_be_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
]);
Ok(Timestamp::from_pg_usec(usec))
} else {
let s =
std::str::from_utf8(bytes).map_err(|e| TypeError::InvalidData(e.to_string()))?;
parse_timestamp_text(s)
}
}
}
impl ToPg for Timestamp {
fn to_pg(&self) -> (Vec<u8>, u32, i16) {
(self.usec.to_be_bytes().to_vec(), oid::TIMESTAMP, 1)
}
}
#[cfg(feature = "chrono")]
impl FromPg for chrono::DateTime<chrono::Utc> {
fn from_pg(bytes: &[u8], oid_val: u32, format: i16) -> Result<Self, TypeError> {
if oid_val != oid::TIMESTAMP && oid_val != oid::TIMESTAMPTZ {
return Err(TypeError::UnexpectedOid {
expected: "timestamp",
got: oid_val,
});
}
if format == 1 {
if bytes.len() != 8 {
return Err(TypeError::InvalidData(
"Expected 8 bytes for timestamp".to_string(),
));
}
let pg_usec = i64::from_be_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
]);
let unix_usec = pg_usec.saturating_add(PG_EPOCH_OFFSET_USEC);
chrono::DateTime::<chrono::Utc>::from_timestamp_micros(unix_usec).ok_or_else(|| {
TypeError::InvalidData(format!("Timestamp out of range: {}", unix_usec))
})
} else {
let s =
std::str::from_utf8(bytes).map_err(|e| TypeError::InvalidData(e.to_string()))?;
if oid_val == oid::TIMESTAMPTZ {
chrono::DateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f%#z")
.or_else(|_| chrono::DateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f%#z"))
.or_else(|_| chrono::DateTime::parse_from_rfc3339(s))
.map(|dt| dt.with_timezone(&chrono::Utc))
.map_err(|e| TypeError::InvalidData(format!("Invalid timestamptz: {}", e)))
} else {
chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f")
.or_else(|_| chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f"))
.map(|naive| {
chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(
naive,
chrono::Utc,
)
})
.map_err(|e| TypeError::InvalidData(format!("Invalid timestamp: {}", e)))
}
}
}
}
#[cfg(feature = "chrono")]
impl ToPg for chrono::DateTime<chrono::Utc> {
fn to_pg(&self) -> (Vec<u8>, u32, i16) {
let unix_usec = self.timestamp_micros();
let pg_usec = unix_usec.saturating_sub(PG_EPOCH_OFFSET_USEC);
(pg_usec.to_be_bytes().to_vec(), oid::TIMESTAMPTZ, 1)
}
}
fn parse_timestamp_text(s: &str) -> Result<Timestamp, TypeError> {
let parts: Vec<&str> = s.split(&[' ', 'T'][..]).collect();
if parts.len() < 2 {
return Err(TypeError::InvalidData(format!("Invalid timestamp: {}", s)));
}
let date_parts: Vec<i32> = parts[0].split('-').filter_map(|p| p.parse().ok()).collect();
if date_parts.len() != 3 {
return Err(TypeError::InvalidData(format!(
"Invalid date: {}",
parts[0]
)));
}
let time_str =
parts[1].trim_end_matches(|c: char| c == '+' || c == '-' || c.is_ascii_digit() || c == ':');
let time_parts: Vec<&str> = time_str.split(':').collect();
let hour: i32 = time_parts.first().and_then(|s| s.parse().ok()).unwrap_or(0);
let minute: i32 = time_parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
let second_str = time_parts.get(2).unwrap_or(&"0");
let sec_parts: Vec<&str> = second_str.split('.').collect();
let second: i32 = sec_parts.first().and_then(|s| s.parse().ok()).unwrap_or(0);
let usec: i64 = sec_parts
.get(1)
.map(|s| {
let padded = format!("{:0<6}", s);
padded[..6].parse::<i64>().unwrap_or(0)
})
.unwrap_or(0);
let year = date_parts[0];
let month = date_parts[1];
let day = date_parts[2];
let days_since_epoch = days_from_ymd(year, month, day);
let total_usec = days_since_epoch as i64 * 86_400_000_000
+ hour as i64 * 3_600_000_000
+ minute as i64 * 60_000_000
+ second as i64 * 1_000_000
+ usec;
Ok(Timestamp::from_pg_usec(total_usec))
}
fn days_from_ymd(year: i32, month: i32, day: i32) -> i32 {
let mut days = 0;
for y in 2000..year {
days += if is_leap_year(y) { 366 } else { 365 };
}
for y in year..2000 {
days -= if is_leap_year(y) { 366 } else { 365 };
}
let days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
for m in 1..month {
days += days_in_month[(m - 1) as usize];
if m == 2 && is_leap_year(year) {
days += 1;
}
}
days += day - 1;
days
}
fn is_leap_year(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Date {
pub days: i32,
}
impl FromPg for Date {
fn from_pg(bytes: &[u8], oid_val: u32, format: i16) -> Result<Self, TypeError> {
if oid_val != oid::DATE {
return Err(TypeError::UnexpectedOid {
expected: "date",
got: oid_val,
});
}
if format == 1 {
if bytes.len() != 4 {
return Err(TypeError::InvalidData(
"Expected 4 bytes for date".to_string(),
));
}
let days = i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
Ok(Date { days })
} else {
let s =
std::str::from_utf8(bytes).map_err(|e| TypeError::InvalidData(e.to_string()))?;
let parts: Vec<i32> = s.split('-').filter_map(|p| p.parse().ok()).collect();
if parts.len() != 3 {
return Err(TypeError::InvalidData(format!("Invalid date: {}", s)));
}
Ok(Date {
days: days_from_ymd(parts[0], parts[1], parts[2]),
})
}
}
}
impl ToPg for Date {
fn to_pg(&self) -> (Vec<u8>, u32, i16) {
(self.days.to_be_bytes().to_vec(), oid::DATE, 1)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Time {
pub usec: i64,
}
impl Time {
pub fn new(hour: u8, minute: u8, second: u8, usec: u32) -> Self {
Self {
usec: hour as i64 * 3_600_000_000
+ minute as i64 * 60_000_000
+ second as i64 * 1_000_000
+ usec as i64,
}
}
pub fn hour(&self) -> u8 {
((self.usec / 3_600_000_000) % 24) as u8
}
pub fn minute(&self) -> u8 {
((self.usec / 60_000_000) % 60) as u8
}
pub fn second(&self) -> u8 {
((self.usec / 1_000_000) % 60) as u8
}
pub fn microsecond(&self) -> u32 {
(self.usec % 1_000_000) as u32
}
}
impl FromPg for Time {
fn from_pg(bytes: &[u8], oid_val: u32, format: i16) -> Result<Self, TypeError> {
if oid_val != oid::TIME {
return Err(TypeError::UnexpectedOid {
expected: "time",
got: oid_val,
});
}
if format == 1 {
if bytes.len() != 8 {
return Err(TypeError::InvalidData(
"Expected 8 bytes for time".to_string(),
));
}
let usec = i64::from_be_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
]);
Ok(Time { usec })
} else {
let s =
std::str::from_utf8(bytes).map_err(|e| TypeError::InvalidData(e.to_string()))?;
parse_time_text(s)
}
}
}
impl ToPg for Time {
fn to_pg(&self) -> (Vec<u8>, u32, i16) {
(self.usec.to_be_bytes().to_vec(), oid::TIME, 1)
}
}
fn parse_time_text(s: &str) -> Result<Time, TypeError> {
let parts: Vec<&str> = s.split(':').collect();
if parts.len() < 2 {
return Err(TypeError::InvalidData(format!("Invalid time: {}", s)));
}
let hour: i64 = parts[0]
.parse()
.map_err(|_| TypeError::InvalidData("Invalid hour".to_string()))?;
let minute: i64 = parts[1]
.parse()
.map_err(|_| TypeError::InvalidData("Invalid minute".to_string()))?;
let (second, usec) = if parts.len() > 2 {
let sec_parts: Vec<&str> = parts[2].split('.').collect();
let sec: i64 = sec_parts[0].parse().unwrap_or(0);
let us: i64 = sec_parts
.get(1)
.map(|s| {
let padded = format!("{:0<6}", s);
padded[..6].parse::<i64>().unwrap_or(0)
})
.unwrap_or(0);
(sec, us)
} else {
(0, 0)
};
Ok(Time {
usec: hour * 3_600_000_000 + minute * 60_000_000 + second * 1_000_000 + usec,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "chrono")]
use chrono::{Datelike, Timelike};
#[test]
fn test_timestamp_unix_conversion() {
let ts = Timestamp::from_unix_secs(1704067200);
let back = ts.to_unix_secs();
assert_eq!(back, 1704067200);
}
#[test]
fn test_timestamp_from_pg_binary() {
let usec: i64 = 789_012_345_678_900; let bytes = usec.to_be_bytes();
let ts = Timestamp::from_pg(&bytes, oid::TIMESTAMP, 1).unwrap();
assert_eq!(ts.usec, usec);
}
#[test]
fn test_date_from_pg_binary() {
let days: i32 = 8766;
let bytes = days.to_be_bytes();
let date = Date::from_pg(&bytes, oid::DATE, 1).unwrap();
assert_eq!(date.days, days);
}
#[test]
fn test_time_from_pg_binary() {
let usec: i64 = 12 * 3_600_000_000 + 30 * 60_000_000 + 45 * 1_000_000 + 123456;
let bytes = usec.to_be_bytes();
let time = Time::from_pg(&bytes, oid::TIME, 1).unwrap();
assert_eq!(time.hour(), 12);
assert_eq!(time.minute(), 30);
assert_eq!(time.second(), 45);
assert_eq!(time.microsecond(), 123456);
}
#[test]
fn test_time_from_pg_text() {
let time = parse_time_text("14:30:00").unwrap();
assert_eq!(time.hour(), 14);
assert_eq!(time.minute(), 30);
assert_eq!(time.second(), 0);
}
#[cfg(feature = "chrono")]
#[test]
fn test_chrono_datetime_from_pg_binary() {
let pg_usec = -PG_EPOCH_OFFSET_USEC;
let bytes = pg_usec.to_be_bytes();
let dt = chrono::DateTime::<chrono::Utc>::from_pg(&bytes, oid::TIMESTAMPTZ, 1).unwrap();
assert_eq!(dt.timestamp(), 0);
}
#[cfg(feature = "chrono")]
#[test]
fn test_chrono_datetime_from_pg_text_timestamptz() {
let dt = chrono::DateTime::<chrono::Utc>::from_pg(
b"2024-12-25 17:30:00+00",
oid::TIMESTAMPTZ,
0,
)
.unwrap();
assert_eq!(dt.year(), 2024);
assert_eq!(dt.month(), 12);
assert_eq!(dt.day(), 25);
assert_eq!(dt.hour(), 17);
assert_eq!(dt.minute(), 30);
}
#[cfg(feature = "chrono")]
#[test]
fn test_chrono_datetime_to_pg_binary() {
let dt =
chrono::DateTime::<chrono::Utc>::from_timestamp(1_704_067_200, 123_456_000).unwrap();
let (bytes, oid_val, format) = dt.to_pg();
assert_eq!(oid_val, oid::TIMESTAMPTZ);
assert_eq!(format, 1);
assert_eq!(bytes.len(), 8);
}
}