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"))]
57fn utc_offset_for_local_datetime(y: i32, mo: u32, d: u32, h: u32, mi: u32, s: u32) -> i64 {
58 use chrono::{Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone};
59 let date = NaiveDate::from_ymd_opt(y, mo, d).unwrap_or_default();
60 let time = NaiveTime::from_hms_opt(h, mi, s).unwrap_or_default();
61 let naive = NaiveDateTime::new(date, time);
62 match Local.from_local_datetime(&naive).earliest() {
63 Some(dt) => dt.offset().local_minus_utc() as i64,
64 None => 0, }
66}
67
68#[cfg(target_arch = "wasm32")]
69fn utc_offset_for_local_datetime(_y: i32, _mo: u32, _d: u32, _h: u32, _mi: u32, _s: u32) -> i64 {
70 0
71}
72
73#[cfg(not(target_arch = "wasm32"))]
80fn utc_offset_for_utc_datetime(y: i32, mo: u32, d: u32, h: u32, mi: u32, s: u32) -> i64 {
81 use chrono::{Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
82 let date = NaiveDate::from_ymd_opt(y, mo, d).unwrap_or_default();
83 let time = NaiveTime::from_hms_opt(h, mi, s).unwrap_or_default();
84 let naive = NaiveDateTime::new(date, time);
85 let utc_dt = Utc.from_utc_datetime(&naive);
86 let local_dt = utc_dt.with_timezone(&Local);
87 local_dt.offset().local_minus_utc() as i64
88}
89
90#[cfg(target_arch = "wasm32")]
91fn utc_offset_for_utc_datetime(_y: i32, _mo: u32, _d: u32, _h: u32, _mi: u32, _s: u32) -> i64 {
92 0
93}
94
95fn utc_offset_for_utc_jdn(jdn: f64) -> i64 {
97 let (y, mo, d) = jdn_to_ymd(jdn);
98 let (h, mi, s, _frac) = jdn_to_hms(jdn);
99 utc_offset_for_utc_datetime(y as i32, mo as u32, d as u32, h as u32, mi as u32, s as u32)
100}
101
102fn utc_offset_for_local_jdn(jdn: f64) -> i64 {
104 let (y, mo, d) = jdn_to_ymd(jdn);
105 let (h, mi, s, _frac) = jdn_to_hms(jdn);
106 utc_offset_for_local_datetime(y as i32, mo as u32, d as u32, h as u32, mi as u32, s as u32)
107}
108
109fn ymd_to_jdn(y: i64, m: i64, d: i64) -> f64 {
115 let (y, m) = if m <= 2 {
116 (y.saturating_sub(1), m.saturating_add(12))
117 } else {
118 (y, m)
119 };
120 let a = y / 100;
121 let b = 2_i64.saturating_sub(a).saturating_add(a / 4);
122 (365.25 * y.saturating_add(4716) as f64).floor()
123 + (30.6001 * m.saturating_add(1) as f64).floor()
124 + d as f64
125 + b as f64
126 - 1524.5
127}
128
129fn jdn_to_ymd(jdn: f64) -> (i64, i64, i64) {
136 let z = (jdn + 0.5).floor() as i64;
142 let alpha = ((z as f64 - 1_867_216.25) / 36524.25) as i64;
143 let a = z
144 .saturating_add(1)
145 .saturating_add(alpha)
146 .saturating_sub(alpha / 4);
147 let b = a.saturating_add(1524);
148 let c = ((b as f64 - 122.1) / 365.25) as i64;
149 let d = (365.25 * c as f64) as i64;
150 let e = ((b.saturating_sub(d)) as f64 / 30.6001) as i64;
151
152 let day = b
153 .saturating_sub(d)
154 .saturating_sub((30.6001 * e as f64) as i64);
155 let month = if e < 14 {
156 e.saturating_sub(1)
157 } else {
158 e.saturating_sub(13)
159 };
160 let year = if month > 2 {
161 c.saturating_sub(4716)
162 } else {
163 c.saturating_sub(4715)
164 };
165 (year, month, day)
166}
167
168fn jdn_to_hms(jdn: f64) -> (i64, i64, i64, f64) {
170 let frac = jdn + 0.5 - (jdn + 0.5).floor();
171 let total_ms = (frac * 86_400_000.0).round() as i64;
173 let h = total_ms / 3_600_000;
174 let rem = total_ms % 3_600_000;
175 let m = rem / 60_000;
176 let rem = rem % 60_000;
177 let s = rem / 1000;
178 let ms_frac = (rem % 1000) as f64 / 1000.0;
179 (h, m, s, ms_frac)
180}
181
182fn ymdhms_to_jdn(y: i64, mo: i64, d: i64, h: i64, mi: i64, s: i64, frac: f64) -> f64 {
184 ymd_to_jdn(y, mo, d) + (h as f64 * 3600.0 + mi as f64 * 60.0 + s as f64 + frac) / 86400.0
185}
186
187const UNIX_EPOCH_JDN: f64 = 2_440_587.5;
189const AUTO_JDN_MAX: f64 = 5_373_484.499_999;
191const AUTO_UNIX_MIN: f64 = -210_866_760_000.0;
193const AUTO_UNIX_MAX: f64 = 253_402_300_799.0;
194
195fn jdn_to_unix_millis(jdn: f64) -> i64 {
196 ((jdn - UNIX_EPOCH_JDN) * 86_400_000.0).round() as i64
197}
198
199fn jdn_to_unix(jdn: f64) -> i64 {
200 jdn_to_unix_millis(jdn).div_euclid(1000)
204}
205
206fn jdn_to_unix_subsec(jdn: f64) -> f64 {
207 jdn_to_unix_millis(jdn) as f64 / 1000.0
208}
209
210fn unix_to_jdn(ts: f64) -> f64 {
211 ts / 86400.0 + UNIX_EPOCH_JDN
212}
213
214fn is_leap_year(y: i64) -> bool {
215 (y % 4 == 0 && y % 100 != 0) || y % 400 == 0
216}
217
218fn days_in_month(y: i64, m: i64) -> i64 {
219 match m {
220 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
221 4 | 6 | 9 | 11 => 30,
222 2 => {
223 if is_leap_year(y) {
224 29
225 } else {
226 28
227 }
228 }
229 _ => 30,
230 }
231}
232
233fn day_of_year(y: i64, m: i64, d: i64) -> i64 {
234 let mut doy = d;
235 for mo in 1..m {
236 doy = doy.saturating_add(days_in_month(y, mo));
237 }
238 doy
239}
240
241fn parse_timestring(s: &str) -> Option<f64> {
245 let s = sqlite_c_string_str(s);
248
249 if s.eq_ignore_ascii_case("now")
256 || s.eq_ignore_ascii_case("subsec")
257 || s.eq_ignore_ascii_case("subsecond")
258 {
259 return Some(current_time_jdn());
260 }
261
262 let numeric = s.trim_matches(|c: char| c.is_ascii_whitespace());
265 if let Ok(jdn) = numeric.parse::<f64>()
266 && jdn >= 0.0
267 && jdn.is_finite()
268 {
269 return Some(jdn);
270 }
271
272 parse_iso8601(s.trim_end_matches(|c: char| c.is_ascii_whitespace()))
275}
276
277fn current_time_jdn() -> f64 {
278 if let Some(cached) = crate::builtins::statement_now() {
284 return cached;
285 }
286 use fsqlite_types::sync_primitives::SystemTime;
287
288 let secs = SystemTime::now()
289 .duration_since(SystemTime::UNIX_EPOCH)
290 .unwrap_or_default()
291 .as_secs_f64();
292 let jdn = UNIX_EPOCH_JDN + secs / 86_400.0;
293 crate::builtins::set_statement_now(jdn);
294 jdn
295}
296
297fn sqlite_c_string_bytes(bytes: &[u8]) -> &[u8] {
298 bytes
299 .iter()
300 .position(|&byte| byte == 0)
301 .map_or(bytes, |nul| &bytes[..nul])
302}
303
304fn sqlite_c_string_str(text: &str) -> &str {
305 let bytes = sqlite_c_string_bytes(text.as_bytes());
306 std::str::from_utf8(bytes).unwrap_or("")
309}
310
311fn sqlite_value_datetime_text(value: &SqliteValue) -> Option<Cow<'_, str>> {
312 match value {
313 SqliteValue::Null => None,
314 SqliteValue::Text(text) => Some(Cow::Borrowed(sqlite_c_string_str(text))),
315 SqliteValue::Blob(bytes) => std::str::from_utf8(sqlite_c_string_bytes(bytes))
316 .ok()
317 .map(Cow::Borrowed),
318 SqliteValue::Integer(_) | SqliteValue::Float(_) => Some(Cow::Owned(value.to_text())),
319 }
320}
321
322fn parse_iso8601(s: &str) -> Option<f64> {
323 let bytes = s.as_bytes();
331 let len = bytes.len();
332
333 if len >= 10 && bytes[4] == b'-' && bytes[7] == b'-' {
335 let y = s[0..4].parse::<i64>().ok()?;
336 let m = s[5..7].parse::<i64>().ok()?;
337 let d = s[8..10].parse::<i64>().ok()?;
338
339 if m < 1 || m > 12 || d < 1 || d > 31 {
340 return None;
341 }
342
343 if len == 10 {
344 return Some(ymd_to_jdn(y, m, d));
345 }
346
347 if len > 10 && (bytes[10] == b' ' || bytes[10] == b'T') {
349 let time_part = &s[11..];
350 let (h, mi, sec, frac, tz_offset_min) = parse_time_part_with_tz(time_part)?;
351 let jdn = ymdhms_to_jdn(y, m, d, h, mi, sec, frac);
352 return Some(jdn - (tz_offset_min as f64) / 1440.0);
355 }
356 return None;
357 }
358
359 if len >= 5 && bytes[2] == b':' {
361 let (h, mi, sec, frac, tz_offset_min) = parse_time_part_with_tz(s)?;
362 let jdn = ymdhms_to_jdn(2000, 1, 1, h, mi, sec, frac);
363 return Some(jdn - (tz_offset_min as f64) / 1440.0);
364 }
365
366 None
367}
368
369fn split_tz_suffix(s: &str) -> Option<(&str, i64)> {
377 if let Some(stripped) = s.strip_suffix('Z').or_else(|| s.strip_suffix('z')) {
379 return Some((stripped, 0));
380 }
381
382 let bytes = s.as_bytes();
391 for width in [6usize, 5, 3] {
393 if bytes.len() < width + 1 {
394 continue;
395 }
396 let split_at = bytes.len() - width;
397 let sign_byte = bytes[split_at];
398 if sign_byte != b'+' && sign_byte != b'-' {
399 continue;
400 }
401 let tz_part = &s[split_at..];
402 if let Some(offset) = parse_tz_offset(tz_part) {
403 return Some((&s[..split_at], offset));
404 }
405 }
406
407 Some((s, 0))
409}
410
411fn parse_tz_offset(tz: &str) -> Option<i64> {
414 let bytes = tz.as_bytes();
415 if bytes.is_empty() {
416 return None;
417 }
418 let sign: i64 = match bytes[0] {
419 b'+' => 1,
420 b'-' => -1,
421 _ => return None,
422 };
423 let rest = &tz[1..];
424 let (hours, minutes) = match rest.len() {
425 5 if rest.as_bytes()[2] == b':' => (
427 rest[0..2].parse::<i64>().ok()?,
428 rest[3..5].parse::<i64>().ok()?,
429 ),
430 4 => (
432 rest[0..2].parse::<i64>().ok()?,
433 rest[2..4].parse::<i64>().ok()?,
434 ),
435 2 => (rest.parse::<i64>().ok()?, 0),
437 _ => return None,
438 };
439 if !(0..=23).contains(&hours) || !(0..=59).contains(&minutes) {
440 return None;
441 }
442 Some(sign * (hours * 60 + minutes))
443}
444
445fn parse_time_part_with_tz(s: &str) -> Option<(i64, i64, i64, f64, i64)> {
448 let (time_only, tz_offset_min) = split_tz_suffix(s)?;
449 let (h, mi, sec, frac) = parse_time_part(time_only)?;
450 Some((h, mi, sec, frac, tz_offset_min))
451}
452
453fn parse_time_part(s: &str) -> Option<(i64, i64, i64, f64)> {
455 let [h_tens, h_ones, b':', mi_tens, mi_ones, rest @ ..] = s.as_bytes() else {
456 return None;
457 };
458 let h = parse_two_ascii_digits(*h_tens, *h_ones)?;
463 let mi = parse_two_ascii_digits(*mi_tens, *mi_ones)?;
464 if !(0..=23).contains(&h) || !(0..=59).contains(&mi) {
465 return None;
466 }
467
468 match rest {
473 [] => Some((h, mi, 0, 0.0)),
474 [b':', sec_tens, sec_ones] => {
475 let sec = parse_two_ascii_digits(*sec_tens, *sec_ones)?;
476 if !(0..=59).contains(&sec) {
477 return None;
478 }
479 Some((h, mi, sec, 0.0))
480 }
481 [b':', sec_tens, sec_ones, b'.', ..] => {
482 let sec = parse_two_ascii_digits(*sec_tens, *sec_ones)?;
483 if !(0..=59).contains(&sec) {
484 return None;
485 }
486 let frac = s.get(8..)?.parse::<f64>().ok()?;
487 Some((h, mi, sec, frac))
488 }
489 _ => None,
490 }
491}
492
493#[inline]
494fn parse_two_ascii_digits(tens: u8, ones: u8) -> Option<i64> {
495 if tens.is_ascii_digit() && ones.is_ascii_digit() {
496 Some(i64::from((tens - b'0') * 10 + (ones - b'0')))
497 } else {
498 None
499 }
500}
501
502fn apply_modifier(jdn: f64, modifier: &str) -> Option<f64> {
506 if has_outer_ascii_whitespace(modifier) {
507 return None;
508 }
509 let m = modifier.to_ascii_lowercase();
510
511 if m == "start of month" {
513 let (y, mo, _d) = jdn_to_ymd(jdn);
514 return Some(ymd_to_jdn(y, mo, 1));
515 }
516 if m == "start of year" {
517 let (y, _mo, _d) = jdn_to_ymd(jdn);
518 return Some(ymd_to_jdn(y, 1, 1));
519 }
520 if m == "start of day" {
521 let (y, mo, d) = jdn_to_ymd(jdn);
522 return Some(ymd_to_jdn(y, mo, d));
523 }
524
525 if m == "unixepoch" {
527 return Some(unix_to_jdn(jdn));
528 }
529
530 if m == "julianday" {
533 return Some(jdn);
534 }
535
536 if m == "auto" {
541 if (0.0..=AUTO_JDN_MAX).contains(&jdn) {
542 return Some(jdn);
543 }
544 if (AUTO_UNIX_MIN..=AUTO_UNIX_MAX).contains(&jdn) {
545 return Some(unix_to_jdn(jdn));
546 }
547 return None;
548 }
549
550 if m == "localtime" {
553 let offset = utc_offset_for_utc_jdn(jdn);
554 return Some(jdn + offset as f64 / 86400.0);
555 }
556 if m == "utc" {
559 let offset = utc_offset_for_local_jdn(jdn);
560 return Some(jdn - offset as f64 / 86400.0);
561 }
562
563 if m == "subsec" || m == "subsecond" {
566 return Some(jdn);
567 }
568
569 if let Some(rest) = m.strip_prefix("weekday ") {
571 let wd = rest.trim().parse::<i64>().ok()?;
572 if !(0..=6).contains(&wd) {
573 return None;
574 }
575 let current_jdn_int = (jdn + 0.5).floor() as i64;
577 let current_wd = (current_jdn_int + 1) % 7; let mut diff = wd - current_wd;
579 if diff < 0 {
580 diff += 7;
581 }
582 return Some(jdn + diff as f64);
584 }
585
586 parse_arithmetic_modifier(&m).map(|delta| jdn + delta)
588}
589
590fn parse_arithmetic_modifier(m: &str) -> Option<f64> {
594 let (sign, rest) = if let Some(r) = m.strip_prefix('+') {
595 (1.0, r.trim())
596 } else if let Some(r) = m.strip_prefix('-') {
597 (-1.0, r.trim())
598 } else {
599 (1.0, m.trim())
600 };
601
602 let mut parts = rest.splitn(2, ' ');
603 let num_str = parts.next()?;
604 let unit = parts.next()?.trim();
605
606 let num = num_str.parse::<f64>().ok().filter(|f| f.is_finite())?;
608 let delta = num * sign;
609
610 match unit.trim_end_matches('s') {
611 "day" => Some(delta),
612 "hour" => Some(delta / 24.0),
613 "minute" => Some(delta / 1440.0),
614 "second" => Some(delta / 86400.0),
615 "month" => Some(apply_month_delta(delta)),
616 "year" => Some(apply_month_delta(delta * 12.0)),
617 _ => None,
618 }
619}
620
621fn apply_month_delta(months: f64) -> f64 {
626 months * 30.436875
628}
629
630fn has_outer_ascii_whitespace(value: &str) -> bool {
631 value
632 .as_bytes()
633 .first()
634 .is_some_and(u8::is_ascii_whitespace)
635 || value.as_bytes().last().is_some_and(u8::is_ascii_whitespace)
636}
637
638fn compute_floor(y: i64, m: i64, d: i64) -> i64 {
642 if d <= 28 {
643 0
644 } else if ((1_i64 << m) & 0x15aa) != 0 {
645 0
647 } else if m != 2 {
648 i64::from(d == 31)
650 } else if y % 4 != 0 || (y % 100 == 0 && y % 400 != 0) {
651 d - 28 } else {
653 d - 29 }
655}
656
657fn apply_modifiers(jdn: f64, modifiers: &[String], mut raw_numeric: bool) -> Option<(f64, bool)> {
659 let mut j = jdn;
660 let mut subsec = false;
661 let mut n_floor: i64 = 0;
664 for (index, m) in modifiers.iter().enumerate() {
665 if has_outer_ascii_whitespace(m) {
666 return None;
667 }
668 let m_lower = m.to_ascii_lowercase();
669 if matches!(m_lower.as_str(), "unixepoch" | "julianday" | "auto") {
670 if index != 0 || !raw_numeric {
674 return None;
675 }
676 raw_numeric = false;
677 } else if m_lower != "subsec" && m_lower != "subsecond" {
678 raw_numeric = false;
679 }
680 if m_lower == "subsec" || m_lower == "subsecond" {
681 subsec = true;
682 continue;
683 }
684 if m_lower == "ceiling" {
689 n_floor = 0;
690 continue;
691 }
692 if m_lower == "floor" {
693 j -= n_floor as f64;
694 n_floor = 0;
695 continue;
696 }
697 if is_month_year_modifier(&m_lower) {
703 match apply_month_year_exact(j, &m_lower) {
704 Ok(Some((new_jdn, nf))) => {
705 j = new_jdn;
706 n_floor = nf;
707 continue;
708 }
709 Ok(None) => return None,
710 Err(()) => {
711 }
713 }
714 }
715 n_floor = 0;
717 j = apply_modifier(j, m)?;
718 }
719 Some((j, subsec))
720}
721
722fn is_month_year_modifier(m: &str) -> bool {
723 m.contains("month") || m.contains("year")
728}
729
730fn apply_month_year_exact(jdn: f64, m: &str) -> std::result::Result<Option<(f64, i64)>, ()> {
736 let (sign, rest) = if let Some(r) = m.strip_prefix('+') {
737 (1_i64, r.trim())
738 } else if let Some(r) = m.strip_prefix('-') {
739 (-1_i64, r.trim())
740 } else {
741 (1_i64, m.trim())
745 };
746
747 let mut parts = rest.splitn(2, ' ');
748 let num_str = parts.next().ok_or(())?;
749 let unit = parts.next().ok_or(())?.trim();
750
751 let num = if let Ok(n) = num_str.parse::<i64>() {
753 n
754 } else if let Ok(f) = num_str.parse::<f64>() {
755 if f.fract() == 0.0 && f >= i64::MIN as f64 && f <= i64::MAX as f64 {
756 f as i64
757 } else {
758 return Err(());
759 }
760 } else {
761 return Err(());
762 };
763
764 let (y, mo, d) = jdn_to_ymd(jdn);
765 let (h, mi, s, frac) = jdn_to_hms(jdn);
766
767 let total_months = match unit.trim_end_matches('s') {
768 "month" => {
769 if let Some(val) = num.checked_mul(sign) {
770 val
771 } else {
772 return Ok(None);
773 }
774 }
775 "year" => {
776 if let Some(val) = num.checked_mul(sign).and_then(|v| v.checked_mul(12)) {
777 val
778 } else {
779 return Ok(None);
780 }
781 }
782 _ => return Err(()),
783 };
784
785 let current_months = if let Some(val) = y.checked_mul(12).and_then(|v| v.checked_add(mo - 1)) {
787 val
788 } else {
789 return Ok(None);
790 };
791 let new_total = if let Some(val) = current_months.checked_add(total_months) {
792 val
793 } else {
794 return Ok(None);
795 };
796
797 let new_y = new_total.div_euclid(12);
798 let new_mo = new_total.rem_euclid(12) + 1;
799 let n_floor = compute_floor(new_y, new_mo, d);
804 Ok(Some((
805 ymdhms_to_jdn(new_y, new_mo, d, h, mi, s, frac),
806 n_floor,
807 )))
808}
809
810struct StackStr {
816 buf: [u8; 48],
817 len: usize,
818}
819
820impl StackStr {
821 fn new() -> Self {
822 Self {
823 buf: [0; 48],
824 len: 0,
825 }
826 }
827
828 fn as_str(&self) -> &str {
829 core::str::from_utf8(&self.buf[..self.len]).unwrap_or("")
831 }
832}
833
834impl core::fmt::Write for StackStr {
835 fn write_str(&mut self, s: &str) -> core::fmt::Result {
836 let end = self.len + s.len();
837 if end > self.buf.len() {
838 return Err(core::fmt::Error);
839 }
840 self.buf[self.len..end].copy_from_slice(s.as_bytes());
841 self.len = end;
842 Ok(())
843 }
844}
845
846fn build_small_text(write: impl Fn(&mut dyn core::fmt::Write) -> core::fmt::Result) -> SmallText {
853 let mut buf = StackStr::new();
854 if write(&mut buf).is_ok() {
855 SmallText::new(buf.as_str())
856 } else {
857 let mut heap = String::new();
858 let _ = write(&mut heap);
859 SmallText::from_string(heap)
860 }
861}
862
863fn format_date(jdn: f64) -> SmallText {
864 let (y, m, d) = jdn_to_ymd(jdn);
865 build_small_text(move |w| write!(w, "{y:04}-{m:02}-{d:02}"))
866}
867
868#[derive(Clone, Copy)]
869struct UnmodifiedHms {
870 hour: i64,
871 minute: i64,
872 second: i64,
873 fraction: f64,
874}
875
876fn hms_for_output(jdn: f64, unmodified: Option<UnmodifiedHms>) -> UnmodifiedHms {
877 unmodified.unwrap_or_else(|| {
878 let (hour, minute, second, fraction) = jdn_to_hms(jdn);
879 UnmodifiedHms {
880 hour,
881 minute,
882 second,
883 fraction,
884 }
885 })
886}
887
888fn rounded_second_and_millis(hms: UnmodifiedHms) -> (i64, i64) {
889 let total_millis = ((hms.second as f64 + hms.fraction) * 1000.0 + 0.5).floor() as i64;
893 (total_millis / 1000, total_millis % 1000)
894}
895
896fn format_time(jdn: f64, subsec: bool, unmodified: Option<UnmodifiedHms>) -> SmallText {
897 let hms = hms_for_output(jdn, unmodified);
898 let (h, m) = (hms.hour, hms.minute);
899 if subsec {
900 let (s, ms) = rounded_second_and_millis(hms);
901 build_small_text(move |w| write!(w, "{h:02}:{m:02}:{s:02}.{ms:03}"))
902 } else {
903 let s = hms.second;
904 build_small_text(move |w| write!(w, "{h:02}:{m:02}:{s:02}"))
905 }
906}
907
908fn format_datetime(jdn: f64, subsec: bool, unmodified: Option<UnmodifiedHms>) -> SmallText {
909 let (y, mo, d) = jdn_to_ymd(jdn);
910 let hms = hms_for_output(jdn, unmodified);
911 let (h, mi) = (hms.hour, hms.minute);
912 if subsec {
913 let (s, ms) = rounded_second_and_millis(hms);
914 build_small_text(move |w| write!(w, "{y:04}-{mo:02}-{d:02} {h:02}:{mi:02}:{s:02}.{ms:03}"))
915 } else {
916 let s = hms.second;
917 build_small_text(move |w| write!(w, "{y:04}-{mo:02}-{d:02} {h:02}:{mi:02}:{s:02}"))
918 }
919}
920
921#[inline]
922fn push_format(result: &mut String, args: Arguments<'_>) {
923 let _ = result.write_fmt(args);
924}
925
926#[inline]
927fn push_zero_padded_2(result: &mut String, value: i64) {
928 if (0..=99).contains(&value) {
929 let value = value as u8;
930 result.push(char::from(b'0' + value / 10));
931 result.push(char::from(b'0' + value % 10));
932 } else {
933 push_format(result, format_args!("{value:02}"));
934 }
935}
936
937#[inline]
938fn push_space_padded_2(result: &mut String, value: i64) {
939 if (0..=99).contains(&value) {
940 let value = value as u8;
941 if value >= 10 {
942 result.push(char::from(b'0' + value / 10));
943 } else {
944 result.push(' ');
945 }
946 result.push(char::from(b'0' + value % 10));
947 } else {
948 push_format(result, format_args!("{value:>2}"));
949 }
950}
951
952#[inline]
953fn push_zero_padded_3(result: &mut String, value: i64) {
954 if (0..=999).contains(&value) {
955 let value = value as u16;
956 result.push(char::from(b'0' + (value / 100) as u8));
957 result.push(char::from(b'0' + ((value / 10) % 10) as u8));
958 result.push(char::from(b'0' + (value % 10) as u8));
959 } else {
960 push_format(result, format_args!("{value:03}"));
961 }
962}
963
964#[inline]
965fn push_zero_padded_4(result: &mut String, value: i64) {
966 if (0..=9999).contains(&value) {
967 let value = value as u16;
968 result.push(char::from(b'0' + (value / 1000) as u8));
969 result.push(char::from(b'0' + ((value / 100) % 10) as u8));
970 result.push(char::from(b'0' + ((value / 10) % 10) as u8));
971 result.push(char::from(b'0' + (value % 10) as u8));
972 } else {
973 push_format(result, format_args!("{value:04}"));
974 }
975}
976
977fn format_strftime(fmt: &str, jdn: f64, subsec: bool, unmodified: Option<UnmodifiedHms>) -> String {
979 let (y, mo, d) = jdn_to_ymd(jdn);
980 let hms = hms_for_output(jdn, unmodified);
981 let (h, mi, s, frac) = (hms.hour, hms.minute, hms.second, hms.fraction);
982 let doy = day_of_year(y, mo, d);
983 let jdn_int = (jdn + 0.5).floor() as i64;
985 let dow = (jdn_int + 1) % 7; let mut result = String::with_capacity(fmt.len().saturating_add(8));
988 let bytes = fmt.as_bytes();
989 let mut i = 0;
990 let mut literal_start = 0;
991
992 while i < bytes.len() {
993 if bytes[i] != b'%' || i + 1 >= bytes.len() {
994 i += 1;
995 continue;
996 }
997
998 result.push_str(&fmt[literal_start..i]);
999
1000 let spec_suffix = &fmt[i + 1..];
1001 let Some(spec) = spec_suffix.chars().next() else {
1002 break;
1003 };
1004 i += 1 + spec.len_utf8();
1005 literal_start = i;
1006
1007 match spec {
1008 'd' => push_zero_padded_2(&mut result, d),
1009 'e' => push_space_padded_2(&mut result, d),
1010 'F' => {
1011 push_zero_padded_4(&mut result, y);
1013 result.push('-');
1014 push_zero_padded_2(&mut result, mo);
1015 result.push('-');
1016 push_zero_padded_2(&mut result, d);
1017 }
1018 'f' => {
1019 let total = (s as f64 + frac).min(59.999);
1021 push_format(&mut result, format_args!("{total:06.3}"));
1022 }
1023 'H' => push_zero_padded_2(&mut result, h),
1024 'I' => {
1025 let h12 = if h == 0 {
1027 12
1028 } else if h > 12 {
1029 h - 12
1030 } else {
1031 h
1032 };
1033 push_zero_padded_2(&mut result, h12);
1034 }
1035 'j' => push_zero_padded_3(&mut result, doy),
1036 'J' => {
1037 push_format(&mut result, format_args!("{jdn:.15}"));
1039 while result.as_bytes().last() == Some(&b'0') {
1040 result.pop();
1041 }
1042 if result.as_bytes().last() == Some(&b'.') {
1043 result.pop();
1044 }
1045 }
1046 'k' => {
1047 push_space_padded_2(&mut result, h);
1049 }
1050 'l' => {
1051 let h12 = if h == 0 {
1053 12
1054 } else if h > 12 {
1055 h - 12
1056 } else {
1057 h
1058 };
1059 push_space_padded_2(&mut result, h12);
1060 }
1061 'm' => push_zero_padded_2(&mut result, mo),
1062 'M' => push_zero_padded_2(&mut result, mi),
1063 'p' => {
1064 result.push_str(if h < 12 { "AM" } else { "PM" });
1065 }
1066 'P' => {
1067 result.push_str(if h < 12 { "am" } else { "pm" });
1068 }
1069 'R' => {
1070 push_zero_padded_2(&mut result, h);
1071 result.push(':');
1072 push_zero_padded_2(&mut result, mi);
1073 }
1074 's' => {
1075 if subsec {
1076 let unix = jdn_to_unix_subsec(jdn);
1077 push_format(&mut result, format_args!("{unix:.3}"));
1078 } else {
1079 let unix = jdn_to_unix(jdn);
1080 push_format(&mut result, format_args!("{unix}"));
1081 }
1082 }
1083 'S' => push_zero_padded_2(&mut result, s),
1084 'T' => {
1085 push_zero_padded_2(&mut result, h);
1086 result.push(':');
1087 push_zero_padded_2(&mut result, mi);
1088 result.push(':');
1089 push_zero_padded_2(&mut result, s);
1090 }
1091 'u' => {
1092 let u = if dow == 0 { 7 } else { dow };
1094 push_format(&mut result, format_args!("{u}"));
1095 }
1096 'w' => push_format(&mut result, format_args!("{dow}")),
1097 'W' => {
1098 let w = (doy + 6 - ((dow + 6) % 7)) / 7;
1100 push_zero_padded_2(&mut result, w);
1101 }
1102 'Y' => push_zero_padded_4(&mut result, y),
1103 'G' | 'g' | 'V' => {
1104 let (iso_y, iso_w) = iso_week(y, mo, d);
1106 match spec {
1107 'G' => push_zero_padded_4(&mut result, iso_y),
1108 'g' => push_zero_padded_2(&mut result, iso_y % 100),
1109 'V' => push_zero_padded_2(&mut result, iso_w),
1110 _ => unreachable!(),
1111 }
1112 }
1113 '%' => result.push('%'),
1114 other => {
1115 result.push('%');
1116 result.push(other);
1117 }
1118 }
1119 }
1120
1121 if literal_start < fmt.len() {
1122 result.push_str(&fmt[literal_start..]);
1123 }
1124
1125 result
1126}
1127
1128fn iso_week(y: i64, m: i64, d: i64) -> (i64, i64) {
1130 let jdn = ymd_to_jdn(y, m, d);
1131 let jdn_int = (jdn + 0.5).floor() as i64;
1132 let dow = (jdn_int + 1) % 7;
1134 let iso_dow = if dow == 0 { 7 } else { dow };
1135
1136 let thu_jdn = jdn_int + (4 - iso_dow);
1138 let (thu_y, _, _) = jdn_to_ymd(thu_jdn as f64);
1139
1140 let jan4_jdn = (ymd_to_jdn(thu_y, 1, 4) + 0.5).floor() as i64;
1142 let jan4_dow = (jan4_jdn + 1) % 7;
1143 let jan4_iso_dow = if jan4_dow == 0 { 7 } else { jan4_dow };
1144 let week1_start = jan4_jdn - (jan4_iso_dow - 1);
1145
1146 let week = (thu_jdn - week1_start) / 7 + 1;
1147 (thu_y, week)
1148}
1149
1150fn timediff_impl(jdn1: f64, jdn2: f64) -> String {
1153 let (sign, start_jdn, end_jdn) = if jdn1 >= jdn2 {
1154 ('+', jdn2, jdn1)
1155 } else {
1156 ('-', jdn1, jdn2)
1157 };
1158
1159 let (start_y, start_mo, start_d) = jdn_to_ymd(start_jdn);
1160 let (start_h, start_mi, mut start_s, start_frac) = jdn_to_hms(start_jdn);
1161 let mut start_ms = (start_frac * 1000.0).round() as i64;
1162 if start_ms >= 1000 {
1163 start_ms = 0;
1164 start_s += 1;
1165 }
1166
1167 let (end_y, end_mo, end_d) = jdn_to_ymd(end_jdn);
1168 let (end_h, end_mi, mut end_s, end_frac) = jdn_to_hms(end_jdn);
1169 let mut end_ms = (end_frac * 1000.0).round() as i64;
1170 if end_ms >= 1000 {
1171 end_ms = 0;
1172 end_s += 1;
1173 }
1174
1175 let mut years = end_y - start_y;
1176 let mut months = end_mo - start_mo;
1177 let mut days = end_d - start_d;
1178 let mut hours = end_h - start_h;
1179 let mut minutes = end_mi - start_mi;
1180 let mut seconds = end_s - start_s;
1181 let mut millis = end_ms - start_ms;
1182
1183 if millis < 0 {
1184 millis += 1000;
1185 seconds -= 1;
1186 }
1187 if seconds < 0 {
1188 seconds += 60;
1189 minutes -= 1;
1190 }
1191 if minutes < 0 {
1192 minutes += 60;
1193 hours -= 1;
1194 }
1195 if hours < 0 {
1196 hours += 24;
1197 days -= 1;
1198 }
1199 if days < 0 {
1200 months -= 1;
1201 let (borrow_y, borrow_mo) = if end_mo == 1 {
1202 (end_y - 1, 12)
1203 } else {
1204 (end_y, end_mo - 1)
1205 };
1206 days += days_in_month(borrow_y, borrow_mo);
1207 }
1208 if months < 0 {
1209 months += 12;
1210 years -= 1;
1211 }
1212
1213 format!(
1214 "{sign}{years:04}-{months:02}-{days:02} {hours:02}:{minutes:02}:{seconds:02}.{millis:03}"
1215 )
1216}
1217
1218#[must_use]
1245pub fn is_datetime_invocation_safe_for_schema(function_name: &str, args: &[SqliteValue]) -> bool {
1246 if function_name.eq_ignore_ascii_case("strftime") {
1247 return args.first().is_none_or(SqliteValue::is_null)
1251 || datetime_arguments_are_schema_safe(&args[1..]);
1252 }
1253
1254 if function_name.eq_ignore_ascii_case("timediff") {
1255 if args.len() != 2 {
1259 return true;
1260 }
1261 for time_value in args {
1262 match classify_time_value_for_schema(time_value) {
1263 SchemaTimeValue::Dynamic => return false,
1264 SchemaTimeValue::NullOrInvalid => return true,
1265 SchemaTimeValue::Fixed { .. } => {}
1266 }
1267 }
1268 return true;
1269 }
1270
1271 if function_name.eq_ignore_ascii_case("date")
1272 || function_name.eq_ignore_ascii_case("time")
1273 || function_name.eq_ignore_ascii_case("datetime")
1274 || function_name.eq_ignore_ascii_case("julianday")
1275 || function_name.eq_ignore_ascii_case("unixepoch")
1276 {
1277 return datetime_arguments_are_schema_safe(args);
1278 }
1279
1280 true
1281}
1282
1283fn datetime_arguments_are_schema_safe(args: &[SqliteValue]) -> bool {
1285 let Some(time_value) = args.first() else {
1286 return false;
1287 };
1288 let (input, raw_numeric) = match classify_time_value_for_schema(time_value) {
1289 SchemaTimeValue::Dynamic => return false,
1290 SchemaTimeValue::NullOrInvalid => return true,
1291 SchemaTimeValue::Fixed { input, raw_numeric } => (input, raw_numeric),
1292 };
1293
1294 let mut reached_modifiers = Vec::with_capacity(args.len().saturating_sub(1));
1295 for modifier in &args[1..] {
1296 if modifier.is_null() {
1297 return true;
1298 }
1299 if sqlite_value_is_keyword(modifier, "localtime")
1300 || sqlite_value_is_keyword(modifier, "utc")
1301 {
1302 return false;
1303 }
1304 let Some(modifier) = sqlite_value_datetime_text(modifier) else {
1305 return true;
1306 };
1307 reached_modifiers.push(modifier.into_owned());
1308 if apply_modifiers(input, &reached_modifiers, raw_numeric).is_none() {
1309 return true;
1312 }
1313 }
1314 true
1315}
1316
1317enum SchemaTimeValue {
1318 Dynamic,
1319 NullOrInvalid,
1320 Fixed { input: f64, raw_numeric: bool },
1321}
1322
1323fn classify_time_value_for_schema(value: &SqliteValue) -> SchemaTimeValue {
1324 if value.is_null() {
1325 return SchemaTimeValue::NullOrInvalid;
1326 }
1327 if is_implicit_now_time_value(value) {
1328 return SchemaTimeValue::Dynamic;
1329 }
1330 match parse_time_value(value) {
1331 Some(parsed) => SchemaTimeValue::Fixed {
1332 input: parsed.jdn,
1333 raw_numeric: parsed.raw_numeric,
1334 },
1335 None => SchemaTimeValue::NullOrInvalid,
1336 }
1337}
1338
1339fn is_implicit_now_time_value(value: &SqliteValue) -> bool {
1340 sqlite_value_is_keyword(value, "now")
1341 || sqlite_value_is_keyword(value, "subsec")
1342 || sqlite_value_is_keyword(value, "subsecond")
1343}
1344
1345fn sqlite_value_is_keyword(value: &SqliteValue, keyword: &str) -> bool {
1349 let bytes = match value {
1350 SqliteValue::Text(text) => sqlite_c_string_bytes(text.as_bytes_direct()),
1351 SqliteValue::Blob(bytes) => sqlite_c_string_bytes(bytes),
1352 SqliteValue::Null | SqliteValue::Integer(_) | SqliteValue::Float(_) => return false,
1353 };
1354 bytes.eq_ignore_ascii_case(keyword.as_bytes())
1355}
1356
1357#[derive(Clone, Copy)]
1358struct ParsedTimeValue {
1359 jdn: f64,
1360 raw_numeric: bool,
1361 unmodified_hms: Option<UnmodifiedHms>,
1362}
1363
1364fn parse_time_value(value: &SqliteValue) -> Option<ParsedTimeValue> {
1365 match value {
1366 SqliteValue::Null => None,
1367 SqliteValue::Integer(integer) => Some(ParsedTimeValue {
1368 jdn: *integer as f64,
1369 raw_numeric: true,
1370 unmodified_hms: None,
1371 }),
1372 SqliteValue::Float(float) if float.is_finite() => Some(ParsedTimeValue {
1373 jdn: *float,
1374 raw_numeric: true,
1375 unmodified_hms: None,
1376 }),
1377 SqliteValue::Float(_) => None,
1378 SqliteValue::Text(_) | SqliteValue::Blob(_) => {
1379 let text = sqlite_value_datetime_text(value)?;
1380 let numeric = text.trim_matches(|c: char| c.is_ascii_whitespace());
1381 if let Ok(number) = numeric.parse::<f64>()
1382 && number.is_finite()
1383 {
1384 return Some(ParsedTimeValue {
1385 jdn: number,
1386 raw_numeric: true,
1387 unmodified_hms: None,
1388 });
1389 }
1390 Some(ParsedTimeValue {
1391 jdn: parse_timestring(text.as_ref())?,
1392 raw_numeric: false,
1393 unmodified_hms: unmodified_hms_from_timestring(text.as_ref()),
1394 })
1395 }
1396 }
1397}
1398
1399fn unmodified_hms_from_timestring(value: &str) -> Option<UnmodifiedHms> {
1400 let value = sqlite_c_string_str(value).trim_end_matches(|c: char| c.is_ascii_whitespace());
1401 let time = if value.len() > 10
1402 && value.as_bytes().get(4) == Some(&b'-')
1403 && value.as_bytes().get(7) == Some(&b'-')
1404 && value
1405 .as_bytes()
1406 .get(10)
1407 .is_some_and(|separator| matches!(*separator, b' ' | b'T'))
1408 {
1409 &value[11..]
1410 } else if value.len() >= 5 && value.as_bytes().get(2) == Some(&b':') {
1411 value
1412 } else {
1413 return None;
1414 };
1415 let (hour, minute, second, fraction, timezone_offset) = parse_time_part_with_tz(time)?;
1416 (timezone_offset == 0).then_some(UnmodifiedHms {
1417 hour,
1418 minute,
1419 second,
1420 fraction,
1421 })
1422}
1423
1424struct ParsedDateTimeArgs {
1425 jdn: f64,
1426 subsec: bool,
1427 unmodified_hms: Option<UnmodifiedHms>,
1428}
1429
1430fn parse_args(args: &[SqliteValue]) -> Option<ParsedDateTimeArgs> {
1432 let first_position_subsec = args.first().is_some_and(|value| {
1433 sqlite_value_is_keyword(value, "subsec") || sqlite_value_is_keyword(value, "subsecond")
1434 });
1435 let parsed = match args.first() {
1436 None => ParsedTimeValue {
1437 jdn: current_time_jdn(),
1438 raw_numeric: false,
1439 unmodified_hms: None,
1440 },
1441 Some(value) => parse_time_value(value)?,
1442 };
1443
1444 let mut modifiers = Vec::with_capacity(args.len().saturating_sub(1));
1445 for modifier in args.get(1..).unwrap_or_default() {
1446 modifiers.push(sqlite_value_datetime_text(modifier)?.into_owned());
1447 }
1448
1449 if parsed.raw_numeric {
1454 let first = modifiers
1455 .first()
1456 .map(|modifier| modifier.to_ascii_lowercase());
1457 let reinterprets_raw = matches!(first.as_deref(), Some("unixepoch" | "julianday" | "auto"));
1458 if !reinterprets_raw && !(0.0..=AUTO_JDN_MAX).contains(&parsed.jdn) {
1459 return None;
1460 }
1461 }
1462
1463 let preserves_input_hms = modifiers.iter().all(|modifier| {
1464 modifier.eq_ignore_ascii_case("subsec") || modifier.eq_ignore_ascii_case("subsecond")
1465 });
1466 let (jdn, modifier_subsec) = apply_modifiers(parsed.jdn, &modifiers, parsed.raw_numeric)?;
1467 Some(ParsedDateTimeArgs {
1468 jdn,
1469 subsec: first_position_subsec || modifier_subsec,
1470 unmodified_hms: preserves_input_hms
1471 .then_some(parsed.unmodified_hms)
1472 .flatten(),
1473 })
1474}
1475
1476pub struct DateFunc;
1479
1480impl ScalarFunction for DateFunc {
1481 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1482 match parse_args(args) {
1483 Some(parsed) => Ok(SqliteValue::Text(format_date(parsed.jdn))),
1484 None => Ok(SqliteValue::Null),
1485 }
1486 }
1487
1488 fn num_args(&self) -> i32 {
1489 -1
1490 }
1491
1492 fn is_deterministic(&self) -> bool {
1493 false
1494 }
1495
1496 fn name(&self) -> &str {
1497 "date"
1498 }
1499}
1500
1501pub struct TimeFunc;
1504
1505impl ScalarFunction for TimeFunc {
1506 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1507 match parse_args(args) {
1508 Some(parsed) => Ok(SqliteValue::Text(format_time(
1509 parsed.jdn,
1510 parsed.subsec,
1511 parsed.unmodified_hms,
1512 ))),
1513 None => Ok(SqliteValue::Null),
1514 }
1515 }
1516
1517 fn num_args(&self) -> i32 {
1518 -1
1519 }
1520
1521 fn is_deterministic(&self) -> bool {
1522 false
1523 }
1524
1525 fn name(&self) -> &str {
1526 "time"
1527 }
1528}
1529
1530pub struct DateTimeFunc;
1533
1534impl ScalarFunction for DateTimeFunc {
1535 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1536 match parse_args(args) {
1537 Some(parsed) => Ok(SqliteValue::Text(format_datetime(
1538 parsed.jdn,
1539 parsed.subsec,
1540 parsed.unmodified_hms,
1541 ))),
1542 None => Ok(SqliteValue::Null),
1543 }
1544 }
1545
1546 fn num_args(&self) -> i32 {
1547 -1
1548 }
1549
1550 fn is_deterministic(&self) -> bool {
1551 false
1552 }
1553
1554 fn name(&self) -> &str {
1555 "datetime"
1556 }
1557}
1558
1559pub struct JuliandayFunc;
1562
1563impl ScalarFunction for JuliandayFunc {
1564 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1565 match parse_args(args) {
1566 Some(parsed) => Ok(SqliteValue::Float(parsed.jdn)),
1567 None => Ok(SqliteValue::Null),
1568 }
1569 }
1570
1571 fn num_args(&self) -> i32 {
1572 -1
1573 }
1574
1575 fn is_deterministic(&self) -> bool {
1576 false
1577 }
1578
1579 fn name(&self) -> &str {
1580 "julianday"
1581 }
1582}
1583
1584pub struct UnixepochFunc;
1587
1588impl ScalarFunction for UnixepochFunc {
1589 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1590 match parse_args(args) {
1591 Some(parsed) if parsed.subsec => Ok(SqliteValue::Float(jdn_to_unix_subsec(parsed.jdn))),
1594 Some(parsed) => Ok(SqliteValue::Integer(jdn_to_unix(parsed.jdn))),
1595 None => Ok(SqliteValue::Null),
1596 }
1597 }
1598
1599 fn num_args(&self) -> i32 {
1600 -1
1601 }
1602
1603 fn is_deterministic(&self) -> bool {
1604 false
1605 }
1606
1607 fn name(&self) -> &str {
1608 "unixepoch"
1609 }
1610}
1611
1612pub struct StrftimeFunc;
1615
1616impl ScalarFunction for StrftimeFunc {
1617 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1618 let Some(format_value) = args.first() else {
1619 return Ok(SqliteValue::Null);
1620 };
1621 let Some(fmt) = sqlite_value_datetime_text(format_value) else {
1622 return Ok(SqliteValue::Null);
1623 };
1624 let rest = &args[1..];
1625 match parse_args(rest) {
1626 Some(parsed) => Ok(SqliteValue::Text(
1627 format_strftime(
1628 fmt.as_ref(),
1629 parsed.jdn,
1630 parsed.subsec,
1631 parsed.unmodified_hms,
1632 )
1633 .into(),
1634 )),
1635 None => Ok(SqliteValue::Null),
1636 }
1637 }
1638
1639 fn num_args(&self) -> i32 {
1640 -1
1641 }
1642
1643 fn is_deterministic(&self) -> bool {
1644 false
1645 }
1646
1647 fn name(&self) -> &str {
1648 "strftime"
1649 }
1650}
1651
1652pub struct TimediffFunc;
1655
1656impl ScalarFunction for TimediffFunc {
1657 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1658 if args.len() < 2 || args[0].is_null() || args[1].is_null() {
1659 return Ok(SqliteValue::Null);
1660 }
1661
1662 let jdn1 = parse_time_value(&args[0])
1663 .map(|parsed| parsed.jdn)
1664 .filter(|jdn| (0.0..=AUTO_JDN_MAX).contains(jdn));
1665 let jdn2 = parse_time_value(&args[1])
1666 .map(|parsed| parsed.jdn)
1667 .filter(|jdn| (0.0..=AUTO_JDN_MAX).contains(jdn));
1668
1669 match (jdn1, jdn2) {
1670 (Some(j1), Some(j2)) => Ok(SqliteValue::Text(timediff_impl(j1, j2).into())),
1671 _ => Ok(SqliteValue::Null),
1672 }
1673 }
1674
1675 fn num_args(&self) -> i32 {
1676 2
1677 }
1678
1679 fn is_deterministic(&self) -> bool {
1680 false
1681 }
1682
1683 fn name(&self) -> &str {
1684 "timediff"
1685 }
1686}
1687
1688pub fn register_datetime_builtins(registry: &mut FunctionRegistry) {
1692 registry.register_conditionally_deterministic_scalar(DateFunc);
1693 registry.register_conditionally_deterministic_scalar(TimeFunc);
1694 registry.register_conditionally_deterministic_scalar(DateTimeFunc);
1695 registry.register_conditionally_deterministic_scalar(JuliandayFunc);
1696 registry.register_conditionally_deterministic_scalar(UnixepochFunc);
1697 registry.register_conditionally_deterministic_scalar(StrftimeFunc);
1698 registry.register_conditionally_deterministic_scalar(TimediffFunc);
1699}
1700
1701#[cfg(test)]
1704mod tests {
1705 use super::*;
1706
1707 fn text(s: &str) -> SqliteValue {
1708 SqliteValue::Text(s.into())
1709 }
1710
1711 fn int(v: i64) -> SqliteValue {
1712 SqliteValue::Integer(v)
1713 }
1714
1715 fn float(v: f64) -> SqliteValue {
1716 SqliteValue::Float(v)
1717 }
1718
1719 fn null() -> SqliteValue {
1720 SqliteValue::Null
1721 }
1722
1723 fn assert_text(result: &SqliteValue, expected: &str) {
1724 match result {
1725 SqliteValue::Text(s) => assert_eq!(s.as_ref(), expected, "text mismatch"),
1726 other => panic!("expected Text(\"{expected}\"), got {other:?}"),
1727 }
1728 }
1729
1730 fn blob(s: &str) -> SqliteValue {
1733 SqliteValue::Blob(s.as_bytes().into())
1734 }
1735
1736 #[test]
1737 fn test_schema_safety_common_datetime_argument_layout() {
1738 for name in ["date", "time", "datetime", "julianday", "unixepoch"] {
1739 assert!(
1740 !is_datetime_invocation_safe_for_schema(name, &[]),
1741 "{name}() implicitly reads the current time"
1742 );
1743 assert!(is_datetime_invocation_safe_for_schema(
1744 name,
1745 &[text("2024-03-15 12:34:56")]
1746 ));
1747 assert!(is_datetime_invocation_safe_for_schema(name, &[int(0)]));
1748 assert!(is_datetime_invocation_safe_for_schema(
1749 name,
1750 &[float(2_460_384.5)]
1751 ));
1752 assert!(is_datetime_invocation_safe_for_schema(name, &[null()]));
1753
1754 for current_time in ["now", "NOW", "subsec", "SUBSECOND"] {
1755 assert!(
1756 !is_datetime_invocation_safe_for_schema(name, &[text(current_time)]),
1757 "{name}({current_time:?}) must be conditional"
1758 );
1759 }
1760 assert!(!is_datetime_invocation_safe_for_schema(
1761 name,
1762 &[blob("NOW")]
1763 ));
1764 assert!(!is_datetime_invocation_safe_for_schema(
1765 name,
1766 &[text("now\0ignored")]
1767 ));
1768 assert!(!is_datetime_invocation_safe_for_schema(
1769 name,
1770 &[SqliteValue::Blob(b"subsec\0\xff".as_slice().into())]
1771 ));
1772 for invalid_padded in [" now ", "subsec ", " subsecond"] {
1773 assert!(is_datetime_invocation_safe_for_schema(
1774 name,
1775 &[text(invalid_padded)]
1776 ));
1777 }
1778 assert!(is_datetime_invocation_safe_for_schema(
1779 name,
1780 &[blob(" NoW ")]
1781 ));
1782 assert!(is_datetime_invocation_safe_for_schema(
1783 name,
1784 &[text("nowhere")]
1785 ));
1786
1787 assert!(is_datetime_invocation_safe_for_schema(
1789 name,
1790 &[text("localtime")]
1791 ));
1792 assert!(is_datetime_invocation_safe_for_schema(name, &[text("utc")]));
1793 assert!(is_datetime_invocation_safe_for_schema(
1794 name,
1795 &[text("2024-03-15"), text("subsec")]
1796 ));
1797 assert!(is_datetime_invocation_safe_for_schema(
1798 name,
1799 &[text("2024-03-15"), text("subsecond")]
1800 ));
1801
1802 for modifier in ["localtime", "LOCALTIME", "utc"] {
1803 assert!(
1804 !is_datetime_invocation_safe_for_schema(
1805 name,
1806 &[text("2024-03-15"), text(modifier)]
1807 ),
1808 "{name} modifier {modifier:?} depends on host-local state"
1809 );
1810 }
1811 assert!(!is_datetime_invocation_safe_for_schema(
1812 name,
1813 &[text("2024-03-15"), blob("UTC")]
1814 ));
1815 assert!(!is_datetime_invocation_safe_for_schema(
1816 name,
1817 &[text("2024-03-15"), text("localtime\0ignored")]
1818 ));
1819 assert!(is_datetime_invocation_safe_for_schema(
1820 name,
1821 &[text("2024-03-15"), text(" utc ")]
1822 ));
1823
1824 assert!(is_datetime_invocation_safe_for_schema(
1826 name,
1827 &[text("2024-03-15"), null(), text("localtime")]
1828 ));
1829 assert!(!is_datetime_invocation_safe_for_schema(
1830 name,
1831 &[text("2024-03-15"), text("localtime"), null()]
1832 ));
1833
1834 assert!(is_datetime_invocation_safe_for_schema(
1837 name,
1838 &[text("bogus"), text("localtime")]
1839 ));
1840 assert!(is_datetime_invocation_safe_for_schema(
1841 name,
1842 &[text("2000-01-01"), text("bogus"), text("localtime")]
1843 ));
1844 }
1845 }
1846
1847 #[test]
1848 fn test_schema_safety_strftime_uses_shifted_time_arguments() {
1849 assert!(is_datetime_invocation_safe_for_schema("strftime", &[]));
1850 assert!(is_datetime_invocation_safe_for_schema(
1851 "strftime",
1852 &[null()]
1853 ));
1854 assert!(is_datetime_invocation_safe_for_schema(
1855 "strftime",
1856 &[null(), text("now")]
1857 ));
1858
1859 assert!(!is_datetime_invocation_safe_for_schema(
1861 "strftime",
1862 &[text("%Y")]
1863 ));
1864 assert!(!is_datetime_invocation_safe_for_schema(
1865 "STRFTIME",
1866 &[text("%Y"), text("now")]
1867 ));
1868 assert!(!is_datetime_invocation_safe_for_schema(
1869 "strftime",
1870 &[text("%s"), text("subsecond")]
1871 ));
1872
1873 assert!(is_datetime_invocation_safe_for_schema(
1875 "strftime",
1876 &[text("now localtime utc"), text("2024-03-15")]
1877 ));
1878 assert!(is_datetime_invocation_safe_for_schema(
1879 "strftime",
1880 &[text("%Y"), int(0), text("unixepoch")]
1881 ));
1882 assert!(is_datetime_invocation_safe_for_schema(
1883 "strftime",
1884 &[text("%f"), text("2024-03-15"), text("subsec")]
1885 ));
1886 assert!(!is_datetime_invocation_safe_for_schema(
1887 "strftime",
1888 &[text("%Y"), text("2024-03-15"), text("localtime")]
1889 ));
1890 assert!(is_datetime_invocation_safe_for_schema(
1891 "strftime",
1892 &[text("%Y"), text("2024-03-15"), null(), text("localtime")]
1893 ));
1894 }
1895
1896 #[test]
1897 fn test_schema_safety_timediff_treats_both_inputs_as_time_values() {
1898 assert!(is_datetime_invocation_safe_for_schema("timediff", &[]));
1899 assert!(is_datetime_invocation_safe_for_schema(
1900 "timediff",
1901 &[text("2024-03-15")]
1902 ));
1903 assert!(is_datetime_invocation_safe_for_schema(
1904 "timediff",
1905 &[text("2024-03-15"), text("2024-03-14")]
1906 ));
1907 assert!(is_datetime_invocation_safe_for_schema(
1908 "timediff",
1909 &[int(2_460_384), float(2_460_383.5)]
1910 ));
1911
1912 for current_time in ["now", "subsec", "subsecond"] {
1913 assert!(!is_datetime_invocation_safe_for_schema(
1914 "timediff",
1915 &[text(current_time), text("2024-03-14")]
1916 ));
1917 assert!(!is_datetime_invocation_safe_for_schema(
1918 "timediff",
1919 &[text("2024-03-15"), text(current_time)]
1920 ));
1921 }
1922
1923 assert!(is_datetime_invocation_safe_for_schema(
1926 "timediff",
1927 &[text("localtime"), text("utc")]
1928 ));
1929 assert!(is_datetime_invocation_safe_for_schema(
1930 "timediff",
1931 &[null(), text("now")]
1932 ));
1933 assert!(is_datetime_invocation_safe_for_schema(
1934 "timediff",
1935 &[text("bogus"), text("now")]
1936 ));
1937 assert!(!is_datetime_invocation_safe_for_schema(
1938 "timediff",
1939 &[blob("NOW"), null()]
1940 ));
1941 }
1942
1943 #[test]
1944 fn test_schema_safety_ignores_non_datetime_function_names() {
1945 for name in ["", "my_date", "current_date", "date ", "random"] {
1946 assert!(is_datetime_invocation_safe_for_schema(
1947 name,
1948 &[text("now"), text("localtime")]
1949 ));
1950 }
1951 }
1952
1953 #[test]
1956 fn test_omitted_time_value_uses_current_time() {
1957 let date = DateFunc.invoke(&[]).unwrap();
1958 let time = TimeFunc.invoke(&[]).unwrap();
1959 let datetime = DateTimeFunc.invoke(&[]).unwrap();
1960 let julianday = JuliandayFunc.invoke(&[]).unwrap();
1961 let unixepoch = UnixepochFunc.invoke(&[]).unwrap();
1962 let year = StrftimeFunc.invoke(&[text("%Y")]).unwrap();
1963
1964 assert!(matches!(&date, SqliteValue::Text(value) if value.len() == 10));
1965 assert!(matches!(&time, SqliteValue::Text(value) if value.len() == 8));
1966 assert!(matches!(&datetime, SqliteValue::Text(value) if value.len() == 19));
1967 assert!(matches!(julianday, SqliteValue::Float(_)));
1968 assert!(matches!(unixepoch, SqliteValue::Integer(_)));
1969 assert!(matches!(&year, SqliteValue::Text(value)
1970 if value.len() == 4 && value.as_bytes().iter().all(u8::is_ascii_digit)));
1971
1972 assert_eq!(StrftimeFunc.invoke(&[]).unwrap(), SqliteValue::Null);
1973 assert_eq!(StrftimeFunc.invoke(&[null()]).unwrap(), SqliteValue::Null);
1974 }
1975
1976 #[test]
1977 fn test_first_position_subsec_aliases_use_current_time() {
1978 for alias in ["subsec", "subsecond"] {
1979 assert!(matches!(
1980 DateFunc.invoke(&[text(alias)]).unwrap(),
1981 SqliteValue::Text(value) if value.len() == 10
1982 ));
1983 assert!(matches!(
1984 TimeFunc.invoke(&[text(alias)]).unwrap(),
1985 SqliteValue::Text(value)
1986 if value.len() == 12 && value.as_bytes_direct()[8] == b'.'
1987 ));
1988 assert!(matches!(
1989 DateTimeFunc.invoke(&[text(alias)]).unwrap(),
1990 SqliteValue::Text(value)
1991 if value.len() == 23 && value.as_bytes_direct()[19] == b'.'
1992 ));
1993 assert!(matches!(
1994 JuliandayFunc.invoke(&[text(alias)]).unwrap(),
1995 SqliteValue::Float(_)
1996 ));
1997 assert!(matches!(
1998 UnixepochFunc.invoke(&[text(alias)]).unwrap(),
1999 SqliteValue::Float(_)
2000 ));
2001 assert!(matches!(
2002 StrftimeFunc.invoke(&[text("%s"), text(alias)]).unwrap(),
2003 SqliteValue::Text(value)
2004 if value.rsplit_once('.').is_some_and(|(_, fraction)| fraction.len() == 3)
2005 ));
2006 }
2007 }
2008
2009 #[test]
2010 fn test_padded_and_nul_terminated_special_values() {
2011 for invalid in [" now ", "subsec ", " subsecond"] {
2012 assert_eq!(
2013 DateFunc.invoke(&[text(invalid)]).unwrap(),
2014 SqliteValue::Null
2015 );
2016 }
2017 assert_eq!(
2018 DateFunc
2019 .invoke(&[text("2000-01-01"), text(" localtime ")])
2020 .unwrap(),
2021 SqliteValue::Null
2022 );
2023 assert!(matches!(
2024 DateFunc.invoke(&[text("now\0ignored")]).unwrap(),
2025 SqliteValue::Text(value) if value.len() == 10
2026 ));
2027 assert!(matches!(
2028 TimeFunc
2029 .invoke(&[SqliteValue::Blob(b"subsec\0\xff".as_slice().into())])
2030 .unwrap(),
2031 SqliteValue::Text(value) if value.len() == 12
2032 ));
2033 }
2034
2035 #[test]
2036 fn test_date_basic() {
2037 let r = DateFunc.invoke(&[text("2024-03-15 14:30:00")]).unwrap();
2038 assert_text(&r, "2024-03-15");
2039 }
2040
2041 #[test]
2042 fn test_time_basic() {
2043 let r = TimeFunc.invoke(&[text("2024-03-15 14:30:45")]).unwrap();
2044 assert_text(&r, "14:30:45");
2045 }
2046
2047 #[test]
2048 fn test_datetime_basic() {
2049 let r = DateTimeFunc.invoke(&[text("2024-03-15 14:30:00")]).unwrap();
2050 assert_text(&r, "2024-03-15 14:30:00");
2051 }
2052
2053 #[test]
2054 fn test_julianday_basic() {
2055 let r = JuliandayFunc.invoke(&[text("2024-03-15")]).unwrap();
2056 match r {
2057 SqliteValue::Float(jdn) => {
2058 assert!((jdn - 2_460_384.5).abs() < 0.01, "unexpected JDN: {jdn}");
2060 }
2061 other => panic!("expected Float, got {other:?}"),
2062 }
2063 }
2064
2065 fn julianday_float(input: &str) -> f64 {
2073 match JuliandayFunc.invoke(&[text(input)]).unwrap() {
2074 SqliteValue::Float(v) => v,
2075 other => panic!("expected Float, got {other:?} for input {input:?}"),
2076 }
2077 }
2078
2079 fn assert_jdn_close(actual: f64, expected: f64, ctx: &str) {
2080 assert!(
2082 (actual - expected).abs() < 1e-6,
2083 "JDN mismatch for {ctx}: got {actual}, expected {expected}"
2084 );
2085 }
2086
2087 #[test]
2088 fn test_julianday_rfc3339_z_suffix() {
2089 let naive = julianday_float("2026-04-07 16:00:00");
2091 assert_jdn_close(julianday_float("2026-04-07T16:00:00Z"), naive, "T...Z");
2092 assert_jdn_close(
2093 julianday_float("2026-04-07T16:00:00z"),
2094 naive,
2095 "lowercase z",
2096 );
2097 }
2098
2099 #[test]
2100 fn test_julianday_rfc3339_zero_offset() {
2101 let naive = julianday_float("2026-04-07 16:00:00");
2102 assert_jdn_close(
2103 julianday_float("2026-04-07T16:00:00+00:00"),
2104 naive,
2105 "+00:00",
2106 );
2107 assert_jdn_close(
2108 julianday_float("2026-04-07T16:00:00-00:00"),
2109 naive,
2110 "-00:00",
2111 );
2112 }
2113
2114 #[test]
2115 fn test_julianday_rfc3339_positive_offset() {
2116 let base = julianday_float("2026-04-07 16:00:00");
2118 let expected = base - 1.0 / 24.0;
2119 assert_jdn_close(
2120 julianday_float("2026-04-07T16:00:00+01:00"),
2121 expected,
2122 "+01:00",
2123 );
2124 }
2125
2126 #[test]
2127 fn test_julianday_rfc3339_negative_offset() {
2128 let base = julianday_float("2026-04-07 16:00:00");
2130 let expected = base + 5.0 / 24.0;
2131 assert_jdn_close(
2132 julianday_float("2026-04-07T16:00:00-05:00"),
2133 expected,
2134 "-05:00",
2135 );
2136 }
2137
2138 #[test]
2139 fn test_julianday_rfc3339_half_hour_offset() {
2140 let base = julianday_float("2026-04-07 16:00:00");
2142 let expected = base - 5.5 / 24.0;
2143 assert_jdn_close(
2144 julianday_float("2026-04-07T16:00:00+05:30"),
2145 expected,
2146 "+05:30",
2147 );
2148 }
2149
2150 #[test]
2151 fn test_julianday_rfc3339_compact_offsets() {
2152 let base = julianday_float("2026-04-07 16:00:00");
2154 assert_jdn_close(
2155 julianday_float("2026-04-07T16:00:00+0100"),
2156 base - 1.0 / 24.0,
2157 "+0100",
2158 );
2159 assert_jdn_close(
2160 julianday_float("2026-04-07T16:00:00-0530"),
2161 base + 5.5 / 24.0,
2162 "-0530",
2163 );
2164 assert_jdn_close(
2165 julianday_float("2026-04-07T16:00:00+09"),
2166 base - 9.0 / 24.0,
2167 "+09",
2168 );
2169 }
2170
2171 #[test]
2172 fn test_julianday_rfc3339_fractional_seconds_with_tz() {
2173 let base = julianday_float("2026-04-07 16:00:00.500");
2175 assert_jdn_close(
2176 julianday_float("2026-04-07T16:00:00.500Z"),
2177 base,
2178 "fractional + Z",
2179 );
2180 assert_jdn_close(
2181 julianday_float("2026-04-07T16:00:00.500+01:00"),
2182 base - 1.0 / 24.0,
2183 "fractional + +01:00",
2184 );
2185 }
2186
2187 #[test]
2188 fn test_date_and_time_rfc3339_round_trip() {
2189 assert_text(
2192 &DateFunc
2193 .invoke(&[text("2026-04-07T16:00:00+05:00")])
2194 .unwrap(),
2195 "2026-04-07",
2197 );
2198 assert_text(
2199 &TimeFunc
2200 .invoke(&[text("2026-04-07T16:00:00+05:00")])
2201 .unwrap(),
2202 "11:00:00",
2203 );
2204 assert_text(
2205 &DateTimeFunc
2206 .invoke(&[text("2026-04-07T16:00:00+05:00")])
2207 .unwrap(),
2208 "2026-04-07 11:00:00",
2209 );
2210 }
2211
2212 #[test]
2213 fn test_julianday_rfc3339_invalid_offsets_return_null() {
2214 for bad in &[
2216 "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", ] {
2221 let result = JuliandayFunc.invoke(&[text(bad)]).unwrap();
2222 assert_eq!(
2223 result,
2224 SqliteValue::Null,
2225 "expected NULL for malformed offset {bad:?}, got {result:?}"
2226 );
2227 }
2228 }
2229
2230 #[test]
2231 fn test_julianday_rejects_malformed_time_fields() {
2232 for bad in &[
2236 "+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", ] {
2248 let result = JuliandayFunc.invoke(&[text(bad)]).unwrap();
2249 assert_eq!(
2250 result,
2251 SqliteValue::Null,
2252 "expected NULL for signed time field {bad:?}, got {result:?}"
2253 );
2254 }
2255 }
2256
2257 #[test]
2258 fn test_unixepoch_basic() {
2259 let r = UnixepochFunc
2260 .invoke(&[text("1970-01-01 00:00:00")])
2261 .unwrap();
2262 assert_eq!(r, int(0));
2263 }
2264
2265 #[test]
2266 fn test_unixepoch_known_date() {
2267 let r = UnixepochFunc
2268 .invoke(&[text("2024-01-01 00:00:00")])
2269 .unwrap();
2270 assert_eq!(r, int(1_704_067_200));
2272 }
2273
2274 #[test]
2277 fn test_modifier_days() {
2278 let r = DateFunc
2279 .invoke(&[text("2024-01-15"), text("+10 days")])
2280 .unwrap();
2281 assert_text(&r, "2024-01-25");
2282 }
2283
2284 #[test]
2285 fn test_modifier_months() {
2286 let r = DateFunc
2289 .invoke(&[text("2024-01-31"), text("+1 months")])
2290 .unwrap();
2291 assert_text(&r, "2024-03-02");
2292 }
2293
2294 #[test]
2295 fn test_modifier_years() {
2296 let r = DateFunc
2299 .invoke(&[text("2024-02-29"), text("+1 years")])
2300 .unwrap();
2301 assert_text(&r, "2025-03-01");
2302 }
2303
2304 #[test]
2305 fn test_modifier_hours() {
2306 let r = DateTimeFunc
2307 .invoke(&[text("2024-01-01 23:00:00"), text("+2 hours")])
2308 .unwrap();
2309 assert_text(&r, "2024-01-02 01:00:00");
2310 }
2311
2312 #[test]
2313 fn test_modifier_unsigned_is_positive_bd_t8g1e() {
2314 assert_text(
2320 &DateFunc
2321 .invoke(&[text("2024-01-15"), text("10 days")])
2322 .unwrap(),
2323 "2024-01-25",
2324 );
2325 assert_text(
2326 &DateTimeFunc
2327 .invoke(&[text("2024-01-01"), text("5 hours")])
2328 .unwrap(),
2329 "2024-01-01 05:00:00",
2330 );
2331 assert_text(
2332 &DateTimeFunc
2333 .invoke(&[text("2024-01-01"), text("90 minutes")])
2334 .unwrap(),
2335 "2024-01-01 01:30:00",
2336 );
2337 assert_text(
2338 &DateTimeFunc
2339 .invoke(&[text("2024-01-01"), text("86400 seconds")])
2340 .unwrap(),
2341 "2024-01-02 00:00:00",
2342 );
2343 assert_text(
2346 &DateFunc
2347 .invoke(&[text("2024-01-01"), text("2 months")])
2348 .unwrap(),
2349 "2024-03-01",
2350 );
2351 assert_text(
2352 &DateFunc
2353 .invoke(&[text("2024-01-01"), text("1 year")])
2354 .unwrap(),
2355 "2025-01-01",
2356 );
2357 assert_text(
2359 &DateTimeFunc
2360 .invoke(&[text("2024-01-01 12:00"), text("1.5 hours")])
2361 .unwrap(),
2362 "2024-01-01 13:30:00",
2363 );
2364 assert_text(
2368 &DateFunc
2369 .invoke(&[text("2024-03-15"), text("start of month")])
2370 .unwrap(),
2371 "2024-03-01",
2372 );
2373 assert_text(
2374 &DateFunc
2375 .invoke(&[text("2024-06-15"), text("start of year")])
2376 .unwrap(),
2377 "2024-01-01",
2378 );
2379 assert_text(
2381 &DateFunc
2382 .invoke(&[text("2024-01-15"), text("-10 days")])
2383 .unwrap(),
2384 "2024-01-05",
2385 );
2386 }
2387
2388 #[test]
2389 fn test_modifier_start_of_month() {
2390 let r = DateFunc
2391 .invoke(&[text("2024-03-15"), text("start of month")])
2392 .unwrap();
2393 assert_text(&r, "2024-03-01");
2394 }
2395
2396 #[test]
2397 fn test_modifier_start_of_year() {
2398 let r = DateFunc
2399 .invoke(&[text("2024-06-15"), text("start of year")])
2400 .unwrap();
2401 assert_text(&r, "2024-01-01");
2402 }
2403
2404 #[test]
2405 fn test_modifier_start_of_day() {
2406 let r = DateTimeFunc
2407 .invoke(&[text("2024-03-15 14:30:00"), text("start of day")])
2408 .unwrap();
2409 assert_text(&r, "2024-03-15 00:00:00");
2410 }
2411
2412 #[test]
2413 fn test_modifier_unixepoch() {
2414 let r = DateTimeFunc.invoke(&[int(0), text("unixepoch")]).unwrap();
2415 assert_text(&r, "1970-01-01 00:00:00");
2416 }
2417
2418 #[test]
2419 fn test_modifier_weekday() {
2420 let r = DateFunc
2422 .invoke(&[text("2024-03-15"), text("weekday 0")])
2423 .unwrap();
2424 assert_text(&r, "2024-03-17");
2425 }
2426
2427 #[test]
2428 fn test_modifier_auto_unixepoch() {
2429 let ts = int(1_710_531_045);
2430 let r = DateTimeFunc.invoke(&[ts.clone(), text("auto")]).unwrap();
2431 let expected = DateTimeFunc.invoke(&[ts, text("unixepoch")]).unwrap();
2432 assert_eq!(
2433 r, expected,
2434 "auto and unixepoch should agree for unix-like values"
2435 );
2436 }
2437
2438 #[test]
2439 fn test_modifier_auto_julian_day() {
2440 let r = DateFunc
2441 .invoke(&[float(2_460_384.5), text("auto")])
2442 .unwrap();
2443 assert_text(&r, "2024-03-15");
2444 }
2445
2446 #[test]
2447 fn test_modifier_localtime_utc_roundtrip() {
2448 let r = DateTimeFunc
2450 .invoke(&[text("2024-03-15 14:30:45"), text("localtime"), text("utc")])
2451 .unwrap();
2452 assert_text(&r, "2024-03-15 14:30:45");
2453 }
2454
2455 #[test]
2456 fn test_modifier_localtime_shifts_value() {
2457 let offset = utc_offset_for_utc_jdn(ymdhms_to_jdn(2024, 3, 15, 12, 0, 0, 0.0));
2459 if offset != 0 {
2460 let r = DateTimeFunc
2461 .invoke(&[text("2024-03-15 12:00:00"), text("localtime")])
2462 .unwrap();
2463 let shifted = match &r {
2465 SqliteValue::Text(s) => s.clone(),
2466 _ => panic!("expected text"),
2467 };
2468 assert_ne!(&*shifted, "2024-03-15 12:00:00");
2469 }
2470 }
2471
2472 #[test]
2473 fn test_modifier_auto_out_of_range_returns_null() {
2474 let r = DateTimeFunc.invoke(&[float(1.0e20), text("auto")]).unwrap();
2475 assert_eq!(r, SqliteValue::Null);
2476 }
2477
2478 #[test]
2479 fn test_modifier_order_matters() {
2480 let r1 = DateFunc
2482 .invoke(&[text("2024-03-15"), text("start of month"), text("+1 days")])
2483 .unwrap();
2484 assert_text(&r1, "2024-03-02");
2485
2486 let r2 = DateFunc
2488 .invoke(&[text("2024-03-15"), text("+1 days"), text("start of month")])
2489 .unwrap();
2490 assert_text(&r2, "2024-03-01");
2491 }
2492
2493 #[test]
2494 fn test_modifier_weekday_same_day_is_noop() {
2495 let r = DateFunc
2497 .invoke(&[text("2024-03-17"), text("weekday 0")])
2498 .unwrap();
2499 assert_text(&r, "2024-03-17");
2500 }
2501
2502 #[test]
2505 fn test_bare_time_defaults() {
2506 let r = DateFunc.invoke(&[text("12:30:00")]).unwrap();
2507 assert_text(&r, "2000-01-01");
2508 }
2509
2510 #[test]
2511 fn test_t_separator() {
2512 let r = DateTimeFunc.invoke(&[text("2024-03-15T14:30:00")]).unwrap();
2513 assert_text(&r, "2024-03-15 14:30:00");
2514 }
2515
2516 #[test]
2517 fn test_julian_day_input() {
2518 let r = DateFunc.invoke(&[float(2_460_384.5)]).unwrap();
2520 assert_text(&r, "2024-03-15");
2521 }
2522
2523 #[test]
2524 fn test_null_input() {
2525 assert_eq!(DateFunc.invoke(&[null()]).unwrap(), SqliteValue::Null);
2526 }
2527
2528 #[test]
2529 fn test_invalid_input() {
2530 assert_eq!(
2531 DateFunc.invoke(&[text("not-a-date")]).unwrap(),
2532 SqliteValue::Null
2533 );
2534 }
2535
2536 #[test]
2537 fn test_negative_time_component_invalid() {
2538 let r = TimeFunc.invoke(&[text("-01:00")]).unwrap();
2539 assert_eq!(r, SqliteValue::Null);
2540 }
2541
2542 #[test]
2545 fn test_leap_year() {
2546 let r = DateFunc
2547 .invoke(&[text("2024-02-28"), text("+1 days")])
2548 .unwrap();
2549 assert_text(&r, "2024-02-29");
2550 }
2551
2552 #[test]
2553 fn test_non_leap_year() {
2554 let r = DateFunc
2555 .invoke(&[text("2023-02-28"), text("+1 days")])
2556 .unwrap();
2557 assert_text(&r, "2023-03-01");
2558 }
2559
2560 #[test]
2563 fn test_strftime_basic() {
2564 let r = StrftimeFunc
2565 .invoke(&[text("%Y-%m-%d"), text("2024-03-15")])
2566 .unwrap();
2567 assert_text(&r, "2024-03-15");
2568 }
2569
2570 #[test]
2571 fn test_strftime_time_specifiers() {
2572 let r = StrftimeFunc
2573 .invoke(&[text("%H:%M:%S"), text("2024-03-15 14:30:45")])
2574 .unwrap();
2575 assert_text(&r, "14:30:45");
2576 }
2577
2578 #[test]
2579 fn test_strftime_unix_seconds() {
2580 let r = StrftimeFunc
2581 .invoke(&[text("%s"), text("1970-01-01 00:00:00")])
2582 .unwrap();
2583 assert_text(&r, "0");
2584 }
2585
2586 #[test]
2587 fn test_strftime_day_of_year() {
2588 let r = StrftimeFunc
2589 .invoke(&[text("%j"), text("2024-03-15")])
2590 .unwrap();
2591 assert_text(&r, "075");
2593 }
2594
2595 #[test]
2596 fn test_strftime_day_of_week() {
2597 let r = StrftimeFunc
2599 .invoke(&[text("%w"), text("2024-03-15")])
2600 .unwrap();
2601 assert_text(&r, "5");
2602
2603 let r = StrftimeFunc
2604 .invoke(&[text("%u"), text("2024-03-15")])
2605 .unwrap();
2606 assert_text(&r, "5");
2607 }
2608
2609 #[test]
2610 fn test_strftime_12hour() {
2611 let r = StrftimeFunc
2612 .invoke(&[text("%I %p"), text("2024-03-15 14:30:00")])
2613 .unwrap();
2614 assert_text(&r, "02 PM");
2615
2616 let r = StrftimeFunc
2617 .invoke(&[text("%I %P"), text("2024-03-15 09:30:00")])
2618 .unwrap();
2619 assert_text(&r, "09 am");
2620 }
2621
2622 #[test]
2623 fn test_strftime_all_specifiers_presence() {
2624 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|%%";
2625 let r = StrftimeFunc
2626 .invoke(&[text(fmt), text("2024-03-15 14:30:45.123")])
2627 .unwrap();
2628
2629 let s = match r {
2630 SqliteValue::Text(v) => v,
2631 other => panic!("expected Text, got {other:?}"),
2632 };
2633 let parts: Vec<&str> = s.split('|').collect();
2634 assert_eq!(parts.len(), 25, "unexpected specifier output: {s}");
2635 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!(
2642 parts[6].parse::<f64>().is_ok(),
2643 "expected numeric %J output, got {}",
2644 parts[6]
2645 );
2646 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!(
2654 parts[14].parse::<i64>().is_ok(),
2655 "expected numeric %s output, got {}",
2656 parts[14]
2657 );
2658 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], "%"); }
2669
2670 #[test]
2671 fn test_strftime_null() {
2672 assert_eq!(
2673 StrftimeFunc.invoke(&[null(), text("2024-01-01")]).unwrap(),
2674 SqliteValue::Null
2675 );
2676 assert_eq!(
2677 StrftimeFunc.invoke(&[text("%Y"), null()]).unwrap(),
2678 SqliteValue::Null
2679 );
2680 }
2681
2682 #[test]
2683 #[ignore = "perf-only benchmark"]
2684 fn perf_strftime_timestamp_rows() {
2685 use std::hint::black_box;
2686 use std::time::Instant;
2687
2688 const ROWS: usize = 200_000;
2689 const REPEATS: usize = 5;
2690 const FORMAT: &str = "%Y-%m-%d %H:%M:%S";
2691 const INPUT: &str = "2024-03-15 14:30:45";
2692
2693 let func = StrftimeFunc;
2694 let fmt = text(FORMAT);
2695 let input = text(INPUT);
2696 let mut best_ns = u128::MAX;
2697 let mut output_len = 0usize;
2698
2699 for _ in 0..REPEATS {
2700 let started = Instant::now();
2701 for _ in 0..ROWS {
2702 let result = black_box(
2703 func.invoke(black_box(&[fmt.clone(), input.clone()]))
2704 .expect("strftime benchmark invocation must succeed"),
2705 );
2706 output_len = match result {
2707 SqliteValue::Text(text) => text.len(),
2708 SqliteValue::Null
2709 | SqliteValue::Integer(_)
2710 | SqliteValue::Float(_)
2711 | SqliteValue::Blob(_) => 0,
2712 };
2713 }
2714 let elapsed_ns = started.elapsed().as_nanos();
2715 if elapsed_ns < best_ns {
2716 best_ns = elapsed_ns;
2717 }
2718 }
2719
2720 println!(
2721 "strftime_timestamp_rows rows={ROWS} repeats={REPEATS} best_ns={best_ns} output_len={output_len}"
2722 );
2723 }
2724
2725 #[test]
2728 fn test_timediff_basic() {
2729 let r = TimediffFunc
2730 .invoke(&[text("2024-03-15"), text("2024-03-10")])
2731 .unwrap();
2732 assert_text(&r, "+0000-00-05 00:00:00.000");
2733 }
2734
2735 #[test]
2736 fn test_timediff_negative() {
2737 let r = TimediffFunc
2738 .invoke(&[text("2024-03-10"), text("2024-03-15")])
2739 .unwrap();
2740 assert_text(&r, "-0000-00-05 00:00:00.000");
2741 }
2742
2743 #[test]
2744 fn test_timediff_year_boundary() {
2745 let r = TimediffFunc
2746 .invoke(&[text("2024-01-01 01:00:00"), text("2023-12-31 23:00:00")])
2747 .unwrap();
2748 assert_text(&r, "+0000-00-00 02:00:00.000");
2749 }
2750
2751 #[test]
2754 fn test_modifier_subsec() {
2755 assert_text(
2756 &TimeFunc
2757 .invoke(&[text("2024-01-01 12:00:00"), text("subsec")])
2758 .unwrap(),
2759 "12:00:00.000",
2760 );
2761 assert_text(
2762 &DateTimeFunc
2763 .invoke(&[text("2024-01-01 12:00:00"), text("subsecond")])
2764 .unwrap(),
2765 "2024-01-01 12:00:00.000",
2766 );
2767 assert_text(
2768 &TimeFunc
2769 .invoke(&[text("2024-01-01 12:00:00.123"), text("subsec")])
2770 .unwrap(),
2771 "12:00:00.123",
2772 );
2773 }
2774
2775 #[test]
2776 fn test_subsec_unix_seconds_and_integer_flooring() {
2777 assert_text(
2778 &StrftimeFunc
2779 .invoke(&[text("%s"), text("1970-01-01 00:00:00.125"), text("subsec")])
2780 .unwrap(),
2781 "0.125",
2782 );
2783 assert_eq!(
2784 UnixepochFunc
2785 .invoke(&[text("1970-01-01 00:00:00.125"), text("subsec")])
2786 .unwrap(),
2787 float(0.125)
2788 );
2789
2790 for input in ["1970-01-01 00:00:00.500", "1970-01-01 00:00:00.999"] {
2791 assert_eq!(UnixepochFunc.invoke(&[text(input)]).unwrap(), int(0));
2792 assert_text(
2793 &StrftimeFunc.invoke(&[text("%s"), text(input)]).unwrap(),
2794 "0",
2795 );
2796 }
2797 assert_eq!(
2798 UnixepochFunc
2799 .invoke(&[text("1969-12-31 23:59:59.999")])
2800 .unwrap(),
2801 int(-1)
2802 );
2803 assert_text(
2804 &StrftimeFunc
2805 .invoke(&[text("%s"), text("1969-12-31 23:59:59.999")])
2806 .unwrap(),
2807 "-1",
2808 );
2809 }
2810
2811 #[test]
2812 fn test_subsec_rounding_preserves_sqlite_second_60() {
2813 assert_text(
2814 &TimeFunc
2815 .invoke(&[text("12:34:59.9995"), text("subsec")])
2816 .unwrap(),
2817 "12:34:60.000",
2818 );
2819 assert_text(
2820 &DateTimeFunc
2821 .invoke(&[text("1970-01-01 23:59:59.9995"), text("subsec")])
2822 .unwrap(),
2823 "1970-01-01 23:59:60.000",
2824 );
2825 assert_text(
2826 &StrftimeFunc
2827 .invoke(&[
2828 text("%H:%M:%f|%s"),
2829 text("1970-01-01 23:59:59.9995"),
2830 text("subsec"),
2831 ])
2832 .unwrap(),
2833 "23:59:59.999|86400.000",
2834 );
2835 }
2836
2837 #[test]
2840 fn test_register_datetime_builtins_all_present() {
2841 let mut reg = FunctionRegistry::new();
2842 register_datetime_builtins(&mut reg);
2843
2844 let expected = [
2845 "date",
2846 "time",
2847 "datetime",
2848 "julianday",
2849 "unixepoch",
2850 "strftime",
2851 "timediff",
2852 ];
2853
2854 for name in expected {
2855 assert!(
2856 reg.find_scalar(name, 1).is_some() || reg.find_scalar(name, 2).is_some(),
2857 "datetime function '{name}' not registered"
2858 );
2859 }
2860 }
2861
2862 #[test]
2863 fn test_public_datetime_function_metadata_fails_closed_for_direct_registration() {
2864 assert!(!DateFunc.is_deterministic());
2865 assert!(!TimeFunc.is_deterministic());
2866 assert!(!DateTimeFunc.is_deterministic());
2867 assert!(!JuliandayFunc.is_deterministic());
2868 assert!(!UnixepochFunc.is_deterministic());
2869 assert!(!StrftimeFunc.is_deterministic());
2870 assert!(!TimediffFunc.is_deterministic());
2871
2872 let mut registry = FunctionRegistry::new();
2873 registry.register_scalar(DateFunc);
2874 registry.register_scalar(TimeFunc);
2875 registry.register_scalar(DateTimeFunc);
2876 registry.register_scalar(JuliandayFunc);
2877 registry.register_scalar(UnixepochFunc);
2878 registry.register_scalar(StrftimeFunc);
2879 registry.register_scalar(TimediffFunc);
2880 for (name, num_args) in [
2881 ("date", 0),
2882 ("time", 0),
2883 ("datetime", 0),
2884 ("julianday", 0),
2885 ("unixepoch", 0),
2886 ("strftime", 1),
2887 ("timediff", 2),
2888 ] {
2889 assert_eq!(
2890 registry.scalar_schema_safety(name, num_args),
2891 Some(crate::ScalarSchemaSafety::Never),
2892 "generic registration of {name}/{num_args} must fail closed"
2893 );
2894 }
2895 }
2896
2897 #[test]
2900 fn test_modifier_year_overflow() {
2901 let huge = i64::MAX;
2904 let modifier = format!("+{huge} years");
2905 let r = DateFunc.invoke(&[text("2000-01-01"), text(&modifier)]);
2906 assert_eq!(r.unwrap(), SqliteValue::Null);
2909 }
2910
2911 #[test]
2912 fn test_jdn_roundtrip() {
2913 let dates = [
2915 (2024, 3, 15),
2916 (2000, 1, 1),
2917 (1970, 1, 1),
2918 (2024, 2, 29),
2919 (1900, 1, 1),
2920 (2099, 12, 31),
2921 ];
2922 for (y, m, d) in dates {
2923 let jdn = ymd_to_jdn(y, m, d);
2924 let (y2, m2, d2) = jdn_to_ymd(jdn);
2925 assert_eq!(
2926 (y, m, d),
2927 (y2, m2, d2),
2928 "roundtrip failed for {y}-{m}-{d} (JDN={jdn})"
2929 );
2930 }
2931 }
2932
2933 #[test]
2934 fn test_unix_epoch_roundtrip() {
2935 let jdn = ymd_to_jdn(1970, 1, 1);
2936 let unix = jdn_to_unix(jdn);
2937 assert_eq!(unix, 0, "Unix epoch should be 0");
2938
2939 let jdn2 = unix_to_jdn(0.0);
2940 assert!((jdn2 - UNIX_EPOCH_JDN).abs() < 1e-10, "roundtrip failed");
2941 }
2942}