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 set_txn_clock(ts: Option<i64>) {
683 TXN_CLOCK.with(|slot| slot.set(ts));
684}
685
686pub fn txn_or_clock_micros() -> i64 {
689 TXN_CLOCK.with(|slot| slot.get()).unwrap_or_else(now_micros)
690}
691
692pub fn today_days() -> i32 {
693 ts_to_date_floor(now_micros())
694}
695
696pub fn current_time_micros() -> i64 {
697 ts_split(now_micros()).1
698}
699
700pub fn add_interval_to_timestamp(ts: i64, months: i32, days: i32, micros: i64) -> Result<i64> {
701 if ts == TS_INFINITY_MICROS || ts == TS_NEG_INFINITY_MICROS {
702 return Ok(ts);
703 }
704 let jts =
705 JTimestamp::from_microsecond(ts).map_err(|e| SqlError::InvalidValue(format!("ts: {e}")))?;
706 let span = Span::new()
708 .try_months(months as i64)
709 .map_err(|e| SqlError::InvalidValue(format!("months overflow: {e}")))?
710 .try_days(days as i64)
711 .map_err(|e| SqlError::InvalidValue(format!("days overflow: {e}")))?
712 .try_microseconds(micros)
713 .map_err(|e| SqlError::InvalidValue(format!("micros overflow: {e}")))?;
714 let result = jts
715 .to_zoned(TimeZone::UTC)
716 .checked_add(span)
717 .map_err(|_| SqlError::IntegerOverflow)?;
718 Ok(result.timestamp().as_microsecond())
719}
720
721pub fn add_interval_to_date(days: i32, months: i32, i_days: i32, micros: i64) -> Result<i64> {
723 if is_infinity_date(days) {
724 return Ok(if days == DATE_INFINITY_DAYS {
725 TS_INFINITY_MICROS
726 } else {
727 TS_NEG_INFINITY_MICROS
728 });
729 }
730 let ts = date_to_ts(days);
731 add_interval_to_timestamp(ts, months, i_days, micros)
732}
733
734pub fn add_days_to_date(days: i32, n: i64) -> Result<i32> {
735 if is_infinity_date(days) {
736 return Ok(days);
737 }
738 let new_days = (days as i64)
739 .checked_add(n)
740 .ok_or(SqlError::IntegerOverflow)?;
741 if new_days >= i32::MIN as i64 && new_days <= i32::MAX as i64 {
742 Ok(new_days as i32)
743 } else {
744 Err(SqlError::IntegerOverflow)
745 }
746}
747
748pub fn add_interval_to_time(t: i64, months: i32, days: i32, micros: i64) -> Result<i64> {
749 if months != 0 || days != 0 {
750 return Err(SqlError::InvalidValue(
751 "cannot add month/day interval to TIME".into(),
752 ));
753 }
754 let combined = t.checked_add(micros).unwrap_or(t);
756 Ok(combined.rem_euclid(MICROS_PER_DAY))
757}
758
759pub fn subtract_timestamps(a: i64, b: i64) -> (i32, i64) {
761 let diff = a.saturating_sub(b);
762 let days = (diff / MICROS_PER_DAY) as i32;
763 let micros = diff % MICROS_PER_DAY;
764 (days, micros)
765}
766
767pub fn age(ts_a: i64, ts_b: i64) -> Result<(i32, i32, i64)> {
769 let a = JTimestamp::from_microsecond(ts_a)
770 .map_err(|e| SqlError::InvalidValue(format!("ts_a: {e}")))?
771 .to_zoned(TimeZone::UTC);
772 let b = JTimestamp::from_microsecond(ts_b)
773 .map_err(|e| SqlError::InvalidValue(format!("ts_b: {e}")))?
774 .to_zoned(TimeZone::UTC);
775 let span = a
776 .since((Unit::Year, &b))
777 .map_err(|e| SqlError::InvalidValue(format!("age: {e}")))?;
778 span_to_triple(&span)
779}
780
781fn span_to_triple(span: &Span) -> Result<(i32, i32, i64)> {
782 let months = i64::from(span.get_years()) * 12 + i64::from(span.get_months());
783 let days = i64::from(span.get_weeks()) * 7 + i64::from(span.get_days());
784 let micros = i64::from(span.get_hours()) * MICROS_PER_HOUR
785 + span.get_minutes() * MICROS_PER_MIN
786 + span.get_seconds() * MICROS_PER_SEC
787 + span.get_milliseconds() * 1000
788 + span.get_microseconds()
789 + span.get_nanoseconds() / 1000;
790 Ok((clamp_i32(months)?, clamp_i32(days)?, micros))
791}
792
793pub fn justify_days(months: i32, days: i32, micros: i64) -> (i32, i32, i64) {
794 let extra_months = days / 30;
796 let rem_days = days % 30;
797 let new_months = months.saturating_add(extra_months);
798 (new_months, rem_days, micros)
799}
800
801pub fn justify_hours(months: i32, days: i32, micros: i64) -> (i32, i32, i64) {
802 let extra_days = (micros / MICROS_PER_DAY) as i32;
804 let rem_micros = micros % MICROS_PER_DAY;
805 let new_days = days.saturating_add(extra_days);
806 (months, new_days, rem_micros)
807}
808
809pub fn justify_interval(months: i32, days: i32, micros: i64) -> (i32, i32, i64) {
810 let (m1, d1, us1) = justify_hours(months, days, micros);
811 let (m2, d2, us2) = justify_days(m1, d1, us1);
812 (m2, d2, us2)
813}
814
815pub fn interval_to_total_micros(months: i32, days: i32, micros: i64) -> i128 {
817 (months as i128) * 30 * (MICROS_PER_DAY as i128)
818 + (days as i128) * (MICROS_PER_DAY as i128)
819 + micros as i128
820}
821
822pub fn extract(field: &str, v: &Value) -> Result<Value> {
823 let f = field.trim();
824 match v {
825 Value::Null => Ok(Value::Null),
826 Value::Date(d) => extract_from_date(f, *d),
827 Value::Time(t) => extract_from_time(f, *t),
828 Value::Timestamp(t) => extract_from_timestamp(f, *t),
829 Value::Interval {
830 months,
831 days,
832 micros,
833 } => extract_from_interval(f, *months, *days, *micros),
834 _ => Err(SqlError::TypeMismatch {
835 expected: "temporal type".into(),
836 got: v.data_type().to_string(),
837 }),
838 }
839}
840
841fn extract_from_date(field: &str, days: i32) -> Result<Value> {
842 if field.eq_ignore_ascii_case("epoch") {
843 return Ok(Value::Integer((days as i64) * 86400));
844 }
845 let (y, m, d) = days_to_ymd(days);
846 if field.eq_ignore_ascii_case("year") {
847 return Ok(Value::Integer(y as i64));
848 }
849 if field.eq_ignore_ascii_case("month") {
850 return Ok(Value::Integer(m as i64));
851 }
852 if field.eq_ignore_ascii_case("day") {
853 return Ok(Value::Integer(d as i64));
854 }
855 if field.eq_ignore_ascii_case("hour")
856 || field.eq_ignore_ascii_case("minute")
857 || field.eq_ignore_ascii_case("second")
858 || field.eq_ignore_ascii_case("microseconds")
859 || field.eq_ignore_ascii_case("milliseconds")
860 {
861 return Ok(Value::Integer(0));
862 }
863 let f = field.to_ascii_lowercase();
865 match f.as_str() {
866 "dow" => {
867 let jd = JDate::new(y as i16, m as i8, d as i8)
868 .map_err(|e| SqlError::InvalidValue(format!("{e}")))?;
869 let w = jd.weekday().to_monday_one_offset() as i64;
871 let dow = if w == 7 { 0 } else { w }; Ok(Value::Integer(dow))
873 }
874 "isodow" => {
875 let jd = JDate::new(y as i16, m as i8, d as i8)
876 .map_err(|e| SqlError::InvalidValue(format!("{e}")))?;
877 Ok(Value::Integer(jd.weekday().to_monday_one_offset() as i64))
878 }
879 "doy" => {
880 let jd = JDate::new(y as i16, m as i8, d as i8)
881 .map_err(|e| SqlError::InvalidValue(format!("{e}")))?;
882 Ok(Value::Integer(jd.day_of_year() as i64))
883 }
884 "quarter" => Ok(Value::Integer(((m - 1) / 3 + 1) as i64)),
885 "decade" => Ok(Value::Integer((y / 10) as i64)),
886 "century" => Ok(Value::Integer(if y > 0 {
887 ((y - 1) / 100 + 1) as i64
888 } else {
889 (y / 100 - 1) as i64
890 })),
891 "millennium" => Ok(Value::Integer(if y > 0 {
892 ((y - 1) / 1000 + 1) as i64
893 } else {
894 (y / 1000 - 1) as i64
895 })),
896 "julian" => Ok(Value::Integer(days as i64 + 2_440_588)),
897 "week" | "isoyear" => {
898 let jd = JDate::new(y as i16, m as i8, d as i8)
899 .map_err(|e| SqlError::InvalidValue(format!("{e}")))?;
900 let iso = jd.iso_week_date();
901 if field == "week" {
902 Ok(Value::Integer(iso.week() as i64))
903 } else {
904 Ok(Value::Integer(iso.year() as i64))
905 }
906 }
907 _ => Err(SqlError::InvalidExtractField(format!("{field} from DATE"))),
908 }
909}
910
911fn extract_from_time(field: &str, micros: i64) -> Result<Value> {
912 let (h, m, s, us) = micros_to_hmsn(micros);
913 match field {
914 "hour" => Ok(Value::Integer(h as i64)),
915 "minute" => Ok(Value::Integer(m as i64)),
916 "second" => {
917 if us == 0 {
918 Ok(Value::Integer(s as i64))
919 } else {
920 Ok(Value::Real(s as f64 + (us as f64) / 1_000_000.0))
921 }
922 }
923 "microseconds" => Ok(Value::Integer((s as i64) * 1_000_000 + us as i64)),
924 "milliseconds" => Ok(Value::Real(s as f64 * 1000.0 + (us as f64) / 1000.0)),
925 "epoch" => Ok(Value::Integer(micros / MICROS_PER_SEC)),
926 _ => Err(SqlError::InvalidExtractField(format!("{field} from TIME"))),
927 }
928}
929
930fn extract_from_timestamp(field: &str, ts: i64) -> Result<Value> {
931 if field.eq_ignore_ascii_case("hour") {
932 return Ok(Value::Integer(
933 ts.rem_euclid(MICROS_PER_DAY) / MICROS_PER_HOUR,
934 ));
935 }
936 if field.eq_ignore_ascii_case("minute") {
937 return Ok(Value::Integer(
938 ts.rem_euclid(MICROS_PER_HOUR) / MICROS_PER_MIN,
939 ));
940 }
941 if field.eq_ignore_ascii_case("epoch") {
942 return Ok(Value::Integer(ts / MICROS_PER_SEC));
943 }
944 let (date_days, time_micros) = ts_split(ts);
945 let date_fields = [
947 "year",
948 "month",
949 "day",
950 "dow",
951 "isodow",
952 "doy",
953 "quarter",
954 "decade",
955 "century",
956 "millennium",
957 "julian",
958 "week",
959 "isoyear",
960 ];
961 if date_fields.iter().any(|&f| field.eq_ignore_ascii_case(f)) {
962 return extract_from_date(field, date_days);
963 }
964 let time_fields = ["second", "microseconds", "milliseconds"];
966 if time_fields.iter().any(|&f| field.eq_ignore_ascii_case(f)) {
967 return extract_from_time(field, time_micros);
968 }
969 Err(SqlError::InvalidExtractField(format!(
970 "{field} from TIMESTAMP"
971 )))
972}
973
974fn extract_from_interval(field: &str, months: i32, days: i32, micros: i64) -> Result<Value> {
975 match field {
976 "year" => Ok(Value::Integer((months / 12) as i64)),
977 "month" => Ok(Value::Integer((months % 12) as i64)),
978 "day" => Ok(Value::Integer(days as i64)),
979 "hour" => Ok(Value::Integer(micros / MICROS_PER_HOUR)),
980 "minute" => Ok(Value::Integer((micros % MICROS_PER_HOUR) / MICROS_PER_MIN)),
981 "second" => {
982 let rem = micros % MICROS_PER_MIN;
983 let sec_part = rem / MICROS_PER_SEC;
984 let us_part = rem % MICROS_PER_SEC;
985 if us_part == 0 {
986 Ok(Value::Integer(sec_part))
987 } else {
988 Ok(Value::Real(sec_part as f64 + us_part as f64 / 1_000_000.0))
989 }
990 }
991 "microseconds" => Ok(Value::Integer(micros % MICROS_PER_MIN)),
992 "epoch" => {
993 let total = interval_to_total_micros(months, days, micros);
994 Ok(Value::Real(total as f64 / 1_000_000.0))
995 }
996 _ => Err(SqlError::InvalidExtractField(format!(
997 "{field} from INTERVAL"
998 ))),
999 }
1000}
1001
1002pub fn date_trunc(unit: &str, v: &Value) -> Result<Value> {
1003 let u = unit.trim().to_ascii_lowercase();
1004 match v {
1005 Value::Null => Ok(Value::Null),
1006 Value::Date(d) => date_trunc_date(&u, *d).map(Value::Date),
1007 Value::Timestamp(t) => date_trunc_timestamp(&u, *t).map(Value::Timestamp),
1008 Value::Time(t) => date_trunc_time(&u, *t).map(Value::Time),
1009 Value::Interval {
1010 months,
1011 days,
1012 micros,
1013 } => date_trunc_interval(&u, *months, *days, *micros).map(|(m, d, us)| Value::Interval {
1014 months: m,
1015 days: d,
1016 micros: us,
1017 }),
1018 _ => Err(SqlError::TypeMismatch {
1019 expected: "temporal type".into(),
1020 got: v.data_type().to_string(),
1021 }),
1022 }
1023}
1024
1025fn date_trunc_date(unit: &str, days: i32) -> Result<i32> {
1026 if is_infinity_date(days) {
1027 return Ok(days);
1028 }
1029 let (y, m, d) = days_to_ymd(days);
1030 match unit {
1031 "microseconds" | "milliseconds" | "second" | "minute" | "hour" | "day" => Ok(days),
1032 "week" => {
1033 let jd = JDate::new(y as i16, m as i8, d as i8)
1035 .map_err(|e| SqlError::InvalidValue(format!("{e}")))?;
1036 let dow = jd.weekday().to_monday_one_offset() as i32; add_days_to_date(days, -(dow - 1) as i64)
1038 }
1039 "month" => {
1040 ymd_to_days(y, m, 1).ok_or_else(|| SqlError::InvalidValue("date_trunc month".into()))
1041 }
1042 "quarter" => {
1043 let qm = ((m - 1) / 3) * 3 + 1;
1044 ymd_to_days(y, qm, 1).ok_or_else(|| SqlError::InvalidValue("date_trunc quarter".into()))
1045 }
1046 "year" => {
1047 ymd_to_days(y, 1, 1).ok_or_else(|| SqlError::InvalidValue("date_trunc year".into()))
1048 }
1049 "decade" => ymd_to_days(y - (y % 10), 1, 1)
1050 .ok_or_else(|| SqlError::InvalidValue("date_trunc decade".into())),
1051 "century" => {
1052 let cy = if y > 0 {
1053 ((y - 1) / 100) * 100 + 1
1054 } else {
1055 (y / 100) * 100 - 99
1056 };
1057 ymd_to_days(cy, 1, 1).ok_or_else(|| SqlError::InvalidValue("date_trunc century".into()))
1058 }
1059 "millennium" => {
1060 let my = if y > 0 {
1061 ((y - 1) / 1000) * 1000 + 1
1062 } else {
1063 (y / 1000) * 1000 - 999
1064 };
1065 ymd_to_days(my, 1, 1)
1066 .ok_or_else(|| SqlError::InvalidValue("date_trunc millennium".into()))
1067 }
1068 _ => Err(SqlError::InvalidDateTruncUnit(unit.into())),
1069 }
1070}
1071
1072fn date_trunc_timestamp(unit: &str, ts: i64) -> Result<i64> {
1073 if is_infinity_ts(ts) {
1074 return Ok(ts);
1075 }
1076 let (date_days, time_micros) = ts_split(ts);
1077 match unit {
1079 "microseconds" => Ok(ts),
1080 "milliseconds" => Ok(ts_combine(date_days, time_micros - time_micros % 1000)),
1081 "second" => Ok(ts_combine(
1082 date_days,
1083 time_micros - time_micros % MICROS_PER_SEC,
1084 )),
1085 "minute" => Ok(ts_combine(
1086 date_days,
1087 time_micros - time_micros % MICROS_PER_MIN,
1088 )),
1089 "hour" => Ok(ts_combine(
1090 date_days,
1091 time_micros - time_micros % MICROS_PER_HOUR,
1092 )),
1093 "day" => Ok(ts_combine(date_days, 0)),
1094 _ => {
1095 let trunc_days = date_trunc_date(unit, date_days)?;
1097 Ok(ts_combine(trunc_days, 0))
1098 }
1099 }
1100}
1101
1102fn date_trunc_time(unit: &str, micros: i64) -> Result<i64> {
1103 match unit {
1104 "microseconds" => Ok(micros),
1105 "milliseconds" => Ok(micros - (micros % 1000)),
1106 "second" => Ok(micros - (micros % MICROS_PER_SEC)),
1107 "minute" => Ok(micros - (micros % MICROS_PER_MIN)),
1108 "hour" => Ok(micros - (micros % MICROS_PER_HOUR)),
1109 _ => Err(SqlError::InvalidDateTruncUnit(format!(
1110 "{unit} is invalid for TIME"
1111 ))),
1112 }
1113}
1114
1115fn date_trunc_interval(unit: &str, months: i32, days: i32, micros: i64) -> Result<(i32, i32, i64)> {
1116 match unit {
1117 "microseconds" => Ok((months, days, micros)),
1118 "milliseconds" => Ok((months, days, micros - (micros % 1000))),
1119 "second" => Ok((months, days, micros - (micros % MICROS_PER_SEC))),
1120 "minute" => Ok((months, days, micros - (micros % MICROS_PER_MIN))),
1121 "hour" => Ok((months, days, micros - (micros % MICROS_PER_HOUR))),
1122 "day" => Ok((months, days, 0)),
1123 "month" => Ok((months, 0, 0)),
1124 "year" => Ok(((months / 12) * 12, 0, 0)),
1125 "quarter" => Ok(((months / 3) * 3, 0, 0)),
1126 "decade" => Ok(((months / 120) * 120, 0, 0)),
1127 "century" => Ok(((months / 1200) * 1200, 0, 0)),
1128 "millennium" => Ok(((months / 12000) * 12000, 0, 0)),
1129 _ => Err(SqlError::InvalidDateTruncUnit(unit.into())),
1130 }
1131}
1132
1133pub fn strftime(fmt: &str, v: &Value) -> Result<String> {
1134 let ts_micros = match v {
1135 Value::Null => return Ok(String::new()),
1136 Value::Timestamp(t) => *t,
1137 Value::Date(d) => date_to_ts(*d),
1138 Value::Time(t) => *t, _ => {
1140 return Err(SqlError::TypeMismatch {
1141 expected: "temporal type".into(),
1142 got: v.data_type().to_string(),
1143 })
1144 }
1145 };
1146 let z = JTimestamp::from_microsecond(ts_micros)
1147 .map_err(|e| SqlError::InvalidValue(format!("ts: {e}")))?
1148 .to_zoned(TimeZone::UTC);
1149 let mut prepared = String::with_capacity(fmt.len());
1151 let mut chars = fmt.chars().peekable();
1152 while let Some(c) = chars.next() {
1153 if c == '%' {
1154 match chars.peek() {
1155 Some('J') => {
1156 chars.next();
1157 let julian = ts_to_date_floor(ts_micros) as f64
1159 + 2_440_587.5
1160 + (ts_split(ts_micros).1 as f64) / (MICROS_PER_DAY as f64);
1161 prepared.push_str(&format!("{julian}"));
1162 }
1163 Some('f') => {
1164 chars.next();
1165 let subsec = ts_split(ts_micros).1 % MICROS_PER_SEC;
1166 prepared.push_str(&format!("{:06}", subsec));
1167 }
1168 Some('s') => {
1169 chars.next();
1170 prepared.push_str(&format!("{}", ts_micros / MICROS_PER_SEC));
1171 }
1172 Some(&next) => {
1173 prepared.push('%');
1174 prepared.push(next);
1175 chars.next();
1176 }
1177 None => prepared.push('%'),
1178 }
1179 } else {
1180 prepared.push(c);
1181 }
1182 }
1183 Ok(z.strftime(&prepared).to_string())
1184}
1185
1186pub fn pg_normalized_interval_cmp(a: (i32, i32, i64), b: (i32, i32, i64)) -> std::cmp::Ordering {
1188 let at = interval_to_total_micros(a.0, a.1, a.2);
1189 let bt = interval_to_total_micros(b.0, b.1, b.2);
1190 at.cmp(&bt)
1191}
1192
1193#[cfg(test)]
1194mod tests {
1195 use super::*;
1196
1197 #[test]
1198 fn ymd_roundtrip_epoch() {
1199 assert_eq!(days_to_ymd(0), (1970, 1, 1));
1200 assert_eq!(ymd_to_days(1970, 1, 1), Some(0));
1201 }
1202
1203 #[test]
1204 fn ymd_roundtrip_leap_day() {
1205 let days = ymd_to_days(2024, 2, 29).unwrap();
1206 assert_eq!(days_to_ymd(days), (2024, 2, 29));
1207 }
1208
1209 #[test]
1210 fn ymd_pre_epoch() {
1211 let days = ymd_to_days(1960, 1, 1).unwrap();
1212 assert!(days < 0);
1213 assert_eq!(days_to_ymd(days), (1960, 1, 1));
1214 }
1215
1216 #[test]
1217 fn hmsn_roundtrip() {
1218 let us = hmsn_to_micros(12, 30, 45, 123456).unwrap();
1219 assert_eq!(micros_to_hmsn(us), (12, 30, 45, 123456));
1220 }
1221
1222 #[test]
1223 fn time_upper_bound_inclusive() {
1224 assert_eq!(hmsn_to_micros(24, 0, 0, 0), Some(MICROS_PER_DAY));
1225 assert_eq!(hmsn_to_micros(24, 0, 0, 1), None);
1226 }
1227
1228 #[test]
1229 fn ts_split_pre_1970() {
1230 let (d, t) = ts_split(-1);
1232 assert_eq!(d, -1); assert_eq!(t, MICROS_PER_DAY - 1);
1234 }
1235
1236 #[test]
1237 fn parse_format_date_roundtrip() {
1238 let d = parse_date("2024-01-15").unwrap();
1239 assert_eq!(format_date(d), "2024-01-15");
1240 }
1241
1242 #[test]
1243 fn parse_date_bc() {
1244 let ad = parse_date("0001-01-01").unwrap();
1246 let bc = parse_date("0001-01-01 BC").unwrap();
1247 assert!(bc < ad);
1248 }
1249
1250 #[test]
1251 fn parse_date_rejects_year_0() {
1252 assert!(parse_date("0000-01-01").is_err());
1253 }
1254
1255 #[test]
1256 fn parse_date_infinity() {
1257 assert_eq!(parse_date("infinity").unwrap(), DATE_INFINITY_DAYS);
1258 assert_eq!(parse_date("-infinity").unwrap(), DATE_NEG_INFINITY_DAYS);
1259 }
1260
1261 #[test]
1262 fn parse_time_with_fractional() {
1263 let t = parse_time("12:30:45.123456").unwrap();
1264 assert_eq!(format_time(t), "12:30:45.123456");
1265 }
1266
1267 #[test]
1268 fn parse_time_24_00() {
1269 assert_eq!(parse_time("24:00:00").unwrap(), MICROS_PER_DAY);
1270 }
1271
1272 #[test]
1273 fn parse_timestamp_iso() {
1274 let t = parse_timestamp("2024-01-15T12:30:45Z").unwrap();
1275 assert_eq!(format_timestamp(t), "2024-01-15 12:30:45");
1276 }
1277
1278 #[test]
1279 fn parse_timestamp_naive() {
1280 let t1 = parse_timestamp("2024-01-15 12:30:45").unwrap();
1281 let t2 = parse_timestamp("2024-01-15T12:30:45").unwrap();
1282 assert_eq!(t1, t2);
1283 }
1284
1285 #[test]
1286 fn parse_timestamp_infinity() {
1287 assert_eq!(parse_timestamp("infinity").unwrap(), TS_INFINITY_MICROS);
1288 }
1289
1290 #[test]
1291 fn parse_timestamp_bc() {
1292 let ad = parse_timestamp("0001-01-01 00:00:00").unwrap();
1294 let bc = parse_timestamp("0001-12-31 00:00:00 BC").unwrap();
1295 assert_eq!(ad - bc, MICROS_PER_DAY);
1296 }
1297
1298 #[test]
1299 fn parse_timestamp_rejects_year_0() {
1300 assert!(parse_timestamp("0000-06-15 12:00:00").is_err());
1301 }
1302
1303 #[test]
1304 fn parse_interval_pg_verbose() {
1305 let (m, d, us) = parse_interval("1 year 2 months 3 days").unwrap();
1306 assert_eq!((m, d, us), (14, 3, 0));
1307 }
1308
1309 #[test]
1310 fn parse_interval_with_hms() {
1311 let (m, d, us) = parse_interval("3 days 04:05:06.789").unwrap();
1312 assert_eq!(m, 0);
1313 assert_eq!(d, 3);
1314 let expected_us = 4 * MICROS_PER_HOUR + 5 * MICROS_PER_MIN + 6 * MICROS_PER_SEC + 789000;
1315 assert_eq!(us, expected_us);
1316 }
1317
1318 #[test]
1319 fn parse_interval_iso8601() {
1320 let (m, d, us) = parse_interval("P1Y2M3DT4H5M6S").unwrap();
1321 assert_eq!(m, 14);
1322 assert_eq!(d, 3);
1323 assert_eq!(
1324 us,
1325 4 * MICROS_PER_HOUR + 5 * MICROS_PER_MIN + 6 * MICROS_PER_SEC
1326 );
1327 }
1328
1329 #[test]
1330 fn format_interval_zero() {
1331 assert_eq!(format_interval(0, 0, 0), "00:00:00");
1332 }
1333
1334 #[test]
1335 fn format_interval_mixed() {
1336 assert_eq!(
1337 format_interval(
1338 14,
1339 3,
1340 4 * MICROS_PER_HOUR + 5 * MICROS_PER_MIN + 6 * MICROS_PER_SEC
1341 ),
1342 "1 year 2 mons 3 days 04:05:06"
1343 );
1344 }
1345
1346 #[test]
1347 fn add_interval_month_clamp() {
1348 let jan31 = parse_date("2024-01-31").unwrap();
1350 let ts = add_interval_to_date(jan31, 1, 0, 0).unwrap();
1351 let (d, _t) = ts_split(ts);
1352 let (y, mo, da) = days_to_ymd(d);
1353 assert_eq!((y, mo, da), (2024, 2, 29));
1354 }
1355
1356 #[test]
1357 fn add_interval_month_clamp_non_leap() {
1358 let jan31 = parse_date("2023-01-31").unwrap();
1359 let ts = add_interval_to_date(jan31, 1, 0, 0).unwrap();
1360 let (d, _t) = ts_split(ts);
1361 let (y, mo, da) = days_to_ymd(d);
1362 assert_eq!((y, mo, da), (2023, 2, 28));
1363 }
1364
1365 #[test]
1366 fn interval_normalized_compare() {
1367 let a = (1i32, 0i32, 0i64);
1369 let b = (0i32, 30i32, 0i64);
1370 assert_eq!(pg_normalized_interval_cmp(a, b), std::cmp::Ordering::Equal);
1371 }
1372
1373 #[test]
1374 fn justify_days_basic() {
1375 let (m, d, us) = justify_days(0, 65, 0);
1376 assert_eq!((m, d, us), (2, 5, 0));
1377 }
1378
1379 #[test]
1380 fn justify_hours_basic() {
1381 let (m, d, us) = justify_hours(0, 0, 50 * MICROS_PER_HOUR + 10 * MICROS_PER_MIN);
1382 assert_eq!(
1383 (m, d, us),
1384 (0, 2, 2 * MICROS_PER_HOUR + 10 * MICROS_PER_MIN)
1385 );
1386 }
1387
1388 #[test]
1389 fn time_add_wrap() {
1390 let t = parse_time("23:00:00").unwrap();
1391 let result = add_interval_to_time(t, 0, 0, 2 * MICROS_PER_HOUR).unwrap();
1392 assert_eq!(format_time(result), "01:00:00");
1393 }
1394
1395 #[test]
1396 fn time_add_rejects_days() {
1397 let t = parse_time("12:00:00").unwrap();
1398 assert!(add_interval_to_time(t, 0, 1, 0).is_err());
1399 }
1400
1401 #[test]
1402 fn subtract_timestamps_basic() {
1403 let a = parse_timestamp("2024-01-02 12:00:00").unwrap();
1404 let b = parse_timestamp("2024-01-01 00:00:00").unwrap();
1405 let (days, micros) = subtract_timestamps(a, b);
1406 assert_eq!(days, 1);
1407 assert_eq!(micros, 12 * MICROS_PER_HOUR);
1408 }
1409
1410 #[test]
1411 fn ts_to_date_floor_pre_epoch() {
1412 assert_eq!(ts_to_date_floor(-1), -1);
1413 assert_eq!(ts_to_date_floor(0), 0);
1414 assert_eq!(ts_to_date_floor(MICROS_PER_DAY - 1), 0);
1415 assert_eq!(ts_to_date_floor(MICROS_PER_DAY), 1);
1416 }
1417
1418 #[test]
1419 fn extract_year_from_date() {
1420 let d = parse_date("2024-03-15").unwrap();
1421 assert_eq!(
1422 extract("year", &Value::Date(d)).unwrap(),
1423 Value::Integer(2024)
1424 );
1425 }
1426
1427 #[test]
1428 fn extract_dow_sunday() {
1429 let d = parse_date("2024-01-07").unwrap();
1431 assert_eq!(extract("dow", &Value::Date(d)).unwrap(), Value::Integer(0));
1432 assert_eq!(
1433 extract("isodow", &Value::Date(d)).unwrap(),
1434 Value::Integer(7)
1435 );
1436 }
1437
1438 #[test]
1439 fn date_trunc_month() {
1440 let ts = parse_timestamp("2024-03-15 12:30:45").unwrap();
1441 let result = date_trunc("month", &Value::Timestamp(ts)).unwrap();
1442 if let Value::Timestamp(t) = result {
1443 assert_eq!(format_timestamp(t), "2024-03-01 00:00:00");
1444 } else {
1445 panic!("expected Timestamp");
1446 }
1447 }
1448
1449 #[test]
1450 fn date_trunc_week_monday() {
1451 let d = parse_date("2024-01-07").unwrap();
1453 let Value::Date(trunc) = date_trunc("week", &Value::Date(d)).unwrap() else {
1454 panic!("expected Date");
1455 };
1456 assert_eq!(format_date(trunc), "2024-01-01");
1457 }
1458
1459 #[test]
1460 fn age_basic() {
1461 let a = parse_timestamp("2024-04-10 00:00:00").unwrap();
1462 let b = parse_timestamp("2024-01-01 00:00:00").unwrap();
1463 let (m, d, us) = age(a, b).unwrap();
1464 assert_eq!(m, 3);
1466 assert_eq!(d, 9);
1467 assert_eq!(us, 0);
1468 }
1469
1470 #[test]
1471 fn strftime_basic() {
1472 let ts = parse_timestamp("2024-03-15 12:30:45").unwrap();
1473 let s = strftime("%Y-%m-%d", &Value::Timestamp(ts)).unwrap();
1474 assert_eq!(s, "2024-03-15");
1475 }
1476
1477 #[test]
1478 fn strftime_unix_epoch() {
1479 let ts = parse_timestamp("2024-01-01 00:00:00").unwrap();
1480 let s = strftime("%s", &Value::Timestamp(ts)).unwrap();
1481 assert_eq!(s, (ts / MICROS_PER_SEC).to_string());
1482 }
1483
1484 #[test]
1485 fn is_finite_temporal_sentinels() {
1486 assert!(!Value::Date(i32::MAX).is_finite_temporal());
1487 assert!(!Value::Date(i32::MIN).is_finite_temporal());
1488 assert!(Value::Date(0).is_finite_temporal());
1489 assert!(!Value::Timestamp(i64::MAX).is_finite_temporal());
1490 assert!(Value::Timestamp(0).is_finite_temporal());
1491 }
1492
1493 #[test]
1494 fn add_interval_infinity() {
1495 let result = add_interval_to_timestamp(TS_INFINITY_MICROS, 1, 1, 0).unwrap();
1496 assert_eq!(result, TS_INFINITY_MICROS);
1497 }
1498
1499 #[test]
1500 fn format_date_bc() {
1501 let bc1 = parse_date("0001-01-01 BC").unwrap();
1503 assert_eq!(format_date(bc1), "0001-01-01 BC");
1504 }
1505}