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;
14pub const MICROS_PER_MIN: i64 = 60 * MICROS_PER_SEC;
15pub const MICROS_PER_HOUR: i64 = 60 * MICROS_PER_MIN;
16pub const MICROS_PER_DAY: i64 = 24 * MICROS_PER_HOUR;
17
18pub const DATE_INFINITY_DAYS: i32 = i32::MAX;
20pub const DATE_NEG_INFINITY_DAYS: i32 = i32::MIN;
21pub const TS_INFINITY_MICROS: i64 = i64::MAX;
22pub const TS_NEG_INFINITY_MICROS: i64 = i64::MIN;
23
24pub fn is_infinity_date(d: i32) -> bool {
25 d == DATE_INFINITY_DAYS || d == DATE_NEG_INFINITY_DAYS
26}
27
28pub fn is_infinity_ts(t: i64) -> bool {
29 t == TS_INFINITY_MICROS || t == TS_NEG_INFINITY_MICROS
30}
31
32fn epoch_date() -> JDate {
34 JDate::new(1970, 1, 1).expect("1970-01-01 is a valid date")
35}
36
37pub fn days_to_ymd(days: i32) -> (i32, u8, u8) {
39 let epoch = epoch_date();
40 let d = epoch.checked_add((days as i64).days()).unwrap_or(epoch);
41 (d.year() as i32, d.month() as u8, d.day() as u8)
42}
43
44pub fn ymd_to_days(y: i32, m: u8, d: u8) -> Option<i32> {
46 let date = JDate::new(y as i16, m as i8, d as i8).ok()?;
47 let span = date.since((Unit::Day, epoch_date())).ok()?;
48 let days = span.get_days() as i64;
49 if (i32::MIN as i64..=i32::MAX as i64).contains(&days) {
50 Some(days as i32)
51 } else {
52 None
53 }
54}
55
56pub fn micros_to_hmsn(micros: i64) -> (u8, u8, u8, u32) {
58 let hour = (micros / MICROS_PER_HOUR) as u8;
59 let rem = micros % MICROS_PER_HOUR;
60 let min = (rem / MICROS_PER_MIN) as u8;
61 let rem = rem % MICROS_PER_MIN;
62 let sec = (rem / MICROS_PER_SEC) as u8;
63 let subsec = (rem % MICROS_PER_SEC) as u32;
64 (hour, min, sec, subsec)
65}
66
67pub fn hmsn_to_micros(h: u8, m: u8, s: u8, us: u32) -> Option<i64> {
69 let total = (h as i64) * MICROS_PER_HOUR
70 + (m as i64) * MICROS_PER_MIN
71 + (s as i64) * MICROS_PER_SEC
72 + us as i64;
73 if (0..=MICROS_PER_DAY).contains(&total) {
74 Some(total)
75 } else {
76 None
77 }
78}
79
80pub fn ts_split(micros: i64) -> (i32, i64) {
82 let days = micros.div_euclid(MICROS_PER_DAY);
83 let rem = micros.rem_euclid(MICROS_PER_DAY);
84 (days as i32, rem)
85}
86
87pub fn ts_combine(date_days: i32, time_micros: i64) -> i64 {
89 (date_days as i64) * MICROS_PER_DAY + time_micros
90}
91
92pub fn date_to_ts(days: i32) -> i64 {
94 (days as i64).saturating_mul(MICROS_PER_DAY)
95}
96
97pub fn ts_to_date_floor(micros: i64) -> i32 {
99 if micros == TS_INFINITY_MICROS {
100 DATE_INFINITY_DAYS
101 } else if micros == TS_NEG_INFINITY_MICROS {
102 DATE_NEG_INFINITY_DAYS
103 } else {
104 micros.div_euclid(MICROS_PER_DAY) as i32
105 }
106}
107
108pub fn parse_date(s: &str) -> Result<i32> {
110 let trimmed = s.trim();
111 let lower = trimmed.to_ascii_lowercase();
112 if lower == "infinity" || lower == "+infinity" {
113 return Ok(DATE_INFINITY_DAYS);
114 }
115 if lower == "-infinity" {
116 return Ok(DATE_NEG_INFINITY_DAYS);
117 }
118
119 let (body, is_bc) = if let Some(stripped) = trimmed.strip_suffix(" BC") {
121 (stripped.trim(), true)
122 } else if let Some(stripped) = trimmed.strip_suffix(" bc") {
123 (stripped.trim(), true)
124 } else {
125 (trimmed, false)
126 };
127
128 let d = JDate::strptime("%Y-%m-%d", body)
129 .map_err(|e| SqlError::InvalidDateLiteral(format!("{body}: {e}")))?;
130 if d.year() == 0 {
131 return Err(SqlError::InvalidDateLiteral(
132 "year 0 is not supported; use '0001-01-01 BC' for 1 BC".into(),
133 ));
134 }
135 let year_adjusted = if is_bc {
136 -(d.year() as i32 - 1)
137 } else {
138 d.year() as i32
139 };
140 let canonical = JDate::new(year_adjusted as i16, d.month(), d.day())
141 .map_err(|e| SqlError::InvalidDateLiteral(format!("{body}: {e}")))?;
142 let span = canonical
143 .since((Unit::Day, epoch_date()))
144 .map_err(|e| SqlError::InvalidDateLiteral(format!("{body}: {e}")))?;
145 let days = span.get_days() as i64;
146 if (i32::MIN as i64..=i32::MAX as i64).contains(&days) {
147 Ok(days as i32)
148 } else {
149 Err(SqlError::InvalidDateLiteral(format!(
150 "{body}: date out of i32 range"
151 )))
152 }
153}
154
155pub fn parse_time(s: &str) -> Result<i64> {
157 let trimmed = s.trim();
158 if trimmed == "24:00:00" || trimmed == "24:00:00.000000" {
160 return Ok(MICROS_PER_DAY);
161 }
162 let t = JTime::strptime("%H:%M:%S%.f", trimmed)
163 .or_else(|_| JTime::strptime("%H:%M:%S", trimmed))
164 .or_else(|_| JTime::strptime("%H:%M", trimmed))
165 .map_err(|e| SqlError::InvalidTimeLiteral(format!("{trimmed}: {e}")))?;
166 let subsec_micros = (t.subsec_nanosecond() / 1000) as u32;
167 hmsn_to_micros(
168 t.hour() as u8,
169 t.minute() as u8,
170 t.second() as u8,
171 subsec_micros,
172 )
173 .ok_or_else(|| SqlError::InvalidTimeLiteral(format!("{trimmed}: out of range")))
174}
175
176pub fn parse_timestamp(s: &str) -> Result<i64> {
180 let trimmed = s.trim();
181 let lower = trimmed.to_ascii_lowercase();
182 if lower == "infinity" || lower == "+infinity" {
183 return Ok(TS_INFINITY_MICROS);
184 }
185 if lower == "-infinity" {
186 return Ok(TS_NEG_INFINITY_MICROS);
187 }
188
189 let (body, is_bc) = if let Some(stripped) = trimmed.strip_suffix(" BC") {
191 (stripped.trim_end(), true)
192 } else if let Some(stripped) = trimmed.strip_suffix(" bc") {
193 (stripped.trim_end(), true)
194 } else {
195 (trimmed, false)
196 };
197
198 if !is_bc {
200 if let Ok(z) = body.parse::<Zoned>() {
201 reject_year_zero_ts(z.timestamp().as_microsecond())?;
202 return Ok(z.timestamp().as_microsecond());
203 }
204 if let Ok(ts) = body.parse::<JTimestamp>() {
206 reject_year_zero_ts(ts.as_microsecond())?;
207 return Ok(ts.as_microsecond());
208 }
209 }
210
211 let parsers = [
214 "%Y-%m-%d %H:%M:%S%.f",
215 "%Y-%m-%dT%H:%M:%S%.f",
216 "%Y-%m-%d %H:%M:%S",
217 "%Y-%m-%dT%H:%M:%S",
218 "%Y-%m-%d %H:%M",
219 "%Y-%m-%dT%H:%M",
220 "%Y-%m-%d",
221 ];
222 for fmt in &parsers {
223 if let Ok(dt) = JDateTime::strptime(fmt, body) {
224 let adjusted = apply_bc_and_check_year_zero(dt, is_bc, body)?;
225 return adjusted
226 .to_zoned(TimeZone::UTC)
227 .map(|z| z.timestamp().as_microsecond())
228 .map_err(|e| SqlError::InvalidTimestampLiteral(format!("{body}: {e}")));
229 }
230 }
231 if !is_bc {
233 if let Some(space_idx) = body.rfind(' ') {
234 let (ts_part, zone_part) = body.split_at(space_idx);
235 let zone_name = zone_part.trim();
236 if let Ok(tz) = TimeZone::get(zone_name) {
237 for fmt in &parsers {
238 if let Ok(dt) = JDateTime::strptime(fmt, ts_part.trim()) {
239 if dt.year() == 0 {
240 return Err(SqlError::InvalidTimestampLiteral(
241 "year 0 is not supported; use 'YYYY-MM-DD HH:MM:SS BC' for 1 BC"
242 .into(),
243 ));
244 }
245 return dt
246 .to_zoned(tz.clone())
247 .map(|z| z.timestamp().as_microsecond())
248 .map_err(|e| {
249 SqlError::InvalidTimestampLiteral(format!("{body}: {e}"))
250 });
251 }
252 }
253 }
254 }
255 }
256 Err(SqlError::InvalidTimestampLiteral(format!(
257 "{trimmed}: unrecognized timestamp format"
258 )))
259}
260
261fn reject_year_zero_ts(micros: i64) -> Result<()> {
262 let date_days = ts_to_date_floor(micros);
263 let (y, _, _) = days_to_ymd(date_days);
264 if y == 0 {
265 return Err(SqlError::InvalidTimestampLiteral(
266 "year 0 is not supported; use 'YYYY-MM-DD HH:MM:SS BC' for 1 BC".into(),
267 ));
268 }
269 Ok(())
270}
271
272fn apply_bc_and_check_year_zero(dt: JDateTime, is_bc: bool, body: &str) -> Result<JDateTime> {
273 if dt.year() == 0 {
274 return Err(SqlError::InvalidTimestampLiteral(
275 "year 0 is not supported; use 'YYYY-MM-DD HH:MM:SS BC' for 1 BC".into(),
276 ));
277 }
278 if !is_bc {
279 return Ok(dt);
280 }
281 let astro_year = -(dt.year() as i32 - 1);
283 let date = JDate::new(astro_year as i16, dt.month(), dt.day())
284 .map_err(|e| SqlError::InvalidTimestampLiteral(format!("{body}: {e}")))?;
285 let time = JTime::new(dt.hour(), dt.minute(), dt.second(), dt.subsec_nanosecond())
286 .map_err(|e| SqlError::InvalidTimestampLiteral(format!("{body}: {e}")))?;
287 Ok(JDateTime::from_parts(date, time))
288}
289
290pub fn parse_interval(s: &str) -> Result<(i32, i32, i64)> {
293 let trimmed = s.trim();
294 if trimmed.is_empty() {
295 return Err(SqlError::InvalidIntervalLiteral("empty interval".into()));
296 }
297
298 if let Some(rest) = trimmed
300 .strip_prefix('P')
301 .or_else(|| trimmed.strip_prefix('-').and_then(|r| r.strip_prefix('P')))
302 {
303 let negate = trimmed.starts_with('-');
304 return parse_iso8601_duration(rest, negate);
305 }
306
307 parse_pg_interval(trimmed)
309}
310
311fn parse_iso8601_duration(s: &str, global_negate: bool) -> Result<(i32, i32, i64)> {
312 let mut months: i64 = 0;
314 let mut days: i64 = 0;
315 let mut micros: i64 = 0;
316 let mut in_time = false;
317 let mut num_buf = String::new();
318 let sign = if global_negate { -1i64 } else { 1 };
319
320 for ch in s.chars() {
321 if ch == 'T' {
322 in_time = true;
323 continue;
324 }
325 if ch.is_ascii_digit() || ch == '.' || ch == '-' {
326 num_buf.push(ch);
327 continue;
328 }
329 if num_buf.is_empty() {
330 return Err(SqlError::InvalidIntervalLiteral(format!(
331 "expected number before '{ch}'"
332 )));
333 }
334 let v: f64 = num_buf
335 .parse()
336 .map_err(|_| SqlError::InvalidIntervalLiteral(format!("invalid number: {num_buf}")))?;
337 num_buf.clear();
338 let v_units = sign * v as i64;
339 let v_frac_micros = ((v.fract() * 1_000_000.0) as i64) * sign;
340 match ch {
341 'Y' if !in_time => months = months.saturating_add(v_units * 12),
342 'M' if !in_time => months = months.saturating_add(v_units),
343 'W' if !in_time => days = days.saturating_add(v_units * 7),
344 'D' if !in_time => days = days.saturating_add(v_units),
345 'H' if in_time => micros = micros.saturating_add(v_units * MICROS_PER_HOUR),
346 'M' if in_time => micros = micros.saturating_add(v_units * MICROS_PER_MIN),
347 'S' if in_time => {
348 micros = micros.saturating_add(v_units * MICROS_PER_SEC + v_frac_micros)
349 }
350 _ => {
351 return Err(SqlError::InvalidIntervalLiteral(format!(
352 "unknown unit '{ch}' (in_time={in_time})"
353 )))
354 }
355 }
356 }
357 if !num_buf.is_empty() {
358 return Err(SqlError::InvalidIntervalLiteral(format!(
359 "trailing number without unit: {num_buf}"
360 )));
361 }
362 Ok((clamp_i32(months)?, clamp_i32(days)?, micros))
363}
364
365fn parse_pg_interval(s: &str) -> Result<(i32, i32, i64)> {
366 let mut s = s.trim().to_ascii_lowercase();
367 if let Some(rest) = s.strip_prefix('@') {
368 s = rest.trim().to_string();
369 }
370 let ago = s.ends_with(" ago");
371 if ago {
372 s.truncate(s.len() - 4);
373 s = s.trim().to_string();
374 }
375 let sign: i64 = if ago { -1 } else { 1 };
376
377 let mut months: i64 = 0;
378 let mut days: i64 = 0;
379 let mut micros: i64 = 0;
380
381 let tokens: Vec<&str> = s.split_whitespace().collect();
382 let mut i = 0;
383 while i < tokens.len() {
384 let tok = tokens[i];
385 if tok.contains(':') {
387 let (h, m, sc, us) = parse_hms_token(tok)?;
388 let tok_sign = if tok.starts_with('-') { -1 } else { 1 };
389 let tok_micros =
390 (h * MICROS_PER_HOUR + m * MICROS_PER_MIN + sc * MICROS_PER_SEC + us as i64)
391 * tok_sign
392 * sign;
393 micros = micros.saturating_add(tok_micros);
394 i += 1;
395 continue;
396 }
397
398 let num: f64 = tok.parse().map_err(|_| {
400 SqlError::InvalidIntervalLiteral(format!("expected number, got '{tok}'"))
401 })?;
402 if i + 1 >= tokens.len() {
403 return Err(SqlError::InvalidIntervalLiteral(format!(
404 "missing unit after '{tok}'"
405 )));
406 }
407 let unit = tokens[i + 1].trim_end_matches(',');
408 let v_units = sign * num as i64;
409 let v_frac_micros = ((num.fract() * 1_000_000.0) as i64) * sign;
410 match unit {
411 "year" | "years" | "yr" | "yrs" | "y" => months = months.saturating_add(v_units * 12),
412 "month" | "months" | "mon" | "mons" => months = months.saturating_add(v_units),
413 "week" | "weeks" | "w" => days = days.saturating_add(v_units * 7),
414 "day" | "days" | "d" => days = days.saturating_add(v_units),
415 "hour" | "hours" | "hr" | "hrs" | "h" => {
416 micros = micros.saturating_add(v_units * MICROS_PER_HOUR)
417 }
418 "minute" | "minutes" | "min" | "mins" | "m" => {
419 micros = micros.saturating_add(v_units * MICROS_PER_MIN)
420 }
421 "second" | "seconds" | "sec" | "secs" | "s" => {
422 micros = micros.saturating_add(v_units * MICROS_PER_SEC + v_frac_micros)
423 }
424 "millisecond" | "milliseconds" | "ms" => micros = micros.saturating_add(v_units * 1000),
425 "microsecond" | "microseconds" | "us" => micros = micros.saturating_add(v_units),
426 other => {
427 return Err(SqlError::InvalidIntervalLiteral(format!(
428 "unknown unit: {other}"
429 )))
430 }
431 }
432 i += 2;
433 }
434 Ok((clamp_i32(months)?, clamp_i32(days)?, micros))
435}
436
437fn parse_hms_token(tok: &str) -> Result<(i64, i64, i64, u32)> {
438 let tok = tok.trim_start_matches('-').trim_start_matches('+');
439 let mut parts = tok.split(':');
440 let h: i64 = parts
441 .next()
442 .ok_or_else(|| SqlError::InvalidIntervalLiteral(format!("bad hms: {tok}")))?
443 .parse()
444 .map_err(|_| SqlError::InvalidIntervalLiteral(format!("bad hour: {tok}")))?;
445 let m: i64 = parts
446 .next()
447 .ok_or_else(|| SqlError::InvalidIntervalLiteral(format!("bad hms: {tok}")))?
448 .parse()
449 .map_err(|_| SqlError::InvalidIntervalLiteral(format!("bad minute: {tok}")))?;
450 let (sc, us) = if let Some(sec_part) = parts.next() {
451 if let Some((s_whole, s_frac)) = sec_part.split_once('.') {
452 let s: i64 = s_whole
453 .parse()
454 .map_err(|_| SqlError::InvalidIntervalLiteral(format!("bad second: {tok}")))?;
455 let mut frac = s_frac.to_string();
457 while frac.len() < 6 {
458 frac.push('0');
459 }
460 frac.truncate(6);
461 let us: u32 = frac
462 .parse()
463 .map_err(|_| SqlError::InvalidIntervalLiteral(format!("bad subsec: {tok}")))?;
464 (s, us)
465 } else {
466 let s: i64 = sec_part
467 .parse()
468 .map_err(|_| SqlError::InvalidIntervalLiteral(format!("bad second: {tok}")))?;
469 (s, 0u32)
470 }
471 } else {
472 (0, 0u32)
473 };
474 Ok((h, m, sc, us))
475}
476
477fn clamp_i32(n: i64) -> Result<i32> {
478 if (i32::MIN as i64..=i32::MAX as i64).contains(&n) {
479 Ok(n as i32)
480 } else {
481 Err(SqlError::InvalidIntervalLiteral(format!(
482 "interval component overflow: {n}"
483 )))
484 }
485}
486
487pub fn format_date(days: i32) -> String {
488 if days == DATE_INFINITY_DAYS {
489 return "infinity".to_string();
490 }
491 if days == DATE_NEG_INFINITY_DAYS {
492 return "-infinity".to_string();
493 }
494 let (y, m, d) = days_to_ymd(days);
495 if y >= 1 {
496 format!("{y:04}-{m:02}-{d:02}")
497 } else {
498 format!("{:04}-{m:02}-{d:02} BC", 1 - y)
500 }
501}
502
503pub fn format_time(micros: i64) -> String {
504 if micros == MICROS_PER_DAY {
505 return "24:00:00".to_string();
506 }
507 let (h, m, s, us) = micros_to_hmsn(micros);
508 if us == 0 {
509 format!("{h:02}:{m:02}:{s:02}")
510 } else {
511 format!("{h:02}:{m:02}:{s:02}.{us:06}")
512 }
513}
514
515pub fn format_timestamp(micros: i64) -> String {
516 if micros == TS_INFINITY_MICROS {
517 return "infinity".to_string();
518 }
519 if micros == TS_NEG_INFINITY_MICROS {
520 return "-infinity".to_string();
521 }
522 let (date_days, time_micros) = ts_split(micros);
523 let date_part = format_date(date_days);
524 let time_part = format_time(time_micros);
525 format!("{date_part} {time_part}")
526}
527
528pub fn format_timestamp_in_zone(micros: i64, zone: &str) -> Result<String> {
529 if micros == TS_INFINITY_MICROS {
530 return Ok("infinity".to_string());
531 }
532 if micros == TS_NEG_INFINITY_MICROS {
533 return Ok("-infinity".to_string());
534 }
535 let tz = resolve_timezone(zone)?;
536 let ts = JTimestamp::from_microsecond(micros)
537 .map_err(|e| SqlError::InvalidTimestampLiteral(format!("{micros}: {e}")))?;
538 let z = ts.to_zoned(tz);
539 let subsec = z.subsec_nanosecond() / 1000;
540 let fmt = if subsec == 0 {
541 "%Y-%m-%d %H:%M:%S%:z"
542 } else {
543 "%Y-%m-%d %H:%M:%S%.6f%:z"
544 };
545 z.strftime(fmt).to_string().pipe(Ok)
546}
547
548pub fn resolve_timezone(zone: &str) -> Result<TimeZone> {
551 let trimmed = zone.trim();
552 if let Ok(tz) = TimeZone::get(trimmed) {
553 return Ok(tz);
554 }
555 if let Some(offset) = parse_iso_fixed_offset(trimmed) {
556 return jiff::tz::Offset::from_seconds(offset)
557 .map(TimeZone::fixed)
558 .map_err(|e| SqlError::InvalidTimezone(format!("{zone}: {e}")));
559 }
560 let lower = trimmed.to_ascii_lowercase();
561 if lower.starts_with("utc+")
562 || lower.starts_with("utc-")
563 || lower.starts_with("gmt+")
564 || lower.starts_with("gmt-")
565 {
566 return Err(SqlError::InvalidTimezone(format!(
567 "{zone}: ambiguous POSIX form; use ISO-8601 offset like '+05:00' or a named zone"
568 )));
569 }
570 Err(SqlError::InvalidTimezone(format!(
571 "{zone}: not a recognized IANA name or ISO-8601 offset"
572 )))
573}
574
575fn parse_iso_fixed_offset(s: &str) -> Option<i32> {
577 if s.eq_ignore_ascii_case("z") || s.eq_ignore_ascii_case("utc") {
578 return Some(0);
579 }
580 let bytes = s.as_bytes();
581 if bytes.is_empty() {
582 return None;
583 }
584 let sign: i32 = match bytes[0] {
585 b'+' => 1,
586 b'-' => -1,
587 _ => return None,
588 };
589 let rest = &s[1..];
590 let (hh, mm) = if let Some((h, m)) = rest.split_once(':') {
591 (h, m)
592 } else if rest.len() == 4 {
593 (&rest[..2], &rest[2..])
594 } else if rest.len() == 2 {
595 (rest, "00")
596 } else {
597 return None;
598 };
599 let h: i32 = hh.parse().ok()?;
600 let m: i32 = mm.parse().ok()?;
601 if !(0..=23).contains(&h) || !(0..=59).contains(&m) {
602 return None;
603 }
604 Some(sign * (h * 3600 + m * 60))
605}
606
607pub fn format_interval(months: i32, days: i32, micros: i64) -> String {
608 if months == 0 && days == 0 && micros == 0 {
609 return "00:00:00".to_string();
610 }
611 let mut parts = Vec::with_capacity(4);
612 if months != 0 {
613 let years = months / 12;
614 let mon = months % 12;
615 if years != 0 {
616 parts.push(format!(
617 "{} year{}",
618 years,
619 if years.abs() == 1 { "" } else { "s" }
620 ));
621 }
622 if mon != 0 {
623 parts.push(format!(
624 "{} mon{}",
625 mon,
626 if mon.abs() == 1 { "" } else { "s" }
627 ));
628 }
629 }
630 if days != 0 {
631 parts.push(format!(
632 "{} day{}",
633 days,
634 if days.abs() == 1 { "" } else { "s" }
635 ));
636 }
637 if micros != 0 {
638 let sign = if micros < 0 { "-" } else { "" };
639 let abs_us = micros.unsigned_abs() as i64;
640 let (h, m, s, us) = micros_to_hmsn(abs_us);
641 if us == 0 {
642 parts.push(format!("{sign}{h:02}:{m:02}:{s:02}"));
643 } else {
644 parts.push(format!("{sign}{h:02}:{m:02}:{s:02}.{us:06}"));
645 }
646 }
647 parts.join(" ")
648}
649
650trait Pipe: Sized {
652 fn pipe<T>(self, f: impl FnOnce(Self) -> T) -> T {
653 f(self)
654 }
655}
656impl<T> Pipe for T {}
657
658pub fn now_micros() -> i64 {
659 JTimestamp::now().as_microsecond()
660}
661
662thread_local! {
663 static TXN_CLOCK: std::cell::Cell<Option<i64>> = const { std::cell::Cell::new(None) };
665}
666
667pub fn with_txn_clock<R>(ts: Option<i64>, f: impl FnOnce() -> R) -> R {
670 TXN_CLOCK.with(|slot| {
671 let prev = slot.replace(ts);
672 let r = f();
673 slot.set(prev);
674 r
675 })
676}
677
678pub fn set_txn_clock(ts: Option<i64>) {
679 TXN_CLOCK.with(|slot| slot.set(ts));
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)]
1190#[path = "datetime_tests.rs"]
1191mod tests;