json_eval_rs/parse_schema/
parsed.rs

1/// ParsedSchema parsing for schema caching and reuse
2
3use indexmap::{IndexMap, IndexSet};
4use serde_json::{Map, Value};
5use std::sync::Arc;
6
7use crate::parse_schema::common::compute_column_partitions;
8use crate::{topo_sort, LogicId, RLogic, path_utils};
9use crate::table_metadata::{ColumnMetadata, RepeatBoundMetadata, RowMetadata, TableMetadata};
10use crate::ParsedSchema;
11
12pub fn parse_schema_into(parsed: &mut ParsedSchema) -> Result<(), String> {
13    /// Single-pass schema walker that collects everything
14    fn walk(
15        value: &Value,
16        path: &str,
17        engine: &mut RLogic,
18        evaluations: &mut IndexMap<String, LogicId>,
19        tables: &mut IndexMap<String, Value>,
20        deps: &mut IndexMap<String, IndexSet<String>>,
21        value_fields: &mut Vec<String>,
22        layout_paths: &mut Vec<String>,
23        dependents: &mut IndexMap<String, Vec<crate::DependentItem>>,
24        options_templates: &mut Vec<(String, String, String)>,
25        subforms: &mut Vec<(String, serde_json::Map<String, Value>, Value)>,
26        fields_with_rules: &mut Vec<String>,
27    ) -> Result<(), String> {
28        match value {
29            Value::Object(map) => {
30                // Check for $evaluation
31                if let Some(evaluation) = map.get("$evaluation") {
32                    let key = path.to_string();
33                    let logic_value = evaluation.get("logic").unwrap_or(evaluation);
34                    let logic_id = engine
35                        .compile(logic_value)
36                        .map_err(|e| format!("failed to compile evaluation at {key}: {e}"))?;
37                    evaluations.insert(key.clone(), logic_id);
38
39                    // Collect dependencies with smart table inheritance
40                    // Normalize all dependencies to JSON pointer format to avoid duplicates
41                    let mut refs: IndexSet<String> = engine
42                        .get_referenced_vars(&logic_id)
43                        .unwrap_or_default()
44                        .into_iter()
45                        .map(|dep| path_utils::normalize_to_json_pointer(&dep))
46                        .filter(|dep| {
47                            // Filter out simple column references (e.g., "/INSAGE_YEAR", "/PREM_PP")
48                            // These are FINDINDEX/MATCH column names, not actual data dependencies
49                            // Real dependencies have multiple path segments (e.g., "/illustration/properties/...")
50                            dep.matches('/').count() > 1 || dep.starts_with("/$")
51                        })
52                        .collect();
53                    let mut extra_refs = IndexSet::new();
54                    collect_refs(logic_value, &mut extra_refs);
55                    if !extra_refs.is_empty() {
56                        refs.extend(extra_refs.into_iter());
57                    }
58
59                    // For table dependencies, inherit parent table path instead of individual rows
60                    let refs: IndexSet<String> = refs
61                        .into_iter()
62                        .filter_map(|dep| {
63                            // If dependency is a table row (contains /$table/), inherit table path
64                            if let Some(table_idx) = dep.find("/$table/") {
65                                let table_path = &dep[..table_idx];
66                                Some(table_path.to_string())
67                            } else {
68                                Some(dep)
69                            }
70                        })
71                        .collect();
72
73                    if !refs.is_empty() {
74                        deps.insert(key.clone(), refs);
75                    }
76                }
77
78                // Check for $table
79                if let Some(table) = map.get("$table") {
80                    let key = path.to_string();
81
82                    let rows = table.clone();
83                    let datas = map
84                        .get("$datas")
85                        .cloned()
86                        .unwrap_or_else(|| Value::Array(vec![]));
87                    let skip = map.get("$skip").cloned().unwrap_or(Value::Bool(false));
88                    let clear = map.get("$clear").cloned().unwrap_or(Value::Bool(false));
89
90                    let mut table_entry = Map::new();
91                    table_entry.insert("rows".to_string(), rows);
92                    table_entry.insert("datas".to_string(), datas);
93                    table_entry.insert("skip".to_string(), skip);
94                    table_entry.insert("clear".to_string(), clear);
95
96                    tables.insert(key, Value::Object(table_entry));
97                }
98
99                // Check for $layout with elements
100                if let Some(layout_obj) = map.get("$layout") {
101                    if let Some(Value::Array(_)) = layout_obj.get("elements") {
102                        let layout_elements_path = format!("{}/$layout/elements", path);
103                        layout_paths.push(layout_elements_path);
104                    }
105                }
106                
107                // Check for rules object - collect field path for efficient validation
108                if map.contains_key("rules") && !path.is_empty() && !path.starts_with("#/$") {
109                    // Convert JSON pointer path to dotted notation for validation
110                    // E.g., "#/properties/form/properties/name" -> "form.name"
111                    let field_path = path
112                        .trim_start_matches('#')
113                        .replace("/properties/", ".")
114                        .trim_start_matches('/')
115                        .trim_start_matches('.')
116                        .to_string();
117                    
118                    if !field_path.is_empty() && !field_path.starts_with("$") {
119                        fields_with_rules.push(field_path);
120                    }
121                }
122
123                // Check for options with URL templates
124                if let Some(Value::String(url)) = map.get("url") {
125                    // Check if URL contains template pattern {variable}
126                    if url.contains('{') && url.contains('}') {
127                        // Convert to JSON pointer format for evaluated_schema access
128                        let url_path = path_utils::normalize_to_json_pointer(&format!("{}/url", path));
129                        let params_path = path_utils::normalize_to_json_pointer(&format!("{}/params", path));
130                        options_templates.push((url_path, url.clone(), params_path));
131                    }
132                }
133                
134                // Check for array fields with items (subforms)
135                if let Some(Value::String(type_str)) = map.get("type") {
136                    if type_str == "array" {
137                        if let Some(items) = map.get("items") {
138                            // Store subform info for later creation (after walk completes)
139                            subforms.push((path.to_string(), map.clone(), items.clone()));
140                            // Don't recurse into items - it will be processed as a separate subform
141                            return Ok(());
142                        }
143                    }
144                }
145
146                // Check for dependents array
147                if let Some(Value::Array(dependents_arr)) = map.get("dependents") {
148                    let mut dependent_items = Vec::new();
149                    
150                    for (dep_idx, dep_item) in dependents_arr.iter().enumerate() {
151                        if let Value::Object(dep_obj) = dep_item {
152                            if let Some(Value::String(ref_path)) = dep_obj.get("$ref") {
153                                // Process clear - compile if it's an $evaluation
154                                let clear_val = if let Some(clear) = dep_obj.get("clear") {
155                                    if let Value::Object(clear_obj) = clear {
156                                        if clear_obj.contains_key("$evaluation") {
157                                            // Compile and store the evaluation
158                                            let clear_eval = clear_obj.get("$evaluation").unwrap();
159                                            let clear_key = format!("{}/dependents/{}/clear", path, dep_idx);
160                                            let logic_id = engine.compile(clear_eval)
161                                                .map_err(|e| format!("Failed to compile dependent clear at {}: {}", clear_key, e))?;
162                                            evaluations.insert(clear_key.clone(), logic_id);
163                                            // Replace with eval key reference
164                                            Some(Value::String(clear_key))
165                                        } else {
166                                            Some(clear.clone())
167                                        }
168                                    } else {
169                                        Some(clear.clone())
170                                    }
171                                } else {
172                                    None
173                                };
174                                
175                                // Process value - compile if it's an $evaluation
176                                let value_val = if let Some(value) = dep_obj.get("value") {
177                                    if let Value::Object(value_obj) = value {
178                                        if value_obj.contains_key("$evaluation") {
179                                            // Compile and store the evaluation
180                                            let value_eval = value_obj.get("$evaluation").unwrap();
181                                            let value_key = format!("{}/dependents/{}/value", path, dep_idx);
182                                            let logic_id = engine.compile(value_eval)
183                                                .map_err(|e| format!("Failed to compile dependent value at {}: {}", value_key, e))?;
184                                            evaluations.insert(value_key.clone(), logic_id);
185                                            // Replace with eval key reference
186                                            Some(Value::String(value_key))
187                                        } else {
188                                            Some(value.clone())
189                                        }
190                                    } else {
191                                        Some(value.clone())
192                                    }
193                                } else {
194                                    None
195                                };
196                                
197                                dependent_items.push(crate::DependentItem {
198                                    ref_path: ref_path.clone(),
199                                    clear: clear_val,
200                                    value: value_val,
201                                });
202                            }
203                        }
204                    }
205                    
206                    if !dependent_items.is_empty() {
207                        dependents.insert(path.to_string(), dependent_items);
208                    }
209                }
210
211                // Recurse into children
212                Ok(for (key, val) in map {
213                    // Skip special evaluation and dependents keys from recursion (already processed above)
214                    if key == "$evaluation" || key == "dependents" {
215                        continue;
216                    }
217                    
218                    let next_path = if path == "#" {
219                        format!("#/{key}")
220                    } else {
221                        format!("{path}/{key}")
222                    };
223                    
224                    // Check if this is a "value" field
225                    if key == "value" && !next_path.starts_with("#/$") && !next_path.contains("/$layout/") && !next_path.contains("/items/") && !next_path.contains("/options/") && !next_path.contains("/dependents/") && !next_path.contains("/rules/") {
226                        value_fields.push(next_path.clone());
227                    }
228                    
229                    // Recurse into all children (including $ keys like $table, $datas, etc.)
230                    walk(val, &next_path, engine, evaluations, tables, deps, value_fields, layout_paths, dependents, options_templates, subforms, fields_with_rules)?;
231                })
232            }
233            Value::Array(arr) => Ok(for (index, item) in arr.iter().enumerate() {
234                let next_path = if path == "#" {
235                    format!("#/{index}")
236                } else {
237                    format!("{path}/{index}")
238                };
239                walk(item, &next_path, engine, evaluations, tables, deps, value_fields, layout_paths, dependents, options_templates, subforms, fields_with_rules)?;
240            }),
241            _ => Ok(()),
242        }
243    }
244
245    fn collect_refs(value: &Value, refs: &mut IndexSet<String>) {
246        match value {
247            Value::Object(map) => {
248                if let Some(path) = map.get("$ref").and_then(Value::as_str) {
249                    refs.insert(path_utils::normalize_to_json_pointer(path));
250                }
251                if let Some(path) = map.get("ref").and_then(Value::as_str) {
252                    refs.insert(path_utils::normalize_to_json_pointer(path));
253                }
254                if let Some(var_val) = map.get("var") {
255                    match var_val {
256                        Value::String(s) => {
257                            refs.insert(s.clone());
258                        }
259                        Value::Array(arr) => {
260                            if let Some(path) = arr.get(0).and_then(Value::as_str) {
261                                refs.insert(path.to_string());
262                            }
263                        }
264                        _ => {}
265                    }
266                }
267                for val in map.values() {
268                    collect_refs(val, refs);
269                }
270            }
271            Value::Array(arr) => {
272                for val in arr {
273                    collect_refs(val, refs);
274                }
275            }
276            _ => {}
277        }
278    }
279
280    // Single-pass collection: walk schema once and collect everything
281    let mut evaluations = IndexMap::new();
282    let mut tables = IndexMap::new();
283    let mut dependencies = IndexMap::new();
284    let mut value_fields = Vec::new();
285    let mut layout_paths = Vec::new();
286    let mut dependents_evaluations = IndexMap::new();
287    let mut options_templates = Vec::new();
288    let mut subforms_data = Vec::new();
289    
290    // Get mutable access to engine through Arc
291    let engine = Arc::get_mut(&mut parsed.engine)
292        .ok_or("Cannot get mutable reference to engine - ParsedSchema is shared")?;
293    
294    let mut fields_with_rules = Vec::new();
295    
296    walk(
297        &parsed.schema,
298        "#",
299        engine,
300        &mut evaluations,
301        &mut tables,
302        &mut dependencies,
303        &mut value_fields,
304        &mut layout_paths,
305        &mut dependents_evaluations,
306        &mut options_templates,
307        &mut subforms_data,
308        &mut fields_with_rules,
309    )?;
310    
311    parsed.evaluations = Arc::new(evaluations);
312    parsed.tables = Arc::new(tables);
313    parsed.dependencies = Arc::new(dependencies);
314    parsed.layout_paths = Arc::new(layout_paths);
315    parsed.dependents_evaluations = Arc::new(dependents_evaluations);
316    parsed.options_templates = Arc::new(options_templates);
317    parsed.fields_with_rules = Arc::new(fields_with_rules);
318    
319    // Build subforms from collected data (after walk completes)
320    parsed.subforms = build_subforms_from_data_parsed(subforms_data, parsed)?;
321    
322    // Collect table-level dependencies by aggregating all column dependencies
323    collect_table_dependencies_parsed(parsed);
324    
325    parsed.sorted_evaluations = Arc::new(topo_sort::parsed::topological_sort_parsed(parsed)?);
326    
327    // Categorize evaluations for result handling
328    categorize_evaluations_parsed(parsed);
329    
330    // Process collected value fields
331    process_value_fields_parsed(parsed, value_fields);
332    
333    // Pre-compile all table metadata for zero-copy evaluation
334    build_table_metadata_parsed(parsed)?;
335    
336    Ok(())
337}
338
339// ============================================================================
340
341/// Build subforms from collected data for ParsedSchema
342fn build_subforms_from_data_parsed(
343    subforms_data: Vec<(String, serde_json::Map<String, Value>, Value)>,
344    parsed: &ParsedSchema,
345) -> Result<IndexMap<String, Arc<ParsedSchema>>, String> {
346    let mut subforms = IndexMap::new();
347    
348    for (path, field_map, items) in subforms_data {
349        create_subform_parsed(&path, &field_map, &items, &mut subforms, parsed)?;
350    }
351    
352    Ok(subforms)
353}
354
355/// Create an isolated ParsedSchema for a subform (ParsedSchema version)
356/// 
357/// This creates an Arc<ParsedSchema> instead of Box<JSONEval> for efficient sharing.
358/// Subforms can now be cloned cheaply across multiple JSONEval instances.
359fn create_subform_parsed(
360    path: &str,
361    field_map: &serde_json::Map<String, Value>,
362    items: &Value,
363    subforms: &mut IndexMap<String, Arc<ParsedSchema>>,
364    parsed: &ParsedSchema,
365) -> Result<(), String> {
366    // Extract field key from path (e.g., "#/riders" -> "riders")
367    let field_key = path.trim_start_matches('#').trim_start_matches('/');
368    
369    // Build subform schema: { $params: from parent, [field_key]: items content }
370    let mut subform_schema = serde_json::Map::new();
371    
372    // Copy $params from parent schema
373    if let Some(params) = parsed.schema.get("$params") {
374        subform_schema.insert("$params".to_string(), params.clone());
375    }
376    
377    // Create field object with items content
378    let mut field_obj = serde_json::Map::new();
379    
380    // Copy properties from items
381    if let Value::Object(items_map) = items {
382        for (key, value) in items_map {
383            field_obj.insert(key.clone(), value.clone());
384        }
385    }
386    
387    // Copy field-level properties (title, etc.) but exclude items and type="array"
388    for (key, value) in field_map {
389        if key != "items" && key != "type" {
390            field_obj.insert(key.clone(), value.clone());
391        }
392    }
393    
394    // Set type to "object" for the subform root
395    field_obj.insert("type".to_string(), Value::String("object".to_string()));
396    
397    subform_schema.insert(field_key.to_string(), Value::Object(field_obj));
398    
399    // Parse into ParsedSchema (more efficient than JSONEval)
400    // This allows the subform to be shared via Arc across multiple evaluations
401    let subform_schema_value = Value::Object(subform_schema);
402    let subform_parsed = ParsedSchema::parse_value(subform_schema_value)
403        .map_err(|e| format!("Failed to parse subform schema for {}: {}", field_key, e))?;
404    
405    subforms.insert(path.to_string(), Arc::new(subform_parsed));
406    
407    Ok(())
408}
409
410/// Collect dependencies for tables (ParsedSchema version)
411fn collect_table_dependencies_parsed(parsed: &mut ParsedSchema) {
412    let table_keys: Vec<String> = parsed.tables.keys().cloned().collect();
413    
414    // Clone the dependencies to a mutable map
415    let mut dependencies = (*parsed.dependencies).clone();
416    
417    for table_key in table_keys {
418        let mut table_deps = IndexSet::new();
419        
420        // Collect dependencies from all evaluations that belong to this table
421        for (eval_key, deps) in &dependencies {
422            // Check if this evaluation is within the table
423            if eval_key.starts_with(&table_key) && eval_key != &table_key {
424                // Add all dependencies from table cells/columns
425                for dep in deps {
426                    // Filter out self-references and internal table paths
427                    if !dep.starts_with(&table_key) {
428                        table_deps.insert(dep.clone());
429                    }
430                }
431            }
432        }
433        
434        // Store aggregated dependencies for the table
435        if !table_deps.is_empty() {
436            dependencies.insert(table_key.clone(), table_deps);
437        }
438    }
439    
440    // Wrap the updated dependencies in Arc
441    parsed.dependencies = Arc::new(dependencies);
442}
443
444/// Categorize evaluations for different result handling (ParsedSchema version)
445fn categorize_evaluations_parsed(parsed: &mut ParsedSchema) {
446    // Collect all evaluation keys that are in sorted_evaluations (batches)
447    let batched_keys: IndexSet<String> = parsed.sorted_evaluations
448        .iter()
449        .flatten()
450        .cloned()
451        .collect();
452    
453    let mut rules_evaluations = Vec::new();
454    let mut others_evaluations = Vec::new();
455    
456    // Find evaluations NOT in batches and categorize them
457    for eval_key in parsed.evaluations.keys() {
458        // Skip if already in sorted_evaluations batches
459        if batched_keys.contains(eval_key) {
460            continue;
461        }
462        
463        // Skip table-related evaluations
464        if parsed.tables.iter().any(|(key, _)| eval_key.starts_with(key)) {
465            continue;
466        }
467
468        // Categorize based on path patterns
469        if eval_key.contains("/rules/") {
470            rules_evaluations.push(eval_key.clone());
471        } else if !eval_key.contains("/dependents/") {
472            // Don't add dependents to others_evaluations
473            others_evaluations.push(eval_key.clone());
474        }
475    }
476    
477    // Wrap in Arc
478    parsed.rules_evaluations = Arc::new(rules_evaluations);
479    parsed.others_evaluations = Arc::new(others_evaluations);
480}
481
482/// Process collected value fields (ParsedSchema version)
483fn process_value_fields_parsed(parsed: &mut ParsedSchema, value_fields: Vec<String>) {
484    let mut value_evaluations = Vec::new();
485    
486    for path in value_fields {
487        // Skip if already collected from evaluations in categorize_evaluations
488        if value_evaluations.contains(&path) {
489            continue;
490        }
491        
492        // Skip table-related paths
493        if parsed.tables.iter().any(|(key, _)| path.starts_with(key)) {
494            continue;
495        }
496        
497        value_evaluations.push(path);
498    }
499    
500    // Wrap in Arc
501    parsed.value_evaluations = Arc::new(value_evaluations);
502}
503
504/// Build pre-compiled table metadata (ParsedSchema version)
505fn build_table_metadata_parsed(parsed: &mut ParsedSchema) -> Result<(), String> {
506    let mut table_metadata = IndexMap::new();
507    
508    for (eval_key, table) in parsed.tables.iter() {
509        let metadata = compile_table_metadata_parsed(parsed, eval_key, table)?;
510        table_metadata.insert(eval_key.to_string(), metadata);
511    }
512    
513    parsed.table_metadata = Arc::new(table_metadata);
514    Ok(())
515}
516
517/// Compile table metadata at parse time (ParsedSchema version)
518fn compile_table_metadata_parsed(
519    parsed: &ParsedSchema,
520    eval_key: &str,
521    table: &Value,
522) -> Result<TableMetadata, String> {
523    let rows = table
524        .get("rows")
525        .and_then(|v| v.as_array())
526        .ok_or("table missing rows")?;
527    let empty_datas = Vec::new();
528    let datas = table
529        .get("datas")
530        .and_then(|v| v.as_array())
531        .unwrap_or(&empty_datas);
532
533    // Pre-compile data plans with Arc sharing
534    let mut data_plans = Vec::with_capacity(datas.len());
535    for (idx, entry) in datas.iter().enumerate() {
536        let Some(name) = entry.get("name").and_then(|v| v.as_str()) else { continue };
537        let logic_path = format!("{eval_key}/$datas/{idx}/data");
538        let logic = parsed.evaluations.get(&logic_path).copied();
539        let literal = entry.get("data").map(|v| Arc::new(v.clone()));
540        data_plans.push((Arc::from(name), logic, literal));
541    }
542
543    // Pre-compile row plans with dependency analysis
544    let mut row_plans = Vec::with_capacity(rows.len());
545    for (row_idx, row_val) in rows.iter().enumerate() {
546        let Some(row_obj) = row_val.as_object() else {
547            continue;
548        };
549
550        if let Some(repeat_arr) = row_obj.get("$repeat").and_then(|v| v.as_array()) {
551            if repeat_arr.len() == 3 {
552                let start_logic_path = format!("{eval_key}/$table/{row_idx}/$repeat/0");
553                let end_logic_path = format!("{eval_key}/$table/{row_idx}/$repeat/1");
554                let start_logic = parsed.evaluations.get(&start_logic_path).copied();
555                let end_logic = parsed.evaluations.get(&end_logic_path).copied();
556
557                let start_literal = Arc::new(repeat_arr.get(0).cloned().unwrap_or(Value::Null));
558                let end_literal = Arc::new(repeat_arr.get(1).cloned().unwrap_or(Value::Null));
559
560                if let Some(template) = repeat_arr.get(2).and_then(|v| v.as_object()) {
561                    let mut columns = Vec::with_capacity(template.len());
562                    for (col_name, col_val) in template {
563                        let col_eval_path =
564                            format!("{eval_key}/$table/{row_idx}/$repeat/2/{col_name}");
565                        let logic = parsed.evaluations.get(&col_eval_path).copied();
566                        let literal = if logic.is_none() {
567                            Some(col_val.clone())
568                        } else {
569                            None
570                        };
571                        
572                        // Extract dependencies ONCE at parse time (not during evaluation)
573                        let (dependencies, has_forward_ref) = if let Some(logic_id) = logic {
574                            let deps = parsed.engine.get_referenced_vars(&logic_id)
575                                .unwrap_or_default()
576                                .into_iter()
577                                .filter(|v| v.starts_with('$') && v != "$iteration" && v != "$threshold")
578                                .collect();
579                            let has_fwd = parsed.engine.has_forward_reference(&logic_id);
580                            (deps, has_fwd)
581                        } else {
582                            (Vec::new(), false)
583                        };
584                        
585                        columns.push(ColumnMetadata::new(col_name, logic, literal, dependencies, has_forward_ref));
586                    }
587
588                    // Pre-compute forward column propagation (transitive closure)
589                    let (forward_cols, normal_cols) = compute_column_partitions(&columns);
590
591                    row_plans.push(RowMetadata::Repeat {
592                        start: RepeatBoundMetadata {
593                            logic: start_logic,
594                            literal: start_literal,
595                        },
596                        end: RepeatBoundMetadata {
597                            logic: end_logic,
598                            literal: end_literal,
599                        },
600                        columns: columns.into(),
601                        forward_cols: forward_cols.into(),
602                        normal_cols: normal_cols.into(),
603                    });
604                    continue;
605                }
606            }
607        }
608
609        // Static row
610        let mut columns = Vec::with_capacity(row_obj.len());
611        for (col_name, col_val) in row_obj {
612            if col_name == "$repeat" {
613                continue;
614            }
615            let col_eval_path = format!("{eval_key}/$table/{row_idx}/{col_name}");
616            let logic = parsed.evaluations.get(&col_eval_path).copied();
617            let literal = if logic.is_none() {
618                Some(col_val.clone())
619            } else {
620                None
621            };
622            
623            // Extract dependencies ONCE at parse time
624            let (dependencies, has_forward_ref) = if let Some(logic_id) = logic {
625                let deps = parsed.engine.get_referenced_vars(&logic_id)
626                    .unwrap_or_default()
627                    .into_iter()
628                    .filter(|v| v.starts_with('$') && v != "$iteration" && v != "$threshold")
629                    .collect();
630                let has_fwd = parsed.engine.has_forward_reference(&logic_id);
631                (deps, has_fwd)
632            } else {
633                (Vec::new(), false)
634            };
635            
636            columns.push(ColumnMetadata::new(col_name, logic, literal, dependencies, has_forward_ref));
637        }
638        row_plans.push(RowMetadata::Static { columns: columns.into() });
639    }
640
641    // Pre-compile skip/clear logic
642    let skip_logic = parsed.evaluations.get(&format!("{eval_key}/$skip")).copied();
643    let skip_literal = table.get("skip").and_then(Value::as_bool).unwrap_or(false);
644    let clear_logic = parsed.evaluations.get(&format!("{eval_key}/$clear")).copied();
645    let clear_literal = table.get("clear").and_then(Value::as_bool).unwrap_or(false);
646
647    Ok(TableMetadata {
648        data_plans: data_plans.into(),
649        row_plans: row_plans.into(),
650        skip_logic,
651        skip_literal,
652        clear_logic,
653        clear_literal,
654    })
655}