1mod decode;
10
11use rupg_common::SqlState;
12
13pub use decode::{
14 Abbrev, DateTimeInput, DateUnit, NoZones, ZoneAbbrevs, ZoneLookup, ZoneName, date_in,
15 decode_numeric_timezone, decode_timezone_abbrev_prefix, decode_timezone_name, interval_in,
16 local_offset, time_in, timestamp_in, timestamptz_in, timetz_in,
17};
18
19use crate::binary::Recv;
20use crate::error::TypeError;
21use crate::number::u64_out;
22use crate::typmod::{
23 INTERVAL_FULL_PRECISION, INTERVAL_FULL_RANGE, IntervalField, MAX_TIME_PRECISION,
24};
25
26pub const POSTGRES_EPOCH_JDATE: i32 = 2451545;
28pub const UNIX_EPOCH_JDATE: i32 = 2440588;
30pub const UNIX_TO_POSTGRES_DAYS: i32 = UNIX_EPOCH_JDATE - POSTGRES_EPOCH_JDATE;
32pub const USECS_PER_SEC: i64 = 1_000_000;
33pub const USECS_PER_DAY: i64 = 86_400 * USECS_PER_SEC;
34pub const UNIX_TO_POSTGRES_USECS: i64 = UNIX_TO_POSTGRES_DAYS as i64 * USECS_PER_DAY;
36
37pub const DATE_NEGATIVE_INFINITY: i32 = i32::MIN;
39pub const DATE_INFINITY: i32 = i32::MAX;
40pub const TIMESTAMP_NEGATIVE_INFINITY: i64 = i64::MIN;
42pub const TIMESTAMP_INFINITY: i64 = i64::MAX;
43
44const DATE_END_JULIAN: i32 = 2147483494;
46const MIN_TIMESTAMP: i64 = -211813488000000000;
48const END_TIMESTAMP: i64 = 9223371331200000000;
49const TZDISP_LIMIT: i32 = 16 * 3600;
51const MAX_TZ_LEN: usize = 10;
53
54const DAYS: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
55const MONTHS: [&str; 12] =
56 ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum DateStyle {
61 Iso,
62 Sql,
63 Postgres,
64 German,
65 Xsd,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum DateOrder {
72 Mdy,
73 Dmy,
74 Ymd,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub struct DateFormat {
80 pub style: DateStyle,
81 pub order: DateOrder,
82}
83
84impl DateFormat {
85 pub const ISO_MDY: DateFormat = DateFormat { style: DateStyle::Iso, order: DateOrder::Mdy };
86
87 fn day_first(self) -> bool {
88 self.order == DateOrder::Dmy
89 }
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum IntervalStyle {
95 Postgres,
96 PostgresVerbose,
97 SqlStandard,
98 Iso8601,
99}
100
101pub trait TimeZone {
103 fn at(&self, unix_seconds: i64) -> (i32, &str);
104
105 fn next_change(&self, unix_seconds: i64) -> (i32, Option<(i64, i32)>) {
107 (self.at(unix_seconds).0, None)
108 }
109
110 fn abbrev_meaning(&self, _abbrev: &str) -> Option<AbbrevMeaning> {
112 None
113 }
114
115 fn abbrev_at(&self, _abbrev: &str, _unix_seconds: i64) -> Option<(i32, bool)> {
117 None
118 }
119
120 fn fixed_offset(&self) -> Option<i32> {
122 None
123 }
124
125 fn abbrevs(&self) -> Vec<&str> {
127 Vec::new()
128 }
129}
130
131impl<T: TimeZone + ?Sized> TimeZone for std::sync::Arc<T> {
132 fn at(&self, unix_seconds: i64) -> (i32, &str) {
133 (**self).at(unix_seconds)
134 }
135
136 fn next_change(&self, unix_seconds: i64) -> (i32, Option<(i64, i32)>) {
137 (**self).next_change(unix_seconds)
138 }
139
140 fn abbrev_meaning(&self, abbrev: &str) -> Option<AbbrevMeaning> {
141 (**self).abbrev_meaning(abbrev)
142 }
143
144 fn abbrev_at(&self, abbrev: &str, unix_seconds: i64) -> Option<(i32, bool)> {
145 (**self).abbrev_at(abbrev, unix_seconds)
146 }
147
148 fn fixed_offset(&self) -> Option<i32> {
149 (**self).fixed_offset()
150 }
151
152 fn abbrevs(&self) -> Vec<&str> {
153 (**self).abbrevs()
154 }
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159pub enum AbbrevMeaning {
160 Fixed { offset: i32, dst: bool },
162 Varies,
164}
165
166#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct FixedZone {
169 pub offset: i32,
171 pub abbrev: String,
172}
173
174impl FixedZone {
175 pub fn utc() -> FixedZone {
176 FixedZone { offset: 0, abbrev: "UTC".to_string() }
177 }
178}
179
180impl TimeZone for FixedZone {
181 fn at(&self, _: i64) -> (i32, &str) {
182 (self.offset, &self.abbrev)
183 }
184
185 fn abbrev_meaning(&self, abbrev: &str) -> Option<AbbrevMeaning> {
186 (!self.abbrev.is_empty() && abbrev == self.abbrev)
187 .then_some(AbbrevMeaning::Fixed { offset: self.offset, dst: false })
188 }
189
190 fn abbrev_at(&self, abbrev: &str, _: i64) -> Option<(i32, bool)> {
191 (!self.abbrev.is_empty() && abbrev == self.abbrev).then_some((self.offset, false))
192 }
193
194 fn fixed_offset(&self) -> Option<i32> {
195 Some(self.offset)
196 }
197
198 fn abbrevs(&self) -> Vec<&str> {
199 vec![&self.abbrev]
200 }
201}
202
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205pub struct Interval {
206 pub time: i64,
207 pub day: i32,
208 pub month: i32,
209}
210
211impl Interval {
212 pub const NEGATIVE_INFINITY: Interval =
213 Interval { time: i64::MIN, day: i32::MIN, month: i32::MIN };
214 pub const INFINITY: Interval = Interval { time: i64::MAX, day: i32::MAX, month: i32::MAX };
215}
216
217pub fn date_from_unix(days: i32) -> Result<i32, TypeError> {
219 match days {
220 i32::MAX => Ok(DATE_INFINITY),
221 days if days == -i32::MAX => Ok(DATE_NEGATIVE_INFINITY),
222 days => {
223 let date = i64::from(days) + i64::from(UNIX_TO_POSTGRES_DAYS);
224 let valid =
225 i64::from(-POSTGRES_EPOCH_JDATE)..i64::from(DATE_END_JULIAN - POSTGRES_EPOCH_JDATE);
226 if valid.contains(&date) { Ok(date as i32) } else { Err(out_of_range("date")) }
227 }
228 }
229}
230
231pub fn timestamp_from_unix(micros: i64) -> Result<i64, TypeError> {
233 match micros {
234 i64::MAX => Ok(TIMESTAMP_INFINITY),
235 micros if micros == -i64::MAX => Ok(TIMESTAMP_NEGATIVE_INFINITY),
236 micros => micros
237 .checked_add(UNIX_TO_POSTGRES_USECS)
238 .filter(|ts| (MIN_TIMESTAMP..END_TIMESTAMP).contains(ts))
239 .ok_or_else(|| out_of_range("timestamp")),
240 }
241}
242
243fn out_of_range(what: &str) -> TypeError {
244 TypeError::new(SqlState::DATETIME_VALUE_OUT_OF_RANGE, format!("{what} out of range"))
245}
246
247pub fn date2j(year: i32, month: i32, day: i32) -> i32 {
251 let (month, year) = if month > 2 {
252 (month.wrapping_add(1), year.wrapping_add(4800))
253 } else {
254 (month.wrapping_add(13), year.wrapping_add(4799))
255 };
256 let century = year / 100;
257 let julian = year.wrapping_mul(365).wrapping_sub(32167);
258 let julian = julian.wrapping_add(year / 4 - century + century / 4);
259 julian.wrapping_add(7834i32.wrapping_mul(month) / 256).wrapping_add(day)
260}
261
262pub fn j2date(jd: i32) -> (i32, u32, u32) {
264 let mut julian = (jd as u32).wrapping_add(32044);
265 let mut quad = julian / 146097;
266 let extra = (julian - quad * 146097) * 4 + 3;
267 julian = julian.wrapping_add(60 + quad * 3 + extra / 146097);
268 quad = julian / 1461;
269 julian -= quad * 1461;
270 let mut y = julian * 4 / 1461;
271 julian = if y != 0 { (julian + 305) % 365 } else { (julian + 306) % 366 } + 123;
272 y += quad * 4;
273 let quad = julian * 2141 / 65536;
274 (y as i32 - 4800, (quad + 10) % 12 + 1, julian - 7834 * quad / 256)
275}
276
277struct Fields {
279 year: i32,
280 month: u32,
281 day: u32,
282 hour: u32,
283 minute: u32,
284 second: u32,
285 usec: u32,
286}
287
288impl Fields {
289 fn of_timestamp(ts: i64) -> Option<Fields> {
291 let date = ts.div_euclid(USECS_PER_DAY) + i64::from(POSTGRES_EPOCH_JDATE);
292 let jd = i32::try_from(date).ok().filter(|&jd| jd >= 0)?;
293 let time = ts.rem_euclid(USECS_PER_DAY);
294 let (year, month, day) = j2date(jd);
295 let (hour, minute, second, usec) = split_time(time);
296 Some(Fields { year, month, day, hour, minute, second, usec })
297 }
298}
299
300fn split_time(time: i64) -> (u32, u32, u32, u32) {
302 let usec = (time % USECS_PER_SEC) as u32;
303 let seconds = time / USECS_PER_SEC;
304 ((seconds / 3600) as u32, (seconds / 60 % 60) as u32, (seconds % 60) as u32, usec)
305}
306
307fn zeropad(n: u64, width: usize, out: &mut Vec<u8>) {
309 let start = out.len();
310 u64_out(n, out);
311 let len = out.len() - start;
312 if len < width {
313 out.splice(start..start, std::iter::repeat_n(b'0', width - len));
314 }
315}
316
317fn two(n: u32, out: &mut Vec<u8>) {
319 debug_assert!(n < 100);
320 out.extend_from_slice(&[b'0' + (n / 10) as u8, b'0' + (n % 10) as u8]);
321}
322
323fn year(year: i32, out: &mut Vec<u8>) {
325 let shown = if year > 0 { year } else { 1 - year };
326 zeropad(shown as u64, 4, out);
327}
328
329fn seconds(sec: i64, usec: i64, fill: bool, out: &mut Vec<u8>) {
331 if fill {
332 zeropad(sec.unsigned_abs(), 2, out);
333 } else {
334 u64_out(sec.unsigned_abs(), out);
335 }
336 if usec != 0 {
337 out.push(b'.');
338 let mut usec = usec.unsigned_abs();
339 let mut digits = 6;
340 while usec.is_multiple_of(10) {
341 usec /= 10;
342 digits -= 1;
343 }
344 zeropad(usec, digits, out);
345 }
346}
347
348fn zone_offset(offset: i32, minutes: bool, out: &mut Vec<u8>) {
350 out.push(if offset >= 0 { b'+' } else { b'-' });
351 let total = offset.unsigned_abs();
352 let (hour, min, sec) = (total / 3600, total / 60 % 60, total % 60);
353 zeropad(u64::from(hour), 2, out);
354 if min != 0 || sec != 0 || minutes {
355 out.push(b':');
356 two(min, out);
357 }
358 if sec != 0 {
359 out.push(b':');
360 two(sec, out);
361 }
362}
363
364fn date_part(f: &Fields, format: DateFormat, out: &mut Vec<u8>) {
366 let (first, second, sep) = match (format.style, format.day_first()) {
367 (DateStyle::Iso | DateStyle::Xsd, _) => {
368 year(f.year, out);
369 out.push(b'-');
370 two(f.month, out);
371 out.push(b'-');
372 two(f.day, out);
373 return;
374 }
375 (DateStyle::German, _) => (f.day, f.month, b'.'),
376 (DateStyle::Sql, true) => (f.day, f.month, b'/'),
377 (DateStyle::Sql, false) => (f.month, f.day, b'/'),
378 (DateStyle::Postgres, true) => (f.day, f.month, b'-'),
379 (DateStyle::Postgres, false) => (f.month, f.day, b'-'),
380 };
381 two(first, out);
382 out.push(sep);
383 two(second, out);
384 out.push(sep);
385 year(f.year, out);
386}
387
388fn time_part(hour: u32, minute: u32, second: u32, usec: u32, out: &mut Vec<u8>) {
389 zeropad(u64::from(hour), 2, out);
390 out.push(b':');
391 two(minute, out);
392 out.push(b':');
393 seconds(i64::from(second), i64::from(usec), true, out);
394}
395
396fn bc(year: i32, out: &mut Vec<u8>) {
397 if year <= 0 {
398 out.extend_from_slice(b" BC");
399 }
400}
401
402pub fn date_out(date: i32, format: DateFormat, out: &mut Vec<u8>) {
404 match date {
405 DATE_NEGATIVE_INFINITY => return out.extend_from_slice(b"-infinity"),
406 DATE_INFINITY => return out.extend_from_slice(b"infinity"),
407 _ => {}
408 }
409 let (year, month, day) = j2date(date.wrapping_add(POSTGRES_EPOCH_JDATE));
410 let fields = Fields { year, month, day, hour: 0, minute: 0, second: 0, usec: 0 };
411 date_part(&fields, format, out);
412 bc(year, out);
413}
414
415pub fn date_recv(recv: &mut Recv<'_>) -> Result<i32, TypeError> {
417 let date = recv.i32()?;
418 let valid = -POSTGRES_EPOCH_JDATE..DATE_END_JULIAN - POSTGRES_EPOCH_JDATE;
419 if date == DATE_NEGATIVE_INFINITY || date == DATE_INFINITY || valid.contains(&date) {
420 Ok(date)
421 } else {
422 Err(out_of_range("date"))
423 }
424}
425
426pub fn time_out(time: i64, out: &mut Vec<u8>) {
428 let (hour, minute, second, usec) = split_time(time);
429 time_part(hour, minute, second, usec, out);
430}
431
432pub fn timetz_out(time: i64, zone: i32, out: &mut Vec<u8>) {
434 time_out(time, out);
435 zone_offset(-zone, false, out);
436}
437
438fn round_usecs(value: i64, precision: i32) -> i64 {
440 let scale = 10i64.pow((MAX_TIME_PRECISION - precision) as u32);
441 let half = scale / 2;
442 if value >= 0 { (value + half) / scale * scale } else { -((-value + half) / scale * scale) }
443}
444
445fn time_value(recv: &mut Recv<'_>) -> Result<i64, TypeError> {
446 let time = recv.i64()?;
447 if !(0..=USECS_PER_DAY).contains(&time) {
448 return Err(out_of_range("time"));
449 }
450 Ok(time)
451}
452
453pub fn adjust_time(time: i64, typmod: i32) -> i64 {
455 if (0..=MAX_TIME_PRECISION).contains(&typmod) { round_usecs(time, typmod) } else { time }
456}
457
458pub fn time_recv(recv: &mut Recv<'_>, typmod: i32) -> Result<i64, TypeError> {
460 Ok(adjust_time(time_value(recv)?, typmod))
461}
462
463pub fn timetz_recv(recv: &mut Recv<'_>, typmod: i32) -> Result<(i64, i32), TypeError> {
465 let time = time_value(recv)?;
466 let zone = recv.i32()?;
467 if zone <= -TZDISP_LIMIT || zone >= TZDISP_LIMIT {
468 return Err(TypeError::new(
469 SqlState::INVALID_TIME_ZONE_DISPLACEMENT_VALUE,
470 "time zone displacement out of range".to_string(),
471 ));
472 }
473 Ok((adjust_time(time, typmod), zone))
474}
475
476fn date_time(f: &Fields, zone: Option<(i32, &str)>, format: DateFormat, out: &mut Vec<u8>) {
478 let abbrev = |abbrev: &str, out: &mut Vec<u8>| {
479 out.push(b' ');
480 out.extend_from_slice(&abbrev.as_bytes()[..abbrev.len().min(MAX_TZ_LEN)]);
481 };
482 match format.style {
483 DateStyle::Iso | DateStyle::Xsd => {
484 let xsd = format.style == DateStyle::Xsd;
485 date_part(f, format, out);
486 out.push(if xsd { b'T' } else { b' ' });
487 time_part(f.hour, f.minute, f.second, f.usec, out);
488 if let Some((offset, _)) = zone {
489 zone_offset(offset, xsd, out);
490 }
491 }
492 DateStyle::Sql | DateStyle::German => {
493 date_part(f, format, out);
494 out.push(b' ');
495 time_part(f.hour, f.minute, f.second, f.usec, out);
496 if let Some((_, name)) = zone {
497 abbrev(name, out);
498 }
499 }
500 DateStyle::Postgres => {
501 let weekday = (date2j(f.year, f.month as i32, f.day as i32) + 1).rem_euclid(7);
502 out.extend_from_slice(DAYS[weekday as usize].as_bytes());
503 out.push(b' ');
504 let month = MONTHS[f.month as usize - 1].as_bytes();
505 if format.day_first() {
506 two(f.day, out);
507 out.push(b' ');
508 out.extend_from_slice(month);
509 } else {
510 out.extend_from_slice(month);
511 out.push(b' ');
512 two(f.day, out);
513 }
514 out.push(b' ');
515 time_part(f.hour, f.minute, f.second, f.usec, out);
516 out.push(b' ');
517 year(f.year, out);
518 if let Some((_, name)) = zone {
519 abbrev(name, out);
520 }
521 }
522 }
523 bc(f.year, out);
524}
525
526pub fn timestamp_out(ts: i64, format: DateFormat, out: &mut Vec<u8>) -> Result<(), TypeError> {
528 match ts {
529 TIMESTAMP_NEGATIVE_INFINITY => out.extend_from_slice(b"-infinity"),
530 TIMESTAMP_INFINITY => out.extend_from_slice(b"infinity"),
531 _ => date_time(
532 &Fields::of_timestamp(ts).ok_or_else(|| out_of_range("timestamp"))?,
533 None,
534 format,
535 out,
536 ),
537 }
538 Ok(())
539}
540
541pub fn timestamptz_out(
543 ts: i64,
544 format: DateFormat,
545 zone: &(impl TimeZone + ?Sized),
546 out: &mut Vec<u8>,
547) -> Result<(), TypeError> {
548 let infinity: &[u8] = match ts {
549 TIMESTAMP_NEGATIVE_INFINITY => b"-infinity",
550 TIMESTAMP_INFINITY => b"infinity",
551 _ => b"",
552 };
553 if !infinity.is_empty() {
554 out.extend_from_slice(infinity);
555 return Ok(());
556 }
557 let utc = Fields::of_timestamp(ts).ok_or_else(|| out_of_range("timestamp"))?;
559 let unix = ts.div_euclid(USECS_PER_SEC) - UNIX_TO_POSTGRES_USECS / USECS_PER_SEC;
560 let (offset, name) = zone.at(unix);
561 let local = unix + i64::from(offset);
562 let (year, month, day) = j2date((local.div_euclid(86400) + i64::from(UNIX_EPOCH_JDATE)) as i32);
563 let (hour, minute, second, _) = split_time(local.rem_euclid(86400) * USECS_PER_SEC);
564 let fields = Fields { year, month, day, hour, minute, second, usec: utc.usec };
565 date_time(&fields, Some((offset, name)), format, out);
566 Ok(())
567}
568
569pub fn timestamp_recv(recv: &mut Recv<'_>, typmod: i32) -> Result<i64, TypeError> {
571 let ts = recv.i64()?;
572 if ts == TIMESTAMP_NEGATIVE_INFINITY || ts == TIMESTAMP_INFINITY {
573 return Ok(ts);
574 }
575 if Fields::of_timestamp(ts).is_none() || !(MIN_TIMESTAMP..END_TIMESTAMP).contains(&ts) {
576 return Err(out_of_range("timestamp"));
577 }
578 adjust_timestamp(ts, typmod)
579}
580
581pub fn adjust_timestamp(ts: i64, typmod: i32) -> Result<i64, TypeError> {
583 if ts == TIMESTAMP_NEGATIVE_INFINITY
584 || ts == TIMESTAMP_INFINITY
585 || typmod == -1
586 || typmod == MAX_TIME_PRECISION
587 {
588 return Ok(ts);
589 }
590 if !(0..=MAX_TIME_PRECISION).contains(&typmod) {
591 return Err(TypeError::new(
592 SqlState::INVALID_PARAMETER_VALUE,
593 format!("timestamp({typmod}) precision must be between 0 and {MAX_TIME_PRECISION}"),
594 ));
595 }
596 Ok(round_usecs(ts, typmod))
597}
598
599struct Parts {
601 zero: bool,
602 before: bool,
603}
604
605impl Parts {
606 fn postgres(&mut self, value: i64, unit: &str, out: &mut Vec<u8>) {
608 if value == 0 {
609 return;
610 }
611 if !self.zero {
612 out.push(b' ');
613 }
614 if self.before && value > 0 {
615 out.push(b'+');
616 }
617 out.extend_from_slice(format!("{value} {unit}").as_bytes());
618 if value != 1 {
619 out.push(b's');
620 }
621 self.before = value < 0;
622 self.zero = false;
623 }
624
625 fn verbose(&mut self, mut value: i64, unit: &str, out: &mut Vec<u8>) {
627 if value == 0 {
628 return;
629 }
630 if self.zero {
631 self.before = value < 0;
632 value = value.abs();
633 } else if self.before {
634 value = -value;
635 }
636 out.extend_from_slice(format!(" {value} {unit}").as_bytes());
637 if value != 1 {
638 out.push(b's');
639 }
640 self.zero = false;
641 }
642}
643
644pub fn interval_out(period: &Interval, style: IntervalStyle, out: &mut Vec<u8>) {
646 if *period == Interval::NEGATIVE_INFINITY {
647 return out.extend_from_slice(b"-infinity");
648 }
649 if *period == Interval::INFINITY {
650 return out.extend_from_slice(b"infinity");
651 }
652 let mut year = i64::from(period.month / 12);
654 let mut mon = i64::from(period.month % 12);
655 let mut mday = i64::from(period.day);
656 let mut hour = period.time / 3_600_000_000;
657 let mut min = period.time / 60_000_000 % 60;
658 let mut sec = period.time / USECS_PER_SEC % 60;
659 let mut usec = period.time % USECS_PER_SEC;
660 let time_negative = hour < 0 || min < 0 || sec < 0 || usec < 0;
661 let time_nonzero = hour != 0 || min != 0 || sec != 0 || usec != 0;
662 let mut parts = Parts { zero: true, before: false };
663 match style {
664 IntervalStyle::SqlStandard => {
665 let negative = year < 0 || mon < 0 || mday < 0 || time_negative;
666 let positive =
667 year > 0 || mon > 0 || mday > 0 || hour > 0 || min > 0 || sec > 0 || usec > 0;
668 let year_month = year != 0 || mon != 0;
669 let day_time = mday != 0 || time_nonzero;
670 let standard = !(negative && positive) && !(year_month && day_time);
671 if negative && standard {
672 out.push(b'-');
673 (year, mon, mday, hour, min, sec, usec) =
674 (-year, -mon, -mday, -hour, -min, -sec, -usec);
675 }
676 if !negative && !positive {
677 out.push(b'0');
678 } else if !standard {
679 let sign = |negative: bool| if negative { '-' } else { '+' };
680 out.extend_from_slice(
681 format!(
682 "{}{}-{} {}{} {}{}:{:02}:",
683 sign(year < 0 || mon < 0),
684 year.abs(),
685 mon.abs(),
686 sign(mday < 0),
687 mday.abs(),
688 sign(time_negative),
689 hour.abs(),
690 min.abs()
691 )
692 .as_bytes(),
693 );
694 seconds(sec, usec, true, out);
695 } else if year_month {
696 out.extend_from_slice(format!("{year}-{mon}").as_bytes());
697 } else {
698 if mday != 0 {
699 out.extend_from_slice(format!("{mday} ").as_bytes());
700 }
701 out.extend_from_slice(format!("{hour}:{min:02}:").as_bytes());
702 seconds(sec, usec, true, out);
703 }
704 }
705 IntervalStyle::Iso8601 => {
706 if year == 0 && mon == 0 && mday == 0 && !time_nonzero {
707 return out.extend_from_slice(b"PT0S");
708 }
709 out.push(b'P');
710 let part = |value: i64, unit: char, out: &mut Vec<u8>| {
711 if value != 0 {
712 out.extend_from_slice(format!("{value}{unit}").as_bytes());
713 }
714 };
715 part(year, 'Y', out);
716 part(mon, 'M', out);
717 part(mday, 'D', out);
718 if time_nonzero {
719 out.push(b'T');
720 }
721 part(hour, 'H', out);
722 part(min, 'M', out);
723 if sec != 0 || usec != 0 {
724 if sec < 0 || usec < 0 {
725 out.push(b'-');
726 }
727 seconds(sec, usec, false, out);
728 out.push(b'S');
729 }
730 }
731 IntervalStyle::Postgres => {
732 parts.postgres(year, "year", out);
733 parts.postgres(mon, "mon", out);
734 parts.postgres(mday, "day", out);
735 if parts.zero || time_nonzero {
736 if !parts.zero {
737 out.push(b' ');
738 }
739 if time_negative {
740 out.push(b'-');
741 } else if parts.before {
742 out.push(b'+');
743 }
744 zeropad(hour.unsigned_abs(), 2, out);
745 out.push(b':');
746 zeropad(min.unsigned_abs(), 2, out);
747 out.push(b':');
748 seconds(sec, usec, true, out);
749 }
750 }
751 IntervalStyle::PostgresVerbose => {
752 out.push(b'@');
753 parts.verbose(year, "year", out);
754 parts.verbose(mon, "mon", out);
755 parts.verbose(mday, "day", out);
756 parts.verbose(hour, "hour", out);
757 parts.verbose(min, "min", out);
758 if sec != 0 || usec != 0 {
759 out.push(b' ');
760 if sec < 0 || (sec == 0 && usec < 0) {
761 if parts.zero {
762 parts.before = true;
763 } else if !parts.before {
764 out.push(b'-');
765 }
766 } else if parts.before {
767 out.push(b'-');
768 }
769 seconds(sec, usec, false, out);
770 out.extend_from_slice(if sec.abs() != 1 || usec != 0 { b" secs" } else { b" sec" });
771 parts.zero = false;
772 }
773 if parts.zero {
774 out.extend_from_slice(b" 0");
775 }
776 if parts.before {
777 out.extend_from_slice(b" ago");
778 }
779 }
780 }
781}
782
783pub fn interval_recv(recv: &mut Recv<'_>, typmod: i32) -> Result<Interval, TypeError> {
785 let period = Interval { time: recv.i64()?, day: recv.i32()?, month: recv.i32()? };
786 adjust_interval(period, typmod)
787}
788
789pub fn adjust_interval(mut period: Interval, typmod: i32) -> Result<Interval, TypeError> {
791 if typmod < 0 || period == Interval::INFINITY || period == Interval::NEGATIVE_INFINITY {
792 return Ok(period);
793 }
794 let (range, precision) = ((typmod >> 16) & 0x7fff, typmod & 0xffff);
795 let mask = |fields: &[IntervalField]| crate::typmod::interval_range(fields);
796 use IntervalField::*;
797 const HOUR_USECS: i64 = 3_600_000_000;
798 const MINUTE_USECS: i64 = 60_000_000;
799 let hours = |time: i64| time / HOUR_USECS * HOUR_USECS;
800 let minutes = |time: i64| time / MINUTE_USECS * MINUTE_USECS;
801 match range {
802 r if r == INTERVAL_FULL_RANGE
803 || r == mask(&[Second])
804 || r == mask(&[Day, Hour, Minute, Second])
805 || r == mask(&[Hour, Minute, Second])
806 || r == mask(&[Minute, Second]) => {}
807 r if r == mask(&[Year]) => {
808 period = Interval { time: 0, day: 0, month: period.month / 12 * 12 }
809 }
810 r if r == mask(&[Month]) || r == mask(&[Year, Month]) => (period.day, period.time) = (0, 0),
811 r if r == mask(&[Day]) => period.time = 0,
812 r if r == mask(&[Hour]) || r == mask(&[Day, Hour]) => period.time = hours(period.time),
813 r if r == mask(&[Minute])
814 || r == mask(&[Day, Hour, Minute])
815 || r == mask(&[Hour, Minute]) =>
816 {
817 period.time = minutes(period.time)
818 }
819 _ => {
820 return Err(TypeError::new(
821 SqlState::INTERNAL_ERROR,
822 format!("unrecognized interval typmod: {typmod}"),
823 ));
824 }
825 }
826 if precision != INTERVAL_FULL_PRECISION {
827 if !(0..=MAX_TIME_PRECISION).contains(&precision) {
828 return Err(TypeError::new(
829 SqlState::INVALID_PARAMETER_VALUE,
830 format!(
831 "interval({precision}) precision must be between 0 and {MAX_TIME_PRECISION}"
832 ),
833 ));
834 }
835 let scale = 10i64.pow((MAX_TIME_PRECISION - precision) as u32);
836 let half = scale / 2;
837 let time = if period.time >= 0 {
838 period.time.checked_add(half)
839 } else {
840 period.time.checked_sub(half)
841 };
842 let time = time.ok_or_else(|| out_of_range("interval"))?;
843 period.time = time - time % scale;
844 }
845 Ok(period)
846}
847
848pub fn interval_send(period: &Interval, out: &mut Vec<u8>) {
850 out.extend_from_slice(&period.time.to_be_bytes());
851 out.extend_from_slice(&period.day.to_be_bytes());
852 out.extend_from_slice(&period.month.to_be_bytes());
853}
854
855#[cfg(test)]
856mod tests {
857 use super::*;
858
859 fn text(f: impl FnOnce(&mut Vec<u8>)) -> String {
860 let mut out = Vec::new();
861 f(&mut out);
862 String::from_utf8(out).unwrap()
863 }
864
865 #[test]
866 fn the_julian_day_goes_both_ways() {
867 assert_eq!(date2j(2000, 1, 1), POSTGRES_EPOCH_JDATE);
868 assert_eq!(date2j(1970, 1, 1), UNIX_EPOCH_JDATE);
869 assert_eq!(date2j(5874898, 1, 1), DATE_END_JULIAN);
870 assert_eq!(date2j(-4713, 11, 24), 0);
871 for jd in [0, 1, 1721425, 1721426, 2451545, 2460954, 109203528, DATE_END_JULIAN - 1] {
872 let (y, m, d) = j2date(jd);
873 assert_eq!(date2j(y, m as i32, d as i32), jd);
874 }
875 assert_eq!(UNIX_TO_POSTGRES_USECS, -946_684_800_000_000);
876 }
877
878 #[test]
879 fn each_style_writes_its_layout() {
880 let ts =
881 (date2j(2026, 10, 5) - POSTGRES_EPOCH_JDATE) as i64 * USECS_PER_DAY + 45_296_789_000;
882 let at =
883 |style, order| text(|out| timestamp_out(ts, DateFormat { style, order }, out).unwrap());
884 assert_eq!(at(DateStyle::Iso, DateOrder::Mdy), "2026-10-05 12:34:56.789");
885 assert_eq!(at(DateStyle::Sql, DateOrder::Mdy), "10/05/2026 12:34:56.789");
886 assert_eq!(at(DateStyle::Sql, DateOrder::Dmy), "05/10/2026 12:34:56.789");
887 assert_eq!(at(DateStyle::German, DateOrder::Ymd), "05.10.2026 12:34:56.789");
888 assert_eq!(at(DateStyle::Postgres, DateOrder::Mdy), "Mon Oct 05 12:34:56.789 2026");
889 assert_eq!(at(DateStyle::Postgres, DateOrder::Dmy), "Mon 05 Oct 12:34:56.789 2026");
890 let zone = FixedZone { offset: 19800, abbrev: "IST".to_string() };
891 let tz = |style| {
892 text(|out| {
893 timestamptz_out(ts, DateFormat { style, order: DateOrder::Mdy }, &zone, out)
894 .unwrap()
895 })
896 };
897 assert_eq!(tz(DateStyle::Iso), "2026-10-05 18:04:56.789+05:30");
898 assert_eq!(tz(DateStyle::Postgres), "Mon Oct 05 18:04:56.789 2026 IST");
899 let bc = (date2j(-43, 3, 15) - POSTGRES_EPOCH_JDATE) as i64 * USECS_PER_DAY;
900 assert_eq!(
901 text(|out| timestamp_out(bc, DateFormat::ISO_MDY, out).unwrap()),
902 "0044-03-15 00:00:00 BC"
903 );
904 assert_eq!(text(|out| timetz_out(USECS_PER_DAY, -20130, out)), "24:00:00+05:35:30");
905 assert_eq!(text(|out| date_out(DATE_INFINITY, DateFormat::ISO_MDY, out)), "infinity");
906 let error = timestamp_out(i64::MIN + 1, DateFormat::ISO_MDY, &mut Vec::new()).unwrap_err();
907 assert_eq!(error.message, "timestamp out of range");
908 }
909
910 #[test]
911 fn the_receive_functions_check_the_range() {
912 let date = |d: i32| date_recv(&mut Recv::new(&d.to_be_bytes()));
913 assert_eq!(date(i32::MIN), Ok(i32::MIN));
914 assert_eq!(date(-POSTGRES_EPOCH_JDATE), Ok(-POSTGRES_EPOCH_JDATE));
915 assert_eq!(date(-POSTGRES_EPOCH_JDATE - 1).unwrap_err().message, "date out of range");
916 let time = |t: i64, typmod| time_recv(&mut Recv::new(&t.to_be_bytes()), typmod);
917 assert_eq!(time(USECS_PER_DAY, -1), Ok(USECS_PER_DAY));
918 assert_eq!(time(USECS_PER_DAY + 1, -1).unwrap_err().message, "time out of range");
919 assert_eq!(time(1_500_000, 0), Ok(2_000_000));
920 let ts = |t: i64, typmod| timestamp_recv(&mut Recv::new(&t.to_be_bytes()), typmod);
921 assert_eq!(ts(-1_500_000, 0), Ok(-2_000_000));
922 assert_eq!(ts(END_TIMESTAMP, -1).unwrap_err().message, "timestamp out of range");
923 assert_eq!(ts(0, 7).unwrap_err().message, "timestamp(7) precision must be between 0 and 6");
924 let zone = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xe1, 0];
925 assert_eq!(timetz_recv(&mut Recv::new(&zone), -1).unwrap_err().sqlstate.as_str(), "22009");
926 }
927
928 #[test]
929 fn an_interval_typmod_clears_the_small_fields() {
930 let period = Interval { time: 90_061_500_000, day: 3, month: 14 };
931 let mut bytes = Vec::new();
932 interval_send(&period, &mut bytes);
933 let recv = |typmod| interval_recv(&mut Recv::new(&bytes), typmod).unwrap();
934 let typmod = |fields: &[IntervalField], precision| {
935 crate::typmod::interval_typmod(precision, crate::typmod::interval_range(fields))
936 };
937 use IntervalField::*;
938 assert_eq!(recv(-1), period);
939 assert_eq!(
940 recv(typmod(&[Year], INTERVAL_FULL_PRECISION)),
941 Interval { time: 0, day: 0, month: 12 }
942 );
943 assert_eq!(recv(typmod(&[Day, Hour], INTERVAL_FULL_PRECISION)).time, 90_000_000_000);
944 assert_eq!(recv(typmod(&[Day, Hour, Minute, Second], 0)).time, 90_062_000_000);
945 let error = interval_recv(&mut Recv::new(&bytes), typmod(&[Second], 7)).unwrap_err();
946 assert_eq!(error.message, "interval(7) precision must be between 0 and 6");
947 let error = interval_recv(&mut Recv::new(&bytes), typmod(&[Year, Day], 0)).unwrap_err();
948 assert_eq!(error.sqlstate.as_str(), "XX000");
949 }
950}