Skip to main content

qail_pg/types/
temporal.rs

1//! Timestamp type conversions for PostgreSQL.
2//!
3//! PostgreSQL timestamps are stored as microseconds since 2000-01-01 00:00:00 UTC.
4
5use super::{FromPg, ToPg, TypeError};
6use crate::protocol::types::oid;
7
8/// PostgreSQL epoch: 2000-01-01 00:00:00 UTC
9/// Difference from Unix epoch (1970-01-01) in microseconds
10const PG_EPOCH_OFFSET_USEC: i64 = 946_684_800_000_000;
11
12/// Timestamp without timezone (microseconds since 2000-01-01)
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct Timestamp {
15    /// Microseconds since PostgreSQL epoch (2000-01-01 00:00:00)
16    pub usec: i64,
17}
18
19impl Timestamp {
20    /// Create from microseconds since PostgreSQL epoch
21    pub fn from_pg_usec(usec: i64) -> Self {
22        Self { usec }
23    }
24
25    /// Create from Unix timestamp (seconds since 1970-01-01)
26    pub fn from_unix_secs(secs: i64) -> Self {
27        Self {
28            usec: secs * 1_000_000 - PG_EPOCH_OFFSET_USEC,
29        }
30    }
31
32    /// Convert to Unix timestamp (seconds since 1970-01-01)
33    pub fn to_unix_secs(&self) -> i64 {
34        (self.usec + PG_EPOCH_OFFSET_USEC) / 1_000_000
35    }
36
37    /// Convert to Unix timestamp with microseconds
38    pub fn to_unix_usec(&self) -> i64 {
39        self.usec + PG_EPOCH_OFFSET_USEC
40    }
41}
42
43impl FromPg for Timestamp {
44    fn from_pg(bytes: &[u8], oid_val: u32, format: i16) -> Result<Self, TypeError> {
45        if oid_val != oid::TIMESTAMP && oid_val != oid::TIMESTAMPTZ {
46            return Err(TypeError::UnexpectedOid {
47                expected: "timestamp",
48                got: oid_val,
49            });
50        }
51
52        if format == 1 {
53            // Binary: 8 bytes, microseconds since 2000-01-01
54            if bytes.len() != 8 {
55                return Err(TypeError::InvalidData(
56                    "Expected 8 bytes for timestamp".to_string(),
57                ));
58            }
59            let usec = i64::from_be_bytes([
60                bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
61            ]);
62            Ok(Timestamp::from_pg_usec(usec))
63        } else {
64            // Text format: parse ISO 8601
65            let s =
66                std::str::from_utf8(bytes).map_err(|e| TypeError::InvalidData(e.to_string()))?;
67            parse_timestamp_text(s)
68        }
69    }
70}
71
72impl ToPg for Timestamp {
73    fn to_pg(&self) -> (Vec<u8>, u32, i16) {
74        (self.usec.to_be_bytes().to_vec(), oid::TIMESTAMP, 1)
75    }
76}
77
78/// Parse PostgreSQL text timestamp format
79fn parse_timestamp_text(s: &str) -> Result<Timestamp, TypeError> {
80    // Format: "2024-12-25 17:30:00" or "2024-12-25 17:30:00.123456"
81    // This is a simplified parser - production would use chrono or time crate
82
83    let parts: Vec<&str> = s.split(&[' ', 'T'][..]).collect();
84    if parts.len() < 2 {
85        return Err(TypeError::InvalidData(format!("Invalid timestamp: {}", s)));
86    }
87
88    let date_parts: Vec<i32> = parts[0].split('-').filter_map(|p| p.parse().ok()).collect();
89
90    if date_parts.len() != 3 {
91        return Err(TypeError::InvalidData(format!(
92            "Invalid date: {}",
93            parts[0]
94        )));
95    }
96
97    let time_str =
98        parts[1].trim_end_matches(|c: char| c == '+' || c == '-' || c.is_ascii_digit() || c == ':');
99    let time_parts: Vec<&str> = time_str.split(':').collect();
100
101    let hour: i32 = time_parts.first().and_then(|s| s.parse().ok()).unwrap_or(0);
102    let minute: i32 = time_parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
103    let second_str = time_parts.get(2).unwrap_or(&"0");
104    let sec_parts: Vec<&str> = second_str.split('.').collect();
105    let second: i32 = sec_parts.first().and_then(|s| s.parse().ok()).unwrap_or(0);
106    let usec: i64 = sec_parts
107        .get(1)
108        .map(|s| {
109            let padded = format!("{:0<6}", s);
110            padded[..6].parse::<i64>().unwrap_or(0)
111        })
112        .unwrap_or(0);
113
114    // Calculate days since 2000-01-01
115    let year = date_parts[0];
116    let month = date_parts[1];
117    let day = date_parts[2];
118
119    // Simplified calculation (not accounting for all leap years correctly)
120    let days_since_epoch = days_from_ymd(year, month, day);
121
122    let total_usec = days_since_epoch as i64 * 86_400_000_000
123        + hour as i64 * 3_600_000_000
124        + minute as i64 * 60_000_000
125        + second as i64 * 1_000_000
126        + usec;
127
128    Ok(Timestamp::from_pg_usec(total_usec))
129}
130
131/// Calculate days since 2000-01-01
132fn days_from_ymd(year: i32, month: i32, day: i32) -> i32 {
133    // Days from 2000-01-01 to given date
134    let mut days = 0;
135
136    // Years
137    for y in 2000..year {
138        days += if is_leap_year(y) { 366 } else { 365 };
139    }
140    for y in year..2000 {
141        days -= if is_leap_year(y) { 366 } else { 365 };
142    }
143
144    // Months
145    let days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
146    for m in 1..month {
147        days += days_in_month[(m - 1) as usize];
148        if m == 2 && is_leap_year(year) {
149            days += 1;
150        }
151    }
152
153    // Days
154    days += day - 1;
155
156    days
157}
158
159fn is_leap_year(year: i32) -> bool {
160    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
161}
162
163/// Date type (days since 2000-01-01)
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub struct Date {
166    /// Days since PostgreSQL epoch (2000-01-01). Negative values represent dates before the epoch.
167    pub days: i32,
168}
169
170impl FromPg for Date {
171    fn from_pg(bytes: &[u8], oid_val: u32, format: i16) -> Result<Self, TypeError> {
172        if oid_val != oid::DATE {
173            return Err(TypeError::UnexpectedOid {
174                expected: "date",
175                got: oid_val,
176            });
177        }
178
179        if format == 1 {
180            // Binary: 4 bytes, days since 2000-01-01
181            if bytes.len() != 4 {
182                return Err(TypeError::InvalidData(
183                    "Expected 4 bytes for date".to_string(),
184                ));
185            }
186            let days = i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
187            Ok(Date { days })
188        } else {
189            // Text format: YYYY-MM-DD
190            let s =
191                std::str::from_utf8(bytes).map_err(|e| TypeError::InvalidData(e.to_string()))?;
192            let parts: Vec<i32> = s.split('-').filter_map(|p| p.parse().ok()).collect();
193            if parts.len() != 3 {
194                return Err(TypeError::InvalidData(format!("Invalid date: {}", s)));
195            }
196            Ok(Date {
197                days: days_from_ymd(parts[0], parts[1], parts[2]),
198            })
199        }
200    }
201}
202
203impl ToPg for Date {
204    fn to_pg(&self) -> (Vec<u8>, u32, i16) {
205        (self.days.to_be_bytes().to_vec(), oid::DATE, 1)
206    }
207}
208
209/// Time type (microseconds since midnight)
210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211pub struct Time {
212    /// Microseconds since midnight
213    pub usec: i64,
214}
215
216impl Time {
217    /// Create from hours, minutes, seconds, microseconds.
218    ///
219    /// # Arguments
220    ///
221    /// * `hour` — Hour component (0–23).
222    /// * `minute` — Minute component (0–59).
223    /// * `second` — Second component (0–59).
224    /// * `usec` — Microseconds within the current second.
225    pub fn new(hour: u8, minute: u8, second: u8, usec: u32) -> Self {
226        Self {
227            usec: hour as i64 * 3_600_000_000
228                + minute as i64 * 60_000_000
229                + second as i64 * 1_000_000
230                + usec as i64,
231        }
232    }
233
234    /// Get hours component (0-23)
235    pub fn hour(&self) -> u8 {
236        ((self.usec / 3_600_000_000) % 24) as u8
237    }
238
239    /// Get minutes component (0-59)
240    pub fn minute(&self) -> u8 {
241        ((self.usec / 60_000_000) % 60) as u8
242    }
243
244    /// Get seconds component (0-59)
245    pub fn second(&self) -> u8 {
246        ((self.usec / 1_000_000) % 60) as u8
247    }
248
249    /// Get microseconds component (0-999999)
250    pub fn microsecond(&self) -> u32 {
251        (self.usec % 1_000_000) as u32
252    }
253}
254
255impl FromPg for Time {
256    fn from_pg(bytes: &[u8], oid_val: u32, format: i16) -> Result<Self, TypeError> {
257        if oid_val != oid::TIME {
258            return Err(TypeError::UnexpectedOid {
259                expected: "time",
260                got: oid_val,
261            });
262        }
263
264        if format == 1 {
265            // Binary: 8 bytes, microseconds since midnight
266            if bytes.len() != 8 {
267                return Err(TypeError::InvalidData(
268                    "Expected 8 bytes for time".to_string(),
269                ));
270            }
271            let usec = i64::from_be_bytes([
272                bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
273            ]);
274            Ok(Time { usec })
275        } else {
276            // Text format: HH:MM:SS or HH:MM:SS.ffffff
277            let s =
278                std::str::from_utf8(bytes).map_err(|e| TypeError::InvalidData(e.to_string()))?;
279            parse_time_text(s)
280        }
281    }
282}
283
284impl ToPg for Time {
285    fn to_pg(&self) -> (Vec<u8>, u32, i16) {
286        (self.usec.to_be_bytes().to_vec(), oid::TIME, 1)
287    }
288}
289
290/// Parse PostgreSQL text time format
291fn parse_time_text(s: &str) -> Result<Time, TypeError> {
292    let parts: Vec<&str> = s.split(':').collect();
293    if parts.len() < 2 {
294        return Err(TypeError::InvalidData(format!("Invalid time: {}", s)));
295    }
296
297    let hour: i64 = parts[0]
298        .parse()
299        .map_err(|_| TypeError::InvalidData("Invalid hour".to_string()))?;
300    let minute: i64 = parts[1]
301        .parse()
302        .map_err(|_| TypeError::InvalidData("Invalid minute".to_string()))?;
303
304    let (second, usec) = if parts.len() > 2 {
305        let sec_parts: Vec<&str> = parts[2].split('.').collect();
306        let sec: i64 = sec_parts[0].parse().unwrap_or(0);
307        let us: i64 = sec_parts
308            .get(1)
309            .map(|s| {
310                let padded = format!("{:0<6}", s);
311                padded[..6].parse::<i64>().unwrap_or(0)
312            })
313            .unwrap_or(0);
314        (sec, us)
315    } else {
316        (0, 0)
317    };
318
319    Ok(Time {
320        usec: hour * 3_600_000_000 + minute * 60_000_000 + second * 1_000_000 + usec,
321    })
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn test_timestamp_unix_conversion() {
330        // 2024-01-01 00:00:00 UTC
331        let ts = Timestamp::from_unix_secs(1704067200);
332        let back = ts.to_unix_secs();
333        assert_eq!(back, 1704067200);
334    }
335
336    #[test]
337    fn test_timestamp_from_pg_binary() {
338        // Some arbitrary timestamp in binary
339        let usec: i64 = 789_012_345_678_900; // ~25 years after 2000
340        let bytes = usec.to_be_bytes();
341        let ts = Timestamp::from_pg(&bytes, oid::TIMESTAMP, 1).unwrap();
342        assert_eq!(ts.usec, usec);
343    }
344
345    #[test]
346    fn test_date_from_pg_binary() {
347        // 2024-01-01 = 8766 days since 2000-01-01
348        let days: i32 = 8766;
349        let bytes = days.to_be_bytes();
350        let date = Date::from_pg(&bytes, oid::DATE, 1).unwrap();
351        assert_eq!(date.days, days);
352    }
353
354    #[test]
355    fn test_time_from_pg_binary() {
356        // 12:30:45.123456 = 45045123456 microseconds
357        let usec: i64 = 12 * 3_600_000_000 + 30 * 60_000_000 + 45 * 1_000_000 + 123456;
358        let bytes = usec.to_be_bytes();
359        let time = Time::from_pg(&bytes, oid::TIME, 1).unwrap();
360        assert_eq!(time.hour(), 12);
361        assert_eq!(time.minute(), 30);
362        assert_eq!(time.second(), 45);
363        assert_eq!(time.microsecond(), 123456);
364    }
365
366    #[test]
367    fn test_time_from_pg_text() {
368        let time = parse_time_text("14:30:00").unwrap();
369        assert_eq!(time.hour(), 14);
370        assert_eq!(time.minute(), 30);
371        assert_eq!(time.second(), 0);
372    }
373}