Skip to main content

json_eval_rs/parse_schema/
common.rs

1use crate::jsoneval::path_utils;
2use crate::jsoneval::table_metadata::{
3    ColumnMetadata, RepeatBoundMetadata, RowMetadata, TableMetadata,
4};
5use crate::{LogicId, RLogic};
6/// Shared utilities for schema parsing (used by both legacy and parsed implementations)
7use indexmap::{IndexMap, IndexSet};
8use serde_json::Map;
9use serde_json::Value;
10use std::sync::Arc;
11
12/// Collect schema field pointers referenced by `$layout` elements.
13///
14/// This runs once during schema parsing. Runtime schema-value extraction then uses
15/// an O(1) set membership check instead of re-walking a potentially large schema.
16pub fn collect_layout_field_refs(value: &Value, refs: &mut IndexSet<String>) {
17    fn collect_elements(elements: &Value, refs: &mut IndexSet<String>) {
18        let Some(elements) = elements.as_array() else {
19            return;
20        };
21        for element in elements {
22            let Some(map) = element.as_object() else {
23                continue;
24            };
25            if let Some(reference) = map.get("$ref").and_then(Value::as_str) {
26                let pointer = path_utils::normalize_to_json_pointer(reference);
27                refs.insert(pointer.trim_start_matches('#').to_string());
28            }
29            if let Some(children) = map.get("elements") {
30                collect_elements(children, refs);
31            }
32        }
33    }
34
35    match value {
36        Value::Object(map) => {
37            if let Some(elements) = map
38                .get("$layout")
39                .and_then(Value::as_object)
40                .and_then(|layout| layout.get("elements"))
41            {
42                collect_elements(elements, refs);
43            }
44            for child in map.values() {
45                collect_layout_field_refs(child, refs);
46            }
47        }
48        Value::Array(values) => {
49            for child in values {
50                collect_layout_field_refs(child, refs);
51            }
52        }
53        _ => {}
54    }
55}
56
57/// Collect $ref dependencies from a JSON value recursively
58pub fn collect_refs(value: &Value, refs: &mut IndexSet<String>) {
59    match value {
60        Value::Object(map) => {
61            if let Some(path) = map.get("$ref").and_then(Value::as_str) {
62                refs.insert(path_utils::normalize_to_json_pointer(path).into_owned());
63            }
64            if let Some(path) = map.get("ref").and_then(Value::as_str) {
65                refs.insert(path_utils::normalize_to_json_pointer(path).into_owned());
66            }
67            if let Some(var_val) = map.get("var") {
68                match var_val {
69                    Value::String(s) => {
70                        refs.insert(s.clone());
71                    }
72                    Value::Array(arr) => {
73                        if let Some(path) = arr.get(0).and_then(Value::as_str) {
74                            refs.insert(path.to_string());
75                        }
76                    }
77                    _ => {}
78                }
79            }
80            for val in map.values() {
81                collect_refs(val, refs);
82            }
83        }
84        Value::Array(arr) => {
85            for val in arr {
86                collect_refs(val, refs);
87            }
88        }
89        _ => {}
90    }
91}
92
93/// Check if a value contains any actionable schema keys recursively (with depth limit for arrays)
94/// used to skip large pure-data arrays during schema walking
95#[inline]
96pub fn has_actionable_keys(value: &Value) -> bool {
97    match value {
98        Value::Object(map) => {
99            if map.contains_key("$evaluation")
100                || map.contains_key("$table")
101                || map.contains_key("dependents")
102                || map.contains_key("$layout")
103            {
104                return true;
105            }
106
107            // Check for conditional hidden/disabled fields
108            if let Some(Value::Object(condition)) = map.get("condition") {
109                if condition.contains_key("hidden") || condition.contains_key("disabled") {
110                    return true;
111                }
112            }
113
114            // Check for rules object
115            if map.contains_key("rules") {
116                return true;
117            }
118
119            // Check for type="array" with items (subforms)
120            if let Some(Value::String(type_str)) = map.get("type") {
121                if type_str == "array" && map.contains_key("items") {
122                    return true;
123                }
124            }
125
126            // Check for options with URL templates
127            if let Some(Value::String(url)) = map.get("url") {
128                if url.contains('{') && url.contains('}') {
129                    return true;
130                }
131            }
132
133            map.values().any(has_actionable_keys)
134        }
135        Value::Array(arr) => arr.iter().take(5).any(has_actionable_keys),
136        _ => false,
137    }
138}
139
140/// Compute forward/normal column partitions with transitive closure
141///
142/// This function identifies which columns have forward references (dependencies on later columns)
143/// and separates them from normal columns for proper evaluation order.
144pub fn compute_column_partitions(columns: &[ColumnMetadata]) -> (Vec<usize>, Vec<usize>) {
145    use std::collections::HashSet;
146
147    // Build set of all forward-referencing column names (direct + transitive)
148    let mut fwd_cols = HashSet::new();
149    for col in columns {
150        if col.has_forward_ref {
151            fwd_cols.insert(col.name.as_ref());
152        }
153    }
154
155    // Transitive closure: any column that depends on forward columns is also forward
156    loop {
157        let mut changed = false;
158        for col in columns {
159            if !fwd_cols.contains(col.name.as_ref()) {
160                // Check if this column depends on any forward column
161                for dep in col.dependencies.iter() {
162                    // Strip $ prefix from dependency name for comparison
163                    let dep_name = dep.trim_start_matches('$');
164                    if fwd_cols.contains(dep_name) {
165                        fwd_cols.insert(col.name.as_ref());
166                        changed = true;
167                        break;
168                    }
169                }
170            }
171        }
172        // Stop when no more changes
173        if !changed {
174            break;
175        }
176    }
177
178    // Separate into forward and normal indices
179    let mut forward_indices = Vec::new();
180    let mut normal_indices = Vec::new();
181
182    for (idx, col) in columns.iter().enumerate() {
183        if fwd_cols.contains(col.name.as_ref()) {
184            forward_indices.push(idx);
185        } else {
186            normal_indices.push(idx);
187        }
188    }
189
190    (forward_indices, normal_indices)
191}
192
193pub fn walk_schema(
194    value: &Value,
195    path: &str,
196    engine: &mut RLogic,
197    evaluations: &mut IndexMap<String, LogicId>,
198    tables: &mut IndexMap<String, Value>,
199    deps: &mut IndexMap<String, IndexSet<String>>,
200    value_fields: &mut Vec<String>,
201    layout_paths: &mut Vec<String>,
202    dependents: &mut IndexMap<String, Vec<crate::DependentItem>>,
203    options_templates: &mut Vec<(String, String, String)>,
204    subforms: &mut Vec<(String, serde_json::Map<String, Value>, Value)>,
205    fields_with_rules: &mut Vec<String>,
206    conditional_hidden_fields: &mut Vec<String>,
207    conditional_readonly_fields: &mut Vec<String>,
208) -> Result<(), String> {
209    match value {
210        Value::Object(map) => {
211            // Check for $evaluation
212            if let Some(evaluation) = map.get("$evaluation") {
213                let key = path.to_string();
214                let logic_value = evaluation.get("logic").unwrap_or(evaluation);
215                let logic_id = engine
216                    .compile(logic_value)
217                    .map_err(|e| format!("failed to compile evaluation at {key}: {e}"))?;
218                evaluations.insert(key.clone(), logic_id);
219
220                // Collect dependencies with smart table inheritance
221                let mut refs: IndexSet<String> = engine
222                    .get_referenced_vars(&logic_id)
223                    .unwrap_or_default()
224                    .into_iter()
225                    .map(|dep| path_utils::canonicalize_schema_path(&dep).into_owned())
226                    .filter(|dep| {
227                        // Filter out simple column references (e.g., "/INSAGE_YEAR", "/PREM_PP")
228                        // These are FINDINDEX/MATCH column names, not actual data dependencies
229                        // Real dependencies have multiple path segments (e.g., "/illustration/properties/...")
230                        // Update: allow top-level fields only if they are system paths or deeper paths
231                        dep.matches('/').count() > 1 || dep.starts_with("/$")
232                    })
233                    .collect();
234                let mut extra_refs = IndexSet::new();
235                collect_refs(logic_value, &mut extra_refs);
236                if !extra_refs.is_empty() {
237                    refs.extend(extra_refs.into_iter());
238                }
239
240                // For table dependencies, inherit parent table path instead of individual rows
241                let refs: IndexSet<String> = refs
242                    .into_iter()
243                    .filter_map(|dep| {
244                        // If dependency is a table row (contains /$table/), inherit table path
245                        if let Some(table_idx) = dep.find("/$table/") {
246                            let table_path = &dep[..table_idx];
247                            Some(table_path.to_string())
248                        } else {
249                            Some(dep.to_string())
250                        }
251                    })
252                    .collect();
253
254                if !refs.is_empty() {
255                    deps.insert(key.clone(), refs);
256                }
257            }
258
259            // Check for $table
260            if let Some(table) = map.get("$table") {
261                let key = path.to_string();
262
263                let rows = table.clone();
264                let datas = map
265                    .get("$datas")
266                    .cloned()
267                    .unwrap_or_else(|| Value::Array(vec![]));
268                let skip = map.get("$skip").cloned().unwrap_or(Value::Bool(false));
269                let clear = map.get("$clear").cloned().unwrap_or(Value::Bool(false));
270
271                let mut table_entry = Map::new();
272                table_entry.insert("rows".to_string(), rows);
273                table_entry.insert("datas".to_string(), datas);
274                table_entry.insert("skip".to_string(), skip);
275                table_entry.insert("clear".to_string(), clear);
276
277                tables.insert(key, Value::Object(table_entry));
278            }
279
280            // Check for $layout with elements
281            if let Some(layout_obj) = map.get("$layout") {
282                if let Some(Value::Array(_)) = layout_obj.get("elements") {
283                    let layout_elements_path = format!("{}/$layout/elements", path);
284                    layout_paths.push(layout_elements_path);
285                }
286            }
287
288            // Check for rules object - collect field path for efficient validation
289            if map.contains_key("rules") && !path.is_empty() && !path.starts_with("#/$") {
290                // Convert JSON pointer path to dotted notation for validation
291                // E.g., "#/properties/form/properties/name" -> "form.name"
292                let field_path = path
293                    .trim_start_matches('#')
294                    .replace("/properties/", ".")
295                    .trim_start_matches('/')
296                    .trim_start_matches('.')
297                    .to_string();
298
299                if !field_path.is_empty() && !field_path.starts_with("$") {
300                    fields_with_rules.push(field_path);
301                }
302            }
303
304            // Check for options with URL templates
305            if let Some(Value::String(url)) = map.get("url") {
306                // Check if URL contains template pattern {variable}
307                if url.contains('{') && url.contains('}') {
308                    // Convert to JSON pointer format for evaluated_schema access
309                    let url_path = path_utils::normalize_to_json_pointer(&format!("{}/url", path))
310                        .into_owned();
311                    let params_path =
312                        path_utils::normalize_to_json_pointer(&format!("{}/params", path))
313                            .into_owned();
314                    options_templates.push((url_path, url.clone(), params_path));
315                }
316            }
317
318            // Check for array fields with items (subforms)
319            if let Some(Value::String(type_str)) = map.get("type") {
320                if type_str == "array" {
321                    if let Some(items) = map.get("items") {
322                        // Store subform info for later creation (after walk completes)
323                        subforms.push((path.to_string(), map.clone(), items.clone()));
324                    }
325                }
326            }
327
328            // Check for conditional hidden/disabled fields
329            if let Some(Value::Object(condition)) = map.get("condition") {
330                // Hidden
331                if condition.contains_key("hidden") {
332                    conditional_hidden_fields.push(path.to_string());
333                }
334                // Disabled (Read Only) - only relevant if it has a value enforce
335                if condition.contains_key("disabled") && map.contains_key("value") {
336                    conditional_readonly_fields.push(path.to_string());
337                }
338            }
339
340            // Check for dependents array
341            if let Some(Value::Array(dependents_arr)) = map.get("dependents") {
342                let mut dependent_items = Vec::new();
343
344                for (dep_idx, dep_item) in dependents_arr.iter().enumerate() {
345                    if let Value::Object(dep_obj) = dep_item {
346                        if let Some(Value::String(ref_path)) = dep_obj.get("$ref") {
347                            // Process clear - compile if it's an $evaluation
348                            let clear_val = if let Some(clear) = dep_obj.get("clear") {
349                                if let Value::Object(clear_obj) = clear {
350                                    if clear_obj.contains_key("$evaluation") {
351                                        // Compile and store the evaluation
352                                        let clear_eval = clear_obj.get("$evaluation").unwrap();
353                                        let clear_key =
354                                            format!("{}/dependents/{}/clear", path, dep_idx);
355                                        let logic_id = engine.compile(clear_eval).map_err(|e| {
356                                            format!(
357                                                "Failed to compile dependent clear at {}: {}",
358                                                clear_key, e
359                                            )
360                                        })?;
361                                        evaluations.insert(clear_key.clone(), logic_id);
362                                        // Replace with eval key reference
363                                        Some(Value::String(clear_key))
364                                    } else {
365                                        Some(clear.clone())
366                                    }
367                                } else {
368                                    Some(clear.clone())
369                                }
370                            } else {
371                                None
372                            };
373
374                            // Process value - compile if it's an $evaluation
375                            let value_val = if let Some(value) = dep_obj.get("value") {
376                                if let Value::Object(value_obj) = value {
377                                    if value_obj.contains_key("$evaluation") {
378                                        // Compile and store the evaluation
379                                        let value_eval = value_obj.get("$evaluation").unwrap();
380                                        let value_key =
381                                            format!("{}/dependents/{}/value", path, dep_idx);
382                                        let logic_id = engine.compile(value_eval).map_err(|e| {
383                                            format!(
384                                                "Failed to compile dependent value at {}: {}",
385                                                value_key, e
386                                            )
387                                        })?;
388                                        evaluations.insert(value_key.clone(), logic_id);
389                                        // Replace with eval key reference
390                                        Some(Value::String(value_key))
391                                    } else {
392                                        Some(value.clone())
393                                    }
394                                } else {
395                                    Some(value.clone())
396                                }
397                            } else {
398                                None
399                            };
400
401                            dependent_items.push(crate::DependentItem {
402                                ref_path: ref_path.clone(),
403                                clear: clear_val,
404                                value: value_val,
405                            });
406                        }
407                    }
408                }
409
410                if !dependent_items.is_empty() {
411                    dependents.insert(path.to_string(), dependent_items);
412                }
413            }
414
415            // Recurse into children
416            Ok(for (key, val) in map {
417                // Skip special evaluation and dependents keys from recursion (already processed above)
418                if key == "$evaluation"
419                    || key == "dependents"
420                    || (key == "items" && map.get("type").and_then(Value::as_str) == Some("array"))
421                {
422                    continue;
423                }
424
425                let next_path = if path == "#" {
426                    format!("#/{key}")
427                } else {
428                    format!("{path}/{key}")
429                };
430
431                // Check if this is a "value" field
432                // Allow $params but exclude other special $ paths like $layout, $items, etc.
433                let is_excluded_special_path = next_path.contains("/$layout/")
434                    || next_path.contains("/$items/")
435                    || next_path.contains("/$options/")
436                    || next_path.contains("/$dependents/")
437                    || next_path.contains("/$rules/");
438
439                if key == "value" && !is_excluded_special_path {
440                    value_fields.push(next_path.clone());
441                }
442
443                // Recurse into all children (including $ keys like $table, $datas, etc.)
444                walk_schema(
445                    val,
446                    &next_path,
447                    engine,
448                    evaluations,
449                    tables,
450                    deps,
451                    value_fields,
452                    layout_paths,
453                    dependents,
454                    options_templates,
455                    subforms,
456                    fields_with_rules,
457                    conditional_hidden_fields,
458                    conditional_readonly_fields,
459                )?;
460            })
461        }
462        Value::Array(arr) => {
463            // Skip large arrays that contain no actionable schema keys.
464            // This avoids recursively walking pure-data arrays (e.g., table rows in $params).
465            // We specifically avoid skipping layout elements which can be large.
466            let is_layout_array = path.contains("$layout") || path.contains("elements");
467            if !is_layout_array && arr.len() > 10 && !has_actionable_keys(value) {
468                return Ok(());
469            }
470            Ok(for (index, item) in arr.iter().enumerate() {
471                let next_path = if path == "#" {
472                    format!("#/{index}")
473                } else {
474                    format!("{path}/{index}")
475                };
476                walk_schema(
477                    item,
478                    &next_path,
479                    engine,
480                    evaluations,
481                    tables,
482                    deps,
483                    value_fields,
484                    layout_paths,
485                    dependents,
486                    options_templates,
487                    subforms,
488                    fields_with_rules,
489                    conditional_hidden_fields,
490                    conditional_readonly_fields,
491                )?;
492            })
493        }
494        _ => Ok(()),
495    }
496}
497pub fn collect_table_dependencies(
498    tables: &IndexMap<String, Value>,
499    dependencies: &mut IndexMap<String, IndexSet<String>>,
500) {
501    for (table_key, _) in tables.iter() {
502        let mut table_deps = IndexSet::new();
503
504        let table_data_prefix = path_utils::normalize_to_json_pointer(table_key)
505            .replace("/properties/", "/")
506            .trim_start_matches('#')
507            .to_string();
508        let table_data_prefix_slash = format!("{}/", table_data_prefix);
509
510        for (eval_key, deps) in dependencies.iter() {
511            let is_child = eval_key.len() > table_key.len()
512                && eval_key.starts_with(table_key.as_str())
513                && eval_key.as_bytes().get(table_key.len()) == Some(&b'/');
514
515            if is_child {
516                if eval_key.contains("/$datas/") {
517                    continue;
518                }
519
520                for dep in deps {
521                    let dep_data_path = path_utils::normalize_to_json_pointer(dep)
522                        .replace("/properties/", "/")
523                        .trim_start_matches('#')
524                        .to_string();
525
526                    if dep_data_path == table_data_prefix
527                        || dep_data_path.starts_with(&table_data_prefix_slash)
528                    {
529                        continue;
530                    }
531                    let is_params_dep = dep.contains("$params");
532                    let is_inline_system = !is_params_dep
533                        && !dep.contains("$context")
534                        && (dep.starts_with("/$") || dep.starts_with('$'));
535                    if is_inline_system {
536                        continue;
537                    }
538                    table_deps.insert(dep.clone());
539                }
540            }
541        }
542
543        if !table_deps.is_empty() {
544            dependencies.insert(table_key.clone(), table_deps);
545        }
546    }
547}
548
549pub fn categorize_evaluations(
550    sorted_evaluations: &[Vec<String>],
551    evaluations: &IndexMap<String, crate::LogicId>,
552    tables: &IndexMap<String, Value>,
553) -> (Vec<String>, Vec<String>) {
554    let batched_keys: IndexSet<String> = sorted_evaluations.iter().flatten().cloned().collect();
555
556    let mut rules_evaluations = Vec::new();
557    let mut others_evaluations = Vec::new();
558
559    for eval_key in evaluations.keys() {
560        if batched_keys.contains(eval_key) {
561            continue;
562        }
563
564        if tables.iter().any(|(key, _)| eval_key.starts_with(key)) {
565            continue;
566        }
567
568        if eval_key.contains("/$params/") {
569            continue;
570        }
571
572        if eval_key.contains("/rules/") {
573            rules_evaluations.push(eval_key.clone());
574        } else if !eval_key.contains("/dependents/") {
575            others_evaluations.push(eval_key.clone());
576        }
577    }
578
579    (rules_evaluations, others_evaluations)
580}
581
582pub fn process_value_fields(
583    value_fields: Vec<String>,
584    tables: &IndexMap<String, Value>,
585) -> Vec<String> {
586    let mut value_evaluations = Vec::new();
587
588    for path in value_fields {
589        if value_evaluations.contains(&path) {
590            continue;
591        }
592
593        if path.contains("/$params/") || tables.iter().any(|(key, _)| path.starts_with(key)) {
594            continue;
595        }
596
597        value_evaluations.push(path);
598    }
599
600    value_evaluations
601}
602
603pub fn compile_table_metadata(
604    evaluations: &IndexMap<String, crate::LogicId>,
605    engine: &crate::RLogic,
606    eval_key: &str,
607    table: &Value,
608) -> Result<TableMetadata, String> {
609    let rows = table
610        .get("rows")
611        .and_then(|v| v.as_array())
612        .ok_or("table missing rows")?;
613    let empty_datas = Vec::new();
614    let datas = table
615        .get("datas")
616        .and_then(|v| v.as_array())
617        .unwrap_or(&empty_datas);
618
619    // Pre-compile data plans with Arc sharing
620    let mut data_plans = Vec::with_capacity(datas.len());
621    for (idx, entry) in datas.iter().enumerate() {
622        let Some(name) = entry.get("name").and_then(|v| v.as_str()) else {
623            continue;
624        };
625        let logic_path = format!("{eval_key}/$datas/{idx}/data");
626        let logic = evaluations.get(&logic_path).copied();
627        let literal = entry.get("data").map(|v| Arc::new(v.clone()));
628        data_plans.push((Arc::from(name), logic, literal));
629    }
630
631    // Pre-compile row plans with dependency analysis
632    let mut row_plans = Vec::with_capacity(rows.len());
633    for (row_idx, row_val) in rows.iter().enumerate() {
634        let Some(row_obj) = row_val.as_object() else {
635            continue;
636        };
637
638        if let Some(repeat_arr) = row_obj.get("$repeat").and_then(|v| v.as_array()) {
639            if repeat_arr.len() == 3 {
640                let start_logic_path = format!("{eval_key}/$table/{row_idx}/$repeat/0");
641                let end_logic_path = format!("{eval_key}/$table/{row_idx}/$repeat/1");
642                let start_logic = evaluations.get(&start_logic_path).copied();
643                let end_logic = evaluations.get(&end_logic_path).copied();
644
645                let start_literal = Arc::new(repeat_arr.get(0).cloned().unwrap_or(Value::Null));
646                let end_literal = Arc::new(repeat_arr.get(1).cloned().unwrap_or(Value::Null));
647
648                if let Some(template) = repeat_arr.get(2).and_then(|v| v.as_object()) {
649                    let mut columns = Vec::with_capacity(template.len());
650                    for (col_name, col_val) in template {
651                        let col_eval_path =
652                            format!("{eval_key}/$table/{row_idx}/$repeat/2/{col_name}");
653                        let logic = evaluations.get(&col_eval_path).copied();
654                        let literal = if logic.is_none() {
655                            Some(col_val.clone())
656                        } else {
657                            None
658                        };
659
660                        // Extract dependencies ONCE at parse time (not during evaluation)
661                        let (dependencies, has_forward_ref) = if let Some(logic_id) = logic {
662                            let deps = engine
663                                .get_referenced_vars(&logic_id)
664                                .unwrap_or_default()
665                                .into_iter()
666                                .filter(|v| {
667                                    v.starts_with('$') && v != "$iteration" && v != "$threshold"
668                                })
669                                .collect();
670                            let has_fwd = engine.has_forward_reference(&logic_id);
671                            (deps, has_fwd)
672                        } else {
673                            (Vec::new(), false)
674                        };
675
676                        columns.push(ColumnMetadata::new(
677                            col_name,
678                            logic,
679                            literal,
680                            dependencies,
681                            has_forward_ref,
682                        ));
683                    }
684
685                    // Pre-compute forward column propagation (transitive closure)
686                    let (forward_cols, normal_cols) = compute_column_partitions(&columns);
687
688                    row_plans.push(RowMetadata::Repeat {
689                        start: RepeatBoundMetadata {
690                            logic: start_logic,
691                            literal: start_literal,
692                        },
693                        end: RepeatBoundMetadata {
694                            logic: end_logic,
695                            literal: end_literal,
696                        },
697                        columns: columns.into(),
698                        forward_cols: forward_cols.into(),
699                        normal_cols: normal_cols.into(),
700                    });
701                    continue;
702                }
703            }
704        }
705
706        // Static row
707        let mut columns = Vec::with_capacity(row_obj.len());
708        for (col_name, col_val) in row_obj {
709            if col_name == "$repeat" {
710                continue;
711            }
712            let col_eval_path = format!("{eval_key}/$table/{row_idx}/{col_name}");
713            let logic = evaluations.get(&col_eval_path).copied();
714            let literal = if logic.is_none() {
715                Some(col_val.clone())
716            } else {
717                None
718            };
719
720            // Extract dependencies ONCE at parse time
721            let (dependencies, has_forward_ref) = if let Some(logic_id) = logic {
722                let deps = engine
723                    .get_referenced_vars(&logic_id)
724                    .unwrap_or_default()
725                    .into_iter()
726                    .filter(|v| v.starts_with('$') && v != "$iteration" && v != "$threshold")
727                    .collect();
728                let has_fwd = engine.has_forward_reference(&logic_id);
729                (deps, has_fwd)
730            } else {
731                (Vec::new(), false)
732            };
733
734            columns.push(ColumnMetadata::new(
735                col_name,
736                logic,
737                literal,
738                dependencies,
739                has_forward_ref,
740            ));
741        }
742        row_plans.push(RowMetadata::Static {
743            columns: columns.into(),
744        });
745    }
746
747    // Pre-compile skip/clear logic
748    let skip_logic = evaluations.get(&format!("{eval_key}/$skip")).copied();
749    let skip_literal = table.get("skip").and_then(Value::as_bool).unwrap_or(false);
750    let clear_logic = evaluations.get(&format!("{eval_key}/$clear")).copied();
751    let clear_literal = table.get("clear").and_then(Value::as_bool).unwrap_or(false);
752
753    Ok(TableMetadata {
754        data_plans: data_plans.into(),
755        row_plans: row_plans.into(),
756        skip_logic,
757        skip_literal,
758        clear_logic,
759        clear_literal,
760    })
761}
762pub fn build_reffed_by(
763    dependencies: &IndexMap<String, IndexSet<String>>,
764) -> IndexMap<String, Vec<String>> {
765    let mut reffed_by: IndexMap<String, Vec<String>> = IndexMap::new();
766
767    for (eval_path, deps) in dependencies.iter() {
768        if eval_path.ends_with("/condition/hidden") {
769            let subject_path = eval_path[..eval_path.len() - 17].to_string();
770
771            for dep in deps {
772                let normalized_dep = path_utils::normalize_to_json_pointer(dep)
773                    .replace("/properties/", "/")
774                    .trim_start_matches('#')
775                    .to_string();
776
777                let dep_key = if normalized_dep.starts_with('/') {
778                    normalized_dep
779                } else {
780                    format!("/{}", normalized_dep)
781                };
782
783                reffed_by
784                    .entry(dep_key)
785                    .or_insert_with(Vec::new)
786                    .push(subject_path.clone());
787            }
788        }
789    }
790
791    reffed_by
792}
793
794pub fn build_dep_formula_triggers(
795    dependents_evaluations: &IndexMap<String, Vec<crate::DependentItem>>,
796    evaluations: &IndexMap<String, crate::LogicId>,
797    engine: &crate::RLogic,
798) -> IndexMap<String, Vec<(String, usize)>> {
799    let mut triggers: IndexMap<String, Vec<(String, usize)>> = IndexMap::new();
800
801    for (source_path, dep_items) in dependents_evaluations.iter() {
802        for (dep_idx, dep_item) in dep_items.iter().enumerate() {
803            let formula_keys: Vec<String> = [
804                dep_item
805                    .value
806                    .as_ref()
807                    .and_then(|v| v.as_str())
808                    .map(|s| s.to_string()),
809                dep_item
810                    .clear
811                    .as_ref()
812                    .and_then(|v| v.as_str())
813                    .map(|s| s.to_string()),
814            ]
815            .into_iter()
816            .flatten()
817            .filter(|k| k.contains("/dependents/"))
818            .collect();
819
820            for formula_key in formula_keys {
821                let logic_id = match evaluations.get(&formula_key).copied() {
822                    Some(id) => id,
823                    None => continue,
824                };
825
826                let refs = engine.get_referenced_vars(&logic_id).unwrap_or_default();
827
828                for dep_ref in refs {
829                    let normalized = path_utils::normalize_to_json_pointer(&dep_ref)
830                        .replace("/properties/", "/")
831                        .trim_start_matches('#')
832                        .to_string();
833                    let dep_key = if normalized.starts_with('/') {
834                        normalized
835                    } else {
836                        format!("/{}", normalized)
837                    };
838
839                    if dep_key.starts_with("/$") {
840                        continue;
841                    }
842
843                    if dep_key.matches('/').count() <= 1 {
844                        continue;
845                    }
846
847                    let source_data = path_utils::normalize_to_json_pointer(source_path)
848                        .replace("/properties/", "/")
849                        .trim_start_matches('#')
850                        .to_string();
851                    let source_data_key = if source_data.starts_with('/') {
852                        source_data
853                    } else {
854                        format!("/{}", source_data)
855                    };
856                    if dep_key == source_data_key {
857                        continue;
858                    }
859
860                    let target_data = path_utils::normalize_to_json_pointer(&dep_item.ref_path)
861                        .replace("/properties/", "/")
862                        .trim_start_matches('#')
863                        .to_string();
864                    let target_data_key = if target_data.starts_with('/') {
865                        target_data
866                    } else {
867                        format!("/{}", target_data)
868                    };
869                    if dep_key == target_data_key {
870                        continue;
871                    }
872
873                    let pair = (source_path.clone(), dep_idx);
874                    let entry = triggers.entry(dep_key).or_insert_with(Vec::new);
875                    if !entry.contains(&pair) {
876                        entry.push(pair);
877                    }
878                }
879            }
880        }
881    }
882
883    for sources in triggers.values_mut() {
884        sources.sort();
885        // Since we check contains above and we sort, it's pretty clean.
886    }
887
888    triggers
889}