1use crate::error::{Result, SqlError};
8use crate::types::Value;
9use jiff::civil::{Date as JDate, DateTime as JDateTime, Time as JTime};
10use jiff::tz::TimeZone;
11use jiff::{Span, Timestamp as JTimestamp, ToSpan, Unit, Zoned};
12
13pub const MICROS_PER_SEC: i64 = 1_000_000;
15pub const MICROS_PER_MIN: i64 = 60 * MICROS_PER_SEC;
17pub const MICROS_PER_HOUR: i64 = 60 * MICROS_PER_MIN;
19pub const MICROS_PER_DAY: i64 = 24 * MICROS_PER_HOUR;
21
22pub const DATE_INFINITY_DAYS: i32 = i32::MAX;
24pub const DATE_NEG_INFINITY_DAYS: i32 = i32::MIN;
25pub const TS_INFINITY_MICROS: i64 = i64::MAX;
26pub const TS_NEG_INFINITY_MICROS: i64 = i64::MIN;
27
28pub fn is_infinity_date(d: i32) -> bool {
29 d == DATE_INFINITY_DAYS || d == DATE_NEG_INFINITY_DAYS
30}
31
32pub fn is_infinity_ts(t: i64) -> bool {
33 t == TS_INFINITY_MICROS || t == TS_NEG_INFINITY_MICROS
34}
35
36fn epoch_date() -> JDate {
38 JDate::new(1970, 1, 1).expect("1970-01-01 is a valid date")
39}
40
41pub fn days_to_ymd(days: i32) -> (i32, u8, u8) {
43 let epoch = epoch_date();
44 let d = epoch.checked_add((days as i64).days()).unwrap_or(epoch);
45 (d.year() as i32, d.month() as u8, d.day() as u8)
46}
47
48pub fn ymd_to_days(y: i32, m: u8, d: u8) -> Option<i32> {
50 let date = JDate::new(y as i16, m as i8, d as i8).ok()?;
51 let span = date.since((Unit::Day, epoch_date())).ok()?;
52 let days = span.get_days() as i64;
53 if (i32::MIN as i64..=i32::MAX as i64).contains(&days) {
54 Some(days as i32)
55 } else {
56 None
57 }
58}
59
60pub fn micros_to_hmsn(micros: i64) -> (u8, u8, u8, u32) {
62 let hour = (micros / MICROS_PER_HOUR) as u8;
63 let rem = micros % MICROS_PER_HOUR;
64 let min = (rem / MICROS_PER_MIN) as u8;
65 let rem = rem % MICROS_PER_MIN;
66 let sec = (rem / MICROS_PER_SEC) as u8;
67 let subsec = (rem % MICROS_PER_SEC) as u32;
68 (hour, min, sec, subsec)
69}
70
71pub fn hmsn_to_micros(h: u8, m: u8, s: u8, us: u32) -> Option<i64> {
73 let total = (h as i64) * MICROS_PER_HOUR
74 + (m as i64) * MICROS_PER_MIN
75 + (s as i64) * MICROS_PER_SEC
76 + us as i64;
77 if (0..=MICROS_PER_DAY).contains(&total) {
78 Some(total)
79 } else {
80 None
81 }
82}
83
84pub fn ts_split(micros: i64) -> (i32, i64) {
86 let days = micros.div_euclid(MICROS_PER_DAY);
87 let rem = micros.rem_euclid(MICROS_PER_DAY);
88 (days as i32, rem)
89}
90
91pub fn ts_combine(date_days: i32, time_micros: i64) -> i64 {
93 (date_days as i64) * MICROS_PER_DAY + time_micros
94}
95
96pub fn date_to_ts(days: i32) -> i64 {
98 (days as i64).saturating_mul(MICROS_PER_DAY)
99}
100
101pub fn ts_to_date_floor(micros: i64) -> i32 {
103 if micros == TS_INFINITY_MICROS {
104 DATE_INFINITY_DAYS
105 } else if micros == TS_NEG_INFINITY_MICROS {
106 DATE_NEG_INFINITY_DAYS
107 } else {
108 micros.div_euclid(MICROS_PER_DAY) as i32
109 }
110}
111
112pub fn parse_date(s: &str) -> Result<i32> {
114 let trimmed = s.trim();
115 let lower = trimmed.to_ascii_lowercase();
116 if lower == "infinity" || lower == "+infinity" {
117 return Ok(DATE_INFINITY_DAYS);
118 }
119 if lower == "-infinity" {
120 return Ok(DATE_NEG_INFINITY_DAYS);
121 }
122
123 let (body, is_bc) = if let Some(stripped) = trimmed.strip_suffix(" BC") {
125 (stripped.trim(), true)
126 } else if let Some(stripped) = trimmed.strip_suffix(" bc") {
127 (stripped.trim(), true)
128 } else {
129 (trimmed, false)
130 };
131
132 let d = JDate::strptime("%Y-%m-%d", body)
133 .map_err(|e| SqlError::InvalidDateLiteral(format!("{body}: {e}")))?;
134 if d.year() == 0 {
135 return Err(SqlError::InvalidDateLiteral(
136 "year 0 is not supported; use '0001-01-01 BC' for 1 BC".into(),
137 ));
138 }
139 let year_adjusted = if is_bc {
140 -(d.year() as i32 - 1)
141 } else {
142 d.year() as i32
143 };
144 let canonical = JDate::new(year_adjusted as i16, d.month(), d.day())
145 .map_err(|e| SqlError::InvalidDateLiteral(format!("{body}: {e}")))?;
146 let span = canonical
147 .since((Unit::Day, epoch_date()))
148 .map_err(|e| SqlError::InvalidDateLiteral(format!("{body}: {e}")))?;
149 let days = span.get_days() as i64;
150 if (i32::MIN as i64..=i32::MAX as i64).contains(&days) {
151 Ok(days as i32)
152 } else {
153 Err(SqlError::InvalidDateLiteral(format!(
154 "{body}: date out of i32 range"
155 )))
156 }
157}
158
159pub fn parse_time(s: &str) -> Result<i64> {
161 let trimmed = s.trim();
162 if trimmed == "24:00:00" || trimmed == "24:00:00.000000" {
164 return Ok(MICROS_PER_DAY);
165 }
166 let t = JTime::strptime("%H:%M:%S%.f", trimmed)
167 .or_else(|_| JTime::strptime("%H:%M:%S", trimmed))
168 .or_else(|_| JTime::strptime("%H:%M", trimmed))
169 .map_err(|e| SqlError::InvalidTimeLiteral(format!("{trimmed}: {e}")))?;
170 let subsec_micros = (t.subsec_nanosecond() / 1000) as u32;
171 hmsn_to_micros(
172 t.hour() as u8,
173 t.minute() as u8,
174 t.second() as u8,
175 subsec_micros,
176 )
177 .ok_or_else(|| SqlError::InvalidTimeLiteral(format!("{trimmed}: out of range")))
178}
179
180pub fn parse_timestamp(s: &str) -> Result<i64> {
184 let trimmed = s.trim();
185 let lower = trimmed.to_ascii_lowercase();
186 if lower == "infinity" || lower == "+infinity" {
187 return Ok(TS_INFINITY_MICROS);
188 }
189 if lower == "-infinity" {
190 return Ok(TS_NEG_INFINITY_MICROS);
191 }
192
193 let (body, is_bc) = if let Some(stripped) = trimmed.strip_suffix(" BC") {
195 (stripped.trim_end(), true)
196 } else if let Some(stripped) = trimmed.strip_suffix(" bc") {
197 (stripped.trim_end(), true)
198 } else {
199 (trimmed, false)
200 };
201
202 if !is_bc {
204 if let Ok(z) = body.parse::<Zoned>() {
205 reject_year_zero_ts(z.timestamp().as_microsecond())?;
206 return Ok(z.timestamp().as_microsecond());
207 }
208 if let Ok(ts) = body.parse::<JTimestamp>() {
210 reject_year_zero_ts(ts.as_microsecond())?;
211 return Ok(ts.as_microsecond());
212 }
213 }
214
215 let parsers = [
218 "%Y-%m-%d %H:%M:%S%.f",
219 "%Y-%m-%dT%H:%M:%S%.f",
220 "%Y-%m-%d %H:%M:%S",
221 "%Y-%m-%dT%H:%M:%S",
222 "%Y-%m-%d %H:%M",
223 "%Y-%m-%dT%H:%M",
224 "%Y-%m-%d",
225 ];
226 for fmt in &parsers {
227 if let Ok(dt) = JDateTime::strptime(fmt, body) {
228 let adjusted = apply_bc_and_check_year_zero(dt, is_bc, body)?;
229 return adjusted
230 .to_zoned(TimeZone::UTC)
231 .map(|z| z.timestamp().as_microsecond())
232 .map_err(|e| SqlError::InvalidTimestampLiteral(format!("{body}: {e}")));
233 }
234 }
235 if !is_bc {
237 if let Some(space_idx) = body.rfind(' ') {
238 let (ts_part, zone_part) = body.split_at(space_idx);
239 let zone_name = zone_part.trim();
240 if let Ok(tz) = TimeZone::get(zone_name) {
241 for fmt in &parsers {
242 if let Ok(dt) = JDateTime::strptime(fmt, ts_part.trim()) {
243 if dt.year() == 0 {
244 return Err(SqlError::InvalidTimestampLiteral(
245 "year 0 is not supported; use 'YYYY-MM-DD HH:MM:SS BC' for 1 BC"
246 .into(),
247 ));
248 }
249 return dt
250 .to_zoned(tz.clone())
251 .map(|z| z.timestamp().as_microsecond())
252 .map_err(|e| {
253 SqlError::InvalidTimestampLiteral(format!("{body}: {e}"))
254 });
255 }
256 }
257 }
258 }
259 }
260 Err(SqlError::InvalidTimestampLiteral(format!(
261 "{trimmed}: unrecognized timestamp format"
262 )))
263}
264
265fn reject_year_zero_ts(micros: i64) -> Result<()> {
266 let date_days = ts_to_date_floor(micros);
267 let (y, _, _) = days_to_ymd(date_days);
268 if y == 0 {
269 return Err(SqlError::InvalidTimestampLiteral(
270 "year 0 is not supported; use 'YYYY-MM-DD HH:MM:SS BC' for 1 BC".into(),
271 ));
272 }
273 Ok(())
274}
275
276fn apply_bc_and_check_year_zero(dt: JDateTime, is_bc: bool, body: &str) -> Result<JDateTime> {
277 if dt.year() == 0 {
278 return Err(SqlError::InvalidTimestampLiteral(
279 "year 0 is not supported; use 'YYYY-MM-DD HH:MM:SS BC' for 1 BC".into(),
280 ));
281 }
282 if !is_bc {
283 return Ok(dt);
284 }
285 let astro_year = -(dt.year() as i32 - 1);
287 let date = JDate::new(astro_year as i16, dt.month(), dt.day())
288 .map_err(|e| SqlError::InvalidTimestampLiteral(format!("{body}: {e}")))?;
289 let time = JTime::new(dt.hour(), dt.minute(), dt.second(), dt.subsec_nanosecond())
290 .map_err(|e| SqlError::InvalidTimestampLiteral(format!("{body}: {e}")))?;
291 Ok(JDateTime::from_parts(date, time))
292}
293
294pub fn parse_interval(s: &str) -> Result<(i32, i32, i64)> {
297 let trimmed = s.trim();
298 if trimmed.is_empty() {
299 return Err(SqlError::InvalidIntervalLiteral("empty interval".into()));
300 }
301
302 if let Some(rest) = trimmed
304 .strip_prefix('P')
305 .or_else(|| trimmed.strip_prefix('-').and_then(|r| r.strip_prefix('P')))
306 {
307 let negate = trimmed.starts_with('-');
308 return parse_iso8601_duration(rest, negate);
309 }
310
311 parse_pg_interval(trimmed)
313}
314
315fn parse_iso8601_duration(s: &str, global_negate: bool) -> Result<(i32, i32, i64)> {
316 let mut months: i64 = 0;
318 let mut days: i64 = 0;
319 let mut micros: i64 = 0;
320 let mut in_time = false;
321 let mut num_buf = String::new();
322 let sign = if global_negate { -1i64 } else { 1 };
323
324 for ch in s.chars() {
325 if ch == 'T' {
326 in_time = true;
327 continue;
328 }
329 if ch.is_ascii_digit() || ch == '.' || ch == '-' {
330 num_buf.push(ch);
331 continue;
332 }
333 if num_buf.is_empty() {
334 return Err(SqlError::InvalidIntervalLiteral(format!(
335 "expected number before '{ch}'"
336 )));
337 }
338 let v: f64 = num_buf
339 .parse()
340 .map_err(|_| SqlError::InvalidIntervalLiteral(format!("invalid number: {num_buf}")))?;
341 num_buf.clear();
342 let v_units = sign * v as i64;
343 let v_frac_micros = ((v.fract() * 1_000_000.0) as i64) * sign;
344 match ch {
345 'Y' if !in_time => months = months.saturating_add(v_units * 12),
346 'M' if !in_time => months = months.saturating_add(v_units),
347 'W' if !in_time => days = days.saturating_add(v_units * 7),
348 'D' if !in_time => days = days.saturating_add(v_units),
349 'H' if in_time => micros = micros.saturating_add(v_units * MICROS_PER_HOUR),
350 'M' if in_time => micros = micros.saturating_add(v_units * MICROS_PER_MIN),
351 'S' if in_time => {
352 micros = micros.saturating_add(v_units * MICROS_PER_SEC + v_frac_micros)
353 }
354 _ => {
355 return Err(SqlError::InvalidIntervalLiteral(format!(
356 "unknown unit '{ch}' (in_time={in_time})"
357 )))
358 }
359 }
360 }
361 if !num_buf.is_empty() {
362 return Err(SqlError::InvalidIntervalLiteral(format!(
363 "trailing number without unit: {num_buf}"
364 )));
365 }
366 Ok((clamp_i32(months)?, clamp_i32(days)?, micros))
367}
368
369fn parse_pg_interval(s: &str) -> Result<(i32, i32, i64)> {
370 let mut s = s.trim().to_ascii_lowercase();
371 if let Some(rest) = s.strip_prefix('@') {
372 s = rest.trim().to_string();
373 }
374 let ago = s.ends_with(" ago");
375 if ago {
376 s.truncate(s.len() - 4);
377 s = s.trim().to_string();
378 }
379 let sign: i64 = if ago { -1 } else { 1 };
380
381 let mut months: i64 = 0;
382 let mut days: i64 = 0;
383 let mut micros: i64 = 0;
384
385 let tokens: Vec<&str> = s.split_whitespace().collect();
386 let mut i = 0;
387 while i < tokens.len() {
388 let tok = tokens[i];
389 if tok.contains(':') {
391 let (h, m, sc, us) = parse_hms_token(tok)?;
392 let tok_sign = if tok.starts_with('-') { -1 } else { 1 };
393 let tok_micros =
394 (h * MICROS_PER_HOUR + m * MICROS_PER_MIN + sc * MICROS_PER_SEC + us as i64)
395 * tok_sign
396 * sign;
397 micros = micros.saturating_add(tok_micros);
398 i += 1;
399 continue;
400 }
401
402 let num: f64 = tok.parse().map_err(|_| {
404 SqlError::InvalidIntervalLiteral(format!("expected number, got '{tok}'"))
405 })?;
406 if i + 1 >= tokens.len() {
407 return Err(SqlError::InvalidIntervalLiteral(format!(
408 "missing unit after '{tok}'"
409 )));
410 }
411 let unit = tokens[i + 1].trim_end_matches(',');
412 let v_units = sign * num as i64;
413 let v_frac_micros = ((num.fract() * 1_000_000.0) as i64) * sign;
414 match unit {
415 "year" | "years" | "yr" | "yrs" | "y" => months = months.saturating_add(v_units * 12),
416 "month" | "months" | "mon" | "mons" => months = months.saturating_add(v_units),
417 "week" | "weeks" | "w" => days = days.saturating_add(v_units * 7),
418 "day" | "days" | "d" => days = days.saturating_add(v_units),
419 "hour" | "hours" | "hr" | "hrs" | "h" => {
420 micros = micros.saturating_add(v_units * MICROS_PER_HOUR)
421 }
422 "minute" | "minutes" | "min" | "mins" | "m" => {
423 micros = micros.saturating_add(v_units * MICROS_PER_MIN)
424 }
425 "second" | "seconds" | "sec" | "secs" | "s" => {
426 micros = micros.saturating_add(v_units * MICROS_PER_SEC + v_frac_micros)
427 }
428 "millisecond" | "milliseconds" | "ms" => micros = micros.saturating_add(v_units * 1000),
429 "microsecond" | "microseconds" | "us" => micros = micros.saturating_add(v_units),
430 other => {
431 return Err(SqlError::InvalidIntervalLiteral(format!(
432 "unknown unit: {other}"
433 )))
434 }
435 }
436 i += 2;
437 }
438 Ok((clamp_i32(months)?, clamp_i32(days)?, micros))
439}
440
441fn parse_hms_token(tok: &str) -> Result<(i64, i64, i64, u32)> {
442 let tok = tok.trim_start_matches('-').trim_start_matches('+');
443 let mut parts = tok.split(':');
444 let h: i64 = parts
445 .next()
446 .ok_or_else(|| SqlError::InvalidIntervalLiteral(format!("bad hms: {tok}")))?
447 .parse()
448 .map_err(|_| SqlError::InvalidIntervalLiteral(format!("bad hour: {tok}")))?;
449 let m: i64 = parts
450 .next()
451 .ok_or_else(|| SqlError::InvalidIntervalLiteral(format!("bad hms: {tok}")))?
452 .parse()
453 .map_err(|_| SqlError::InvalidIntervalLiteral(format!("bad minute: {tok}")))?;
454 let (sc, us) = if let Some(sec_part) = parts.next() {
455 if let Some((s_whole, s_frac)) = sec_part.split_once('.') {
456 let s: i64 = s_whole
457 .parse()
458 .map_err(|_| SqlError::InvalidIntervalLiteral(format!("bad second: {tok}")))?;
459 let mut frac = s_frac.to_string();
461 while frac.len() < 6 {
462 frac.push('0');
463 }
464 frac.truncate(6);
465 let us: u32 = frac
466 .parse()
467 .map_err(|_| SqlError::InvalidIntervalLiteral(format!("bad subsec: {tok}")))?;
468 (s, us)
469 } else {
470 let s: i64 = sec_part
471 .parse()
472 .map_err(|_| SqlError::InvalidIntervalLiteral(format!("bad second: {tok}")))?;
473 (s, 0u32)
474 }
475 } else {
476 (0, 0u32)
477 };
478 Ok((h, m, sc, us))
479}
480
481fn clamp_i32(n: i64) -> Result<i32> {
482 if (i32::MIN as i64..=i32::MAX as i64).contains(&n) {
483 Ok(n as i32)
484 } else {
485 Err(SqlError::InvalidIntervalLiteral(format!(
486 "interval component overflow: {n}"
487 )))
488 }
489}
490
491pub fn format_date(days: i32) -> String {
492 if days == DATE_INFINITY_DAYS {
493 return "infinity".to_string();
494 }
495 if days == DATE_NEG_INFINITY_DAYS {
496 return "-infinity".to_string();
497 }
498 let (y, m, d) = days_to_ymd(days);
499 if y >= 1 {
500 format!("{y:04}-{m:02}-{d:02}")
501 } else {
502 format!("{:04}-{m:02}-{d:02} BC", 1 - y)
504 }
505}
506
507pub fn format_time(micros: i64) -> String {
508 if micros == MICROS_PER_DAY {
509 return "24:00:00".to_string();
510 }
511 let (h, m, s, us) = micros_to_hmsn(micros);
512 if us == 0 {
513 format!("{h:02}:{m:02}:{s:02}")
514 } else {
515 format!("{h:02}:{m:02}:{s:02}.{us:06}")
516 }
517}
518
519pub fn format_timestamp(micros: i64) -> String {
520 if micros == TS_INFINITY_MICROS {
521 return "infinity".to_string();
522 }
523 if micros == TS_NEG_INFINITY_MICROS {
524 return "-infinity".to_string();
525 }
526 let (date_days, time_micros) = ts_split(micros);
527 let date_part = format_date(date_days);
528 let time_part = format_time(time_micros);
529 format!("{date_part} {time_part}")
530}
531
532pub fn format_timestamp_in_zone(micros: i64, zone: &str) -> Result<String> {
533 if micros == TS_INFINITY_MICROS {
534 return Ok("infinity".to_string());
535 }
536 if micros == TS_NEG_INFINITY_MICROS {
537 return Ok("-infinity".to_string());
538 }
539 let tz = resolve_timezone(zone)?;
540 let ts = JTimestamp::from_microsecond(micros)
541 .map_err(|e| SqlError::InvalidTimestampLiteral(format!("{micros}: {e}")))?;
542 let z = ts.to_zoned(tz);
543 let subsec = z.subsec_nanosecond() / 1000;
544 let fmt = if subsec == 0 {
545 "%Y-%m-%d %H:%M:%S%:z"
546 } else {
547 "%Y-%m-%d %H:%M:%S%.6f%:z"
548 };
549 z.strftime(fmt).to_string().pipe(Ok)
550}
551
552pub fn resolve_timezone(zone: &str) -> Result<TimeZone> {
555 let trimmed = zone.trim();
556 if let Ok(tz) = TimeZone::get(trimmed) {
557 return Ok(tz);
558 }
559 if let Some(offset) = parse_iso_fixed_offset(trimmed) {
560 return jiff::tz::Offset::from_seconds(offset)
561 .map(TimeZone::fixed)
562 .map_err(|e| SqlError::InvalidTimezone(format!("{zone}: {e}")));
563 }
564 let lower = trimmed.to_ascii_lowercase();
565 if lower.starts_with("utc+")
566 || lower.starts_with("utc-")
567 || lower.starts_with("gmt+")
568 || lower.starts_with("gmt-")
569 {
570 return Err(SqlError::InvalidTimezone(format!(
571 "{zone}: ambiguous POSIX form; use ISO-8601 offset like '+05:00' or a named zone"
572 )));
573 }
574 Err(SqlError::InvalidTimezone(format!(
575 "{zone}: not a recognized IANA name or ISO-8601 offset"
576 )))
577}
578
579fn parse_iso_fixed_offset(s: &str) -> Option<i32> {
581 if s.eq_ignore_ascii_case("z") || s.eq_ignore_ascii_case("utc") {
582 return Some(0);
583 }
584 let bytes = s.as_bytes();
585 if bytes.is_empty() {
586 return None;
587 }
588 let sign: i32 = match bytes[0] {
589 b'+' => 1,
590 b'-' => -1,
591 _ => return None,
592 };
593 let rest = &s[1..];
594 let (hh, mm) = if let Some((h, m)) = rest.split_once(':') {
595 (h, m)
596 } else if rest.len() == 4 {
597 (&rest[..2], &rest[2..])
598 } else if rest.len() == 2 {
599 (rest, "00")
600 } else {
601 return None;
602 };
603 let h: i32 = hh.parse().ok()?;
604 let m: i32 = mm.parse().ok()?;
605 if !(0..=23).contains(&h) || !(0..=59).contains(&m) {
606 return None;
607 }
608 Some(sign * (h * 3600 + m * 60))
609}
610
611pub fn format_interval(months: i32, days: i32, micros: i64) -> String {
612 if months == 0 && days == 0 && micros == 0 {
613 return "00:00:00".to_string();
614 }
615 let mut parts = Vec::with_capacity(4);
616 if months != 0 {
617 let years = months / 12;
618 let mon = months % 12;
619 if years != 0 {
620 parts.push(format!(
621 "{} year{}",
622 years,
623 if years.abs() == 1 { "" } else { "s" }
624 ));
625 }
626 if mon != 0 {
627 parts.push(format!(
628 "{} mon{}",
629 mon,
630 if mon.abs() == 1 { "" } else { "s" }
631 ));
632 }
633 }
634 if days != 0 {
635 parts.push(format!(
636 "{} day{}",
637 days,
638 if days.abs() == 1 { "" } else { "s" }
639 ));
640 }
641 if micros != 0 {
642 let sign = if micros < 0 { "-" } else { "" };
643 let abs_us = micros.unsigned_abs() as i64;
644 let (h, m, s, us) = micros_to_hmsn(abs_us);
645 if us == 0 {
646 parts.push(format!("{sign}{h:02}:{m:02}:{s:02}"));
647 } else {
648 parts.push(format!("{sign}{h:02}:{m:02}:{s:02}.{us:06}"));
649 }
650 }
651 parts.join(" ")
652}
653
654trait Pipe: Sized {
656 fn pipe<T>(self, f: impl FnOnce(Self) -> T) -> T {
657 f(self)
658 }
659}
660impl<T> Pipe for T {}
661
662pub fn now_micros() -> i64 {
663 JTimestamp::now().as_microsecond()
664}
665
666thread_local! {
667 static TXN_CLOCK: std::cell::Cell<Option<i64>> = const { std::cell::Cell::new(None) };
669}
670
671pub fn with_txn_clock<R>(ts: Option<i64>, f: impl FnOnce() -> R) -> R {
674 TXN_CLOCK.with(|slot| {
675 let prev = slot.replace(ts);
676 let r = f();
677 slot.set(prev);
678 r
679 })
680}
681
682pub fn txn_or_clock_micros() -> i64 {
685 TXN_CLOCK.with(|slot| slot.get()).unwrap_or_else(now_micros)
686}
687
688pub fn today_days() -> i32 {
689 ts_to_date_floor(now_micros())
690}
691
692pub fn current_time_micros() -> i64 {
693 ts_split(now_micros()).1
694}
695
696pub fn add_interval_to_timestamp(ts: i64, months: i32, days: i32, micros: i64) -> Result<i64> {
697 if ts == TS_INFINITY_MICROS || ts == TS_NEG_INFINITY_MICROS {
698 return Ok(ts);
699 }
700 let jts =
701 JTimestamp::from_microsecond(ts).map_err(|e| SqlError::InvalidValue(format!("ts: {e}")))?;
702 let span = Span::new()
704 .try_months(months as i64)
705 .map_err(|e| SqlError::InvalidValue(format!("months overflow: {e}")))?
706 .try_days(days as i64)
707 .map_err(|e| SqlError::InvalidValue(format!("days overflow: {e}")))?
708 .try_microseconds(micros)
709 .map_err(|e| SqlError::InvalidValue(format!("micros overflow: {e}")))?;
710 let result = jts
711 .to_zoned(TimeZone::UTC)
712 .checked_add(span)
713 .map_err(|_| SqlError::IntegerOverflow)?;
714 Ok(result.timestamp().as_microsecond())
715}
716
717pub fn add_interval_to_date(days: i32, months: i32, i_days: i32, micros: i64) -> Result<i64> {
719 if is_infinity_date(days) {
720 return Ok(if days == DATE_INFINITY_DAYS {
721 TS_INFINITY_MICROS
722 } else {
723 TS_NEG_INFINITY_MICROS
724 });
725 }
726 let ts = date_to_ts(days);
727 add_interval_to_timestamp(ts, months, i_days, micros)
728}
729
730pub fn add_days_to_date(days: i32, n: i64) -> Result<i32> {
731 if is_infinity_date(days) {
732 return Ok(days);
733 }
734 let new_days = (days as i64)
735 .checked_add(n)
736 .ok_or(SqlError::IntegerOverflow)?;
737 if new_days >= i32::MIN as i64 && new_days <= i32::MAX as i64 {
738 Ok(new_days as i32)
739 } else {
740 Err(SqlError::IntegerOverflow)
741 }
742}
743
744pub fn add_interval_to_time(t: i64, months: i32, days: i32, micros: i64) -> Result<i64> {
745 if months != 0 || days != 0 {
746 return Err(SqlError::InvalidValue(
747 "cannot add month/day interval to TIME".into(),
748 ));
749 }
750 let combined = t.checked_add(micros).unwrap_or(t);
752 Ok(combined.rem_euclid(MICROS_PER_DAY))
753}
754
755pub fn subtract_timestamps(a: i64, b: i64) -> (i32, i64) {
757 let diff = a.saturating_sub(b);
758 let days = (diff / MICROS_PER_DAY) as i32;
759 let micros = diff % MICROS_PER_DAY;
760 (days, micros)
761}
762
763pub fn age(ts_a: i64, ts_b: i64) -> Result<(i32, i32, i64)> {
765 let a = JTimestamp::from_microsecond(ts_a)
766 .map_err(|e| SqlError::InvalidValue(format!("ts_a: {e}")))?
767 .to_zoned(TimeZone::UTC);
768 let b = JTimestamp::from_microsecond(ts_b)
769 .map_err(|e| SqlError::InvalidValue(format!("ts_b: {e}")))?
770 .to_zoned(TimeZone::UTC);
771 let span = a
772 .since((Unit::Year, &b))
773 .map_err(|e| SqlError::InvalidValue(format!("age: {e}")))?;
774 span_to_triple(&span)
775}
776
777fn span_to_triple(span: &Span) -> Result<(i32, i32, i64)> {
778 let months = i64::from(span.get_years()) * 12 + i64::from(span.get_months());
779 let days = i64::from(span.get_weeks()) * 7 + i64::from(span.get_days());
780 let micros = i64::from(span.get_hours()) * MICROS_PER_HOUR
781 + span.get_minutes() * MICROS_PER_MIN
782 + span.get_seconds() * MICROS_PER_SEC
783 + span.get_milliseconds() * 1000
784 + span.get_microseconds()
785 + span.get_nanoseconds() / 1000;
786 Ok((clamp_i32(months)?, clamp_i32(days)?, micros))
787}
788
789pub fn justify_days(months: i32, days: i32, micros: i64) -> (i32, i32, i64) {
790 let extra_months = days / 30;
792 let rem_days = days % 30;
793 let new_months = months.saturating_add(extra_months);
794 (new_months, rem_days, micros)
795}
796
797pub fn justify_hours(months: i32, days: i32, micros: i64) -> (i32, i32, i64) {
798 let extra_days = (micros / MICROS_PER_DAY) as i32;
800 let rem_micros = micros % MICROS_PER_DAY;
801 let new_days = days.saturating_add(extra_days);
802 (months, new_days, rem_micros)
803}
804
805pub fn justify_interval(months: i32, days: i32, micros: i64) -> (i32, i32, i64) {
806 let (m1, d1, us1) = justify_hours(months, days, micros);
807 let (m2, d2, us2) = justify_days(m1, d1, us1);
808 (m2, d2, us2)
809}
810
811pub fn interval_to_total_micros(months: i32, days: i32, micros: i64) -> i128 {
813 (months as i128) * 30 * (MICROS_PER_DAY as i128)
814 + (days as i128) * (MICROS_PER_DAY as i128)
815 + micros as i128
816}
817
818pub fn extract(field: &str, v: &Value) -> Result<Value> {
819 let f = field.trim();
820 match v {
821 Value::Null => Ok(Value::Null),
822 Value::Date(d) => extract_from_date(f, *d),
823 Value::Time(t) => extract_from_time(f, *t),
824 Value::Timestamp(t) => extract_from_timestamp(f, *t),
825 Value::Interval {
826 months,
827 days,
828 micros,
829 } => extract_from_interval(f, *months, *days, *micros),
830 _ => Err(SqlError::TypeMismatch {
831 expected: "temporal type".into(),
832 got: v.data_type().to_string(),
833 }),
834 }
835}
836
837fn extract_from_date(field: &str, days: i32) -> Result<Value> {
838 if field.eq_ignore_ascii_case("epoch") {
839 return Ok(Value::Integer((days as i64) * 86400));
840 }
841 let (y, m, d) = days_to_ymd(days);
842 if field.eq_ignore_ascii_case("year") {
843 return Ok(Value::Integer(y as i64));
844 }
845 if field.eq_ignore_ascii_case("month") {
846 return Ok(Value::Integer(m as i64));
847 }
848 if field.eq_ignore_ascii_case("day") {
849 return Ok(Value::Integer(d as i64));
850 }
851 if field.eq_ignore_ascii_case("hour")
852 || field.eq_ignore_ascii_case("minute")
853 || field.eq_ignore_ascii_case("second")
854 || field.eq_ignore_ascii_case("microseconds")
855 || field.eq_ignore_ascii_case("milliseconds")
856 {
857 return Ok(Value::Integer(0));
858 }
859 let f = field.to_ascii_lowercase();
861 match f.as_str() {
862 "dow" => {
863 let jd = JDate::new(y as i16, m as i8, d as i8)
864 .map_err(|e| SqlError::InvalidValue(format!("{e}")))?;
865 let w = jd.weekday().to_monday_one_offset() as i64;
867 let dow = if w == 7 { 0 } else { w }; Ok(Value::Integer(dow))
869 }
870 "isodow" => {
871 let jd = JDate::new(y as i16, m as i8, d as i8)
872 .map_err(|e| SqlError::InvalidValue(format!("{e}")))?;
873 Ok(Value::Integer(jd.weekday().to_monday_one_offset() as i64))
874 }
875 "doy" => {
876 let jd = JDate::new(y as i16, m as i8, d as i8)
877 .map_err(|e| SqlError::InvalidValue(format!("{e}")))?;
878 Ok(Value::Integer(jd.day_of_year() as i64))
879 }
880 "quarter" => Ok(Value::Integer(((m - 1) / 3 + 1) as i64)),
881 "decade" => Ok(Value::Integer((y / 10) as i64)),
882 "century" => Ok(Value::Integer(if y > 0 {
883 ((y - 1) / 100 + 1) as i64
884 } else {
885 (y / 100 - 1) as i64
886 })),
887 "millennium" => Ok(Value::Integer(if y > 0 {
888 ((y - 1) / 1000 + 1) as i64
889 } else {
890 (y / 1000 - 1) as i64
891 })),
892 "julian" => Ok(Value::Integer(days as i64 + 2_440_588)),
893 "week" | "isoyear" => {
894 let jd = JDate::new(y as i16, m as i8, d as i8)
895 .map_err(|e| SqlError::InvalidValue(format!("{e}")))?;
896 let iso = jd.iso_week_date();
897 if field == "week" {
898 Ok(Value::Integer(iso.week() as i64))
899 } else {
900 Ok(Value::Integer(iso.year() as i64))
901 }
902 }
903 _ => Err(SqlError::InvalidExtractField(format!("{field} from DATE"))),
904 }
905}
906
907fn extract_from_time(field: &str, micros: i64) -> Result<Value> {
908 let (h, m, s, us) = micros_to_hmsn(micros);
909 match field {
910 "hour" => Ok(Value::Integer(h as i64)),
911 "minute" => Ok(Value::Integer(m as i64)),
912 "second" => {
913 if us == 0 {
914 Ok(Value::Integer(s as i64))
915 } else {
916 Ok(Value::Real(s as f64 + (us as f64) / 1_000_000.0))
917 }
918 }
919 "microseconds" => Ok(Value::Integer((s as i64) * 1_000_000 + us as i64)),
920 "milliseconds" => Ok(Value::Real(s as f64 * 1000.0 + (us as f64) / 1000.0)),
921 "epoch" => Ok(Value::Integer(micros / MICROS_PER_SEC)),
922 _ => Err(SqlError::InvalidExtractField(format!("{field} from TIME"))),
923 }
924}
925
926fn extract_from_timestamp(field: &str, ts: i64) -> Result<Value> {
927 if field.eq_ignore_ascii_case("hour") {
928 return Ok(Value::Integer(
929 ts.rem_euclid(MICROS_PER_DAY) / MICROS_PER_HOUR,
930 ));
931 }
932 if field.eq_ignore_ascii_case("minute") {
933 return Ok(Value::Integer(
934 ts.rem_euclid(MICROS_PER_HOUR) / MICROS_PER_MIN,
935 ));
936 }
937 if field.eq_ignore_ascii_case("epoch") {
938 return Ok(Value::Integer(ts / MICROS_PER_SEC));
939 }
940 let (date_days, time_micros) = ts_split(ts);
941 let date_fields = [
943 "year",
944 "month",
945 "day",
946 "dow",
947 "isodow",
948 "doy",
949 "quarter",
950 "decade",
951 "century",
952 "millennium",
953 "julian",
954 "week",
955 "isoyear",
956 ];
957 if date_fields.iter().any(|&f| field.eq_ignore_ascii_case(f)) {
958 return extract_from_date(field, date_days);
959 }
960 let time_fields = ["second", "microseconds", "milliseconds"];
962 if time_fields.iter().any(|&f| field.eq_ignore_ascii_case(f)) {
963 return extract_from_time(field, time_micros);
964 }
965 Err(SqlError::InvalidExtractField(format!(
966 "{field} from TIMESTAMP"
967 )))
968}
969
970fn extract_from_interval(field: &str, months: i32, days: i32, micros: i64) -> Result<Value> {
971 match field {
972 "year" => Ok(Value::Integer((months / 12) as i64)),
973 "month" => Ok(Value::Integer((months % 12) as i64)),
974 "day" => Ok(Value::Integer(days as i64)),
975 "hour" => Ok(Value::Integer(micros / MICROS_PER_HOUR)),
976 "minute" => Ok(Value::Integer((micros % MICROS_PER_HOUR) / MICROS_PER_MIN)),
977 "second" => {
978 let rem = micros % MICROS_PER_MIN;
979 let sec_part = rem / MICROS_PER_SEC;
980 let us_part = rem % MICROS_PER_SEC;
981 if us_part == 0 {
982 Ok(Value::Integer(sec_part))
983 } else {
984 Ok(Value::Real(sec_part as f64 + us_part as f64 / 1_000_000.0))
985 }
986 }
987 "microseconds" => Ok(Value::Integer(micros % MICROS_PER_MIN)),
988 "epoch" => {
989 let total = interval_to_total_micros(months, days, micros);
990 Ok(Value::Real(total as f64 / 1_000_000.0))
991 }
992 _ => Err(SqlError::InvalidExtractField(format!(
993 "{field} from INTERVAL"
994 ))),
995 }
996}
997
998pub fn date_trunc(unit: &str, v: &Value) -> Result<Value> {
999 let u = unit.trim().to_ascii_lowercase();
1000 match v {
1001 Value::Null => Ok(Value::Null),
1002 Value::Date(d) => date_trunc_date(&u, *d).map(Value::Date),
1003 Value::Timestamp(t) => date_trunc_timestamp(&u, *t).map(Value::Timestamp),
1004 Value::Time(t) => date_trunc_time(&u, *t).map(Value::Time),
1005 Value::Interval {
1006 months,
1007 days,
1008 micros,
1009 } => date_trunc_interval(&u, *months, *days, *micros).map(|(m, d, us)| Value::Interval {
1010 months: m,
1011 days: d,
1012 micros: us,
1013 }),
1014 _ => Err(SqlError::TypeMismatch {
1015 expected: "temporal type".into(),
1016 got: v.data_type().to_string(),
1017 }),
1018 }
1019}
1020
1021fn date_trunc_date(unit: &str, days: i32) -> Result<i32> {
1022 if is_infinity_date(days) {
1023 return Ok(days);
1024 }
1025 let (y, m, d) = days_to_ymd(days);
1026 match unit {
1027 "microseconds" | "milliseconds" | "second" | "minute" | "hour" | "day" => Ok(days),
1028 "week" => {
1029 let jd = JDate::new(y as i16, m as i8, d as i8)
1031 .map_err(|e| SqlError::InvalidValue(format!("{e}")))?;
1032 let dow = jd.weekday().to_monday_one_offset() as i32; add_days_to_date(days, -(dow - 1) as i64)
1034 }
1035 "month" => {
1036 ymd_to_days(y, m, 1).ok_or_else(|| SqlError::InvalidValue("date_trunc month".into()))
1037 }
1038 "quarter" => {
1039 let qm = ((m - 1) / 3) * 3 + 1;
1040 ymd_to_days(y, qm, 1).ok_or_else(|| SqlError::InvalidValue("date_trunc quarter".into()))
1041 }
1042 "year" => {
1043 ymd_to_days(y, 1, 1).ok_or_else(|| SqlError::InvalidValue("date_trunc year".into()))
1044 }
1045 "decade" => ymd_to_days(y - (y % 10), 1, 1)
1046 .ok_or_else(|| SqlError::InvalidValue("date_trunc decade".into())),
1047 "century" => {
1048 let cy = if y > 0 {
1049 ((y - 1) / 100) * 100 + 1
1050 } else {
1051 (y / 100) * 100 - 99
1052 };
1053 ymd_to_days(cy, 1, 1).ok_or_else(|| SqlError::InvalidValue("date_trunc century".into()))
1054 }
1055 "millennium" => {
1056 let my = if y > 0 {
1057 ((y - 1) / 1000) * 1000 + 1
1058 } else {
1059 (y / 1000) * 1000 - 999
1060 };
1061 ymd_to_days(my, 1, 1)
1062 .ok_or_else(|| SqlError::InvalidValue("date_trunc millennium".into()))
1063 }
1064 _ => Err(SqlError::InvalidDateTruncUnit(unit.into())),
1065 }
1066}
1067
1068fn date_trunc_timestamp(unit: &str, ts: i64) -> Result<i64> {
1069 if is_infinity_ts(ts) {
1070 return Ok(ts);
1071 }
1072 let (date_days, time_micros) = ts_split(ts);
1073 match unit {
1075 "microseconds" => Ok(ts),
1076 "milliseconds" => Ok(ts_combine(date_days, time_micros - time_micros % 1000)),
1077 "second" => Ok(ts_combine(
1078 date_days,
1079 time_micros - time_micros % MICROS_PER_SEC,
1080 )),
1081 "minute" => Ok(ts_combine(
1082 date_days,
1083 time_micros - time_micros % MICROS_PER_MIN,
1084 )),
1085 "hour" => Ok(ts_combine(
1086 date_days,
1087 time_micros - time_micros % MICROS_PER_HOUR,
1088 )),
1089 "day" => Ok(ts_combine(date_days, 0)),
1090 _ => {
1091 let trunc_days = date_trunc_date(unit, date_days)?;
1093 Ok(ts_combine(trunc_days, 0))
1094 }
1095 }
1096}
1097
1098fn date_trunc_time(unit: &str, micros: i64) -> Result<i64> {
1099 match unit {
1100 "microseconds" => Ok(micros),
1101 "milliseconds" => Ok(micros - (micros % 1000)),
1102 "second" => Ok(micros - (micros % MICROS_PER_SEC)),
1103 "minute" => Ok(micros - (micros % MICROS_PER_MIN)),
1104 "hour" => Ok(micros - (micros % MICROS_PER_HOUR)),
1105 _ => Err(SqlError::InvalidDateTruncUnit(format!(
1106 "{unit} is invalid for TIME"
1107 ))),
1108 }
1109}
1110
1111fn date_trunc_interval(unit: &str, months: i32, days: i32, micros: i64) -> Result<(i32, i32, i64)> {
1112 match unit {
1113 "microseconds" => Ok((months, days, micros)),
1114 "milliseconds" => Ok((months, days, micros - (micros % 1000))),
1115 "second" => Ok((months, days, micros - (micros % MICROS_PER_SEC))),
1116 "minute" => Ok((months, days, micros - (micros % MICROS_PER_MIN))),
1117 "hour" => Ok((months, days, micros - (micros % MICROS_PER_HOUR))),
1118 "day" => Ok((months, days, 0)),
1119 "month" => Ok((months, 0, 0)),
1120 "year" => Ok(((months / 12) * 12, 0, 0)),
1121 "quarter" => Ok(((months / 3) * 3, 0, 0)),
1122 "decade" => Ok(((months / 120) * 120, 0, 0)),
1123 "century" => Ok(((months / 1200) * 1200, 0, 0)),
1124 "millennium" => Ok(((months / 12000) * 12000, 0, 0)),
1125 _ => Err(SqlError::InvalidDateTruncUnit(unit.into())),
1126 }
1127}
1128
1129pub fn strftime(fmt: &str, v: &Value) -> Result<String> {
1130 let ts_micros = match v {
1131 Value::Null => return Ok(String::new()),
1132 Value::Timestamp(t) => *t,
1133 Value::Date(d) => date_to_ts(*d),
1134 Value::Time(t) => *t, _ => {
1136 return Err(SqlError::TypeMismatch {
1137 expected: "temporal type".into(),
1138 got: v.data_type().to_string(),
1139 })
1140 }
1141 };
1142 let z = JTimestamp::from_microsecond(ts_micros)
1143 .map_err(|e| SqlError::InvalidValue(format!("ts: {e}")))?
1144 .to_zoned(TimeZone::UTC);
1145 let mut prepared = String::with_capacity(fmt.len());
1147 let mut chars = fmt.chars().peekable();
1148 while let Some(c) = chars.next() {
1149 if c == '%' {
1150 match chars.peek() {
1151 Some('J') => {
1152 chars.next();
1153 let julian = ts_to_date_floor(ts_micros) as f64
1155 + 2_440_587.5
1156 + (ts_split(ts_micros).1 as f64) / (MICROS_PER_DAY as f64);
1157 prepared.push_str(&format!("{julian}"));
1158 }
1159 Some('f') => {
1160 chars.next();
1161 let subsec = ts_split(ts_micros).1 % MICROS_PER_SEC;
1162 prepared.push_str(&format!("{:06}", subsec));
1163 }
1164 Some('s') => {
1165 chars.next();
1166 prepared.push_str(&format!("{}", ts_micros / MICROS_PER_SEC));
1167 }
1168 Some(&next) => {
1169 prepared.push('%');
1170 prepared.push(next);
1171 chars.next();
1172 }
1173 None => prepared.push('%'),
1174 }
1175 } else {
1176 prepared.push(c);
1177 }
1178 }
1179 Ok(z.strftime(&prepared).to_string())
1180}
1181
1182pub fn pg_normalized_interval_cmp(a: (i32, i32, i64), b: (i32, i32, i64)) -> std::cmp::Ordering {
1184 let at = interval_to_total_micros(a.0, a.1, a.2);
1185 let bt = interval_to_total_micros(b.0, b.1, b.2);
1186 at.cmp(&bt)
1187}
1188
1189#[cfg(test)]
1190mod tests {
1191 use super::*;
1192
1193 #[test]
1194 fn ymd_roundtrip_epoch() {
1195 assert_eq!(days_to_ymd(0), (1970, 1, 1));
1196 assert_eq!(ymd_to_days(1970, 1, 1), Some(0));
1197 }
1198
1199 #[test]
1200 fn ymd_roundtrip_leap_day() {
1201 let days = ymd_to_days(2024, 2, 29).unwrap();
1202 assert_eq!(days_to_ymd(days), (2024, 2, 29));
1203 }
1204
1205 #[test]
1206 fn ymd_pre_epoch() {
1207 let days = ymd_to_days(1960, 1, 1).unwrap();
1208 assert!(days < 0);
1209 assert_eq!(days_to_ymd(days), (1960, 1, 1));
1210 }
1211
1212 #[test]
1213 fn hmsn_roundtrip() {
1214 let us = hmsn_to_micros(12, 30, 45, 123456).unwrap();
1215 assert_eq!(micros_to_hmsn(us), (12, 30, 45, 123456));
1216 }
1217
1218 #[test]
1219 fn time_upper_bound_inclusive() {
1220 assert_eq!(hmsn_to_micros(24, 0, 0, 0), Some(MICROS_PER_DAY));
1221 assert_eq!(hmsn_to_micros(24, 0, 0, 1), None);
1222 }
1223
1224 #[test]
1225 fn ts_split_pre_1970() {
1226 let (d, t) = ts_split(-1);
1228 assert_eq!(d, -1); assert_eq!(t, MICROS_PER_DAY - 1);
1230 }
1231
1232 #[test]
1233 fn parse_format_date_roundtrip() {
1234 let d = parse_date("2024-01-15").unwrap();
1235 assert_eq!(format_date(d), "2024-01-15");
1236 }
1237
1238 #[test]
1239 fn parse_date_bc() {
1240 let ad = parse_date("0001-01-01").unwrap();
1242 let bc = parse_date("0001-01-01 BC").unwrap();
1243 assert!(bc < ad);
1244 }
1245
1246 #[test]
1247 fn parse_date_rejects_year_0() {
1248 assert!(parse_date("0000-01-01").is_err());
1249 }
1250
1251 #[test]
1252 fn parse_date_infinity() {
1253 assert_eq!(parse_date("infinity").unwrap(), DATE_INFINITY_DAYS);
1254 assert_eq!(parse_date("-infinity").unwrap(), DATE_NEG_INFINITY_DAYS);
1255 }
1256
1257 #[test]
1258 fn parse_time_with_fractional() {
1259 let t = parse_time("12:30:45.123456").unwrap();
1260 assert_eq!(format_time(t), "12:30:45.123456");
1261 }
1262
1263 #[test]
1264 fn parse_time_24_00() {
1265 assert_eq!(parse_time("24:00:00").unwrap(), MICROS_PER_DAY);
1266 }
1267
1268 #[test]
1269 fn parse_timestamp_iso() {
1270 let t = parse_timestamp("2024-01-15T12:30:45Z").unwrap();
1271 assert_eq!(format_timestamp(t), "2024-01-15 12:30:45");
1272 }
1273
1274 #[test]
1275 fn parse_timestamp_naive() {
1276 let t1 = parse_timestamp("2024-01-15 12:30:45").unwrap();
1277 let t2 = parse_timestamp("2024-01-15T12:30:45").unwrap();
1278 assert_eq!(t1, t2);
1279 }
1280
1281 #[test]
1282 fn parse_timestamp_infinity() {
1283 assert_eq!(parse_timestamp("infinity").unwrap(), TS_INFINITY_MICROS);
1284 }
1285
1286 #[test]
1287 fn parse_timestamp_bc() {
1288 let ad = parse_timestamp("0001-01-01 00:00:00").unwrap();
1290 let bc = parse_timestamp("0001-12-31 00:00:00 BC").unwrap();
1291 assert_eq!(ad - bc, MICROS_PER_DAY);
1292 }
1293
1294 #[test]
1295 fn parse_timestamp_rejects_year_0() {
1296 assert!(parse_timestamp("0000-06-15 12:00:00").is_err());
1297 }
1298
1299 #[test]
1300 fn parse_interval_pg_verbose() {
1301 let (m, d, us) = parse_interval("1 year 2 months 3 days").unwrap();
1302 assert_eq!((m, d, us), (14, 3, 0));
1303 }
1304
1305 #[test]
1306 fn parse_interval_with_hms() {
1307 let (m, d, us) = parse_interval("3 days 04:05:06.789").unwrap();
1308 assert_eq!(m, 0);
1309 assert_eq!(d, 3);
1310 let expected_us = 4 * MICROS_PER_HOUR + 5 * MICROS_PER_MIN + 6 * MICROS_PER_SEC + 789000;
1311 assert_eq!(us, expected_us);
1312 }
1313
1314 #[test]
1315 fn parse_interval_iso8601() {
1316 let (m, d, us) = parse_interval("P1Y2M3DT4H5M6S").unwrap();
1317 assert_eq!(m, 14);
1318 assert_eq!(d, 3);
1319 assert_eq!(
1320 us,
1321 4 * MICROS_PER_HOUR + 5 * MICROS_PER_MIN + 6 * MICROS_PER_SEC
1322 );
1323 }
1324
1325 #[test]
1326 fn format_interval_zero() {
1327 assert_eq!(format_interval(0, 0, 0), "00:00:00");
1328 }
1329
1330 #[test]
1331 fn format_interval_mixed() {
1332 assert_eq!(
1333 format_interval(
1334 14,
1335 3,
1336 4 * MICROS_PER_HOUR + 5 * MICROS_PER_MIN + 6 * MICROS_PER_SEC
1337 ),
1338 "1 year 2 mons 3 days 04:05:06"
1339 );
1340 }
1341
1342 #[test]
1343 fn add_interval_month_clamp() {
1344 let jan31 = parse_date("2024-01-31").unwrap();
1346 let ts = add_interval_to_date(jan31, 1, 0, 0).unwrap();
1347 let (d, _t) = ts_split(ts);
1348 let (y, mo, da) = days_to_ymd(d);
1349 assert_eq!((y, mo, da), (2024, 2, 29));
1350 }
1351
1352 #[test]
1353 fn add_interval_month_clamp_non_leap() {
1354 let jan31 = parse_date("2023-01-31").unwrap();
1355 let ts = add_interval_to_date(jan31, 1, 0, 0).unwrap();
1356 let (d, _t) = ts_split(ts);
1357 let (y, mo, da) = days_to_ymd(d);
1358 assert_eq!((y, mo, da), (2023, 2, 28));
1359 }
1360
1361 #[test]
1362 fn interval_normalized_compare() {
1363 let a = (1i32, 0i32, 0i64);
1365 let b = (0i32, 30i32, 0i64);
1366 assert_eq!(pg_normalized_interval_cmp(a, b), std::cmp::Ordering::Equal);
1367 }
1368
1369 #[test]
1370 fn justify_days_basic() {
1371 let (m, d, us) = justify_days(0, 65, 0);
1372 assert_eq!((m, d, us), (2, 5, 0));
1373 }
1374
1375 #[test]
1376 fn justify_hours_basic() {
1377 let (m, d, us) = justify_hours(0, 0, 50 * MICROS_PER_HOUR + 10 * MICROS_PER_MIN);
1378 assert_eq!(
1379 (m, d, us),
1380 (0, 2, 2 * MICROS_PER_HOUR + 10 * MICROS_PER_MIN)
1381 );
1382 }
1383
1384 #[test]
1385 fn time_add_wrap() {
1386 let t = parse_time("23:00:00").unwrap();
1387 let result = add_interval_to_time(t, 0, 0, 2 * MICROS_PER_HOUR).unwrap();
1388 assert_eq!(format_time(result), "01:00:00");
1389 }
1390
1391 #[test]
1392 fn time_add_rejects_days() {
1393 let t = parse_time("12:00:00").unwrap();
1394 assert!(add_interval_to_time(t, 0, 1, 0).is_err());
1395 }
1396
1397 #[test]
1398 fn subtract_timestamps_basic() {
1399 let a = parse_timestamp("2024-01-02 12:00:00").unwrap();
1400 let b = parse_timestamp("2024-01-01 00:00:00").unwrap();
1401 let (days, micros) = subtract_timestamps(a, b);
1402 assert_eq!(days, 1);
1403 assert_eq!(micros, 12 * MICROS_PER_HOUR);
1404 }
1405
1406 #[test]
1407 fn ts_to_date_floor_pre_epoch() {
1408 assert_eq!(ts_to_date_floor(-1), -1);
1409 assert_eq!(ts_to_date_floor(0), 0);
1410 assert_eq!(ts_to_date_floor(MICROS_PER_DAY - 1), 0);
1411 assert_eq!(ts_to_date_floor(MICROS_PER_DAY), 1);
1412 }
1413
1414 #[test]
1415 fn extract_year_from_date() {
1416 let d = parse_date("2024-03-15").unwrap();
1417 assert_eq!(
1418 extract("year", &Value::Date(d)).unwrap(),
1419 Value::Integer(2024)
1420 );
1421 }
1422
1423 #[test]
1424 fn extract_dow_sunday() {
1425 let d = parse_date("2024-01-07").unwrap();
1427 assert_eq!(extract("dow", &Value::Date(d)).unwrap(), Value::Integer(0));
1428 assert_eq!(
1429 extract("isodow", &Value::Date(d)).unwrap(),
1430 Value::Integer(7)
1431 );
1432 }
1433
1434 #[test]
1435 fn date_trunc_month() {
1436 let ts = parse_timestamp("2024-03-15 12:30:45").unwrap();
1437 let result = date_trunc("month", &Value::Timestamp(ts)).unwrap();
1438 if let Value::Timestamp(t) = result {
1439 assert_eq!(format_timestamp(t), "2024-03-01 00:00:00");
1440 } else {
1441 panic!("expected Timestamp");
1442 }
1443 }
1444
1445 #[test]
1446 fn date_trunc_week_monday() {
1447 let d = parse_date("2024-01-07").unwrap();
1449 let Value::Date(trunc) = date_trunc("week", &Value::Date(d)).unwrap() else {
1450 panic!("expected Date");
1451 };
1452 assert_eq!(format_date(trunc), "2024-01-01");
1453 }
1454
1455 #[test]
1456 fn age_basic() {
1457 let a = parse_timestamp("2024-04-10 00:00:00").unwrap();
1458 let b = parse_timestamp("2024-01-01 00:00:00").unwrap();
1459 let (m, d, us) = age(a, b).unwrap();
1460 assert_eq!(m, 3);
1462 assert_eq!(d, 9);
1463 assert_eq!(us, 0);
1464 }
1465
1466 #[test]
1467 fn strftime_basic() {
1468 let ts = parse_timestamp("2024-03-15 12:30:45").unwrap();
1469 let s = strftime("%Y-%m-%d", &Value::Timestamp(ts)).unwrap();
1470 assert_eq!(s, "2024-03-15");
1471 }
1472
1473 #[test]
1474 fn strftime_unix_epoch() {
1475 let ts = parse_timestamp("2024-01-01 00:00:00").unwrap();
1476 let s = strftime("%s", &Value::Timestamp(ts)).unwrap();
1477 assert_eq!(s, (ts / MICROS_PER_SEC).to_string());
1478 }
1479
1480 #[test]
1481 fn is_finite_temporal_sentinels() {
1482 assert!(!Value::Date(i32::MAX).is_finite_temporal());
1483 assert!(!Value::Date(i32::MIN).is_finite_temporal());
1484 assert!(Value::Date(0).is_finite_temporal());
1485 assert!(!Value::Timestamp(i64::MAX).is_finite_temporal());
1486 assert!(Value::Timestamp(0).is_finite_temporal());
1487 }
1488
1489 #[test]
1490 fn add_interval_infinity() {
1491 let result = add_interval_to_timestamp(TS_INFINITY_MICROS, 1, 1, 0).unwrap();
1492 assert_eq!(result, TS_INFINITY_MICROS);
1493 }
1494
1495 #[test]
1496 fn format_date_bc() {
1497 let bc1 = parse_date("0001-01-01 BC").unwrap();
1499 assert_eq!(format_date(bc1), "0001-01-01 BC");
1500 }
1501}