1#![allow(
11 clippy::unnecessary_literal_bound,
12 clippy::too_many_lines,
13 clippy::cast_possible_truncation,
14 clippy::cast_possible_wrap,
15 clippy::cast_sign_loss,
16 clippy::cast_precision_loss,
17 clippy::items_after_statements,
18 clippy::match_same_arms,
19 clippy::float_cmp,
20 clippy::suboptimal_flops,
21 clippy::manual_let_else,
22 clippy::single_match_else,
23 clippy::unnecessary_wraps,
24 clippy::cognitive_complexity,
25 clippy::similar_names,
26 clippy::many_single_char_names,
27 clippy::unreadable_literal,
28 clippy::manual_range_contains,
29 clippy::range_plus_one,
30 clippy::format_push_string,
31 clippy::redundant_else
32)]
33
34use std::{
35 borrow::Cow,
36 fmt::{Arguments, Write as _},
37};
38
39use fsqlite_error::Result;
40use fsqlite_types::{SmallText, SqliteValue};
41
42use crate::{FunctionRegistry, ScalarFunction};
43
44#[cfg(not(target_arch = "wasm32"))]
59fn utc_offset_for_utc_datetime(y: i32, mo: u32, d: u32, h: u32, mi: u32, s: u32) -> i64 {
60 use chrono::{Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
61 let date = NaiveDate::from_ymd_opt(y, mo, d).unwrap_or_default();
62 let time = NaiveTime::from_hms_opt(h, mi, s).unwrap_or_default();
63 let naive = NaiveDateTime::new(date, time);
64 let utc_dt = Utc.from_utc_datetime(&naive);
65 let local_dt = utc_dt.with_timezone(&Local);
66 local_dt.offset().local_minus_utc() as i64
67}
68
69#[cfg(target_arch = "wasm32")]
70fn utc_offset_for_utc_datetime(_y: i32, _mo: u32, _d: u32, _h: u32, _mi: u32, _s: u32) -> i64 {
71 0
72}
73
74fn utc_offset_for_utc_jdn(jdn: f64) -> i64 {
76 let (y, mo, d) = jdn_to_ymd(jdn);
77 let (h, mi, s, _frac) = jdn_to_hms(jdn);
78 utc_offset_for_utc_datetime(y as i32, mo as u32, d as u32, h as u32, mi as u32, s as u32)
79}
80
81#[cfg(not(target_arch = "wasm32"))]
90fn local_jdn_to_utc_jdn(local_jdn: f64) -> f64 {
91 let orig = local_jdn;
92 let mut guess = orig;
93 let mut err = 0.0_f64;
94 let mut cnt = 0u32;
95 loop {
96 guess -= err;
97 let off = utc_offset_for_utc_jdn(guess);
100 let localized = guess + off as f64 / 86400.0;
101 err = localized - orig;
102 if err.abs() < 0.5 / 86400.0 || cnt >= 3 {
107 break;
108 }
109 cnt += 1;
110 }
111 guess
112}
113
114#[cfg(target_arch = "wasm32")]
115fn local_jdn_to_utc_jdn(local_jdn: f64) -> f64 {
116 local_jdn
118}
119
120fn ymd_to_jdn(y: i64, m: i64, d: i64) -> f64 {
126 let (y, m) = if m <= 2 {
127 (y.saturating_sub(1), m.saturating_add(12))
128 } else {
129 (y, m)
130 };
131 let a = y / 100;
132 let b = 2_i64.saturating_sub(a).saturating_add(a / 4);
133 (365.25 * y.saturating_add(4716) as f64).floor()
134 + (30.6001 * m.saturating_add(1) as f64).floor()
135 + d as f64
136 + b as f64
137 - 1524.5
138}
139
140fn jdn_to_ymd(jdn: f64) -> (i64, i64, i64) {
147 let z = (jdn + 0.5).floor() as i64;
153 let alpha = ((z as f64 - 1_867_216.25) / 36524.25) as i64;
154 let a = z
155 .saturating_add(1)
156 .saturating_add(alpha)
157 .saturating_sub(alpha / 4);
158 let b = a.saturating_add(1524);
159 let c = ((b as f64 - 122.1) / 365.25) as i64;
160 let d = (365.25 * c as f64) as i64;
161 let e = ((b.saturating_sub(d)) as f64 / 30.6001) as i64;
162
163 let day = b
164 .saturating_sub(d)
165 .saturating_sub((30.6001 * e as f64) as i64);
166 let month = if e < 14 {
167 e.saturating_sub(1)
168 } else {
169 e.saturating_sub(13)
170 };
171 let year = if month > 2 {
172 c.saturating_sub(4716)
173 } else {
174 c.saturating_sub(4715)
175 };
176 (year, month, day)
177}
178
179fn jdn_to_hms(jdn: f64) -> (i64, i64, i64, f64) {
181 let frac = jdn + 0.5 - (jdn + 0.5).floor();
182 let total_ms = (frac * 86_400_000.0).round() as i64;
184 let h = total_ms / 3_600_000;
185 let rem = total_ms % 3_600_000;
186 let m = rem / 60_000;
187 let rem = rem % 60_000;
188 let s = rem / 1000;
189 let ms_frac = (rem % 1000) as f64 / 1000.0;
190 (h, m, s, ms_frac)
191}
192
193fn ymdhms_to_jdn(y: i64, mo: i64, d: i64, h: i64, mi: i64, s: i64, frac: f64) -> f64 {
195 ymd_to_jdn(y, mo, d) + (h as f64 * 3600.0 + mi as f64 * 60.0 + s as f64 + frac) / 86400.0
196}
197
198const UNIX_EPOCH_JDN: f64 = 2_440_587.5;
200const AUTO_JDN_MAX: f64 = 5_373_484.499_999;
202const AUTO_UNIX_MIN: f64 = -210_866_760_000.0;
204const AUTO_UNIX_MAX: f64 = 253_402_300_799.0;
205
206fn jdn_to_unix_millis(jdn: f64) -> i64 {
207 ((jdn - UNIX_EPOCH_JDN) * 86_400_000.0).round() as i64
208}
209
210fn jdn_to_unix(jdn: f64) -> i64 {
211 jdn_to_unix_millis(jdn).div_euclid(1000)
215}
216
217fn jdn_to_unix_subsec(jdn: f64) -> f64 {
218 jdn_to_unix_millis(jdn) as f64 / 1000.0
219}
220
221fn unix_to_jdn(ts: f64) -> f64 {
222 ts / 86400.0 + UNIX_EPOCH_JDN
223}
224
225fn is_leap_year(y: i64) -> bool {
226 (y % 4 == 0 && y % 100 != 0) || y % 400 == 0
227}
228
229fn days_in_month(y: i64, m: i64) -> i64 {
230 match m {
231 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
232 4 | 6 | 9 | 11 => 30,
233 2 => {
234 if is_leap_year(y) {
235 29
236 } else {
237 28
238 }
239 }
240 _ => 30,
241 }
242}
243
244fn day_of_year(y: i64, m: i64, d: i64) -> i64 {
245 let mut doy = d;
246 for mo in 1..m {
247 doy = doy.saturating_add(days_in_month(y, mo));
248 }
249 doy
250}
251
252fn parse_timestring(s: &str) -> Option<f64> {
256 let s = sqlite_c_string_str(s);
259
260 if s.eq_ignore_ascii_case("now")
267 || s.eq_ignore_ascii_case("subsec")
268 || s.eq_ignore_ascii_case("subsecond")
269 {
270 return Some(current_time_jdn());
271 }
272
273 let numeric = s.trim_matches(|c: char| c.is_ascii_whitespace());
276 if let Ok(jdn) = numeric.parse::<f64>()
277 && jdn >= 0.0
278 && jdn.is_finite()
279 {
280 return Some(jdn);
281 }
282
283 parse_iso8601(s.trim_end_matches(|c: char| c.is_ascii_whitespace()))
286}
287
288fn current_time_jdn() -> f64 {
289 if let Some(cached) = crate::builtins::statement_now() {
295 return cached;
296 }
297 use fsqlite_types::sync_primitives::SystemTime;
298
299 let secs = SystemTime::now()
300 .duration_since(SystemTime::UNIX_EPOCH)
301 .unwrap_or_default()
302 .as_secs_f64();
303 let jdn = UNIX_EPOCH_JDN + secs / 86_400.0;
304 crate::builtins::set_statement_now(jdn);
305 jdn
306}
307
308fn sqlite_c_string_bytes(bytes: &[u8]) -> &[u8] {
309 bytes
310 .iter()
311 .position(|&byte| byte == 0)
312 .map_or(bytes, |nul| &bytes[..nul])
313}
314
315fn sqlite_c_string_str(text: &str) -> &str {
316 let bytes = sqlite_c_string_bytes(text.as_bytes());
317 std::str::from_utf8(bytes).unwrap_or("")
320}
321
322fn sqlite_value_datetime_text(value: &SqliteValue) -> Option<Cow<'_, str>> {
323 match value {
324 SqliteValue::Null => None,
325 SqliteValue::Text(text) => Some(Cow::Borrowed(sqlite_c_string_str(text))),
326 SqliteValue::Blob(bytes) => std::str::from_utf8(sqlite_c_string_bytes(bytes))
327 .ok()
328 .map(Cow::Borrowed),
329 SqliteValue::Integer(_) | SqliteValue::Float(_) => Some(Cow::Owned(value.to_text())),
330 }
331}
332
333fn parse_iso8601(s: &str) -> Option<f64> {
334 let bytes = s.as_bytes();
342 let len = bytes.len();
343
344 if len >= 10 && bytes[4] == b'-' && bytes[7] == b'-' {
346 let y = s[0..4].parse::<i64>().ok()?;
347 let m = s[5..7].parse::<i64>().ok()?;
348 let d = s[8..10].parse::<i64>().ok()?;
349
350 if m < 1 || m > 12 || d < 1 || d > 31 {
351 return None;
352 }
353
354 if len == 10 {
355 return Some(ymd_to_jdn(y, m, d));
356 }
357
358 if len > 10 && (bytes[10] == b' ' || bytes[10] == b'T') {
360 let time_part = &s[11..];
361 let (h, mi, sec, frac, tz_offset_min) = parse_time_part_with_tz(time_part)?;
362 let jdn = ymdhms_to_jdn(y, m, d, h, mi, sec, frac);
363 return Some(jdn - (tz_offset_min as f64) / 1440.0);
366 }
367 return None;
368 }
369
370 if len >= 5 && bytes[2] == b':' {
372 let (h, mi, sec, frac, tz_offset_min) = parse_time_part_with_tz(s)?;
373 let jdn = ymdhms_to_jdn(2000, 1, 1, h, mi, sec, frac);
374 return Some(jdn - (tz_offset_min as f64) / 1440.0);
375 }
376
377 None
378}
379
380fn split_tz_suffix(s: &str) -> Option<(&str, i64)> {
388 if let Some(stripped) = s.strip_suffix('Z').or_else(|| s.strip_suffix('z')) {
390 return Some((stripped, 0));
391 }
392
393 let bytes = s.as_bytes();
402 for width in [6usize, 5, 3] {
404 if bytes.len() < width + 1 {
405 continue;
406 }
407 let split_at = bytes.len() - width;
408 let sign_byte = bytes[split_at];
409 if sign_byte != b'+' && sign_byte != b'-' {
410 continue;
411 }
412 let tz_part = &s[split_at..];
413 if let Some(offset) = parse_tz_offset(tz_part) {
414 return Some((&s[..split_at], offset));
415 }
416 }
417
418 Some((s, 0))
420}
421
422fn parse_tz_offset(tz: &str) -> Option<i64> {
425 let bytes = tz.as_bytes();
426 if bytes.is_empty() {
427 return None;
428 }
429 let sign: i64 = match bytes[0] {
430 b'+' => 1,
431 b'-' => -1,
432 _ => return None,
433 };
434 let rest = &tz[1..];
435 let (hours, minutes) = match rest.len() {
436 5 if rest.as_bytes()[2] == b':' => (
438 rest[0..2].parse::<i64>().ok()?,
439 rest[3..5].parse::<i64>().ok()?,
440 ),
441 4 => (
443 rest[0..2].parse::<i64>().ok()?,
444 rest[2..4].parse::<i64>().ok()?,
445 ),
446 2 => (rest.parse::<i64>().ok()?, 0),
448 _ => return None,
449 };
450 if !(0..=23).contains(&hours) || !(0..=59).contains(&minutes) {
451 return None;
452 }
453 Some(sign * (hours * 60 + minutes))
454}
455
456fn parse_time_part_with_tz(s: &str) -> Option<(i64, i64, i64, f64, i64)> {
459 let (time_only, tz_offset_min) = split_tz_suffix(s)?;
460 let (h, mi, sec, frac) = parse_time_part(time_only)?;
461 Some((h, mi, sec, frac, tz_offset_min))
462}
463
464fn parse_time_part(s: &str) -> Option<(i64, i64, i64, f64)> {
466 let [h_tens, h_ones, b':', mi_tens, mi_ones, rest @ ..] = s.as_bytes() else {
467 return None;
468 };
469 let h = parse_two_ascii_digits(*h_tens, *h_ones)?;
474 let mi = parse_two_ascii_digits(*mi_tens, *mi_ones)?;
475 if !(0..=23).contains(&h) || !(0..=59).contains(&mi) {
476 return None;
477 }
478
479 match rest {
484 [] => Some((h, mi, 0, 0.0)),
485 [b':', sec_tens, sec_ones] => {
486 let sec = parse_two_ascii_digits(*sec_tens, *sec_ones)?;
487 if !(0..=59).contains(&sec) {
488 return None;
489 }
490 Some((h, mi, sec, 0.0))
491 }
492 [b':', sec_tens, sec_ones, b'.', ..] => {
493 let sec = parse_two_ascii_digits(*sec_tens, *sec_ones)?;
494 if !(0..=59).contains(&sec) {
495 return None;
496 }
497 let frac = s.get(8..)?.parse::<f64>().ok()?;
498 Some((h, mi, sec, frac))
499 }
500 _ => None,
501 }
502}
503
504#[inline]
505fn parse_two_ascii_digits(tens: u8, ones: u8) -> Option<i64> {
506 if tens.is_ascii_digit() && ones.is_ascii_digit() {
507 Some(i64::from((tens - b'0') * 10 + (ones - b'0')))
508 } else {
509 None
510 }
511}
512
513fn apply_modifier(jdn: f64, modifier: &str) -> Option<f64> {
517 if has_outer_ascii_whitespace(modifier) {
518 return None;
519 }
520 let m = modifier.to_ascii_lowercase();
521
522 if m == "start of month" {
524 let (y, mo, _d) = jdn_to_ymd(jdn);
525 return Some(ymd_to_jdn(y, mo, 1));
526 }
527 if m == "start of year" {
528 let (y, _mo, _d) = jdn_to_ymd(jdn);
529 return Some(ymd_to_jdn(y, 1, 1));
530 }
531 if m == "start of day" {
532 let (y, mo, d) = jdn_to_ymd(jdn);
533 return Some(ymd_to_jdn(y, mo, d));
534 }
535
536 if m == "unixepoch" {
538 return Some(unix_to_jdn(jdn));
539 }
540
541 if m == "julianday" {
544 return Some(jdn);
545 }
546
547 if m == "auto" {
552 if (0.0..=AUTO_JDN_MAX).contains(&jdn) {
553 return Some(jdn);
554 }
555 if (AUTO_UNIX_MIN..=AUTO_UNIX_MAX).contains(&jdn) {
556 return Some(unix_to_jdn(jdn));
557 }
558 return None;
559 }
560
561 if m == "localtime" {
564 let offset = utc_offset_for_utc_jdn(jdn);
565 return Some(jdn + offset as f64 / 86400.0);
566 }
567 if m == "utc" {
571 return Some(local_jdn_to_utc_jdn(jdn));
572 }
573
574 if m == "subsec" || m == "subsecond" {
577 return Some(jdn);
578 }
579
580 if let Some(rest) = m.strip_prefix("weekday ") {
582 let wd = rest.trim().parse::<i64>().ok()?;
583 if !(0..=6).contains(&wd) {
584 return None;
585 }
586 let current_jdn_int = (jdn + 0.5).floor() as i64;
588 let current_wd = (current_jdn_int + 1) % 7; let mut diff = wd - current_wd;
590 if diff < 0 {
591 diff += 7;
592 }
593 return Some(jdn + diff as f64);
595 }
596
597 parse_arithmetic_modifier(&m).map(|delta| jdn + delta)
599}
600
601fn parse_arithmetic_modifier(m: &str) -> Option<f64> {
605 let (sign, rest) = if let Some(r) = m.strip_prefix('+') {
606 (1.0, r.trim())
607 } else if let Some(r) = m.strip_prefix('-') {
608 (-1.0, r.trim())
609 } else {
610 (1.0, m.trim())
611 };
612
613 let mut parts = rest.splitn(2, ' ');
614 let num_str = parts.next()?;
615 let unit = parts.next()?.trim();
616
617 let num = num_str.parse::<f64>().ok().filter(|f| f.is_finite())?;
619 let delta = num * sign;
620
621 match unit.trim_end_matches('s') {
622 "day" => Some(delta),
623 "hour" => Some(delta / 24.0),
624 "minute" => Some(delta / 1440.0),
625 "second" => Some(delta / 86400.0),
626 "month" => Some(apply_month_delta(delta)),
627 "year" => Some(apply_month_delta(delta * 12.0)),
628 _ => None,
629 }
630}
631
632fn apply_month_delta(months: f64) -> f64 {
637 months * 30.436875
639}
640
641fn has_outer_ascii_whitespace(value: &str) -> bool {
642 value
643 .as_bytes()
644 .first()
645 .is_some_and(u8::is_ascii_whitespace)
646 || value.as_bytes().last().is_some_and(u8::is_ascii_whitespace)
647}
648
649fn compute_floor(y: i64, m: i64, d: i64) -> i64 {
653 if d <= 28 {
654 0
655 } else if ((1_i64 << m) & 0x15aa) != 0 {
656 0
658 } else if m != 2 {
659 i64::from(d == 31)
661 } else if y % 4 != 0 || (y % 100 == 0 && y % 400 != 0) {
662 d - 28 } else {
664 d - 29 }
666}
667
668fn apply_modifiers(jdn: f64, modifiers: &[String], mut raw_numeric: bool) -> Option<(f64, bool)> {
670 let mut j = jdn;
671 let mut subsec = false;
672 let mut n_floor: i64 = 0;
675 for (index, m) in modifiers.iter().enumerate() {
676 if has_outer_ascii_whitespace(m) {
677 return None;
678 }
679 let m_lower = m.to_ascii_lowercase();
680 if matches!(m_lower.as_str(), "unixepoch" | "julianday" | "auto") {
681 if index != 0 || !raw_numeric {
685 return None;
686 }
687 raw_numeric = false;
688 } else if m_lower != "subsec" && m_lower != "subsecond" {
689 raw_numeric = false;
690 }
691 if m_lower == "subsec" || m_lower == "subsecond" {
692 subsec = true;
693 continue;
694 }
695 if m_lower == "ceiling" {
700 n_floor = 0;
701 continue;
702 }
703 if m_lower == "floor" {
704 j -= n_floor as f64;
705 n_floor = 0;
706 continue;
707 }
708 if is_month_year_modifier(&m_lower) {
714 match apply_month_year_exact(j, &m_lower) {
715 Ok(Some((new_jdn, nf))) => {
716 j = new_jdn;
717 n_floor = nf;
718 continue;
719 }
720 Ok(None) => return None,
721 Err(()) => {
722 }
724 }
725 }
726 n_floor = 0;
728 j = apply_modifier(j, m)?;
729 }
730 Some((j, subsec))
731}
732
733fn is_month_year_modifier(m: &str) -> bool {
734 m.contains("month") || m.contains("year")
739}
740
741fn apply_month_year_exact(jdn: f64, m: &str) -> std::result::Result<Option<(f64, i64)>, ()> {
747 let (sign, rest) = if let Some(r) = m.strip_prefix('+') {
748 (1_i64, r.trim())
749 } else if let Some(r) = m.strip_prefix('-') {
750 (-1_i64, r.trim())
751 } else {
752 (1_i64, m.trim())
756 };
757
758 let mut parts = rest.splitn(2, ' ');
759 let num_str = parts.next().ok_or(())?;
760 let unit = parts.next().ok_or(())?.trim();
761
762 let (int_num, num_frac) = if let Ok(n) = num_str.parse::<i64>() {
770 (n, 0.0_f64)
771 } else if let Ok(f) = num_str.parse::<f64>() {
772 let int_part = f.trunc();
773 if !f.is_finite() || int_part < i64::MIN as f64 || int_part > i64::MAX as f64 {
774 return Err(());
775 }
776 (int_part as i64, f - int_part)
780 } else {
781 return Err(());
782 };
783
784 let (y, mo, d) = jdn_to_ymd(jdn);
785 let (h, mi, s, frac) = jdn_to_hms(jdn);
786
787 let unit = unit.trim_end_matches('s');
788 let (total_months, frac_days) = match unit {
789 "month" => {
790 let Some(months) = int_num.checked_mul(sign) else {
791 return Ok(None);
792 };
793 (months, num_frac * sign as f64 * 30.0)
794 }
795 "year" => {
796 let Some(months) = int_num.checked_mul(sign).and_then(|v| v.checked_mul(12)) else {
797 return Ok(None);
798 };
799 (months, num_frac * sign as f64 * 365.0)
800 }
801 _ => return Err(()),
802 };
803
804 let current_months = if let Some(val) = y.checked_mul(12).and_then(|v| v.checked_add(mo - 1)) {
806 val
807 } else {
808 return Ok(None);
809 };
810 let new_total = if let Some(val) = current_months.checked_add(total_months) {
811 val
812 } else {
813 return Ok(None);
814 };
815
816 let new_y = new_total.div_euclid(12);
817 let new_mo = new_total.rem_euclid(12) + 1;
818 let n_floor = compute_floor(new_y, new_mo, d);
823 Ok(Some((
824 ymdhms_to_jdn(new_y, new_mo, d, h, mi, s, frac) + frac_days,
825 n_floor,
826 )))
827}
828
829struct StackStr {
835 buf: [u8; 48],
836 len: usize,
837}
838
839impl StackStr {
840 fn new() -> Self {
841 Self {
842 buf: [0; 48],
843 len: 0,
844 }
845 }
846
847 fn as_str(&self) -> &str {
848 core::str::from_utf8(&self.buf[..self.len]).unwrap_or("")
850 }
851}
852
853impl core::fmt::Write for StackStr {
854 fn write_str(&mut self, s: &str) -> core::fmt::Result {
855 let end = self.len + s.len();
856 if end > self.buf.len() {
857 return Err(core::fmt::Error);
858 }
859 self.buf[self.len..end].copy_from_slice(s.as_bytes());
860 self.len = end;
861 Ok(())
862 }
863}
864
865fn build_small_text(write: impl Fn(&mut dyn core::fmt::Write) -> core::fmt::Result) -> SmallText {
872 let mut buf = StackStr::new();
873 if write(&mut buf).is_ok() {
874 SmallText::new(buf.as_str())
875 } else {
876 let mut heap = String::new();
877 let _ = write(&mut heap);
878 SmallText::from_string(heap)
879 }
880}
881
882fn write_year(w: &mut dyn core::fmt::Write, y: i64) -> core::fmt::Result {
888 if y < 0 {
889 write!(w, "-{:04}", y.unsigned_abs())
890 } else {
891 write!(w, "{y:04}")
892 }
893}
894
895fn format_date(jdn: f64) -> SmallText {
896 let (y, m, d) = jdn_to_ymd(jdn);
897 build_small_text(move |w| {
898 write_year(w, y)?;
899 write!(w, "-{m:02}-{d:02}")
900 })
901}
902
903#[derive(Clone, Copy)]
904struct UnmodifiedHms {
905 hour: i64,
906 minute: i64,
907 second: i64,
908 fraction: f64,
909}
910
911fn hms_for_output(jdn: f64, unmodified: Option<UnmodifiedHms>) -> UnmodifiedHms {
912 unmodified.unwrap_or_else(|| {
913 let (hour, minute, second, fraction) = jdn_to_hms(jdn);
914 UnmodifiedHms {
915 hour,
916 minute,
917 second,
918 fraction,
919 }
920 })
921}
922
923fn rounded_second_and_millis(hms: UnmodifiedHms) -> (i64, i64) {
924 let total_millis = ((hms.second as f64 + hms.fraction) * 1000.0 + 0.5).floor() as i64;
928 (total_millis / 1000, total_millis % 1000)
929}
930
931fn format_time(jdn: f64, subsec: bool, unmodified: Option<UnmodifiedHms>) -> SmallText {
932 let hms = hms_for_output(jdn, unmodified);
933 let (h, m) = (hms.hour, hms.minute);
934 if subsec {
935 let (s, ms) = rounded_second_and_millis(hms);
936 build_small_text(move |w| write!(w, "{h:02}:{m:02}:{s:02}.{ms:03}"))
937 } else {
938 let s = hms.second;
939 build_small_text(move |w| write!(w, "{h:02}:{m:02}:{s:02}"))
940 }
941}
942
943fn format_datetime(jdn: f64, subsec: bool, unmodified: Option<UnmodifiedHms>) -> SmallText {
944 let (y, mo, d) = jdn_to_ymd(jdn);
945 let hms = hms_for_output(jdn, unmodified);
946 let (h, mi) = (hms.hour, hms.minute);
947 if subsec {
948 let (s, ms) = rounded_second_and_millis(hms);
949 build_small_text(move |w| {
950 write_year(w, y)?;
951 write!(w, "-{mo:02}-{d:02} {h:02}:{mi:02}:{s:02}.{ms:03}")
952 })
953 } else {
954 let s = hms.second;
955 build_small_text(move |w| {
956 write_year(w, y)?;
957 write!(w, "-{mo:02}-{d:02} {h:02}:{mi:02}:{s:02}")
958 })
959 }
960}
961
962#[inline]
963fn push_format(result: &mut String, args: Arguments<'_>) {
964 let _ = result.write_fmt(args);
965}
966
967#[inline]
968fn push_zero_padded_2(result: &mut String, value: i64) {
969 if (0..=99).contains(&value) {
970 let value = value as u8;
971 result.push(char::from(b'0' + value / 10));
972 result.push(char::from(b'0' + value % 10));
973 } else {
974 push_format(result, format_args!("{value:02}"));
975 }
976}
977
978#[inline]
979fn push_space_padded_2(result: &mut String, value: i64) {
980 if (0..=99).contains(&value) {
981 let value = value as u8;
982 if value >= 10 {
983 result.push(char::from(b'0' + value / 10));
984 } else {
985 result.push(' ');
986 }
987 result.push(char::from(b'0' + value % 10));
988 } else {
989 push_format(result, format_args!("{value:>2}"));
990 }
991}
992
993#[inline]
994fn push_zero_padded_3(result: &mut String, value: i64) {
995 if (0..=999).contains(&value) {
996 let value = value as u16;
997 result.push(char::from(b'0' + (value / 100) as u8));
998 result.push(char::from(b'0' + ((value / 10) % 10) as u8));
999 result.push(char::from(b'0' + (value % 10) as u8));
1000 } else {
1001 push_format(result, format_args!("{value:03}"));
1002 }
1003}
1004
1005#[inline]
1006fn push_zero_padded_4(result: &mut String, value: i64) {
1007 if (0..=9999).contains(&value) {
1008 let value = value as u16;
1009 result.push(char::from(b'0' + (value / 1000) as u8));
1010 result.push(char::from(b'0' + ((value / 100) % 10) as u8));
1011 result.push(char::from(b'0' + ((value / 10) % 10) as u8));
1012 result.push(char::from(b'0' + (value % 10) as u8));
1013 } else {
1014 push_format(result, format_args!("{value:04}"));
1015 }
1016}
1017
1018fn format_strftime(
1020 fmt: &str,
1021 jdn: f64,
1022 subsec: bool,
1023 unmodified: Option<UnmodifiedHms>,
1024) -> Option<String> {
1025 let (y, mo, d) = jdn_to_ymd(jdn);
1026 let hms = hms_for_output(jdn, unmodified);
1027 let (h, mi, s, frac) = (hms.hour, hms.minute, hms.second, hms.fraction);
1028 let doy = day_of_year(y, mo, d);
1029 let jdn_int = (jdn + 0.5).floor() as i64;
1031 let dow = (jdn_int + 1) % 7; let mut result = String::with_capacity(fmt.len().saturating_add(8));
1034 let bytes = fmt.as_bytes();
1035 let mut i = 0;
1036 let mut literal_start = 0;
1037
1038 while i < bytes.len() {
1039 if bytes[i] != b'%' || i + 1 >= bytes.len() {
1040 i += 1;
1041 continue;
1042 }
1043
1044 result.push_str(&fmt[literal_start..i]);
1045
1046 let spec_suffix = &fmt[i + 1..];
1047 let Some(spec) = spec_suffix.chars().next() else {
1048 break;
1049 };
1050 i += 1 + spec.len_utf8();
1051 literal_start = i;
1052
1053 match spec {
1054 'd' => push_zero_padded_2(&mut result, d),
1055 'e' => push_space_padded_2(&mut result, d),
1056 'F' => {
1057 push_zero_padded_4(&mut result, y);
1059 result.push('-');
1060 push_zero_padded_2(&mut result, mo);
1061 result.push('-');
1062 push_zero_padded_2(&mut result, d);
1063 }
1064 'f' => {
1065 let total = (s as f64 + frac).min(59.999);
1067 push_format(&mut result, format_args!("{total:06.3}"));
1068 }
1069 'H' => push_zero_padded_2(&mut result, h),
1070 'I' => {
1071 let h12 = if h == 0 {
1073 12
1074 } else if h > 12 {
1075 h - 12
1076 } else {
1077 h
1078 };
1079 push_zero_padded_2(&mut result, h12);
1080 }
1081 'j' => push_zero_padded_3(&mut result, doy),
1082 'J' => {
1083 result.push_str(&crate::builtins::format_float_g(jdn, 16, false, false, 16));
1090 }
1091 'k' => {
1092 push_space_padded_2(&mut result, h);
1094 }
1095 'l' => {
1096 let h12 = if h == 0 {
1098 12
1099 } else if h > 12 {
1100 h - 12
1101 } else {
1102 h
1103 };
1104 push_space_padded_2(&mut result, h12);
1105 }
1106 'm' => push_zero_padded_2(&mut result, mo),
1107 'M' => push_zero_padded_2(&mut result, mi),
1108 'p' => {
1109 result.push_str(if h < 12 { "AM" } else { "PM" });
1110 }
1111 'P' => {
1112 result.push_str(if h < 12 { "am" } else { "pm" });
1113 }
1114 'R' => {
1115 push_zero_padded_2(&mut result, h);
1116 result.push(':');
1117 push_zero_padded_2(&mut result, mi);
1118 }
1119 's' => {
1120 if subsec {
1121 let unix = jdn_to_unix_subsec(jdn);
1122 push_format(&mut result, format_args!("{unix:.3}"));
1123 } else {
1124 let unix = jdn_to_unix(jdn);
1125 push_format(&mut result, format_args!("{unix}"));
1126 }
1127 }
1128 'S' => push_zero_padded_2(&mut result, s),
1129 'T' => {
1130 push_zero_padded_2(&mut result, h);
1131 result.push(':');
1132 push_zero_padded_2(&mut result, mi);
1133 result.push(':');
1134 push_zero_padded_2(&mut result, s);
1135 }
1136 'u' => {
1137 let u = if dow == 0 { 7 } else { dow };
1139 push_format(&mut result, format_args!("{u}"));
1140 }
1141 'w' => push_format(&mut result, format_args!("{dow}")),
1142 'W' => {
1143 let w = (doy + 6 - ((dow + 6) % 7)) / 7;
1145 push_zero_padded_2(&mut result, w);
1146 }
1147 'U' => {
1148 let u = (doy + 6 - dow) / 7;
1153 push_zero_padded_2(&mut result, u);
1154 }
1155 'Y' => push_zero_padded_4(&mut result, y),
1156 'G' | 'g' | 'V' => {
1157 let (iso_y, iso_w) = iso_week(y, mo, d);
1159 match spec {
1160 'G' => push_zero_padded_4(&mut result, iso_y),
1161 'g' => push_zero_padded_2(&mut result, iso_y % 100),
1162 'V' => push_zero_padded_2(&mut result, iso_w),
1163 _ => unreachable!(),
1164 }
1165 }
1166 '%' => result.push('%'),
1167 _ => return None,
1171 }
1172 }
1173
1174 if literal_start < fmt.len() {
1175 result.push_str(&fmt[literal_start..]);
1176 }
1177
1178 Some(result)
1179}
1180
1181fn iso_week(y: i64, m: i64, d: i64) -> (i64, i64) {
1183 let jdn = ymd_to_jdn(y, m, d);
1184 let jdn_int = (jdn + 0.5).floor() as i64;
1185 let dow = (jdn_int + 1) % 7;
1187 let iso_dow = if dow == 0 { 7 } else { dow };
1188
1189 let thu_jdn = jdn_int + (4 - iso_dow);
1191 let (thu_y, _, _) = jdn_to_ymd(thu_jdn as f64);
1192
1193 let jan4_jdn = (ymd_to_jdn(thu_y, 1, 4) + 0.5).floor() as i64;
1195 let jan4_dow = (jan4_jdn + 1) % 7;
1196 let jan4_iso_dow = if jan4_dow == 0 { 7 } else { jan4_dow };
1197 let week1_start = jan4_jdn - (jan4_iso_dow - 1);
1198
1199 let week = (thu_jdn - week1_start) / 7 + 1;
1200 (thu_y, week)
1201}
1202
1203fn timediff_impl(jdn1: f64, jdn2: f64) -> String {
1206 let (sign, start_jdn, end_jdn) = if jdn1 >= jdn2 {
1207 ('+', jdn2, jdn1)
1208 } else {
1209 ('-', jdn1, jdn2)
1210 };
1211
1212 let (start_y, start_mo, start_d) = jdn_to_ymd(start_jdn);
1213 let (start_h, start_mi, mut start_s, start_frac) = jdn_to_hms(start_jdn);
1214 let mut start_ms = (start_frac * 1000.0).round() as i64;
1215 if start_ms >= 1000 {
1216 start_ms = 0;
1217 start_s += 1;
1218 }
1219
1220 let (end_y, end_mo, end_d) = jdn_to_ymd(end_jdn);
1221 let (end_h, end_mi, mut end_s, end_frac) = jdn_to_hms(end_jdn);
1222 let mut end_ms = (end_frac * 1000.0).round() as i64;
1223 if end_ms >= 1000 {
1224 end_ms = 0;
1225 end_s += 1;
1226 }
1227
1228 let mut years = end_y - start_y;
1229 let mut months = end_mo - start_mo;
1230 let mut days = end_d - start_d;
1231 let mut hours = end_h - start_h;
1232 let mut minutes = end_mi - start_mi;
1233 let mut seconds = end_s - start_s;
1234 let mut millis = end_ms - start_ms;
1235
1236 if millis < 0 {
1237 millis += 1000;
1238 seconds -= 1;
1239 }
1240 if seconds < 0 {
1241 seconds += 60;
1242 minutes -= 1;
1243 }
1244 if minutes < 0 {
1245 minutes += 60;
1246 hours -= 1;
1247 }
1248 if hours < 0 {
1249 hours += 24;
1250 days -= 1;
1251 }
1252 if days < 0 {
1253 months -= 1;
1254 let (borrow_y, borrow_mo) = if end_mo == 1 {
1255 (end_y - 1, 12)
1256 } else {
1257 (end_y, end_mo - 1)
1258 };
1259 days += days_in_month(borrow_y, borrow_mo);
1260 }
1261 if months < 0 {
1262 months += 12;
1263 years -= 1;
1264 }
1265
1266 format!(
1267 "{sign}{years:04}-{months:02}-{days:02} {hours:02}:{minutes:02}:{seconds:02}.{millis:03}"
1268 )
1269}
1270
1271#[must_use]
1298pub fn is_datetime_invocation_safe_for_schema(function_name: &str, args: &[SqliteValue]) -> bool {
1299 if function_name.eq_ignore_ascii_case("strftime") {
1300 return args.first().is_none_or(SqliteValue::is_null)
1304 || datetime_arguments_are_schema_safe(&args[1..]);
1305 }
1306
1307 if function_name.eq_ignore_ascii_case("timediff") {
1308 if args.len() != 2 {
1312 return true;
1313 }
1314 for time_value in args {
1315 match classify_time_value_for_schema(time_value) {
1316 SchemaTimeValue::Dynamic => return false,
1317 SchemaTimeValue::NullOrInvalid => return true,
1318 SchemaTimeValue::Fixed { .. } => {}
1319 }
1320 }
1321 return true;
1322 }
1323
1324 if function_name.eq_ignore_ascii_case("date")
1325 || function_name.eq_ignore_ascii_case("time")
1326 || function_name.eq_ignore_ascii_case("datetime")
1327 || function_name.eq_ignore_ascii_case("julianday")
1328 || function_name.eq_ignore_ascii_case("unixepoch")
1329 {
1330 return datetime_arguments_are_schema_safe(args);
1331 }
1332
1333 true
1334}
1335
1336fn datetime_arguments_are_schema_safe(args: &[SqliteValue]) -> bool {
1338 let Some(time_value) = args.first() else {
1339 return false;
1340 };
1341 let (input, raw_numeric) = match classify_time_value_for_schema(time_value) {
1342 SchemaTimeValue::Dynamic => return false,
1343 SchemaTimeValue::NullOrInvalid => return true,
1344 SchemaTimeValue::Fixed { input, raw_numeric } => (input, raw_numeric),
1345 };
1346
1347 let mut reached_modifiers = Vec::with_capacity(args.len().saturating_sub(1));
1348 for modifier in &args[1..] {
1349 if modifier.is_null() {
1350 return true;
1351 }
1352 if sqlite_value_is_keyword(modifier, "localtime")
1353 || sqlite_value_is_keyword(modifier, "utc")
1354 {
1355 return false;
1356 }
1357 let Some(modifier) = sqlite_value_datetime_text(modifier) else {
1358 return true;
1359 };
1360 reached_modifiers.push(modifier.into_owned());
1361 if apply_modifiers(input, &reached_modifiers, raw_numeric).is_none() {
1362 return true;
1365 }
1366 }
1367 true
1368}
1369
1370enum SchemaTimeValue {
1371 Dynamic,
1372 NullOrInvalid,
1373 Fixed { input: f64, raw_numeric: bool },
1374}
1375
1376fn classify_time_value_for_schema(value: &SqliteValue) -> SchemaTimeValue {
1377 if value.is_null() {
1378 return SchemaTimeValue::NullOrInvalid;
1379 }
1380 if is_implicit_now_time_value(value) {
1381 return SchemaTimeValue::Dynamic;
1382 }
1383 match parse_time_value(value) {
1384 Some(parsed) => SchemaTimeValue::Fixed {
1385 input: parsed.jdn,
1386 raw_numeric: parsed.raw_numeric,
1387 },
1388 None => SchemaTimeValue::NullOrInvalid,
1389 }
1390}
1391
1392fn is_implicit_now_time_value(value: &SqliteValue) -> bool {
1393 sqlite_value_is_keyword(value, "now")
1394 || sqlite_value_is_keyword(value, "subsec")
1395 || sqlite_value_is_keyword(value, "subsecond")
1396}
1397
1398fn sqlite_value_is_keyword(value: &SqliteValue, keyword: &str) -> bool {
1402 let bytes = match value {
1403 SqliteValue::Text(text) => sqlite_c_string_bytes(text.as_bytes_direct()),
1404 SqliteValue::Blob(bytes) => sqlite_c_string_bytes(bytes),
1405 SqliteValue::Null | SqliteValue::Integer(_) | SqliteValue::Float(_) => return false,
1406 };
1407 bytes.eq_ignore_ascii_case(keyword.as_bytes())
1408}
1409
1410#[derive(Clone, Copy)]
1411struct ParsedTimeValue {
1412 jdn: f64,
1413 raw_numeric: bool,
1414 unmodified_hms: Option<UnmodifiedHms>,
1415}
1416
1417fn parse_time_value(value: &SqliteValue) -> Option<ParsedTimeValue> {
1418 match value {
1419 SqliteValue::Null => None,
1420 SqliteValue::Integer(integer) => Some(ParsedTimeValue {
1421 jdn: *integer as f64,
1422 raw_numeric: true,
1423 unmodified_hms: None,
1424 }),
1425 SqliteValue::Float(float) if float.is_finite() => Some(ParsedTimeValue {
1426 jdn: *float,
1427 raw_numeric: true,
1428 unmodified_hms: None,
1429 }),
1430 SqliteValue::Float(_) => None,
1431 SqliteValue::Text(_) | SqliteValue::Blob(_) => {
1432 let text = sqlite_value_datetime_text(value)?;
1433 let numeric = text.trim_matches(|c: char| c.is_ascii_whitespace());
1434 if let Ok(number) = numeric.parse::<f64>()
1435 && number.is_finite()
1436 {
1437 return Some(ParsedTimeValue {
1438 jdn: number,
1439 raw_numeric: true,
1440 unmodified_hms: None,
1441 });
1442 }
1443 Some(ParsedTimeValue {
1444 jdn: parse_timestring(text.as_ref())?,
1445 raw_numeric: false,
1446 unmodified_hms: unmodified_hms_from_timestring(text.as_ref()),
1447 })
1448 }
1449 }
1450}
1451
1452fn unmodified_hms_from_timestring(value: &str) -> Option<UnmodifiedHms> {
1453 let value = sqlite_c_string_str(value).trim_end_matches(|c: char| c.is_ascii_whitespace());
1454 let time = if value.len() > 10
1455 && value.as_bytes().get(4) == Some(&b'-')
1456 && value.as_bytes().get(7) == Some(&b'-')
1457 && value
1458 .as_bytes()
1459 .get(10)
1460 .is_some_and(|separator| matches!(*separator, b' ' | b'T'))
1461 {
1462 &value[11..]
1463 } else if value.len() >= 5 && value.as_bytes().get(2) == Some(&b':') {
1464 value
1465 } else {
1466 return None;
1467 };
1468 let (hour, minute, second, fraction, timezone_offset) = parse_time_part_with_tz(time)?;
1469 (timezone_offset == 0).then_some(UnmodifiedHms {
1470 hour,
1471 minute,
1472 second,
1473 fraction,
1474 })
1475}
1476
1477struct ParsedDateTimeArgs {
1478 jdn: f64,
1479 subsec: bool,
1480 unmodified_hms: Option<UnmodifiedHms>,
1481}
1482
1483fn parse_args(args: &[SqliteValue]) -> Option<ParsedDateTimeArgs> {
1485 let first_position_subsec = args.first().is_some_and(|value| {
1486 sqlite_value_is_keyword(value, "subsec") || sqlite_value_is_keyword(value, "subsecond")
1487 });
1488 let parsed = match args.first() {
1489 None => ParsedTimeValue {
1490 jdn: current_time_jdn(),
1491 raw_numeric: false,
1492 unmodified_hms: None,
1493 },
1494 Some(value) => parse_time_value(value)?,
1495 };
1496
1497 let mut modifiers = Vec::with_capacity(args.len().saturating_sub(1));
1498 for modifier in args.get(1..).unwrap_or_default() {
1499 modifiers.push(sqlite_value_datetime_text(modifier)?.into_owned());
1500 }
1501
1502 if parsed.raw_numeric {
1507 let first = modifiers
1508 .first()
1509 .map(|modifier| modifier.to_ascii_lowercase());
1510 let reinterprets_raw = matches!(first.as_deref(), Some("unixepoch" | "julianday" | "auto"));
1511 if !reinterprets_raw && !(0.0..=AUTO_JDN_MAX).contains(&parsed.jdn) {
1512 return None;
1513 }
1514 }
1515
1516 let preserves_input_hms = modifiers.iter().all(|modifier| {
1517 modifier.eq_ignore_ascii_case("subsec") || modifier.eq_ignore_ascii_case("subsecond")
1518 });
1519 let (jdn, modifier_subsec) = apply_modifiers(parsed.jdn, &modifiers, parsed.raw_numeric)?;
1520 if !(0.0..=AUTO_JDN_MAX).contains(&jdn) {
1528 return None;
1529 }
1530 Some(ParsedDateTimeArgs {
1531 jdn,
1532 subsec: first_position_subsec || modifier_subsec,
1533 unmodified_hms: preserves_input_hms
1534 .then_some(parsed.unmodified_hms)
1535 .flatten(),
1536 })
1537}
1538
1539pub struct DateFunc;
1542
1543impl ScalarFunction for DateFunc {
1544 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1545 match parse_args(args) {
1546 Some(parsed) => Ok(SqliteValue::Text(format_date(parsed.jdn))),
1547 None => Ok(SqliteValue::Null),
1548 }
1549 }
1550
1551 fn num_args(&self) -> i32 {
1552 -1
1553 }
1554
1555 fn is_deterministic(&self) -> bool {
1556 false
1557 }
1558
1559 fn name(&self) -> &str {
1560 "date"
1561 }
1562}
1563
1564pub struct TimeFunc;
1567
1568impl ScalarFunction for TimeFunc {
1569 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1570 match parse_args(args) {
1571 Some(parsed) => Ok(SqliteValue::Text(format_time(
1572 parsed.jdn,
1573 parsed.subsec,
1574 parsed.unmodified_hms,
1575 ))),
1576 None => Ok(SqliteValue::Null),
1577 }
1578 }
1579
1580 fn num_args(&self) -> i32 {
1581 -1
1582 }
1583
1584 fn is_deterministic(&self) -> bool {
1585 false
1586 }
1587
1588 fn name(&self) -> &str {
1589 "time"
1590 }
1591}
1592
1593pub struct DateTimeFunc;
1596
1597impl ScalarFunction for DateTimeFunc {
1598 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1599 match parse_args(args) {
1600 Some(parsed) => Ok(SqliteValue::Text(format_datetime(
1601 parsed.jdn,
1602 parsed.subsec,
1603 parsed.unmodified_hms,
1604 ))),
1605 None => Ok(SqliteValue::Null),
1606 }
1607 }
1608
1609 fn num_args(&self) -> i32 {
1610 -1
1611 }
1612
1613 fn is_deterministic(&self) -> bool {
1614 false
1615 }
1616
1617 fn name(&self) -> &str {
1618 "datetime"
1619 }
1620}
1621
1622pub struct JuliandayFunc;
1625
1626impl ScalarFunction for JuliandayFunc {
1627 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1628 match parse_args(args) {
1629 Some(parsed) => Ok(SqliteValue::Float(parsed.jdn)),
1630 None => Ok(SqliteValue::Null),
1631 }
1632 }
1633
1634 fn num_args(&self) -> i32 {
1635 -1
1636 }
1637
1638 fn is_deterministic(&self) -> bool {
1639 false
1640 }
1641
1642 fn name(&self) -> &str {
1643 "julianday"
1644 }
1645}
1646
1647pub struct UnixepochFunc;
1650
1651impl ScalarFunction for UnixepochFunc {
1652 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1653 match parse_args(args) {
1654 Some(parsed) if parsed.subsec => Ok(SqliteValue::Float(jdn_to_unix_subsec(parsed.jdn))),
1657 Some(parsed) => Ok(SqliteValue::Integer(jdn_to_unix(parsed.jdn))),
1658 None => Ok(SqliteValue::Null),
1659 }
1660 }
1661
1662 fn num_args(&self) -> i32 {
1663 -1
1664 }
1665
1666 fn is_deterministic(&self) -> bool {
1667 false
1668 }
1669
1670 fn name(&self) -> &str {
1671 "unixepoch"
1672 }
1673}
1674
1675pub struct StrftimeFunc;
1678
1679impl ScalarFunction for StrftimeFunc {
1680 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1681 let Some(format_value) = args.first() else {
1682 return Ok(SqliteValue::Null);
1683 };
1684 let Some(fmt) = sqlite_value_datetime_text(format_value) else {
1685 return Ok(SqliteValue::Null);
1686 };
1687 let rest = &args[1..];
1688 match parse_args(rest) {
1689 Some(parsed) => Ok(
1690 match format_strftime(
1691 fmt.as_ref(),
1692 parsed.jdn,
1693 parsed.subsec,
1694 parsed.unmodified_hms,
1695 ) {
1696 Some(text) => SqliteValue::Text(text.into()),
1698 None => SqliteValue::Null,
1699 },
1700 ),
1701 None => Ok(SqliteValue::Null),
1702 }
1703 }
1704
1705 fn num_args(&self) -> i32 {
1706 -1
1707 }
1708
1709 fn is_deterministic(&self) -> bool {
1710 false
1711 }
1712
1713 fn name(&self) -> &str {
1714 "strftime"
1715 }
1716}
1717
1718pub struct TimediffFunc;
1721
1722impl ScalarFunction for TimediffFunc {
1723 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1724 if args.len() < 2 || args[0].is_null() || args[1].is_null() {
1725 return Ok(SqliteValue::Null);
1726 }
1727
1728 let jdn1 = parse_time_value(&args[0])
1729 .map(|parsed| parsed.jdn)
1730 .filter(|jdn| (0.0..=AUTO_JDN_MAX).contains(jdn));
1731 let jdn2 = parse_time_value(&args[1])
1732 .map(|parsed| parsed.jdn)
1733 .filter(|jdn| (0.0..=AUTO_JDN_MAX).contains(jdn));
1734
1735 match (jdn1, jdn2) {
1736 (Some(j1), Some(j2)) => Ok(SqliteValue::Text(timediff_impl(j1, j2).into())),
1737 _ => Ok(SqliteValue::Null),
1738 }
1739 }
1740
1741 fn num_args(&self) -> i32 {
1742 2
1743 }
1744
1745 fn is_deterministic(&self) -> bool {
1746 false
1747 }
1748
1749 fn name(&self) -> &str {
1750 "timediff"
1751 }
1752}
1753
1754pub fn register_datetime_builtins(registry: &mut FunctionRegistry) {
1758 registry.register_conditionally_deterministic_scalar(DateFunc);
1759 registry.register_conditionally_deterministic_scalar(TimeFunc);
1760 registry.register_conditionally_deterministic_scalar(DateTimeFunc);
1761 registry.register_conditionally_deterministic_scalar(JuliandayFunc);
1762 registry.register_conditionally_deterministic_scalar(UnixepochFunc);
1763 registry.register_conditionally_deterministic_scalar(StrftimeFunc);
1764 registry.register_conditionally_deterministic_scalar(TimediffFunc);
1765}
1766
1767#[cfg(test)]
1770mod tests {
1771 use super::*;
1772
1773 #[test]
1779 #[cfg(not(target_arch = "wasm32"))]
1780 fn utc_modifier_dst_transition_matches_stock_bd_tl6ly() {
1781 if std::env::var("TZ").ok().as_deref() != Some("America/New_York") {
1782 eprintln!(
1783 "SKIP utc_modifier_dst_transition_matches_stock_bd_tl6ly: needs TZ=America/New_York"
1784 );
1785 return;
1786 }
1787 let utc_of = |y, mo, d, h, mi, s| -> (i64, i64, i64, i64, i64, i64) {
1788 let jdn = ymdhms_to_jdn(y, mo, d, h, mi, s, 0.0);
1789 let out = apply_modifier(jdn, "utc").expect("utc modifier");
1790 let (yy, mm, dd) = jdn_to_ymd(out);
1791 let (hh, nn, ss, _f) = jdn_to_hms(out);
1792 (yy, mm, dd, hh, nn, ss)
1793 };
1794 assert_eq!(utc_of(2024, 1, 15, 12, 0, 0), (2024, 1, 15, 17, 0, 0));
1796 assert_eq!(utc_of(2024, 7, 15, 12, 0, 0), (2024, 7, 15, 16, 0, 0));
1798 assert_eq!(utc_of(2024, 3, 10, 6, 30, 0), (2024, 3, 10, 10, 30, 0));
1800 assert_eq!(utc_of(2024, 3, 10, 2, 30, 0), (2024, 3, 10, 7, 30, 0));
1802 assert_eq!(utc_of(2024, 11, 3, 1, 30, 0), (2024, 11, 3, 5, 30, 0));
1804 }
1805
1806 fn text(s: &str) -> SqliteValue {
1807 SqliteValue::Text(s.into())
1808 }
1809
1810 fn int(v: i64) -> SqliteValue {
1811 SqliteValue::Integer(v)
1812 }
1813
1814 fn float(v: f64) -> SqliteValue {
1815 SqliteValue::Float(v)
1816 }
1817
1818 fn null() -> SqliteValue {
1819 SqliteValue::Null
1820 }
1821
1822 fn assert_text(result: &SqliteValue, expected: &str) {
1823 match result {
1824 SqliteValue::Text(s) => assert_eq!(s.as_ref(), expected, "text mismatch"),
1825 other => panic!("expected Text(\"{expected}\"), got {other:?}"),
1826 }
1827 }
1828
1829 fn blob(s: &str) -> SqliteValue {
1832 SqliteValue::Blob(s.as_bytes().into())
1833 }
1834
1835 #[test]
1836 fn test_schema_safety_common_datetime_argument_layout() {
1837 for name in ["date", "time", "datetime", "julianday", "unixepoch"] {
1838 assert!(
1839 !is_datetime_invocation_safe_for_schema(name, &[]),
1840 "{name}() implicitly reads the current time"
1841 );
1842 assert!(is_datetime_invocation_safe_for_schema(
1843 name,
1844 &[text("2024-03-15 12:34:56")]
1845 ));
1846 assert!(is_datetime_invocation_safe_for_schema(name, &[int(0)]));
1847 assert!(is_datetime_invocation_safe_for_schema(
1848 name,
1849 &[float(2_460_384.5)]
1850 ));
1851 assert!(is_datetime_invocation_safe_for_schema(name, &[null()]));
1852
1853 for current_time in ["now", "NOW", "subsec", "SUBSECOND"] {
1854 assert!(
1855 !is_datetime_invocation_safe_for_schema(name, &[text(current_time)]),
1856 "{name}({current_time:?}) must be conditional"
1857 );
1858 }
1859 assert!(!is_datetime_invocation_safe_for_schema(
1860 name,
1861 &[blob("NOW")]
1862 ));
1863 assert!(!is_datetime_invocation_safe_for_schema(
1864 name,
1865 &[text("now\0ignored")]
1866 ));
1867 assert!(!is_datetime_invocation_safe_for_schema(
1868 name,
1869 &[SqliteValue::Blob(b"subsec\0\xff".as_slice().into())]
1870 ));
1871 for invalid_padded in [" now ", "subsec ", " subsecond"] {
1872 assert!(is_datetime_invocation_safe_for_schema(
1873 name,
1874 &[text(invalid_padded)]
1875 ));
1876 }
1877 assert!(is_datetime_invocation_safe_for_schema(
1878 name,
1879 &[blob(" NoW ")]
1880 ));
1881 assert!(is_datetime_invocation_safe_for_schema(
1882 name,
1883 &[text("nowhere")]
1884 ));
1885
1886 assert!(is_datetime_invocation_safe_for_schema(
1888 name,
1889 &[text("localtime")]
1890 ));
1891 assert!(is_datetime_invocation_safe_for_schema(name, &[text("utc")]));
1892 assert!(is_datetime_invocation_safe_for_schema(
1893 name,
1894 &[text("2024-03-15"), text("subsec")]
1895 ));
1896 assert!(is_datetime_invocation_safe_for_schema(
1897 name,
1898 &[text("2024-03-15"), text("subsecond")]
1899 ));
1900
1901 for modifier in ["localtime", "LOCALTIME", "utc"] {
1902 assert!(
1903 !is_datetime_invocation_safe_for_schema(
1904 name,
1905 &[text("2024-03-15"), text(modifier)]
1906 ),
1907 "{name} modifier {modifier:?} depends on host-local state"
1908 );
1909 }
1910 assert!(!is_datetime_invocation_safe_for_schema(
1911 name,
1912 &[text("2024-03-15"), blob("UTC")]
1913 ));
1914 assert!(!is_datetime_invocation_safe_for_schema(
1915 name,
1916 &[text("2024-03-15"), text("localtime\0ignored")]
1917 ));
1918 assert!(is_datetime_invocation_safe_for_schema(
1919 name,
1920 &[text("2024-03-15"), text(" utc ")]
1921 ));
1922
1923 assert!(is_datetime_invocation_safe_for_schema(
1925 name,
1926 &[text("2024-03-15"), null(), text("localtime")]
1927 ));
1928 assert!(!is_datetime_invocation_safe_for_schema(
1929 name,
1930 &[text("2024-03-15"), text("localtime"), null()]
1931 ));
1932
1933 assert!(is_datetime_invocation_safe_for_schema(
1936 name,
1937 &[text("bogus"), text("localtime")]
1938 ));
1939 assert!(is_datetime_invocation_safe_for_schema(
1940 name,
1941 &[text("2000-01-01"), text("bogus"), text("localtime")]
1942 ));
1943 }
1944 }
1945
1946 #[test]
1947 fn test_schema_safety_strftime_uses_shifted_time_arguments() {
1948 assert!(is_datetime_invocation_safe_for_schema("strftime", &[]));
1949 assert!(is_datetime_invocation_safe_for_schema(
1950 "strftime",
1951 &[null()]
1952 ));
1953 assert!(is_datetime_invocation_safe_for_schema(
1954 "strftime",
1955 &[null(), text("now")]
1956 ));
1957
1958 assert!(!is_datetime_invocation_safe_for_schema(
1960 "strftime",
1961 &[text("%Y")]
1962 ));
1963 assert!(!is_datetime_invocation_safe_for_schema(
1964 "STRFTIME",
1965 &[text("%Y"), text("now")]
1966 ));
1967 assert!(!is_datetime_invocation_safe_for_schema(
1968 "strftime",
1969 &[text("%s"), text("subsecond")]
1970 ));
1971
1972 assert!(is_datetime_invocation_safe_for_schema(
1974 "strftime",
1975 &[text("now localtime utc"), text("2024-03-15")]
1976 ));
1977 assert!(is_datetime_invocation_safe_for_schema(
1978 "strftime",
1979 &[text("%Y"), int(0), text("unixepoch")]
1980 ));
1981 assert!(is_datetime_invocation_safe_for_schema(
1982 "strftime",
1983 &[text("%f"), text("2024-03-15"), text("subsec")]
1984 ));
1985 assert!(!is_datetime_invocation_safe_for_schema(
1986 "strftime",
1987 &[text("%Y"), text("2024-03-15"), text("localtime")]
1988 ));
1989 assert!(is_datetime_invocation_safe_for_schema(
1990 "strftime",
1991 &[text("%Y"), text("2024-03-15"), null(), text("localtime")]
1992 ));
1993 }
1994
1995 #[test]
1996 fn test_schema_safety_timediff_treats_both_inputs_as_time_values() {
1997 assert!(is_datetime_invocation_safe_for_schema("timediff", &[]));
1998 assert!(is_datetime_invocation_safe_for_schema(
1999 "timediff",
2000 &[text("2024-03-15")]
2001 ));
2002 assert!(is_datetime_invocation_safe_for_schema(
2003 "timediff",
2004 &[text("2024-03-15"), text("2024-03-14")]
2005 ));
2006 assert!(is_datetime_invocation_safe_for_schema(
2007 "timediff",
2008 &[int(2_460_384), float(2_460_383.5)]
2009 ));
2010
2011 for current_time in ["now", "subsec", "subsecond"] {
2012 assert!(!is_datetime_invocation_safe_for_schema(
2013 "timediff",
2014 &[text(current_time), text("2024-03-14")]
2015 ));
2016 assert!(!is_datetime_invocation_safe_for_schema(
2017 "timediff",
2018 &[text("2024-03-15"), text(current_time)]
2019 ));
2020 }
2021
2022 assert!(is_datetime_invocation_safe_for_schema(
2025 "timediff",
2026 &[text("localtime"), text("utc")]
2027 ));
2028 assert!(is_datetime_invocation_safe_for_schema(
2029 "timediff",
2030 &[null(), text("now")]
2031 ));
2032 assert!(is_datetime_invocation_safe_for_schema(
2033 "timediff",
2034 &[text("bogus"), text("now")]
2035 ));
2036 assert!(!is_datetime_invocation_safe_for_schema(
2037 "timediff",
2038 &[blob("NOW"), null()]
2039 ));
2040 }
2041
2042 #[test]
2043 fn test_schema_safety_ignores_non_datetime_function_names() {
2044 for name in ["", "my_date", "current_date", "date ", "random"] {
2045 assert!(is_datetime_invocation_safe_for_schema(
2046 name,
2047 &[text("now"), text("localtime")]
2048 ));
2049 }
2050 }
2051
2052 #[test]
2055 fn test_omitted_time_value_uses_current_time() {
2056 let date = DateFunc.invoke(&[]).unwrap();
2057 let time = TimeFunc.invoke(&[]).unwrap();
2058 let datetime = DateTimeFunc.invoke(&[]).unwrap();
2059 let julianday = JuliandayFunc.invoke(&[]).unwrap();
2060 let unixepoch = UnixepochFunc.invoke(&[]).unwrap();
2061 let year = StrftimeFunc.invoke(&[text("%Y")]).unwrap();
2062
2063 assert!(matches!(&date, SqliteValue::Text(value) if value.len() == 10));
2064 assert!(matches!(&time, SqliteValue::Text(value) if value.len() == 8));
2065 assert!(matches!(&datetime, SqliteValue::Text(value) if value.len() == 19));
2066 assert!(matches!(julianday, SqliteValue::Float(_)));
2067 assert!(matches!(unixepoch, SqliteValue::Integer(_)));
2068 assert!(matches!(&year, SqliteValue::Text(value)
2069 if value.len() == 4 && value.as_bytes().iter().all(u8::is_ascii_digit)));
2070
2071 assert_eq!(StrftimeFunc.invoke(&[]).unwrap(), SqliteValue::Null);
2072 assert_eq!(StrftimeFunc.invoke(&[null()]).unwrap(), SqliteValue::Null);
2073 }
2074
2075 #[test]
2076 fn test_datetime_oracle_edges_2026_08() {
2077 let date = |args: &[SqliteValue]| -> Option<String> {
2082 match DateFunc.invoke(args).unwrap() {
2083 SqliteValue::Text(t) => Some(t.as_str().to_owned()),
2084 SqliteValue::Null => None,
2085 other => panic!("date -> {other:?}"),
2086 }
2087 };
2088 let dtime = |args: &[SqliteValue]| -> Option<String> {
2089 match DateTimeFunc.invoke(args).unwrap() {
2090 SqliteValue::Text(t) => Some(t.as_str().to_owned()),
2091 SqliteValue::Null => None,
2092 other => panic!("datetime -> {other:?}"),
2093 }
2094 };
2095 let strf = |args: &[SqliteValue]| -> Option<String> {
2096 match StrftimeFunc.invoke(args).unwrap() {
2097 SqliteValue::Text(t) => Some(t.as_str().to_owned()),
2098 SqliteValue::Null => None,
2099 other => panic!("strftime -> {other:?}"),
2100 }
2101 };
2102 let s = |x: &str| Some(x.to_owned());
2103
2104 assert_eq!(
2106 date(&[text("2020-01-31"), text("+1 month")]),
2107 s("2020-03-02")
2108 );
2109 assert_eq!(
2110 date(&[text("2020-01-31"), text("+1 month"), text("+1 month")]),
2111 s("2020-04-02")
2112 );
2113 assert_eq!(date(&[text("2021-02-28"), text("+1 day")]), s("2021-03-01"));
2114 assert_eq!(
2115 date(&[text("2020-02-29"), text("+1 year")]),
2116 s("2021-03-01")
2117 );
2118 assert_eq!(date(&[text("2020-02-30")]), s("2020-03-01"));
2121 assert_eq!(
2123 date(&[text("2020-01-31"), text("+1 month"), text("floor")]),
2124 s("2020-02-29")
2125 );
2126 assert_eq!(
2127 date(&[text("2020-02-29"), text("+1.0 year"), text("floor")]),
2128 s("2021-02-28")
2129 );
2130 assert_eq!(
2132 date(&[text("2020-01-31"), text("start of month")]),
2133 s("2020-01-01")
2134 );
2135 assert_eq!(
2136 date(&[text("2020-06-15"), text("weekday 0")]),
2137 s("2020-06-21")
2138 );
2139 assert_eq!(
2140 date(&[text("2020-06-15"), text("weekday 6")]),
2141 s("2020-06-20")
2142 );
2143 assert_eq!(date(&[text("2020-06-15"), text("weekday 7")]), None);
2144 assert_eq!(
2148 date(&[text("2020-01-15"), text("+1.5 months")]),
2149 s("2020-03-01")
2150 );
2151 assert_eq!(
2152 dtime(&[text("2020-01-15 00:00:00"), text("+0.5 months")]),
2153 s("2020-01-30 00:00:00")
2154 );
2155 assert_eq!(
2156 dtime(&[text("2020-01-15 00:00:00"), text("+1.5 months")]),
2157 s("2020-03-01 00:00:00")
2158 );
2159 assert_eq!(
2160 dtime(&[text("2020-01-15 00:00:00"), text("+2.5 months")]),
2161 s("2020-03-30 00:00:00")
2162 );
2163 assert_eq!(
2164 dtime(&[text("2020-01-15 00:00:00"), text("+1.25 months")]),
2165 s("2020-02-22 12:00:00")
2166 );
2167 assert_eq!(
2168 dtime(&[text("2020-01-15 00:00:00"), text("+1.75 months")]),
2169 s("2020-03-08 12:00:00")
2170 );
2171 assert_eq!(
2172 dtime(&[text("2020-01-15 00:00:00"), text("-0.5 months")]),
2173 s("2019-12-31 00:00:00")
2174 );
2175 assert_eq!(
2176 dtime(&[text("2020-01-15 00:00:00"), text("-1.5 months")]),
2177 s("2019-11-30 00:00:00")
2178 );
2179 assert_eq!(
2181 dtime(&[text("2020-01-31 00:00:00"), text("+1.5 months")]),
2182 s("2020-03-17 00:00:00")
2183 );
2184 assert_eq!(
2186 dtime(&[text("2020-01-15 00:00:00"), text("+1.5 years")]),
2187 s("2021-07-16 12:00:00")
2188 );
2189 assert_eq!(
2190 dtime(&[text("2020-01-15 00:00:00"), text("+0.5 years")]),
2191 s("2020-07-15 12:00:00")
2192 );
2193 assert_eq!(date(&[text("2020-13-01")]), None);
2195 assert_eq!(date(&[text("2020-00-10")]), None);
2196 assert_eq!(date(&[text("not-a-date")]), None);
2197
2198 assert_eq!(
2200 dtime(&[text("2020-06-15 12:34:56"), text("start of day")]),
2201 s("2020-06-15 00:00:00")
2202 );
2203 assert_eq!(
2204 dtime(&[text("2020-01-01"), text("+90 minutes")]),
2205 s("2020-01-01 01:30:00")
2206 );
2207 assert_eq!(
2208 dtime(&[text("2020-01-01"), text("+1.5 days")]),
2209 s("2020-01-02 12:00:00")
2210 );
2211 assert_eq!(
2212 dtime(&[int(0), text("unixepoch")]),
2213 s("1970-01-01 00:00:00")
2214 );
2215
2216 assert_eq!(
2218 strf(&[text("%f"), text("2020-01-01 00:00:01.25")]),
2219 s("01.250")
2220 );
2221 assert_eq!(strf(&[text("%j"), text("2020-03-01")]), s("061"));
2222 assert_eq!(strf(&[text("%w"), text("2020-06-15")]), s("1"));
2223 assert_eq!(strf(&[text("%W"), text("2020-01-01")]), s("00"));
2224 assert_eq!(
2225 strf(&[text("%s"), text("2020-01-01 00:00:00")]),
2226 s("1577836800")
2227 );
2228 assert_eq!(strf(&[text("%e"), text("2020-06-05")]), s(" 5"));
2229 assert_eq!(strf(&[text("%I"), text("2020-06-15 13:00:00")]), s("01"));
2230 assert_eq!(strf(&[text("%p"), text("2020-06-15 13:00:00")]), s("PM"));
2231 assert_eq!(strf(&[text("%P"), text("2020-06-15 13:00:00")]), s("pm"));
2232 assert_eq!(strf(&[text("%R"), text("2020-06-15 13:05:00")]), s("13:05"));
2233 assert_eq!(
2234 strf(&[text("%T"), text("2020-06-15 13:05:07")]),
2235 s("13:05:07")
2236 );
2237 assert_eq!(strf(&[text("%G"), text("2020-12-31")]), s("2020"));
2240 assert_eq!(strf(&[text("%V"), text("2020-12-31")]), s("53"));
2241 assert_eq!(strf(&[text("%u"), text("2020-06-15")]), s("1"));
2242 assert_eq!(strf(&[text("%g"), text("2021-01-01")]), s("20"));
2243 assert_eq!(strf(&[text("%V"), text("2021-01-01")]), s("53"));
2244 assert_eq!(
2245 strf(&[text("%G-%V-%u"), text("2021-01-01")]),
2246 s("2020-53-5")
2247 );
2248 assert_eq!(strf(&[text("%V"), text("2016-01-01")]), s("53"));
2249 assert_eq!(strf(&[text("%V"), text("2018-12-31")]), s("01"));
2250
2251 assert_eq!(
2253 JuliandayFunc
2254 .invoke(&[text("2000-01-01 12:00:00")])
2255 .unwrap(),
2256 SqliteValue::Float(2451545.0)
2257 );
2258 }
2259
2260 #[test]
2261 fn test_datetime_more_edges_2026_08() {
2262 let dtime = |args: &[SqliteValue]| -> Option<String> {
2265 match DateTimeFunc.invoke(args).unwrap() {
2266 SqliteValue::Text(t) => Some(t.as_str().to_owned()),
2267 SqliteValue::Null => None,
2268 other => panic!("datetime -> {other:?}"),
2269 }
2270 };
2271 let strf = |args: &[SqliteValue]| -> String {
2272 match StrftimeFunc.invoke(args).unwrap() {
2273 SqliteValue::Text(t) => t.as_str().to_owned(),
2274 other => panic!("strftime -> {other:?}"),
2275 }
2276 };
2277 let s = |x: &str| Some(x.to_owned());
2278 let txt = |x: &str| SqliteValue::Text(SmallText::from_string(x));
2279
2280 assert_eq!(
2282 TimediffFunc
2283 .invoke(&[text("2020-03-01"), text("2020-01-15")])
2284 .unwrap(),
2285 txt("+0000-01-15 00:00:00.000")
2286 );
2287 assert_eq!(
2288 TimediffFunc
2289 .invoke(&[text("2020-01-15 12:00:00"), text("2020-01-15 10:30:00")])
2290 .unwrap(),
2291 txt("+0000-00-00 01:30:00.000")
2292 );
2293
2294 assert_eq!(
2296 strf(&[text("%J"), text("2000-01-01 18:00:00")]),
2297 "2451545.25"
2298 );
2299 assert_eq!(strf(&[text("%k"), text("2020-06-15 09:00:00")]), " 9");
2300 assert_eq!(strf(&[text("%l"), text("2020-06-15 13:00:00")]), " 1");
2301 assert_eq!(
2302 strf(&[text("%f"), text("2020-01-01 00:00:00.999")]),
2303 "00.999"
2304 );
2305 assert_eq!(
2306 strf(&[text("%s"), text("2020-01-01 00:00:00.5")]),
2307 "1577836800"
2308 );
2309
2310 assert_eq!(
2312 dtime(&[text("2020-01-01 00:00:00"), text("subsec")]),
2313 s("2020-01-01 00:00:00.000")
2314 );
2315 assert_eq!(
2316 dtime(&[float(1_577_836_800.5), text("unixepoch")]),
2317 s("2020-01-01 00:00:00")
2318 );
2319 assert_eq!(dtime(&[text("2020-01-01"), text("+100000000 days")]), None);
2320
2321 assert_eq!(
2323 JuliandayFunc.invoke(&[text("1970-01-01")]).unwrap(),
2324 SqliteValue::Float(2440587.5)
2325 );
2326 assert_eq!(
2327 UnixepochFunc
2328 .invoke(&[text("2020-01-01"), text("subsec")])
2329 .unwrap(),
2330 SqliteValue::Float(1_577_836_800.0)
2331 );
2332 }
2333
2334 #[test]
2335 fn test_first_position_subsec_aliases_use_current_time() {
2336 for alias in ["subsec", "subsecond"] {
2337 assert!(matches!(
2338 DateFunc.invoke(&[text(alias)]).unwrap(),
2339 SqliteValue::Text(value) if value.len() == 10
2340 ));
2341 assert!(matches!(
2342 TimeFunc.invoke(&[text(alias)]).unwrap(),
2343 SqliteValue::Text(value)
2344 if value.len() == 12 && value.as_bytes_direct()[8] == b'.'
2345 ));
2346 assert!(matches!(
2347 DateTimeFunc.invoke(&[text(alias)]).unwrap(),
2348 SqliteValue::Text(value)
2349 if value.len() == 23 && value.as_bytes_direct()[19] == b'.'
2350 ));
2351 assert!(matches!(
2352 JuliandayFunc.invoke(&[text(alias)]).unwrap(),
2353 SqliteValue::Float(_)
2354 ));
2355 assert!(matches!(
2356 UnixepochFunc.invoke(&[text(alias)]).unwrap(),
2357 SqliteValue::Float(_)
2358 ));
2359 assert!(matches!(
2360 StrftimeFunc.invoke(&[text("%s"), text(alias)]).unwrap(),
2361 SqliteValue::Text(value)
2362 if value.rsplit_once('.').is_some_and(|(_, fraction)| fraction.len() == 3)
2363 ));
2364 }
2365 }
2366
2367 #[test]
2368 fn test_padded_and_nul_terminated_special_values() {
2369 for invalid in [" now ", "subsec ", " subsecond"] {
2370 assert_eq!(
2371 DateFunc.invoke(&[text(invalid)]).unwrap(),
2372 SqliteValue::Null
2373 );
2374 }
2375 assert_eq!(
2376 DateFunc
2377 .invoke(&[text("2000-01-01"), text(" localtime ")])
2378 .unwrap(),
2379 SqliteValue::Null
2380 );
2381 assert!(matches!(
2382 DateFunc.invoke(&[text("now\0ignored")]).unwrap(),
2383 SqliteValue::Text(value) if value.len() == 10
2384 ));
2385 assert!(matches!(
2386 TimeFunc
2387 .invoke(&[SqliteValue::Blob(b"subsec\0\xff".as_slice().into())])
2388 .unwrap(),
2389 SqliteValue::Text(value) if value.len() == 12
2390 ));
2391 }
2392
2393 #[test]
2394 fn test_date_basic() {
2395 let r = DateFunc.invoke(&[text("2024-03-15 14:30:00")]).unwrap();
2396 assert_text(&r, "2024-03-15");
2397 }
2398
2399 #[test]
2400 fn test_time_basic() {
2401 let r = TimeFunc.invoke(&[text("2024-03-15 14:30:45")]).unwrap();
2402 assert_text(&r, "14:30:45");
2403 }
2404
2405 #[test]
2406 fn test_datetime_basic() {
2407 let r = DateTimeFunc.invoke(&[text("2024-03-15 14:30:00")]).unwrap();
2408 assert_text(&r, "2024-03-15 14:30:00");
2409 }
2410
2411 #[test]
2412 fn test_julianday_basic() {
2413 let r = JuliandayFunc.invoke(&[text("2024-03-15")]).unwrap();
2414 match r {
2415 SqliteValue::Float(jdn) => {
2416 assert!((jdn - 2_460_384.5).abs() < 0.01, "unexpected JDN: {jdn}");
2418 }
2419 other => panic!("expected Float, got {other:?}"),
2420 }
2421 }
2422
2423 #[test]
2424 fn test_computed_result_out_of_range_returns_null() {
2425 let over_datetime = &[text("9999-12-31 23:59:59"), text("+1 second")];
2432 assert_eq!(
2433 DateTimeFunc.invoke(over_datetime).unwrap(),
2434 SqliteValue::Null
2435 );
2436 assert_eq!(TimeFunc.invoke(over_datetime).unwrap(), SqliteValue::Null);
2437 assert_eq!(
2438 JuliandayFunc.invoke(over_datetime).unwrap(),
2439 SqliteValue::Null
2440 );
2441 assert_eq!(
2442 UnixepochFunc.invoke(over_datetime).unwrap(),
2443 SqliteValue::Null
2444 );
2445 assert_eq!(
2446 DateFunc
2447 .invoke(&[text("9999-12-31"), text("+1 day")])
2448 .unwrap(),
2449 SqliteValue::Null
2450 );
2451 assert_eq!(
2452 DateTimeFunc
2453 .invoke(&[text("9999-12-31"), text("+1 month")])
2454 .unwrap(),
2455 SqliteValue::Null
2456 );
2457 assert_eq!(
2458 StrftimeFunc
2459 .invoke(&[
2460 text("%Y-%m-%d"),
2461 text("9999-12-31 23:59:59"),
2462 text("+1 second")
2463 ])
2464 .unwrap(),
2465 SqliteValue::Null
2466 );
2467 assert_eq!(
2469 DateTimeFunc
2470 .invoke(&[text("-4714-11-24 12:00:00"), text("-1 day")])
2471 .unwrap(),
2472 SqliteValue::Null
2473 );
2474
2475 assert_text(
2477 &DateTimeFunc.invoke(&[text("9999-12-31 23:59:59")]).unwrap(),
2478 "9999-12-31 23:59:59",
2479 );
2480 assert!(matches!(
2483 DateTimeFunc
2484 .invoke(&[text("0000-01-01"), text("-1 second")])
2485 .unwrap(),
2486 SqliteValue::Text(_)
2487 ));
2488 }
2489
2490 #[test]
2491 fn test_negative_year_padding_matches_sqlite() {
2492 assert_text(
2497 &DateTimeFunc
2498 .invoke(&[text("0000-01-01"), text("-1 second")])
2499 .unwrap(),
2500 "-0001-12-31 23:59:59",
2501 );
2502 assert_text(
2503 &DateFunc
2504 .invoke(&[text("0000-01-01"), text("-1 day")])
2505 .unwrap(),
2506 "-0001-12-31",
2507 );
2508 assert_text(
2509 &DateTimeFunc
2510 .invoke(&[text("0000-01-01"), text("-100 years")])
2511 .unwrap(),
2512 "-0100-01-01 00:00:00",
2513 );
2514 assert_text(
2515 &DateTimeFunc
2516 .invoke(&[text("0000-01-01"), text("-4000 years")])
2517 .unwrap(),
2518 "-4000-01-01 00:00:00",
2519 );
2520 assert_text(
2522 &DateTimeFunc.invoke(&[text("2024-03-15 14:30:00")]).unwrap(),
2523 "2024-03-15 14:30:00",
2524 );
2525 assert_text(
2526 &DateFunc.invoke(&[text("0000-01-01")]).unwrap(),
2527 "0000-01-01",
2528 );
2529 assert_text(
2532 &StrftimeFunc
2533 .invoke(&[text("%Y"), text("0000-01-01"), text("-1 day")])
2534 .unwrap(),
2535 "-001",
2536 );
2537 }
2538
2539 fn julianday_float(input: &str) -> f64 {
2547 match JuliandayFunc.invoke(&[text(input)]).unwrap() {
2548 SqliteValue::Float(v) => v,
2549 other => panic!("expected Float, got {other:?} for input {input:?}"),
2550 }
2551 }
2552
2553 fn assert_jdn_close(actual: f64, expected: f64, ctx: &str) {
2554 assert!(
2556 (actual - expected).abs() < 1e-6,
2557 "JDN mismatch for {ctx}: got {actual}, expected {expected}"
2558 );
2559 }
2560
2561 #[test]
2562 fn test_julianday_rfc3339_z_suffix() {
2563 let naive = julianday_float("2026-04-07 16:00:00");
2565 assert_jdn_close(julianday_float("2026-04-07T16:00:00Z"), naive, "T...Z");
2566 assert_jdn_close(
2567 julianday_float("2026-04-07T16:00:00z"),
2568 naive,
2569 "lowercase z",
2570 );
2571 }
2572
2573 #[test]
2574 fn test_julianday_rfc3339_zero_offset() {
2575 let naive = julianday_float("2026-04-07 16:00:00");
2576 assert_jdn_close(
2577 julianday_float("2026-04-07T16:00:00+00:00"),
2578 naive,
2579 "+00:00",
2580 );
2581 assert_jdn_close(
2582 julianday_float("2026-04-07T16:00:00-00:00"),
2583 naive,
2584 "-00:00",
2585 );
2586 }
2587
2588 #[test]
2589 fn test_julianday_rfc3339_positive_offset() {
2590 let base = julianday_float("2026-04-07 16:00:00");
2592 let expected = base - 1.0 / 24.0;
2593 assert_jdn_close(
2594 julianday_float("2026-04-07T16:00:00+01:00"),
2595 expected,
2596 "+01:00",
2597 );
2598 }
2599
2600 #[test]
2601 fn test_julianday_rfc3339_negative_offset() {
2602 let base = julianday_float("2026-04-07 16:00:00");
2604 let expected = base + 5.0 / 24.0;
2605 assert_jdn_close(
2606 julianday_float("2026-04-07T16:00:00-05:00"),
2607 expected,
2608 "-05:00",
2609 );
2610 }
2611
2612 #[test]
2613 fn test_julianday_rfc3339_half_hour_offset() {
2614 let base = julianday_float("2026-04-07 16:00:00");
2616 let expected = base - 5.5 / 24.0;
2617 assert_jdn_close(
2618 julianday_float("2026-04-07T16:00:00+05:30"),
2619 expected,
2620 "+05:30",
2621 );
2622 }
2623
2624 #[test]
2625 fn test_julianday_rfc3339_compact_offsets() {
2626 let base = julianday_float("2026-04-07 16:00:00");
2628 assert_jdn_close(
2629 julianday_float("2026-04-07T16:00:00+0100"),
2630 base - 1.0 / 24.0,
2631 "+0100",
2632 );
2633 assert_jdn_close(
2634 julianday_float("2026-04-07T16:00:00-0530"),
2635 base + 5.5 / 24.0,
2636 "-0530",
2637 );
2638 assert_jdn_close(
2639 julianday_float("2026-04-07T16:00:00+09"),
2640 base - 9.0 / 24.0,
2641 "+09",
2642 );
2643 }
2644
2645 #[test]
2646 fn test_julianday_rfc3339_fractional_seconds_with_tz() {
2647 let base = julianday_float("2026-04-07 16:00:00.500");
2649 assert_jdn_close(
2650 julianday_float("2026-04-07T16:00:00.500Z"),
2651 base,
2652 "fractional + Z",
2653 );
2654 assert_jdn_close(
2655 julianday_float("2026-04-07T16:00:00.500+01:00"),
2656 base - 1.0 / 24.0,
2657 "fractional + +01:00",
2658 );
2659 }
2660
2661 #[test]
2662 fn test_date_and_time_rfc3339_round_trip() {
2663 assert_text(
2666 &DateFunc
2667 .invoke(&[text("2026-04-07T16:00:00+05:00")])
2668 .unwrap(),
2669 "2026-04-07",
2671 );
2672 assert_text(
2673 &TimeFunc
2674 .invoke(&[text("2026-04-07T16:00:00+05:00")])
2675 .unwrap(),
2676 "11:00:00",
2677 );
2678 assert_text(
2679 &DateTimeFunc
2680 .invoke(&[text("2026-04-07T16:00:00+05:00")])
2681 .unwrap(),
2682 "2026-04-07 11:00:00",
2683 );
2684 }
2685
2686 #[test]
2687 fn test_julianday_rfc3339_invalid_offsets_return_null() {
2688 for bad in &[
2690 "2026-04-07T16:00:00+25:00", "2026-04-07T16:00:00+01:99", "2026-04-07T16:00:00+1", "2026-04-07T16:00:00+123", ] {
2695 let result = JuliandayFunc.invoke(&[text(bad)]).unwrap();
2696 assert_eq!(
2697 result,
2698 SqliteValue::Null,
2699 "expected NULL for malformed offset {bad:?}, got {result:?}"
2700 );
2701 }
2702 }
2703
2704 #[test]
2705 fn test_julianday_rejects_malformed_time_fields() {
2706 for bad in &[
2710 "+01:00", "-05:30", "+12:30:00", "12:+30:00", "12:30:+45", "12:30:+45.123", "0:00:00", "12:0:00", "12:30:0", "123:00:00", "12:345:00", ] {
2722 let result = JuliandayFunc.invoke(&[text(bad)]).unwrap();
2723 assert_eq!(
2724 result,
2725 SqliteValue::Null,
2726 "expected NULL for signed time field {bad:?}, got {result:?}"
2727 );
2728 }
2729 }
2730
2731 #[test]
2732 fn test_unixepoch_basic() {
2733 let r = UnixepochFunc
2734 .invoke(&[text("1970-01-01 00:00:00")])
2735 .unwrap();
2736 assert_eq!(r, int(0));
2737 }
2738
2739 #[test]
2740 fn test_unixepoch_known_date() {
2741 let r = UnixepochFunc
2742 .invoke(&[text("2024-01-01 00:00:00")])
2743 .unwrap();
2744 assert_eq!(r, int(1_704_067_200));
2746 }
2747
2748 #[test]
2751 fn test_modifier_days() {
2752 let r = DateFunc
2753 .invoke(&[text("2024-01-15"), text("+10 days")])
2754 .unwrap();
2755 assert_text(&r, "2024-01-25");
2756 }
2757
2758 #[test]
2759 fn test_modifier_months() {
2760 let r = DateFunc
2763 .invoke(&[text("2024-01-31"), text("+1 months")])
2764 .unwrap();
2765 assert_text(&r, "2024-03-02");
2766 }
2767
2768 #[test]
2769 fn test_modifier_years() {
2770 let r = DateFunc
2773 .invoke(&[text("2024-02-29"), text("+1 years")])
2774 .unwrap();
2775 assert_text(&r, "2025-03-01");
2776 }
2777
2778 #[test]
2779 fn test_modifier_hours() {
2780 let r = DateTimeFunc
2781 .invoke(&[text("2024-01-01 23:00:00"), text("+2 hours")])
2782 .unwrap();
2783 assert_text(&r, "2024-01-02 01:00:00");
2784 }
2785
2786 #[test]
2787 fn test_modifier_unsigned_is_positive_bd_t8g1e() {
2788 assert_text(
2794 &DateFunc
2795 .invoke(&[text("2024-01-15"), text("10 days")])
2796 .unwrap(),
2797 "2024-01-25",
2798 );
2799 assert_text(
2800 &DateTimeFunc
2801 .invoke(&[text("2024-01-01"), text("5 hours")])
2802 .unwrap(),
2803 "2024-01-01 05:00:00",
2804 );
2805 assert_text(
2806 &DateTimeFunc
2807 .invoke(&[text("2024-01-01"), text("90 minutes")])
2808 .unwrap(),
2809 "2024-01-01 01:30:00",
2810 );
2811 assert_text(
2812 &DateTimeFunc
2813 .invoke(&[text("2024-01-01"), text("86400 seconds")])
2814 .unwrap(),
2815 "2024-01-02 00:00:00",
2816 );
2817 assert_text(
2820 &DateFunc
2821 .invoke(&[text("2024-01-01"), text("2 months")])
2822 .unwrap(),
2823 "2024-03-01",
2824 );
2825 assert_text(
2826 &DateFunc
2827 .invoke(&[text("2024-01-01"), text("1 year")])
2828 .unwrap(),
2829 "2025-01-01",
2830 );
2831 assert_text(
2833 &DateTimeFunc
2834 .invoke(&[text("2024-01-01 12:00"), text("1.5 hours")])
2835 .unwrap(),
2836 "2024-01-01 13:30:00",
2837 );
2838 assert_text(
2842 &DateFunc
2843 .invoke(&[text("2024-03-15"), text("start of month")])
2844 .unwrap(),
2845 "2024-03-01",
2846 );
2847 assert_text(
2848 &DateFunc
2849 .invoke(&[text("2024-06-15"), text("start of year")])
2850 .unwrap(),
2851 "2024-01-01",
2852 );
2853 assert_text(
2855 &DateFunc
2856 .invoke(&[text("2024-01-15"), text("-10 days")])
2857 .unwrap(),
2858 "2024-01-05",
2859 );
2860 }
2861
2862 #[test]
2863 fn test_modifier_start_of_month() {
2864 let r = DateFunc
2865 .invoke(&[text("2024-03-15"), text("start of month")])
2866 .unwrap();
2867 assert_text(&r, "2024-03-01");
2868 }
2869
2870 #[test]
2871 fn test_modifier_start_of_year() {
2872 let r = DateFunc
2873 .invoke(&[text("2024-06-15"), text("start of year")])
2874 .unwrap();
2875 assert_text(&r, "2024-01-01");
2876 }
2877
2878 #[test]
2879 fn test_modifier_start_of_day() {
2880 let r = DateTimeFunc
2881 .invoke(&[text("2024-03-15 14:30:00"), text("start of day")])
2882 .unwrap();
2883 assert_text(&r, "2024-03-15 00:00:00");
2884 }
2885
2886 #[test]
2887 fn test_modifier_unixepoch() {
2888 let r = DateTimeFunc.invoke(&[int(0), text("unixepoch")]).unwrap();
2889 assert_text(&r, "1970-01-01 00:00:00");
2890 }
2891
2892 #[test]
2893 fn test_modifier_weekday() {
2894 let r = DateFunc
2896 .invoke(&[text("2024-03-15"), text("weekday 0")])
2897 .unwrap();
2898 assert_text(&r, "2024-03-17");
2899 }
2900
2901 #[test]
2902 fn test_modifier_auto_unixepoch() {
2903 let ts = int(1_710_531_045);
2904 let r = DateTimeFunc.invoke(&[ts.clone(), text("auto")]).unwrap();
2905 let expected = DateTimeFunc.invoke(&[ts, text("unixepoch")]).unwrap();
2906 assert_eq!(
2907 r, expected,
2908 "auto and unixepoch should agree for unix-like values"
2909 );
2910 }
2911
2912 #[test]
2913 fn test_modifier_auto_julian_day() {
2914 let r = DateFunc
2915 .invoke(&[float(2_460_384.5), text("auto")])
2916 .unwrap();
2917 assert_text(&r, "2024-03-15");
2918 }
2919
2920 #[test]
2921 fn test_modifier_localtime_utc_roundtrip() {
2922 let r = DateTimeFunc
2924 .invoke(&[text("2024-03-15 14:30:45"), text("localtime"), text("utc")])
2925 .unwrap();
2926 assert_text(&r, "2024-03-15 14:30:45");
2927 }
2928
2929 #[test]
2930 fn test_modifier_localtime_shifts_value() {
2931 let offset = utc_offset_for_utc_jdn(ymdhms_to_jdn(2024, 3, 15, 12, 0, 0, 0.0));
2933 if offset != 0 {
2934 let r = DateTimeFunc
2935 .invoke(&[text("2024-03-15 12:00:00"), text("localtime")])
2936 .unwrap();
2937 let shifted = match &r {
2939 SqliteValue::Text(s) => s.clone(),
2940 _ => panic!("expected text"),
2941 };
2942 assert_ne!(&*shifted, "2024-03-15 12:00:00");
2943 }
2944 }
2945
2946 #[test]
2947 fn test_modifier_auto_out_of_range_returns_null() {
2948 let r = DateTimeFunc.invoke(&[float(1.0e20), text("auto")]).unwrap();
2949 assert_eq!(r, SqliteValue::Null);
2950 }
2951
2952 #[test]
2953 fn test_modifier_order_matters() {
2954 let r1 = DateFunc
2956 .invoke(&[text("2024-03-15"), text("start of month"), text("+1 days")])
2957 .unwrap();
2958 assert_text(&r1, "2024-03-02");
2959
2960 let r2 = DateFunc
2962 .invoke(&[text("2024-03-15"), text("+1 days"), text("start of month")])
2963 .unwrap();
2964 assert_text(&r2, "2024-03-01");
2965 }
2966
2967 #[test]
2968 fn test_modifier_weekday_same_day_is_noop() {
2969 let r = DateFunc
2971 .invoke(&[text("2024-03-17"), text("weekday 0")])
2972 .unwrap();
2973 assert_text(&r, "2024-03-17");
2974 }
2975
2976 #[test]
2979 fn test_bare_time_defaults() {
2980 let r = DateFunc.invoke(&[text("12:30:00")]).unwrap();
2981 assert_text(&r, "2000-01-01");
2982 }
2983
2984 #[test]
2985 fn test_t_separator() {
2986 let r = DateTimeFunc.invoke(&[text("2024-03-15T14:30:00")]).unwrap();
2987 assert_text(&r, "2024-03-15 14:30:00");
2988 }
2989
2990 #[test]
2991 fn test_julian_day_input() {
2992 let r = DateFunc.invoke(&[float(2_460_384.5)]).unwrap();
2994 assert_text(&r, "2024-03-15");
2995 }
2996
2997 #[test]
2998 fn test_null_input() {
2999 assert_eq!(DateFunc.invoke(&[null()]).unwrap(), SqliteValue::Null);
3000 }
3001
3002 #[test]
3003 fn test_invalid_input() {
3004 assert_eq!(
3005 DateFunc.invoke(&[text("not-a-date")]).unwrap(),
3006 SqliteValue::Null
3007 );
3008 }
3009
3010 #[test]
3011 fn test_negative_time_component_invalid() {
3012 let r = TimeFunc.invoke(&[text("-01:00")]).unwrap();
3013 assert_eq!(r, SqliteValue::Null);
3014 }
3015
3016 #[test]
3019 fn test_leap_year() {
3020 let r = DateFunc
3021 .invoke(&[text("2024-02-28"), text("+1 days")])
3022 .unwrap();
3023 assert_text(&r, "2024-02-29");
3024 }
3025
3026 #[test]
3027 fn test_non_leap_year() {
3028 let r = DateFunc
3029 .invoke(&[text("2023-02-28"), text("+1 days")])
3030 .unwrap();
3031 assert_text(&r, "2023-03-01");
3032 }
3033
3034 #[test]
3037 fn test_strftime_basic() {
3038 let r = StrftimeFunc
3039 .invoke(&[text("%Y-%m-%d"), text("2024-03-15")])
3040 .unwrap();
3041 assert_text(&r, "2024-03-15");
3042 }
3043
3044 #[test]
3045 fn test_strftime_time_specifiers() {
3046 let r = StrftimeFunc
3047 .invoke(&[text("%H:%M:%S"), text("2024-03-15 14:30:45")])
3048 .unwrap();
3049 assert_text(&r, "14:30:45");
3050 }
3051
3052 #[test]
3053 fn test_strftime_unix_seconds() {
3054 let r = StrftimeFunc
3055 .invoke(&[text("%s"), text("1970-01-01 00:00:00")])
3056 .unwrap();
3057 assert_text(&r, "0");
3058 }
3059
3060 #[test]
3061 fn test_strftime_day_of_year() {
3062 let r = StrftimeFunc
3063 .invoke(&[text("%j"), text("2024-03-15")])
3064 .unwrap();
3065 assert_text(&r, "075");
3067 }
3068
3069 #[test]
3070 fn test_strftime_day_of_week() {
3071 let r = StrftimeFunc
3073 .invoke(&[text("%w"), text("2024-03-15")])
3074 .unwrap();
3075 assert_text(&r, "5");
3076
3077 let r = StrftimeFunc
3078 .invoke(&[text("%u"), text("2024-03-15")])
3079 .unwrap();
3080 assert_text(&r, "5");
3081 }
3082
3083 #[test]
3084 fn test_strftime_12hour() {
3085 let r = StrftimeFunc
3086 .invoke(&[text("%I %p"), text("2024-03-15 14:30:00")])
3087 .unwrap();
3088 assert_text(&r, "02 PM");
3089
3090 let r = StrftimeFunc
3091 .invoke(&[text("%I %P"), text("2024-03-15 09:30:00")])
3092 .unwrap();
3093 assert_text(&r, "09 am");
3094 }
3095
3096 #[test]
3097 fn test_strftime_all_specifiers_presence() {
3098 let fmt = "%d|%e|%f|%H|%I|%j|%J|%k|%l|%m|%M|%p|%P|%R|%s|%S|%T|%u|%w|%W|%G|%g|%V|%Y|%%";
3099 let r = StrftimeFunc
3100 .invoke(&[text(fmt), text("2024-03-15 14:30:45.123")])
3101 .unwrap();
3102
3103 let s = match r {
3104 SqliteValue::Text(v) => v,
3105 other => panic!("expected Text, got {other:?}"),
3106 };
3107 let parts: Vec<&str> = s.split('|').collect();
3108 assert_eq!(parts.len(), 25, "unexpected specifier output: {s}");
3109 assert_eq!(parts[0], "15"); assert_eq!(parts[1], "15"); assert_eq!(parts[2], "45.123"); assert_eq!(parts[3], "14"); assert_eq!(parts[4], "02"); assert_eq!(parts[5], "075"); assert!(
3116 parts[6].parse::<f64>().is_ok(),
3117 "expected numeric %J output, got {}",
3118 parts[6]
3119 );
3120 assert_eq!(parts[7], "14"); assert_eq!(parts[8], " 2"); assert_eq!(parts[9], "03"); assert_eq!(parts[10], "30"); assert_eq!(parts[11], "PM"); assert_eq!(parts[12], "pm"); assert_eq!(parts[13], "14:30"); assert!(
3128 parts[14].parse::<i64>().is_ok(),
3129 "expected numeric %s output, got {}",
3130 parts[14]
3131 );
3132 assert_eq!(parts[15], "45"); assert_eq!(parts[16], "14:30:45"); assert_eq!(parts[17], "5"); assert_eq!(parts[18], "5"); assert_eq!(parts[19], "11"); assert_eq!(parts[20], "2024"); assert_eq!(parts[21], "24"); assert_eq!(parts[22], "11"); assert_eq!(parts[23], "2024"); assert_eq!(parts[24], "%"); }
3143
3144 #[test]
3145 fn test_strftime_null() {
3146 assert_eq!(
3147 StrftimeFunc.invoke(&[null(), text("2024-01-01")]).unwrap(),
3148 SqliteValue::Null
3149 );
3150 assert_eq!(
3151 StrftimeFunc.invoke(&[text("%Y"), null()]).unwrap(),
3152 SqliteValue::Null
3153 );
3154 }
3155
3156 #[test]
3157 #[ignore = "perf-only benchmark"]
3158 fn perf_strftime_timestamp_rows() {
3159 use std::hint::black_box;
3160 use std::time::Instant;
3161
3162 const ROWS: usize = 200_000;
3163 const REPEATS: usize = 5;
3164 const FORMAT: &str = "%Y-%m-%d %H:%M:%S";
3165 const INPUT: &str = "2024-03-15 14:30:45";
3166
3167 let func = StrftimeFunc;
3168 let fmt = text(FORMAT);
3169 let input = text(INPUT);
3170 let mut best_ns = u128::MAX;
3171 let mut output_len = 0usize;
3172
3173 for _ in 0..REPEATS {
3174 let started = Instant::now();
3175 for _ in 0..ROWS {
3176 let result = black_box(
3177 func.invoke(black_box(&[fmt.clone(), input.clone()]))
3178 .expect("strftime benchmark invocation must succeed"),
3179 );
3180 output_len = match result {
3181 SqliteValue::Text(text) => text.len(),
3182 SqliteValue::Null
3183 | SqliteValue::Integer(_)
3184 | SqliteValue::Float(_)
3185 | SqliteValue::Blob(_) => 0,
3186 };
3187 }
3188 let elapsed_ns = started.elapsed().as_nanos();
3189 if elapsed_ns < best_ns {
3190 best_ns = elapsed_ns;
3191 }
3192 }
3193
3194 println!(
3195 "strftime_timestamp_rows rows={ROWS} repeats={REPEATS} best_ns={best_ns} output_len={output_len}"
3196 );
3197 }
3198
3199 #[test]
3202 fn test_timediff_basic() {
3203 let r = TimediffFunc
3204 .invoke(&[text("2024-03-15"), text("2024-03-10")])
3205 .unwrap();
3206 assert_text(&r, "+0000-00-05 00:00:00.000");
3207 }
3208
3209 #[test]
3210 fn test_timediff_negative() {
3211 let r = TimediffFunc
3212 .invoke(&[text("2024-03-10"), text("2024-03-15")])
3213 .unwrap();
3214 assert_text(&r, "-0000-00-05 00:00:00.000");
3215 }
3216
3217 #[test]
3218 fn test_timediff_year_boundary() {
3219 let r = TimediffFunc
3220 .invoke(&[text("2024-01-01 01:00:00"), text("2023-12-31 23:00:00")])
3221 .unwrap();
3222 assert_text(&r, "+0000-00-00 02:00:00.000");
3223 }
3224
3225 #[test]
3228 fn test_modifier_subsec() {
3229 assert_text(
3230 &TimeFunc
3231 .invoke(&[text("2024-01-01 12:00:00"), text("subsec")])
3232 .unwrap(),
3233 "12:00:00.000",
3234 );
3235 assert_text(
3236 &DateTimeFunc
3237 .invoke(&[text("2024-01-01 12:00:00"), text("subsecond")])
3238 .unwrap(),
3239 "2024-01-01 12:00:00.000",
3240 );
3241 assert_text(
3242 &TimeFunc
3243 .invoke(&[text("2024-01-01 12:00:00.123"), text("subsec")])
3244 .unwrap(),
3245 "12:00:00.123",
3246 );
3247 }
3248
3249 #[test]
3250 fn test_subsec_unix_seconds_and_integer_flooring() {
3251 assert_text(
3252 &StrftimeFunc
3253 .invoke(&[text("%s"), text("1970-01-01 00:00:00.125"), text("subsec")])
3254 .unwrap(),
3255 "0.125",
3256 );
3257 assert_eq!(
3258 UnixepochFunc
3259 .invoke(&[text("1970-01-01 00:00:00.125"), text("subsec")])
3260 .unwrap(),
3261 float(0.125)
3262 );
3263
3264 for input in ["1970-01-01 00:00:00.500", "1970-01-01 00:00:00.999"] {
3265 assert_eq!(UnixepochFunc.invoke(&[text(input)]).unwrap(), int(0));
3266 assert_text(
3267 &StrftimeFunc.invoke(&[text("%s"), text(input)]).unwrap(),
3268 "0",
3269 );
3270 }
3271 assert_eq!(
3272 UnixepochFunc
3273 .invoke(&[text("1969-12-31 23:59:59.999")])
3274 .unwrap(),
3275 int(-1)
3276 );
3277 assert_text(
3278 &StrftimeFunc
3279 .invoke(&[text("%s"), text("1969-12-31 23:59:59.999")])
3280 .unwrap(),
3281 "-1",
3282 );
3283 }
3284
3285 #[test]
3286 fn test_subsec_rounding_preserves_sqlite_second_60() {
3287 assert_text(
3288 &TimeFunc
3289 .invoke(&[text("12:34:59.9995"), text("subsec")])
3290 .unwrap(),
3291 "12:34:60.000",
3292 );
3293 assert_text(
3294 &DateTimeFunc
3295 .invoke(&[text("1970-01-01 23:59:59.9995"), text("subsec")])
3296 .unwrap(),
3297 "1970-01-01 23:59:60.000",
3298 );
3299 assert_text(
3300 &StrftimeFunc
3301 .invoke(&[
3302 text("%H:%M:%f|%s"),
3303 text("1970-01-01 23:59:59.9995"),
3304 text("subsec"),
3305 ])
3306 .unwrap(),
3307 "23:59:59.999|86400.000",
3308 );
3309 }
3310
3311 #[test]
3314 fn test_register_datetime_builtins_all_present() {
3315 let mut reg = FunctionRegistry::new();
3316 register_datetime_builtins(&mut reg);
3317
3318 let expected = [
3319 "date",
3320 "time",
3321 "datetime",
3322 "julianday",
3323 "unixepoch",
3324 "strftime",
3325 "timediff",
3326 ];
3327
3328 for name in expected {
3329 assert!(
3330 reg.find_scalar(name, 1).is_some() || reg.find_scalar(name, 2).is_some(),
3331 "datetime function '{name}' not registered"
3332 );
3333 }
3334 }
3335
3336 #[test]
3337 fn test_public_datetime_function_metadata_fails_closed_for_direct_registration() {
3338 assert!(!DateFunc.is_deterministic());
3339 assert!(!TimeFunc.is_deterministic());
3340 assert!(!DateTimeFunc.is_deterministic());
3341 assert!(!JuliandayFunc.is_deterministic());
3342 assert!(!UnixepochFunc.is_deterministic());
3343 assert!(!StrftimeFunc.is_deterministic());
3344 assert!(!TimediffFunc.is_deterministic());
3345
3346 let mut registry = FunctionRegistry::new();
3347 registry.register_scalar(DateFunc);
3348 registry.register_scalar(TimeFunc);
3349 registry.register_scalar(DateTimeFunc);
3350 registry.register_scalar(JuliandayFunc);
3351 registry.register_scalar(UnixepochFunc);
3352 registry.register_scalar(StrftimeFunc);
3353 registry.register_scalar(TimediffFunc);
3354 for (name, num_args) in [
3355 ("date", 0),
3356 ("time", 0),
3357 ("datetime", 0),
3358 ("julianday", 0),
3359 ("unixepoch", 0),
3360 ("strftime", 1),
3361 ("timediff", 2),
3362 ] {
3363 assert_eq!(
3364 registry.scalar_schema_safety(name, num_args),
3365 Some(crate::ScalarSchemaSafety::Never),
3366 "generic registration of {name}/{num_args} must fail closed"
3367 );
3368 }
3369 }
3370
3371 #[test]
3374 fn test_modifier_year_overflow() {
3375 let huge = i64::MAX;
3378 let modifier = format!("+{huge} years");
3379 let r = DateFunc.invoke(&[text("2000-01-01"), text(&modifier)]);
3380 assert_eq!(r.unwrap(), SqliteValue::Null);
3383 }
3384
3385 #[test]
3386 fn test_jdn_roundtrip() {
3387 let dates = [
3389 (2024, 3, 15),
3390 (2000, 1, 1),
3391 (1970, 1, 1),
3392 (2024, 2, 29),
3393 (1900, 1, 1),
3394 (2099, 12, 31),
3395 ];
3396 for (y, m, d) in dates {
3397 let jdn = ymd_to_jdn(y, m, d);
3398 let (y2, m2, d2) = jdn_to_ymd(jdn);
3399 assert_eq!(
3400 (y, m, d),
3401 (y2, m2, d2),
3402 "roundtrip failed for {y}-{m}-{d} (JDN={jdn})"
3403 );
3404 }
3405 }
3406
3407 #[test]
3408 fn test_unix_epoch_roundtrip() {
3409 let jdn = ymd_to_jdn(1970, 1, 1);
3410 let unix = jdn_to_unix(jdn);
3411 assert_eq!(unix, 0, "Unix epoch should be 0");
3412
3413 let jdn2 = unix_to_jdn(0.0);
3414 assert!((jdn2 - UNIX_EPOCH_JDN).abs() < 1e-10, "roundtrip failed");
3415 }
3416}