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 use fsqlite_types::sync_primitives::SystemTime;
279
280 let secs = SystemTime::now()
281 .duration_since(SystemTime::UNIX_EPOCH)
282 .unwrap_or_default()
283 .as_secs_f64();
284 UNIX_EPOCH_JDN + secs / 86_400.0
285}
286
287fn sqlite_c_string_bytes(bytes: &[u8]) -> &[u8] {
288 bytes
289 .iter()
290 .position(|&byte| byte == 0)
291 .map_or(bytes, |nul| &bytes[..nul])
292}
293
294fn sqlite_c_string_str(text: &str) -> &str {
295 let bytes = sqlite_c_string_bytes(text.as_bytes());
296 std::str::from_utf8(bytes).unwrap_or("")
299}
300
301fn sqlite_value_datetime_text(value: &SqliteValue) -> Option<Cow<'_, str>> {
302 match value {
303 SqliteValue::Null => None,
304 SqliteValue::Text(text) => Some(Cow::Borrowed(sqlite_c_string_str(text))),
305 SqliteValue::Blob(bytes) => std::str::from_utf8(sqlite_c_string_bytes(bytes))
306 .ok()
307 .map(Cow::Borrowed),
308 SqliteValue::Integer(_) | SqliteValue::Float(_) => Some(Cow::Owned(value.to_text())),
309 }
310}
311
312fn parse_iso8601(s: &str) -> Option<f64> {
313 let bytes = s.as_bytes();
321 let len = bytes.len();
322
323 if len >= 10 && bytes[4] == b'-' && bytes[7] == b'-' {
325 let y = s[0..4].parse::<i64>().ok()?;
326 let m = s[5..7].parse::<i64>().ok()?;
327 let d = s[8..10].parse::<i64>().ok()?;
328
329 if m < 1 || m > 12 || d < 1 || d > 31 {
330 return None;
331 }
332
333 if len == 10 {
334 return Some(ymd_to_jdn(y, m, d));
335 }
336
337 if len > 10 && (bytes[10] == b' ' || bytes[10] == b'T') {
339 let time_part = &s[11..];
340 let (h, mi, sec, frac, tz_offset_min) = parse_time_part_with_tz(time_part)?;
341 let jdn = ymdhms_to_jdn(y, m, d, h, mi, sec, frac);
342 return Some(jdn - (tz_offset_min as f64) / 1440.0);
345 }
346 return None;
347 }
348
349 if len >= 5 && bytes[2] == b':' {
351 let (h, mi, sec, frac, tz_offset_min) = parse_time_part_with_tz(s)?;
352 let jdn = ymdhms_to_jdn(2000, 1, 1, h, mi, sec, frac);
353 return Some(jdn - (tz_offset_min as f64) / 1440.0);
354 }
355
356 None
357}
358
359fn split_tz_suffix(s: &str) -> Option<(&str, i64)> {
367 if let Some(stripped) = s.strip_suffix('Z').or_else(|| s.strip_suffix('z')) {
369 return Some((stripped, 0));
370 }
371
372 let bytes = s.as_bytes();
381 for width in [6usize, 5, 3] {
383 if bytes.len() < width + 1 {
384 continue;
385 }
386 let split_at = bytes.len() - width;
387 let sign_byte = bytes[split_at];
388 if sign_byte != b'+' && sign_byte != b'-' {
389 continue;
390 }
391 let tz_part = &s[split_at..];
392 if let Some(offset) = parse_tz_offset(tz_part) {
393 return Some((&s[..split_at], offset));
394 }
395 }
396
397 Some((s, 0))
399}
400
401fn parse_tz_offset(tz: &str) -> Option<i64> {
404 let bytes = tz.as_bytes();
405 if bytes.is_empty() {
406 return None;
407 }
408 let sign: i64 = match bytes[0] {
409 b'+' => 1,
410 b'-' => -1,
411 _ => return None,
412 };
413 let rest = &tz[1..];
414 let (hours, minutes) = match rest.len() {
415 5 if rest.as_bytes()[2] == b':' => (
417 rest[0..2].parse::<i64>().ok()?,
418 rest[3..5].parse::<i64>().ok()?,
419 ),
420 4 => (
422 rest[0..2].parse::<i64>().ok()?,
423 rest[2..4].parse::<i64>().ok()?,
424 ),
425 2 => (rest.parse::<i64>().ok()?, 0),
427 _ => return None,
428 };
429 if !(0..=23).contains(&hours) || !(0..=59).contains(&minutes) {
430 return None;
431 }
432 Some(sign * (hours * 60 + minutes))
433}
434
435fn parse_time_part_with_tz(s: &str) -> Option<(i64, i64, i64, f64, i64)> {
438 let (time_only, tz_offset_min) = split_tz_suffix(s)?;
439 let (h, mi, sec, frac) = parse_time_part(time_only)?;
440 Some((h, mi, sec, frac, tz_offset_min))
441}
442
443fn parse_time_part(s: &str) -> Option<(i64, i64, i64, f64)> {
445 let [h_tens, h_ones, b':', mi_tens, mi_ones, rest @ ..] = s.as_bytes() else {
446 return None;
447 };
448 let h = parse_two_ascii_digits(*h_tens, *h_ones)?;
453 let mi = parse_two_ascii_digits(*mi_tens, *mi_ones)?;
454 if !(0..=23).contains(&h) || !(0..=59).contains(&mi) {
455 return None;
456 }
457
458 match rest {
463 [] => Some((h, mi, 0, 0.0)),
464 [b':', sec_tens, sec_ones] => {
465 let sec = parse_two_ascii_digits(*sec_tens, *sec_ones)?;
466 if !(0..=59).contains(&sec) {
467 return None;
468 }
469 Some((h, mi, sec, 0.0))
470 }
471 [b':', sec_tens, sec_ones, b'.', ..] => {
472 let sec = parse_two_ascii_digits(*sec_tens, *sec_ones)?;
473 if !(0..=59).contains(&sec) {
474 return None;
475 }
476 let frac = s.get(8..)?.parse::<f64>().ok()?;
477 Some((h, mi, sec, frac))
478 }
479 _ => None,
480 }
481}
482
483#[inline]
484fn parse_two_ascii_digits(tens: u8, ones: u8) -> Option<i64> {
485 if tens.is_ascii_digit() && ones.is_ascii_digit() {
486 Some(i64::from((tens - b'0') * 10 + (ones - b'0')))
487 } else {
488 None
489 }
490}
491
492fn apply_modifier(jdn: f64, modifier: &str) -> Option<f64> {
496 if has_outer_ascii_whitespace(modifier) {
497 return None;
498 }
499 let m = modifier.to_ascii_lowercase();
500
501 if m == "start of month" {
503 let (y, mo, _d) = jdn_to_ymd(jdn);
504 return Some(ymd_to_jdn(y, mo, 1));
505 }
506 if m == "start of year" {
507 let (y, _mo, _d) = jdn_to_ymd(jdn);
508 return Some(ymd_to_jdn(y, 1, 1));
509 }
510 if m == "start of day" {
511 let (y, mo, d) = jdn_to_ymd(jdn);
512 return Some(ymd_to_jdn(y, mo, d));
513 }
514
515 if m == "unixepoch" {
517 return Some(unix_to_jdn(jdn));
518 }
519
520 if m == "julianday" {
523 return Some(jdn);
524 }
525
526 if m == "auto" {
531 if (0.0..=AUTO_JDN_MAX).contains(&jdn) {
532 return Some(jdn);
533 }
534 if (AUTO_UNIX_MIN..=AUTO_UNIX_MAX).contains(&jdn) {
535 return Some(unix_to_jdn(jdn));
536 }
537 return None;
538 }
539
540 if m == "localtime" {
543 let offset = utc_offset_for_utc_jdn(jdn);
544 return Some(jdn + offset as f64 / 86400.0);
545 }
546 if m == "utc" {
549 let offset = utc_offset_for_local_jdn(jdn);
550 return Some(jdn - offset as f64 / 86400.0);
551 }
552
553 if m == "subsec" || m == "subsecond" {
556 return Some(jdn);
557 }
558
559 if let Some(rest) = m.strip_prefix("weekday ") {
561 let wd = rest.trim().parse::<i64>().ok()?;
562 if !(0..=6).contains(&wd) {
563 return None;
564 }
565 let current_jdn_int = (jdn + 0.5).floor() as i64;
567 let current_wd = (current_jdn_int + 1) % 7; let mut diff = wd - current_wd;
569 if diff < 0 {
570 diff += 7;
571 }
572 return Some(jdn + diff as f64);
574 }
575
576 parse_arithmetic_modifier(&m).map(|delta| jdn + delta)
578}
579
580fn parse_arithmetic_modifier(m: &str) -> Option<f64> {
582 let (sign, rest) = if let Some(r) = m.strip_prefix('+') {
583 (1.0, r.trim())
584 } else {
585 let r = m.strip_prefix('-')?;
586 (-1.0, r.trim())
587 };
588
589 let mut parts = rest.splitn(2, ' ');
590 let num_str = parts.next()?;
591 let unit = parts.next()?.trim();
592
593 let num = num_str.parse::<f64>().ok().filter(|f| f.is_finite())?;
595 let delta = num * sign;
596
597 match unit.trim_end_matches('s') {
598 "day" => Some(delta),
599 "hour" => Some(delta / 24.0),
600 "minute" => Some(delta / 1440.0),
601 "second" => Some(delta / 86400.0),
602 "month" => Some(apply_month_delta(delta)),
603 "year" => Some(apply_month_delta(delta * 12.0)),
604 _ => None,
605 }
606}
607
608fn apply_month_delta(months: f64) -> f64 {
613 months * 30.436875
615}
616
617fn has_outer_ascii_whitespace(value: &str) -> bool {
618 value
619 .as_bytes()
620 .first()
621 .is_some_and(u8::is_ascii_whitespace)
622 || value.as_bytes().last().is_some_and(u8::is_ascii_whitespace)
623}
624
625fn compute_floor(y: i64, m: i64, d: i64) -> i64 {
629 if d <= 28 {
630 0
631 } else if ((1_i64 << m) & 0x15aa) != 0 {
632 0
634 } else if m != 2 {
635 i64::from(d == 31)
637 } else if y % 4 != 0 || (y % 100 == 0 && y % 400 != 0) {
638 d - 28 } else {
640 d - 29 }
642}
643
644fn apply_modifiers(jdn: f64, modifiers: &[String], mut raw_numeric: bool) -> Option<(f64, bool)> {
646 let mut j = jdn;
647 let mut subsec = false;
648 let mut n_floor: i64 = 0;
651 for (index, m) in modifiers.iter().enumerate() {
652 if has_outer_ascii_whitespace(m) {
653 return None;
654 }
655 let m_lower = m.to_ascii_lowercase();
656 if matches!(m_lower.as_str(), "unixepoch" | "julianday" | "auto") {
657 if index != 0 || !raw_numeric {
661 return None;
662 }
663 raw_numeric = false;
664 } else if m_lower != "subsec" && m_lower != "subsecond" {
665 raw_numeric = false;
666 }
667 if m_lower == "subsec" || m_lower == "subsecond" {
668 subsec = true;
669 continue;
670 }
671 if m_lower == "ceiling" {
676 n_floor = 0;
677 continue;
678 }
679 if m_lower == "floor" {
680 j -= n_floor as f64;
681 n_floor = 0;
682 continue;
683 }
684 if is_month_year_modifier(&m_lower) {
690 match apply_month_year_exact(j, &m_lower) {
691 Ok(Some((new_jdn, nf))) => {
692 j = new_jdn;
693 n_floor = nf;
694 continue;
695 }
696 Ok(None) => return None,
697 Err(()) => {
698 }
700 }
701 }
702 n_floor = 0;
704 j = apply_modifier(j, m)?;
705 }
706 Some((j, subsec))
707}
708
709fn is_month_year_modifier(m: &str) -> bool {
710 (m.contains("month") || m.contains("year")) && (m.starts_with('+') || m.starts_with('-'))
711}
712
713fn apply_month_year_exact(jdn: f64, m: &str) -> std::result::Result<Option<(f64, i64)>, ()> {
719 let (sign, rest) = if let Some(r) = m.strip_prefix('+') {
720 (1_i64, r.trim())
721 } else if let Some(r) = m.strip_prefix('-') {
722 (-1_i64, r.trim())
723 } else {
724 return Err(());
725 };
726
727 let mut parts = rest.splitn(2, ' ');
728 let num_str = parts.next().ok_or(())?;
729 let unit = parts.next().ok_or(())?.trim();
730
731 let num = if let Ok(n) = num_str.parse::<i64>() {
733 n
734 } else if let Ok(f) = num_str.parse::<f64>() {
735 if f.fract() == 0.0 && f >= i64::MIN as f64 && f <= i64::MAX as f64 {
736 f as i64
737 } else {
738 return Err(());
739 }
740 } else {
741 return Err(());
742 };
743
744 let (y, mo, d) = jdn_to_ymd(jdn);
745 let (h, mi, s, frac) = jdn_to_hms(jdn);
746
747 let total_months = match unit.trim_end_matches('s') {
748 "month" => {
749 if let Some(val) = num.checked_mul(sign) {
750 val
751 } else {
752 return Ok(None);
753 }
754 }
755 "year" => {
756 if let Some(val) = num.checked_mul(sign).and_then(|v| v.checked_mul(12)) {
757 val
758 } else {
759 return Ok(None);
760 }
761 }
762 _ => return Err(()),
763 };
764
765 let current_months = if let Some(val) = y.checked_mul(12).and_then(|v| v.checked_add(mo - 1)) {
767 val
768 } else {
769 return Ok(None);
770 };
771 let new_total = if let Some(val) = current_months.checked_add(total_months) {
772 val
773 } else {
774 return Ok(None);
775 };
776
777 let new_y = new_total.div_euclid(12);
778 let new_mo = new_total.rem_euclid(12) + 1;
779 let n_floor = compute_floor(new_y, new_mo, d);
784 Ok(Some((
785 ymdhms_to_jdn(new_y, new_mo, d, h, mi, s, frac),
786 n_floor,
787 )))
788}
789
790struct StackStr {
796 buf: [u8; 48],
797 len: usize,
798}
799
800impl StackStr {
801 fn new() -> Self {
802 Self {
803 buf: [0; 48],
804 len: 0,
805 }
806 }
807
808 fn as_str(&self) -> &str {
809 core::str::from_utf8(&self.buf[..self.len]).unwrap_or("")
811 }
812}
813
814impl core::fmt::Write for StackStr {
815 fn write_str(&mut self, s: &str) -> core::fmt::Result {
816 let end = self.len + s.len();
817 if end > self.buf.len() {
818 return Err(core::fmt::Error);
819 }
820 self.buf[self.len..end].copy_from_slice(s.as_bytes());
821 self.len = end;
822 Ok(())
823 }
824}
825
826fn build_small_text(write: impl Fn(&mut dyn core::fmt::Write) -> core::fmt::Result) -> SmallText {
833 let mut buf = StackStr::new();
834 if write(&mut buf).is_ok() {
835 SmallText::new(buf.as_str())
836 } else {
837 let mut heap = String::new();
838 let _ = write(&mut heap);
839 SmallText::from_string(heap)
840 }
841}
842
843fn format_date(jdn: f64) -> SmallText {
844 let (y, m, d) = jdn_to_ymd(jdn);
845 build_small_text(move |w| write!(w, "{y:04}-{m:02}-{d:02}"))
846}
847
848#[derive(Clone, Copy)]
849struct UnmodifiedHms {
850 hour: i64,
851 minute: i64,
852 second: i64,
853 fraction: f64,
854}
855
856fn hms_for_output(jdn: f64, unmodified: Option<UnmodifiedHms>) -> UnmodifiedHms {
857 unmodified.unwrap_or_else(|| {
858 let (hour, minute, second, fraction) = jdn_to_hms(jdn);
859 UnmodifiedHms {
860 hour,
861 minute,
862 second,
863 fraction,
864 }
865 })
866}
867
868fn rounded_second_and_millis(hms: UnmodifiedHms) -> (i64, i64) {
869 let total_millis = ((hms.second as f64 + hms.fraction) * 1000.0 + 0.5).floor() as i64;
873 (total_millis / 1000, total_millis % 1000)
874}
875
876fn format_time(jdn: f64, subsec: bool, unmodified: Option<UnmodifiedHms>) -> SmallText {
877 let hms = hms_for_output(jdn, unmodified);
878 let (h, m) = (hms.hour, hms.minute);
879 if subsec {
880 let (s, ms) = rounded_second_and_millis(hms);
881 build_small_text(move |w| write!(w, "{h:02}:{m:02}:{s:02}.{ms:03}"))
882 } else {
883 let s = hms.second;
884 build_small_text(move |w| write!(w, "{h:02}:{m:02}:{s:02}"))
885 }
886}
887
888fn format_datetime(jdn: f64, subsec: bool, unmodified: Option<UnmodifiedHms>) -> SmallText {
889 let (y, mo, d) = jdn_to_ymd(jdn);
890 let hms = hms_for_output(jdn, unmodified);
891 let (h, mi) = (hms.hour, hms.minute);
892 if subsec {
893 let (s, ms) = rounded_second_and_millis(hms);
894 build_small_text(move |w| write!(w, "{y:04}-{mo:02}-{d:02} {h:02}:{mi:02}:{s:02}.{ms:03}"))
895 } else {
896 let s = hms.second;
897 build_small_text(move |w| write!(w, "{y:04}-{mo:02}-{d:02} {h:02}:{mi:02}:{s:02}"))
898 }
899}
900
901#[inline]
902fn push_format(result: &mut String, args: Arguments<'_>) {
903 let _ = result.write_fmt(args);
904}
905
906#[inline]
907fn push_zero_padded_2(result: &mut String, value: i64) {
908 if (0..=99).contains(&value) {
909 let value = value as u8;
910 result.push(char::from(b'0' + value / 10));
911 result.push(char::from(b'0' + value % 10));
912 } else {
913 push_format(result, format_args!("{value:02}"));
914 }
915}
916
917#[inline]
918fn push_space_padded_2(result: &mut String, value: i64) {
919 if (0..=99).contains(&value) {
920 let value = value as u8;
921 if value >= 10 {
922 result.push(char::from(b'0' + value / 10));
923 } else {
924 result.push(' ');
925 }
926 result.push(char::from(b'0' + value % 10));
927 } else {
928 push_format(result, format_args!("{value:>2}"));
929 }
930}
931
932#[inline]
933fn push_zero_padded_3(result: &mut String, value: i64) {
934 if (0..=999).contains(&value) {
935 let value = value as u16;
936 result.push(char::from(b'0' + (value / 100) as u8));
937 result.push(char::from(b'0' + ((value / 10) % 10) as u8));
938 result.push(char::from(b'0' + (value % 10) as u8));
939 } else {
940 push_format(result, format_args!("{value:03}"));
941 }
942}
943
944#[inline]
945fn push_zero_padded_4(result: &mut String, value: i64) {
946 if (0..=9999).contains(&value) {
947 let value = value as u16;
948 result.push(char::from(b'0' + (value / 1000) as u8));
949 result.push(char::from(b'0' + ((value / 100) % 10) as u8));
950 result.push(char::from(b'0' + ((value / 10) % 10) as u8));
951 result.push(char::from(b'0' + (value % 10) as u8));
952 } else {
953 push_format(result, format_args!("{value:04}"));
954 }
955}
956
957fn format_strftime(fmt: &str, jdn: f64, subsec: bool, unmodified: Option<UnmodifiedHms>) -> String {
959 let (y, mo, d) = jdn_to_ymd(jdn);
960 let hms = hms_for_output(jdn, unmodified);
961 let (h, mi, s, frac) = (hms.hour, hms.minute, hms.second, hms.fraction);
962 let doy = day_of_year(y, mo, d);
963 let jdn_int = (jdn + 0.5).floor() as i64;
965 let dow = (jdn_int + 1) % 7; let mut result = String::with_capacity(fmt.len().saturating_add(8));
968 let bytes = fmt.as_bytes();
969 let mut i = 0;
970 let mut literal_start = 0;
971
972 while i < bytes.len() {
973 if bytes[i] != b'%' || i + 1 >= bytes.len() {
974 i += 1;
975 continue;
976 }
977
978 result.push_str(&fmt[literal_start..i]);
979
980 let spec_suffix = &fmt[i + 1..];
981 let Some(spec) = spec_suffix.chars().next() else {
982 break;
983 };
984 i += 1 + spec.len_utf8();
985 literal_start = i;
986
987 match spec {
988 'd' => push_zero_padded_2(&mut result, d),
989 'e' => push_space_padded_2(&mut result, d),
990 'F' => {
991 push_zero_padded_4(&mut result, y);
993 result.push('-');
994 push_zero_padded_2(&mut result, mo);
995 result.push('-');
996 push_zero_padded_2(&mut result, d);
997 }
998 'f' => {
999 let total = (s as f64 + frac).min(59.999);
1001 push_format(&mut result, format_args!("{total:06.3}"));
1002 }
1003 'H' => push_zero_padded_2(&mut result, h),
1004 'I' => {
1005 let h12 = if h == 0 {
1007 12
1008 } else if h > 12 {
1009 h - 12
1010 } else {
1011 h
1012 };
1013 push_zero_padded_2(&mut result, h12);
1014 }
1015 'j' => push_zero_padded_3(&mut result, doy),
1016 'J' => {
1017 push_format(&mut result, format_args!("{jdn:.15}"));
1019 while result.as_bytes().last() == Some(&b'0') {
1020 result.pop();
1021 }
1022 if result.as_bytes().last() == Some(&b'.') {
1023 result.pop();
1024 }
1025 }
1026 'k' => {
1027 push_space_padded_2(&mut result, h);
1029 }
1030 'l' => {
1031 let h12 = if h == 0 {
1033 12
1034 } else if h > 12 {
1035 h - 12
1036 } else {
1037 h
1038 };
1039 push_space_padded_2(&mut result, h12);
1040 }
1041 'm' => push_zero_padded_2(&mut result, mo),
1042 'M' => push_zero_padded_2(&mut result, mi),
1043 'p' => {
1044 result.push_str(if h < 12 { "AM" } else { "PM" });
1045 }
1046 'P' => {
1047 result.push_str(if h < 12 { "am" } else { "pm" });
1048 }
1049 'R' => {
1050 push_zero_padded_2(&mut result, h);
1051 result.push(':');
1052 push_zero_padded_2(&mut result, mi);
1053 }
1054 's' => {
1055 if subsec {
1056 let unix = jdn_to_unix_subsec(jdn);
1057 push_format(&mut result, format_args!("{unix:.3}"));
1058 } else {
1059 let unix = jdn_to_unix(jdn);
1060 push_format(&mut result, format_args!("{unix}"));
1061 }
1062 }
1063 'S' => push_zero_padded_2(&mut result, s),
1064 'T' => {
1065 push_zero_padded_2(&mut result, h);
1066 result.push(':');
1067 push_zero_padded_2(&mut result, mi);
1068 result.push(':');
1069 push_zero_padded_2(&mut result, s);
1070 }
1071 'u' => {
1072 let u = if dow == 0 { 7 } else { dow };
1074 push_format(&mut result, format_args!("{u}"));
1075 }
1076 'w' => push_format(&mut result, format_args!("{dow}")),
1077 'W' => {
1078 let w = (doy + 6 - ((dow + 6) % 7)) / 7;
1080 push_zero_padded_2(&mut result, w);
1081 }
1082 'Y' => push_zero_padded_4(&mut result, y),
1083 'G' | 'g' | 'V' => {
1084 let (iso_y, iso_w) = iso_week(y, mo, d);
1086 match spec {
1087 'G' => push_zero_padded_4(&mut result, iso_y),
1088 'g' => push_zero_padded_2(&mut result, iso_y % 100),
1089 'V' => push_zero_padded_2(&mut result, iso_w),
1090 _ => unreachable!(),
1091 }
1092 }
1093 '%' => result.push('%'),
1094 other => {
1095 result.push('%');
1096 result.push(other);
1097 }
1098 }
1099 }
1100
1101 if literal_start < fmt.len() {
1102 result.push_str(&fmt[literal_start..]);
1103 }
1104
1105 result
1106}
1107
1108fn iso_week(y: i64, m: i64, d: i64) -> (i64, i64) {
1110 let jdn = ymd_to_jdn(y, m, d);
1111 let jdn_int = (jdn + 0.5).floor() as i64;
1112 let dow = (jdn_int + 1) % 7;
1114 let iso_dow = if dow == 0 { 7 } else { dow };
1115
1116 let thu_jdn = jdn_int + (4 - iso_dow);
1118 let (thu_y, _, _) = jdn_to_ymd(thu_jdn as f64);
1119
1120 let jan4_jdn = (ymd_to_jdn(thu_y, 1, 4) + 0.5).floor() as i64;
1122 let jan4_dow = (jan4_jdn + 1) % 7;
1123 let jan4_iso_dow = if jan4_dow == 0 { 7 } else { jan4_dow };
1124 let week1_start = jan4_jdn - (jan4_iso_dow - 1);
1125
1126 let week = (thu_jdn - week1_start) / 7 + 1;
1127 (thu_y, week)
1128}
1129
1130fn timediff_impl(jdn1: f64, jdn2: f64) -> String {
1133 let (sign, start_jdn, end_jdn) = if jdn1 >= jdn2 {
1134 ('+', jdn2, jdn1)
1135 } else {
1136 ('-', jdn1, jdn2)
1137 };
1138
1139 let (start_y, start_mo, start_d) = jdn_to_ymd(start_jdn);
1140 let (start_h, start_mi, mut start_s, start_frac) = jdn_to_hms(start_jdn);
1141 let mut start_ms = (start_frac * 1000.0).round() as i64;
1142 if start_ms >= 1000 {
1143 start_ms = 0;
1144 start_s += 1;
1145 }
1146
1147 let (end_y, end_mo, end_d) = jdn_to_ymd(end_jdn);
1148 let (end_h, end_mi, mut end_s, end_frac) = jdn_to_hms(end_jdn);
1149 let mut end_ms = (end_frac * 1000.0).round() as i64;
1150 if end_ms >= 1000 {
1151 end_ms = 0;
1152 end_s += 1;
1153 }
1154
1155 let mut years = end_y - start_y;
1156 let mut months = end_mo - start_mo;
1157 let mut days = end_d - start_d;
1158 let mut hours = end_h - start_h;
1159 let mut minutes = end_mi - start_mi;
1160 let mut seconds = end_s - start_s;
1161 let mut millis = end_ms - start_ms;
1162
1163 if millis < 0 {
1164 millis += 1000;
1165 seconds -= 1;
1166 }
1167 if seconds < 0 {
1168 seconds += 60;
1169 minutes -= 1;
1170 }
1171 if minutes < 0 {
1172 minutes += 60;
1173 hours -= 1;
1174 }
1175 if hours < 0 {
1176 hours += 24;
1177 days -= 1;
1178 }
1179 if days < 0 {
1180 months -= 1;
1181 let (borrow_y, borrow_mo) = if end_mo == 1 {
1182 (end_y - 1, 12)
1183 } else {
1184 (end_y, end_mo - 1)
1185 };
1186 days += days_in_month(borrow_y, borrow_mo);
1187 }
1188 if months < 0 {
1189 months += 12;
1190 years -= 1;
1191 }
1192
1193 format!(
1194 "{sign}{years:04}-{months:02}-{days:02} {hours:02}:{minutes:02}:{seconds:02}.{millis:03}"
1195 )
1196}
1197
1198#[must_use]
1225pub fn is_datetime_invocation_safe_for_schema(function_name: &str, args: &[SqliteValue]) -> bool {
1226 if function_name.eq_ignore_ascii_case("strftime") {
1227 return args.first().is_none_or(SqliteValue::is_null)
1231 || datetime_arguments_are_schema_safe(&args[1..]);
1232 }
1233
1234 if function_name.eq_ignore_ascii_case("timediff") {
1235 if args.len() != 2 {
1239 return true;
1240 }
1241 for time_value in args {
1242 match classify_time_value_for_schema(time_value) {
1243 SchemaTimeValue::Dynamic => return false,
1244 SchemaTimeValue::NullOrInvalid => return true,
1245 SchemaTimeValue::Fixed { .. } => {}
1246 }
1247 }
1248 return true;
1249 }
1250
1251 if function_name.eq_ignore_ascii_case("date")
1252 || function_name.eq_ignore_ascii_case("time")
1253 || function_name.eq_ignore_ascii_case("datetime")
1254 || function_name.eq_ignore_ascii_case("julianday")
1255 || function_name.eq_ignore_ascii_case("unixepoch")
1256 {
1257 return datetime_arguments_are_schema_safe(args);
1258 }
1259
1260 true
1261}
1262
1263fn datetime_arguments_are_schema_safe(args: &[SqliteValue]) -> bool {
1265 let Some(time_value) = args.first() else {
1266 return false;
1267 };
1268 let (input, raw_numeric) = match classify_time_value_for_schema(time_value) {
1269 SchemaTimeValue::Dynamic => return false,
1270 SchemaTimeValue::NullOrInvalid => return true,
1271 SchemaTimeValue::Fixed { input, raw_numeric } => (input, raw_numeric),
1272 };
1273
1274 let mut reached_modifiers = Vec::with_capacity(args.len().saturating_sub(1));
1275 for modifier in &args[1..] {
1276 if modifier.is_null() {
1277 return true;
1278 }
1279 if sqlite_value_is_keyword(modifier, "localtime")
1280 || sqlite_value_is_keyword(modifier, "utc")
1281 {
1282 return false;
1283 }
1284 let Some(modifier) = sqlite_value_datetime_text(modifier) else {
1285 return true;
1286 };
1287 reached_modifiers.push(modifier.into_owned());
1288 if apply_modifiers(input, &reached_modifiers, raw_numeric).is_none() {
1289 return true;
1292 }
1293 }
1294 true
1295}
1296
1297enum SchemaTimeValue {
1298 Dynamic,
1299 NullOrInvalid,
1300 Fixed { input: f64, raw_numeric: bool },
1301}
1302
1303fn classify_time_value_for_schema(value: &SqliteValue) -> SchemaTimeValue {
1304 if value.is_null() {
1305 return SchemaTimeValue::NullOrInvalid;
1306 }
1307 if is_implicit_now_time_value(value) {
1308 return SchemaTimeValue::Dynamic;
1309 }
1310 match parse_time_value(value) {
1311 Some(parsed) => SchemaTimeValue::Fixed {
1312 input: parsed.jdn,
1313 raw_numeric: parsed.raw_numeric,
1314 },
1315 None => SchemaTimeValue::NullOrInvalid,
1316 }
1317}
1318
1319fn is_implicit_now_time_value(value: &SqliteValue) -> bool {
1320 sqlite_value_is_keyword(value, "now")
1321 || sqlite_value_is_keyword(value, "subsec")
1322 || sqlite_value_is_keyword(value, "subsecond")
1323}
1324
1325fn sqlite_value_is_keyword(value: &SqliteValue, keyword: &str) -> bool {
1329 let bytes = match value {
1330 SqliteValue::Text(text) => sqlite_c_string_bytes(text.as_bytes_direct()),
1331 SqliteValue::Blob(bytes) => sqlite_c_string_bytes(bytes),
1332 SqliteValue::Null | SqliteValue::Integer(_) | SqliteValue::Float(_) => return false,
1333 };
1334 bytes.eq_ignore_ascii_case(keyword.as_bytes())
1335}
1336
1337#[derive(Clone, Copy)]
1338struct ParsedTimeValue {
1339 jdn: f64,
1340 raw_numeric: bool,
1341 unmodified_hms: Option<UnmodifiedHms>,
1342}
1343
1344fn parse_time_value(value: &SqliteValue) -> Option<ParsedTimeValue> {
1345 match value {
1346 SqliteValue::Null => None,
1347 SqliteValue::Integer(integer) => Some(ParsedTimeValue {
1348 jdn: *integer as f64,
1349 raw_numeric: true,
1350 unmodified_hms: None,
1351 }),
1352 SqliteValue::Float(float) if float.is_finite() => Some(ParsedTimeValue {
1353 jdn: *float,
1354 raw_numeric: true,
1355 unmodified_hms: None,
1356 }),
1357 SqliteValue::Float(_) => None,
1358 SqliteValue::Text(_) | SqliteValue::Blob(_) => {
1359 let text = sqlite_value_datetime_text(value)?;
1360 let numeric = text.trim_matches(|c: char| c.is_ascii_whitespace());
1361 if let Ok(number) = numeric.parse::<f64>()
1362 && number.is_finite()
1363 {
1364 return Some(ParsedTimeValue {
1365 jdn: number,
1366 raw_numeric: true,
1367 unmodified_hms: None,
1368 });
1369 }
1370 Some(ParsedTimeValue {
1371 jdn: parse_timestring(text.as_ref())?,
1372 raw_numeric: false,
1373 unmodified_hms: unmodified_hms_from_timestring(text.as_ref()),
1374 })
1375 }
1376 }
1377}
1378
1379fn unmodified_hms_from_timestring(value: &str) -> Option<UnmodifiedHms> {
1380 let value = sqlite_c_string_str(value).trim_end_matches(|c: char| c.is_ascii_whitespace());
1381 let time = if value.len() > 10
1382 && value.as_bytes().get(4) == Some(&b'-')
1383 && value.as_bytes().get(7) == Some(&b'-')
1384 && value
1385 .as_bytes()
1386 .get(10)
1387 .is_some_and(|separator| matches!(*separator, b' ' | b'T'))
1388 {
1389 &value[11..]
1390 } else if value.len() >= 5 && value.as_bytes().get(2) == Some(&b':') {
1391 value
1392 } else {
1393 return None;
1394 };
1395 let (hour, minute, second, fraction, timezone_offset) = parse_time_part_with_tz(time)?;
1396 (timezone_offset == 0).then_some(UnmodifiedHms {
1397 hour,
1398 minute,
1399 second,
1400 fraction,
1401 })
1402}
1403
1404struct ParsedDateTimeArgs {
1405 jdn: f64,
1406 subsec: bool,
1407 unmodified_hms: Option<UnmodifiedHms>,
1408}
1409
1410fn parse_args(args: &[SqliteValue]) -> Option<ParsedDateTimeArgs> {
1412 let first_position_subsec = args.first().is_some_and(|value| {
1413 sqlite_value_is_keyword(value, "subsec") || sqlite_value_is_keyword(value, "subsecond")
1414 });
1415 let parsed = match args.first() {
1416 None => ParsedTimeValue {
1417 jdn: current_time_jdn(),
1418 raw_numeric: false,
1419 unmodified_hms: None,
1420 },
1421 Some(value) => parse_time_value(value)?,
1422 };
1423
1424 let mut modifiers = Vec::with_capacity(args.len().saturating_sub(1));
1425 for modifier in args.get(1..).unwrap_or_default() {
1426 modifiers.push(sqlite_value_datetime_text(modifier)?.into_owned());
1427 }
1428
1429 if parsed.raw_numeric {
1434 let first = modifiers
1435 .first()
1436 .map(|modifier| modifier.to_ascii_lowercase());
1437 let reinterprets_raw = matches!(first.as_deref(), Some("unixepoch" | "julianday" | "auto"));
1438 if !reinterprets_raw && !(0.0..=AUTO_JDN_MAX).contains(&parsed.jdn) {
1439 return None;
1440 }
1441 }
1442
1443 let preserves_input_hms = modifiers.iter().all(|modifier| {
1444 modifier.eq_ignore_ascii_case("subsec") || modifier.eq_ignore_ascii_case("subsecond")
1445 });
1446 let (jdn, modifier_subsec) = apply_modifiers(parsed.jdn, &modifiers, parsed.raw_numeric)?;
1447 Some(ParsedDateTimeArgs {
1448 jdn,
1449 subsec: first_position_subsec || modifier_subsec,
1450 unmodified_hms: preserves_input_hms
1451 .then_some(parsed.unmodified_hms)
1452 .flatten(),
1453 })
1454}
1455
1456pub struct DateFunc;
1459
1460impl ScalarFunction for DateFunc {
1461 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1462 match parse_args(args) {
1463 Some(parsed) => Ok(SqliteValue::Text(format_date(parsed.jdn))),
1464 None => Ok(SqliteValue::Null),
1465 }
1466 }
1467
1468 fn num_args(&self) -> i32 {
1469 -1
1470 }
1471
1472 fn is_deterministic(&self) -> bool {
1473 false
1474 }
1475
1476 fn name(&self) -> &str {
1477 "date"
1478 }
1479}
1480
1481pub struct TimeFunc;
1484
1485impl ScalarFunction for TimeFunc {
1486 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1487 match parse_args(args) {
1488 Some(parsed) => Ok(SqliteValue::Text(format_time(
1489 parsed.jdn,
1490 parsed.subsec,
1491 parsed.unmodified_hms,
1492 ))),
1493 None => Ok(SqliteValue::Null),
1494 }
1495 }
1496
1497 fn num_args(&self) -> i32 {
1498 -1
1499 }
1500
1501 fn is_deterministic(&self) -> bool {
1502 false
1503 }
1504
1505 fn name(&self) -> &str {
1506 "time"
1507 }
1508}
1509
1510pub struct DateTimeFunc;
1513
1514impl ScalarFunction for DateTimeFunc {
1515 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1516 match parse_args(args) {
1517 Some(parsed) => Ok(SqliteValue::Text(format_datetime(
1518 parsed.jdn,
1519 parsed.subsec,
1520 parsed.unmodified_hms,
1521 ))),
1522 None => Ok(SqliteValue::Null),
1523 }
1524 }
1525
1526 fn num_args(&self) -> i32 {
1527 -1
1528 }
1529
1530 fn is_deterministic(&self) -> bool {
1531 false
1532 }
1533
1534 fn name(&self) -> &str {
1535 "datetime"
1536 }
1537}
1538
1539pub struct JuliandayFunc;
1542
1543impl ScalarFunction for JuliandayFunc {
1544 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1545 match parse_args(args) {
1546 Some(parsed) => Ok(SqliteValue::Float(parsed.jdn)),
1547 None => Ok(SqliteValue::Null),
1548 }
1549 }
1550
1551 fn num_args(&self) -> i32 {
1552 -1
1553 }
1554
1555 fn is_deterministic(&self) -> bool {
1556 false
1557 }
1558
1559 fn name(&self) -> &str {
1560 "julianday"
1561 }
1562}
1563
1564pub struct UnixepochFunc;
1567
1568impl ScalarFunction for UnixepochFunc {
1569 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1570 match parse_args(args) {
1571 Some(parsed) if parsed.subsec => Ok(SqliteValue::Float(jdn_to_unix_subsec(parsed.jdn))),
1574 Some(parsed) => Ok(SqliteValue::Integer(jdn_to_unix(parsed.jdn))),
1575 None => Ok(SqliteValue::Null),
1576 }
1577 }
1578
1579 fn num_args(&self) -> i32 {
1580 -1
1581 }
1582
1583 fn is_deterministic(&self) -> bool {
1584 false
1585 }
1586
1587 fn name(&self) -> &str {
1588 "unixepoch"
1589 }
1590}
1591
1592pub struct StrftimeFunc;
1595
1596impl ScalarFunction for StrftimeFunc {
1597 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1598 let Some(format_value) = args.first() else {
1599 return Ok(SqliteValue::Null);
1600 };
1601 let Some(fmt) = sqlite_value_datetime_text(format_value) else {
1602 return Ok(SqliteValue::Null);
1603 };
1604 let rest = &args[1..];
1605 match parse_args(rest) {
1606 Some(parsed) => Ok(SqliteValue::Text(
1607 format_strftime(
1608 fmt.as_ref(),
1609 parsed.jdn,
1610 parsed.subsec,
1611 parsed.unmodified_hms,
1612 )
1613 .into(),
1614 )),
1615 None => Ok(SqliteValue::Null),
1616 }
1617 }
1618
1619 fn num_args(&self) -> i32 {
1620 -1
1621 }
1622
1623 fn is_deterministic(&self) -> bool {
1624 false
1625 }
1626
1627 fn name(&self) -> &str {
1628 "strftime"
1629 }
1630}
1631
1632pub struct TimediffFunc;
1635
1636impl ScalarFunction for TimediffFunc {
1637 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1638 if args.len() < 2 || args[0].is_null() || args[1].is_null() {
1639 return Ok(SqliteValue::Null);
1640 }
1641
1642 let jdn1 = parse_time_value(&args[0])
1643 .map(|parsed| parsed.jdn)
1644 .filter(|jdn| (0.0..=AUTO_JDN_MAX).contains(jdn));
1645 let jdn2 = parse_time_value(&args[1])
1646 .map(|parsed| parsed.jdn)
1647 .filter(|jdn| (0.0..=AUTO_JDN_MAX).contains(jdn));
1648
1649 match (jdn1, jdn2) {
1650 (Some(j1), Some(j2)) => Ok(SqliteValue::Text(timediff_impl(j1, j2).into())),
1651 _ => Ok(SqliteValue::Null),
1652 }
1653 }
1654
1655 fn num_args(&self) -> i32 {
1656 2
1657 }
1658
1659 fn is_deterministic(&self) -> bool {
1660 false
1661 }
1662
1663 fn name(&self) -> &str {
1664 "timediff"
1665 }
1666}
1667
1668pub fn register_datetime_builtins(registry: &mut FunctionRegistry) {
1672 registry.register_conditionally_deterministic_scalar(DateFunc);
1673 registry.register_conditionally_deterministic_scalar(TimeFunc);
1674 registry.register_conditionally_deterministic_scalar(DateTimeFunc);
1675 registry.register_conditionally_deterministic_scalar(JuliandayFunc);
1676 registry.register_conditionally_deterministic_scalar(UnixepochFunc);
1677 registry.register_conditionally_deterministic_scalar(StrftimeFunc);
1678 registry.register_conditionally_deterministic_scalar(TimediffFunc);
1679}
1680
1681#[cfg(test)]
1684mod tests {
1685 use super::*;
1686
1687 fn text(s: &str) -> SqliteValue {
1688 SqliteValue::Text(s.into())
1689 }
1690
1691 fn int(v: i64) -> SqliteValue {
1692 SqliteValue::Integer(v)
1693 }
1694
1695 fn float(v: f64) -> SqliteValue {
1696 SqliteValue::Float(v)
1697 }
1698
1699 fn null() -> SqliteValue {
1700 SqliteValue::Null
1701 }
1702
1703 fn assert_text(result: &SqliteValue, expected: &str) {
1704 match result {
1705 SqliteValue::Text(s) => assert_eq!(s.as_ref(), expected, "text mismatch"),
1706 other => panic!("expected Text(\"{expected}\"), got {other:?}"),
1707 }
1708 }
1709
1710 fn blob(s: &str) -> SqliteValue {
1713 SqliteValue::Blob(s.as_bytes().into())
1714 }
1715
1716 #[test]
1717 fn test_schema_safety_common_datetime_argument_layout() {
1718 for name in ["date", "time", "datetime", "julianday", "unixepoch"] {
1719 assert!(
1720 !is_datetime_invocation_safe_for_schema(name, &[]),
1721 "{name}() implicitly reads the current time"
1722 );
1723 assert!(is_datetime_invocation_safe_for_schema(
1724 name,
1725 &[text("2024-03-15 12:34:56")]
1726 ));
1727 assert!(is_datetime_invocation_safe_for_schema(name, &[int(0)]));
1728 assert!(is_datetime_invocation_safe_for_schema(
1729 name,
1730 &[float(2_460_384.5)]
1731 ));
1732 assert!(is_datetime_invocation_safe_for_schema(name, &[null()]));
1733
1734 for current_time in ["now", "NOW", "subsec", "SUBSECOND"] {
1735 assert!(
1736 !is_datetime_invocation_safe_for_schema(name, &[text(current_time)]),
1737 "{name}({current_time:?}) must be conditional"
1738 );
1739 }
1740 assert!(!is_datetime_invocation_safe_for_schema(
1741 name,
1742 &[blob("NOW")]
1743 ));
1744 assert!(!is_datetime_invocation_safe_for_schema(
1745 name,
1746 &[text("now\0ignored")]
1747 ));
1748 assert!(!is_datetime_invocation_safe_for_schema(
1749 name,
1750 &[SqliteValue::Blob(b"subsec\0\xff".as_slice().into())]
1751 ));
1752 for invalid_padded in [" now ", "subsec ", " subsecond"] {
1753 assert!(is_datetime_invocation_safe_for_schema(
1754 name,
1755 &[text(invalid_padded)]
1756 ));
1757 }
1758 assert!(is_datetime_invocation_safe_for_schema(
1759 name,
1760 &[blob(" NoW ")]
1761 ));
1762 assert!(is_datetime_invocation_safe_for_schema(
1763 name,
1764 &[text("nowhere")]
1765 ));
1766
1767 assert!(is_datetime_invocation_safe_for_schema(
1769 name,
1770 &[text("localtime")]
1771 ));
1772 assert!(is_datetime_invocation_safe_for_schema(name, &[text("utc")]));
1773 assert!(is_datetime_invocation_safe_for_schema(
1774 name,
1775 &[text("2024-03-15"), text("subsec")]
1776 ));
1777 assert!(is_datetime_invocation_safe_for_schema(
1778 name,
1779 &[text("2024-03-15"), text("subsecond")]
1780 ));
1781
1782 for modifier in ["localtime", "LOCALTIME", "utc"] {
1783 assert!(
1784 !is_datetime_invocation_safe_for_schema(
1785 name,
1786 &[text("2024-03-15"), text(modifier)]
1787 ),
1788 "{name} modifier {modifier:?} depends on host-local state"
1789 );
1790 }
1791 assert!(!is_datetime_invocation_safe_for_schema(
1792 name,
1793 &[text("2024-03-15"), blob("UTC")]
1794 ));
1795 assert!(!is_datetime_invocation_safe_for_schema(
1796 name,
1797 &[text("2024-03-15"), text("localtime\0ignored")]
1798 ));
1799 assert!(is_datetime_invocation_safe_for_schema(
1800 name,
1801 &[text("2024-03-15"), text(" utc ")]
1802 ));
1803
1804 assert!(is_datetime_invocation_safe_for_schema(
1806 name,
1807 &[text("2024-03-15"), null(), text("localtime")]
1808 ));
1809 assert!(!is_datetime_invocation_safe_for_schema(
1810 name,
1811 &[text("2024-03-15"), text("localtime"), null()]
1812 ));
1813
1814 assert!(is_datetime_invocation_safe_for_schema(
1817 name,
1818 &[text("bogus"), text("localtime")]
1819 ));
1820 assert!(is_datetime_invocation_safe_for_schema(
1821 name,
1822 &[text("2000-01-01"), text("bogus"), text("localtime")]
1823 ));
1824 }
1825 }
1826
1827 #[test]
1828 fn test_schema_safety_strftime_uses_shifted_time_arguments() {
1829 assert!(is_datetime_invocation_safe_for_schema("strftime", &[]));
1830 assert!(is_datetime_invocation_safe_for_schema(
1831 "strftime",
1832 &[null()]
1833 ));
1834 assert!(is_datetime_invocation_safe_for_schema(
1835 "strftime",
1836 &[null(), text("now")]
1837 ));
1838
1839 assert!(!is_datetime_invocation_safe_for_schema(
1841 "strftime",
1842 &[text("%Y")]
1843 ));
1844 assert!(!is_datetime_invocation_safe_for_schema(
1845 "STRFTIME",
1846 &[text("%Y"), text("now")]
1847 ));
1848 assert!(!is_datetime_invocation_safe_for_schema(
1849 "strftime",
1850 &[text("%s"), text("subsecond")]
1851 ));
1852
1853 assert!(is_datetime_invocation_safe_for_schema(
1855 "strftime",
1856 &[text("now localtime utc"), text("2024-03-15")]
1857 ));
1858 assert!(is_datetime_invocation_safe_for_schema(
1859 "strftime",
1860 &[text("%Y"), int(0), text("unixepoch")]
1861 ));
1862 assert!(is_datetime_invocation_safe_for_schema(
1863 "strftime",
1864 &[text("%f"), text("2024-03-15"), text("subsec")]
1865 ));
1866 assert!(!is_datetime_invocation_safe_for_schema(
1867 "strftime",
1868 &[text("%Y"), text("2024-03-15"), text("localtime")]
1869 ));
1870 assert!(is_datetime_invocation_safe_for_schema(
1871 "strftime",
1872 &[text("%Y"), text("2024-03-15"), null(), text("localtime")]
1873 ));
1874 }
1875
1876 #[test]
1877 fn test_schema_safety_timediff_treats_both_inputs_as_time_values() {
1878 assert!(is_datetime_invocation_safe_for_schema("timediff", &[]));
1879 assert!(is_datetime_invocation_safe_for_schema(
1880 "timediff",
1881 &[text("2024-03-15")]
1882 ));
1883 assert!(is_datetime_invocation_safe_for_schema(
1884 "timediff",
1885 &[text("2024-03-15"), text("2024-03-14")]
1886 ));
1887 assert!(is_datetime_invocation_safe_for_schema(
1888 "timediff",
1889 &[int(2_460_384), float(2_460_383.5)]
1890 ));
1891
1892 for current_time in ["now", "subsec", "subsecond"] {
1893 assert!(!is_datetime_invocation_safe_for_schema(
1894 "timediff",
1895 &[text(current_time), text("2024-03-14")]
1896 ));
1897 assert!(!is_datetime_invocation_safe_for_schema(
1898 "timediff",
1899 &[text("2024-03-15"), text(current_time)]
1900 ));
1901 }
1902
1903 assert!(is_datetime_invocation_safe_for_schema(
1906 "timediff",
1907 &[text("localtime"), text("utc")]
1908 ));
1909 assert!(is_datetime_invocation_safe_for_schema(
1910 "timediff",
1911 &[null(), text("now")]
1912 ));
1913 assert!(is_datetime_invocation_safe_for_schema(
1914 "timediff",
1915 &[text("bogus"), text("now")]
1916 ));
1917 assert!(!is_datetime_invocation_safe_for_schema(
1918 "timediff",
1919 &[blob("NOW"), null()]
1920 ));
1921 }
1922
1923 #[test]
1924 fn test_schema_safety_ignores_non_datetime_function_names() {
1925 for name in ["", "my_date", "current_date", "date ", "random"] {
1926 assert!(is_datetime_invocation_safe_for_schema(
1927 name,
1928 &[text("now"), text("localtime")]
1929 ));
1930 }
1931 }
1932
1933 #[test]
1936 fn test_omitted_time_value_uses_current_time() {
1937 let date = DateFunc.invoke(&[]).unwrap();
1938 let time = TimeFunc.invoke(&[]).unwrap();
1939 let datetime = DateTimeFunc.invoke(&[]).unwrap();
1940 let julianday = JuliandayFunc.invoke(&[]).unwrap();
1941 let unixepoch = UnixepochFunc.invoke(&[]).unwrap();
1942 let year = StrftimeFunc.invoke(&[text("%Y")]).unwrap();
1943
1944 assert!(matches!(&date, SqliteValue::Text(value) if value.len() == 10));
1945 assert!(matches!(&time, SqliteValue::Text(value) if value.len() == 8));
1946 assert!(matches!(&datetime, SqliteValue::Text(value) if value.len() == 19));
1947 assert!(matches!(julianday, SqliteValue::Float(_)));
1948 assert!(matches!(unixepoch, SqliteValue::Integer(_)));
1949 assert!(matches!(&year, SqliteValue::Text(value)
1950 if value.len() == 4 && value.as_bytes().iter().all(u8::is_ascii_digit)));
1951
1952 assert_eq!(StrftimeFunc.invoke(&[]).unwrap(), SqliteValue::Null);
1953 assert_eq!(StrftimeFunc.invoke(&[null()]).unwrap(), SqliteValue::Null);
1954 }
1955
1956 #[test]
1957 fn test_first_position_subsec_aliases_use_current_time() {
1958 for alias in ["subsec", "subsecond"] {
1959 assert!(matches!(
1960 DateFunc.invoke(&[text(alias)]).unwrap(),
1961 SqliteValue::Text(value) if value.len() == 10
1962 ));
1963 assert!(matches!(
1964 TimeFunc.invoke(&[text(alias)]).unwrap(),
1965 SqliteValue::Text(value)
1966 if value.len() == 12 && value.as_bytes_direct()[8] == b'.'
1967 ));
1968 assert!(matches!(
1969 DateTimeFunc.invoke(&[text(alias)]).unwrap(),
1970 SqliteValue::Text(value)
1971 if value.len() == 23 && value.as_bytes_direct()[19] == b'.'
1972 ));
1973 assert!(matches!(
1974 JuliandayFunc.invoke(&[text(alias)]).unwrap(),
1975 SqliteValue::Float(_)
1976 ));
1977 assert!(matches!(
1978 UnixepochFunc.invoke(&[text(alias)]).unwrap(),
1979 SqliteValue::Float(_)
1980 ));
1981 assert!(matches!(
1982 StrftimeFunc.invoke(&[text("%s"), text(alias)]).unwrap(),
1983 SqliteValue::Text(value)
1984 if value.rsplit_once('.').is_some_and(|(_, fraction)| fraction.len() == 3)
1985 ));
1986 }
1987 }
1988
1989 #[test]
1990 fn test_padded_and_nul_terminated_special_values() {
1991 for invalid in [" now ", "subsec ", " subsecond"] {
1992 assert_eq!(
1993 DateFunc.invoke(&[text(invalid)]).unwrap(),
1994 SqliteValue::Null
1995 );
1996 }
1997 assert_eq!(
1998 DateFunc
1999 .invoke(&[text("2000-01-01"), text(" localtime ")])
2000 .unwrap(),
2001 SqliteValue::Null
2002 );
2003 assert!(matches!(
2004 DateFunc.invoke(&[text("now\0ignored")]).unwrap(),
2005 SqliteValue::Text(value) if value.len() == 10
2006 ));
2007 assert!(matches!(
2008 TimeFunc
2009 .invoke(&[SqliteValue::Blob(b"subsec\0\xff".as_slice().into())])
2010 .unwrap(),
2011 SqliteValue::Text(value) if value.len() == 12
2012 ));
2013 }
2014
2015 #[test]
2016 fn test_date_basic() {
2017 let r = DateFunc.invoke(&[text("2024-03-15 14:30:00")]).unwrap();
2018 assert_text(&r, "2024-03-15");
2019 }
2020
2021 #[test]
2022 fn test_time_basic() {
2023 let r = TimeFunc.invoke(&[text("2024-03-15 14:30:45")]).unwrap();
2024 assert_text(&r, "14:30:45");
2025 }
2026
2027 #[test]
2028 fn test_datetime_basic() {
2029 let r = DateTimeFunc.invoke(&[text("2024-03-15 14:30:00")]).unwrap();
2030 assert_text(&r, "2024-03-15 14:30:00");
2031 }
2032
2033 #[test]
2034 fn test_julianday_basic() {
2035 let r = JuliandayFunc.invoke(&[text("2024-03-15")]).unwrap();
2036 match r {
2037 SqliteValue::Float(jdn) => {
2038 assert!((jdn - 2_460_384.5).abs() < 0.01, "unexpected JDN: {jdn}");
2040 }
2041 other => panic!("expected Float, got {other:?}"),
2042 }
2043 }
2044
2045 fn julianday_float(input: &str) -> f64 {
2053 match JuliandayFunc.invoke(&[text(input)]).unwrap() {
2054 SqliteValue::Float(v) => v,
2055 other => panic!("expected Float, got {other:?} for input {input:?}"),
2056 }
2057 }
2058
2059 fn assert_jdn_close(actual: f64, expected: f64, ctx: &str) {
2060 assert!(
2062 (actual - expected).abs() < 1e-6,
2063 "JDN mismatch for {ctx}: got {actual}, expected {expected}"
2064 );
2065 }
2066
2067 #[test]
2068 fn test_julianday_rfc3339_z_suffix() {
2069 let naive = julianday_float("2026-04-07 16:00:00");
2071 assert_jdn_close(julianday_float("2026-04-07T16:00:00Z"), naive, "T...Z");
2072 assert_jdn_close(
2073 julianday_float("2026-04-07T16:00:00z"),
2074 naive,
2075 "lowercase z",
2076 );
2077 }
2078
2079 #[test]
2080 fn test_julianday_rfc3339_zero_offset() {
2081 let naive = julianday_float("2026-04-07 16:00:00");
2082 assert_jdn_close(
2083 julianday_float("2026-04-07T16:00:00+00:00"),
2084 naive,
2085 "+00:00",
2086 );
2087 assert_jdn_close(
2088 julianday_float("2026-04-07T16:00:00-00:00"),
2089 naive,
2090 "-00:00",
2091 );
2092 }
2093
2094 #[test]
2095 fn test_julianday_rfc3339_positive_offset() {
2096 let base = julianday_float("2026-04-07 16:00:00");
2098 let expected = base - 1.0 / 24.0;
2099 assert_jdn_close(
2100 julianday_float("2026-04-07T16:00:00+01:00"),
2101 expected,
2102 "+01:00",
2103 );
2104 }
2105
2106 #[test]
2107 fn test_julianday_rfc3339_negative_offset() {
2108 let base = julianday_float("2026-04-07 16:00:00");
2110 let expected = base + 5.0 / 24.0;
2111 assert_jdn_close(
2112 julianday_float("2026-04-07T16:00:00-05:00"),
2113 expected,
2114 "-05:00",
2115 );
2116 }
2117
2118 #[test]
2119 fn test_julianday_rfc3339_half_hour_offset() {
2120 let base = julianday_float("2026-04-07 16:00:00");
2122 let expected = base - 5.5 / 24.0;
2123 assert_jdn_close(
2124 julianday_float("2026-04-07T16:00:00+05:30"),
2125 expected,
2126 "+05:30",
2127 );
2128 }
2129
2130 #[test]
2131 fn test_julianday_rfc3339_compact_offsets() {
2132 let base = julianday_float("2026-04-07 16:00:00");
2134 assert_jdn_close(
2135 julianday_float("2026-04-07T16:00:00+0100"),
2136 base - 1.0 / 24.0,
2137 "+0100",
2138 );
2139 assert_jdn_close(
2140 julianday_float("2026-04-07T16:00:00-0530"),
2141 base + 5.5 / 24.0,
2142 "-0530",
2143 );
2144 assert_jdn_close(
2145 julianday_float("2026-04-07T16:00:00+09"),
2146 base - 9.0 / 24.0,
2147 "+09",
2148 );
2149 }
2150
2151 #[test]
2152 fn test_julianday_rfc3339_fractional_seconds_with_tz() {
2153 let base = julianday_float("2026-04-07 16:00:00.500");
2155 assert_jdn_close(
2156 julianday_float("2026-04-07T16:00:00.500Z"),
2157 base,
2158 "fractional + Z",
2159 );
2160 assert_jdn_close(
2161 julianday_float("2026-04-07T16:00:00.500+01:00"),
2162 base - 1.0 / 24.0,
2163 "fractional + +01:00",
2164 );
2165 }
2166
2167 #[test]
2168 fn test_date_and_time_rfc3339_round_trip() {
2169 assert_text(
2172 &DateFunc
2173 .invoke(&[text("2026-04-07T16:00:00+05:00")])
2174 .unwrap(),
2175 "2026-04-07",
2177 );
2178 assert_text(
2179 &TimeFunc
2180 .invoke(&[text("2026-04-07T16:00:00+05:00")])
2181 .unwrap(),
2182 "11:00:00",
2183 );
2184 assert_text(
2185 &DateTimeFunc
2186 .invoke(&[text("2026-04-07T16:00:00+05:00")])
2187 .unwrap(),
2188 "2026-04-07 11:00:00",
2189 );
2190 }
2191
2192 #[test]
2193 fn test_julianday_rfc3339_invalid_offsets_return_null() {
2194 for bad in &[
2196 "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", ] {
2201 let result = JuliandayFunc.invoke(&[text(bad)]).unwrap();
2202 assert_eq!(
2203 result,
2204 SqliteValue::Null,
2205 "expected NULL for malformed offset {bad:?}, got {result:?}"
2206 );
2207 }
2208 }
2209
2210 #[test]
2211 fn test_julianday_rejects_malformed_time_fields() {
2212 for bad in &[
2216 "+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", ] {
2228 let result = JuliandayFunc.invoke(&[text(bad)]).unwrap();
2229 assert_eq!(
2230 result,
2231 SqliteValue::Null,
2232 "expected NULL for signed time field {bad:?}, got {result:?}"
2233 );
2234 }
2235 }
2236
2237 #[test]
2238 fn test_unixepoch_basic() {
2239 let r = UnixepochFunc
2240 .invoke(&[text("1970-01-01 00:00:00")])
2241 .unwrap();
2242 assert_eq!(r, int(0));
2243 }
2244
2245 #[test]
2246 fn test_unixepoch_known_date() {
2247 let r = UnixepochFunc
2248 .invoke(&[text("2024-01-01 00:00:00")])
2249 .unwrap();
2250 assert_eq!(r, int(1_704_067_200));
2252 }
2253
2254 #[test]
2257 fn test_modifier_days() {
2258 let r = DateFunc
2259 .invoke(&[text("2024-01-15"), text("+10 days")])
2260 .unwrap();
2261 assert_text(&r, "2024-01-25");
2262 }
2263
2264 #[test]
2265 fn test_modifier_months() {
2266 let r = DateFunc
2269 .invoke(&[text("2024-01-31"), text("+1 months")])
2270 .unwrap();
2271 assert_text(&r, "2024-03-02");
2272 }
2273
2274 #[test]
2275 fn test_modifier_years() {
2276 let r = DateFunc
2279 .invoke(&[text("2024-02-29"), text("+1 years")])
2280 .unwrap();
2281 assert_text(&r, "2025-03-01");
2282 }
2283
2284 #[test]
2285 fn test_modifier_hours() {
2286 let r = DateTimeFunc
2287 .invoke(&[text("2024-01-01 23:00:00"), text("+2 hours")])
2288 .unwrap();
2289 assert_text(&r, "2024-01-02 01:00:00");
2290 }
2291
2292 #[test]
2293 fn test_modifier_start_of_month() {
2294 let r = DateFunc
2295 .invoke(&[text("2024-03-15"), text("start of month")])
2296 .unwrap();
2297 assert_text(&r, "2024-03-01");
2298 }
2299
2300 #[test]
2301 fn test_modifier_start_of_year() {
2302 let r = DateFunc
2303 .invoke(&[text("2024-06-15"), text("start of year")])
2304 .unwrap();
2305 assert_text(&r, "2024-01-01");
2306 }
2307
2308 #[test]
2309 fn test_modifier_start_of_day() {
2310 let r = DateTimeFunc
2311 .invoke(&[text("2024-03-15 14:30:00"), text("start of day")])
2312 .unwrap();
2313 assert_text(&r, "2024-03-15 00:00:00");
2314 }
2315
2316 #[test]
2317 fn test_modifier_unixepoch() {
2318 let r = DateTimeFunc.invoke(&[int(0), text("unixepoch")]).unwrap();
2319 assert_text(&r, "1970-01-01 00:00:00");
2320 }
2321
2322 #[test]
2323 fn test_modifier_weekday() {
2324 let r = DateFunc
2326 .invoke(&[text("2024-03-15"), text("weekday 0")])
2327 .unwrap();
2328 assert_text(&r, "2024-03-17");
2329 }
2330
2331 #[test]
2332 fn test_modifier_auto_unixepoch() {
2333 let ts = int(1_710_531_045);
2334 let r = DateTimeFunc.invoke(&[ts.clone(), text("auto")]).unwrap();
2335 let expected = DateTimeFunc.invoke(&[ts, text("unixepoch")]).unwrap();
2336 assert_eq!(
2337 r, expected,
2338 "auto and unixepoch should agree for unix-like values"
2339 );
2340 }
2341
2342 #[test]
2343 fn test_modifier_auto_julian_day() {
2344 let r = DateFunc
2345 .invoke(&[float(2_460_384.5), text("auto")])
2346 .unwrap();
2347 assert_text(&r, "2024-03-15");
2348 }
2349
2350 #[test]
2351 fn test_modifier_localtime_utc_roundtrip() {
2352 let r = DateTimeFunc
2354 .invoke(&[text("2024-03-15 14:30:45"), text("localtime"), text("utc")])
2355 .unwrap();
2356 assert_text(&r, "2024-03-15 14:30:45");
2357 }
2358
2359 #[test]
2360 fn test_modifier_localtime_shifts_value() {
2361 let offset = utc_offset_for_utc_jdn(ymdhms_to_jdn(2024, 3, 15, 12, 0, 0, 0.0));
2363 if offset != 0 {
2364 let r = DateTimeFunc
2365 .invoke(&[text("2024-03-15 12:00:00"), text("localtime")])
2366 .unwrap();
2367 let shifted = match &r {
2369 SqliteValue::Text(s) => s.clone(),
2370 _ => panic!("expected text"),
2371 };
2372 assert_ne!(&*shifted, "2024-03-15 12:00:00");
2373 }
2374 }
2375
2376 #[test]
2377 fn test_modifier_auto_out_of_range_returns_null() {
2378 let r = DateTimeFunc.invoke(&[float(1.0e20), text("auto")]).unwrap();
2379 assert_eq!(r, SqliteValue::Null);
2380 }
2381
2382 #[test]
2383 fn test_modifier_order_matters() {
2384 let r1 = DateFunc
2386 .invoke(&[text("2024-03-15"), text("start of month"), text("+1 days")])
2387 .unwrap();
2388 assert_text(&r1, "2024-03-02");
2389
2390 let r2 = DateFunc
2392 .invoke(&[text("2024-03-15"), text("+1 days"), text("start of month")])
2393 .unwrap();
2394 assert_text(&r2, "2024-03-01");
2395 }
2396
2397 #[test]
2398 fn test_modifier_weekday_same_day_is_noop() {
2399 let r = DateFunc
2401 .invoke(&[text("2024-03-17"), text("weekday 0")])
2402 .unwrap();
2403 assert_text(&r, "2024-03-17");
2404 }
2405
2406 #[test]
2409 fn test_bare_time_defaults() {
2410 let r = DateFunc.invoke(&[text("12:30:00")]).unwrap();
2411 assert_text(&r, "2000-01-01");
2412 }
2413
2414 #[test]
2415 fn test_t_separator() {
2416 let r = DateTimeFunc.invoke(&[text("2024-03-15T14:30:00")]).unwrap();
2417 assert_text(&r, "2024-03-15 14:30:00");
2418 }
2419
2420 #[test]
2421 fn test_julian_day_input() {
2422 let r = DateFunc.invoke(&[float(2_460_384.5)]).unwrap();
2424 assert_text(&r, "2024-03-15");
2425 }
2426
2427 #[test]
2428 fn test_null_input() {
2429 assert_eq!(DateFunc.invoke(&[null()]).unwrap(), SqliteValue::Null);
2430 }
2431
2432 #[test]
2433 fn test_invalid_input() {
2434 assert_eq!(
2435 DateFunc.invoke(&[text("not-a-date")]).unwrap(),
2436 SqliteValue::Null
2437 );
2438 }
2439
2440 #[test]
2441 fn test_negative_time_component_invalid() {
2442 let r = TimeFunc.invoke(&[text("-01:00")]).unwrap();
2443 assert_eq!(r, SqliteValue::Null);
2444 }
2445
2446 #[test]
2449 fn test_leap_year() {
2450 let r = DateFunc
2451 .invoke(&[text("2024-02-28"), text("+1 days")])
2452 .unwrap();
2453 assert_text(&r, "2024-02-29");
2454 }
2455
2456 #[test]
2457 fn test_non_leap_year() {
2458 let r = DateFunc
2459 .invoke(&[text("2023-02-28"), text("+1 days")])
2460 .unwrap();
2461 assert_text(&r, "2023-03-01");
2462 }
2463
2464 #[test]
2467 fn test_strftime_basic() {
2468 let r = StrftimeFunc
2469 .invoke(&[text("%Y-%m-%d"), text("2024-03-15")])
2470 .unwrap();
2471 assert_text(&r, "2024-03-15");
2472 }
2473
2474 #[test]
2475 fn test_strftime_time_specifiers() {
2476 let r = StrftimeFunc
2477 .invoke(&[text("%H:%M:%S"), text("2024-03-15 14:30:45")])
2478 .unwrap();
2479 assert_text(&r, "14:30:45");
2480 }
2481
2482 #[test]
2483 fn test_strftime_unix_seconds() {
2484 let r = StrftimeFunc
2485 .invoke(&[text("%s"), text("1970-01-01 00:00:00")])
2486 .unwrap();
2487 assert_text(&r, "0");
2488 }
2489
2490 #[test]
2491 fn test_strftime_day_of_year() {
2492 let r = StrftimeFunc
2493 .invoke(&[text("%j"), text("2024-03-15")])
2494 .unwrap();
2495 assert_text(&r, "075");
2497 }
2498
2499 #[test]
2500 fn test_strftime_day_of_week() {
2501 let r = StrftimeFunc
2503 .invoke(&[text("%w"), text("2024-03-15")])
2504 .unwrap();
2505 assert_text(&r, "5");
2506
2507 let r = StrftimeFunc
2508 .invoke(&[text("%u"), text("2024-03-15")])
2509 .unwrap();
2510 assert_text(&r, "5");
2511 }
2512
2513 #[test]
2514 fn test_strftime_12hour() {
2515 let r = StrftimeFunc
2516 .invoke(&[text("%I %p"), text("2024-03-15 14:30:00")])
2517 .unwrap();
2518 assert_text(&r, "02 PM");
2519
2520 let r = StrftimeFunc
2521 .invoke(&[text("%I %P"), text("2024-03-15 09:30:00")])
2522 .unwrap();
2523 assert_text(&r, "09 am");
2524 }
2525
2526 #[test]
2527 fn test_strftime_all_specifiers_presence() {
2528 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|%%";
2529 let r = StrftimeFunc
2530 .invoke(&[text(fmt), text("2024-03-15 14:30:45.123")])
2531 .unwrap();
2532
2533 let s = match r {
2534 SqliteValue::Text(v) => v,
2535 other => panic!("expected Text, got {other:?}"),
2536 };
2537 let parts: Vec<&str> = s.split('|').collect();
2538 assert_eq!(parts.len(), 25, "unexpected specifier output: {s}");
2539 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!(
2546 parts[6].parse::<f64>().is_ok(),
2547 "expected numeric %J output, got {}",
2548 parts[6]
2549 );
2550 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!(
2558 parts[14].parse::<i64>().is_ok(),
2559 "expected numeric %s output, got {}",
2560 parts[14]
2561 );
2562 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], "%"); }
2573
2574 #[test]
2575 fn test_strftime_null() {
2576 assert_eq!(
2577 StrftimeFunc.invoke(&[null(), text("2024-01-01")]).unwrap(),
2578 SqliteValue::Null
2579 );
2580 assert_eq!(
2581 StrftimeFunc.invoke(&[text("%Y"), null()]).unwrap(),
2582 SqliteValue::Null
2583 );
2584 }
2585
2586 #[test]
2587 #[ignore = "perf-only benchmark"]
2588 fn perf_strftime_timestamp_rows() {
2589 use std::hint::black_box;
2590 use std::time::Instant;
2591
2592 const ROWS: usize = 200_000;
2593 const REPEATS: usize = 5;
2594 const FORMAT: &str = "%Y-%m-%d %H:%M:%S";
2595 const INPUT: &str = "2024-03-15 14:30:45";
2596
2597 let func = StrftimeFunc;
2598 let fmt = text(FORMAT);
2599 let input = text(INPUT);
2600 let mut best_ns = u128::MAX;
2601 let mut output_len = 0usize;
2602
2603 for _ in 0..REPEATS {
2604 let started = Instant::now();
2605 for _ in 0..ROWS {
2606 let result = black_box(
2607 func.invoke(black_box(&[fmt.clone(), input.clone()]))
2608 .expect("strftime benchmark invocation must succeed"),
2609 );
2610 output_len = match result {
2611 SqliteValue::Text(text) => text.len(),
2612 SqliteValue::Null
2613 | SqliteValue::Integer(_)
2614 | SqliteValue::Float(_)
2615 | SqliteValue::Blob(_) => 0,
2616 };
2617 }
2618 let elapsed_ns = started.elapsed().as_nanos();
2619 if elapsed_ns < best_ns {
2620 best_ns = elapsed_ns;
2621 }
2622 }
2623
2624 println!(
2625 "strftime_timestamp_rows rows={ROWS} repeats={REPEATS} best_ns={best_ns} output_len={output_len}"
2626 );
2627 }
2628
2629 #[test]
2632 fn test_timediff_basic() {
2633 let r = TimediffFunc
2634 .invoke(&[text("2024-03-15"), text("2024-03-10")])
2635 .unwrap();
2636 assert_text(&r, "+0000-00-05 00:00:00.000");
2637 }
2638
2639 #[test]
2640 fn test_timediff_negative() {
2641 let r = TimediffFunc
2642 .invoke(&[text("2024-03-10"), text("2024-03-15")])
2643 .unwrap();
2644 assert_text(&r, "-0000-00-05 00:00:00.000");
2645 }
2646
2647 #[test]
2648 fn test_timediff_year_boundary() {
2649 let r = TimediffFunc
2650 .invoke(&[text("2024-01-01 01:00:00"), text("2023-12-31 23:00:00")])
2651 .unwrap();
2652 assert_text(&r, "+0000-00-00 02:00:00.000");
2653 }
2654
2655 #[test]
2658 fn test_modifier_subsec() {
2659 assert_text(
2660 &TimeFunc
2661 .invoke(&[text("2024-01-01 12:00:00"), text("subsec")])
2662 .unwrap(),
2663 "12:00:00.000",
2664 );
2665 assert_text(
2666 &DateTimeFunc
2667 .invoke(&[text("2024-01-01 12:00:00"), text("subsecond")])
2668 .unwrap(),
2669 "2024-01-01 12:00:00.000",
2670 );
2671 assert_text(
2672 &TimeFunc
2673 .invoke(&[text("2024-01-01 12:00:00.123"), text("subsec")])
2674 .unwrap(),
2675 "12:00:00.123",
2676 );
2677 }
2678
2679 #[test]
2680 fn test_subsec_unix_seconds_and_integer_flooring() {
2681 assert_text(
2682 &StrftimeFunc
2683 .invoke(&[text("%s"), text("1970-01-01 00:00:00.125"), text("subsec")])
2684 .unwrap(),
2685 "0.125",
2686 );
2687 assert_eq!(
2688 UnixepochFunc
2689 .invoke(&[text("1970-01-01 00:00:00.125"), text("subsec")])
2690 .unwrap(),
2691 float(0.125)
2692 );
2693
2694 for input in ["1970-01-01 00:00:00.500", "1970-01-01 00:00:00.999"] {
2695 assert_eq!(UnixepochFunc.invoke(&[text(input)]).unwrap(), int(0));
2696 assert_text(
2697 &StrftimeFunc.invoke(&[text("%s"), text(input)]).unwrap(),
2698 "0",
2699 );
2700 }
2701 assert_eq!(
2702 UnixepochFunc
2703 .invoke(&[text("1969-12-31 23:59:59.999")])
2704 .unwrap(),
2705 int(-1)
2706 );
2707 assert_text(
2708 &StrftimeFunc
2709 .invoke(&[text("%s"), text("1969-12-31 23:59:59.999")])
2710 .unwrap(),
2711 "-1",
2712 );
2713 }
2714
2715 #[test]
2716 fn test_subsec_rounding_preserves_sqlite_second_60() {
2717 assert_text(
2718 &TimeFunc
2719 .invoke(&[text("12:34:59.9995"), text("subsec")])
2720 .unwrap(),
2721 "12:34:60.000",
2722 );
2723 assert_text(
2724 &DateTimeFunc
2725 .invoke(&[text("1970-01-01 23:59:59.9995"), text("subsec")])
2726 .unwrap(),
2727 "1970-01-01 23:59:60.000",
2728 );
2729 assert_text(
2730 &StrftimeFunc
2731 .invoke(&[
2732 text("%H:%M:%f|%s"),
2733 text("1970-01-01 23:59:59.9995"),
2734 text("subsec"),
2735 ])
2736 .unwrap(),
2737 "23:59:59.999|86400.000",
2738 );
2739 }
2740
2741 #[test]
2744 fn test_register_datetime_builtins_all_present() {
2745 let mut reg = FunctionRegistry::new();
2746 register_datetime_builtins(&mut reg);
2747
2748 let expected = [
2749 "date",
2750 "time",
2751 "datetime",
2752 "julianday",
2753 "unixepoch",
2754 "strftime",
2755 "timediff",
2756 ];
2757
2758 for name in expected {
2759 assert!(
2760 reg.find_scalar(name, 1).is_some() || reg.find_scalar(name, 2).is_some(),
2761 "datetime function '{name}' not registered"
2762 );
2763 }
2764 }
2765
2766 #[test]
2767 fn test_public_datetime_function_metadata_fails_closed_for_direct_registration() {
2768 assert!(!DateFunc.is_deterministic());
2769 assert!(!TimeFunc.is_deterministic());
2770 assert!(!DateTimeFunc.is_deterministic());
2771 assert!(!JuliandayFunc.is_deterministic());
2772 assert!(!UnixepochFunc.is_deterministic());
2773 assert!(!StrftimeFunc.is_deterministic());
2774 assert!(!TimediffFunc.is_deterministic());
2775
2776 let mut registry = FunctionRegistry::new();
2777 registry.register_scalar(DateFunc);
2778 registry.register_scalar(TimeFunc);
2779 registry.register_scalar(DateTimeFunc);
2780 registry.register_scalar(JuliandayFunc);
2781 registry.register_scalar(UnixepochFunc);
2782 registry.register_scalar(StrftimeFunc);
2783 registry.register_scalar(TimediffFunc);
2784 for (name, num_args) in [
2785 ("date", 0),
2786 ("time", 0),
2787 ("datetime", 0),
2788 ("julianday", 0),
2789 ("unixepoch", 0),
2790 ("strftime", 1),
2791 ("timediff", 2),
2792 ] {
2793 assert_eq!(
2794 registry.scalar_schema_safety(name, num_args),
2795 Some(crate::ScalarSchemaSafety::Never),
2796 "generic registration of {name}/{num_args} must fail closed"
2797 );
2798 }
2799 }
2800
2801 #[test]
2804 fn test_modifier_year_overflow() {
2805 let huge = i64::MAX;
2808 let modifier = format!("+{huge} years");
2809 let r = DateFunc.invoke(&[text("2000-01-01"), text(&modifier)]);
2810 assert_eq!(r.unwrap(), SqliteValue::Null);
2813 }
2814
2815 #[test]
2816 fn test_jdn_roundtrip() {
2817 let dates = [
2819 (2024, 3, 15),
2820 (2000, 1, 1),
2821 (1970, 1, 1),
2822 (2024, 2, 29),
2823 (1900, 1, 1),
2824 (2099, 12, 31),
2825 ];
2826 for (y, m, d) in dates {
2827 let jdn = ymd_to_jdn(y, m, d);
2828 let (y2, m2, d2) = jdn_to_ymd(jdn);
2829 assert_eq!(
2830 (y, m, d),
2831 (y2, m2, d2),
2832 "roundtrip failed for {y}-{m}-{d} (JDN={jdn})"
2833 );
2834 }
2835 }
2836
2837 #[test]
2838 fn test_unix_epoch_roundtrip() {
2839 let jdn = ymd_to_jdn(1970, 1, 1);
2840 let unix = jdn_to_unix(jdn);
2841 assert_eq!(unix, 0, "Unix epoch should be 0");
2842
2843 let jdn2 = unix_to_jdn(0.0);
2844 assert!((jdn2 - UNIX_EPOCH_JDN).abs() < 1e-10, "roundtrip failed");
2845 }
2846}