Skip to main content

uni_locy/
result.rs

1use std::collections::HashMap;
2use std::time::Duration;
3
4use uni_common::{Properties, Value};
5
6use crate::types::{RuntimeWarning, RuntimeWarningCode};
7
8/// A single row of bindings from a Locy evaluation result.
9pub type FactRow = HashMap<String, Value>;
10
11/// The result of evaluating a compiled Locy program.
12#[derive(Debug, Clone)]
13pub struct LocyResult {
14    /// Derived facts per rule name.
15    pub derived: HashMap<String, Vec<FactRow>>,
16    /// Execution statistics.
17    pub stats: LocyStats,
18    /// Results from Phase 4 commands.
19    pub command_results: Vec<CommandResult>,
20    /// Runtime warnings collected during evaluation.
21    pub warnings: Vec<RuntimeWarning>,
22    /// Groups where BDD computation fell back to independence mode.
23    /// Maps rule name → list of human-readable key group descriptions.
24    pub approximate_groups: HashMap<String, Vec<String>>,
25    /// When present, contains the derived facts from a session-level DERIVE
26    /// that have not yet been applied. Use `tx.apply(derived)` to materialize.
27    pub derived_fact_set: Option<DerivedFactSet>,
28}
29
30/// Result of executing a single Phase 4 command.
31#[derive(Debug, Clone)]
32pub enum CommandResult {
33    Query(Vec<FactRow>),
34    Assume(Vec<FactRow>),
35    Explain(DerivationNode),
36    Abduce(AbductionResult),
37    Derive { affected: usize },
38    Cypher(Vec<FactRow>),
39}
40
41/// A node in a derivation tree, produced by EXPLAIN RULE.
42#[derive(Debug, Clone)]
43pub struct DerivationNode {
44    pub rule: String,
45    pub clause_index: usize,
46    pub priority: Option<i64>,
47    pub bindings: HashMap<String, Value>,
48    pub along_values: HashMap<String, Value>,
49    pub children: Vec<DerivationNode>,
50    pub graph_fact: Option<String>,
51    /// True when this node's probability was computed via BDD fallback
52    /// (independence mode) because the group exceeded `max_bdd_variables`.
53    pub approximate: bool,
54    /// Probability of this specific proof path, populated when top-k proof
55    /// filtering is active (Scallop, Huang et al. 2021).
56    pub proof_probability: Option<f64>,
57}
58
59/// Result of an ABDUCE query.
60#[derive(Debug, Clone)]
61pub struct AbductionResult {
62    pub modifications: Vec<ValidatedModification>,
63}
64
65/// A modification with validation status and cost.
66#[derive(Debug, Clone)]
67pub struct ValidatedModification {
68    pub modification: Modification,
69    pub validated: bool,
70    pub cost: f64,
71}
72
73/// A proposed graph modification from ABDUCE.
74#[derive(Debug, Clone)]
75pub enum Modification {
76    RemoveEdge {
77        source_var: String,
78        target_var: String,
79        edge_var: String,
80        edge_type: String,
81        match_properties: HashMap<String, Value>,
82    },
83    ChangeProperty {
84        element_var: String,
85        property: String,
86        old_value: Box<Value>,
87        new_value: Box<Value>,
88    },
89    AddEdge {
90        source_var: String,
91        target_var: String,
92        edge_type: String,
93        properties: HashMap<String, Value>,
94    },
95}
96
97/// A derived edge to be materialized.
98#[derive(Debug, Clone)]
99pub struct DerivedEdge {
100    pub edge_type: String,
101    pub source_label: String,
102    pub source_properties: Properties,
103    pub target_label: String,
104    pub target_properties: Properties,
105    pub edge_properties: Properties,
106}
107
108/// Pure-data representation of facts derived by a session-level DERIVE.
109///
110/// Apply to a transaction via `tx.apply(derived)` or `tx.apply_with(derived)`.
111#[derive(Debug, Clone)]
112pub struct DerivedFactSet {
113    /// New vertices grouped by label.
114    pub vertices: HashMap<String, Vec<Properties>>,
115    /// Derived edges connecting source/target vertices.
116    pub edges: Vec<DerivedEdge>,
117    /// Evaluation statistics from the DERIVE run.
118    pub stats: LocyStats,
119    /// Database version at evaluation time (for staleness detection).
120    pub evaluated_at_version: u64,
121    /// Internal: Cypher ASTs for faithful replay during `tx.apply()`.
122    #[doc(hidden)]
123    pub mutation_queries: Vec<uni_cypher::ast::Query>,
124}
125
126impl DerivedFactSet {
127    /// Total number of derived facts (vertices + edges).
128    pub fn fact_count(&self) -> usize {
129        self.vertices.values().map(|v| v.len()).sum::<usize>() + self.edges.len()
130    }
131
132    /// True when no facts were derived.
133    pub fn is_empty(&self) -> bool {
134        self.vertices.is_empty() && self.edges.is_empty()
135    }
136}
137
138/// Statistics collected during Locy program evaluation.
139#[derive(Debug, Clone, Default)]
140pub struct LocyStats {
141    pub strata_evaluated: usize,
142    pub total_iterations: usize,
143    pub derived_nodes: usize,
144    pub derived_edges: usize,
145    pub evaluation_time: Duration,
146    pub queries_executed: usize,
147    pub mutations_executed: usize,
148    /// Peak memory used by derived relations (in bytes).
149    pub peak_memory_bytes: usize,
150}
151
152impl LocyResult {
153    /// Get derived facts for a specific rule.
154    pub fn derived_facts(&self, rule: &str) -> Option<&Vec<FactRow>> {
155        self.derived.get(rule)
156    }
157
158    /// Get rows from the first Query command result.
159    pub fn rows(&self) -> Option<&Vec<FactRow>> {
160        self.command_results.iter().find_map(|cr| cr.as_query())
161    }
162
163    /// Get column names from the first Query command result's first row.
164    pub fn columns(&self) -> Option<Vec<String>> {
165        self.rows()
166            .and_then(|rows| rows.first().map(|row| row.keys().cloned().collect()))
167    }
168
169    /// Get execution statistics.
170    pub fn stats(&self) -> &LocyStats {
171        &self.stats
172    }
173
174    /// Get the total number of fixpoint iterations.
175    pub fn iterations(&self) -> usize {
176        self.stats.total_iterations
177    }
178
179    /// Get runtime warnings collected during evaluation.
180    pub fn warnings(&self) -> &[RuntimeWarning] {
181        &self.warnings
182    }
183
184    /// Check whether a specific warning code was emitted.
185    pub fn has_warning(&self, code: &RuntimeWarningCode) -> bool {
186        self.warnings.iter().any(|w| w.code == *code)
187    }
188}
189
190impl CommandResult {
191    /// If this is an Explain result, return the derivation node.
192    pub fn as_explain(&self) -> Option<&DerivationNode> {
193        match self {
194            CommandResult::Explain(node) => Some(node),
195            _ => None,
196        }
197    }
198
199    /// If this is a Query result, return the rows.
200    pub fn as_query(&self) -> Option<&Vec<FactRow>> {
201        match self {
202            CommandResult::Query(rows) => Some(rows),
203            _ => None,
204        }
205    }
206
207    /// If this is an Abduce result, return it.
208    pub fn as_abduce(&self) -> Option<&AbductionResult> {
209        match self {
210            CommandResult::Abduce(result) => Some(result),
211            _ => None,
212        }
213    }
214}