Skip to main content

sparrowdb_execution/
functions.rs

1//! openCypher built-in function library.
2//!
3//! Covers SPA-140 (string), SPA-141 (math), SPA-142 (list), and SPA-143
4//! (type conversion & predicate) functions.
5//!
6//! Each function takes evaluated `Vec<Value>` arguments and returns
7//! `Result<Value>`.  The dispatcher is `dispatch_function`.
8
9use sparrowdb_common::{Error, Result};
10
11use crate::types::Value;
12
13// ── Public dispatcher ─────────────────────────────────────────────────────────
14
15/// Dispatch a built-in function call by name.
16///
17/// `name` is compared case-insensitively.
18/// Returns `Err(InvalidArgument)` for unknown function names or arity errors.
19pub fn dispatch_function(name: &str, args: Vec<Value>) -> Result<Value> {
20    match name.to_lowercase().as_str() {
21        // ── SPA-140: String functions ─────────────────────────────────────────
22        "toupper" => fn_to_upper(args),
23        "tolower" => fn_to_lower(args),
24        "trim" => fn_trim(args),
25        "ltrim" => fn_ltrim(args),
26        "rtrim" => fn_rtrim(args),
27        "split" => fn_split(args),
28        "substring" => fn_substring(args),
29        "size" => fn_size(args),
30        "startswith" => fn_starts_with(args),
31        "endswith" => fn_ends_with(args),
32        "contains" => fn_contains(args),
33        "replace" => fn_replace(args),
34
35        // ── SPA-141: Math functions ───────────────────────────────────────────
36        "abs" => fn_abs(args),
37        "ceil" => fn_ceil(args),
38        "floor" => fn_floor(args),
39        "round" => fn_round(args),
40        "sqrt" => fn_sqrt(args),
41        "log" => fn_log(args),
42        "log10" => fn_log10(args),
43        "exp" => fn_exp(args),
44        "sign" => fn_sign(args),
45        "rand" => fn_rand(args),
46
47        // ── SPA-142: List functions ───────────────────────────────────────────
48        "range" => fn_range(args),
49        "head" => fn_head(args),
50        "tail" => fn_tail(args),
51        "last" => fn_last(args),
52        "reverse" => fn_reverse(args),
53        "sort" => fn_sort(args),
54        "distinct" => fn_distinct(args),
55        "reduce" => Err(Error::InvalidArgument(
56            "reduce() must be handled by the evaluator (it requires a lambda)".into(),
57        )),
58
59        // ── SPA-143: Type conversion & predicate functions ────────────────────
60        "tostring" => fn_to_string(args),
61        "tointeger" => fn_to_integer(args),
62        "tofloat" => fn_to_float(args),
63        "toboolean" => fn_to_boolean(args),
64        "type" => fn_type(args),
65        "labels" => fn_labels(args),
66        "keys" => fn_keys(args),
67        "properties" => fn_properties(args),
68        "id" => fn_id(args),
69        "coalesce" => fn_coalesce(args),
70        "isnull" => fn_is_null(args),
71        "isnotnull" => fn_is_not_null(args),
72
73        // Aggregate functions — handled by the engine's aggregate_rows(), not as scalar functions.
74        "collect" | "count" | "sum" | "avg" | "min" | "max" => Err(Error::InvalidArgument(
75            format!("{name}() is an aggregate function and cannot be used as a scalar expression"),
76        )),
77
78        // ── Temporal functions ────────────────────────────────────────────────
79        "datetime" => fn_datetime(args),
80        "timestamp" => fn_datetime(args), // alias for datetime()
81        "date" => fn_date(args),
82        "duration" => fn_duration(args),
83
84        other => Err(Error::InvalidArgument(format!("unknown function: {other}"))),
85    }
86}
87
88// ── Arity helpers ─────────────────────────────────────────────────────────────
89
90fn expect_arity(name: &str, args: &[Value], expected: usize) -> Result<()> {
91    if args.len() != expected {
92        Err(Error::InvalidArgument(format!(
93            "{name}() expects {expected} argument(s), got {}",
94            args.len()
95        )))
96    } else {
97        Ok(())
98    }
99}
100
101fn expect_min_arity(name: &str, args: &[Value], min: usize) -> Result<()> {
102    if args.len() < min {
103        Err(Error::InvalidArgument(format!(
104            "{name}() expects at least {min} argument(s), got {}",
105            args.len()
106        )))
107    } else {
108        Ok(())
109    }
110}
111
112fn as_string<'a>(name: &str, v: &'a Value) -> Result<&'a str> {
113    match v {
114        Value::String(s) => Ok(s.as_str()),
115        Value::Null => Err(Error::InvalidArgument(format!(
116            "{name}(): argument is null"
117        ))),
118        other => Err(Error::InvalidArgument(format!(
119            "{name}(): expected string, got {other}"
120        ))),
121    }
122}
123
124fn as_int(name: &str, v: &Value) -> Result<i64> {
125    match v {
126        Value::Int64(n) => Ok(*n),
127        Value::Float64(f) => Ok(*f as i64),
128        Value::Null => Err(Error::InvalidArgument(format!(
129            "{name}(): argument is null"
130        ))),
131        other => Err(Error::InvalidArgument(format!(
132            "{name}(): expected integer, got {other}"
133        ))),
134    }
135}
136
137fn as_float(name: &str, v: &Value) -> Result<f64> {
138    match v {
139        Value::Float64(f) => Ok(*f),
140        Value::Int64(n) => Ok(*n as f64),
141        Value::Null => Err(Error::InvalidArgument(format!(
142            "{name}(): argument is null"
143        ))),
144        other => Err(Error::InvalidArgument(format!(
145            "{name}(): expected numeric, got {other}"
146        ))),
147    }
148}
149
150// ── SPA-140: String functions ─────────────────────────────────────────────────
151
152fn fn_to_upper(args: Vec<Value>) -> Result<Value> {
153    expect_arity("toUpper", &args, 1)?;
154    if matches!(args[0], Value::Null) {
155        return Ok(Value::Null);
156    }
157    let s = as_string("toUpper", &args[0])?;
158    Ok(Value::String(s.to_uppercase()))
159}
160
161fn fn_to_lower(args: Vec<Value>) -> Result<Value> {
162    expect_arity("toLower", &args, 1)?;
163    if matches!(args[0], Value::Null) {
164        return Ok(Value::Null);
165    }
166    let s = as_string("toLower", &args[0])?;
167    Ok(Value::String(s.to_lowercase()))
168}
169
170fn fn_trim(args: Vec<Value>) -> Result<Value> {
171    expect_arity("trim", &args, 1)?;
172    if matches!(args[0], Value::Null) {
173        return Ok(Value::Null);
174    }
175    let s = as_string("trim", &args[0])?;
176    Ok(Value::String(s.trim().to_string()))
177}
178
179fn fn_ltrim(args: Vec<Value>) -> Result<Value> {
180    expect_arity("ltrim", &args, 1)?;
181    if matches!(args[0], Value::Null) {
182        return Ok(Value::Null);
183    }
184    let s = as_string("ltrim", &args[0])?;
185    Ok(Value::String(s.trim_start().to_string()))
186}
187
188fn fn_rtrim(args: Vec<Value>) -> Result<Value> {
189    expect_arity("rtrim", &args, 1)?;
190    if matches!(args[0], Value::Null) {
191        return Ok(Value::Null);
192    }
193    let s = as_string("rtrim", &args[0])?;
194    Ok(Value::String(s.trim_end().to_string()))
195}
196
197/// `split(string, delimiter)` — returns the first part for now.
198///
199/// openCypher `split()` returns a list; since SparrowDB's `Value` type has no
200/// `List` variant yet, we return the number of parts as `Int64`.  This is a
201/// pragmatic stub: calling code that needs individual parts should use UNWIND.
202fn fn_split(args: Vec<Value>) -> Result<Value> {
203    expect_arity("split", &args, 2)?;
204    if matches!(args[0], Value::Null) || matches!(args[1], Value::Null) {
205        return Ok(Value::Null);
206    }
207    let s = as_string("split", &args[0])?;
208    let delim = as_string("split", &args[1])?;
209    // Return the count of parts (openCypher returns a list; we return its size
210    // since `Value::List` doesn't exist yet).
211    let count = s.split(delim).count() as i64;
212    Ok(Value::Int64(count))
213}
214
215/// `substring(string, start[, length])`.
216fn fn_substring(args: Vec<Value>) -> Result<Value> {
217    expect_min_arity("substring", &args, 2)?;
218    if matches!(args[0], Value::Null) {
219        return Ok(Value::Null);
220    }
221    let s = as_string("substring", &args[0])?;
222    let start = as_int("substring", &args[1])?;
223    let chars: Vec<char> = s.chars().collect();
224    let len = chars.len() as i64;
225
226    let start = start.max(0).min(len) as usize;
227
228    let result: String = if args.len() >= 3 {
229        let take = as_int("substring", &args[2])?.max(0) as usize;
230        chars[start..].iter().take(take).collect()
231    } else {
232        chars[start..].iter().collect()
233    };
234
235    Ok(Value::String(result))
236}
237
238/// `size(string)` — character length; also handles `null` → `null`.
239fn fn_size(args: Vec<Value>) -> Result<Value> {
240    expect_arity("size", &args, 1)?;
241    match &args[0] {
242        Value::Null => Ok(Value::Null),
243        Value::String(s) => Ok(Value::Int64(s.chars().count() as i64)),
244        other => Err(Error::InvalidArgument(format!(
245            "size(): expected string or null, got {other}"
246        ))),
247    }
248}
249
250fn fn_starts_with(args: Vec<Value>) -> Result<Value> {
251    expect_arity("startsWith", &args, 2)?;
252    if matches!(args[0], Value::Null) || matches!(args[1], Value::Null) {
253        return Ok(Value::Null);
254    }
255    let s = as_string("startsWith", &args[0])?;
256    let prefix = as_string("startsWith", &args[1])?;
257    Ok(Value::Bool(s.starts_with(prefix)))
258}
259
260fn fn_ends_with(args: Vec<Value>) -> Result<Value> {
261    expect_arity("endsWith", &args, 2)?;
262    if matches!(args[0], Value::Null) || matches!(args[1], Value::Null) {
263        return Ok(Value::Null);
264    }
265    let s = as_string("endsWith", &args[0])?;
266    let suffix = as_string("endsWith", &args[1])?;
267    Ok(Value::Bool(s.ends_with(suffix)))
268}
269
270fn fn_contains(args: Vec<Value>) -> Result<Value> {
271    expect_arity("contains", &args, 2)?;
272    if matches!(args[0], Value::Null) || matches!(args[1], Value::Null) {
273        return Ok(Value::Null);
274    }
275    let s = as_string("contains", &args[0])?;
276    let needle = as_string("contains", &args[1])?;
277    Ok(Value::Bool(s.contains(needle)))
278}
279
280fn fn_replace(args: Vec<Value>) -> Result<Value> {
281    expect_arity("replace", &args, 3)?;
282    if matches!(args[0], Value::Null) {
283        return Ok(Value::Null);
284    }
285    let s = as_string("replace", &args[0])?;
286    let from = as_string("replace", &args[1])?;
287    let to = as_string("replace", &args[2])?;
288    Ok(Value::String(s.replace(from, to)))
289}
290
291// ── SPA-141: Math functions ───────────────────────────────────────────────────
292
293fn fn_abs(args: Vec<Value>) -> Result<Value> {
294    expect_arity("abs", &args, 1)?;
295    match &args[0] {
296        Value::Null => Ok(Value::Null),
297        Value::Int64(n) => Ok(Value::Int64(n.abs())),
298        Value::Float64(f) => Ok(Value::Float64(f.abs())),
299        other => Err(Error::InvalidArgument(format!(
300            "abs(): expected numeric, got {other}"
301        ))),
302    }
303}
304
305fn fn_ceil(args: Vec<Value>) -> Result<Value> {
306    expect_arity("ceil", &args, 1)?;
307    if matches!(args[0], Value::Null) {
308        return Ok(Value::Null);
309    }
310    let f = as_float("ceil", &args[0])?;
311    Ok(Value::Float64(f.ceil()))
312}
313
314fn fn_floor(args: Vec<Value>) -> Result<Value> {
315    expect_arity("floor", &args, 1)?;
316    if matches!(args[0], Value::Null) {
317        return Ok(Value::Null);
318    }
319    let f = as_float("floor", &args[0])?;
320    Ok(Value::Float64(f.floor()))
321}
322
323fn fn_round(args: Vec<Value>) -> Result<Value> {
324    expect_arity("round", &args, 1)?;
325    if matches!(args[0], Value::Null) {
326        return Ok(Value::Null);
327    }
328    let f = as_float("round", &args[0])?;
329    Ok(Value::Float64(f.round()))
330}
331
332fn fn_sqrt(args: Vec<Value>) -> Result<Value> {
333    expect_arity("sqrt", &args, 1)?;
334    if matches!(args[0], Value::Null) {
335        return Ok(Value::Null);
336    }
337    let f = as_float("sqrt", &args[0])?;
338    Ok(Value::Float64(f.sqrt()))
339}
340
341/// `log(n)` — natural logarithm.
342fn fn_log(args: Vec<Value>) -> Result<Value> {
343    expect_arity("log", &args, 1)?;
344    if matches!(args[0], Value::Null) {
345        return Ok(Value::Null);
346    }
347    let f = as_float("log", &args[0])?;
348    Ok(Value::Float64(f.ln()))
349}
350
351fn fn_log10(args: Vec<Value>) -> Result<Value> {
352    expect_arity("log10", &args, 1)?;
353    if matches!(args[0], Value::Null) {
354        return Ok(Value::Null);
355    }
356    let f = as_float("log10", &args[0])?;
357    Ok(Value::Float64(f.log10()))
358}
359
360fn fn_exp(args: Vec<Value>) -> Result<Value> {
361    expect_arity("exp", &args, 1)?;
362    if matches!(args[0], Value::Null) {
363        return Ok(Value::Null);
364    }
365    let f = as_float("exp", &args[0])?;
366    Ok(Value::Float64(f.exp()))
367}
368
369fn fn_sign(args: Vec<Value>) -> Result<Value> {
370    expect_arity("sign", &args, 1)?;
371    match &args[0] {
372        Value::Null => Ok(Value::Null),
373        Value::Int64(n) => Ok(Value::Int64(n.signum())),
374        Value::Float64(f) => {
375            let s = if *f > 0.0 {
376                1i64
377            } else if *f < 0.0 {
378                -1
379            } else {
380                0
381            };
382            Ok(Value::Int64(s))
383        }
384        other => Err(Error::InvalidArgument(format!(
385            "sign(): expected numeric, got {other}"
386        ))),
387    }
388}
389
390/// `rand()` — uniform random float in [0, 1).
391fn fn_rand(args: Vec<Value>) -> Result<Value> {
392    expect_arity("rand", &args, 0)?;
393    // Use a simple LCG seeded from current time to avoid pulling in rand crate.
394    use std::time::{SystemTime, UNIX_EPOCH};
395    let seed = SystemTime::now()
396        .duration_since(UNIX_EPOCH)
397        .map(|d| d.subsec_nanos())
398        .unwrap_or(42);
399    // LCG: same constants as glibc.
400    let v = (seed as u64)
401        .wrapping_mul(6364136223846793005)
402        .wrapping_add(1442695040888963407);
403    let f = (v >> 11) as f64 / (1u64 << 53) as f64;
404    Ok(Value::Float64(f))
405}
406
407// ── SPA-142: List functions ───────────────────────────────────────────────────
408
409/// `range(start, end[, step])` — returns a list of integers.
410///
411/// Matches openCypher semantics: `range(0, 5)` returns `[0,1,2,3,4,5]`.
412fn fn_range(args: Vec<Value>) -> Result<Value> {
413    expect_min_arity("range", &args, 2)?;
414    let start = as_int("range", &args[0])?;
415    let end = as_int("range", &args[1])?;
416    let step: i64 = if args.len() >= 3 {
417        as_int("range", &args[2])?
418    } else {
419        1
420    };
421    if step == 0 {
422        return Err(Error::InvalidArgument(
423            "range(): step must not be zero".into(),
424        ));
425    }
426    let mut values = Vec::new();
427    if step > 0 {
428        let mut i = start;
429        while i <= end {
430            values.push(Value::Int64(i));
431            i += step;
432        }
433    } else {
434        let mut i = start;
435        while i >= end {
436            values.push(Value::Int64(i));
437            i += step;
438        }
439    }
440    Ok(Value::List(values))
441}
442
443/// `head(list)` — first element of a list-like value.
444///
445/// Since `Value` has no `List` variant, this is a no-op that returns `Null`
446/// unless the argument is already a scalar (in which case we return it).
447/// When UNWIND is used the caller gets per-element rows; `head` is rarely
448/// needed in that pattern.
449fn fn_head(args: Vec<Value>) -> Result<Value> {
450    expect_arity("head", &args, 1)?;
451    // Without a List variant we cannot iterate — return Null.
452    match &args[0] {
453        Value::Null => Ok(Value::Null),
454        // If someone passes a scalar, treat it as a single-element list.
455        v => Ok(v.clone()),
456    }
457}
458
459fn fn_tail(args: Vec<Value>) -> Result<Value> {
460    expect_arity("tail", &args, 1)?;
461    // Without a List variant there is no "rest" to return.
462    Ok(Value::Null)
463}
464
465fn fn_last(args: Vec<Value>) -> Result<Value> {
466    expect_arity("last", &args, 1)?;
467    match &args[0] {
468        Value::Null => Ok(Value::Null),
469        v => Ok(v.clone()),
470    }
471}
472
473fn fn_reverse(args: Vec<Value>) -> Result<Value> {
474    expect_arity("reverse", &args, 1)?;
475    match &args[0] {
476        Value::Null => Ok(Value::Null),
477        Value::String(s) => Ok(Value::String(s.chars().rev().collect())),
478        // For non-string scalars we cannot reverse in-place without a List type.
479        v => Ok(v.clone()),
480    }
481}
482
483fn fn_sort(args: Vec<Value>) -> Result<Value> {
484    expect_arity("sort", &args, 1)?;
485    // Without a List variant, sorting is a no-op.
486    Ok(args.into_iter().next().unwrap_or(Value::Null))
487}
488
489fn fn_distinct(args: Vec<Value>) -> Result<Value> {
490    expect_arity("distinct", &args, 1)?;
491    Ok(args.into_iter().next().unwrap_or(Value::Null))
492}
493
494// ── SPA-143: Type conversion & predicate functions ────────────────────────────
495
496fn fn_to_string(args: Vec<Value>) -> Result<Value> {
497    expect_arity("toString", &args, 1)?;
498    match &args[0] {
499        Value::Null => Ok(Value::Null),
500        Value::String(s) => Ok(Value::String(s.clone())),
501        Value::Int64(n) => Ok(Value::String(n.to_string())),
502        Value::Float64(f) => Ok(Value::String(f.to_string())),
503        Value::Bool(b) => Ok(Value::String(b.to_string())),
504        Value::NodeRef(id) => Ok(Value::String(format!("node({})", id.0))),
505        Value::EdgeRef(id) => Ok(Value::String(format!("edge({})", id.0))),
506        Value::List(items) => Ok(Value::String(format!(
507            "{}",
508            crate::types::Value::List(items.clone())
509        ))),
510        Value::Map(entries) => Ok(Value::String(format!(
511            "{}",
512            crate::types::Value::Map(entries.clone())
513        ))),
514    }
515}
516
517fn fn_to_integer(args: Vec<Value>) -> Result<Value> {
518    expect_arity("toInteger", &args, 1)?;
519    match &args[0] {
520        Value::Null => Ok(Value::Null),
521        Value::Int64(n) => Ok(Value::Int64(*n)),
522        Value::Float64(f) => Ok(Value::Int64(*f as i64)),
523        Value::Bool(b) => Ok(Value::Int64(if *b { 1 } else { 0 })),
524        Value::String(s) => {
525            // Try integer parse first, then float.
526            if let Ok(n) = s.trim().parse::<i64>() {
527                Ok(Value::Int64(n))
528            } else if let Ok(f) = s.trim().parse::<f64>() {
529                Ok(Value::Int64(f as i64))
530            } else {
531                Ok(Value::Null) // openCypher: return null for non-parseable
532            }
533        }
534        _ => Ok(Value::Null),
535    }
536}
537
538fn fn_to_float(args: Vec<Value>) -> Result<Value> {
539    expect_arity("toFloat", &args, 1)?;
540    match &args[0] {
541        Value::Null => Ok(Value::Null),
542        Value::Float64(f) => Ok(Value::Float64(*f)),
543        Value::Int64(n) => Ok(Value::Float64(*n as f64)),
544        Value::Bool(b) => Ok(Value::Float64(if *b { 1.0 } else { 0.0 })),
545        Value::String(s) => {
546            if let Ok(f) = s.trim().parse::<f64>() {
547                Ok(Value::Float64(f))
548            } else {
549                Ok(Value::Null)
550            }
551        }
552        _ => Ok(Value::Null),
553    }
554}
555
556fn fn_to_boolean(args: Vec<Value>) -> Result<Value> {
557    expect_arity("toBoolean", &args, 1)?;
558    match &args[0] {
559        Value::Null => Ok(Value::Null),
560        Value::Bool(b) => Ok(Value::Bool(*b)),
561        Value::Int64(n) => Ok(Value::Bool(*n != 0)),
562        Value::String(s) => match s.to_lowercase().as_str() {
563            "true" => Ok(Value::Bool(true)),
564            "false" => Ok(Value::Bool(false)),
565            _ => Ok(Value::Null),
566        },
567        _ => Ok(Value::Null),
568    }
569}
570
571/// `type(rel)` — relationship type name.
572///
573/// Without a type-metadata lookup in Value, we return a placeholder string.
574/// A real implementation would look up the rel table name from the catalog.
575fn fn_type(args: Vec<Value>) -> Result<Value> {
576    expect_arity("type", &args, 1)?;
577    match &args[0] {
578        Value::Null => Ok(Value::Null),
579        Value::EdgeRef(_) => {
580            // Catalog lookup would be needed for the actual type name.
581            // Return a sentinel; callers should use the rel variable's type
582            // from the query pattern directly.
583            Ok(Value::String("UNKNOWN".into()))
584        }
585        other => Err(Error::InvalidArgument(format!(
586            "type(): expected relationship, got {other}"
587        ))),
588    }
589}
590
591/// `labels(node)` — node labels.  Returns a stub string.
592fn fn_labels(args: Vec<Value>) -> Result<Value> {
593    expect_arity("labels", &args, 1)?;
594    match &args[0] {
595        Value::Null => Ok(Value::Null),
596        Value::NodeRef(_) => {
597            // Without schema lookup in Value, we cannot enumerate labels here.
598            Ok(Value::String("[]".into()))
599        }
600        other => Err(Error::InvalidArgument(format!(
601            "labels(): expected node, got {other}"
602        ))),
603    }
604}
605
606/// `keys(node|rel)` — property key names.  Returns a stub string.
607fn fn_keys(args: Vec<Value>) -> Result<Value> {
608    expect_arity("keys", &args, 1)?;
609    match &args[0] {
610        Value::Null => Ok(Value::Null),
611        Value::NodeRef(_) | Value::EdgeRef(_) => Ok(Value::String("[]".into())),
612        other => Err(Error::InvalidArgument(format!(
613            "keys(): expected node or relationship, got {other}"
614        ))),
615    }
616}
617
618/// `properties(node|rel)` — returns a stub string.
619fn fn_properties(args: Vec<Value>) -> Result<Value> {
620    expect_arity("properties", &args, 1)?;
621    match &args[0] {
622        Value::Null => Ok(Value::Null),
623        Value::NodeRef(_) | Value::EdgeRef(_) => Ok(Value::String("{}".into())),
624        other => Err(Error::InvalidArgument(format!(
625            "properties(): expected node or relationship, got {other}"
626        ))),
627    }
628}
629
630/// `id(node)` — node internal ID.
631fn fn_id(args: Vec<Value>) -> Result<Value> {
632    expect_arity("id", &args, 1)?;
633    match &args[0] {
634        Value::Null => Ok(Value::Null),
635        Value::NodeRef(id) => Ok(Value::Int64(id.0 as i64)),
636        Value::EdgeRef(id) => Ok(Value::Int64(id.0 as i64)),
637        other => Err(Error::InvalidArgument(format!(
638            "id(): expected node or relationship, got {other}"
639        ))),
640    }
641}
642
643/// `coalesce(expr, …)` — first non-null value.
644fn fn_coalesce(args: Vec<Value>) -> Result<Value> {
645    for v in args {
646        if !matches!(v, Value::Null) {
647            return Ok(v);
648        }
649    }
650    Ok(Value::Null)
651}
652
653fn fn_is_null(args: Vec<Value>) -> Result<Value> {
654    expect_arity("isNull", &args, 1)?;
655    Ok(Value::Bool(matches!(args[0], Value::Null)))
656}
657
658fn fn_is_not_null(args: Vec<Value>) -> Result<Value> {
659    expect_arity("isNotNull", &args, 1)?;
660    Ok(Value::Bool(!matches!(args[0], Value::Null)))
661}
662
663// ── Temporal functions ─────────────────────────────────────────────────────────
664
665/// `datetime()` / `timestamp()` — current UTC time as epoch milliseconds.
666///
667/// Returns `Value::Int64` with the number of milliseconds since the Unix epoch
668/// (1970-01-01T00:00:00Z).  No arguments are accepted.
669fn fn_datetime(args: Vec<Value>) -> Result<Value> {
670    expect_arity("datetime", &args, 0)?;
671    use std::time::{SystemTime, UNIX_EPOCH};
672    let millis = SystemTime::now()
673        .duration_since(UNIX_EPOCH)
674        .map(|d| d.as_millis() as i64)
675        .unwrap_or(0);
676    Ok(Value::Int64(millis))
677}
678
679/// `date()` — today as days since the Unix epoch.
680///
681/// Returns `Value::Int64`.  Computed directly from whole seconds to avoid
682/// floating-point rounding errors.
683fn fn_date(args: Vec<Value>) -> Result<Value> {
684    expect_arity("date", &args, 0)?;
685    use std::time::{SystemTime, UNIX_EPOCH};
686    let secs = SystemTime::now()
687        .duration_since(UNIX_EPOCH)
688        .map(|d| d.as_secs() as i64)
689        .unwrap_or(0);
690    Ok(Value::Int64(secs / 86_400))
691}
692
693/// `duration(iso_string)` — minimal ISO-8601 duration stub.
694///
695/// Parses a small subset of ISO-8601 period strings and returns the
696/// equivalent number of **milliseconds** as `Value::Int64`.
697///
698/// Supported tokens: `P`, `nY`, `nM`, `nW`, `nD`, `T`, `nH`, `nM`, `nS`.
699/// Calendar approximations: 1 year ≈ 365 days, 1 month ≈ 30 days.
700///
701/// Unrecognised strings return `Err(InvalidArgument)`.
702fn fn_duration(args: Vec<Value>) -> Result<Value> {
703    expect_arity("duration", &args, 1)?;
704    if matches!(args[0], Value::Null) {
705        return Ok(Value::Null);
706    }
707    let s = as_string("duration", &args[0])?;
708    let millis = parse_iso_duration(s).ok_or_else(|| {
709        Error::InvalidArgument(format!("duration(): cannot parse ISO-8601 duration: {s}"))
710    })?;
711    Ok(Value::Int64(millis))
712}
713
714/// Parse a tiny subset of ISO-8601 duration strings → milliseconds.
715fn parse_iso_duration(s: &str) -> Option<i64> {
716    let s = s.trim();
717    // Must start with 'P' or 'p'.
718    let s = if s.starts_with(['P', 'p']) {
719        &s[1..]
720    } else {
721        return None;
722    };
723
724    const MS_PER_SEC: i64 = 1_000;
725    const MS_PER_MIN: i64 = 60 * MS_PER_SEC;
726    const MS_PER_HOUR: i64 = 60 * MS_PER_MIN;
727    const MS_PER_DAY: i64 = 24 * MS_PER_HOUR;
728    const MS_PER_WEEK: i64 = 7 * MS_PER_DAY;
729    const MS_PER_MONTH: i64 = 30 * MS_PER_DAY;
730    const MS_PER_YEAR: i64 = 365 * MS_PER_DAY;
731
732    let mut total: i64 = 0;
733    let mut in_time = false;
734    let mut buf = String::new();
735
736    for ch in s.chars() {
737        match ch {
738            'T' | 't' => {
739                in_time = true;
740                buf.clear();
741            }
742            '0'..='9' | '.' => buf.push(ch),
743            'Y' | 'y' if !in_time => {
744                let n: i64 = buf.parse().ok()?;
745                total += n * MS_PER_YEAR;
746                buf.clear();
747            }
748            'M' | 'm' if !in_time => {
749                let n: i64 = buf.parse().ok()?;
750                total += n * MS_PER_MONTH;
751                buf.clear();
752            }
753            'W' | 'w' if !in_time => {
754                let n: i64 = buf.parse().ok()?;
755                total += n * MS_PER_WEEK;
756                buf.clear();
757            }
758            'D' | 'd' if !in_time => {
759                let n: i64 = buf.parse().ok()?;
760                total += n * MS_PER_DAY;
761                buf.clear();
762            }
763            'H' | 'h' if in_time => {
764                let n: i64 = buf.parse().ok()?;
765                total += n * MS_PER_HOUR;
766                buf.clear();
767            }
768            'M' | 'm' if in_time => {
769                let n: i64 = buf.parse().ok()?;
770                total += n * MS_PER_MIN;
771                buf.clear();
772            }
773            'S' | 's' if in_time => {
774                // Seconds may have a fractional part.
775                let f: f64 = buf.parse().ok()?;
776                total += (f * MS_PER_SEC as f64) as i64;
777                buf.clear();
778            }
779            _ => return None,
780        }
781    }
782
783    Some(total)
784}