1use super::{FromPg, ToPg, TypeError};
6use crate::protocol::types::oid;
7
8const PG_EPOCH_OFFSET_USEC: i64 = 946_684_800_000_000;
11const USEC_PER_DAY: i64 = 86_400_000_000;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct Timestamp {
16 pub usec: i64,
18}
19
20impl Timestamp {
21 pub fn from_pg_usec(usec: i64) -> Self {
23 Self { usec }
24 }
25
26 pub fn from_unix_secs(secs: i64) -> Self {
28 Self {
29 usec: secs * 1_000_000 - PG_EPOCH_OFFSET_USEC,
30 }
31 }
32
33 pub fn to_unix_secs(&self) -> i64 {
35 (self.usec + PG_EPOCH_OFFSET_USEC) / 1_000_000
36 }
37
38 pub fn to_unix_usec(&self) -> i64 {
40 self.usec + PG_EPOCH_OFFSET_USEC
41 }
42}
43
44impl FromPg for Timestamp {
45 fn from_pg(bytes: &[u8], oid_val: u32, format: i16) -> Result<Self, TypeError> {
46 if oid_val != oid::TIMESTAMP && oid_val != oid::TIMESTAMPTZ {
47 return Err(TypeError::UnexpectedOid {
48 expected: "timestamp",
49 got: oid_val,
50 });
51 }
52
53 if format == 1 {
54 if bytes.len() != 8 {
56 return Err(TypeError::InvalidData(
57 "Expected 8 bytes for timestamp".to_string(),
58 ));
59 }
60 let usec = i64::from_be_bytes([
61 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
62 ]);
63 Ok(Timestamp::from_pg_usec(usec))
64 } else {
65 let s =
67 std::str::from_utf8(bytes).map_err(|e| TypeError::InvalidData(e.to_string()))?;
68 parse_timestamp_text(s)
69 }
70 }
71}
72
73impl ToPg for Timestamp {
74 fn to_pg(&self) -> (Vec<u8>, u32, i16) {
75 (self.usec.to_be_bytes().to_vec(), oid::TIMESTAMP, 1)
76 }
77}
78
79#[cfg(feature = "chrono")]
80impl FromPg for chrono::DateTime<chrono::Utc> {
81 fn from_pg(bytes: &[u8], oid_val: u32, format: i16) -> Result<Self, TypeError> {
82 if oid_val != oid::TIMESTAMP && oid_val != oid::TIMESTAMPTZ {
83 return Err(TypeError::UnexpectedOid {
84 expected: "timestamp",
85 got: oid_val,
86 });
87 }
88
89 if format == 1 {
90 if bytes.len() != 8 {
91 return Err(TypeError::InvalidData(
92 "Expected 8 bytes for timestamp".to_string(),
93 ));
94 }
95 let pg_usec = i64::from_be_bytes([
96 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
97 ]);
98 let unix_usec = pg_usec.saturating_add(PG_EPOCH_OFFSET_USEC);
99 chrono::DateTime::<chrono::Utc>::from_timestamp_micros(unix_usec).ok_or_else(|| {
100 TypeError::InvalidData(format!("Timestamp out of range: {}", unix_usec))
101 })
102 } else {
103 let s =
104 std::str::from_utf8(bytes).map_err(|e| TypeError::InvalidData(e.to_string()))?;
105
106 if oid_val == oid::TIMESTAMPTZ {
107 chrono::DateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f%#z")
108 .or_else(|_| chrono::DateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f%#z"))
109 .or_else(|_| chrono::DateTime::parse_from_rfc3339(s))
110 .map(|dt| dt.with_timezone(&chrono::Utc))
111 .map_err(|e| TypeError::InvalidData(format!("Invalid timestamptz: {}", e)))
112 } else {
113 chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f")
114 .or_else(|_| chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f"))
115 .map(|naive| {
116 chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(
117 naive,
118 chrono::Utc,
119 )
120 })
121 .map_err(|e| TypeError::InvalidData(format!("Invalid timestamp: {}", e)))
122 }
123 }
124 }
125}
126
127#[cfg(feature = "chrono")]
128impl ToPg for chrono::DateTime<chrono::Utc> {
129 fn to_pg(&self) -> (Vec<u8>, u32, i16) {
130 let unix_usec = self.timestamp_micros();
131 let pg_usec = unix_usec.saturating_sub(PG_EPOCH_OFFSET_USEC);
132 (pg_usec.to_be_bytes().to_vec(), oid::TIMESTAMPTZ, 1)
133 }
134}
135
136fn parse_timestamp_text(s: &str) -> Result<Timestamp, TypeError> {
138 let parts: Vec<&str> = s.splitn(2, &[' ', 'T'][..]).collect();
142 if parts.len() != 2 {
143 return Err(TypeError::InvalidData(format!("Invalid timestamp: {}", s)));
144 }
145
146 let (year, month, day) = parse_date_components(parts[0])?;
147 let (time_str, timezone_offset_usec) = split_timezone_suffix(parts[1])?;
148 let (hour, minute, second, usec) = parse_time_components(time_str)?;
149 let days_since_epoch = days_from_ymd_checked(year, month, day)?;
150
151 let total_usec = days_since_epoch as i64 * 86_400_000_000
152 + hour as i64 * 3_600_000_000
153 + minute as i64 * 60_000_000
154 + second as i64 * 1_000_000
155 + usec;
156
157 Ok(Timestamp::from_pg_usec(total_usec - timezone_offset_usec))
158}
159
160fn parse_date_components(s: &str) -> Result<(i32, i32, i32), TypeError> {
161 let parts: Vec<&str> = s.split('-').collect();
162 if parts.len() != 3 {
163 return Err(TypeError::InvalidData(format!("Invalid date: {}", s)));
164 }
165 let year = parse_i32_part(parts[0], "year")?;
166 let month = parse_i32_part(parts[1], "month")?;
167 let day = parse_i32_part(parts[2], "day")?;
168 validate_ymd(year, month, day)?;
169 Ok((year, month, day))
170}
171
172fn split_timezone_suffix(s: &str) -> Result<(&str, i64), TypeError> {
173 let s = s.trim_end();
174 if let Some(stripped) = s.strip_suffix('Z') {
175 return Ok((stripped, 0));
176 }
177 if let Some(idx) = s
178 .char_indices()
179 .skip(1)
180 .find_map(|(idx, c)| (c == '+' || c == '-').then_some(idx))
181 {
182 let offset = parse_timezone_offset_usec(&s[idx..]).ok_or_else(|| {
183 TypeError::InvalidData(format!("Invalid timezone offset: {}", &s[idx..]))
184 })?;
185 Ok((&s[..idx], offset))
186 } else {
187 Ok((s, 0))
188 }
189}
190
191fn parse_timezone_offset_usec(s: &str) -> Option<i64> {
192 let sign = match s.as_bytes().first()? {
193 b'+' => 1_i64,
194 b'-' => -1_i64,
195 _ => return None,
196 };
197 let raw = &s[1..];
198 if raw.is_empty() {
199 return None;
200 }
201
202 let (hours, minutes) = if let Some((hours, minutes)) = raw.split_once(':') {
203 (hours, minutes)
204 } else if raw.len() == 4 {
205 (&raw[..2], &raw[2..])
206 } else {
207 (raw, "0")
208 };
209
210 if hours.is_empty() || minutes.is_empty() {
211 return None;
212 }
213 let hours = hours.parse::<i64>().ok()?;
214 let minutes = minutes.parse::<i64>().ok()?;
215 if !(0..=23).contains(&hours) || !(0..=59).contains(&minutes) {
216 return None;
217 }
218
219 Some(sign * ((hours * 3_600 + minutes * 60) * 1_000_000))
220}
221
222fn parse_time_components(s: &str) -> Result<(i32, i32, i32, i64), TypeError> {
223 let parts: Vec<&str> = s.split(':').collect();
224 if !(2..=3).contains(&parts.len()) {
225 return Err(TypeError::InvalidData(format!("Invalid time: {}", s)));
226 }
227
228 let hour = parse_i32_part(parts[0], "hour")?;
229 let minute = parse_i32_part(parts[1], "minute")?;
230 let (second, usec) = if let Some(second_part) = parts.get(2) {
231 parse_second_usec(second_part)?
232 } else {
233 (0, 0)
234 };
235
236 validate_time_components(hour, minute, second, usec)?;
237 Ok((hour, minute, second, usec))
238}
239
240fn parse_second_usec(s: &str) -> Result<(i32, i64), TypeError> {
241 let (second, fraction) = match s.split_once('.') {
242 Some((second, fraction)) => (second, Some(fraction)),
243 None => (s, None),
244 };
245 let second = parse_i32_part(second, "second")?;
246 let usec = match fraction {
247 Some(fraction) => parse_usec_fraction(fraction)?,
248 None => 0,
249 };
250 Ok((second, usec))
251}
252
253fn parse_usec_fraction(s: &str) -> Result<i64, TypeError> {
254 if s.is_empty() || s.len() > 6 || !s.bytes().all(|b| b.is_ascii_digit()) {
255 return Err(TypeError::InvalidData(
256 "Invalid microsecond fraction".to_string(),
257 ));
258 }
259 let padded = format!("{:0<6}", s);
260 padded
261 .parse::<i64>()
262 .map_err(|_| TypeError::InvalidData("Invalid microsecond fraction".to_string()))
263}
264
265fn parse_i32_part(s: &str, label: &str) -> Result<i32, TypeError> {
266 if s.is_empty() {
267 return Err(TypeError::InvalidData(format!("Invalid {}", label)));
268 }
269 s.parse()
270 .map_err(|_| TypeError::InvalidData(format!("Invalid {}", label)))
271}
272
273fn validate_ymd(year: i32, month: i32, day: i32) -> Result<(), TypeError> {
274 if !(1..=12).contains(&month) {
275 return Err(TypeError::InvalidData("Invalid month".to_string()));
276 }
277 let max_day = days_in_month(year, month);
278 if !(1..=max_day).contains(&day) {
279 return Err(TypeError::InvalidData("Invalid day".to_string()));
280 }
281 Ok(())
282}
283
284fn validate_time_components(
285 hour: i32,
286 minute: i32,
287 second: i32,
288 usec: i64,
289) -> Result<(), TypeError> {
290 if !(0..=23).contains(&hour) {
291 return Err(TypeError::InvalidData("Invalid hour".to_string()));
292 }
293 if !(0..=59).contains(&minute) {
294 return Err(TypeError::InvalidData("Invalid minute".to_string()));
295 }
296 if !(0..=59).contains(&second) {
297 return Err(TypeError::InvalidData("Invalid second".to_string()));
298 }
299 if !(0..=999_999).contains(&usec) {
300 return Err(TypeError::InvalidData("Invalid microsecond".to_string()));
301 }
302 Ok(())
303}
304
305fn validate_time_usec(usec: i64) -> Result<(), TypeError> {
306 if !(0..USEC_PER_DAY).contains(&usec) {
307 return Err(TypeError::InvalidData(format!(
308 "Time out of range: {} microseconds",
309 usec
310 )));
311 }
312 Ok(())
313}
314
315fn days_in_month(year: i32, month: i32) -> i32 {
316 match month {
317 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
318 4 | 6 | 9 | 11 => 30,
319 2 if is_leap_year(year) => 29,
320 2 => 28,
321 _ => 0,
322 }
323}
324
325fn days_from_ymd_checked(year: i32, month: i32, day: i32) -> Result<i32, TypeError> {
327 validate_ymd(year, month, day)?;
328 let epoch_days = days_from_civil(2000, 1, 1);
329 let days = days_from_civil(year, month, day)
330 .checked_sub(epoch_days)
331 .ok_or_else(|| TypeError::InvalidData("Date out of range".to_string()))?;
332 i32::try_from(days).map_err(|_| TypeError::InvalidData("Date out of range".to_string()))
333}
334
335fn days_from_civil(year: i32, month: i32, day: i32) -> i64 {
336 let mut year = year as i64;
337 let month = month as i64;
338 let day = day as i64;
339 year -= (month <= 2) as i64;
340 let era = if year >= 0 { year } else { year - 399 } / 400;
341 let year_of_era = year - era * 400;
342 let month_adjusted = month + if month > 2 { -3 } else { 9 };
343 let day_of_year = (153 * month_adjusted + 2) / 5 + day - 1;
344 let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
345 era * 146_097 + day_of_era
346}
347
348fn is_leap_year(year: i32) -> bool {
349 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
350}
351
352#[derive(Debug, Clone, Copy, PartialEq, Eq)]
354pub struct Date {
355 pub days: i32,
357}
358
359impl FromPg for Date {
360 fn from_pg(bytes: &[u8], oid_val: u32, format: i16) -> Result<Self, TypeError> {
361 if oid_val != oid::DATE {
362 return Err(TypeError::UnexpectedOid {
363 expected: "date",
364 got: oid_val,
365 });
366 }
367
368 if format == 1 {
369 if bytes.len() != 4 {
371 return Err(TypeError::InvalidData(
372 "Expected 4 bytes for date".to_string(),
373 ));
374 }
375 let days = i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
376 Ok(Date { days })
377 } else {
378 let s =
380 std::str::from_utf8(bytes).map_err(|e| TypeError::InvalidData(e.to_string()))?;
381 let (year, month, day) = parse_date_components(s)?;
382 Ok(Date {
383 days: days_from_ymd_checked(year, month, day)?,
384 })
385 }
386 }
387}
388
389impl ToPg for Date {
390 fn to_pg(&self) -> (Vec<u8>, u32, i16) {
391 (self.days.to_be_bytes().to_vec(), oid::DATE, 1)
392 }
393}
394
395#[derive(Debug, Clone, Copy, PartialEq, Eq)]
397pub struct Time {
398 pub usec: i64,
400}
401
402impl Time {
403 pub fn new(hour: u8, minute: u8, second: u8, usec: u32) -> Self {
412 Self {
413 usec: hour as i64 * 3_600_000_000
414 + minute as i64 * 60_000_000
415 + second as i64 * 1_000_000
416 + usec as i64,
417 }
418 }
419
420 pub fn hour(&self) -> u8 {
422 ((self.usec / 3_600_000_000) % 24) as u8
423 }
424
425 pub fn minute(&self) -> u8 {
427 ((self.usec / 60_000_000) % 60) as u8
428 }
429
430 pub fn second(&self) -> u8 {
432 ((self.usec / 1_000_000) % 60) as u8
433 }
434
435 pub fn microsecond(&self) -> u32 {
437 (self.usec % 1_000_000) as u32
438 }
439}
440
441impl FromPg for Time {
442 fn from_pg(bytes: &[u8], oid_val: u32, format: i16) -> Result<Self, TypeError> {
443 if oid_val != oid::TIME {
444 return Err(TypeError::UnexpectedOid {
445 expected: "time",
446 got: oid_val,
447 });
448 }
449
450 if format == 1 {
451 if bytes.len() != 8 {
453 return Err(TypeError::InvalidData(
454 "Expected 8 bytes for time".to_string(),
455 ));
456 }
457 let usec = i64::from_be_bytes([
458 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
459 ]);
460 validate_time_usec(usec)?;
461 Ok(Time { usec })
462 } else {
463 let s =
465 std::str::from_utf8(bytes).map_err(|e| TypeError::InvalidData(e.to_string()))?;
466 parse_time_text(s)
467 }
468 }
469}
470
471impl ToPg for Time {
472 fn to_pg(&self) -> (Vec<u8>, u32, i16) {
473 (self.usec.to_be_bytes().to_vec(), oid::TIME, 1)
474 }
475}
476
477fn parse_time_text(s: &str) -> Result<Time, TypeError> {
479 let (hour, minute, second, usec) = parse_time_components(s)?;
480
481 Ok(Time {
482 usec: hour as i64 * 3_600_000_000
483 + minute as i64 * 60_000_000
484 + second as i64 * 1_000_000
485 + usec,
486 })
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492 #[cfg(feature = "chrono")]
493 use chrono::{Datelike, Timelike};
494
495 #[test]
496 fn test_timestamp_unix_conversion() {
497 let ts = Timestamp::from_unix_secs(1704067200);
499 let back = ts.to_unix_secs();
500 assert_eq!(back, 1704067200);
501 }
502
503 #[test]
504 fn test_timestamp_from_pg_binary() {
505 let usec: i64 = 789_012_345_678_900; let bytes = usec.to_be_bytes();
508 let ts = Timestamp::from_pg(&bytes, oid::TIMESTAMP, 1).unwrap();
509 assert_eq!(ts.usec, usec);
510 }
511
512 #[test]
513 fn test_date_from_pg_binary() {
514 let days: i32 = 8766;
516 let bytes = days.to_be_bytes();
517 let date = Date::from_pg(&bytes, oid::DATE, 1).unwrap();
518 assert_eq!(date.days, days);
519 }
520
521 #[test]
522 fn test_time_from_pg_binary() {
523 let usec: i64 = 12 * 3_600_000_000 + 30 * 60_000_000 + 45 * 1_000_000 + 123456;
525 let bytes = usec.to_be_bytes();
526 let time = Time::from_pg(&bytes, oid::TIME, 1).unwrap();
527 assert_eq!(time.hour(), 12);
528 assert_eq!(time.minute(), 30);
529 assert_eq!(time.second(), 45);
530 assert_eq!(time.microsecond(), 123456);
531 }
532
533 #[test]
534 fn test_time_from_pg_binary_rejects_out_of_range_values() {
535 assert!(Time::from_pg(&(-1i64).to_be_bytes(), oid::TIME, 1).is_err());
536 assert!(Time::from_pg(&USEC_PER_DAY.to_be_bytes(), oid::TIME, 1).is_err());
537 }
538
539 #[test]
540 fn test_time_from_pg_text() {
541 let time = parse_time_text("14:30:00").unwrap();
542 assert_eq!(time.hour(), 14);
543 assert_eq!(time.minute(), 30);
544 assert_eq!(time.second(), 0);
545 }
546
547 #[test]
548 fn test_timestamp_from_pg_text_preserves_time_components() {
549 let ts = parse_timestamp_text("2024-12-25 17:30:45.123456").unwrap();
550 let expected_days = days_from_ymd_checked(2024, 12, 25).unwrap() as i64;
551 let expected_usec = expected_days * 86_400_000_000
552 + 17 * 3_600_000_000
553 + 30 * 60_000_000
554 + 45 * 1_000_000
555 + 123_456;
556 assert_eq!(ts.usec, expected_usec);
557 }
558
559 #[test]
560 fn test_timestamp_from_pg_text_rejects_invalid_components() {
561 assert!(parse_timestamp_text("2024-12-25 xx:30:00").is_err());
562 assert!(parse_timestamp_text("2024-12-25 17:bad:00").is_err());
563 assert!(parse_timestamp_text("2024-12-25 17:30:bad").is_err());
564 assert!(parse_timestamp_text("2024-13-25 17:30:00").is_err());
565 assert!(parse_timestamp_text("2024-02-30 17:30:00").is_err());
566 assert!(parse_timestamp_text("2024-12-25 17:30:00+bad").is_err());
567 assert!(parse_timestamp_text("2024-12-25 17:30:00+25:00").is_err());
568 }
569
570 #[test]
571 fn test_timestamp_from_pg_text_ignores_timezone_suffix_without_trimming_time() {
572 let ts = parse_timestamp_text("2024-12-25 17:30:45+00").unwrap();
573 let expected_days = days_from_ymd_checked(2024, 12, 25).unwrap() as i64;
574 let expected_usec =
575 expected_days * 86_400_000_000 + 17 * 3_600_000_000 + 30 * 60_000_000 + 45 * 1_000_000;
576 assert_eq!(ts.usec, expected_usec);
577 }
578
579 #[test]
580 fn test_timestamp_from_pg_text_applies_timezone_offset() {
581 let ts = parse_timestamp_text("2024-12-25 17:30:45+02:30").unwrap();
582 let expected_days = days_from_ymd_checked(2024, 12, 25).unwrap() as i64;
583 let expected_usec = expected_days * 86_400_000_000 + 15 * 3_600_000_000 + 45 * 1_000_000;
584 assert_eq!(ts.usec, expected_usec);
585
586 let negative = parse_timestamp_text("2024-12-25 17:30:45-0330").unwrap();
587 let negative_expected =
588 expected_days * 86_400_000_000 + 21 * 3_600_000_000 + 45 * 1_000_000;
589 assert_eq!(negative.usec, negative_expected);
590 }
591
592 #[test]
593 fn test_date_from_pg_text_rejects_invalid_components() {
594 assert!(Date::from_pg(b"2024-13-01", oid::DATE, 0).is_err());
595 assert!(Date::from_pg(b"2024-aa-01", oid::DATE, 0).is_err());
596 assert!(Date::from_pg(b"2024-02-30", oid::DATE, 0).is_err());
597 }
598
599 #[test]
600 fn test_time_from_pg_text_rejects_invalid_components() {
601 assert!(parse_time_text("24:00:00").is_err());
602 assert!(parse_time_text("14:60:00").is_err());
603 assert!(parse_time_text("14:30:bad").is_err());
604 assert!(parse_time_text("14:30:00.bad").is_err());
605 assert!(parse_time_text("14:30:00.1234567").is_err());
606 }
607
608 #[cfg(feature = "chrono")]
609 #[test]
610 fn test_chrono_datetime_from_pg_binary() {
611 let pg_usec = -PG_EPOCH_OFFSET_USEC;
613 let bytes = pg_usec.to_be_bytes();
614 let dt = chrono::DateTime::<chrono::Utc>::from_pg(&bytes, oid::TIMESTAMPTZ, 1).unwrap();
615 assert_eq!(dt.timestamp(), 0);
616 }
617
618 #[cfg(feature = "chrono")]
619 #[test]
620 fn test_chrono_datetime_from_pg_text_timestamptz() {
621 let dt = chrono::DateTime::<chrono::Utc>::from_pg(
622 b"2024-12-25 17:30:00+00",
623 oid::TIMESTAMPTZ,
624 0,
625 )
626 .unwrap();
627 assert_eq!(dt.year(), 2024);
628 assert_eq!(dt.month(), 12);
629 assert_eq!(dt.day(), 25);
630 assert_eq!(dt.hour(), 17);
631 assert_eq!(dt.minute(), 30);
632 }
633
634 #[cfg(feature = "chrono")]
635 #[test]
636 fn test_chrono_datetime_to_pg_binary() {
637 let dt =
638 chrono::DateTime::<chrono::Utc>::from_timestamp(1_704_067_200, 123_456_000).unwrap();
639 let (bytes, oid_val, format) = dt.to_pg();
640 assert_eq!(oid_val, oid::TIMESTAMPTZ);
641 assert_eq!(format, 1);
642 assert_eq!(bytes.len(), 8);
643 }
644}