1use helios_fhir::FhirVersion;
18use helios_sof::ConstantValue;
19use serde_json::Value;
20
21use crate::core::sof_runner::SofError;
22
23use super::compile_path::{CompileEnv, Constant, compile_fhirpath_expr};
24use super::ir::{Column, LitValue, PathStep, PlanNode, SqlExpr, SqlType};
25
26const ROOT_ALIAS: &str = "r";
27const FOREACH_ALIAS_PREFIX: &str = "fe";
28
29pub fn build_plan(
40 view_json: &Value,
41 dialect: &dyn super::dialect::Dialect,
42 target: super::compiler::CompileTarget,
43 fhir_version: FhirVersion,
44) -> Result<(PlanNode, Vec<LitValue>), SofError> {
45 let resource_type = view_json
46 .get("resource")
47 .and_then(|v| v.as_str())
48 .filter(|s| !s.is_empty())
49 .ok_or_else(|| {
50 SofError::InvalidViewDefinition("ViewDefinition.resource is required".to_string())
51 })?
52 .to_string();
53
54 let selects = view_json
55 .get("select")
56 .and_then(|v| v.as_array())
57 .ok_or_else(|| {
58 SofError::InvalidViewDefinition(
59 "ViewDefinition.select must be a non-null array".to_string(),
60 )
61 })?;
62 if selects.is_empty() {
63 return Err(SofError::InvalidViewDefinition(
64 "ViewDefinition.select must have at least one clause".to_string(),
65 ));
66 }
67
68 let mut env = CompileEnv::new_for_resource(
69 format!("{ROOT_ALIAS}.data"),
70 resource_type.clone(),
71 fhir_version,
72 );
73 populate_constants(view_json, &mut env)?;
74
75 let mut where_predicates: Vec<SqlExpr> = Vec::new();
77 if let Some(wheres) = view_json.get("where").and_then(|v| v.as_array()) {
78 for w in wheres {
79 if let Some(path) = w.get("path").and_then(|v| v.as_str()) {
80 if where_path_is_provably_non_boolean(path) {
86 return Err(SofError::InvalidViewDefinition(format!(
87 "ViewDefinition.where[].path '{path}' must resolve to a \
88 boolean (got a plain navigation expression)"
89 )));
90 }
91 let pred = compile_fhirpath_expr(path, &mut env)?;
92 where_predicates.push(pred);
93 }
94 }
95 }
96
97 let scan = PlanNode::Scan {
98 alias: ROOT_ALIAS.to_string(),
99 resource_type: resource_type.clone(),
100 };
101 let mut root_plan = scan;
102 for pred in where_predicates {
103 root_plan = PlanNode::Filter {
104 parent: Box::new(root_plan),
105 predicate: pred,
106 };
107 }
108
109 let mut alias_seq = AliasSeq::new();
110 let plan = plan_clause_list(
111 selects,
112 &root_plan,
113 &format!("{ROOT_ALIAS}.data"),
114 &mut env,
115 &mut alias_seq,
116 dialect,
117 target,
118 )
119 .and_then(ensure_project)?;
120 Ok((plan, env.param_bindings))
121}
122
123fn populate_constants(view_json: &Value, env: &mut CompileEnv) -> Result<(), SofError> {
131 let Some(constants) = view_json.get("constant").and_then(|v| v.as_array()) else {
132 return Ok(());
133 };
134 for c in constants {
135 let (name, value) = helios_sof::parse_constant_from_json(c).map_err(lift_sof_error)?;
136 env.constants.insert(
137 name,
138 Constant {
139 value: lit_value_from_constant(value),
140 bound_to: None,
141 },
142 );
143 }
144 Ok(())
145}
146
147fn lit_value_from_constant(value: ConstantValue) -> LitValue {
152 match value {
153 ConstantValue::String(s)
154 | ConstantValue::Code(s)
155 | ConstantValue::Identifier(s)
156 | ConstantValue::Base64Binary(s)
157 | ConstantValue::Markdown(s)
158 | ConstantValue::Date(s)
159 | ConstantValue::DateTime(s)
160 | ConstantValue::Time(s)
161 | ConstantValue::Instant(s) => LitValue::Str(s),
162 ConstantValue::Boolean(b) => LitValue::Bool(b),
163 ConstantValue::Integer(i)
164 | ConstantValue::PositiveInt(i)
165 | ConstantValue::UnsignedInt(i)
166 | ConstantValue::Integer64(i) => LitValue::Int(i),
167 ConstantValue::Decimal(s) => LitValue::Decimal(s),
168 }
169}
170
171fn lift_sof_error(e: helios_sof::SofError) -> SofError {
176 match e {
177 helios_sof::SofError::InvalidViewDefinition(msg) => SofError::InvalidViewDefinition(msg),
178 other => SofError::InvalidViewDefinition(other.to_string()),
179 }
180}
181
182fn plan_clause_list(
186 clauses: &[Value],
187 parent_plan: &PlanNode,
188 parent_focus: &str,
189 env: &mut CompileEnv,
190 alias_seq: &mut AliasSeq,
191 dialect: &dyn super::dialect::Dialect,
192 target: super::compiler::CompileTarget,
193) -> Result<PlanNode, SofError> {
194 let mut shared_columns: Vec<Column> = Vec::new();
198 let mut shared_unnests: Vec<UnnestStep> = Vec::new();
199 let mut shared_recurse: Option<RecurseInfo> = None;
200 let mut union_branches: Option<&Vec<Value>> = None;
201
202 for clause in clauses {
203 if let Some(branches) = clause.get("unionAll").and_then(|v| v.as_array()) {
204 if union_branches.is_some() {
205 return Err(SofError::Uncompilable {
206 reason: "multiple unionAll clauses at the same level are not supported"
207 .to_string(),
208 });
209 }
210 if branches.is_empty() {
211 return Err(SofError::InvalidViewDefinition(
212 "unionAll branches list is empty".to_string(),
213 ));
214 }
215 union_branches = Some(branches);
216 let parts = read_clause_columns_and_iter(
219 clause,
220 parent_focus,
221 env,
222 alias_seq,
223 dialect,
224 target,
225 )?;
226 shared_columns.extend(parts.columns);
227 shared_unnests.extend(parts.unnests);
228 continue;
229 }
230
231 let parts =
232 read_clause_columns_and_iter(clause, parent_focus, env, alias_seq, dialect, target)?;
233 if let Some(rec) = parts.recurse {
234 if shared_recurse.is_some() {
235 return Err(SofError::Uncompilable {
236 reason: "multiple repeat clauses at the same level are not supported"
237 .to_string(),
238 });
239 }
240 shared_recurse = Some(rec);
241 }
242 shared_columns.extend(parts.columns);
243 shared_unnests.extend(parts.unnests);
244 }
245
246 let Some(branches) = union_branches else {
249 if shared_columns.is_empty() {
250 return Err(SofError::InvalidViewDefinition(
251 "no columns found in select clauses".to_string(),
252 ));
253 }
254 let mut plan = parent_plan.clone();
255 if let Some(rec) = shared_recurse {
256 plan = PlanNode::Recurse {
260 parent: Box::new(plan),
261 seed: SqlExpr::Lit(LitValue::Null), step_paths: rec.step_paths,
263 out_alias: rec.out_alias,
264 };
265 plan = apply_unnests(plan, &shared_unnests);
266 } else {
267 plan = apply_unnests(plan, &shared_unnests);
268 }
269 return Ok(PlanNode::Project {
270 parent: Box::new(plan),
271 columns: shared_columns,
272 });
273 };
274
275 if shared_recurse.is_some() {
276 return Err(SofError::Uncompilable {
277 reason: "select.repeat combined with sibling unionAll is not yet supported".to_string(),
278 });
279 }
280
281 let flat_branches = flatten_union_branches(branches);
284
285 let branch_focus = shared_unnests
290 .last()
291 .map(|u| format!("{}.value", u.out_alias))
292 .unwrap_or_else(|| parent_focus.to_string());
293
294 let mut branch_plans: Vec<PlanNode> = Vec::with_capacity(flat_branches.len());
297 for branch in &flat_branches {
298 let parts =
299 read_clause_columns_and_iter(branch, &branch_focus, env, alias_seq, dialect, target)?;
300 let mut branch_plan = if let Some(rec) = parts.recurse {
304 if !shared_unnests.is_empty() || !parts.unnests.is_empty() {
305 return Err(SofError::Uncompilable {
306 reason: "select.repeat inside a unionAll branch combined with forEach is \
307 not yet supported"
308 .to_string(),
309 });
310 }
311 PlanNode::Recurse {
312 parent: Box::new(parent_plan.clone()),
313 seed: SqlExpr::Lit(LitValue::Null),
314 step_paths: rec.step_paths,
315 out_alias: rec.out_alias,
316 }
317 } else {
318 let mut combined_unnests = shared_unnests.clone();
321 combined_unnests.extend(parts.unnests);
322 apply_unnests(parent_plan.clone(), &combined_unnests)
323 };
324 if let Some(filter) = parts.extra_filter {
328 branch_plan = PlanNode::Filter {
329 parent: Box::new(branch_plan),
330 predicate: filter,
331 };
332 }
333
334 let mut combined_cols = shared_columns.clone();
335 combined_cols.extend(parts.columns);
336 if combined_cols.is_empty() {
337 return Err(SofError::InvalidViewDefinition(
338 "unionAll branch produced no output columns".to_string(),
339 ));
340 }
341 branch_plans.push(PlanNode::Project {
342 parent: Box::new(branch_plan),
343 columns: combined_cols,
344 });
345 }
346
347 Ok(PlanNode::Union(branch_plans))
348}
349
350fn flatten_union_branches(branches: &[Value]) -> Vec<Value> {
355 let mut out: Vec<Value> = Vec::new();
356 for b in branches {
357 if let Some(inner) = b.get("unionAll").and_then(|v| v.as_array())
358 && b.as_object().map(|o| o.len() == 1).unwrap_or(false)
359 {
360 out.extend(flatten_union_branches(inner));
361 } else {
362 out.push(b.clone());
363 }
364 }
365 out
366}
367
368#[derive(Debug, Clone)]
370struct UnnestStep {
371 source: SqlExpr,
372 out_alias: String,
373 left_join: bool,
374 on_filter: Option<SqlExpr>,
378 flat_index: Option<i64>,
382}
383
384#[derive(Debug, Clone)]
387struct RecurseInfo {
388 step_paths: Vec<super::ir::JsonPath>,
391 out_alias: String,
393}
394
395#[derive(Debug)]
398struct ClauseParts {
399 columns: Vec<Column>,
400 unnests: Vec<UnnestStep>,
401 recurse: Option<RecurseInfo>,
402 extra_filter: Option<SqlExpr>,
406}
407
408fn read_clause_columns_and_iter(
413 clause: &Value,
414 parent_focus: &str,
415 env: &mut CompileEnv,
416 alias_seq: &mut AliasSeq,
417 dialect: &dyn super::dialect::Dialect,
418 target: super::compiler::CompileTarget,
419) -> Result<ClauseParts, SofError> {
420 if let Some(repeat) = clause.get("repeat").and_then(|v| v.as_array()) {
422 if repeat.is_empty() {
423 return Err(SofError::InvalidViewDefinition(
424 "ViewDefinition select.repeat must contain at least one path".to_string(),
425 ));
426 }
427 if clause.get("forEach").is_some() || clause.get("forEachOrNull").is_some() {
428 return Err(SofError::Uncompilable {
429 reason: "select.repeat combined with forEach is not yet supported".to_string(),
430 });
431 }
432 let mut step_paths: Vec<super::ir::JsonPath> = Vec::with_capacity(repeat.len());
433 for p in repeat {
434 let s = p.as_str().ok_or_else(|| {
435 SofError::InvalidViewDefinition("select.repeat entries must be strings".to_string())
436 })?;
437 let prev_root = env.root_alias.clone();
438 env.root_alias = parent_focus.to_string();
439 let expr = compile_fhirpath_expr(s, env)?;
440 env.root_alias = prev_root;
441 match expr {
442 SqlExpr::JsonPath { path, .. } => step_paths.push(path),
443 _ => {
444 return Err(SofError::Uncompilable {
445 reason: format!("repeat path '{s}' must be a simple JSON path"),
446 });
447 }
448 }
449 }
450 let alias = alias_seq.next_recurse();
451 let focus = format!("{alias}.node");
452 let mut columns = read_columns(clause, &focus, env)?;
453 let mut nested_unnests: Vec<UnnestStep> = Vec::new();
459 if let Some(nested) = clause.get("select").and_then(|v| v.as_array()) {
460 for sub in nested {
461 let sub_parts =
462 read_clause_columns_and_iter(sub, &focus, env, alias_seq, dialect, target)?;
463 if sub_parts.recurse.is_some() {
464 return Err(SofError::Uncompilable {
465 reason: "select.repeat with nested repeat is not yet supported".to_string(),
466 });
467 }
468 nested_unnests.extend(sub_parts.unnests);
469 columns.extend(sub_parts.columns);
470 }
471 }
472 return Ok(ClauseParts {
473 columns,
474 unnests: nested_unnests,
475 recurse: Some(RecurseInfo {
476 step_paths,
477 out_alias: alias,
478 }),
479 extra_filter: None,
480 });
481 }
482
483 let for_each_expr = clause
484 .get("forEach")
485 .and_then(|v| v.as_str())
486 .map(String::from);
487 let for_each_or_null_expr = clause
488 .get("forEachOrNull")
489 .and_then(|v| v.as_str())
490 .map(String::from);
491
492 let iter_path_src = for_each_expr.or(for_each_or_null_expr.clone());
493 let is_left_join = for_each_or_null_expr.is_some();
494
495 let (mut unnests, focus): (Vec<UnnestStep>, String) = if let Some(src) = iter_path_src {
496 let (path_src, where_crit_src): (String, Option<String>) =
502 split_trailing_where(&src).unwrap_or((src.clone(), None));
503
504 let prev_root = env.root_alias.clone();
505 env.root_alias = parent_focus.to_string();
506 let path_expr = compile_fhirpath_expr(&path_src, env)?;
507 env.root_alias = prev_root;
508 let path = match path_expr {
509 SqlExpr::JsonPath { path, .. } => path,
510 _ => {
511 return Err(SofError::Uncompilable {
512 reason: format!("forEach path '{src}' must be a simple JSON path"),
513 });
514 }
515 };
516 let trailing_index = match path.0.last() {
522 Some(super::ir::PathStep::Index(n)) if path.0.len() > 1 => Some(*n),
523 _ => None,
524 };
525 if let Some(idx) = trailing_index
531 && target.supports_correlated_from_subqueries()
532 {
533 let trimmed_path = super::ir::JsonPath(path.0[..path.0.len() - 1].to_vec());
534 let segments = split_path_into_segments(&trimmed_path);
535 let (chain_sql, deepest_alias) =
536 build_degenerate_chain_sql(&segments, parent_focus, alias_seq, dialect);
537 let column_focus = format!("{deepest_alias}.value");
538 let raw_columns = read_columns(clause, &column_focus, env)?;
539 let columns: Vec<Column> = raw_columns
543 .into_iter()
544 .map(|c| Column {
545 name: c.name,
546 expr: SqlExpr::ScalarFromChain {
547 chain_sql: chain_sql.clone(),
548 projection: Box::new(c.expr),
549 offset: idx,
550 },
551 collection: c.collection,
552 ty: c.ty,
553 })
554 .collect();
555 let extra_filter = if is_left_join {
560 None
561 } else {
562 Some(SqlExpr::ScalarFromChain {
563 chain_sql: chain_sql.clone(),
564 projection: Box::new(SqlExpr::Lit(LitValue::Int(1))),
565 offset: idx,
566 })
567 };
568 return Ok(ClauseParts {
569 columns,
570 unnests: Vec::new(),
571 recurse: None,
572 extra_filter,
573 });
574 }
575 let mut unnests: Vec<UnnestStep> = Vec::new();
583 let mut focus = parent_focus.to_string();
584 let unnest_path = if trailing_index.is_some() {
589 super::ir::JsonPath(path.0[..path.0.len() - 1].to_vec())
590 } else {
591 path.clone()
592 };
593 let segments = split_path_into_segments(&unnest_path);
594 let last_idx = segments.len().saturating_sub(1);
595 for (i, seg_path) in segments.into_iter().enumerate() {
596 let alias = alias_seq.next();
597 let source = SqlExpr::JsonPath {
598 root: focus.clone(),
599 path: seg_path,
600 };
601 let on_filter = if i == last_idx {
605 if let Some(ref crit_src) = where_crit_src {
606 let prev_root = env.root_alias.clone();
607 env.root_alias = format!("{alias}.value");
608 let pred = compile_fhirpath_expr(crit_src, env);
609 env.root_alias = prev_root;
610 Some(pred?)
611 } else {
612 None
613 }
614 } else {
615 None
616 };
617 unnests.push(UnnestStep {
618 source,
619 out_alias: alias.clone(),
620 left_join: is_left_join && i == last_idx,
621 on_filter,
622 flat_index: None,
623 });
624 focus = format!("{alias}.value");
625 }
626 if let Some(n) = trailing_index
631 && let Some(last) = unnests.last_mut()
632 {
633 last.flat_index = Some(n);
634 }
635 (unnests, focus)
636 } else {
637 (Vec::new(), parent_focus.to_string())
638 };
639
640 let mut columns = read_columns(clause, &focus, env)?;
641
642 if let Some(nested) = clause.get("select").and_then(|v| v.as_array()) {
647 for sub in nested {
648 if sub.get("unionAll").is_some() {
649 return Err(SofError::Uncompilable {
650 reason: "unionAll nested inside another select is not supported".to_string(),
651 });
652 }
653 let sub_parts =
654 read_clause_columns_and_iter(sub, &focus, env, alias_seq, dialect, target)?;
655 if sub_parts.recurse.is_some() {
656 return Err(SofError::Uncompilable {
657 reason: "select.repeat nested inside another select is not yet supported"
658 .to_string(),
659 });
660 }
661 unnests.extend(sub_parts.unnests);
662 columns.extend(sub_parts.columns);
663 }
664 }
665
666 Ok(ClauseParts {
667 columns,
668 unnests,
669 recurse: None,
670 extra_filter: None,
671 })
672}
673
674fn read_columns(
676 clause: &Value,
677 focus: &str,
678 env: &mut CompileEnv,
679) -> Result<Vec<Column>, SofError> {
680 let columns = match clause.get("column").and_then(|v| v.as_array()) {
681 Some(cols) if !cols.is_empty() => cols,
682 _ => return Ok(Vec::new()),
683 };
684
685 let prev_root = env.root_alias.clone();
686 env.root_alias = focus.to_string();
687
688 let mut out = Vec::with_capacity(columns.len());
689 for col in columns {
690 let path = col.get("path").and_then(|v| v.as_str()).ok_or_else(|| {
691 SofError::InvalidViewDefinition("column.path is required".to_string())
692 })?;
693 let name = col.get("name").and_then(|v| v.as_str()).ok_or_else(|| {
694 SofError::InvalidViewDefinition("column.name is required".to_string())
695 })?;
696 let collection_opt = col.get("collection").and_then(|v| v.as_bool());
697 let collection = collection_opt.unwrap_or(false);
698 if collection_opt == Some(false)
704 && path_likely_multi_valued(path, &env.resource_type, env.fhir_version)
705 {
706 return Err(SofError::InvalidViewDefinition(format!(
707 "column '{}' declares `collection: false` but path '{}' may yield \
708 multiple values; declare `collection: true` or pick a single element",
709 col.get("name").and_then(|v| v.as_str()).unwrap_or(""),
710 path
711 )));
712 }
713
714 let column_type = col.get("type").and_then(|v| v.as_str()).map(String::from);
718 let prev_type_hint = env.column_type_hint.take();
719 env.column_type_hint = column_type.clone();
720 let expr_result = compile_fhirpath_expr(path, env);
721 env.column_type_hint = prev_type_hint;
722 let expr = expr_result?;
723
724 let ty = column_type_from_hint(column_type.as_deref());
725 let final_expr = if collection {
730 match expr {
731 SqlExpr::JsonPath { root, path } => SqlExpr::CollectionAgg { root, path },
732 other => other,
733 }
734 } else {
735 expr
736 };
737 out.push(Column {
738 name: name.to_string(),
739 expr: final_expr,
740 collection: false, ty,
742 });
743 }
744 env.root_alias = prev_root;
745 Ok(out)
746}
747
748fn where_path_is_provably_non_boolean(path: &str) -> bool {
754 let trimmed = path.trim();
755 if trimmed.is_empty() {
756 return false;
757 }
758 let has_operator = trimmed.contains('=')
762 || trimmed.contains('!')
763 || trimmed.contains('<')
764 || trimmed.contains('>');
765 let has_call = trimmed.contains('(');
766 let has_bool_kw = [" and ", " or ", " not ", " in ", " contains "]
767 .iter()
768 .any(|k| trimmed.contains(k));
769 !has_operator && !has_call && !has_bool_kw && trimmed.contains('.')
770}
771
772fn path_likely_multi_valued(path: &str, resource_type: &str, fhir_version: FhirVersion) -> bool {
784 let trimmed = path.trim();
785 if trimmed.is_empty() || resource_type.is_empty() {
786 return false;
787 }
788 let mut parent = resource_type.to_string();
789 let mut segments = trimmed.split('.').peekable();
790 while let Some(seg) = segments.next() {
791 if seg.is_empty() || seg.chars().any(|c| !c.is_ascii_alphanumeric()) {
794 return false;
795 }
796 let Some((field_type, is_collection)) =
797 super::lookup_field_type(fhir_version, &parent, seg)
798 else {
799 return false;
800 };
801 if is_collection && segments.peek().is_some() {
805 return true;
806 }
807 parent = field_type.to_string();
808 }
809 false
810}
811
812fn split_trailing_where(src: &str) -> Option<(String, Option<String>)> {
818 let trimmed = src.trim();
819 let suffix = ".where(";
820 let pos = trimmed.rfind(suffix)?;
821 if !trimmed.ends_with(')') {
822 return None;
823 }
824 let base = trimmed[..pos].trim().to_string();
825 let crit = trimmed[pos + suffix.len()..trimmed.len() - 1]
826 .trim()
827 .to_string();
828 Some((base, Some(crit)))
829}
830
831fn column_type_from_hint(hint: Option<&str>) -> SqlType {
836 match hint {
837 Some("boolean") => SqlType::Boolean,
838 Some("integer") | Some("positiveInt") | Some("unsignedInt") => SqlType::Integer,
839 Some("decimal") => SqlType::Decimal,
840 _ => SqlType::Text,
841 }
842}
843
844fn build_degenerate_chain_sql(
857 segments: &[super::ir::JsonPath],
858 parent_focus: &str,
859 alias_seq: &mut AliasSeq,
860 dialect: &dyn super::dialect::Dialect,
861) -> (String, String) {
862 use super::ir::PathStep;
863 let mut from_parts: Vec<String> = Vec::new();
864 let mut prev = parent_focus.to_string();
865 let mut last_alias = String::new();
866 let is_sqlite = dialect.lateral_keyword().is_empty();
867 for seg in segments {
868 let alias = alias_seq.next();
869 let segs_owned: Vec<String> = seg
870 .0
871 .iter()
872 .filter_map(|s| match s {
873 PathStep::Field(n) => Some(n.clone()),
874 PathStep::Index(n) => Some(n.to_string()),
875 _ => None,
876 })
877 .collect();
878 let segs: Vec<&str> = segs_owned.iter().map(String::as_str).collect();
879 let unnest_sql = if is_sqlite {
880 let mut path_str = String::from("$");
883 for s in &segs {
884 if s.chars().all(|c| c.is_ascii_digit()) {
885 path_str.push('[');
886 path_str.push_str(s);
887 path_str.push(']');
888 } else {
889 path_str.push('.');
890 path_str.push_str(s);
891 }
892 }
893 if prev == "r.data" && !path_str.contains('[') {
894 format!("json_each({prev}, '{path_str}')")
895 } else {
896 let extracted = format!("json_extract({prev}, '{path_str}')");
897 let type_check = format!("json_type({prev}, '{path_str}')");
898 format!(
899 "json_each(CASE WHEN {type_check} = 'array' THEN {extracted} \
900 WHEN {type_check} IN ('object', 'array') THEN json_array(json({extracted})) \
901 WHEN {type_check} IS NOT NULL THEN json_array({extracted}) \
902 ELSE '[]' END)"
903 )
904 }
905 } else {
906 let prev_jsonb = format!("({prev})::jsonb");
918 let nav = if segs.len() == 1 {
919 format!("{prev_jsonb}->'{}'", segs[0])
920 } else {
921 format!("{prev_jsonb}#>'{{{}}}'", segs.join(","))
922 };
923 format!(
924 "jsonb_array_elements(CASE WHEN jsonb_typeof({nav}) = 'array' THEN {nav} \
925 WHEN jsonb_typeof({nav}) IS NOT NULL THEN jsonb_build_array({nav}) \
926 ELSE '[]'::jsonb END)"
927 )
928 };
929 let from_part = if is_sqlite {
930 format!("{unnest_sql} {alias}")
931 } else {
932 format!("{unnest_sql} AS {alias}(value)")
935 };
936 from_parts.push(from_part);
937 last_alias = alias.clone();
938 prev = format!("{alias}.value");
939 }
940 (from_parts.join(", "), last_alias)
941}
942
943fn split_path_into_segments(path: &super::ir::JsonPath) -> Vec<super::ir::JsonPath> {
949 let mut segments: Vec<super::ir::JsonPath> = Vec::new();
950 let mut current: Vec<PathStep> = Vec::new();
951 for step in &path.0 {
952 match step {
953 PathStep::Field(_) => {
954 if !current.is_empty() {
955 segments.push(super::ir::JsonPath(std::mem::take(&mut current)));
956 }
957 current.push(step.clone());
958 }
959 _ => current.push(step.clone()),
960 }
961 }
962 if !current.is_empty() {
963 segments.push(super::ir::JsonPath(current));
964 }
965 segments
966}
967
968fn apply_unnests(parent: PlanNode, unnests: &[UnnestStep]) -> PlanNode {
971 let mut p = parent;
972 for u in unnests {
973 p = PlanNode::LateralUnnest {
974 parent: Box::new(p),
975 source: u.source.clone(),
976 out_alias: u.out_alias.clone(),
977 left_join: u.left_join,
978 on_filter: u.on_filter.clone(),
979 flat_index: u.flat_index,
980 };
981 }
982 p
983}
984
985fn ensure_project(plan: PlanNode) -> Result<PlanNode, SofError> {
989 match &plan {
990 PlanNode::Project { .. } | PlanNode::Union(_) => Ok(plan),
991 other => Err(SofError::InvalidViewDefinition(format!(
992 "plan_clause_list returned an unexpected top node: {other:?}"
993 ))),
994 }
995}
996
997#[derive(Debug, Default)]
1001struct AliasSeq {
1002 next: usize,
1003}
1004
1005impl AliasSeq {
1006 fn new() -> Self {
1007 Self { next: 0 }
1008 }
1009 fn next(&mut self) -> String {
1010 self.next += 1;
1011 if self.next == 1 {
1014 FOREACH_ALIAS_PREFIX.to_string()
1015 } else {
1016 format!("{FOREACH_ALIAS_PREFIX}{}", self.next)
1017 }
1018 }
1019 fn next_recurse(&mut self) -> String {
1020 self.next += 1;
1021 format!("rec_{}", self.next - 1)
1022 }
1023}
1024
1025const _: Option<PathStep> = None;