Skip to main content

haste_sql_on_fhir/
lib.rs

1use base64::{Engine as _, engine::general_purpose};
2use futures::future::{BoxFuture, try_join, try_join_all};
3use haste_fhir_client::{FHIRClient, url::ParsedParameters};
4use haste_fhir_generated_ops::generated::ViewDefinitionRun;
5use haste_fhir_model::r4::{
6    datetime::Instant,
7    generated::{
8        resources::{
9            Binary, BundleEntry, Resource, ResourceType, ViewDefinition, ViewDefinitionSelect,
10        },
11        terminology::{BoundCode, HttpVerb, IssueType, OutputFormatCodes},
12        types::{FHIRBase64Binary, FHIRBoolean, FHIRCode, FHIRInstant, FHIRString, Reference},
13    },
14};
15use haste_fhir_operation_error::OperationOutcomeError;
16use haste_fhirpath::{Config, Context, FPEngine};
17use haste_reflect::MetaValue;
18use itertools::Itertools as _;
19use ordermap::OrderMap;
20use serde::{Deserialize, Serialize};
21use std::{
22    borrow::Cow,
23    collections::{HashMap, HashSet},
24    sync::Arc,
25};
26
27use crate::conversions::primitives::PrimitiveValue;
28
29mod compartment;
30mod conversions;
31mod output;
32mod union;
33
34/// An undefined constant or an unparsable expression is the caller's mistake; any other
35/// `FHIRPath` failure stays a server error.
36fn fhirpath_issue_type(e: &haste_fhirpath::FHIRPathError) -> BoundCode<IssueType> {
37    match e {
38        haste_fhirpath::FHIRPathError::UndefinedConstant(_)
39        | haste_fhirpath::FHIRPathError::ParseError(_) => IssueType::invalid(),
40        _ => IssueType::exception(),
41    }
42}
43
44fn reference_value(reference: &Reference) -> Result<String, OperationOutcomeError> {
45    reference
46        .reference
47        .as_ref()
48        .and_then(|r| r.value.clone())
49        .ok_or_else(|| {
50            OperationOutcomeError::error(
51                IssueType::invalid(),
52                "Reference.reference is required".to_string(),
53            )
54        })
55}
56
57fn invalid(message: String) -> OperationOutcomeError {
58    OperationOutcomeError::error(IssueType::invalid(), message)
59}
60
61/// The id a `<resource_type>/<id>` reference names, if it names one of that type.
62fn reference_id<'a>(reference: &'a str, resource_type: &str) -> Option<&'a str> {
63    reference
64        .strip_prefix(resource_type)
65        .and_then(|rest| rest.strip_prefix('/'))
66        .and_then(|rest| rest.split('/').next())
67        .filter(|id| !id.is_empty())
68}
69
70/// Reads the current version of each `(type, id)` in one round trip, leaving out the keys that
71/// don't resolve. The server backs it with the repository's batch read, so a run's `patient` and
72/// `group` lookups cost one query however many Groups are given.
73pub type ReadMany<CTX> = Arc<
74    dyn Fn(
75            CTX,
76            Vec<(ResourceType, String)>,
77        ) -> BoxFuture<'static, Result<Vec<Resource>, OperationOutcomeError>>
78        + Send
79        + Sync,
80>;
81
82/// Keys one batch read takes.
83const SCOPE_KEYS_PER_READ: usize = 1000;
84
85/// The id the scope `parameter` names, refused when it isn't a reference to `resource_type`.
86fn scope_id(
87    parameter: &str,
88    reference: &Reference,
89    resource_type: &ResourceType,
90) -> Result<String, OperationOutcomeError> {
91    let value = reference_value(reference)?;
92    reference_id(&value, resource_type.as_ref())
93        .map(str::to_string)
94        .ok_or_else(|| {
95            invalid(format!(
96                "'{parameter}' must reference a {}, not '{value}'.",
97                resource_type.as_ref()
98            ))
99        })
100}
101
102/// The patients a run is limited to: `None` without `patient` or `group` (or with resources sent
103/// inline), else the ids of the Patients every given parameter allows. Both are limits, so with
104/// both a run covers only the `patient` and only if it is in a group. An empty list means no
105/// resource qualifies.
106///
107/// The Patient and the Groups are read in one batch; one the server doesn't have is refused.
108/// Members that aren't Patients are left out: only the Patient compartment is supported.
109async fn patient_scope<CTX: Clone>(
110    context: CTX,
111    read_many: &ReadMany<CTX>,
112    input: &ViewDefinitionRun::Input,
113) -> Result<Option<Vec<String>>, OperationOutcomeError> {
114    if input.resource.is_some() {
115        return Ok(None);
116    }
117
118    let patient = input
119        .patient
120        .as_ref()
121        .map(|reference| scope_id("patient", reference, &ResourceType::Patient))
122        .transpose()?;
123    let groups = input
124        .group
125        .iter()
126        .flatten()
127        .map(|reference| scope_id("group", reference, &ResourceType::Group))
128        .collect::<Result<Vec<_>, _>>()?;
129    if patient.is_none() && groups.is_empty() {
130        return Ok(None);
131    }
132
133    let keys = patient
134        .iter()
135        .map(|id| (ResourceType::Patient, id.clone()))
136        .chain(groups.iter().map(|id| (ResourceType::Group, id.clone())))
137        .collect::<Vec<_>>();
138    let mut found = HashMap::new();
139    for chunk in keys.chunks(SCOPE_KEYS_PER_READ) {
140        for resource in read_many(context.clone(), chunk.to_vec()).await? {
141            if let Some(id) = resource.id().clone() {
142                found.insert((resource.resource_type(), id), resource);
143            }
144        }
145    }
146
147    if let Some(id) = &patient
148        && !found.contains_key(&(ResourceType::Patient, id.clone()))
149    {
150        return Err(invalid(format!("Patient with id '{id}' not found.")));
151    }
152
153    let mut members = Vec::new();
154    for id in &groups {
155        let Some(Resource::Group(group)) = found.get(&(ResourceType::Group, id.clone())) else {
156            return Err(invalid(format!("Group with id '{id}' not found.")));
157        };
158        members.extend(group.member.iter().flatten().filter_map(|member| {
159            let entity = member.entity.reference.as_ref()?.value.as_ref()?;
160            reference_id(entity, ResourceType::Patient.as_ref()).map(str::to_string)
161        }));
162    }
163    members.sort_unstable();
164    members.dedup();
165
166    Ok(Some(match patient {
167        Some(patient) if groups.is_empty() => vec![patient],
168        Some(patient) => members
169            .into_iter()
170            .filter(|member| *member == patient)
171            .collect(),
172        None => members,
173    }))
174}
175
176async fn resolve_view_definition<
177    'a,
178    CTX: Send + Sync + Clone + 'static,
179    Client: FHIRClient<CTX, OperationOutcomeError> + Send + Sync + 'static,
180>(
181    context: CTX,
182    client: &Client,
183    input: &'a ViewDefinitionRun::Input,
184) -> Result<Cow<'a, ViewDefinition>, OperationOutcomeError> {
185    if input.viewResource.is_some() && input.viewReference.is_some() {
186        return Err(invalid(
187            "Only one of viewResource and viewReference may be provided.".to_string(),
188        ));
189    }
190
191    if let Some(view_definition) = &input.viewResource {
192        Ok(Cow::Borrowed(view_definition))
193    } else if let Some(view_definition_reference) = input.viewReference.as_ref() {
194        let view_definition_reference = view_definition_reference
195            .reference
196            .as_ref()
197            .ok_or_else(|| {
198                OperationOutcomeError::error(
199                    IssueType::invalid(),
200                    "viewReference.reference is required".to_string(),
201                )
202            })?
203            .value
204            .as_ref()
205            .ok_or_else(|| {
206                OperationOutcomeError::error(
207                    IssueType::invalid(),
208                    "viewReference.reference.value is required".to_string(),
209                )
210            })?;
211
212        let reference_pieces = view_definition_reference.split('/').collect::<Vec<_>>();
213
214        let view_definition_id = reference_pieces
215            .last()
216            .ok_or_else(|| {
217                OperationOutcomeError::error(
218                    IssueType::invalid(),
219                    "Invalid viewReference.reference format".to_string(),
220                )
221            })?
222            .to_string();
223
224        let result = client
225            .read(
226                context,
227                ResourceType::ViewDefinition,
228                view_definition_id.clone(),
229            )
230            .await?
231            .ok_or_else(|| {
232                OperationOutcomeError::error(
233                    IssueType::not_found(),
234                    format!("ViewDefinition not found with id '{view_definition_id:?}'"),
235                )
236            })?;
237
238        if let Resource::ViewDefinition(view_definition) = result {
239            Ok(Cow::Owned(view_definition))
240        } else {
241            Err(OperationOutcomeError::error(
242                IssueType::invalid(),
243                "Referenced resource is not a ViewDefinition".to_string(),
244            ))
245        }
246    } else {
247        Err(OperationOutcomeError::error(
248            IssueType::invalid(),
249            "Either viewResource or viewReference must be provided".to_string(),
250        ))
251    }
252}
253
254/// Bounds on one run. The operation answers synchronously with one table, so a run that would
255/// pass either is refused rather than answered with part of the table.
256#[derive(Debug, Clone, Copy)]
257pub struct RunLimits {
258    /// Most rows a run returns. A larger `_limit` is refused.
259    pub max_rows: usize,
260    /// Most resources a run reads from storage, earlier versions included.
261    pub max_resources: usize,
262}
263
264impl Default for RunLimits {
265    fn default() -> Self {
266        Self {
267            max_rows: 10_000,
268            max_resources: 50_000,
269        }
270    }
271}
272
273/// Resources fetched per round trip.
274const PAGE_SIZE: usize = 1000;
275
276/// Patients one compartment search covers. Larger lists are searched in batches.
277const PATIENTS_PER_SEARCH: usize = 100;
278
279/// Deepest one search pages. Elasticsearch refuses `_offset` + `_count` past 10,000 by default
280/// (`index.max_result_window`), and deep offsets get slow on Postgres.
281const MAX_SEARCH_DEPTH: usize = 10_000;
282
283/// Refuses a run that would pass a limit: 422, as the request is well formed but past this
284/// server's limits (the same answer as an `ImplementationGuide/$install` that is too large).
285fn too_costly(what: &str) -> OperationOutcomeError {
286    let error = OperationOutcomeError::error(
287        IssueType::too_costly(),
288        format!("{what} Narrow the run with _limit, patient, group or _since."),
289    );
290    OperationOutcomeError::unprocessable(error.outcome().clone())
291}
292
293/// Refuses parameters this server doesn't take, before anything is read.
294fn check_parameters(
295    input: &ViewDefinitionRun::Input,
296    limits: &RunLimits,
297) -> Result<(), OperationOutcomeError> {
298    if input.source.is_some() {
299        return Err(invalid(
300            "The server does not support the source parameter.".to_string(),
301        ));
302    }
303
304    match requested_limit(input) {
305        Some(limit) if limit > limits.max_rows => Err(invalid(format!(
306            "_limit {limit} is above {}, the most rows one run returns.",
307            limits.max_rows
308        ))),
309        _ => Ok(()),
310    }
311}
312
313/// What every page of a run is processed with.
314struct Run<'v> {
315    fp_engine: &'v FPEngine,
316    variables: Arc<HashMap<String, &'v dyn MetaValue>>,
317    view_definition: &'v ViewDefinition,
318    where_clauses: Vec<&'v str>,
319    resource_type: ResourceType,
320    /// The caller's `_limit`.
321    limit: Option<usize>,
322    limits: RunLimits,
323}
324
325/// Adds the rows of `resources`: those of the view's type that pass its `where`. Returns whether
326/// the run has all the rows it asked for. Without `_limit`, more than `max_rows` refuses the run.
327async fn add_rows<'v>(
328    run: &Run<'v>,
329    resources: &[Resource],
330    rows: &mut Vec<Row<'v>>,
331) -> Result<bool, OperationOutcomeError> {
332    for resource in resources_of_type(resources, &run.resource_type) {
333        if !passes_where_clauses(
334            run.fp_engine,
335            run.variables.clone(),
336            &run.where_clauses,
337            resource,
338        )
339        .await?
340        {
341            continue;
342        }
343
344        rows.extend(
345            process_resource(
346                run.fp_engine,
347                run.variables.clone(),
348                run.view_definition,
349                resource,
350            )
351            .await?,
352        );
353
354        match run.limit {
355            Some(limit) if rows.len() >= limit => {
356                rows.truncate(limit);
357                return Ok(true);
358            }
359            None if rows.len() > run.limits.max_rows => {
360                return Err(too_costly(&format!(
361                    "The view gives more than {} rows, the most one run returns.",
362                    run.limits.max_rows
363                )));
364            }
365            _ => {}
366        }
367    }
368
369    Ok(false)
370}
371
372/// Counts `count` more resources read, refusing the run past `max_resources`.
373fn count_read(
374    read: &mut usize,
375    count: usize,
376    limits: &RunLimits,
377) -> Result<(), OperationOutcomeError> {
378    *read += count;
379    if *read > limits.max_resources {
380        return Err(too_costly(&format!(
381            "The run reads more than {} resources, the most one run reads.",
382            limits.max_resources
383        )));
384    }
385    Ok(())
386}
387
388/// The view's rows: over the resources sent with the request, else over the server's data,
389/// limited to the patients of `scope` (see [`patient_scope`]) and to `_since`.
390async fn view_rows<
391    'v,
392    CTX: Send + Sync + Clone + 'static,
393    Client: FHIRClient<CTX, OperationOutcomeError> + Send + Sync + 'static,
394>(
395    context: CTX,
396    client: &Client,
397    run: &Run<'v>,
398    input: &ViewDefinitionRun::Input,
399    scope: Option<Vec<String>>,
400) -> Result<Vec<Row<'v>>, OperationOutcomeError> {
401    let mut rows = Vec::new();
402
403    if let Some(resources) = &input.resource {
404        add_rows(run, resources, &mut rows).await?;
405        return Ok(rows);
406    }
407
408    let since = input.since.as_ref().and_then(|since| since.value.as_ref());
409
410    match scope {
411        None => rows_from_history(context, client, run, since, &mut rows).await?,
412        Some(patients) => {
413            rows_from_compartments(context, client, run, &patients, since, &mut rows).await?;
414        }
415    }
416
417    Ok(rows)
418}
419
420/// Rows from the current resources of the view's type, newest first. The type's history is read
421/// a page at a time; the first entry of an id is its current version, so later ones are skipped.
422async fn rows_from_history<
423    'v,
424    CTX: Send + Sync + Clone + 'static,
425    Client: FHIRClient<CTX, OperationOutcomeError> + Send + Sync + 'static,
426>(
427    context: CTX,
428    client: &Client,
429    run: &Run<'v>,
430    since: Option<&Instant>,
431    rows: &mut Vec<Row<'v>>,
432) -> Result<(), OperationOutcomeError> {
433    let mut seen = HashSet::new();
434    let mut read = 0;
435    let mut offset = 0;
436
437    loop {
438        let mut parameters = vec![
439            ("_count".to_string(), vec![PAGE_SIZE.to_string()]),
440            ("_offset".to_string(), vec![offset.to_string()]),
441        ];
442        if let Some(since) = since {
443            parameters.push(("_since".to_string(), vec![since.to_string()]));
444        }
445
446        let entries = client
447            .history_type(
448                context.clone(),
449                run.resource_type.clone(),
450                parameters.into(),
451            )
452            .await?
453            .entry
454            .unwrap_or_default();
455        let last_page = entries.len() < PAGE_SIZE;
456
457        count_read(&mut read, entries.len(), &run.limits)?;
458        let current = current_versions(entries, &mut seen, since);
459        if add_rows(run, &current, rows).await? || last_page {
460            return Ok(());
461        }
462
463        offset += PAGE_SIZE;
464    }
465}
466
467/// Rows from the compartments of `patients`, in id order. Each compartment parameter is searched
468/// a batch of patients and a page at a time. A resource in several compartments counts once.
469async fn rows_from_compartments<
470    'v,
471    CTX: Send + Sync + Clone + 'static,
472    Client: FHIRClient<CTX, OperationOutcomeError> + Send + Sync + 'static,
473>(
474    context: CTX,
475    client: &Client,
476    run: &Run<'v>,
477    patients: &[String],
478    since: Option<&Instant>,
479    rows: &mut Vec<Row<'v>>,
480) -> Result<(), OperationOutcomeError> {
481    let mut seen = HashSet::new();
482    let mut read = 0;
483
484    for parameter in compartment::patient_search_parameters(&run.resource_type)? {
485        for patients in patients.chunks(PATIENTS_PER_SEARCH) {
486            let mut offset = 0;
487
488            loop {
489                if offset >= MAX_SEARCH_DEPTH {
490                    return Err(too_costly(&format!(
491                        "More than {MAX_SEARCH_DEPTH} {} resources match one search of these \
492                         patients.",
493                        run.resource_type.as_ref()
494                    )));
495                }
496
497                let resources: Vec<Resource> = client
498                    .search_type(
499                        context.clone(),
500                        run.resource_type.clone(),
501                        compartment_search(&parameter, patients, since, offset),
502                    )
503                    .await?
504                    .entry
505                    .into_iter()
506                    .flatten()
507                    .filter_map(|entry| entry.resource.map(|resource| *resource))
508                    .collect();
509                let last_page = resources.len() < PAGE_SIZE;
510
511                count_read(&mut read, resources.len(), &run.limits)?;
512                let unseen: Vec<Resource> = resources
513                    .into_iter()
514                    .filter(|resource| seen.insert(resource.id().clone()))
515                    .collect();
516                if add_rows(run, &unseen, rows).await? {
517                    return Ok(());
518                }
519                if last_page {
520                    break;
521                }
522
523                offset += PAGE_SIZE;
524            }
525        }
526    }
527
528    Ok(())
529}
530
531/// One page of the resources whose compartment `parameter` names one of `patients`, sorted by id
532/// so pages don't overlap, and changed after `since` when given.
533fn compartment_search(
534    parameter: &str,
535    patients: &[String],
536    since: Option<&Instant>,
537    offset: usize,
538) -> ParsedParameters {
539    let values = patients
540        .iter()
541        .map(|id| {
542            if parameter == "_id" {
543                id.clone()
544            } else {
545                format!("{}/{id}", ResourceType::Patient.as_ref())
546            }
547        })
548        .collect();
549
550    let mut parameters = vec![
551        (parameter.to_string(), values),
552        ("_sort".to_string(), vec!["_id".to_string()]),
553        ("_count".to_string(), vec![PAGE_SIZE.to_string()]),
554        ("_offset".to_string(), vec![offset.to_string()]),
555    ];
556    if let Some(since) = since {
557        parameters.push((
558            "_lastUpdated".to_string(),
559            vec![format!("gt{}", since.to_string())],
560        ));
561    }
562
563    parameters.into()
564}
565
566/// The client-requested `_limit`, if any: the most output rows to return.
567fn requested_limit(input: &ViewDefinitionRun::Input) -> Option<usize> {
568    input
569        .limit
570        .as_ref()
571        .and_then(|limit| limit.value)
572        .and_then(|limit| usize::try_from(limit).ok())
573}
574
575/// The resource type a view runs over (`ViewDefinition.resource`).
576fn view_resource_type(
577    view_definition: &ViewDefinition,
578) -> Result<ResourceType, OperationOutcomeError> {
579    let Some(resource_type) = view_definition.resource.as_str() else {
580        return Err(OperationOutcomeError::error(
581            IssueType::invalid(),
582            "ViewDefinition.resource is required".to_string(),
583        ));
584    };
585
586    ResourceType::try_from(resource_type).map_err(|e| {
587        OperationOutcomeError::error(IssueType::invalid(), format!("Invalid resource type: {e}"))
588    })
589}
590
591/// The current version of each resource in a page of history, newest first as the history is.
592/// `seen` carries the ids of earlier pages: a view has one row set per resource, so earlier
593/// versions are dropped. A resource whose latest entry is a delete is left out, as is one not
594/// changed after `since` (history's `_since` also returns versions made at that instant).
595fn current_versions(
596    entries: Vec<BundleEntry>,
597    seen: &mut HashSet<Option<String>>,
598    since: Option<&Instant>,
599) -> Vec<Resource> {
600    entries
601        .into_iter()
602        .filter_map(|entry| {
603            let resource = entry.resource?;
604            if !seen.insert(resource.id().clone()) {
605                return None;
606            }
607
608            let deleted = entry
609                .request
610                .is_some_and(|request| request.method == HttpVerb::delete());
611
612            (!deleted && changed_after(&resource, since)).then_some(*resource)
613        })
614        .collect()
615}
616
617/// Whether `resource` changed after `since`. Always without `since`, and for a resource with no
618/// `meta.lastUpdated`, as the specification allows.
619fn changed_after(resource: &Resource, since: Option<&Instant>) -> bool {
620    let Some(Instant::Iso8601(since)) = since else {
621        return true;
622    };
623
624    resource
625        .get_field("meta")
626        .and_then(|meta| meta.get_field("lastUpdated"))
627        .and_then(|updated| updated.as_any().downcast_ref::<FHIRInstant>())
628        .and_then(|updated| updated.value.as_ref())
629        .is_none_or(|Instant::Iso8601(updated)| updated > since)
630}
631
632fn build_hashmap_fp_variables(viewdefinition: &ViewDefinition) -> HashMap<String, &dyn MetaValue> {
633    let mut hashmap = HashMap::new();
634
635    if let Some(constants) = &viewdefinition.constant {
636        for constant in constants {
637            // Pass the inner primitive: the choice enum's `as_any` is its Box, which number and boolean downcasts reject.
638            if let (Some(name), Some(value)) = (
639                constant.name.value.as_ref(),
640                constant.value.flatten().first().copied(),
641            ) {
642                hashmap.insert(name.clone(), value);
643            }
644        }
645    }
646
647    hashmap
648}
649
650/// Column values by name. Names borrow from the view.
651type Row<'a> = OrderMap<&'a str, OutputResults>;
652
653/// Cross product of two row sets, `left`'s columns first.
654fn join<'a>(left: &[Row<'a>], right: Vec<Row<'a>>) -> Vec<Row<'a>> {
655    let copy = |(&name, value): (&&'a str, &OutputResults)| (name, value.clone());
656
657    match left {
658        // The fold's empty start row.
659        [row] if row.is_empty() => right,
660        // Usual case: move the right rows instead of copying them.
661        [row] => right
662            .into_iter()
663            .map(|r| row.iter().map(copy).chain(r).collect())
664            .collect(),
665        _ => left
666            .iter()
667            .cartesian_product(&right)
668            .map(|(l, r)| l.iter().chain(r).map(copy).collect())
669            .collect(),
670    }
671}
672
673// Need to distinguish between a scalar value and a collection of values for each column in the output. This enum helps to represent that distinction.
674#[derive(Debug, Clone, Deserialize, Serialize)]
675#[serde(untagged)]
676enum OutputResults {
677    Scalar(Option<PrimitiveValue>),
678    Collection(Vec<Option<PrimitiveValue>>),
679}
680
681/// A resource's rows: the cross product of each select's rows. Rows borrow only from the view
682/// (`'v`), not from the resource (`'r`), so a page of resources can go once its rows are made.
683async fn process_resource<'r, 'v: 'r>(
684    fp_engine: &'r FPEngine,
685    variables: Arc<HashMap<String, &'r dyn MetaValue>>,
686    view_definition: &'v ViewDefinition,
687    input: &'r Resource,
688) -> Result<Vec<Row<'v>>, OperationOutcomeError> {
689    let fp_config = Arc::new(
690        Config::builder()
691            .with_variable_resolver(haste_fhirpath::ExternalConstantResolver::Variable(
692                variables,
693            ))
694            .with_resource_id(input.id().clone().unwrap_or_default()),
695    );
696
697    let per_select = select_rows(fp_engine, &fp_config, &view_definition.select, input).await?;
698
699    Ok(per_select
700        .into_iter()
701        .fold(vec![Row::new()], |rows, more| join(&rows, more)))
702}
703
704/// Evaluates `path` on `node`; `what` names the expression in errors.
705async fn evaluate<'a>(
706    fp_engine: &FPEngine,
707    fp_config: &Arc<Config<'a>>,
708    path: &str,
709    node: &'a dyn MetaValue,
710    what: &str,
711) -> Result<Context<'a>, OperationOutcomeError> {
712    fp_engine
713        .evaluate_with_config(path, vec![node], fp_config.clone())
714        .await
715        .map_err(|e| {
716            OperationOutcomeError::error(
717                fhirpath_issue_type(&e),
718                format!("Error evaluating {what}: {e}"),
719            )
720        })
721}
722
723/// Nodes and the evaluations that found them. Computed nodes live in an evaluation's arena, so
724/// keep the evaluations while the nodes are in use.
725type Nodes<'a> = (Vec<&'a dyn MetaValue>, Vec<Context<'a>>);
726
727fn nodes_of(found: Context<'_>) -> Nodes<'_> {
728    (found.values().to_vec(), vec![found])
729}
730
731/// Same limit as `FHIRPath` `repeat()`.
732const REPEAT_ITEM_LIMIT: usize = 10_000;
733
734/// Items a `repeat` reaches from `input`, depth first in document order. Each item is taken once,
735/// so every walk ends.
736async fn walk_repeat<'a>(
737    fp_engine: &FPEngine,
738    fp_config: &Arc<Config<'a>>,
739    paths: &[FHIRString],
740    input: &'a dyn MetaValue,
741) -> Result<Nodes<'a>, OperationOutcomeError> {
742    let (mut nodes, mut arenas) = (Vec::new(), Vec::new());
743    let mut seen = HashSet::new();
744    // The input is walked but is not an item.
745    let mut stack = vec![(input, false)];
746
747    while let Some((node, is_item)) = stack.pop() {
748        if is_item {
749            // Keyed by type too: a struct and its first field share an address.
750            if !seen.insert((
751                std::ptr::from_ref(node).cast::<()>().addr(),
752                node.fhir_type(),
753            )) {
754                continue;
755            }
756            if nodes.len() >= REPEAT_ITEM_LIMIT {
757                return Err(OperationOutcomeError::error(
758                    IssueType::exception(),
759                    format!("repeat produced more than {REPEAT_ITEM_LIMIT} items"),
760                ));
761            }
762            nodes.push(node);
763        }
764
765        // Reversed so the first child pops first.
766        let start = stack.len();
767        for path in paths.iter().filter_map(|path| path.value.as_deref()) {
768            let found = evaluate(fp_engine, fp_config, path, node, "repeat expression").await?;
769            if !found.is_empty() {
770                stack.extend(found.values().iter().map(|&child| (child, true)));
771                arenas.push(found);
772            }
773        }
774        stack[start..].reverse();
775    }
776
777    Ok((nodes, arenas))
778}
779
780/// Each select's rows on `node`, in order.
781async fn select_rows<'r, 'v: 'r>(
782    fp_engine: &'r FPEngine,
783    fp_config: &Arc<Config<'r>>,
784    selects: &'v [ViewDefinitionSelect],
785    node: &'r dyn MetaValue,
786) -> Result<Vec<Vec<Row<'v>>>, OperationOutcomeError> {
787    try_join_all(
788        selects
789            .iter()
790            .map(|select| process_select(fp_engine, fp_config.clone(), select, node)),
791    )
792    .await
793}
794
795/// A select's rows: per focus node, its columns joined with its nested and `unionAll` rows. The
796/// focus nodes are the `forEach`, `forEachOrNull` or `repeat` items, else the input.
797fn process_select<'r, 'v: 'r>(
798    fp_engine: &'r FPEngine,
799    fp_config: Arc<Config<'r>>,
800    select: &'v ViewDefinitionSelect,
801    input: &'r dyn MetaValue,
802) -> BoxFuture<'r, Result<Vec<Row<'v>>, OperationOutcomeError>> {
803    Box::pin(async move {
804        let for_each = select.forEach.as_ref().and_then(|p| p.value.as_deref());
805        let for_each_or_null = select
806            .forEachOrNull
807            .as_ref()
808            .and_then(|p| p.value.as_deref());
809
810        // `_arenas` keeps `nodes` valid until the rows are built.
811        let (nodes, _arenas) = match (for_each, for_each_or_null, select.repeat.as_deref()) {
812            (Some(path), _, _) => {
813                nodes_of(evaluate(fp_engine, &fp_config, path, input, "forEach expression").await?)
814            }
815            (None, Some(path), _) => nodes_of(
816                evaluate(
817                    fp_engine,
818                    &fp_config,
819                    path,
820                    input,
821                    "forEachOrNull expression",
822                )
823                .await?,
824            ),
825            (None, None, Some(paths)) => walk_repeat(fp_engine, &fp_config, paths, input).await?,
826            (None, None, None) => (vec![input], Vec::new()),
827        };
828
829        if nodes.is_empty() && for_each.is_none() && for_each_or_null.is_some() {
830            let mut null_row = Row::new();
831            collect_null_columns(select, &mut null_row)?;
832            return Ok(vec![null_row]);
833        }
834
835        let mut rows = Vec::with_capacity(nodes.len());
836        for node in nodes {
837            let columns = column_row(fp_engine, &fp_config, select, node).await?;
838            let nested = select_rows(
839                fp_engine,
840                &fp_config,
841                select.select.as_deref().unwrap_or_default(),
842                node,
843            )
844            .await?;
845            let union = match select.unionAll.as_deref() {
846                Some(branches) => Some(
847                    select_rows(fp_engine, &fp_config, branches, node)
848                        .await?
849                        .concat(),
850                ),
851                None => None,
852            };
853
854            rows.extend(
855                nested
856                    .into_iter()
857                    .chain(union)
858                    .fold(vec![columns], |rows, more| join(&rows, more)),
859            );
860        }
861
862        Ok(rows)
863    })
864}
865
866/// Adds a null for every column of the select, its nested selects and its `unionAll` branches.
867fn collect_null_columns<'a>(
868    select_statement: &'a ViewDefinitionSelect,
869    row: &mut Row<'a>,
870) -> Result<(), OperationOutcomeError> {
871    for column in select_statement.column.iter().flatten() {
872        let Some(name) = column.name.value.as_deref() else {
873            return Err(OperationOutcomeError::error(
874                IssueType::invalid(),
875                "Column name is required".to_string(),
876            ));
877        };
878
879        row.insert(name, OutputResults::Scalar(None));
880    }
881
882    for nested in select_statement
883        .select
884        .iter()
885        .flatten()
886        .chain(select_statement.unionAll.iter().flatten())
887    {
888        collect_null_columns(nested, row)?;
889    }
890
891    Ok(())
892}
893
894/// A select's own columns on `node`, as one row.
895async fn column_row<'r, 'v>(
896    fp_engine: &FPEngine,
897    fp_config: &Arc<Config<'r>>,
898    select: &'v ViewDefinitionSelect,
899    node: &'r dyn MetaValue,
900) -> Result<Row<'v>, OperationOutcomeError> {
901    let mut row = Row::new();
902
903    for column in select.column.iter().flatten() {
904        let Some(path) = column.path.value.as_deref() else {
905            return Err(OperationOutcomeError::error(
906                IssueType::invalid(),
907                "Column path is required".to_string(),
908            ));
909        };
910
911        let Some(name) = column.name.value.as_deref() else {
912            return Err(OperationOutcomeError::error(
913                IssueType::invalid(),
914                "Column name is required".to_string(),
915            ));
916        };
917
918        let found = evaluate(fp_engine, fp_config, path, node, "expression").await?;
919        let values = found.values();
920
921        let column_type = column
922            .type_
923            .as_ref()
924            .and_then(|t| t.value.as_deref())
925            .unwrap_or_else(|| values.first().map_or("string", |value| value.fhir_type()));
926        let convert = |value: &&dyn MetaValue| {
927            conversions::primitives::convert_meta_value(column_type, *value)
928        };
929
930        let value = if column.collection.as_ref().and_then(|c| c.value) == Some(true) {
931            OutputResults::Collection(values.iter().map(convert).collect::<Result<_, _>>()?)
932        } else {
933            match values {
934                [] => OutputResults::Scalar(None),
935                [value] => OutputResults::Scalar(convert(value)?),
936                _ => {
937                    return Err(OperationOutcomeError::error(
938                        IssueType::invalid(),
939                        "Column result is a collection but the column is not marked as a collection"
940                            .to_string(),
941                    ));
942                }
943            }
944        };
945
946        row.insert(name, value);
947    }
948
949    Ok(row)
950}
951
952/// The resources a view runs over: those of its resource type. A Bundle of another type stands
953/// for its entries, so its entries of that type are kept. Resources sent with the request can be
954/// of any type, and a view gives rows only for its own.
955fn resources_of_type<'r>(
956    resources: &'r [Resource],
957    resource_type: &ResourceType,
958) -> Vec<&'r Resource> {
959    let mut matching = Vec::new();
960    for resource in resources {
961        if &resource.resource_type() == resource_type {
962            matching.push(resource);
963        } else if let Resource::Bundle(bundle) = resource {
964            matching.extend(
965                bundle
966                    .entry
967                    .iter()
968                    .flatten()
969                    .filter_map(|entry| entry.resource.as_deref())
970                    .filter(|resource| &resource.resource_type() == resource_type),
971            );
972        }
973    }
974
975    matching
976}
977
978async fn passes_where_clauses(
979    fp_engine: &FPEngine,
980    variables: Arc<HashMap<String, &dyn MetaValue>>,
981    where_clauses: &[&str],
982    resource: &Resource,
983) -> Result<bool, OperationOutcomeError> {
984    for where_clause in where_clauses {
985        let result = fp_engine
986            .evaluate_with_config(
987                where_clause,
988                vec![resource],
989                Arc::new(Config::builder().with_variable_resolver(
990                    haste_fhirpath::ExternalConstantResolver::Variable(variables.clone()),
991                )),
992            )
993            .await
994            .map_err(|e| {
995                OperationOutcomeError::error(
996                    fhirpath_issue_type(&e),
997                    format!("Error evaluating where clause expression: {e}"),
998                )
999            })?;
1000
1001        // An empty result excludes the resource, like `false`.
1002        if result.is_empty() {
1003            return Ok(false);
1004        }
1005
1006        let bool_results = result
1007            .iter()
1008            .map(|v| match v.fhir_type() {
1009                "boolean" => Ok(v
1010                    .as_any()
1011                    .downcast_ref::<FHIRBoolean>()
1012                    .and_then(|b| b.value.as_ref())
1013                    .unwrap_or(&false)),
1014                "http://hl7.org/fhirpath/System.Boolean" => {
1015                    Ok(v.as_any().downcast_ref::<bool>().unwrap_or(&false))
1016                }
1017                _ => Err(OperationOutcomeError::error(
1018                    IssueType::invalid(),
1019                    format!(
1020                        "Where clause expression must evaluate to a boolean, got: {}",
1021                        v.fhir_type()
1022                    ),
1023                )),
1024            })
1025            .collect::<Result<Vec<_>, _>>()?;
1026
1027        if bool_results.iter().any(|v| !**v) {
1028            return Ok(false);
1029        }
1030    }
1031
1032    Ok(true)
1033}
1034
1035async fn process_view_definition<
1036    CTX: Send + Sync + Clone + 'static,
1037    Client: FHIRClient<CTX, OperationOutcomeError> + Send + Sync + 'static,
1038>(
1039    context: CTX,
1040    output_format: &BoundCode<OutputFormatCodes>,
1041    client: Arc<Client>,
1042    view_definition: &ViewDefinition,
1043    input: &ViewDefinitionRun::Input,
1044    scope: Option<Vec<String>>,
1045    limits: RunLimits,
1046) -> Result<Binary, OperationOutcomeError> {
1047    union::check_unions(&view_definition.select)?;
1048
1049    let fp_engine = FPEngine::new();
1050    let run = Run {
1051        fp_engine: &fp_engine,
1052        variables: Arc::new(build_hashmap_fp_variables(view_definition)),
1053        view_definition,
1054        where_clauses: view_definition
1055            .where_
1056            .iter()
1057            .flatten()
1058            .filter_map(|w| w.path.value.as_deref())
1059            .collect(),
1060        resource_type: view_resource_type(view_definition)?,
1061        limit: requested_limit(input),
1062        limits,
1063    };
1064
1065    let results = view_rows(context, client.as_ref(), &run, input, scope).await?;
1066
1067    let include_header = input
1068        .header
1069        .as_ref()
1070        .and_then(|header| header.value)
1071        .unwrap_or(true);
1072
1073    // `contentType` is required on Binary; without it a client cannot read
1074    // the result back.
1075    let (content_type, data) = match output_format {
1076        binding if binding == &OutputFormatCodes::csv() => {
1077            ("text/csv", output::csv::csv(&results, include_header)?)
1078        }
1079        binding if binding == &OutputFormatCodes::json() => {
1080            ("application/json", output::json::json(&results)?)
1081        }
1082        binding if binding == &OutputFormatCodes::ndjson() => {
1083            ("application/x-ndjson", output::ndjson::ndjson(results)?)
1084        }
1085        _ => {
1086            return Err(OperationOutcomeError::error(
1087                IssueType::not_supported(),
1088                format!("Output format '{output_format:?}' is not supported"),
1089            ));
1090        }
1091    };
1092
1093    Ok(Binary {
1094        contentType: Box::new(FHIRCode {
1095            value: Some(content_type.to_string()),
1096            ..Default::default()
1097        }),
1098        data: Some(Box::new(FHIRBase64Binary {
1099            value: Some(general_purpose::STANDARD.encode(&data)),
1100            ..Default::default()
1101        })),
1102        ..Default::default()
1103    })
1104}
1105
1106/// Runs a FHIR `ViewDefinition` and returns the generated result in the requested
1107/// output format.
1108///
1109/// The output format is read from the `_format` input parameter. If `_format` is
1110/// absent or does not contain a recognized [`OutputFormatCodes`] value, ndjson is
1111/// used, the specification's default.
1112///
1113/// A run that would pass `limits` is refused with `too-costly`.
1114///
1115/// The view is read through `client`; the `patient` and `group` resources that scope the run
1116/// through `read_many`, in one batch.
1117///
1118/// # Errors
1119///
1120/// Returns [`OperationOutcomeError`] if a parameter is refused, the view definition cannot be
1121/// resolved, the run would pass `limits`, or processing the view definition fails.
1122pub async fn view_definition_run<
1123    CTX: Send + Sync + Clone + 'static,
1124    Client: FHIRClient<CTX, OperationOutcomeError> + Send + Sync + 'static,
1125>(
1126    context: CTX,
1127    client: Arc<Client>,
1128    read_many: &ReadMany<CTX>,
1129    input: &ViewDefinitionRun::Input,
1130    limits: RunLimits,
1131) -> Result<ViewDefinitionRun::Output, OperationOutcomeError> {
1132    check_parameters(input, &limits)?;
1133
1134    let output_format = input
1135        .format
1136        .as_ref()
1137        .and_then(|v| v.value.as_ref())
1138        .and_then(|s| BoundCode::<OutputFormatCodes>::new(s))
1139        .unwrap_or(OutputFormatCodes::ndjson());
1140
1141    // The view and the scope don't depend on each other, so they are looked up at once.
1142    let (view_definition, scope) = try_join(
1143        resolve_view_definition(context.clone(), client.as_ref(), input),
1144        patient_scope(context.clone(), read_many, input),
1145    )
1146    .await?;
1147
1148    let output = process_view_definition(
1149        context,
1150        &output_format,
1151        client,
1152        &view_definition,
1153        input,
1154        scope,
1155        limits,
1156    )
1157    .await?;
1158
1159    Ok(ViewDefinitionRun::Output { return_: output })
1160}