1use 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
38pub 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
62pub 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
85pub 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 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 let mut registry = create_function_registry();
114 registry.register_lazy_function(Arc::new(GetResourceKey));
115 registry.register_lazy_function(Arc::new(GetReferenceKey));
116 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 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 pub fn columns(&self) -> &[ColumnInfo] {
171 &self.columns
172 }
173
174 pub fn resource_type(&self) -> &str {
176 &self.ev.resource_type
177 }
178
179 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
212fn 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 fn prepare(&self, path: &str) -> Result<String> {
240 let substituted = substitute_constants(path, &self.constants)?;
241 Ok(rewrite_of_type(&substituted))
242 }
243
244 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 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 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 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 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 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 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 async fn repeat_collect(
411 &self,
412 paths: &[String],
413 node: &Value,
414 out: &mut Vec<Value>,
415 ) -> Result<()> {
416 let mut stack: Vec<Value> = Vec::new();
418 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, ¤t, 0).await?);
431 }
432 for child in children.into_iter().rev() {
433 stack.push(child);
434 }
435 }
436 Ok(())
437 }
438
439 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 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
498fn 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
508fn 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
529fn 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 if path[i..].starts_with(".ofType(") {
542 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 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 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 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
599pub(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
607fn 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
627fn 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
662fn 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 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 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
738fn 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 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
759fn 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
783fn 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 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#[derive(Debug, Clone, Copy)]
840enum BoundaryType {
841 Date,
842 DateTime,
843 Time,
844 Unknown,
845}
846
847fn 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
864fn 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
909fn 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
934fn 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}