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