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 num = if let Ok(n) = num_str.parse::<i64>() {
764 n
765 } else if let Ok(f) = num_str.parse::<f64>() {
766 if f.fract() == 0.0 && f >= i64::MIN as f64 && f <= i64::MAX as f64 {
767 f as i64
768 } else {
769 return Err(());
770 }
771 } else {
772 return Err(());
773 };
774
775 let (y, mo, d) = jdn_to_ymd(jdn);
776 let (h, mi, s, frac) = jdn_to_hms(jdn);
777
778 let total_months = match unit.trim_end_matches('s') {
779 "month" => {
780 if let Some(val) = num.checked_mul(sign) {
781 val
782 } else {
783 return Ok(None);
784 }
785 }
786 "year" => {
787 if let Some(val) = num.checked_mul(sign).and_then(|v| v.checked_mul(12)) {
788 val
789 } else {
790 return Ok(None);
791 }
792 }
793 _ => return Err(()),
794 };
795
796 let current_months = if let Some(val) = y.checked_mul(12).and_then(|v| v.checked_add(mo - 1)) {
798 val
799 } else {
800 return Ok(None);
801 };
802 let new_total = if let Some(val) = current_months.checked_add(total_months) {
803 val
804 } else {
805 return Ok(None);
806 };
807
808 let new_y = new_total.div_euclid(12);
809 let new_mo = new_total.rem_euclid(12) + 1;
810 let n_floor = compute_floor(new_y, new_mo, d);
815 Ok(Some((
816 ymdhms_to_jdn(new_y, new_mo, d, h, mi, s, frac),
817 n_floor,
818 )))
819}
820
821struct StackStr {
827 buf: [u8; 48],
828 len: usize,
829}
830
831impl StackStr {
832 fn new() -> Self {
833 Self {
834 buf: [0; 48],
835 len: 0,
836 }
837 }
838
839 fn as_str(&self) -> &str {
840 core::str::from_utf8(&self.buf[..self.len]).unwrap_or("")
842 }
843}
844
845impl core::fmt::Write for StackStr {
846 fn write_str(&mut self, s: &str) -> core::fmt::Result {
847 let end = self.len + s.len();
848 if end > self.buf.len() {
849 return Err(core::fmt::Error);
850 }
851 self.buf[self.len..end].copy_from_slice(s.as_bytes());
852 self.len = end;
853 Ok(())
854 }
855}
856
857fn build_small_text(write: impl Fn(&mut dyn core::fmt::Write) -> core::fmt::Result) -> SmallText {
864 let mut buf = StackStr::new();
865 if write(&mut buf).is_ok() {
866 SmallText::new(buf.as_str())
867 } else {
868 let mut heap = String::new();
869 let _ = write(&mut heap);
870 SmallText::from_string(heap)
871 }
872}
873
874fn format_date(jdn: f64) -> SmallText {
875 let (y, m, d) = jdn_to_ymd(jdn);
876 build_small_text(move |w| write!(w, "{y:04}-{m:02}-{d:02}"))
877}
878
879#[derive(Clone, Copy)]
880struct UnmodifiedHms {
881 hour: i64,
882 minute: i64,
883 second: i64,
884 fraction: f64,
885}
886
887fn hms_for_output(jdn: f64, unmodified: Option<UnmodifiedHms>) -> UnmodifiedHms {
888 unmodified.unwrap_or_else(|| {
889 let (hour, minute, second, fraction) = jdn_to_hms(jdn);
890 UnmodifiedHms {
891 hour,
892 minute,
893 second,
894 fraction,
895 }
896 })
897}
898
899fn rounded_second_and_millis(hms: UnmodifiedHms) -> (i64, i64) {
900 let total_millis = ((hms.second as f64 + hms.fraction) * 1000.0 + 0.5).floor() as i64;
904 (total_millis / 1000, total_millis % 1000)
905}
906
907fn format_time(jdn: f64, subsec: bool, unmodified: Option<UnmodifiedHms>) -> SmallText {
908 let hms = hms_for_output(jdn, unmodified);
909 let (h, m) = (hms.hour, hms.minute);
910 if subsec {
911 let (s, ms) = rounded_second_and_millis(hms);
912 build_small_text(move |w| write!(w, "{h:02}:{m:02}:{s:02}.{ms:03}"))
913 } else {
914 let s = hms.second;
915 build_small_text(move |w| write!(w, "{h:02}:{m:02}:{s:02}"))
916 }
917}
918
919fn format_datetime(jdn: f64, subsec: bool, unmodified: Option<UnmodifiedHms>) -> SmallText {
920 let (y, mo, d) = jdn_to_ymd(jdn);
921 let hms = hms_for_output(jdn, unmodified);
922 let (h, mi) = (hms.hour, hms.minute);
923 if subsec {
924 let (s, ms) = rounded_second_and_millis(hms);
925 build_small_text(move |w| write!(w, "{y:04}-{mo:02}-{d:02} {h:02}:{mi:02}:{s:02}.{ms:03}"))
926 } else {
927 let s = hms.second;
928 build_small_text(move |w| write!(w, "{y:04}-{mo:02}-{d:02} {h:02}:{mi:02}:{s:02}"))
929 }
930}
931
932#[inline]
933fn push_format(result: &mut String, args: Arguments<'_>) {
934 let _ = result.write_fmt(args);
935}
936
937#[inline]
938fn push_zero_padded_2(result: &mut String, value: i64) {
939 if (0..=99).contains(&value) {
940 let value = value as u8;
941 result.push(char::from(b'0' + value / 10));
942 result.push(char::from(b'0' + value % 10));
943 } else {
944 push_format(result, format_args!("{value:02}"));
945 }
946}
947
948#[inline]
949fn push_space_padded_2(result: &mut String, value: i64) {
950 if (0..=99).contains(&value) {
951 let value = value as u8;
952 if value >= 10 {
953 result.push(char::from(b'0' + value / 10));
954 } else {
955 result.push(' ');
956 }
957 result.push(char::from(b'0' + value % 10));
958 } else {
959 push_format(result, format_args!("{value:>2}"));
960 }
961}
962
963#[inline]
964fn push_zero_padded_3(result: &mut String, value: i64) {
965 if (0..=999).contains(&value) {
966 let value = value as u16;
967 result.push(char::from(b'0' + (value / 100) as u8));
968 result.push(char::from(b'0' + ((value / 10) % 10) as u8));
969 result.push(char::from(b'0' + (value % 10) as u8));
970 } else {
971 push_format(result, format_args!("{value:03}"));
972 }
973}
974
975#[inline]
976fn push_zero_padded_4(result: &mut String, value: i64) {
977 if (0..=9999).contains(&value) {
978 let value = value as u16;
979 result.push(char::from(b'0' + (value / 1000) as u8));
980 result.push(char::from(b'0' + ((value / 100) % 10) as u8));
981 result.push(char::from(b'0' + ((value / 10) % 10) as u8));
982 result.push(char::from(b'0' + (value % 10) as u8));
983 } else {
984 push_format(result, format_args!("{value:04}"));
985 }
986}
987
988fn format_strftime(fmt: &str, jdn: f64, subsec: bool, unmodified: Option<UnmodifiedHms>) -> String {
990 let (y, mo, d) = jdn_to_ymd(jdn);
991 let hms = hms_for_output(jdn, unmodified);
992 let (h, mi, s, frac) = (hms.hour, hms.minute, hms.second, hms.fraction);
993 let doy = day_of_year(y, mo, d);
994 let jdn_int = (jdn + 0.5).floor() as i64;
996 let dow = (jdn_int + 1) % 7; let mut result = String::with_capacity(fmt.len().saturating_add(8));
999 let bytes = fmt.as_bytes();
1000 let mut i = 0;
1001 let mut literal_start = 0;
1002
1003 while i < bytes.len() {
1004 if bytes[i] != b'%' || i + 1 >= bytes.len() {
1005 i += 1;
1006 continue;
1007 }
1008
1009 result.push_str(&fmt[literal_start..i]);
1010
1011 let spec_suffix = &fmt[i + 1..];
1012 let Some(spec) = spec_suffix.chars().next() else {
1013 break;
1014 };
1015 i += 1 + spec.len_utf8();
1016 literal_start = i;
1017
1018 match spec {
1019 'd' => push_zero_padded_2(&mut result, d),
1020 'e' => push_space_padded_2(&mut result, d),
1021 'F' => {
1022 push_zero_padded_4(&mut result, y);
1024 result.push('-');
1025 push_zero_padded_2(&mut result, mo);
1026 result.push('-');
1027 push_zero_padded_2(&mut result, d);
1028 }
1029 'f' => {
1030 let total = (s as f64 + frac).min(59.999);
1032 push_format(&mut result, format_args!("{total:06.3}"));
1033 }
1034 'H' => push_zero_padded_2(&mut result, h),
1035 'I' => {
1036 let h12 = if h == 0 {
1038 12
1039 } else if h > 12 {
1040 h - 12
1041 } else {
1042 h
1043 };
1044 push_zero_padded_2(&mut result, h12);
1045 }
1046 'j' => push_zero_padded_3(&mut result, doy),
1047 'J' => {
1048 push_format(&mut result, format_args!("{jdn:.15}"));
1050 while result.as_bytes().last() == Some(&b'0') {
1051 result.pop();
1052 }
1053 if result.as_bytes().last() == Some(&b'.') {
1054 result.pop();
1055 }
1056 }
1057 'k' => {
1058 push_space_padded_2(&mut result, h);
1060 }
1061 'l' => {
1062 let h12 = if h == 0 {
1064 12
1065 } else if h > 12 {
1066 h - 12
1067 } else {
1068 h
1069 };
1070 push_space_padded_2(&mut result, h12);
1071 }
1072 'm' => push_zero_padded_2(&mut result, mo),
1073 'M' => push_zero_padded_2(&mut result, mi),
1074 'p' => {
1075 result.push_str(if h < 12 { "AM" } else { "PM" });
1076 }
1077 'P' => {
1078 result.push_str(if h < 12 { "am" } else { "pm" });
1079 }
1080 'R' => {
1081 push_zero_padded_2(&mut result, h);
1082 result.push(':');
1083 push_zero_padded_2(&mut result, mi);
1084 }
1085 's' => {
1086 if subsec {
1087 let unix = jdn_to_unix_subsec(jdn);
1088 push_format(&mut result, format_args!("{unix:.3}"));
1089 } else {
1090 let unix = jdn_to_unix(jdn);
1091 push_format(&mut result, format_args!("{unix}"));
1092 }
1093 }
1094 'S' => push_zero_padded_2(&mut result, s),
1095 'T' => {
1096 push_zero_padded_2(&mut result, h);
1097 result.push(':');
1098 push_zero_padded_2(&mut result, mi);
1099 result.push(':');
1100 push_zero_padded_2(&mut result, s);
1101 }
1102 'u' => {
1103 let u = if dow == 0 { 7 } else { dow };
1105 push_format(&mut result, format_args!("{u}"));
1106 }
1107 'w' => push_format(&mut result, format_args!("{dow}")),
1108 'W' => {
1109 let w = (doy + 6 - ((dow + 6) % 7)) / 7;
1111 push_zero_padded_2(&mut result, w);
1112 }
1113 'Y' => push_zero_padded_4(&mut result, y),
1114 'G' | 'g' | 'V' => {
1115 let (iso_y, iso_w) = iso_week(y, mo, d);
1117 match spec {
1118 'G' => push_zero_padded_4(&mut result, iso_y),
1119 'g' => push_zero_padded_2(&mut result, iso_y % 100),
1120 'V' => push_zero_padded_2(&mut result, iso_w),
1121 _ => unreachable!(),
1122 }
1123 }
1124 '%' => result.push('%'),
1125 other => {
1126 result.push('%');
1127 result.push(other);
1128 }
1129 }
1130 }
1131
1132 if literal_start < fmt.len() {
1133 result.push_str(&fmt[literal_start..]);
1134 }
1135
1136 result
1137}
1138
1139fn iso_week(y: i64, m: i64, d: i64) -> (i64, i64) {
1141 let jdn = ymd_to_jdn(y, m, d);
1142 let jdn_int = (jdn + 0.5).floor() as i64;
1143 let dow = (jdn_int + 1) % 7;
1145 let iso_dow = if dow == 0 { 7 } else { dow };
1146
1147 let thu_jdn = jdn_int + (4 - iso_dow);
1149 let (thu_y, _, _) = jdn_to_ymd(thu_jdn as f64);
1150
1151 let jan4_jdn = (ymd_to_jdn(thu_y, 1, 4) + 0.5).floor() as i64;
1153 let jan4_dow = (jan4_jdn + 1) % 7;
1154 let jan4_iso_dow = if jan4_dow == 0 { 7 } else { jan4_dow };
1155 let week1_start = jan4_jdn - (jan4_iso_dow - 1);
1156
1157 let week = (thu_jdn - week1_start) / 7 + 1;
1158 (thu_y, week)
1159}
1160
1161fn timediff_impl(jdn1: f64, jdn2: f64) -> String {
1164 let (sign, start_jdn, end_jdn) = if jdn1 >= jdn2 {
1165 ('+', jdn2, jdn1)
1166 } else {
1167 ('-', jdn1, jdn2)
1168 };
1169
1170 let (start_y, start_mo, start_d) = jdn_to_ymd(start_jdn);
1171 let (start_h, start_mi, mut start_s, start_frac) = jdn_to_hms(start_jdn);
1172 let mut start_ms = (start_frac * 1000.0).round() as i64;
1173 if start_ms >= 1000 {
1174 start_ms = 0;
1175 start_s += 1;
1176 }
1177
1178 let (end_y, end_mo, end_d) = jdn_to_ymd(end_jdn);
1179 let (end_h, end_mi, mut end_s, end_frac) = jdn_to_hms(end_jdn);
1180 let mut end_ms = (end_frac * 1000.0).round() as i64;
1181 if end_ms >= 1000 {
1182 end_ms = 0;
1183 end_s += 1;
1184 }
1185
1186 let mut years = end_y - start_y;
1187 let mut months = end_mo - start_mo;
1188 let mut days = end_d - start_d;
1189 let mut hours = end_h - start_h;
1190 let mut minutes = end_mi - start_mi;
1191 let mut seconds = end_s - start_s;
1192 let mut millis = end_ms - start_ms;
1193
1194 if millis < 0 {
1195 millis += 1000;
1196 seconds -= 1;
1197 }
1198 if seconds < 0 {
1199 seconds += 60;
1200 minutes -= 1;
1201 }
1202 if minutes < 0 {
1203 minutes += 60;
1204 hours -= 1;
1205 }
1206 if hours < 0 {
1207 hours += 24;
1208 days -= 1;
1209 }
1210 if days < 0 {
1211 months -= 1;
1212 let (borrow_y, borrow_mo) = if end_mo == 1 {
1213 (end_y - 1, 12)
1214 } else {
1215 (end_y, end_mo - 1)
1216 };
1217 days += days_in_month(borrow_y, borrow_mo);
1218 }
1219 if months < 0 {
1220 months += 12;
1221 years -= 1;
1222 }
1223
1224 format!(
1225 "{sign}{years:04}-{months:02}-{days:02} {hours:02}:{minutes:02}:{seconds:02}.{millis:03}"
1226 )
1227}
1228
1229#[must_use]
1256pub fn is_datetime_invocation_safe_for_schema(function_name: &str, args: &[SqliteValue]) -> bool {
1257 if function_name.eq_ignore_ascii_case("strftime") {
1258 return args.first().is_none_or(SqliteValue::is_null)
1262 || datetime_arguments_are_schema_safe(&args[1..]);
1263 }
1264
1265 if function_name.eq_ignore_ascii_case("timediff") {
1266 if args.len() != 2 {
1270 return true;
1271 }
1272 for time_value in args {
1273 match classify_time_value_for_schema(time_value) {
1274 SchemaTimeValue::Dynamic => return false,
1275 SchemaTimeValue::NullOrInvalid => return true,
1276 SchemaTimeValue::Fixed { .. } => {}
1277 }
1278 }
1279 return true;
1280 }
1281
1282 if function_name.eq_ignore_ascii_case("date")
1283 || function_name.eq_ignore_ascii_case("time")
1284 || function_name.eq_ignore_ascii_case("datetime")
1285 || function_name.eq_ignore_ascii_case("julianday")
1286 || function_name.eq_ignore_ascii_case("unixepoch")
1287 {
1288 return datetime_arguments_are_schema_safe(args);
1289 }
1290
1291 true
1292}
1293
1294fn datetime_arguments_are_schema_safe(args: &[SqliteValue]) -> bool {
1296 let Some(time_value) = args.first() else {
1297 return false;
1298 };
1299 let (input, raw_numeric) = match classify_time_value_for_schema(time_value) {
1300 SchemaTimeValue::Dynamic => return false,
1301 SchemaTimeValue::NullOrInvalid => return true,
1302 SchemaTimeValue::Fixed { input, raw_numeric } => (input, raw_numeric),
1303 };
1304
1305 let mut reached_modifiers = Vec::with_capacity(args.len().saturating_sub(1));
1306 for modifier in &args[1..] {
1307 if modifier.is_null() {
1308 return true;
1309 }
1310 if sqlite_value_is_keyword(modifier, "localtime")
1311 || sqlite_value_is_keyword(modifier, "utc")
1312 {
1313 return false;
1314 }
1315 let Some(modifier) = sqlite_value_datetime_text(modifier) else {
1316 return true;
1317 };
1318 reached_modifiers.push(modifier.into_owned());
1319 if apply_modifiers(input, &reached_modifiers, raw_numeric).is_none() {
1320 return true;
1323 }
1324 }
1325 true
1326}
1327
1328enum SchemaTimeValue {
1329 Dynamic,
1330 NullOrInvalid,
1331 Fixed { input: f64, raw_numeric: bool },
1332}
1333
1334fn classify_time_value_for_schema(value: &SqliteValue) -> SchemaTimeValue {
1335 if value.is_null() {
1336 return SchemaTimeValue::NullOrInvalid;
1337 }
1338 if is_implicit_now_time_value(value) {
1339 return SchemaTimeValue::Dynamic;
1340 }
1341 match parse_time_value(value) {
1342 Some(parsed) => SchemaTimeValue::Fixed {
1343 input: parsed.jdn,
1344 raw_numeric: parsed.raw_numeric,
1345 },
1346 None => SchemaTimeValue::NullOrInvalid,
1347 }
1348}
1349
1350fn is_implicit_now_time_value(value: &SqliteValue) -> bool {
1351 sqlite_value_is_keyword(value, "now")
1352 || sqlite_value_is_keyword(value, "subsec")
1353 || sqlite_value_is_keyword(value, "subsecond")
1354}
1355
1356fn sqlite_value_is_keyword(value: &SqliteValue, keyword: &str) -> bool {
1360 let bytes = match value {
1361 SqliteValue::Text(text) => sqlite_c_string_bytes(text.as_bytes_direct()),
1362 SqliteValue::Blob(bytes) => sqlite_c_string_bytes(bytes),
1363 SqliteValue::Null | SqliteValue::Integer(_) | SqliteValue::Float(_) => return false,
1364 };
1365 bytes.eq_ignore_ascii_case(keyword.as_bytes())
1366}
1367
1368#[derive(Clone, Copy)]
1369struct ParsedTimeValue {
1370 jdn: f64,
1371 raw_numeric: bool,
1372 unmodified_hms: Option<UnmodifiedHms>,
1373}
1374
1375fn parse_time_value(value: &SqliteValue) -> Option<ParsedTimeValue> {
1376 match value {
1377 SqliteValue::Null => None,
1378 SqliteValue::Integer(integer) => Some(ParsedTimeValue {
1379 jdn: *integer as f64,
1380 raw_numeric: true,
1381 unmodified_hms: None,
1382 }),
1383 SqliteValue::Float(float) if float.is_finite() => Some(ParsedTimeValue {
1384 jdn: *float,
1385 raw_numeric: true,
1386 unmodified_hms: None,
1387 }),
1388 SqliteValue::Float(_) => None,
1389 SqliteValue::Text(_) | SqliteValue::Blob(_) => {
1390 let text = sqlite_value_datetime_text(value)?;
1391 let numeric = text.trim_matches(|c: char| c.is_ascii_whitespace());
1392 if let Ok(number) = numeric.parse::<f64>()
1393 && number.is_finite()
1394 {
1395 return Some(ParsedTimeValue {
1396 jdn: number,
1397 raw_numeric: true,
1398 unmodified_hms: None,
1399 });
1400 }
1401 Some(ParsedTimeValue {
1402 jdn: parse_timestring(text.as_ref())?,
1403 raw_numeric: false,
1404 unmodified_hms: unmodified_hms_from_timestring(text.as_ref()),
1405 })
1406 }
1407 }
1408}
1409
1410fn unmodified_hms_from_timestring(value: &str) -> Option<UnmodifiedHms> {
1411 let value = sqlite_c_string_str(value).trim_end_matches(|c: char| c.is_ascii_whitespace());
1412 let time = if value.len() > 10
1413 && value.as_bytes().get(4) == Some(&b'-')
1414 && value.as_bytes().get(7) == Some(&b'-')
1415 && value
1416 .as_bytes()
1417 .get(10)
1418 .is_some_and(|separator| matches!(*separator, b' ' | b'T'))
1419 {
1420 &value[11..]
1421 } else if value.len() >= 5 && value.as_bytes().get(2) == Some(&b':') {
1422 value
1423 } else {
1424 return None;
1425 };
1426 let (hour, minute, second, fraction, timezone_offset) = parse_time_part_with_tz(time)?;
1427 (timezone_offset == 0).then_some(UnmodifiedHms {
1428 hour,
1429 minute,
1430 second,
1431 fraction,
1432 })
1433}
1434
1435struct ParsedDateTimeArgs {
1436 jdn: f64,
1437 subsec: bool,
1438 unmodified_hms: Option<UnmodifiedHms>,
1439}
1440
1441fn parse_args(args: &[SqliteValue]) -> Option<ParsedDateTimeArgs> {
1443 let first_position_subsec = args.first().is_some_and(|value| {
1444 sqlite_value_is_keyword(value, "subsec") || sqlite_value_is_keyword(value, "subsecond")
1445 });
1446 let parsed = match args.first() {
1447 None => ParsedTimeValue {
1448 jdn: current_time_jdn(),
1449 raw_numeric: false,
1450 unmodified_hms: None,
1451 },
1452 Some(value) => parse_time_value(value)?,
1453 };
1454
1455 let mut modifiers = Vec::with_capacity(args.len().saturating_sub(1));
1456 for modifier in args.get(1..).unwrap_or_default() {
1457 modifiers.push(sqlite_value_datetime_text(modifier)?.into_owned());
1458 }
1459
1460 if parsed.raw_numeric {
1465 let first = modifiers
1466 .first()
1467 .map(|modifier| modifier.to_ascii_lowercase());
1468 let reinterprets_raw = matches!(first.as_deref(), Some("unixepoch" | "julianday" | "auto"));
1469 if !reinterprets_raw && !(0.0..=AUTO_JDN_MAX).contains(&parsed.jdn) {
1470 return None;
1471 }
1472 }
1473
1474 let preserves_input_hms = modifiers.iter().all(|modifier| {
1475 modifier.eq_ignore_ascii_case("subsec") || modifier.eq_ignore_ascii_case("subsecond")
1476 });
1477 let (jdn, modifier_subsec) = apply_modifiers(parsed.jdn, &modifiers, parsed.raw_numeric)?;
1478 Some(ParsedDateTimeArgs {
1479 jdn,
1480 subsec: first_position_subsec || modifier_subsec,
1481 unmodified_hms: preserves_input_hms
1482 .then_some(parsed.unmodified_hms)
1483 .flatten(),
1484 })
1485}
1486
1487pub struct DateFunc;
1490
1491impl ScalarFunction for DateFunc {
1492 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1493 match parse_args(args) {
1494 Some(parsed) => Ok(SqliteValue::Text(format_date(parsed.jdn))),
1495 None => Ok(SqliteValue::Null),
1496 }
1497 }
1498
1499 fn num_args(&self) -> i32 {
1500 -1
1501 }
1502
1503 fn is_deterministic(&self) -> bool {
1504 false
1505 }
1506
1507 fn name(&self) -> &str {
1508 "date"
1509 }
1510}
1511
1512pub struct TimeFunc;
1515
1516impl ScalarFunction for TimeFunc {
1517 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1518 match parse_args(args) {
1519 Some(parsed) => Ok(SqliteValue::Text(format_time(
1520 parsed.jdn,
1521 parsed.subsec,
1522 parsed.unmodified_hms,
1523 ))),
1524 None => Ok(SqliteValue::Null),
1525 }
1526 }
1527
1528 fn num_args(&self) -> i32 {
1529 -1
1530 }
1531
1532 fn is_deterministic(&self) -> bool {
1533 false
1534 }
1535
1536 fn name(&self) -> &str {
1537 "time"
1538 }
1539}
1540
1541pub struct DateTimeFunc;
1544
1545impl ScalarFunction for DateTimeFunc {
1546 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1547 match parse_args(args) {
1548 Some(parsed) => Ok(SqliteValue::Text(format_datetime(
1549 parsed.jdn,
1550 parsed.subsec,
1551 parsed.unmodified_hms,
1552 ))),
1553 None => Ok(SqliteValue::Null),
1554 }
1555 }
1556
1557 fn num_args(&self) -> i32 {
1558 -1
1559 }
1560
1561 fn is_deterministic(&self) -> bool {
1562 false
1563 }
1564
1565 fn name(&self) -> &str {
1566 "datetime"
1567 }
1568}
1569
1570pub struct JuliandayFunc;
1573
1574impl ScalarFunction for JuliandayFunc {
1575 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1576 match parse_args(args) {
1577 Some(parsed) => Ok(SqliteValue::Float(parsed.jdn)),
1578 None => Ok(SqliteValue::Null),
1579 }
1580 }
1581
1582 fn num_args(&self) -> i32 {
1583 -1
1584 }
1585
1586 fn is_deterministic(&self) -> bool {
1587 false
1588 }
1589
1590 fn name(&self) -> &str {
1591 "julianday"
1592 }
1593}
1594
1595pub struct UnixepochFunc;
1598
1599impl ScalarFunction for UnixepochFunc {
1600 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1601 match parse_args(args) {
1602 Some(parsed) if parsed.subsec => Ok(SqliteValue::Float(jdn_to_unix_subsec(parsed.jdn))),
1605 Some(parsed) => Ok(SqliteValue::Integer(jdn_to_unix(parsed.jdn))),
1606 None => Ok(SqliteValue::Null),
1607 }
1608 }
1609
1610 fn num_args(&self) -> i32 {
1611 -1
1612 }
1613
1614 fn is_deterministic(&self) -> bool {
1615 false
1616 }
1617
1618 fn name(&self) -> &str {
1619 "unixepoch"
1620 }
1621}
1622
1623pub struct StrftimeFunc;
1626
1627impl ScalarFunction for StrftimeFunc {
1628 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1629 let Some(format_value) = args.first() else {
1630 return Ok(SqliteValue::Null);
1631 };
1632 let Some(fmt) = sqlite_value_datetime_text(format_value) else {
1633 return Ok(SqliteValue::Null);
1634 };
1635 let rest = &args[1..];
1636 match parse_args(rest) {
1637 Some(parsed) => Ok(SqliteValue::Text(
1638 format_strftime(
1639 fmt.as_ref(),
1640 parsed.jdn,
1641 parsed.subsec,
1642 parsed.unmodified_hms,
1643 )
1644 .into(),
1645 )),
1646 None => Ok(SqliteValue::Null),
1647 }
1648 }
1649
1650 fn num_args(&self) -> i32 {
1651 -1
1652 }
1653
1654 fn is_deterministic(&self) -> bool {
1655 false
1656 }
1657
1658 fn name(&self) -> &str {
1659 "strftime"
1660 }
1661}
1662
1663pub struct TimediffFunc;
1666
1667impl ScalarFunction for TimediffFunc {
1668 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1669 if args.len() < 2 || args[0].is_null() || args[1].is_null() {
1670 return Ok(SqliteValue::Null);
1671 }
1672
1673 let jdn1 = parse_time_value(&args[0])
1674 .map(|parsed| parsed.jdn)
1675 .filter(|jdn| (0.0..=AUTO_JDN_MAX).contains(jdn));
1676 let jdn2 = parse_time_value(&args[1])
1677 .map(|parsed| parsed.jdn)
1678 .filter(|jdn| (0.0..=AUTO_JDN_MAX).contains(jdn));
1679
1680 match (jdn1, jdn2) {
1681 (Some(j1), Some(j2)) => Ok(SqliteValue::Text(timediff_impl(j1, j2).into())),
1682 _ => Ok(SqliteValue::Null),
1683 }
1684 }
1685
1686 fn num_args(&self) -> i32 {
1687 2
1688 }
1689
1690 fn is_deterministic(&self) -> bool {
1691 false
1692 }
1693
1694 fn name(&self) -> &str {
1695 "timediff"
1696 }
1697}
1698
1699pub fn register_datetime_builtins(registry: &mut FunctionRegistry) {
1703 registry.register_conditionally_deterministic_scalar(DateFunc);
1704 registry.register_conditionally_deterministic_scalar(TimeFunc);
1705 registry.register_conditionally_deterministic_scalar(DateTimeFunc);
1706 registry.register_conditionally_deterministic_scalar(JuliandayFunc);
1707 registry.register_conditionally_deterministic_scalar(UnixepochFunc);
1708 registry.register_conditionally_deterministic_scalar(StrftimeFunc);
1709 registry.register_conditionally_deterministic_scalar(TimediffFunc);
1710}
1711
1712#[cfg(test)]
1715mod tests {
1716 use super::*;
1717
1718 #[test]
1724 #[cfg(not(target_arch = "wasm32"))]
1725 fn utc_modifier_dst_transition_matches_stock_bd_tl6ly() {
1726 if std::env::var("TZ").ok().as_deref() != Some("America/New_York") {
1727 eprintln!(
1728 "SKIP utc_modifier_dst_transition_matches_stock_bd_tl6ly: needs TZ=America/New_York"
1729 );
1730 return;
1731 }
1732 let utc_of = |y, mo, d, h, mi, s| -> (i64, i64, i64, i64, i64, i64) {
1733 let jdn = ymdhms_to_jdn(y, mo, d, h, mi, s, 0.0);
1734 let out = apply_modifier(jdn, "utc").expect("utc modifier");
1735 let (yy, mm, dd) = jdn_to_ymd(out);
1736 let (hh, nn, ss, _f) = jdn_to_hms(out);
1737 (yy, mm, dd, hh, nn, ss)
1738 };
1739 assert_eq!(utc_of(2024, 1, 15, 12, 0, 0), (2024, 1, 15, 17, 0, 0));
1741 assert_eq!(utc_of(2024, 7, 15, 12, 0, 0), (2024, 7, 15, 16, 0, 0));
1743 assert_eq!(utc_of(2024, 3, 10, 6, 30, 0), (2024, 3, 10, 10, 30, 0));
1745 assert_eq!(utc_of(2024, 3, 10, 2, 30, 0), (2024, 3, 10, 7, 30, 0));
1747 assert_eq!(utc_of(2024, 11, 3, 1, 30, 0), (2024, 11, 3, 5, 30, 0));
1749 }
1750
1751 fn text(s: &str) -> SqliteValue {
1752 SqliteValue::Text(s.into())
1753 }
1754
1755 fn int(v: i64) -> SqliteValue {
1756 SqliteValue::Integer(v)
1757 }
1758
1759 fn float(v: f64) -> SqliteValue {
1760 SqliteValue::Float(v)
1761 }
1762
1763 fn null() -> SqliteValue {
1764 SqliteValue::Null
1765 }
1766
1767 fn assert_text(result: &SqliteValue, expected: &str) {
1768 match result {
1769 SqliteValue::Text(s) => assert_eq!(s.as_ref(), expected, "text mismatch"),
1770 other => panic!("expected Text(\"{expected}\"), got {other:?}"),
1771 }
1772 }
1773
1774 fn blob(s: &str) -> SqliteValue {
1777 SqliteValue::Blob(s.as_bytes().into())
1778 }
1779
1780 #[test]
1781 fn test_schema_safety_common_datetime_argument_layout() {
1782 for name in ["date", "time", "datetime", "julianday", "unixepoch"] {
1783 assert!(
1784 !is_datetime_invocation_safe_for_schema(name, &[]),
1785 "{name}() implicitly reads the current time"
1786 );
1787 assert!(is_datetime_invocation_safe_for_schema(
1788 name,
1789 &[text("2024-03-15 12:34:56")]
1790 ));
1791 assert!(is_datetime_invocation_safe_for_schema(name, &[int(0)]));
1792 assert!(is_datetime_invocation_safe_for_schema(
1793 name,
1794 &[float(2_460_384.5)]
1795 ));
1796 assert!(is_datetime_invocation_safe_for_schema(name, &[null()]));
1797
1798 for current_time in ["now", "NOW", "subsec", "SUBSECOND"] {
1799 assert!(
1800 !is_datetime_invocation_safe_for_schema(name, &[text(current_time)]),
1801 "{name}({current_time:?}) must be conditional"
1802 );
1803 }
1804 assert!(!is_datetime_invocation_safe_for_schema(
1805 name,
1806 &[blob("NOW")]
1807 ));
1808 assert!(!is_datetime_invocation_safe_for_schema(
1809 name,
1810 &[text("now\0ignored")]
1811 ));
1812 assert!(!is_datetime_invocation_safe_for_schema(
1813 name,
1814 &[SqliteValue::Blob(b"subsec\0\xff".as_slice().into())]
1815 ));
1816 for invalid_padded in [" now ", "subsec ", " subsecond"] {
1817 assert!(is_datetime_invocation_safe_for_schema(
1818 name,
1819 &[text(invalid_padded)]
1820 ));
1821 }
1822 assert!(is_datetime_invocation_safe_for_schema(
1823 name,
1824 &[blob(" NoW ")]
1825 ));
1826 assert!(is_datetime_invocation_safe_for_schema(
1827 name,
1828 &[text("nowhere")]
1829 ));
1830
1831 assert!(is_datetime_invocation_safe_for_schema(
1833 name,
1834 &[text("localtime")]
1835 ));
1836 assert!(is_datetime_invocation_safe_for_schema(name, &[text("utc")]));
1837 assert!(is_datetime_invocation_safe_for_schema(
1838 name,
1839 &[text("2024-03-15"), text("subsec")]
1840 ));
1841 assert!(is_datetime_invocation_safe_for_schema(
1842 name,
1843 &[text("2024-03-15"), text("subsecond")]
1844 ));
1845
1846 for modifier in ["localtime", "LOCALTIME", "utc"] {
1847 assert!(
1848 !is_datetime_invocation_safe_for_schema(
1849 name,
1850 &[text("2024-03-15"), text(modifier)]
1851 ),
1852 "{name} modifier {modifier:?} depends on host-local state"
1853 );
1854 }
1855 assert!(!is_datetime_invocation_safe_for_schema(
1856 name,
1857 &[text("2024-03-15"), blob("UTC")]
1858 ));
1859 assert!(!is_datetime_invocation_safe_for_schema(
1860 name,
1861 &[text("2024-03-15"), text("localtime\0ignored")]
1862 ));
1863 assert!(is_datetime_invocation_safe_for_schema(
1864 name,
1865 &[text("2024-03-15"), text(" utc ")]
1866 ));
1867
1868 assert!(is_datetime_invocation_safe_for_schema(
1870 name,
1871 &[text("2024-03-15"), null(), text("localtime")]
1872 ));
1873 assert!(!is_datetime_invocation_safe_for_schema(
1874 name,
1875 &[text("2024-03-15"), text("localtime"), null()]
1876 ));
1877
1878 assert!(is_datetime_invocation_safe_for_schema(
1881 name,
1882 &[text("bogus"), text("localtime")]
1883 ));
1884 assert!(is_datetime_invocation_safe_for_schema(
1885 name,
1886 &[text("2000-01-01"), text("bogus"), text("localtime")]
1887 ));
1888 }
1889 }
1890
1891 #[test]
1892 fn test_schema_safety_strftime_uses_shifted_time_arguments() {
1893 assert!(is_datetime_invocation_safe_for_schema("strftime", &[]));
1894 assert!(is_datetime_invocation_safe_for_schema(
1895 "strftime",
1896 &[null()]
1897 ));
1898 assert!(is_datetime_invocation_safe_for_schema(
1899 "strftime",
1900 &[null(), text("now")]
1901 ));
1902
1903 assert!(!is_datetime_invocation_safe_for_schema(
1905 "strftime",
1906 &[text("%Y")]
1907 ));
1908 assert!(!is_datetime_invocation_safe_for_schema(
1909 "STRFTIME",
1910 &[text("%Y"), text("now")]
1911 ));
1912 assert!(!is_datetime_invocation_safe_for_schema(
1913 "strftime",
1914 &[text("%s"), text("subsecond")]
1915 ));
1916
1917 assert!(is_datetime_invocation_safe_for_schema(
1919 "strftime",
1920 &[text("now localtime utc"), text("2024-03-15")]
1921 ));
1922 assert!(is_datetime_invocation_safe_for_schema(
1923 "strftime",
1924 &[text("%Y"), int(0), text("unixepoch")]
1925 ));
1926 assert!(is_datetime_invocation_safe_for_schema(
1927 "strftime",
1928 &[text("%f"), text("2024-03-15"), text("subsec")]
1929 ));
1930 assert!(!is_datetime_invocation_safe_for_schema(
1931 "strftime",
1932 &[text("%Y"), text("2024-03-15"), text("localtime")]
1933 ));
1934 assert!(is_datetime_invocation_safe_for_schema(
1935 "strftime",
1936 &[text("%Y"), text("2024-03-15"), null(), text("localtime")]
1937 ));
1938 }
1939
1940 #[test]
1941 fn test_schema_safety_timediff_treats_both_inputs_as_time_values() {
1942 assert!(is_datetime_invocation_safe_for_schema("timediff", &[]));
1943 assert!(is_datetime_invocation_safe_for_schema(
1944 "timediff",
1945 &[text("2024-03-15")]
1946 ));
1947 assert!(is_datetime_invocation_safe_for_schema(
1948 "timediff",
1949 &[text("2024-03-15"), text("2024-03-14")]
1950 ));
1951 assert!(is_datetime_invocation_safe_for_schema(
1952 "timediff",
1953 &[int(2_460_384), float(2_460_383.5)]
1954 ));
1955
1956 for current_time in ["now", "subsec", "subsecond"] {
1957 assert!(!is_datetime_invocation_safe_for_schema(
1958 "timediff",
1959 &[text(current_time), text("2024-03-14")]
1960 ));
1961 assert!(!is_datetime_invocation_safe_for_schema(
1962 "timediff",
1963 &[text("2024-03-15"), text(current_time)]
1964 ));
1965 }
1966
1967 assert!(is_datetime_invocation_safe_for_schema(
1970 "timediff",
1971 &[text("localtime"), text("utc")]
1972 ));
1973 assert!(is_datetime_invocation_safe_for_schema(
1974 "timediff",
1975 &[null(), text("now")]
1976 ));
1977 assert!(is_datetime_invocation_safe_for_schema(
1978 "timediff",
1979 &[text("bogus"), text("now")]
1980 ));
1981 assert!(!is_datetime_invocation_safe_for_schema(
1982 "timediff",
1983 &[blob("NOW"), null()]
1984 ));
1985 }
1986
1987 #[test]
1988 fn test_schema_safety_ignores_non_datetime_function_names() {
1989 for name in ["", "my_date", "current_date", "date ", "random"] {
1990 assert!(is_datetime_invocation_safe_for_schema(
1991 name,
1992 &[text("now"), text("localtime")]
1993 ));
1994 }
1995 }
1996
1997 #[test]
2000 fn test_omitted_time_value_uses_current_time() {
2001 let date = DateFunc.invoke(&[]).unwrap();
2002 let time = TimeFunc.invoke(&[]).unwrap();
2003 let datetime = DateTimeFunc.invoke(&[]).unwrap();
2004 let julianday = JuliandayFunc.invoke(&[]).unwrap();
2005 let unixepoch = UnixepochFunc.invoke(&[]).unwrap();
2006 let year = StrftimeFunc.invoke(&[text("%Y")]).unwrap();
2007
2008 assert!(matches!(&date, SqliteValue::Text(value) if value.len() == 10));
2009 assert!(matches!(&time, SqliteValue::Text(value) if value.len() == 8));
2010 assert!(matches!(&datetime, SqliteValue::Text(value) if value.len() == 19));
2011 assert!(matches!(julianday, SqliteValue::Float(_)));
2012 assert!(matches!(unixepoch, SqliteValue::Integer(_)));
2013 assert!(matches!(&year, SqliteValue::Text(value)
2014 if value.len() == 4 && value.as_bytes().iter().all(u8::is_ascii_digit)));
2015
2016 assert_eq!(StrftimeFunc.invoke(&[]).unwrap(), SqliteValue::Null);
2017 assert_eq!(StrftimeFunc.invoke(&[null()]).unwrap(), SqliteValue::Null);
2018 }
2019
2020 #[test]
2021 fn test_first_position_subsec_aliases_use_current_time() {
2022 for alias in ["subsec", "subsecond"] {
2023 assert!(matches!(
2024 DateFunc.invoke(&[text(alias)]).unwrap(),
2025 SqliteValue::Text(value) if value.len() == 10
2026 ));
2027 assert!(matches!(
2028 TimeFunc.invoke(&[text(alias)]).unwrap(),
2029 SqliteValue::Text(value)
2030 if value.len() == 12 && value.as_bytes_direct()[8] == b'.'
2031 ));
2032 assert!(matches!(
2033 DateTimeFunc.invoke(&[text(alias)]).unwrap(),
2034 SqliteValue::Text(value)
2035 if value.len() == 23 && value.as_bytes_direct()[19] == b'.'
2036 ));
2037 assert!(matches!(
2038 JuliandayFunc.invoke(&[text(alias)]).unwrap(),
2039 SqliteValue::Float(_)
2040 ));
2041 assert!(matches!(
2042 UnixepochFunc.invoke(&[text(alias)]).unwrap(),
2043 SqliteValue::Float(_)
2044 ));
2045 assert!(matches!(
2046 StrftimeFunc.invoke(&[text("%s"), text(alias)]).unwrap(),
2047 SqliteValue::Text(value)
2048 if value.rsplit_once('.').is_some_and(|(_, fraction)| fraction.len() == 3)
2049 ));
2050 }
2051 }
2052
2053 #[test]
2054 fn test_padded_and_nul_terminated_special_values() {
2055 for invalid in [" now ", "subsec ", " subsecond"] {
2056 assert_eq!(
2057 DateFunc.invoke(&[text(invalid)]).unwrap(),
2058 SqliteValue::Null
2059 );
2060 }
2061 assert_eq!(
2062 DateFunc
2063 .invoke(&[text("2000-01-01"), text(" localtime ")])
2064 .unwrap(),
2065 SqliteValue::Null
2066 );
2067 assert!(matches!(
2068 DateFunc.invoke(&[text("now\0ignored")]).unwrap(),
2069 SqliteValue::Text(value) if value.len() == 10
2070 ));
2071 assert!(matches!(
2072 TimeFunc
2073 .invoke(&[SqliteValue::Blob(b"subsec\0\xff".as_slice().into())])
2074 .unwrap(),
2075 SqliteValue::Text(value) if value.len() == 12
2076 ));
2077 }
2078
2079 #[test]
2080 fn test_date_basic() {
2081 let r = DateFunc.invoke(&[text("2024-03-15 14:30:00")]).unwrap();
2082 assert_text(&r, "2024-03-15");
2083 }
2084
2085 #[test]
2086 fn test_time_basic() {
2087 let r = TimeFunc.invoke(&[text("2024-03-15 14:30:45")]).unwrap();
2088 assert_text(&r, "14:30:45");
2089 }
2090
2091 #[test]
2092 fn test_datetime_basic() {
2093 let r = DateTimeFunc.invoke(&[text("2024-03-15 14:30:00")]).unwrap();
2094 assert_text(&r, "2024-03-15 14:30:00");
2095 }
2096
2097 #[test]
2098 fn test_julianday_basic() {
2099 let r = JuliandayFunc.invoke(&[text("2024-03-15")]).unwrap();
2100 match r {
2101 SqliteValue::Float(jdn) => {
2102 assert!((jdn - 2_460_384.5).abs() < 0.01, "unexpected JDN: {jdn}");
2104 }
2105 other => panic!("expected Float, got {other:?}"),
2106 }
2107 }
2108
2109 fn julianday_float(input: &str) -> f64 {
2117 match JuliandayFunc.invoke(&[text(input)]).unwrap() {
2118 SqliteValue::Float(v) => v,
2119 other => panic!("expected Float, got {other:?} for input {input:?}"),
2120 }
2121 }
2122
2123 fn assert_jdn_close(actual: f64, expected: f64, ctx: &str) {
2124 assert!(
2126 (actual - expected).abs() < 1e-6,
2127 "JDN mismatch for {ctx}: got {actual}, expected {expected}"
2128 );
2129 }
2130
2131 #[test]
2132 fn test_julianday_rfc3339_z_suffix() {
2133 let naive = julianday_float("2026-04-07 16:00:00");
2135 assert_jdn_close(julianday_float("2026-04-07T16:00:00Z"), naive, "T...Z");
2136 assert_jdn_close(
2137 julianday_float("2026-04-07T16:00:00z"),
2138 naive,
2139 "lowercase z",
2140 );
2141 }
2142
2143 #[test]
2144 fn test_julianday_rfc3339_zero_offset() {
2145 let naive = julianday_float("2026-04-07 16:00:00");
2146 assert_jdn_close(
2147 julianday_float("2026-04-07T16:00:00+00:00"),
2148 naive,
2149 "+00:00",
2150 );
2151 assert_jdn_close(
2152 julianday_float("2026-04-07T16:00:00-00:00"),
2153 naive,
2154 "-00:00",
2155 );
2156 }
2157
2158 #[test]
2159 fn test_julianday_rfc3339_positive_offset() {
2160 let base = julianday_float("2026-04-07 16:00:00");
2162 let expected = base - 1.0 / 24.0;
2163 assert_jdn_close(
2164 julianday_float("2026-04-07T16:00:00+01:00"),
2165 expected,
2166 "+01:00",
2167 );
2168 }
2169
2170 #[test]
2171 fn test_julianday_rfc3339_negative_offset() {
2172 let base = julianday_float("2026-04-07 16:00:00");
2174 let expected = base + 5.0 / 24.0;
2175 assert_jdn_close(
2176 julianday_float("2026-04-07T16:00:00-05:00"),
2177 expected,
2178 "-05:00",
2179 );
2180 }
2181
2182 #[test]
2183 fn test_julianday_rfc3339_half_hour_offset() {
2184 let base = julianday_float("2026-04-07 16:00:00");
2186 let expected = base - 5.5 / 24.0;
2187 assert_jdn_close(
2188 julianday_float("2026-04-07T16:00:00+05:30"),
2189 expected,
2190 "+05:30",
2191 );
2192 }
2193
2194 #[test]
2195 fn test_julianday_rfc3339_compact_offsets() {
2196 let base = julianday_float("2026-04-07 16:00:00");
2198 assert_jdn_close(
2199 julianday_float("2026-04-07T16:00:00+0100"),
2200 base - 1.0 / 24.0,
2201 "+0100",
2202 );
2203 assert_jdn_close(
2204 julianday_float("2026-04-07T16:00:00-0530"),
2205 base + 5.5 / 24.0,
2206 "-0530",
2207 );
2208 assert_jdn_close(
2209 julianday_float("2026-04-07T16:00:00+09"),
2210 base - 9.0 / 24.0,
2211 "+09",
2212 );
2213 }
2214
2215 #[test]
2216 fn test_julianday_rfc3339_fractional_seconds_with_tz() {
2217 let base = julianday_float("2026-04-07 16:00:00.500");
2219 assert_jdn_close(
2220 julianday_float("2026-04-07T16:00:00.500Z"),
2221 base,
2222 "fractional + Z",
2223 );
2224 assert_jdn_close(
2225 julianday_float("2026-04-07T16:00:00.500+01:00"),
2226 base - 1.0 / 24.0,
2227 "fractional + +01:00",
2228 );
2229 }
2230
2231 #[test]
2232 fn test_date_and_time_rfc3339_round_trip() {
2233 assert_text(
2236 &DateFunc
2237 .invoke(&[text("2026-04-07T16:00:00+05:00")])
2238 .unwrap(),
2239 "2026-04-07",
2241 );
2242 assert_text(
2243 &TimeFunc
2244 .invoke(&[text("2026-04-07T16:00:00+05:00")])
2245 .unwrap(),
2246 "11:00:00",
2247 );
2248 assert_text(
2249 &DateTimeFunc
2250 .invoke(&[text("2026-04-07T16:00:00+05:00")])
2251 .unwrap(),
2252 "2026-04-07 11:00:00",
2253 );
2254 }
2255
2256 #[test]
2257 fn test_julianday_rfc3339_invalid_offsets_return_null() {
2258 for bad in &[
2260 "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", ] {
2265 let result = JuliandayFunc.invoke(&[text(bad)]).unwrap();
2266 assert_eq!(
2267 result,
2268 SqliteValue::Null,
2269 "expected NULL for malformed offset {bad:?}, got {result:?}"
2270 );
2271 }
2272 }
2273
2274 #[test]
2275 fn test_julianday_rejects_malformed_time_fields() {
2276 for bad in &[
2280 "+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", ] {
2292 let result = JuliandayFunc.invoke(&[text(bad)]).unwrap();
2293 assert_eq!(
2294 result,
2295 SqliteValue::Null,
2296 "expected NULL for signed time field {bad:?}, got {result:?}"
2297 );
2298 }
2299 }
2300
2301 #[test]
2302 fn test_unixepoch_basic() {
2303 let r = UnixepochFunc
2304 .invoke(&[text("1970-01-01 00:00:00")])
2305 .unwrap();
2306 assert_eq!(r, int(0));
2307 }
2308
2309 #[test]
2310 fn test_unixepoch_known_date() {
2311 let r = UnixepochFunc
2312 .invoke(&[text("2024-01-01 00:00:00")])
2313 .unwrap();
2314 assert_eq!(r, int(1_704_067_200));
2316 }
2317
2318 #[test]
2321 fn test_modifier_days() {
2322 let r = DateFunc
2323 .invoke(&[text("2024-01-15"), text("+10 days")])
2324 .unwrap();
2325 assert_text(&r, "2024-01-25");
2326 }
2327
2328 #[test]
2329 fn test_modifier_months() {
2330 let r = DateFunc
2333 .invoke(&[text("2024-01-31"), text("+1 months")])
2334 .unwrap();
2335 assert_text(&r, "2024-03-02");
2336 }
2337
2338 #[test]
2339 fn test_modifier_years() {
2340 let r = DateFunc
2343 .invoke(&[text("2024-02-29"), text("+1 years")])
2344 .unwrap();
2345 assert_text(&r, "2025-03-01");
2346 }
2347
2348 #[test]
2349 fn test_modifier_hours() {
2350 let r = DateTimeFunc
2351 .invoke(&[text("2024-01-01 23:00:00"), text("+2 hours")])
2352 .unwrap();
2353 assert_text(&r, "2024-01-02 01:00:00");
2354 }
2355
2356 #[test]
2357 fn test_modifier_unsigned_is_positive_bd_t8g1e() {
2358 assert_text(
2364 &DateFunc
2365 .invoke(&[text("2024-01-15"), text("10 days")])
2366 .unwrap(),
2367 "2024-01-25",
2368 );
2369 assert_text(
2370 &DateTimeFunc
2371 .invoke(&[text("2024-01-01"), text("5 hours")])
2372 .unwrap(),
2373 "2024-01-01 05:00:00",
2374 );
2375 assert_text(
2376 &DateTimeFunc
2377 .invoke(&[text("2024-01-01"), text("90 minutes")])
2378 .unwrap(),
2379 "2024-01-01 01:30:00",
2380 );
2381 assert_text(
2382 &DateTimeFunc
2383 .invoke(&[text("2024-01-01"), text("86400 seconds")])
2384 .unwrap(),
2385 "2024-01-02 00:00:00",
2386 );
2387 assert_text(
2390 &DateFunc
2391 .invoke(&[text("2024-01-01"), text("2 months")])
2392 .unwrap(),
2393 "2024-03-01",
2394 );
2395 assert_text(
2396 &DateFunc
2397 .invoke(&[text("2024-01-01"), text("1 year")])
2398 .unwrap(),
2399 "2025-01-01",
2400 );
2401 assert_text(
2403 &DateTimeFunc
2404 .invoke(&[text("2024-01-01 12:00"), text("1.5 hours")])
2405 .unwrap(),
2406 "2024-01-01 13:30:00",
2407 );
2408 assert_text(
2412 &DateFunc
2413 .invoke(&[text("2024-03-15"), text("start of month")])
2414 .unwrap(),
2415 "2024-03-01",
2416 );
2417 assert_text(
2418 &DateFunc
2419 .invoke(&[text("2024-06-15"), text("start of year")])
2420 .unwrap(),
2421 "2024-01-01",
2422 );
2423 assert_text(
2425 &DateFunc
2426 .invoke(&[text("2024-01-15"), text("-10 days")])
2427 .unwrap(),
2428 "2024-01-05",
2429 );
2430 }
2431
2432 #[test]
2433 fn test_modifier_start_of_month() {
2434 let r = DateFunc
2435 .invoke(&[text("2024-03-15"), text("start of month")])
2436 .unwrap();
2437 assert_text(&r, "2024-03-01");
2438 }
2439
2440 #[test]
2441 fn test_modifier_start_of_year() {
2442 let r = DateFunc
2443 .invoke(&[text("2024-06-15"), text("start of year")])
2444 .unwrap();
2445 assert_text(&r, "2024-01-01");
2446 }
2447
2448 #[test]
2449 fn test_modifier_start_of_day() {
2450 let r = DateTimeFunc
2451 .invoke(&[text("2024-03-15 14:30:00"), text("start of day")])
2452 .unwrap();
2453 assert_text(&r, "2024-03-15 00:00:00");
2454 }
2455
2456 #[test]
2457 fn test_modifier_unixepoch() {
2458 let r = DateTimeFunc.invoke(&[int(0), text("unixepoch")]).unwrap();
2459 assert_text(&r, "1970-01-01 00:00:00");
2460 }
2461
2462 #[test]
2463 fn test_modifier_weekday() {
2464 let r = DateFunc
2466 .invoke(&[text("2024-03-15"), text("weekday 0")])
2467 .unwrap();
2468 assert_text(&r, "2024-03-17");
2469 }
2470
2471 #[test]
2472 fn test_modifier_auto_unixepoch() {
2473 let ts = int(1_710_531_045);
2474 let r = DateTimeFunc.invoke(&[ts.clone(), text("auto")]).unwrap();
2475 let expected = DateTimeFunc.invoke(&[ts, text("unixepoch")]).unwrap();
2476 assert_eq!(
2477 r, expected,
2478 "auto and unixepoch should agree for unix-like values"
2479 );
2480 }
2481
2482 #[test]
2483 fn test_modifier_auto_julian_day() {
2484 let r = DateFunc
2485 .invoke(&[float(2_460_384.5), text("auto")])
2486 .unwrap();
2487 assert_text(&r, "2024-03-15");
2488 }
2489
2490 #[test]
2491 fn test_modifier_localtime_utc_roundtrip() {
2492 let r = DateTimeFunc
2494 .invoke(&[text("2024-03-15 14:30:45"), text("localtime"), text("utc")])
2495 .unwrap();
2496 assert_text(&r, "2024-03-15 14:30:45");
2497 }
2498
2499 #[test]
2500 fn test_modifier_localtime_shifts_value() {
2501 let offset = utc_offset_for_utc_jdn(ymdhms_to_jdn(2024, 3, 15, 12, 0, 0, 0.0));
2503 if offset != 0 {
2504 let r = DateTimeFunc
2505 .invoke(&[text("2024-03-15 12:00:00"), text("localtime")])
2506 .unwrap();
2507 let shifted = match &r {
2509 SqliteValue::Text(s) => s.clone(),
2510 _ => panic!("expected text"),
2511 };
2512 assert_ne!(&*shifted, "2024-03-15 12:00:00");
2513 }
2514 }
2515
2516 #[test]
2517 fn test_modifier_auto_out_of_range_returns_null() {
2518 let r = DateTimeFunc.invoke(&[float(1.0e20), text("auto")]).unwrap();
2519 assert_eq!(r, SqliteValue::Null);
2520 }
2521
2522 #[test]
2523 fn test_modifier_order_matters() {
2524 let r1 = DateFunc
2526 .invoke(&[text("2024-03-15"), text("start of month"), text("+1 days")])
2527 .unwrap();
2528 assert_text(&r1, "2024-03-02");
2529
2530 let r2 = DateFunc
2532 .invoke(&[text("2024-03-15"), text("+1 days"), text("start of month")])
2533 .unwrap();
2534 assert_text(&r2, "2024-03-01");
2535 }
2536
2537 #[test]
2538 fn test_modifier_weekday_same_day_is_noop() {
2539 let r = DateFunc
2541 .invoke(&[text("2024-03-17"), text("weekday 0")])
2542 .unwrap();
2543 assert_text(&r, "2024-03-17");
2544 }
2545
2546 #[test]
2549 fn test_bare_time_defaults() {
2550 let r = DateFunc.invoke(&[text("12:30:00")]).unwrap();
2551 assert_text(&r, "2000-01-01");
2552 }
2553
2554 #[test]
2555 fn test_t_separator() {
2556 let r = DateTimeFunc.invoke(&[text("2024-03-15T14:30:00")]).unwrap();
2557 assert_text(&r, "2024-03-15 14:30:00");
2558 }
2559
2560 #[test]
2561 fn test_julian_day_input() {
2562 let r = DateFunc.invoke(&[float(2_460_384.5)]).unwrap();
2564 assert_text(&r, "2024-03-15");
2565 }
2566
2567 #[test]
2568 fn test_null_input() {
2569 assert_eq!(DateFunc.invoke(&[null()]).unwrap(), SqliteValue::Null);
2570 }
2571
2572 #[test]
2573 fn test_invalid_input() {
2574 assert_eq!(
2575 DateFunc.invoke(&[text("not-a-date")]).unwrap(),
2576 SqliteValue::Null
2577 );
2578 }
2579
2580 #[test]
2581 fn test_negative_time_component_invalid() {
2582 let r = TimeFunc.invoke(&[text("-01:00")]).unwrap();
2583 assert_eq!(r, SqliteValue::Null);
2584 }
2585
2586 #[test]
2589 fn test_leap_year() {
2590 let r = DateFunc
2591 .invoke(&[text("2024-02-28"), text("+1 days")])
2592 .unwrap();
2593 assert_text(&r, "2024-02-29");
2594 }
2595
2596 #[test]
2597 fn test_non_leap_year() {
2598 let r = DateFunc
2599 .invoke(&[text("2023-02-28"), text("+1 days")])
2600 .unwrap();
2601 assert_text(&r, "2023-03-01");
2602 }
2603
2604 #[test]
2607 fn test_strftime_basic() {
2608 let r = StrftimeFunc
2609 .invoke(&[text("%Y-%m-%d"), text("2024-03-15")])
2610 .unwrap();
2611 assert_text(&r, "2024-03-15");
2612 }
2613
2614 #[test]
2615 fn test_strftime_time_specifiers() {
2616 let r = StrftimeFunc
2617 .invoke(&[text("%H:%M:%S"), text("2024-03-15 14:30:45")])
2618 .unwrap();
2619 assert_text(&r, "14:30:45");
2620 }
2621
2622 #[test]
2623 fn test_strftime_unix_seconds() {
2624 let r = StrftimeFunc
2625 .invoke(&[text("%s"), text("1970-01-01 00:00:00")])
2626 .unwrap();
2627 assert_text(&r, "0");
2628 }
2629
2630 #[test]
2631 fn test_strftime_day_of_year() {
2632 let r = StrftimeFunc
2633 .invoke(&[text("%j"), text("2024-03-15")])
2634 .unwrap();
2635 assert_text(&r, "075");
2637 }
2638
2639 #[test]
2640 fn test_strftime_day_of_week() {
2641 let r = StrftimeFunc
2643 .invoke(&[text("%w"), text("2024-03-15")])
2644 .unwrap();
2645 assert_text(&r, "5");
2646
2647 let r = StrftimeFunc
2648 .invoke(&[text("%u"), text("2024-03-15")])
2649 .unwrap();
2650 assert_text(&r, "5");
2651 }
2652
2653 #[test]
2654 fn test_strftime_12hour() {
2655 let r = StrftimeFunc
2656 .invoke(&[text("%I %p"), text("2024-03-15 14:30:00")])
2657 .unwrap();
2658 assert_text(&r, "02 PM");
2659
2660 let r = StrftimeFunc
2661 .invoke(&[text("%I %P"), text("2024-03-15 09:30:00")])
2662 .unwrap();
2663 assert_text(&r, "09 am");
2664 }
2665
2666 #[test]
2667 fn test_strftime_all_specifiers_presence() {
2668 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|%%";
2669 let r = StrftimeFunc
2670 .invoke(&[text(fmt), text("2024-03-15 14:30:45.123")])
2671 .unwrap();
2672
2673 let s = match r {
2674 SqliteValue::Text(v) => v,
2675 other => panic!("expected Text, got {other:?}"),
2676 };
2677 let parts: Vec<&str> = s.split('|').collect();
2678 assert_eq!(parts.len(), 25, "unexpected specifier output: {s}");
2679 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!(
2686 parts[6].parse::<f64>().is_ok(),
2687 "expected numeric %J output, got {}",
2688 parts[6]
2689 );
2690 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!(
2698 parts[14].parse::<i64>().is_ok(),
2699 "expected numeric %s output, got {}",
2700 parts[14]
2701 );
2702 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], "%"); }
2713
2714 #[test]
2715 fn test_strftime_null() {
2716 assert_eq!(
2717 StrftimeFunc.invoke(&[null(), text("2024-01-01")]).unwrap(),
2718 SqliteValue::Null
2719 );
2720 assert_eq!(
2721 StrftimeFunc.invoke(&[text("%Y"), null()]).unwrap(),
2722 SqliteValue::Null
2723 );
2724 }
2725
2726 #[test]
2727 #[ignore = "perf-only benchmark"]
2728 fn perf_strftime_timestamp_rows() {
2729 use std::hint::black_box;
2730 use std::time::Instant;
2731
2732 const ROWS: usize = 200_000;
2733 const REPEATS: usize = 5;
2734 const FORMAT: &str = "%Y-%m-%d %H:%M:%S";
2735 const INPUT: &str = "2024-03-15 14:30:45";
2736
2737 let func = StrftimeFunc;
2738 let fmt = text(FORMAT);
2739 let input = text(INPUT);
2740 let mut best_ns = u128::MAX;
2741 let mut output_len = 0usize;
2742
2743 for _ in 0..REPEATS {
2744 let started = Instant::now();
2745 for _ in 0..ROWS {
2746 let result = black_box(
2747 func.invoke(black_box(&[fmt.clone(), input.clone()]))
2748 .expect("strftime benchmark invocation must succeed"),
2749 );
2750 output_len = match result {
2751 SqliteValue::Text(text) => text.len(),
2752 SqliteValue::Null
2753 | SqliteValue::Integer(_)
2754 | SqliteValue::Float(_)
2755 | SqliteValue::Blob(_) => 0,
2756 };
2757 }
2758 let elapsed_ns = started.elapsed().as_nanos();
2759 if elapsed_ns < best_ns {
2760 best_ns = elapsed_ns;
2761 }
2762 }
2763
2764 println!(
2765 "strftime_timestamp_rows rows={ROWS} repeats={REPEATS} best_ns={best_ns} output_len={output_len}"
2766 );
2767 }
2768
2769 #[test]
2772 fn test_timediff_basic() {
2773 let r = TimediffFunc
2774 .invoke(&[text("2024-03-15"), text("2024-03-10")])
2775 .unwrap();
2776 assert_text(&r, "+0000-00-05 00:00:00.000");
2777 }
2778
2779 #[test]
2780 fn test_timediff_negative() {
2781 let r = TimediffFunc
2782 .invoke(&[text("2024-03-10"), text("2024-03-15")])
2783 .unwrap();
2784 assert_text(&r, "-0000-00-05 00:00:00.000");
2785 }
2786
2787 #[test]
2788 fn test_timediff_year_boundary() {
2789 let r = TimediffFunc
2790 .invoke(&[text("2024-01-01 01:00:00"), text("2023-12-31 23:00:00")])
2791 .unwrap();
2792 assert_text(&r, "+0000-00-00 02:00:00.000");
2793 }
2794
2795 #[test]
2798 fn test_modifier_subsec() {
2799 assert_text(
2800 &TimeFunc
2801 .invoke(&[text("2024-01-01 12:00:00"), text("subsec")])
2802 .unwrap(),
2803 "12:00:00.000",
2804 );
2805 assert_text(
2806 &DateTimeFunc
2807 .invoke(&[text("2024-01-01 12:00:00"), text("subsecond")])
2808 .unwrap(),
2809 "2024-01-01 12:00:00.000",
2810 );
2811 assert_text(
2812 &TimeFunc
2813 .invoke(&[text("2024-01-01 12:00:00.123"), text("subsec")])
2814 .unwrap(),
2815 "12:00:00.123",
2816 );
2817 }
2818
2819 #[test]
2820 fn test_subsec_unix_seconds_and_integer_flooring() {
2821 assert_text(
2822 &StrftimeFunc
2823 .invoke(&[text("%s"), text("1970-01-01 00:00:00.125"), text("subsec")])
2824 .unwrap(),
2825 "0.125",
2826 );
2827 assert_eq!(
2828 UnixepochFunc
2829 .invoke(&[text("1970-01-01 00:00:00.125"), text("subsec")])
2830 .unwrap(),
2831 float(0.125)
2832 );
2833
2834 for input in ["1970-01-01 00:00:00.500", "1970-01-01 00:00:00.999"] {
2835 assert_eq!(UnixepochFunc.invoke(&[text(input)]).unwrap(), int(0));
2836 assert_text(
2837 &StrftimeFunc.invoke(&[text("%s"), text(input)]).unwrap(),
2838 "0",
2839 );
2840 }
2841 assert_eq!(
2842 UnixepochFunc
2843 .invoke(&[text("1969-12-31 23:59:59.999")])
2844 .unwrap(),
2845 int(-1)
2846 );
2847 assert_text(
2848 &StrftimeFunc
2849 .invoke(&[text("%s"), text("1969-12-31 23:59:59.999")])
2850 .unwrap(),
2851 "-1",
2852 );
2853 }
2854
2855 #[test]
2856 fn test_subsec_rounding_preserves_sqlite_second_60() {
2857 assert_text(
2858 &TimeFunc
2859 .invoke(&[text("12:34:59.9995"), text("subsec")])
2860 .unwrap(),
2861 "12:34:60.000",
2862 );
2863 assert_text(
2864 &DateTimeFunc
2865 .invoke(&[text("1970-01-01 23:59:59.9995"), text("subsec")])
2866 .unwrap(),
2867 "1970-01-01 23:59:60.000",
2868 );
2869 assert_text(
2870 &StrftimeFunc
2871 .invoke(&[
2872 text("%H:%M:%f|%s"),
2873 text("1970-01-01 23:59:59.9995"),
2874 text("subsec"),
2875 ])
2876 .unwrap(),
2877 "23:59:59.999|86400.000",
2878 );
2879 }
2880
2881 #[test]
2884 fn test_register_datetime_builtins_all_present() {
2885 let mut reg = FunctionRegistry::new();
2886 register_datetime_builtins(&mut reg);
2887
2888 let expected = [
2889 "date",
2890 "time",
2891 "datetime",
2892 "julianday",
2893 "unixepoch",
2894 "strftime",
2895 "timediff",
2896 ];
2897
2898 for name in expected {
2899 assert!(
2900 reg.find_scalar(name, 1).is_some() || reg.find_scalar(name, 2).is_some(),
2901 "datetime function '{name}' not registered"
2902 );
2903 }
2904 }
2905
2906 #[test]
2907 fn test_public_datetime_function_metadata_fails_closed_for_direct_registration() {
2908 assert!(!DateFunc.is_deterministic());
2909 assert!(!TimeFunc.is_deterministic());
2910 assert!(!DateTimeFunc.is_deterministic());
2911 assert!(!JuliandayFunc.is_deterministic());
2912 assert!(!UnixepochFunc.is_deterministic());
2913 assert!(!StrftimeFunc.is_deterministic());
2914 assert!(!TimediffFunc.is_deterministic());
2915
2916 let mut registry = FunctionRegistry::new();
2917 registry.register_scalar(DateFunc);
2918 registry.register_scalar(TimeFunc);
2919 registry.register_scalar(DateTimeFunc);
2920 registry.register_scalar(JuliandayFunc);
2921 registry.register_scalar(UnixepochFunc);
2922 registry.register_scalar(StrftimeFunc);
2923 registry.register_scalar(TimediffFunc);
2924 for (name, num_args) in [
2925 ("date", 0),
2926 ("time", 0),
2927 ("datetime", 0),
2928 ("julianday", 0),
2929 ("unixepoch", 0),
2930 ("strftime", 1),
2931 ("timediff", 2),
2932 ] {
2933 assert_eq!(
2934 registry.scalar_schema_safety(name, num_args),
2935 Some(crate::ScalarSchemaSafety::Never),
2936 "generic registration of {name}/{num_args} must fail closed"
2937 );
2938 }
2939 }
2940
2941 #[test]
2944 fn test_modifier_year_overflow() {
2945 let huge = i64::MAX;
2948 let modifier = format!("+{huge} years");
2949 let r = DateFunc.invoke(&[text("2000-01-01"), text(&modifier)]);
2950 assert_eq!(r.unwrap(), SqliteValue::Null);
2953 }
2954
2955 #[test]
2956 fn test_jdn_roundtrip() {
2957 let dates = [
2959 (2024, 3, 15),
2960 (2000, 1, 1),
2961 (1970, 1, 1),
2962 (2024, 2, 29),
2963 (1900, 1, 1),
2964 (2099, 12, 31),
2965 ];
2966 for (y, m, d) in dates {
2967 let jdn = ymd_to_jdn(y, m, d);
2968 let (y2, m2, d2) = jdn_to_ymd(jdn);
2969 assert_eq!(
2970 (y, m, d),
2971 (y2, m2, d2),
2972 "roundtrip failed for {y}-{m}-{d} (JDN={jdn})"
2973 );
2974 }
2975 }
2976
2977 #[test]
2978 fn test_unix_epoch_roundtrip() {
2979 let jdn = ymd_to_jdn(1970, 1, 1);
2980 let unix = jdn_to_unix(jdn);
2981 assert_eq!(unix, 0, "Unix epoch should be 0");
2982
2983 let jdn2 = unix_to_jdn(0.0);
2984 assert!((jdn2 - UNIX_EPOCH_JDN).abs() < 1e-10, "roundtrip failed");
2985 }
2986}