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> {
592 let (sign, rest) = if let Some(r) = m.strip_prefix('+') {
593 (1.0, r.trim())
594 } else {
595 let r = m.strip_prefix('-')?;
596 (-1.0, r.trim())
597 };
598
599 let mut parts = rest.splitn(2, ' ');
600 let num_str = parts.next()?;
601 let unit = parts.next()?.trim();
602
603 let num = num_str.parse::<f64>().ok().filter(|f| f.is_finite())?;
605 let delta = num * sign;
606
607 match unit.trim_end_matches('s') {
608 "day" => Some(delta),
609 "hour" => Some(delta / 24.0),
610 "minute" => Some(delta / 1440.0),
611 "second" => Some(delta / 86400.0),
612 "month" => Some(apply_month_delta(delta)),
613 "year" => Some(apply_month_delta(delta * 12.0)),
614 _ => None,
615 }
616}
617
618fn apply_month_delta(months: f64) -> f64 {
623 months * 30.436875
625}
626
627fn has_outer_ascii_whitespace(value: &str) -> bool {
628 value
629 .as_bytes()
630 .first()
631 .is_some_and(u8::is_ascii_whitespace)
632 || value.as_bytes().last().is_some_and(u8::is_ascii_whitespace)
633}
634
635fn compute_floor(y: i64, m: i64, d: i64) -> i64 {
639 if d <= 28 {
640 0
641 } else if ((1_i64 << m) & 0x15aa) != 0 {
642 0
644 } else if m != 2 {
645 i64::from(d == 31)
647 } else if y % 4 != 0 || (y % 100 == 0 && y % 400 != 0) {
648 d - 28 } else {
650 d - 29 }
652}
653
654fn apply_modifiers(jdn: f64, modifiers: &[String], mut raw_numeric: bool) -> Option<(f64, bool)> {
656 let mut j = jdn;
657 let mut subsec = false;
658 let mut n_floor: i64 = 0;
661 for (index, m) in modifiers.iter().enumerate() {
662 if has_outer_ascii_whitespace(m) {
663 return None;
664 }
665 let m_lower = m.to_ascii_lowercase();
666 if matches!(m_lower.as_str(), "unixepoch" | "julianday" | "auto") {
667 if index != 0 || !raw_numeric {
671 return None;
672 }
673 raw_numeric = false;
674 } else if m_lower != "subsec" && m_lower != "subsecond" {
675 raw_numeric = false;
676 }
677 if m_lower == "subsec" || m_lower == "subsecond" {
678 subsec = true;
679 continue;
680 }
681 if m_lower == "ceiling" {
686 n_floor = 0;
687 continue;
688 }
689 if m_lower == "floor" {
690 j -= n_floor as f64;
691 n_floor = 0;
692 continue;
693 }
694 if is_month_year_modifier(&m_lower) {
700 match apply_month_year_exact(j, &m_lower) {
701 Ok(Some((new_jdn, nf))) => {
702 j = new_jdn;
703 n_floor = nf;
704 continue;
705 }
706 Ok(None) => return None,
707 Err(()) => {
708 }
710 }
711 }
712 n_floor = 0;
714 j = apply_modifier(j, m)?;
715 }
716 Some((j, subsec))
717}
718
719fn is_month_year_modifier(m: &str) -> bool {
720 (m.contains("month") || m.contains("year")) && (m.starts_with('+') || m.starts_with('-'))
721}
722
723fn apply_month_year_exact(jdn: f64, m: &str) -> std::result::Result<Option<(f64, i64)>, ()> {
729 let (sign, rest) = if let Some(r) = m.strip_prefix('+') {
730 (1_i64, r.trim())
731 } else if let Some(r) = m.strip_prefix('-') {
732 (-1_i64, r.trim())
733 } else {
734 return Err(());
735 };
736
737 let mut parts = rest.splitn(2, ' ');
738 let num_str = parts.next().ok_or(())?;
739 let unit = parts.next().ok_or(())?.trim();
740
741 let num = if let Ok(n) = num_str.parse::<i64>() {
743 n
744 } else if let Ok(f) = num_str.parse::<f64>() {
745 if f.fract() == 0.0 && f >= i64::MIN as f64 && f <= i64::MAX as f64 {
746 f as i64
747 } else {
748 return Err(());
749 }
750 } else {
751 return Err(());
752 };
753
754 let (y, mo, d) = jdn_to_ymd(jdn);
755 let (h, mi, s, frac) = jdn_to_hms(jdn);
756
757 let total_months = match unit.trim_end_matches('s') {
758 "month" => {
759 if let Some(val) = num.checked_mul(sign) {
760 val
761 } else {
762 return Ok(None);
763 }
764 }
765 "year" => {
766 if let Some(val) = num.checked_mul(sign).and_then(|v| v.checked_mul(12)) {
767 val
768 } else {
769 return Ok(None);
770 }
771 }
772 _ => return Err(()),
773 };
774
775 let current_months = if let Some(val) = y.checked_mul(12).and_then(|v| v.checked_add(mo - 1)) {
777 val
778 } else {
779 return Ok(None);
780 };
781 let new_total = if let Some(val) = current_months.checked_add(total_months) {
782 val
783 } else {
784 return Ok(None);
785 };
786
787 let new_y = new_total.div_euclid(12);
788 let new_mo = new_total.rem_euclid(12) + 1;
789 let n_floor = compute_floor(new_y, new_mo, d);
794 Ok(Some((
795 ymdhms_to_jdn(new_y, new_mo, d, h, mi, s, frac),
796 n_floor,
797 )))
798}
799
800struct StackStr {
806 buf: [u8; 48],
807 len: usize,
808}
809
810impl StackStr {
811 fn new() -> Self {
812 Self {
813 buf: [0; 48],
814 len: 0,
815 }
816 }
817
818 fn as_str(&self) -> &str {
819 core::str::from_utf8(&self.buf[..self.len]).unwrap_or("")
821 }
822}
823
824impl core::fmt::Write for StackStr {
825 fn write_str(&mut self, s: &str) -> core::fmt::Result {
826 let end = self.len + s.len();
827 if end > self.buf.len() {
828 return Err(core::fmt::Error);
829 }
830 self.buf[self.len..end].copy_from_slice(s.as_bytes());
831 self.len = end;
832 Ok(())
833 }
834}
835
836fn build_small_text(write: impl Fn(&mut dyn core::fmt::Write) -> core::fmt::Result) -> SmallText {
843 let mut buf = StackStr::new();
844 if write(&mut buf).is_ok() {
845 SmallText::new(buf.as_str())
846 } else {
847 let mut heap = String::new();
848 let _ = write(&mut heap);
849 SmallText::from_string(heap)
850 }
851}
852
853fn format_date(jdn: f64) -> SmallText {
854 let (y, m, d) = jdn_to_ymd(jdn);
855 build_small_text(move |w| write!(w, "{y:04}-{m:02}-{d:02}"))
856}
857
858#[derive(Clone, Copy)]
859struct UnmodifiedHms {
860 hour: i64,
861 minute: i64,
862 second: i64,
863 fraction: f64,
864}
865
866fn hms_for_output(jdn: f64, unmodified: Option<UnmodifiedHms>) -> UnmodifiedHms {
867 unmodified.unwrap_or_else(|| {
868 let (hour, minute, second, fraction) = jdn_to_hms(jdn);
869 UnmodifiedHms {
870 hour,
871 minute,
872 second,
873 fraction,
874 }
875 })
876}
877
878fn rounded_second_and_millis(hms: UnmodifiedHms) -> (i64, i64) {
879 let total_millis = ((hms.second as f64 + hms.fraction) * 1000.0 + 0.5).floor() as i64;
883 (total_millis / 1000, total_millis % 1000)
884}
885
886fn format_time(jdn: f64, subsec: bool, unmodified: Option<UnmodifiedHms>) -> SmallText {
887 let hms = hms_for_output(jdn, unmodified);
888 let (h, m) = (hms.hour, hms.minute);
889 if subsec {
890 let (s, ms) = rounded_second_and_millis(hms);
891 build_small_text(move |w| write!(w, "{h:02}:{m:02}:{s:02}.{ms:03}"))
892 } else {
893 let s = hms.second;
894 build_small_text(move |w| write!(w, "{h:02}:{m:02}:{s:02}"))
895 }
896}
897
898fn format_datetime(jdn: f64, subsec: bool, unmodified: Option<UnmodifiedHms>) -> SmallText {
899 let (y, mo, d) = jdn_to_ymd(jdn);
900 let hms = hms_for_output(jdn, unmodified);
901 let (h, mi) = (hms.hour, hms.minute);
902 if subsec {
903 let (s, ms) = rounded_second_and_millis(hms);
904 build_small_text(move |w| write!(w, "{y:04}-{mo:02}-{d:02} {h:02}:{mi:02}:{s:02}.{ms:03}"))
905 } else {
906 let s = hms.second;
907 build_small_text(move |w| write!(w, "{y:04}-{mo:02}-{d:02} {h:02}:{mi:02}:{s:02}"))
908 }
909}
910
911#[inline]
912fn push_format(result: &mut String, args: Arguments<'_>) {
913 let _ = result.write_fmt(args);
914}
915
916#[inline]
917fn push_zero_padded_2(result: &mut String, value: i64) {
918 if (0..=99).contains(&value) {
919 let value = value as u8;
920 result.push(char::from(b'0' + value / 10));
921 result.push(char::from(b'0' + value % 10));
922 } else {
923 push_format(result, format_args!("{value:02}"));
924 }
925}
926
927#[inline]
928fn push_space_padded_2(result: &mut String, value: i64) {
929 if (0..=99).contains(&value) {
930 let value = value as u8;
931 if value >= 10 {
932 result.push(char::from(b'0' + value / 10));
933 } else {
934 result.push(' ');
935 }
936 result.push(char::from(b'0' + value % 10));
937 } else {
938 push_format(result, format_args!("{value:>2}"));
939 }
940}
941
942#[inline]
943fn push_zero_padded_3(result: &mut String, value: i64) {
944 if (0..=999).contains(&value) {
945 let value = value as u16;
946 result.push(char::from(b'0' + (value / 100) as u8));
947 result.push(char::from(b'0' + ((value / 10) % 10) as u8));
948 result.push(char::from(b'0' + (value % 10) as u8));
949 } else {
950 push_format(result, format_args!("{value:03}"));
951 }
952}
953
954#[inline]
955fn push_zero_padded_4(result: &mut String, value: i64) {
956 if (0..=9999).contains(&value) {
957 let value = value as u16;
958 result.push(char::from(b'0' + (value / 1000) as u8));
959 result.push(char::from(b'0' + ((value / 100) % 10) as u8));
960 result.push(char::from(b'0' + ((value / 10) % 10) as u8));
961 result.push(char::from(b'0' + (value % 10) as u8));
962 } else {
963 push_format(result, format_args!("{value:04}"));
964 }
965}
966
967fn format_strftime(fmt: &str, jdn: f64, subsec: bool, unmodified: Option<UnmodifiedHms>) -> String {
969 let (y, mo, d) = jdn_to_ymd(jdn);
970 let hms = hms_for_output(jdn, unmodified);
971 let (h, mi, s, frac) = (hms.hour, hms.minute, hms.second, hms.fraction);
972 let doy = day_of_year(y, mo, d);
973 let jdn_int = (jdn + 0.5).floor() as i64;
975 let dow = (jdn_int + 1) % 7; let mut result = String::with_capacity(fmt.len().saturating_add(8));
978 let bytes = fmt.as_bytes();
979 let mut i = 0;
980 let mut literal_start = 0;
981
982 while i < bytes.len() {
983 if bytes[i] != b'%' || i + 1 >= bytes.len() {
984 i += 1;
985 continue;
986 }
987
988 result.push_str(&fmt[literal_start..i]);
989
990 let spec_suffix = &fmt[i + 1..];
991 let Some(spec) = spec_suffix.chars().next() else {
992 break;
993 };
994 i += 1 + spec.len_utf8();
995 literal_start = i;
996
997 match spec {
998 'd' => push_zero_padded_2(&mut result, d),
999 'e' => push_space_padded_2(&mut result, d),
1000 'F' => {
1001 push_zero_padded_4(&mut result, y);
1003 result.push('-');
1004 push_zero_padded_2(&mut result, mo);
1005 result.push('-');
1006 push_zero_padded_2(&mut result, d);
1007 }
1008 'f' => {
1009 let total = (s as f64 + frac).min(59.999);
1011 push_format(&mut result, format_args!("{total:06.3}"));
1012 }
1013 'H' => push_zero_padded_2(&mut result, h),
1014 'I' => {
1015 let h12 = if h == 0 {
1017 12
1018 } else if h > 12 {
1019 h - 12
1020 } else {
1021 h
1022 };
1023 push_zero_padded_2(&mut result, h12);
1024 }
1025 'j' => push_zero_padded_3(&mut result, doy),
1026 'J' => {
1027 push_format(&mut result, format_args!("{jdn:.15}"));
1029 while result.as_bytes().last() == Some(&b'0') {
1030 result.pop();
1031 }
1032 if result.as_bytes().last() == Some(&b'.') {
1033 result.pop();
1034 }
1035 }
1036 'k' => {
1037 push_space_padded_2(&mut result, h);
1039 }
1040 'l' => {
1041 let h12 = if h == 0 {
1043 12
1044 } else if h > 12 {
1045 h - 12
1046 } else {
1047 h
1048 };
1049 push_space_padded_2(&mut result, h12);
1050 }
1051 'm' => push_zero_padded_2(&mut result, mo),
1052 'M' => push_zero_padded_2(&mut result, mi),
1053 'p' => {
1054 result.push_str(if h < 12 { "AM" } else { "PM" });
1055 }
1056 'P' => {
1057 result.push_str(if h < 12 { "am" } else { "pm" });
1058 }
1059 'R' => {
1060 push_zero_padded_2(&mut result, h);
1061 result.push(':');
1062 push_zero_padded_2(&mut result, mi);
1063 }
1064 's' => {
1065 if subsec {
1066 let unix = jdn_to_unix_subsec(jdn);
1067 push_format(&mut result, format_args!("{unix:.3}"));
1068 } else {
1069 let unix = jdn_to_unix(jdn);
1070 push_format(&mut result, format_args!("{unix}"));
1071 }
1072 }
1073 'S' => push_zero_padded_2(&mut result, s),
1074 'T' => {
1075 push_zero_padded_2(&mut result, h);
1076 result.push(':');
1077 push_zero_padded_2(&mut result, mi);
1078 result.push(':');
1079 push_zero_padded_2(&mut result, s);
1080 }
1081 'u' => {
1082 let u = if dow == 0 { 7 } else { dow };
1084 push_format(&mut result, format_args!("{u}"));
1085 }
1086 'w' => push_format(&mut result, format_args!("{dow}")),
1087 'W' => {
1088 let w = (doy + 6 - ((dow + 6) % 7)) / 7;
1090 push_zero_padded_2(&mut result, w);
1091 }
1092 'Y' => push_zero_padded_4(&mut result, y),
1093 'G' | 'g' | 'V' => {
1094 let (iso_y, iso_w) = iso_week(y, mo, d);
1096 match spec {
1097 'G' => push_zero_padded_4(&mut result, iso_y),
1098 'g' => push_zero_padded_2(&mut result, iso_y % 100),
1099 'V' => push_zero_padded_2(&mut result, iso_w),
1100 _ => unreachable!(),
1101 }
1102 }
1103 '%' => result.push('%'),
1104 other => {
1105 result.push('%');
1106 result.push(other);
1107 }
1108 }
1109 }
1110
1111 if literal_start < fmt.len() {
1112 result.push_str(&fmt[literal_start..]);
1113 }
1114
1115 result
1116}
1117
1118fn iso_week(y: i64, m: i64, d: i64) -> (i64, i64) {
1120 let jdn = ymd_to_jdn(y, m, d);
1121 let jdn_int = (jdn + 0.5).floor() as i64;
1122 let dow = (jdn_int + 1) % 7;
1124 let iso_dow = if dow == 0 { 7 } else { dow };
1125
1126 let thu_jdn = jdn_int + (4 - iso_dow);
1128 let (thu_y, _, _) = jdn_to_ymd(thu_jdn as f64);
1129
1130 let jan4_jdn = (ymd_to_jdn(thu_y, 1, 4) + 0.5).floor() as i64;
1132 let jan4_dow = (jan4_jdn + 1) % 7;
1133 let jan4_iso_dow = if jan4_dow == 0 { 7 } else { jan4_dow };
1134 let week1_start = jan4_jdn - (jan4_iso_dow - 1);
1135
1136 let week = (thu_jdn - week1_start) / 7 + 1;
1137 (thu_y, week)
1138}
1139
1140fn timediff_impl(jdn1: f64, jdn2: f64) -> String {
1143 let (sign, start_jdn, end_jdn) = if jdn1 >= jdn2 {
1144 ('+', jdn2, jdn1)
1145 } else {
1146 ('-', jdn1, jdn2)
1147 };
1148
1149 let (start_y, start_mo, start_d) = jdn_to_ymd(start_jdn);
1150 let (start_h, start_mi, mut start_s, start_frac) = jdn_to_hms(start_jdn);
1151 let mut start_ms = (start_frac * 1000.0).round() as i64;
1152 if start_ms >= 1000 {
1153 start_ms = 0;
1154 start_s += 1;
1155 }
1156
1157 let (end_y, end_mo, end_d) = jdn_to_ymd(end_jdn);
1158 let (end_h, end_mi, mut end_s, end_frac) = jdn_to_hms(end_jdn);
1159 let mut end_ms = (end_frac * 1000.0).round() as i64;
1160 if end_ms >= 1000 {
1161 end_ms = 0;
1162 end_s += 1;
1163 }
1164
1165 let mut years = end_y - start_y;
1166 let mut months = end_mo - start_mo;
1167 let mut days = end_d - start_d;
1168 let mut hours = end_h - start_h;
1169 let mut minutes = end_mi - start_mi;
1170 let mut seconds = end_s - start_s;
1171 let mut millis = end_ms - start_ms;
1172
1173 if millis < 0 {
1174 millis += 1000;
1175 seconds -= 1;
1176 }
1177 if seconds < 0 {
1178 seconds += 60;
1179 minutes -= 1;
1180 }
1181 if minutes < 0 {
1182 minutes += 60;
1183 hours -= 1;
1184 }
1185 if hours < 0 {
1186 hours += 24;
1187 days -= 1;
1188 }
1189 if days < 0 {
1190 months -= 1;
1191 let (borrow_y, borrow_mo) = if end_mo == 1 {
1192 (end_y - 1, 12)
1193 } else {
1194 (end_y, end_mo - 1)
1195 };
1196 days += days_in_month(borrow_y, borrow_mo);
1197 }
1198 if months < 0 {
1199 months += 12;
1200 years -= 1;
1201 }
1202
1203 format!(
1204 "{sign}{years:04}-{months:02}-{days:02} {hours:02}:{minutes:02}:{seconds:02}.{millis:03}"
1205 )
1206}
1207
1208#[must_use]
1235pub fn is_datetime_invocation_safe_for_schema(function_name: &str, args: &[SqliteValue]) -> bool {
1236 if function_name.eq_ignore_ascii_case("strftime") {
1237 return args.first().is_none_or(SqliteValue::is_null)
1241 || datetime_arguments_are_schema_safe(&args[1..]);
1242 }
1243
1244 if function_name.eq_ignore_ascii_case("timediff") {
1245 if args.len() != 2 {
1249 return true;
1250 }
1251 for time_value in args {
1252 match classify_time_value_for_schema(time_value) {
1253 SchemaTimeValue::Dynamic => return false,
1254 SchemaTimeValue::NullOrInvalid => return true,
1255 SchemaTimeValue::Fixed { .. } => {}
1256 }
1257 }
1258 return true;
1259 }
1260
1261 if function_name.eq_ignore_ascii_case("date")
1262 || function_name.eq_ignore_ascii_case("time")
1263 || function_name.eq_ignore_ascii_case("datetime")
1264 || function_name.eq_ignore_ascii_case("julianday")
1265 || function_name.eq_ignore_ascii_case("unixepoch")
1266 {
1267 return datetime_arguments_are_schema_safe(args);
1268 }
1269
1270 true
1271}
1272
1273fn datetime_arguments_are_schema_safe(args: &[SqliteValue]) -> bool {
1275 let Some(time_value) = args.first() else {
1276 return false;
1277 };
1278 let (input, raw_numeric) = match classify_time_value_for_schema(time_value) {
1279 SchemaTimeValue::Dynamic => return false,
1280 SchemaTimeValue::NullOrInvalid => return true,
1281 SchemaTimeValue::Fixed { input, raw_numeric } => (input, raw_numeric),
1282 };
1283
1284 let mut reached_modifiers = Vec::with_capacity(args.len().saturating_sub(1));
1285 for modifier in &args[1..] {
1286 if modifier.is_null() {
1287 return true;
1288 }
1289 if sqlite_value_is_keyword(modifier, "localtime")
1290 || sqlite_value_is_keyword(modifier, "utc")
1291 {
1292 return false;
1293 }
1294 let Some(modifier) = sqlite_value_datetime_text(modifier) else {
1295 return true;
1296 };
1297 reached_modifiers.push(modifier.into_owned());
1298 if apply_modifiers(input, &reached_modifiers, raw_numeric).is_none() {
1299 return true;
1302 }
1303 }
1304 true
1305}
1306
1307enum SchemaTimeValue {
1308 Dynamic,
1309 NullOrInvalid,
1310 Fixed { input: f64, raw_numeric: bool },
1311}
1312
1313fn classify_time_value_for_schema(value: &SqliteValue) -> SchemaTimeValue {
1314 if value.is_null() {
1315 return SchemaTimeValue::NullOrInvalid;
1316 }
1317 if is_implicit_now_time_value(value) {
1318 return SchemaTimeValue::Dynamic;
1319 }
1320 match parse_time_value(value) {
1321 Some(parsed) => SchemaTimeValue::Fixed {
1322 input: parsed.jdn,
1323 raw_numeric: parsed.raw_numeric,
1324 },
1325 None => SchemaTimeValue::NullOrInvalid,
1326 }
1327}
1328
1329fn is_implicit_now_time_value(value: &SqliteValue) -> bool {
1330 sqlite_value_is_keyword(value, "now")
1331 || sqlite_value_is_keyword(value, "subsec")
1332 || sqlite_value_is_keyword(value, "subsecond")
1333}
1334
1335fn sqlite_value_is_keyword(value: &SqliteValue, keyword: &str) -> bool {
1339 let bytes = match value {
1340 SqliteValue::Text(text) => sqlite_c_string_bytes(text.as_bytes_direct()),
1341 SqliteValue::Blob(bytes) => sqlite_c_string_bytes(bytes),
1342 SqliteValue::Null | SqliteValue::Integer(_) | SqliteValue::Float(_) => return false,
1343 };
1344 bytes.eq_ignore_ascii_case(keyword.as_bytes())
1345}
1346
1347#[derive(Clone, Copy)]
1348struct ParsedTimeValue {
1349 jdn: f64,
1350 raw_numeric: bool,
1351 unmodified_hms: Option<UnmodifiedHms>,
1352}
1353
1354fn parse_time_value(value: &SqliteValue) -> Option<ParsedTimeValue> {
1355 match value {
1356 SqliteValue::Null => None,
1357 SqliteValue::Integer(integer) => Some(ParsedTimeValue {
1358 jdn: *integer as f64,
1359 raw_numeric: true,
1360 unmodified_hms: None,
1361 }),
1362 SqliteValue::Float(float) if float.is_finite() => Some(ParsedTimeValue {
1363 jdn: *float,
1364 raw_numeric: true,
1365 unmodified_hms: None,
1366 }),
1367 SqliteValue::Float(_) => None,
1368 SqliteValue::Text(_) | SqliteValue::Blob(_) => {
1369 let text = sqlite_value_datetime_text(value)?;
1370 let numeric = text.trim_matches(|c: char| c.is_ascii_whitespace());
1371 if let Ok(number) = numeric.parse::<f64>()
1372 && number.is_finite()
1373 {
1374 return Some(ParsedTimeValue {
1375 jdn: number,
1376 raw_numeric: true,
1377 unmodified_hms: None,
1378 });
1379 }
1380 Some(ParsedTimeValue {
1381 jdn: parse_timestring(text.as_ref())?,
1382 raw_numeric: false,
1383 unmodified_hms: unmodified_hms_from_timestring(text.as_ref()),
1384 })
1385 }
1386 }
1387}
1388
1389fn unmodified_hms_from_timestring(value: &str) -> Option<UnmodifiedHms> {
1390 let value = sqlite_c_string_str(value).trim_end_matches(|c: char| c.is_ascii_whitespace());
1391 let time = if value.len() > 10
1392 && value.as_bytes().get(4) == Some(&b'-')
1393 && value.as_bytes().get(7) == Some(&b'-')
1394 && value
1395 .as_bytes()
1396 .get(10)
1397 .is_some_and(|separator| matches!(*separator, b' ' | b'T'))
1398 {
1399 &value[11..]
1400 } else if value.len() >= 5 && value.as_bytes().get(2) == Some(&b':') {
1401 value
1402 } else {
1403 return None;
1404 };
1405 let (hour, minute, second, fraction, timezone_offset) = parse_time_part_with_tz(time)?;
1406 (timezone_offset == 0).then_some(UnmodifiedHms {
1407 hour,
1408 minute,
1409 second,
1410 fraction,
1411 })
1412}
1413
1414struct ParsedDateTimeArgs {
1415 jdn: f64,
1416 subsec: bool,
1417 unmodified_hms: Option<UnmodifiedHms>,
1418}
1419
1420fn parse_args(args: &[SqliteValue]) -> Option<ParsedDateTimeArgs> {
1422 let first_position_subsec = args.first().is_some_and(|value| {
1423 sqlite_value_is_keyword(value, "subsec") || sqlite_value_is_keyword(value, "subsecond")
1424 });
1425 let parsed = match args.first() {
1426 None => ParsedTimeValue {
1427 jdn: current_time_jdn(),
1428 raw_numeric: false,
1429 unmodified_hms: None,
1430 },
1431 Some(value) => parse_time_value(value)?,
1432 };
1433
1434 let mut modifiers = Vec::with_capacity(args.len().saturating_sub(1));
1435 for modifier in args.get(1..).unwrap_or_default() {
1436 modifiers.push(sqlite_value_datetime_text(modifier)?.into_owned());
1437 }
1438
1439 if parsed.raw_numeric {
1444 let first = modifiers
1445 .first()
1446 .map(|modifier| modifier.to_ascii_lowercase());
1447 let reinterprets_raw = matches!(first.as_deref(), Some("unixepoch" | "julianday" | "auto"));
1448 if !reinterprets_raw && !(0.0..=AUTO_JDN_MAX).contains(&parsed.jdn) {
1449 return None;
1450 }
1451 }
1452
1453 let preserves_input_hms = modifiers.iter().all(|modifier| {
1454 modifier.eq_ignore_ascii_case("subsec") || modifier.eq_ignore_ascii_case("subsecond")
1455 });
1456 let (jdn, modifier_subsec) = apply_modifiers(parsed.jdn, &modifiers, parsed.raw_numeric)?;
1457 Some(ParsedDateTimeArgs {
1458 jdn,
1459 subsec: first_position_subsec || modifier_subsec,
1460 unmodified_hms: preserves_input_hms
1461 .then_some(parsed.unmodified_hms)
1462 .flatten(),
1463 })
1464}
1465
1466pub struct DateFunc;
1469
1470impl ScalarFunction for DateFunc {
1471 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1472 match parse_args(args) {
1473 Some(parsed) => Ok(SqliteValue::Text(format_date(parsed.jdn))),
1474 None => Ok(SqliteValue::Null),
1475 }
1476 }
1477
1478 fn num_args(&self) -> i32 {
1479 -1
1480 }
1481
1482 fn is_deterministic(&self) -> bool {
1483 false
1484 }
1485
1486 fn name(&self) -> &str {
1487 "date"
1488 }
1489}
1490
1491pub struct TimeFunc;
1494
1495impl ScalarFunction for TimeFunc {
1496 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1497 match parse_args(args) {
1498 Some(parsed) => Ok(SqliteValue::Text(format_time(
1499 parsed.jdn,
1500 parsed.subsec,
1501 parsed.unmodified_hms,
1502 ))),
1503 None => Ok(SqliteValue::Null),
1504 }
1505 }
1506
1507 fn num_args(&self) -> i32 {
1508 -1
1509 }
1510
1511 fn is_deterministic(&self) -> bool {
1512 false
1513 }
1514
1515 fn name(&self) -> &str {
1516 "time"
1517 }
1518}
1519
1520pub struct DateTimeFunc;
1523
1524impl ScalarFunction for DateTimeFunc {
1525 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1526 match parse_args(args) {
1527 Some(parsed) => Ok(SqliteValue::Text(format_datetime(
1528 parsed.jdn,
1529 parsed.subsec,
1530 parsed.unmodified_hms,
1531 ))),
1532 None => Ok(SqliteValue::Null),
1533 }
1534 }
1535
1536 fn num_args(&self) -> i32 {
1537 -1
1538 }
1539
1540 fn is_deterministic(&self) -> bool {
1541 false
1542 }
1543
1544 fn name(&self) -> &str {
1545 "datetime"
1546 }
1547}
1548
1549pub struct JuliandayFunc;
1552
1553impl ScalarFunction for JuliandayFunc {
1554 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1555 match parse_args(args) {
1556 Some(parsed) => Ok(SqliteValue::Float(parsed.jdn)),
1557 None => Ok(SqliteValue::Null),
1558 }
1559 }
1560
1561 fn num_args(&self) -> i32 {
1562 -1
1563 }
1564
1565 fn is_deterministic(&self) -> bool {
1566 false
1567 }
1568
1569 fn name(&self) -> &str {
1570 "julianday"
1571 }
1572}
1573
1574pub struct UnixepochFunc;
1577
1578impl ScalarFunction for UnixepochFunc {
1579 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1580 match parse_args(args) {
1581 Some(parsed) if parsed.subsec => Ok(SqliteValue::Float(jdn_to_unix_subsec(parsed.jdn))),
1584 Some(parsed) => Ok(SqliteValue::Integer(jdn_to_unix(parsed.jdn))),
1585 None => Ok(SqliteValue::Null),
1586 }
1587 }
1588
1589 fn num_args(&self) -> i32 {
1590 -1
1591 }
1592
1593 fn is_deterministic(&self) -> bool {
1594 false
1595 }
1596
1597 fn name(&self) -> &str {
1598 "unixepoch"
1599 }
1600}
1601
1602pub struct StrftimeFunc;
1605
1606impl ScalarFunction for StrftimeFunc {
1607 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1608 let Some(format_value) = args.first() else {
1609 return Ok(SqliteValue::Null);
1610 };
1611 let Some(fmt) = sqlite_value_datetime_text(format_value) else {
1612 return Ok(SqliteValue::Null);
1613 };
1614 let rest = &args[1..];
1615 match parse_args(rest) {
1616 Some(parsed) => Ok(SqliteValue::Text(
1617 format_strftime(
1618 fmt.as_ref(),
1619 parsed.jdn,
1620 parsed.subsec,
1621 parsed.unmodified_hms,
1622 )
1623 .into(),
1624 )),
1625 None => Ok(SqliteValue::Null),
1626 }
1627 }
1628
1629 fn num_args(&self) -> i32 {
1630 -1
1631 }
1632
1633 fn is_deterministic(&self) -> bool {
1634 false
1635 }
1636
1637 fn name(&self) -> &str {
1638 "strftime"
1639 }
1640}
1641
1642pub struct TimediffFunc;
1645
1646impl ScalarFunction for TimediffFunc {
1647 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1648 if args.len() < 2 || args[0].is_null() || args[1].is_null() {
1649 return Ok(SqliteValue::Null);
1650 }
1651
1652 let jdn1 = parse_time_value(&args[0])
1653 .map(|parsed| parsed.jdn)
1654 .filter(|jdn| (0.0..=AUTO_JDN_MAX).contains(jdn));
1655 let jdn2 = parse_time_value(&args[1])
1656 .map(|parsed| parsed.jdn)
1657 .filter(|jdn| (0.0..=AUTO_JDN_MAX).contains(jdn));
1658
1659 match (jdn1, jdn2) {
1660 (Some(j1), Some(j2)) => Ok(SqliteValue::Text(timediff_impl(j1, j2).into())),
1661 _ => Ok(SqliteValue::Null),
1662 }
1663 }
1664
1665 fn num_args(&self) -> i32 {
1666 2
1667 }
1668
1669 fn is_deterministic(&self) -> bool {
1670 false
1671 }
1672
1673 fn name(&self) -> &str {
1674 "timediff"
1675 }
1676}
1677
1678pub fn register_datetime_builtins(registry: &mut FunctionRegistry) {
1682 registry.register_conditionally_deterministic_scalar(DateFunc);
1683 registry.register_conditionally_deterministic_scalar(TimeFunc);
1684 registry.register_conditionally_deterministic_scalar(DateTimeFunc);
1685 registry.register_conditionally_deterministic_scalar(JuliandayFunc);
1686 registry.register_conditionally_deterministic_scalar(UnixepochFunc);
1687 registry.register_conditionally_deterministic_scalar(StrftimeFunc);
1688 registry.register_conditionally_deterministic_scalar(TimediffFunc);
1689}
1690
1691#[cfg(test)]
1694mod tests {
1695 use super::*;
1696
1697 fn text(s: &str) -> SqliteValue {
1698 SqliteValue::Text(s.into())
1699 }
1700
1701 fn int(v: i64) -> SqliteValue {
1702 SqliteValue::Integer(v)
1703 }
1704
1705 fn float(v: f64) -> SqliteValue {
1706 SqliteValue::Float(v)
1707 }
1708
1709 fn null() -> SqliteValue {
1710 SqliteValue::Null
1711 }
1712
1713 fn assert_text(result: &SqliteValue, expected: &str) {
1714 match result {
1715 SqliteValue::Text(s) => assert_eq!(s.as_ref(), expected, "text mismatch"),
1716 other => panic!("expected Text(\"{expected}\"), got {other:?}"),
1717 }
1718 }
1719
1720 fn blob(s: &str) -> SqliteValue {
1723 SqliteValue::Blob(s.as_bytes().into())
1724 }
1725
1726 #[test]
1727 fn test_schema_safety_common_datetime_argument_layout() {
1728 for name in ["date", "time", "datetime", "julianday", "unixepoch"] {
1729 assert!(
1730 !is_datetime_invocation_safe_for_schema(name, &[]),
1731 "{name}() implicitly reads the current time"
1732 );
1733 assert!(is_datetime_invocation_safe_for_schema(
1734 name,
1735 &[text("2024-03-15 12:34:56")]
1736 ));
1737 assert!(is_datetime_invocation_safe_for_schema(name, &[int(0)]));
1738 assert!(is_datetime_invocation_safe_for_schema(
1739 name,
1740 &[float(2_460_384.5)]
1741 ));
1742 assert!(is_datetime_invocation_safe_for_schema(name, &[null()]));
1743
1744 for current_time in ["now", "NOW", "subsec", "SUBSECOND"] {
1745 assert!(
1746 !is_datetime_invocation_safe_for_schema(name, &[text(current_time)]),
1747 "{name}({current_time:?}) must be conditional"
1748 );
1749 }
1750 assert!(!is_datetime_invocation_safe_for_schema(
1751 name,
1752 &[blob("NOW")]
1753 ));
1754 assert!(!is_datetime_invocation_safe_for_schema(
1755 name,
1756 &[text("now\0ignored")]
1757 ));
1758 assert!(!is_datetime_invocation_safe_for_schema(
1759 name,
1760 &[SqliteValue::Blob(b"subsec\0\xff".as_slice().into())]
1761 ));
1762 for invalid_padded in [" now ", "subsec ", " subsecond"] {
1763 assert!(is_datetime_invocation_safe_for_schema(
1764 name,
1765 &[text(invalid_padded)]
1766 ));
1767 }
1768 assert!(is_datetime_invocation_safe_for_schema(
1769 name,
1770 &[blob(" NoW ")]
1771 ));
1772 assert!(is_datetime_invocation_safe_for_schema(
1773 name,
1774 &[text("nowhere")]
1775 ));
1776
1777 assert!(is_datetime_invocation_safe_for_schema(
1779 name,
1780 &[text("localtime")]
1781 ));
1782 assert!(is_datetime_invocation_safe_for_schema(name, &[text("utc")]));
1783 assert!(is_datetime_invocation_safe_for_schema(
1784 name,
1785 &[text("2024-03-15"), text("subsec")]
1786 ));
1787 assert!(is_datetime_invocation_safe_for_schema(
1788 name,
1789 &[text("2024-03-15"), text("subsecond")]
1790 ));
1791
1792 for modifier in ["localtime", "LOCALTIME", "utc"] {
1793 assert!(
1794 !is_datetime_invocation_safe_for_schema(
1795 name,
1796 &[text("2024-03-15"), text(modifier)]
1797 ),
1798 "{name} modifier {modifier:?} depends on host-local state"
1799 );
1800 }
1801 assert!(!is_datetime_invocation_safe_for_schema(
1802 name,
1803 &[text("2024-03-15"), blob("UTC")]
1804 ));
1805 assert!(!is_datetime_invocation_safe_for_schema(
1806 name,
1807 &[text("2024-03-15"), text("localtime\0ignored")]
1808 ));
1809 assert!(is_datetime_invocation_safe_for_schema(
1810 name,
1811 &[text("2024-03-15"), text(" utc ")]
1812 ));
1813
1814 assert!(is_datetime_invocation_safe_for_schema(
1816 name,
1817 &[text("2024-03-15"), null(), text("localtime")]
1818 ));
1819 assert!(!is_datetime_invocation_safe_for_schema(
1820 name,
1821 &[text("2024-03-15"), text("localtime"), null()]
1822 ));
1823
1824 assert!(is_datetime_invocation_safe_for_schema(
1827 name,
1828 &[text("bogus"), text("localtime")]
1829 ));
1830 assert!(is_datetime_invocation_safe_for_schema(
1831 name,
1832 &[text("2000-01-01"), text("bogus"), text("localtime")]
1833 ));
1834 }
1835 }
1836
1837 #[test]
1838 fn test_schema_safety_strftime_uses_shifted_time_arguments() {
1839 assert!(is_datetime_invocation_safe_for_schema("strftime", &[]));
1840 assert!(is_datetime_invocation_safe_for_schema(
1841 "strftime",
1842 &[null()]
1843 ));
1844 assert!(is_datetime_invocation_safe_for_schema(
1845 "strftime",
1846 &[null(), text("now")]
1847 ));
1848
1849 assert!(!is_datetime_invocation_safe_for_schema(
1851 "strftime",
1852 &[text("%Y")]
1853 ));
1854 assert!(!is_datetime_invocation_safe_for_schema(
1855 "STRFTIME",
1856 &[text("%Y"), text("now")]
1857 ));
1858 assert!(!is_datetime_invocation_safe_for_schema(
1859 "strftime",
1860 &[text("%s"), text("subsecond")]
1861 ));
1862
1863 assert!(is_datetime_invocation_safe_for_schema(
1865 "strftime",
1866 &[text("now localtime utc"), text("2024-03-15")]
1867 ));
1868 assert!(is_datetime_invocation_safe_for_schema(
1869 "strftime",
1870 &[text("%Y"), int(0), text("unixepoch")]
1871 ));
1872 assert!(is_datetime_invocation_safe_for_schema(
1873 "strftime",
1874 &[text("%f"), text("2024-03-15"), text("subsec")]
1875 ));
1876 assert!(!is_datetime_invocation_safe_for_schema(
1877 "strftime",
1878 &[text("%Y"), text("2024-03-15"), text("localtime")]
1879 ));
1880 assert!(is_datetime_invocation_safe_for_schema(
1881 "strftime",
1882 &[text("%Y"), text("2024-03-15"), null(), text("localtime")]
1883 ));
1884 }
1885
1886 #[test]
1887 fn test_schema_safety_timediff_treats_both_inputs_as_time_values() {
1888 assert!(is_datetime_invocation_safe_for_schema("timediff", &[]));
1889 assert!(is_datetime_invocation_safe_for_schema(
1890 "timediff",
1891 &[text("2024-03-15")]
1892 ));
1893 assert!(is_datetime_invocation_safe_for_schema(
1894 "timediff",
1895 &[text("2024-03-15"), text("2024-03-14")]
1896 ));
1897 assert!(is_datetime_invocation_safe_for_schema(
1898 "timediff",
1899 &[int(2_460_384), float(2_460_383.5)]
1900 ));
1901
1902 for current_time in ["now", "subsec", "subsecond"] {
1903 assert!(!is_datetime_invocation_safe_for_schema(
1904 "timediff",
1905 &[text(current_time), text("2024-03-14")]
1906 ));
1907 assert!(!is_datetime_invocation_safe_for_schema(
1908 "timediff",
1909 &[text("2024-03-15"), text(current_time)]
1910 ));
1911 }
1912
1913 assert!(is_datetime_invocation_safe_for_schema(
1916 "timediff",
1917 &[text("localtime"), text("utc")]
1918 ));
1919 assert!(is_datetime_invocation_safe_for_schema(
1920 "timediff",
1921 &[null(), text("now")]
1922 ));
1923 assert!(is_datetime_invocation_safe_for_schema(
1924 "timediff",
1925 &[text("bogus"), text("now")]
1926 ));
1927 assert!(!is_datetime_invocation_safe_for_schema(
1928 "timediff",
1929 &[blob("NOW"), null()]
1930 ));
1931 }
1932
1933 #[test]
1934 fn test_schema_safety_ignores_non_datetime_function_names() {
1935 for name in ["", "my_date", "current_date", "date ", "random"] {
1936 assert!(is_datetime_invocation_safe_for_schema(
1937 name,
1938 &[text("now"), text("localtime")]
1939 ));
1940 }
1941 }
1942
1943 #[test]
1946 fn test_omitted_time_value_uses_current_time() {
1947 let date = DateFunc.invoke(&[]).unwrap();
1948 let time = TimeFunc.invoke(&[]).unwrap();
1949 let datetime = DateTimeFunc.invoke(&[]).unwrap();
1950 let julianday = JuliandayFunc.invoke(&[]).unwrap();
1951 let unixepoch = UnixepochFunc.invoke(&[]).unwrap();
1952 let year = StrftimeFunc.invoke(&[text("%Y")]).unwrap();
1953
1954 assert!(matches!(&date, SqliteValue::Text(value) if value.len() == 10));
1955 assert!(matches!(&time, SqliteValue::Text(value) if value.len() == 8));
1956 assert!(matches!(&datetime, SqliteValue::Text(value) if value.len() == 19));
1957 assert!(matches!(julianday, SqliteValue::Float(_)));
1958 assert!(matches!(unixepoch, SqliteValue::Integer(_)));
1959 assert!(matches!(&year, SqliteValue::Text(value)
1960 if value.len() == 4 && value.as_bytes().iter().all(u8::is_ascii_digit)));
1961
1962 assert_eq!(StrftimeFunc.invoke(&[]).unwrap(), SqliteValue::Null);
1963 assert_eq!(StrftimeFunc.invoke(&[null()]).unwrap(), SqliteValue::Null);
1964 }
1965
1966 #[test]
1967 fn test_first_position_subsec_aliases_use_current_time() {
1968 for alias in ["subsec", "subsecond"] {
1969 assert!(matches!(
1970 DateFunc.invoke(&[text(alias)]).unwrap(),
1971 SqliteValue::Text(value) if value.len() == 10
1972 ));
1973 assert!(matches!(
1974 TimeFunc.invoke(&[text(alias)]).unwrap(),
1975 SqliteValue::Text(value)
1976 if value.len() == 12 && value.as_bytes_direct()[8] == b'.'
1977 ));
1978 assert!(matches!(
1979 DateTimeFunc.invoke(&[text(alias)]).unwrap(),
1980 SqliteValue::Text(value)
1981 if value.len() == 23 && value.as_bytes_direct()[19] == b'.'
1982 ));
1983 assert!(matches!(
1984 JuliandayFunc.invoke(&[text(alias)]).unwrap(),
1985 SqliteValue::Float(_)
1986 ));
1987 assert!(matches!(
1988 UnixepochFunc.invoke(&[text(alias)]).unwrap(),
1989 SqliteValue::Float(_)
1990 ));
1991 assert!(matches!(
1992 StrftimeFunc.invoke(&[text("%s"), text(alias)]).unwrap(),
1993 SqliteValue::Text(value)
1994 if value.rsplit_once('.').is_some_and(|(_, fraction)| fraction.len() == 3)
1995 ));
1996 }
1997 }
1998
1999 #[test]
2000 fn test_padded_and_nul_terminated_special_values() {
2001 for invalid in [" now ", "subsec ", " subsecond"] {
2002 assert_eq!(
2003 DateFunc.invoke(&[text(invalid)]).unwrap(),
2004 SqliteValue::Null
2005 );
2006 }
2007 assert_eq!(
2008 DateFunc
2009 .invoke(&[text("2000-01-01"), text(" localtime ")])
2010 .unwrap(),
2011 SqliteValue::Null
2012 );
2013 assert!(matches!(
2014 DateFunc.invoke(&[text("now\0ignored")]).unwrap(),
2015 SqliteValue::Text(value) if value.len() == 10
2016 ));
2017 assert!(matches!(
2018 TimeFunc
2019 .invoke(&[SqliteValue::Blob(b"subsec\0\xff".as_slice().into())])
2020 .unwrap(),
2021 SqliteValue::Text(value) if value.len() == 12
2022 ));
2023 }
2024
2025 #[test]
2026 fn test_date_basic() {
2027 let r = DateFunc.invoke(&[text("2024-03-15 14:30:00")]).unwrap();
2028 assert_text(&r, "2024-03-15");
2029 }
2030
2031 #[test]
2032 fn test_time_basic() {
2033 let r = TimeFunc.invoke(&[text("2024-03-15 14:30:45")]).unwrap();
2034 assert_text(&r, "14:30:45");
2035 }
2036
2037 #[test]
2038 fn test_datetime_basic() {
2039 let r = DateTimeFunc.invoke(&[text("2024-03-15 14:30:00")]).unwrap();
2040 assert_text(&r, "2024-03-15 14:30:00");
2041 }
2042
2043 #[test]
2044 fn test_julianday_basic() {
2045 let r = JuliandayFunc.invoke(&[text("2024-03-15")]).unwrap();
2046 match r {
2047 SqliteValue::Float(jdn) => {
2048 assert!((jdn - 2_460_384.5).abs() < 0.01, "unexpected JDN: {jdn}");
2050 }
2051 other => panic!("expected Float, got {other:?}"),
2052 }
2053 }
2054
2055 fn julianday_float(input: &str) -> f64 {
2063 match JuliandayFunc.invoke(&[text(input)]).unwrap() {
2064 SqliteValue::Float(v) => v,
2065 other => panic!("expected Float, got {other:?} for input {input:?}"),
2066 }
2067 }
2068
2069 fn assert_jdn_close(actual: f64, expected: f64, ctx: &str) {
2070 assert!(
2072 (actual - expected).abs() < 1e-6,
2073 "JDN mismatch for {ctx}: got {actual}, expected {expected}"
2074 );
2075 }
2076
2077 #[test]
2078 fn test_julianday_rfc3339_z_suffix() {
2079 let naive = julianday_float("2026-04-07 16:00:00");
2081 assert_jdn_close(julianday_float("2026-04-07T16:00:00Z"), naive, "T...Z");
2082 assert_jdn_close(
2083 julianday_float("2026-04-07T16:00:00z"),
2084 naive,
2085 "lowercase z",
2086 );
2087 }
2088
2089 #[test]
2090 fn test_julianday_rfc3339_zero_offset() {
2091 let naive = julianday_float("2026-04-07 16:00:00");
2092 assert_jdn_close(
2093 julianday_float("2026-04-07T16:00:00+00:00"),
2094 naive,
2095 "+00:00",
2096 );
2097 assert_jdn_close(
2098 julianday_float("2026-04-07T16:00:00-00:00"),
2099 naive,
2100 "-00:00",
2101 );
2102 }
2103
2104 #[test]
2105 fn test_julianday_rfc3339_positive_offset() {
2106 let base = julianday_float("2026-04-07 16:00:00");
2108 let expected = base - 1.0 / 24.0;
2109 assert_jdn_close(
2110 julianday_float("2026-04-07T16:00:00+01:00"),
2111 expected,
2112 "+01:00",
2113 );
2114 }
2115
2116 #[test]
2117 fn test_julianday_rfc3339_negative_offset() {
2118 let base = julianday_float("2026-04-07 16:00:00");
2120 let expected = base + 5.0 / 24.0;
2121 assert_jdn_close(
2122 julianday_float("2026-04-07T16:00:00-05:00"),
2123 expected,
2124 "-05:00",
2125 );
2126 }
2127
2128 #[test]
2129 fn test_julianday_rfc3339_half_hour_offset() {
2130 let base = julianday_float("2026-04-07 16:00:00");
2132 let expected = base - 5.5 / 24.0;
2133 assert_jdn_close(
2134 julianday_float("2026-04-07T16:00:00+05:30"),
2135 expected,
2136 "+05:30",
2137 );
2138 }
2139
2140 #[test]
2141 fn test_julianday_rfc3339_compact_offsets() {
2142 let base = julianday_float("2026-04-07 16:00:00");
2144 assert_jdn_close(
2145 julianday_float("2026-04-07T16:00:00+0100"),
2146 base - 1.0 / 24.0,
2147 "+0100",
2148 );
2149 assert_jdn_close(
2150 julianday_float("2026-04-07T16:00:00-0530"),
2151 base + 5.5 / 24.0,
2152 "-0530",
2153 );
2154 assert_jdn_close(
2155 julianday_float("2026-04-07T16:00:00+09"),
2156 base - 9.0 / 24.0,
2157 "+09",
2158 );
2159 }
2160
2161 #[test]
2162 fn test_julianday_rfc3339_fractional_seconds_with_tz() {
2163 let base = julianday_float("2026-04-07 16:00:00.500");
2165 assert_jdn_close(
2166 julianday_float("2026-04-07T16:00:00.500Z"),
2167 base,
2168 "fractional + Z",
2169 );
2170 assert_jdn_close(
2171 julianday_float("2026-04-07T16:00:00.500+01:00"),
2172 base - 1.0 / 24.0,
2173 "fractional + +01:00",
2174 );
2175 }
2176
2177 #[test]
2178 fn test_date_and_time_rfc3339_round_trip() {
2179 assert_text(
2182 &DateFunc
2183 .invoke(&[text("2026-04-07T16:00:00+05:00")])
2184 .unwrap(),
2185 "2026-04-07",
2187 );
2188 assert_text(
2189 &TimeFunc
2190 .invoke(&[text("2026-04-07T16:00:00+05:00")])
2191 .unwrap(),
2192 "11:00:00",
2193 );
2194 assert_text(
2195 &DateTimeFunc
2196 .invoke(&[text("2026-04-07T16:00:00+05:00")])
2197 .unwrap(),
2198 "2026-04-07 11:00:00",
2199 );
2200 }
2201
2202 #[test]
2203 fn test_julianday_rfc3339_invalid_offsets_return_null() {
2204 for bad in &[
2206 "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", ] {
2211 let result = JuliandayFunc.invoke(&[text(bad)]).unwrap();
2212 assert_eq!(
2213 result,
2214 SqliteValue::Null,
2215 "expected NULL for malformed offset {bad:?}, got {result:?}"
2216 );
2217 }
2218 }
2219
2220 #[test]
2221 fn test_julianday_rejects_malformed_time_fields() {
2222 for bad in &[
2226 "+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", ] {
2238 let result = JuliandayFunc.invoke(&[text(bad)]).unwrap();
2239 assert_eq!(
2240 result,
2241 SqliteValue::Null,
2242 "expected NULL for signed time field {bad:?}, got {result:?}"
2243 );
2244 }
2245 }
2246
2247 #[test]
2248 fn test_unixepoch_basic() {
2249 let r = UnixepochFunc
2250 .invoke(&[text("1970-01-01 00:00:00")])
2251 .unwrap();
2252 assert_eq!(r, int(0));
2253 }
2254
2255 #[test]
2256 fn test_unixepoch_known_date() {
2257 let r = UnixepochFunc
2258 .invoke(&[text("2024-01-01 00:00:00")])
2259 .unwrap();
2260 assert_eq!(r, int(1_704_067_200));
2262 }
2263
2264 #[test]
2267 fn test_modifier_days() {
2268 let r = DateFunc
2269 .invoke(&[text("2024-01-15"), text("+10 days")])
2270 .unwrap();
2271 assert_text(&r, "2024-01-25");
2272 }
2273
2274 #[test]
2275 fn test_modifier_months() {
2276 let r = DateFunc
2279 .invoke(&[text("2024-01-31"), text("+1 months")])
2280 .unwrap();
2281 assert_text(&r, "2024-03-02");
2282 }
2283
2284 #[test]
2285 fn test_modifier_years() {
2286 let r = DateFunc
2289 .invoke(&[text("2024-02-29"), text("+1 years")])
2290 .unwrap();
2291 assert_text(&r, "2025-03-01");
2292 }
2293
2294 #[test]
2295 fn test_modifier_hours() {
2296 let r = DateTimeFunc
2297 .invoke(&[text("2024-01-01 23:00:00"), text("+2 hours")])
2298 .unwrap();
2299 assert_text(&r, "2024-01-02 01:00:00");
2300 }
2301
2302 #[test]
2303 fn test_modifier_start_of_month() {
2304 let r = DateFunc
2305 .invoke(&[text("2024-03-15"), text("start of month")])
2306 .unwrap();
2307 assert_text(&r, "2024-03-01");
2308 }
2309
2310 #[test]
2311 fn test_modifier_start_of_year() {
2312 let r = DateFunc
2313 .invoke(&[text("2024-06-15"), text("start of year")])
2314 .unwrap();
2315 assert_text(&r, "2024-01-01");
2316 }
2317
2318 #[test]
2319 fn test_modifier_start_of_day() {
2320 let r = DateTimeFunc
2321 .invoke(&[text("2024-03-15 14:30:00"), text("start of day")])
2322 .unwrap();
2323 assert_text(&r, "2024-03-15 00:00:00");
2324 }
2325
2326 #[test]
2327 fn test_modifier_unixepoch() {
2328 let r = DateTimeFunc.invoke(&[int(0), text("unixepoch")]).unwrap();
2329 assert_text(&r, "1970-01-01 00:00:00");
2330 }
2331
2332 #[test]
2333 fn test_modifier_weekday() {
2334 let r = DateFunc
2336 .invoke(&[text("2024-03-15"), text("weekday 0")])
2337 .unwrap();
2338 assert_text(&r, "2024-03-17");
2339 }
2340
2341 #[test]
2342 fn test_modifier_auto_unixepoch() {
2343 let ts = int(1_710_531_045);
2344 let r = DateTimeFunc.invoke(&[ts.clone(), text("auto")]).unwrap();
2345 let expected = DateTimeFunc.invoke(&[ts, text("unixepoch")]).unwrap();
2346 assert_eq!(
2347 r, expected,
2348 "auto and unixepoch should agree for unix-like values"
2349 );
2350 }
2351
2352 #[test]
2353 fn test_modifier_auto_julian_day() {
2354 let r = DateFunc
2355 .invoke(&[float(2_460_384.5), text("auto")])
2356 .unwrap();
2357 assert_text(&r, "2024-03-15");
2358 }
2359
2360 #[test]
2361 fn test_modifier_localtime_utc_roundtrip() {
2362 let r = DateTimeFunc
2364 .invoke(&[text("2024-03-15 14:30:45"), text("localtime"), text("utc")])
2365 .unwrap();
2366 assert_text(&r, "2024-03-15 14:30:45");
2367 }
2368
2369 #[test]
2370 fn test_modifier_localtime_shifts_value() {
2371 let offset = utc_offset_for_utc_jdn(ymdhms_to_jdn(2024, 3, 15, 12, 0, 0, 0.0));
2373 if offset != 0 {
2374 let r = DateTimeFunc
2375 .invoke(&[text("2024-03-15 12:00:00"), text("localtime")])
2376 .unwrap();
2377 let shifted = match &r {
2379 SqliteValue::Text(s) => s.clone(),
2380 _ => panic!("expected text"),
2381 };
2382 assert_ne!(&*shifted, "2024-03-15 12:00:00");
2383 }
2384 }
2385
2386 #[test]
2387 fn test_modifier_auto_out_of_range_returns_null() {
2388 let r = DateTimeFunc.invoke(&[float(1.0e20), text("auto")]).unwrap();
2389 assert_eq!(r, SqliteValue::Null);
2390 }
2391
2392 #[test]
2393 fn test_modifier_order_matters() {
2394 let r1 = DateFunc
2396 .invoke(&[text("2024-03-15"), text("start of month"), text("+1 days")])
2397 .unwrap();
2398 assert_text(&r1, "2024-03-02");
2399
2400 let r2 = DateFunc
2402 .invoke(&[text("2024-03-15"), text("+1 days"), text("start of month")])
2403 .unwrap();
2404 assert_text(&r2, "2024-03-01");
2405 }
2406
2407 #[test]
2408 fn test_modifier_weekday_same_day_is_noop() {
2409 let r = DateFunc
2411 .invoke(&[text("2024-03-17"), text("weekday 0")])
2412 .unwrap();
2413 assert_text(&r, "2024-03-17");
2414 }
2415
2416 #[test]
2419 fn test_bare_time_defaults() {
2420 let r = DateFunc.invoke(&[text("12:30:00")]).unwrap();
2421 assert_text(&r, "2000-01-01");
2422 }
2423
2424 #[test]
2425 fn test_t_separator() {
2426 let r = DateTimeFunc.invoke(&[text("2024-03-15T14:30:00")]).unwrap();
2427 assert_text(&r, "2024-03-15 14:30:00");
2428 }
2429
2430 #[test]
2431 fn test_julian_day_input() {
2432 let r = DateFunc.invoke(&[float(2_460_384.5)]).unwrap();
2434 assert_text(&r, "2024-03-15");
2435 }
2436
2437 #[test]
2438 fn test_null_input() {
2439 assert_eq!(DateFunc.invoke(&[null()]).unwrap(), SqliteValue::Null);
2440 }
2441
2442 #[test]
2443 fn test_invalid_input() {
2444 assert_eq!(
2445 DateFunc.invoke(&[text("not-a-date")]).unwrap(),
2446 SqliteValue::Null
2447 );
2448 }
2449
2450 #[test]
2451 fn test_negative_time_component_invalid() {
2452 let r = TimeFunc.invoke(&[text("-01:00")]).unwrap();
2453 assert_eq!(r, SqliteValue::Null);
2454 }
2455
2456 #[test]
2459 fn test_leap_year() {
2460 let r = DateFunc
2461 .invoke(&[text("2024-02-28"), text("+1 days")])
2462 .unwrap();
2463 assert_text(&r, "2024-02-29");
2464 }
2465
2466 #[test]
2467 fn test_non_leap_year() {
2468 let r = DateFunc
2469 .invoke(&[text("2023-02-28"), text("+1 days")])
2470 .unwrap();
2471 assert_text(&r, "2023-03-01");
2472 }
2473
2474 #[test]
2477 fn test_strftime_basic() {
2478 let r = StrftimeFunc
2479 .invoke(&[text("%Y-%m-%d"), text("2024-03-15")])
2480 .unwrap();
2481 assert_text(&r, "2024-03-15");
2482 }
2483
2484 #[test]
2485 fn test_strftime_time_specifiers() {
2486 let r = StrftimeFunc
2487 .invoke(&[text("%H:%M:%S"), text("2024-03-15 14:30:45")])
2488 .unwrap();
2489 assert_text(&r, "14:30:45");
2490 }
2491
2492 #[test]
2493 fn test_strftime_unix_seconds() {
2494 let r = StrftimeFunc
2495 .invoke(&[text("%s"), text("1970-01-01 00:00:00")])
2496 .unwrap();
2497 assert_text(&r, "0");
2498 }
2499
2500 #[test]
2501 fn test_strftime_day_of_year() {
2502 let r = StrftimeFunc
2503 .invoke(&[text("%j"), text("2024-03-15")])
2504 .unwrap();
2505 assert_text(&r, "075");
2507 }
2508
2509 #[test]
2510 fn test_strftime_day_of_week() {
2511 let r = StrftimeFunc
2513 .invoke(&[text("%w"), text("2024-03-15")])
2514 .unwrap();
2515 assert_text(&r, "5");
2516
2517 let r = StrftimeFunc
2518 .invoke(&[text("%u"), text("2024-03-15")])
2519 .unwrap();
2520 assert_text(&r, "5");
2521 }
2522
2523 #[test]
2524 fn test_strftime_12hour() {
2525 let r = StrftimeFunc
2526 .invoke(&[text("%I %p"), text("2024-03-15 14:30:00")])
2527 .unwrap();
2528 assert_text(&r, "02 PM");
2529
2530 let r = StrftimeFunc
2531 .invoke(&[text("%I %P"), text("2024-03-15 09:30:00")])
2532 .unwrap();
2533 assert_text(&r, "09 am");
2534 }
2535
2536 #[test]
2537 fn test_strftime_all_specifiers_presence() {
2538 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|%%";
2539 let r = StrftimeFunc
2540 .invoke(&[text(fmt), text("2024-03-15 14:30:45.123")])
2541 .unwrap();
2542
2543 let s = match r {
2544 SqliteValue::Text(v) => v,
2545 other => panic!("expected Text, got {other:?}"),
2546 };
2547 let parts: Vec<&str> = s.split('|').collect();
2548 assert_eq!(parts.len(), 25, "unexpected specifier output: {s}");
2549 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!(
2556 parts[6].parse::<f64>().is_ok(),
2557 "expected numeric %J output, got {}",
2558 parts[6]
2559 );
2560 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!(
2568 parts[14].parse::<i64>().is_ok(),
2569 "expected numeric %s output, got {}",
2570 parts[14]
2571 );
2572 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], "%"); }
2583
2584 #[test]
2585 fn test_strftime_null() {
2586 assert_eq!(
2587 StrftimeFunc.invoke(&[null(), text("2024-01-01")]).unwrap(),
2588 SqliteValue::Null
2589 );
2590 assert_eq!(
2591 StrftimeFunc.invoke(&[text("%Y"), null()]).unwrap(),
2592 SqliteValue::Null
2593 );
2594 }
2595
2596 #[test]
2597 #[ignore = "perf-only benchmark"]
2598 fn perf_strftime_timestamp_rows() {
2599 use std::hint::black_box;
2600 use std::time::Instant;
2601
2602 const ROWS: usize = 200_000;
2603 const REPEATS: usize = 5;
2604 const FORMAT: &str = "%Y-%m-%d %H:%M:%S";
2605 const INPUT: &str = "2024-03-15 14:30:45";
2606
2607 let func = StrftimeFunc;
2608 let fmt = text(FORMAT);
2609 let input = text(INPUT);
2610 let mut best_ns = u128::MAX;
2611 let mut output_len = 0usize;
2612
2613 for _ in 0..REPEATS {
2614 let started = Instant::now();
2615 for _ in 0..ROWS {
2616 let result = black_box(
2617 func.invoke(black_box(&[fmt.clone(), input.clone()]))
2618 .expect("strftime benchmark invocation must succeed"),
2619 );
2620 output_len = match result {
2621 SqliteValue::Text(text) => text.len(),
2622 SqliteValue::Null
2623 | SqliteValue::Integer(_)
2624 | SqliteValue::Float(_)
2625 | SqliteValue::Blob(_) => 0,
2626 };
2627 }
2628 let elapsed_ns = started.elapsed().as_nanos();
2629 if elapsed_ns < best_ns {
2630 best_ns = elapsed_ns;
2631 }
2632 }
2633
2634 println!(
2635 "strftime_timestamp_rows rows={ROWS} repeats={REPEATS} best_ns={best_ns} output_len={output_len}"
2636 );
2637 }
2638
2639 #[test]
2642 fn test_timediff_basic() {
2643 let r = TimediffFunc
2644 .invoke(&[text("2024-03-15"), text("2024-03-10")])
2645 .unwrap();
2646 assert_text(&r, "+0000-00-05 00:00:00.000");
2647 }
2648
2649 #[test]
2650 fn test_timediff_negative() {
2651 let r = TimediffFunc
2652 .invoke(&[text("2024-03-10"), text("2024-03-15")])
2653 .unwrap();
2654 assert_text(&r, "-0000-00-05 00:00:00.000");
2655 }
2656
2657 #[test]
2658 fn test_timediff_year_boundary() {
2659 let r = TimediffFunc
2660 .invoke(&[text("2024-01-01 01:00:00"), text("2023-12-31 23:00:00")])
2661 .unwrap();
2662 assert_text(&r, "+0000-00-00 02:00:00.000");
2663 }
2664
2665 #[test]
2668 fn test_modifier_subsec() {
2669 assert_text(
2670 &TimeFunc
2671 .invoke(&[text("2024-01-01 12:00:00"), text("subsec")])
2672 .unwrap(),
2673 "12:00:00.000",
2674 );
2675 assert_text(
2676 &DateTimeFunc
2677 .invoke(&[text("2024-01-01 12:00:00"), text("subsecond")])
2678 .unwrap(),
2679 "2024-01-01 12:00:00.000",
2680 );
2681 assert_text(
2682 &TimeFunc
2683 .invoke(&[text("2024-01-01 12:00:00.123"), text("subsec")])
2684 .unwrap(),
2685 "12:00:00.123",
2686 );
2687 }
2688
2689 #[test]
2690 fn test_subsec_unix_seconds_and_integer_flooring() {
2691 assert_text(
2692 &StrftimeFunc
2693 .invoke(&[text("%s"), text("1970-01-01 00:00:00.125"), text("subsec")])
2694 .unwrap(),
2695 "0.125",
2696 );
2697 assert_eq!(
2698 UnixepochFunc
2699 .invoke(&[text("1970-01-01 00:00:00.125"), text("subsec")])
2700 .unwrap(),
2701 float(0.125)
2702 );
2703
2704 for input in ["1970-01-01 00:00:00.500", "1970-01-01 00:00:00.999"] {
2705 assert_eq!(UnixepochFunc.invoke(&[text(input)]).unwrap(), int(0));
2706 assert_text(
2707 &StrftimeFunc.invoke(&[text("%s"), text(input)]).unwrap(),
2708 "0",
2709 );
2710 }
2711 assert_eq!(
2712 UnixepochFunc
2713 .invoke(&[text("1969-12-31 23:59:59.999")])
2714 .unwrap(),
2715 int(-1)
2716 );
2717 assert_text(
2718 &StrftimeFunc
2719 .invoke(&[text("%s"), text("1969-12-31 23:59:59.999")])
2720 .unwrap(),
2721 "-1",
2722 );
2723 }
2724
2725 #[test]
2726 fn test_subsec_rounding_preserves_sqlite_second_60() {
2727 assert_text(
2728 &TimeFunc
2729 .invoke(&[text("12:34:59.9995"), text("subsec")])
2730 .unwrap(),
2731 "12:34:60.000",
2732 );
2733 assert_text(
2734 &DateTimeFunc
2735 .invoke(&[text("1970-01-01 23:59:59.9995"), text("subsec")])
2736 .unwrap(),
2737 "1970-01-01 23:59:60.000",
2738 );
2739 assert_text(
2740 &StrftimeFunc
2741 .invoke(&[
2742 text("%H:%M:%f|%s"),
2743 text("1970-01-01 23:59:59.9995"),
2744 text("subsec"),
2745 ])
2746 .unwrap(),
2747 "23:59:59.999|86400.000",
2748 );
2749 }
2750
2751 #[test]
2754 fn test_register_datetime_builtins_all_present() {
2755 let mut reg = FunctionRegistry::new();
2756 register_datetime_builtins(&mut reg);
2757
2758 let expected = [
2759 "date",
2760 "time",
2761 "datetime",
2762 "julianday",
2763 "unixepoch",
2764 "strftime",
2765 "timediff",
2766 ];
2767
2768 for name in expected {
2769 assert!(
2770 reg.find_scalar(name, 1).is_some() || reg.find_scalar(name, 2).is_some(),
2771 "datetime function '{name}' not registered"
2772 );
2773 }
2774 }
2775
2776 #[test]
2777 fn test_public_datetime_function_metadata_fails_closed_for_direct_registration() {
2778 assert!(!DateFunc.is_deterministic());
2779 assert!(!TimeFunc.is_deterministic());
2780 assert!(!DateTimeFunc.is_deterministic());
2781 assert!(!JuliandayFunc.is_deterministic());
2782 assert!(!UnixepochFunc.is_deterministic());
2783 assert!(!StrftimeFunc.is_deterministic());
2784 assert!(!TimediffFunc.is_deterministic());
2785
2786 let mut registry = FunctionRegistry::new();
2787 registry.register_scalar(DateFunc);
2788 registry.register_scalar(TimeFunc);
2789 registry.register_scalar(DateTimeFunc);
2790 registry.register_scalar(JuliandayFunc);
2791 registry.register_scalar(UnixepochFunc);
2792 registry.register_scalar(StrftimeFunc);
2793 registry.register_scalar(TimediffFunc);
2794 for (name, num_args) in [
2795 ("date", 0),
2796 ("time", 0),
2797 ("datetime", 0),
2798 ("julianday", 0),
2799 ("unixepoch", 0),
2800 ("strftime", 1),
2801 ("timediff", 2),
2802 ] {
2803 assert_eq!(
2804 registry.scalar_schema_safety(name, num_args),
2805 Some(crate::ScalarSchemaSafety::Never),
2806 "generic registration of {name}/{num_args} must fail closed"
2807 );
2808 }
2809 }
2810
2811 #[test]
2814 fn test_modifier_year_overflow() {
2815 let huge = i64::MAX;
2818 let modifier = format!("+{huge} years");
2819 let r = DateFunc.invoke(&[text("2000-01-01"), text(&modifier)]);
2820 assert_eq!(r.unwrap(), SqliteValue::Null);
2823 }
2824
2825 #[test]
2826 fn test_jdn_roundtrip() {
2827 let dates = [
2829 (2024, 3, 15),
2830 (2000, 1, 1),
2831 (1970, 1, 1),
2832 (2024, 2, 29),
2833 (1900, 1, 1),
2834 (2099, 12, 31),
2835 ];
2836 for (y, m, d) in dates {
2837 let jdn = ymd_to_jdn(y, m, d);
2838 let (y2, m2, d2) = jdn_to_ymd(jdn);
2839 assert_eq!(
2840 (y, m, d),
2841 (y2, m2, d2),
2842 "roundtrip failed for {y}-{m}-{d} (JDN={jdn})"
2843 );
2844 }
2845 }
2846
2847 #[test]
2848 fn test_unix_epoch_roundtrip() {
2849 let jdn = ymd_to_jdn(1970, 1, 1);
2850 let unix = jdn_to_unix(jdn);
2851 assert_eq!(unix, 0, "Unix epoch should be 0");
2852
2853 let jdn2 = unix_to_jdn(0.0);
2854 assert!((jdn2 - UNIX_EPOCH_JDN).abs() < 1e-10, "roundtrip failed");
2855 }
2856}