Skip to main content

octofhir_sof/
eval.rs

1//! In-memory ViewDefinition execution.
2//!
3//! Evaluates a [`ViewDefinition`] directly against FHIR resources held as
4//! `serde_json::Value`, with no database. Per-expression FHIRPath evaluation is
5//! delegated to the real `octofhir-fhirpath` [`FhirPathEngine`], so the full
6//! FHIRPath function set (`substring`, `upper`, math, etc.) is available —
7//! unlike the hand-lowered subset used by the SQL generator.
8//!
9//! What stays in this module is the SQL-on-FHIR *relational orchestration* that
10//! is not plain FHIRPath: `forEach`/`forEachOrNull`, nested `select` cartesian
11//! products, `unionAll`, `repeat` (preorder transitive closure), column
12//! ordering, the scalar-vs-collection column rule, and the boolean-`where`
13//! rule. The SoF-specific functions `getResourceKey()` / `getReferenceKey()`
14//! are registered as custom functions on the engine's registry so they can be
15//! used anywhere in an expression (including over `contained[]`).
16
17use std::sync::Arc;
18
19use async_trait::async_trait;
20use octofhir_fhirpath::core::error_code::FP0053;
21use octofhir_fhirpath::evaluator::AsyncNodeEvaluator;
22use octofhir_fhirpath::evaluator::function_registry::{
23    EmptyPropagation, FunctionCategory, FunctionMetadata, FunctionSignature, LazyFunctionEvaluator,
24    PureFunctionEvaluator,
25};
26use octofhir_fhirpath::{
27    Collection, EmptyModelProvider, EvaluationContext, ExpressionNode, FhirPathEngine,
28    FhirPathError, FhirPathValue, ModelProvider, create_function_registry,
29};
30use serde_json::{Map, Value};
31
32use crate::column::{ColumnInfo, ColumnType};
33use crate::runner::ViewResult;
34use crate::sql_generator::{build_constants, substitute_constants};
35use crate::view_definition::{Column, SelectColumn, ViewDefinition};
36use crate::{Error, Result};
37
38/// Execute a ViewDefinition against in-memory FHIR resources.
39///
40/// Resources whose `resourceType` differs from the view's `resource` are
41/// skipped. Returns the tabular result with column metadata and rows.
42///
43/// # Errors
44///
45/// Returns an error if a selector cannot be parsed, a constant is undefined, the
46/// column shape is inconsistent (duplicate names, mismatched `unionAll`
47/// branches), or a non-collection column yields more than one value.
48pub async fn execute(view: &ViewDefinition, resources: &[Value]) -> Result<ViewResult> {
49    let compiled = CompiledView::compile(view).await?;
50    let mut data: Vec<Vec<Value>> = Vec::new();
51    for resource in resources {
52        data.extend(compiled.execute_resource(resource).await?);
53    }
54    let row_count = data.len();
55    Ok(ViewResult {
56        columns: compiled.columns().to_vec(),
57        data,
58        row_count,
59    })
60}
61
62/// Blocking convenience wrapper around [`execute`] for non-async callers.
63///
64/// Spins up a fresh current-thread Tokio runtime on a dedicated thread, so it is
65/// safe to call from synchronous code even when an outer runtime exists. Async
66/// callers should use [`execute`] directly.
67///
68/// # Errors
69///
70/// Same as [`execute`].
71pub fn execute_blocking(view: &ViewDefinition, resources: &[Value]) -> Result<ViewResult> {
72    std::thread::scope(|s| {
73        s.spawn(|| {
74            let rt = tokio::runtime::Builder::new_current_thread()
75                .enable_all()
76                .build()
77                .map_err(|e| Error::FhirPath(format!("building runtime: {e}")))?;
78            rt.block_on(execute(view, resources))
79        })
80        .join()
81        .map_err(|_| Error::FhirPath("evaluation thread panicked".to_string()))?
82    })
83}
84
85/// A ViewDefinition compiled once for repeated, resource-at-a-time execution.
86///
87/// Validates the view (columns, name collisions, constants) up front and builds
88/// the FHIRPath engine once, then lets callers stream resources through
89/// [`CompiledView::execute_resource`] without holding the whole dataset in
90/// memory.
91pub struct CompiledView {
92    ev: Evaluator,
93    selects: Vec<SelectColumn>,
94    where_paths: Vec<String>,
95    shape: Vec<(String, ColumnType)>,
96    columns: Vec<ColumnInfo>,
97}
98
99impl CompiledView {
100    /// Compile and validate a ViewDefinition, building the FHIRPath engine.
101    pub async fn compile(view: &ViewDefinition) -> Result<Self> {
102        if view.resource.trim().is_empty() {
103            return Err(Error::InvalidViewDefinition(
104                "ViewDefinition is missing the required `resource`".to_string(),
105            ));
106        }
107        let constants = build_constants(view)?;
108
109        // Build the engine once with the SoF custom functions registered. No
110        // network, no FHIR package: EmptyModelProvider keeps the library
111        // self-contained (ofType choice resolution is handled by string rewrite
112        // below, not by the model provider).
113        let mut registry = create_function_registry();
114        registry.register_lazy_function(Arc::new(GetResourceKey));
115        registry.register_lazy_function(Arc::new(GetReferenceKey));
116        // SoF-divergent function semantics: override the engine's defaults to
117        // match the SQL-on-FHIR conformance suite (join() with no separator,
118        // and the spec's precision-boundary rules for low/highBoundary).
119        registry.register_pure_function(Arc::new(JoinFn));
120        let model: Arc<dyn ModelProvider + Send + Sync> = Arc::new(EmptyModelProvider);
121        let engine = FhirPathEngine::new(Arc::new(registry), model.clone())
122            .await
123            .map_err(|e| Error::FhirPath(format!("building FHIRPath engine: {e}")))?;
124
125        let ev = Evaluator {
126            resource_type: view.resource.clone(),
127            constants,
128            engine,
129            model,
130        };
131
132        let shape = ev.shape(&view.select)?;
133        if shape.is_empty() {
134            return Err(Error::InvalidViewDefinition(
135                "ViewDefinition produces no columns".to_string(),
136            ));
137        }
138        let mut seen = std::collections::HashSet::new();
139        for (name, _) in &shape {
140            if !seen.insert(name.clone()) {
141                return Err(Error::InvalidViewDefinition(format!(
142                    "column `{name}` is defined more than once"
143                )));
144            }
145        }
146
147        // Validate/normalize every where path up front (constants + ofType
148        // rewrite), so per-resource evaluation just hands strings to the engine.
149        let where_paths = view
150            .where_
151            .iter()
152            .map(|w| ev.prepare(&w.path))
153            .collect::<Result<Vec<_>>>()?;
154
155        let columns = shape
156            .iter()
157            .map(|(name, ty)| ColumnInfo::new(name.clone(), *ty))
158            .collect();
159
160        Ok(Self {
161            ev,
162            selects: view.select.clone(),
163            where_paths,
164            shape,
165            columns,
166        })
167    }
168
169    /// The output columns, in order.
170    pub fn columns(&self) -> &[ColumnInfo] {
171        &self.columns
172    }
173
174    /// The FHIR resource type this view selects from.
175    pub fn resource_type(&self) -> &str {
176        &self.ev.resource_type
177    }
178
179    /// Evaluate the view against a single resource, returning its rows (column
180    /// values in column order). Resources of a different type, or filtered out
181    /// by `where`, yield no rows.
182    pub async fn execute_resource(&self, resource: &Value) -> Result<Vec<Vec<Value>>> {
183        if resource.get("resourceType").and_then(Value::as_str)
184            != Some(self.ev.resource_type.as_str())
185        {
186            return Ok(Vec::new());
187        }
188        for path in &self.where_paths {
189            if !self.ev.eval_where(path, resource).await? {
190                return Ok(Vec::new());
191            }
192        }
193
194        let mut combos = vec![Map::new()];
195        for select in &self.selects {
196            let srows = self.ev.eval_select(select, resource, 0).await?;
197            combos = cartesian(&combos, &srows);
198        }
199
200        Ok(combos
201            .iter()
202            .map(|row| {
203                self.shape
204                    .iter()
205                    .map(|(name, _)| row.get(name).cloned().unwrap_or(Value::Null))
206                    .collect()
207            })
208            .collect())
209    }
210}
211
212/// The cartesian product of two row sets, merging each pair of maps.
213fn cartesian(a: &[Map<String, Value>], b: &[Map<String, Value>]) -> Vec<Map<String, Value>> {
214    let mut out = Vec::with_capacity(a.len() * b.len());
215    for x in a {
216        for y in b {
217            let mut merged = x.clone();
218            for (k, v) in y {
219                merged.insert(k.clone(), v.clone());
220            }
221            out.push(merged);
222        }
223    }
224    out
225}
226
227struct Evaluator {
228    resource_type: String,
229    constants: std::collections::HashMap<String, String>,
230    engine: FhirPathEngine,
231    model: Arc<dyn ModelProvider + Send + Sync>,
232}
233
234impl Evaluator {
235    /// Normalize a FHIRPath path string for the engine: substitute view
236    /// constants (`%name`) into the string, then rewrite `ofType()` over FHIR
237    /// choice elements (the engine, on an `EmptyModelProvider`, cannot map
238    /// `value.ofType(Quantity)` → `valueQuantity` itself).
239    fn prepare(&self, path: &str) -> Result<String> {
240        let substituted = substitute_constants(path, &self.constants)?;
241        Ok(rewrite_of_type(&substituted))
242    }
243
244    /// Evaluate a (prepared) FHIRPath expression against `focus`, with
245    /// `%rowIndex` bound to `rid`, returning the result as a FHIRPath collection
246    /// of JSON values.
247    async fn eval_prepared(&self, path: &str, focus: &Value, rid: i64) -> Result<Vec<Value>> {
248        let ctx = EvaluationContext::new(
249            Collection::from(vec![FhirPathValue::resource(focus.clone())]),
250            self.model.clone(),
251            None,
252            None,
253            None,
254        );
255        // %rowIndex: the engine resolves `%rowIndex` by stripping `%` and
256        // looking up `rowIndex`.
257        ctx.set_variable("rowIndex".to_string(), FhirPathValue::integer(rid));
258
259        let result = self
260            .engine
261            .evaluate(path, &ctx)
262            .await
263            .map_err(|e| Error::FhirPath(e.to_string()))?;
264
265        Ok(collection_to_json(&result.value))
266    }
267
268    /// Prepare and evaluate a path in one step.
269    ///
270    /// `lowBoundary()`/`highBoundary()` diverge from the engine's stock
271    /// behaviour, so they are intercepted here: the base is evaluated by the
272    /// engine and the SoF precision-boundary rules are applied to each value.
273    /// The temporal flavour (`date`/`dateTime`/`time`) is taken from a leading
274    /// `ofType(T)` on the base before the `ofType` rewrite collapses it.
275    async fn eval_path(&self, path: &str, focus: &Value, rid: i64) -> Result<Vec<Value>> {
276        if let Some((base, low, hint)) = boundary_call(path) {
277            let base_vals = Box::pin(self.eval_path(&base, focus, rid)).await?;
278            return Ok(base_vals
279                .iter()
280                .map(|v| boundary(v, low, hint))
281                .filter(|v| !v.is_null())
282                .collect());
283        }
284        let prepared = self.prepare(path)?;
285        self.eval_prepared(&prepared, focus, rid).await
286    }
287
288    /// The ordered column shape of a select list, validating `unionAll` branch
289    /// consistency.
290    fn shape(&self, selects: &[SelectColumn]) -> Result<Vec<(String, ColumnType)>> {
291        let mut cols = Vec::new();
292        for select in selects {
293            cols.extend(self.shape_of(select)?);
294        }
295        Ok(cols)
296    }
297
298    fn shape_of(&self, select: &SelectColumn) -> Result<Vec<(String, ColumnType)>> {
299        let mut cols = Vec::new();
300        if let Some(columns) = &select.column {
301            for col in columns {
302                cols.push((col.name.clone(), column_type(col)));
303            }
304        }
305        for nested in &select.select {
306            cols.extend(self.shape_of(nested)?);
307        }
308        if let Some(branches) = &select.union_all {
309            let mut shapes = branches
310                .iter()
311                .map(|b| self.shape_of(b))
312                .collect::<Result<Vec<_>>>()?;
313            if let Some(first) = shapes.first() {
314                let first_names: Vec<&str> = first.iter().map(|(n, _)| n.as_str()).collect();
315                for other in &shapes[1..] {
316                    let names: Vec<&str> = other.iter().map(|(n, _)| n.as_str()).collect();
317                    if names != first_names {
318                        return Err(Error::InvalidViewDefinition(
319                            "unionAll branches have mismatched column shape".to_string(),
320                        ));
321                    }
322                }
323                cols.extend(shapes.swap_remove(0));
324            }
325        }
326        Ok(cols)
327    }
328
329    /// Evaluate a select node against a focus value, yielding partial rows.
330    async fn eval_select(
331        &self,
332        select: &SelectColumn,
333        ctx: &Value,
334        rid: i64,
335    ) -> Result<Vec<Map<String, Value>>> {
336        let for_each = select.for_each.as_deref();
337        let for_each_or_null = select.for_each_or_null.as_deref();
338
339        // `repeat` recursively traverses the focus, collecting every node reached
340        // by transitively re-applying the repeat path(s) (preorder, the focus
341        // node itself excluded). %rowIndex tracks position in the flattened list.
342        if !select.repeat.is_empty() {
343            let paths = select
344                .repeat
345                .iter()
346                .map(|p| self.prepare(p))
347                .collect::<Result<Vec<_>>>()?;
348            let mut foci = Vec::new();
349            self.repeat_collect(&paths, ctx, &mut foci).await?;
350            let mut rows = Vec::new();
351            for (idx, focus) in foci.iter().enumerate() {
352                rows.extend(Box::pin(self.eval_level(select, focus, idx as i64)).await?);
353            }
354            return Ok(rows);
355        }
356
357        if let Some(path) = for_each.or(for_each_or_null) {
358            let elements = self.eval_path(path, ctx, rid).await?;
359            if elements.is_empty() {
360                if for_each_or_null.is_some() {
361                    return Ok(vec![self.null_row(select)]);
362                }
363                return Ok(Vec::new());
364            }
365            let mut rows = Vec::new();
366            for (idx, elem) in elements.iter().enumerate() {
367                rows.extend(Box::pin(self.eval_level(select, elem, idx as i64)).await?);
368            }
369            Ok(rows)
370        } else {
371            self.eval_level(select, ctx, rid).await
372        }
373    }
374
375    /// Evaluate this select's columns, nested selects and unionAll against an
376    /// already-focused value (after any forEach has selected the element).
377    async fn eval_level(
378        &self,
379        select: &SelectColumn,
380        ctx: &Value,
381        rid: i64,
382    ) -> Result<Vec<Map<String, Value>>> {
383        let mut own = Map::new();
384        if let Some(columns) = &select.column {
385            for col in columns {
386                own.insert(col.name.clone(), self.column_value(col, ctx, rid).await?);
387            }
388        }
389        let mut combos = vec![own];
390
391        for nested in &select.select {
392            let nrows = Box::pin(self.eval_select(nested, ctx, rid)).await?;
393            combos = cartesian(&combos, &nrows);
394        }
395
396        if let Some(branches) = &select.union_all {
397            let mut branch_rows = Vec::new();
398            for branch in branches {
399                branch_rows.extend(Box::pin(self.eval_select(branch, ctx, rid)).await?);
400            }
401            combos = cartesian(&combos, &branch_rows);
402        }
403
404        Ok(combos)
405    }
406
407    /// Preorder transitive closure of the `repeat` path(s): apply every path to
408    /// `node`, push each result, then recurse into it. The starting node itself
409    /// is not emitted.
410    async fn repeat_collect(
411        &self,
412        paths: &[String],
413        node: &Value,
414        out: &mut Vec<Value>,
415    ) -> Result<()> {
416        // Iterative worklist (preorder) to avoid recursive async lifetimes.
417        let mut stack: Vec<Value> = Vec::new();
418        // Seed in reverse so that, popped LIFO, children come out in source order.
419        let mut seed = Vec::new();
420        for path in paths {
421            seed.extend(self.eval_prepared(path, node, 0).await?);
422        }
423        for child in seed.into_iter().rev() {
424            stack.push(child);
425        }
426        while let Some(current) = stack.pop() {
427            out.push(current.clone());
428            let mut children = Vec::new();
429            for path in paths {
430                children.extend(self.eval_prepared(path, &current, 0).await?);
431            }
432            for child in children.into_iter().rev() {
433                stack.push(child);
434            }
435        }
436        Ok(())
437    }
438
439    /// An all-null row for an empty `forEachOrNull`. Per spec, columns whose path
440    /// is `%rowIndex` are bound to 0 rather than null.
441    fn null_row(&self, select: &SelectColumn) -> Map<String, Value> {
442        let mut row = Map::new();
443        self.null_fill(select, &mut row);
444        row
445    }
446
447    fn null_fill(&self, select: &SelectColumn, row: &mut Map<String, Value>) {
448        if let Some(columns) = &select.column {
449            for col in columns {
450                let v = if col.path.trim() == "%rowIndex" {
451                    Value::Number(0.into())
452                } else {
453                    Value::Null
454                };
455                row.insert(col.name.clone(), v);
456            }
457        }
458        for nested in &select.select {
459            self.null_fill(nested, row);
460        }
461        if let Some(first) = select.union_all.as_ref().and_then(|b| b.first()) {
462            self.null_fill(first, row);
463        }
464    }
465
466    async fn column_value(&self, col: &Column, ctx: &Value, rid: i64) -> Result<Value> {
467        let values = self.eval_path(&col.path, ctx, rid).await?;
468        if col.collection.unwrap_or(false) {
469            return Ok(Value::Array(values));
470        }
471        match values.len() {
472            0 => Ok(Value::Null),
473            1 => Ok(values.into_iter().next().unwrap()),
474            _ => Err(Error::InvalidPath(format!(
475                "column `{}` yields multiple values but is not a collection",
476                col.name
477            ))),
478        }
479    }
480
481    /// Evaluate a top-level `where` filter. The expression must yield a boolean
482    /// (or empty); a non-boolean result is an error per the spec. Empty → row
483    /// excluded.
484    async fn eval_where(&self, path: &str, resource: &Value) -> Result<bool> {
485        let coll = self.eval_prepared(path, resource, 0).await?;
486        if coll.is_empty() {
487            return Ok(false);
488        }
489        if coll.iter().any(|v| !v.is_boolean()) {
490            return Err(Error::InvalidViewDefinition(
491                "where path does not evaluate to a boolean".to_string(),
492            ));
493        }
494        Ok(coll.iter().any(|v| v == &Value::Bool(true)))
495    }
496}
497
498/// Convert a FHIRPath [`Collection`] into a flat `Vec` of JSON values, dropping
499/// `null`/empty entries so navigation results round-trip the way the conformance
500/// `expect` multisets assume.
501fn collection_to_json(coll: &Collection) -> Vec<Value> {
502    coll.iter()
503        .map(value_to_json)
504        .filter(|v| !v.is_null())
505        .collect()
506}
507
508/// Convert a single [`FhirPathValue`] to JSON. A nested collection flattens
509/// (collection columns are arrays at the row level, not nested here).
510fn value_to_json(v: &FhirPathValue) -> Value {
511    match v {
512        FhirPathValue::Collection(c) => {
513            let items: Vec<Value> = c
514                .iter()
515                .map(value_to_json)
516                .filter(|x| !x.is_null())
517                .collect();
518            if items.len() == 1 {
519                items.into_iter().next().unwrap()
520            } else {
521                Value::Array(items)
522            }
523        }
524        FhirPathValue::Empty => Value::Null,
525        other => other.to_json_value(),
526    }
527}
528
529/// Rewrite `<base>.ofType(Type)` so a FHIR choice element collapses onto the
530/// concrete property (`value.ofType(Quantity)` → `valueQuantity`,
531/// `value.ofType(string)` → `valueString`), matching how the SQL generator and
532/// the FHIR JSON representation name choice elements. The type name is
533/// capitalized exactly as the choice-element suffix requires. The engine, on an
534/// `EmptyModelProvider`, cannot do this mapping itself.
535fn rewrite_of_type(path: &str) -> String {
536    let bytes = path.as_bytes();
537    let mut out = String::with_capacity(path.len());
538    let mut i = 0;
539    while i < bytes.len() {
540        // Look for the literal `.ofType(` at position i.
541        if path[i..].starts_with(".ofType(") {
542            // The base identifier is the trailing run of [A-Za-z0-9_] already in
543            // `out`. Pop it so we can fuse the capitalized type onto it.
544            let base_start = out
545                .rfind(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
546                .map(|p| p + 1)
547                .unwrap_or(0);
548            let base: String = out[base_start..].to_string();
549            // Find the matching close paren for the argument.
550            let arg_start = i + ".ofType(".len();
551            if let Some(rel_close) = path[arg_start..].find(')') {
552                let arg = path[arg_start..arg_start + rel_close].trim();
553                // Only a bare type identifier is a choice rewrite; anything else
554                // (shouldn't occur) is left to the engine.
555                if !base.is_empty() && is_simple_ident(arg) {
556                    out.truncate(base_start);
557                    out.push_str(&base);
558                    out.push_str(&capitalize_first(arg));
559                    i = arg_start + rel_close + 1;
560                    continue;
561                }
562            }
563        }
564        let ch = path[i..].chars().next().unwrap();
565        out.push(ch);
566        i += ch.len_utf8();
567    }
568    out
569}
570
571fn is_simple_ident(s: &str) -> bool {
572    !s.is_empty()
573        && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
574        && !s.chars().next().unwrap().is_ascii_digit()
575}
576
577fn capitalize_first(s: &str) -> String {
578    let mut chars = s.chars();
579    match chars.next() {
580        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
581        None => String::new(),
582    }
583}
584
585fn column_type(col: &Column) -> ColumnType {
586    if col.collection.unwrap_or(false) {
587        return ColumnType::Json;
588    }
589    // An `ansi/type` tag explicitly overrides the inferred column type.
590    if let Some(ansi) = ansi_type_tag(col) {
591        return ColumnType::from_ansi_type(ansi);
592    }
593    col.col_type
594        .as_deref()
595        .map(ColumnType::from_fhir_type)
596        .unwrap_or(ColumnType::String)
597}
598
599/// The value of a column's `ansi/type` tag, if present.
600pub(crate) fn ansi_type_tag(col: &Column) -> Option<&str> {
601    col.tag
602        .iter()
603        .find(|t| t.name == "ansi/type")
604        .and_then(|t| t.value.as_deref())
605}
606
607// --- SoF custom FHIRPath functions ---------------------------------------
608//
609// `getResourceKey()` and `getReferenceKey([type])` are SQL-on-FHIR special
610// functions, not standard FHIRPath. They are registered as lazy functions so
611// they can read the evaluation context's root resource (for fragment `#id`
612// references resolving into `contained[]`). The first argument (if any) is a
613// bare type identifier read directly off the AST, never evaluated.
614
615/// The resource type carried by the contained resource with the given local id,
616/// if the root resource lists it under `contained[]`.
617fn contained_type(root: &Value, local_id: &str) -> Option<String> {
618    root.get("contained")
619        .and_then(Value::as_array)
620        .into_iter()
621        .flatten()
622        .find(|c| c.get("id").and_then(Value::as_str) == Some(local_id))
623        .and_then(|c| c.get("resourceType").and_then(Value::as_str))
624        .map(str::to_string)
625}
626
627/// `getReferenceKey()` on a single Reference value. Relative/absolute `Type/id`
628/// references key on the id (filtered by the optional expected type). Fragment
629/// `#id` references resolve into the root resource's `contained[]` and key on
630/// the local id, so the value matches `getResourceKey()` on that contained
631/// resource. Absolute URL and `urn:` references are not keyed.
632fn reference_key(item: &Value, root: &Value, want_type: Option<&str>) -> Option<String> {
633    let reference = item.get("reference")?.as_str()?;
634    if let Some(local) = reference.strip_prefix('#') {
635        if local.is_empty() {
636            return None;
637        }
638        let actual = contained_type(root, local)
639            .or_else(|| item.get("type").and_then(Value::as_str).map(str::to_string));
640        return match (want_type, actual.as_deref()) {
641            (Some(t), Some(a)) if t != a => None,
642            (Some(_), None) => None,
643            _ => Some(local.to_string()),
644        };
645    }
646    if reference.starts_with("urn:") || reference.contains("://") {
647        return None;
648    }
649    let trimmed = reference.trim_start_matches('/');
650    let mut parts = trimmed.split('/');
651    let rtype = parts.next()?;
652    let id = parts.next()?;
653    if id.is_empty() {
654        return None;
655    }
656    match want_type {
657        Some(t) if t != rtype => None,
658        _ => Some(id.to_string()),
659    }
660}
661
662/// The root resource JSON from the evaluation context, falling back to the
663/// function input if no root is set.
664fn root_json(context: &EvaluationContext, input: &Collection) -> Value {
665    if let Some(root) = context.root_resource_value() {
666        root.to_json_value()
667    } else if let Some(first) = input.first() {
668        first.to_json_value()
669    } else {
670        Value::Null
671    }
672}
673
674struct GetResourceKey;
675
676#[async_trait]
677impl LazyFunctionEvaluator for GetResourceKey {
678    async fn evaluate(
679        &self,
680        input: Collection,
681        _context: &EvaluationContext,
682        _args: Vec<ExpressionNode>,
683        _evaluator: AsyncNodeEvaluator<'_>,
684    ) -> octofhir_fhirpath::Result<octofhir_fhirpath::EvaluationResult> {
685        // getResourceKey() keys a resource on its `id`.
686        let values: Vec<FhirPathValue> = input
687            .iter()
688            .filter_map(|v| {
689                v.to_json_value()
690                    .get("id")
691                    .and_then(Value::as_str)
692                    .map(|id| FhirPathValue::string(id.to_string()))
693            })
694            .collect();
695        Ok(octofhir_fhirpath::EvaluationResult::from_values(values))
696    }
697
698    fn metadata(&self) -> &FunctionMetadata {
699        static META: std::sync::LazyLock<FunctionMetadata> =
700            std::sync::LazyLock::new(|| sof_fn_metadata("getResourceKey", 0, Some(0)));
701        &META
702    }
703}
704
705struct GetReferenceKey;
706
707#[async_trait]
708impl LazyFunctionEvaluator for GetReferenceKey {
709    async fn evaluate(
710        &self,
711        input: Collection,
712        context: &EvaluationContext,
713        args: Vec<ExpressionNode>,
714        _evaluator: AsyncNodeEvaluator<'_>,
715    ) -> octofhir_fhirpath::Result<octofhir_fhirpath::EvaluationResult> {
716        // Optional bare type identifier, read directly off the AST (not
717        // evaluated): getReferenceKey(Patient).
718        let want_type = match args.first() {
719            Some(node) => Some(ast_type_name(node)?),
720            None => None,
721        };
722        let root = root_json(context, &input);
723        let values: Vec<FhirPathValue> = input
724            .iter()
725            .filter_map(|v| reference_key(&v.to_json_value(), &root, want_type.as_deref()))
726            .map(FhirPathValue::string)
727            .collect();
728        Ok(octofhir_fhirpath::EvaluationResult::from_values(values))
729    }
730
731    fn metadata(&self) -> &FunctionMetadata {
732        static META: std::sync::LazyLock<FunctionMetadata> =
733            std::sync::LazyLock::new(|| sof_fn_metadata("getReferenceKey", 0, Some(1)));
734        &META
735    }
736}
737
738/// Extract a bare type name from an argument node (`Patient`, `Organization`,
739/// or a `System.String`-style qualified type). Errors on anything else.
740fn ast_type_name(node: &ExpressionNode) -> octofhir_fhirpath::Result<String> {
741    match node {
742        ExpressionNode::Identifier(n) => Ok(n.name.clone()),
743        ExpressionNode::TypeInfo(t) => Ok(t.name.clone()),
744        ExpressionNode::PropertyAccess(p) => match p.object.as_ref() {
745            // Qualified type name like FHIR.Patient — take the trailing segment.
746            ExpressionNode::Identifier(_) => Ok(p.property.clone()),
747            _ => Err(FhirPathError::evaluation_error(
748                FP0053,
749                "getReferenceKey() type argument must be a type name",
750            )),
751        },
752        _ => Err(FhirPathError::evaluation_error(
753            FP0053,
754            "getReferenceKey() type argument must be a type name",
755        )),
756    }
757}
758
759/// Minimal metadata for a SoF custom function (polymorphic input, no model or
760/// terminology dependency).
761fn sof_fn_metadata(name: &str, min: usize, max: Option<usize>) -> FunctionMetadata {
762    FunctionMetadata {
763        name: name.to_string(),
764        description: format!("SQL-on-FHIR {name}()"),
765        signature: FunctionSignature {
766            input_type: "Any".to_string(),
767            parameters: Vec::new(),
768            return_type: "String".to_string(),
769            polymorphic: true,
770            min_params: min,
771            max_params: max,
772        },
773        argument_evaluation: Default::default(),
774        null_propagation: Default::default(),
775        empty_propagation: EmptyPropagation::NoPropagation,
776        deterministic: true,
777        category: FunctionCategory::Utility,
778        requires_terminology: false,
779        requires_model: false,
780    }
781}
782
783// --- SoF-divergent overrides: join / lowBoundary / highBoundary -----------
784//
785// These three functions are overridden so the in-memory path matches the
786// SQL-on-FHIR conformance suite, where the engine's stock behaviour differs:
787//   * join() — a zero-argument call defaults to the empty separator (and an
788//     empty input collection yields empty, not "").
789//   * low/highBoundary() — the spec's precision-boundary rules over decimals
790//     and partial date/dateTime/time strings.
791
792/// The FHIR primitive string representation of a JSON value.
793fn value_to_string(v: &Value) -> String {
794    match v {
795        Value::String(s) => s.clone(),
796        Value::Bool(b) => b.to_string(),
797        Value::Number(n) => n.to_string(),
798        Value::Null => String::new(),
799        other => other.to_string(),
800    }
801}
802
803struct JoinFn;
804
805#[async_trait]
806impl PureFunctionEvaluator for JoinFn {
807    async fn evaluate(
808        &self,
809        input: Collection,
810        args: Vec<Collection>,
811    ) -> octofhir_fhirpath::Result<octofhir_fhirpath::EvaluationResult> {
812        // join() over an empty collection yields the empty collection (i.e.
813        // null), not an empty string. (FHIR/sql-on-fhir.js fn_join)
814        if input.is_empty() {
815            return Ok(octofhir_fhirpath::EvaluationResult::from_values(Vec::new()));
816        }
817        let sep = match args.first().and_then(|c| c.first()) {
818            Some(v) => value_to_string(&v.to_json_value()),
819            None => String::new(),
820        };
821        let parts: Vec<String> = input
822            .iter()
823            .map(|v| value_to_string(&v.to_json_value()))
824            .collect();
825        Ok(octofhir_fhirpath::EvaluationResult::from_values(vec![
826            FhirPathValue::string(parts.join(&sep)),
827        ]))
828    }
829
830    fn metadata(&self) -> &FunctionMetadata {
831        static META: std::sync::LazyLock<FunctionMetadata> =
832            std::sync::LazyLock::new(|| sof_fn_metadata("join", 0, Some(1)));
833        &META
834    }
835}
836
837/// The FHIR temporal type a boundary operates on, inferred from a leading
838/// `ofType(T)` on the base.
839#[derive(Debug, Clone, Copy)]
840enum BoundaryType {
841    Date,
842    DateTime,
843    Time,
844    Unknown,
845}
846
847/// If `path` is a `…lowBoundary()`/`…highBoundary()` call, return the base path
848/// string, the low flag, and the temporal hint from a leading `ofType(T)`.
849fn boundary_call(path: &str) -> Option<(String, bool, BoundaryType)> {
850    let ast = octofhir_fhirpath::parse_ast(path).ok()?;
851    let (object, method) = match &ast {
852        ExpressionNode::MethodCall(m)
853            if m.method == "lowBoundary" || m.method == "highBoundary" =>
854        {
855            (m.object.as_ref(), m.method.as_str())
856        }
857        _ => return None,
858    };
859    let low = method == "lowBoundary";
860    let hint = boundary_hint(object);
861    Some((unparse(object), low, hint))
862}
863
864/// Recover a FHIRPath source string for a navigation/`ofType` sub-expression,
865/// enough to re-evaluate the boundary's base through the engine.
866fn unparse(node: &ExpressionNode) -> String {
867    match node {
868        ExpressionNode::Identifier(n) => n.name.clone(),
869        ExpressionNode::PropertyAccess(p) => format!("{}.{}", unparse(&p.object), p.property),
870        ExpressionNode::MethodCall(m) => {
871            let args: Vec<String> = m.arguments.iter().map(unparse).collect();
872            format!("{}.{}({})", unparse(&m.object), m.method, args.join(", "))
873        }
874        ExpressionNode::FunctionCall(f) => {
875            let args: Vec<String> = f.arguments.iter().map(unparse).collect();
876            format!("{}({})", f.name, args.join(", "))
877        }
878        ExpressionNode::Parenthesized(e) => format!("({})", unparse(e)),
879        ExpressionNode::TypeInfo(t) => t.name.clone(),
880        ExpressionNode::Literal(l) => match &l.value {
881            octofhir_fhirpath::LiteralValue::String(s) => format!("'{s}'"),
882            other => other.to_string(),
883        },
884        _ => String::new(),
885    }
886}
887
888fn boundary_hint(object: &ExpressionNode) -> BoundaryType {
889    match object {
890        ExpressionNode::MethodCall(m) if m.method == "ofType" => {
891            let name = match m.arguments.first() {
892                Some(ExpressionNode::Identifier(n)) => n.name.to_lowercase(),
893                Some(ExpressionNode::TypeInfo(t)) => t.name.to_lowercase(),
894                _ => return BoundaryType::Unknown,
895            };
896            match name.as_str() {
897                "datetime" | "instant" => BoundaryType::DateTime,
898                "date" => BoundaryType::Date,
899                "time" => BoundaryType::Time,
900                _ => BoundaryType::Unknown,
901            }
902        }
903        ExpressionNode::PropertyAccess(p) => boundary_hint(&p.object),
904        ExpressionNode::Parenthesized(e) => boundary_hint(e),
905        _ => BoundaryType::Unknown,
906    }
907}
908
909/// FHIR precision boundary; numbers use the decimal half-ulp, temporal strings
910/// are widened to their precision-filled bound.
911fn boundary(v: &Value, low: bool, hint: BoundaryType) -> Value {
912    match v {
913        Value::Number(n) => {
914            let text = n.to_string();
915            let scale = text
916                .split_once('.')
917                .map(|(_, frac)| frac.len())
918                .unwrap_or(0);
919            let half = 0.5 / 10f64.powi(scale as i32);
920            let base = n.as_f64().unwrap_or(0.0);
921            number_value(if low { base - half } else { base + half })
922        }
923        Value::String(s) => Value::String(boundary_string(s, low, hint)),
924        other => other.clone(),
925    }
926}
927
928fn number_value(f: f64) -> Value {
929    serde_json::Number::from_f64(f)
930        .map(Value::Number)
931        .unwrap_or(Value::Null)
932}
933
934/// Widen a partial date/dateTime/time string to its low/high precision bound,
935/// using the temporal `hint` where present (else inferred from the string).
936fn boundary_string(s: &str, low: bool, hint: BoundaryType) -> String {
937    let is_time = matches!(hint, BoundaryType::Time)
938        || (matches!(hint, BoundaryType::Unknown) && s.contains(':') && !s.contains('-'));
939    if is_time {
940        return match (s.len(), low) {
941            (2, true) => format!("{s}:00:00.000"),
942            (2, false) => format!("{s}:59:59.999"),
943            (5, true) => format!("{s}:00.000"),
944            (5, false) => format!("{s}:59.999"),
945            (8, true) => format!("{s}.000"),
946            (8, false) => format!("{s}.999"),
947            _ => s.to_string(),
948        };
949    }
950    let is_datetime = matches!(hint, BoundaryType::DateTime) || s.contains('T');
951    if is_datetime {
952        let time = if low {
953            "T00:00:00.000+14:00"
954        } else {
955            "T23:59:59.999-12:00"
956        };
957        return match (s.len(), low) {
958            (4, true) => format!("{s}-01-01{time}"),
959            (4, false) => format!("{s}-12-31{time}"),
960            (7, true) => format!("{s}-01{time}"),
961            (7, false) => format!("{s}-{}{time}", last_day_of_month(s)),
962            (10, _) => format!("{s}{time}"),
963            _ => s.to_string(),
964        };
965    }
966    match (s.len(), low) {
967        (4, true) => format!("{s}-01-01"),
968        (4, false) => format!("{s}-12-31"),
969        (7, true) => format!("{s}-01"),
970        (7, false) => format!("{s}-{}", last_day_of_month(s)),
971        _ => s.to_string(),
972    }
973}
974
975fn last_day_of_month(year_month: &str) -> String {
976    let mut it = year_month.split('-');
977    let year: i32 = it.next().and_then(|y| y.parse().ok()).unwrap_or(2000);
978    let month: u32 = it.next().and_then(|m| m.parse().ok()).unwrap_or(1);
979    let last = match month {
980        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
981        4 | 6 | 9 | 11 => 30,
982        2 if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 => 29,
983        2 => 28,
984        _ => 30,
985    };
986    format!("{last:02}")
987}