Skip to main content

pattern_core/
pattern_graph.rs

1//! PatternGraph: typed container for nodes, relationships, walks, and annotations.
2//!
3//! Ported from `Pattern.PatternGraph` in the Haskell reference implementation.
4//! Patterns are routed into six typed collections by a `GraphClassifier`.
5//! Duplicate identities are resolved via `ReconciliationPolicy`.
6
7use std::collections::HashMap;
8
9use crate::graph::graph_classifier::{GraphClass, GraphClassifier, GraphValue};
10use crate::graph::graph_query::GraphQuery;
11use crate::pattern::Pattern;
12use crate::reconcile::{HasIdentity, Mergeable, ReconciliationPolicy, Refinable};
13use crate::subject::Symbol;
14
15// -----------------------------------------------------------------------------
16// PatternGraph struct
17// -----------------------------------------------------------------------------
18
19/// Materialized graph container with six typed collections, each keyed by identity.
20pub struct PatternGraph<Extra, V: GraphValue> {
21    pub pg_nodes: HashMap<V::Id, Pattern<V>>,
22    pub pg_relationships: HashMap<V::Id, Pattern<V>>,
23    pub pg_walks: HashMap<V::Id, Pattern<V>>,
24    pub pg_annotations: HashMap<V::Id, Pattern<V>>,
25    pub pg_other: HashMap<V::Id, (Extra, Pattern<V>)>,
26    pub pg_conflicts: HashMap<V::Id, Vec<Pattern<V>>>,
27}
28
29impl<Extra, V: GraphValue> PatternGraph<Extra, V> {
30    /// Returns an empty graph with all six maps empty.
31    pub fn empty() -> Self {
32        PatternGraph {
33            pg_nodes: HashMap::new(),
34            pg_relationships: HashMap::new(),
35            pg_walks: HashMap::new(),
36            pg_annotations: HashMap::new(),
37            pg_other: HashMap::new(),
38            pg_conflicts: HashMap::new(),
39        }
40    }
41}
42
43// -----------------------------------------------------------------------------
44// Trait bounds alias (used throughout)
45// -----------------------------------------------------------------------------
46
47// V must be GraphValue + HasIdentity<V, Symbol> + Mergeable + Refinable + PartialEq + Clone
48// We spell this out on each function rather than using a trait alias (stable Rust).
49
50// -----------------------------------------------------------------------------
51// twoOccurrences helper
52// -----------------------------------------------------------------------------
53
54/// Constructs a synthetic pattern with `existing` as the root and `incoming` as
55/// its single child. This gives `reconcile` exactly two occurrences to resolve.
56fn two_occurrences<V: Clone>(existing: &Pattern<V>, incoming: Pattern<V>) -> Pattern<V> {
57    Pattern {
58        value: existing.value.clone(),
59        elements: vec![incoming],
60    }
61}
62
63// -----------------------------------------------------------------------------
64// Private insert functions
65// -----------------------------------------------------------------------------
66
67fn insert_node<Extra, V>(
68    policy: &ReconciliationPolicy<V::MergeStrategy>,
69    p: Pattern<V>,
70    mut g: PatternGraph<Extra, V>,
71) -> PatternGraph<Extra, V>
72where
73    V: GraphValue<Id = Symbol> + HasIdentity<V, Symbol> + Mergeable + Refinable + PartialEq + Clone,
74{
75    let i = V::identity(&p.value).clone();
76    match g.pg_nodes.remove(&i) {
77        None => {
78            g.pg_nodes.insert(i, p);
79        }
80        Some(existing) => {
81            let synthetic = two_occurrences(&existing, p.clone());
82            match crate::reconcile::reconcile(policy, &synthetic) {
83                Err(_) => {
84                    g.pg_nodes.insert(i.clone(), existing);
85                    g.pg_conflicts.entry(i).or_default().push(p);
86                }
87                Ok(merged) => {
88                    g.pg_nodes.insert(i, merged);
89                }
90            }
91        }
92    }
93    g
94}
95
96fn insert_relationship<Extra, V>(
97    classifier: &GraphClassifier<Extra, V>,
98    policy: &ReconciliationPolicy<V::MergeStrategy>,
99    p: Pattern<V>,
100    g: PatternGraph<Extra, V>,
101) -> PatternGraph<Extra, V>
102where
103    V: GraphValue<Id = Symbol>
104        + HasIdentity<V, Symbol>
105        + Mergeable
106        + Refinable
107        + PartialEq
108        + Clone
109        + 'static,
110    Extra: 'static,
111{
112    // Merge endpoint nodes first.
113    let g1 = if p.elements.len() == 2 {
114        let n1 = p.elements[0].clone();
115        let n2 = p.elements[1].clone();
116        let g1 = merge_with_policy(classifier, policy, n1, g);
117        merge_with_policy(classifier, policy, n2, g1)
118    } else {
119        g
120    };
121
122    let i = V::identity(&p.value).clone();
123    let mut g2 = g1;
124    match g2.pg_relationships.remove(&i) {
125        None => {
126            g2.pg_relationships.insert(i, p);
127        }
128        Some(existing) => {
129            let synthetic = two_occurrences(&existing, p.clone());
130            match crate::reconcile::reconcile(policy, &synthetic) {
131                Err(_) => {
132                    g2.pg_relationships.insert(i.clone(), existing);
133                    g2.pg_conflicts.entry(i).or_default().push(p);
134                }
135                Ok(merged) => {
136                    g2.pg_relationships.insert(i, merged);
137                }
138            }
139        }
140    }
141    g2
142}
143
144fn insert_walk<Extra, V>(
145    classifier: &GraphClassifier<Extra, V>,
146    policy: &ReconciliationPolicy<V::MergeStrategy>,
147    p: Pattern<V>,
148    g: PatternGraph<Extra, V>,
149) -> PatternGraph<Extra, V>
150where
151    V: GraphValue<Id = Symbol>
152        + HasIdentity<V, Symbol>
153        + Mergeable
154        + Refinable
155        + PartialEq
156        + Clone
157        + 'static,
158    Extra: 'static,
159{
160    // Merge each component relationship (which recursively merges their nodes).
161    let elements: Vec<Pattern<V>> = p.elements.clone();
162    let g1 = elements.into_iter().fold(g, |acc, elem| {
163        merge_with_policy(classifier, policy, elem, acc)
164    });
165
166    let i = V::identity(&p.value).clone();
167    let mut g2 = g1;
168    match g2.pg_walks.remove(&i) {
169        None => {
170            g2.pg_walks.insert(i, p);
171        }
172        Some(existing) => {
173            let synthetic = two_occurrences(&existing, p.clone());
174            match crate::reconcile::reconcile(policy, &synthetic) {
175                Err(_) => {
176                    g2.pg_walks.insert(i.clone(), existing);
177                    g2.pg_conflicts.entry(i).or_default().push(p);
178                }
179                Ok(merged) => {
180                    g2.pg_walks.insert(i, merged);
181                }
182            }
183        }
184    }
185    g2
186}
187
188fn insert_annotation<Extra, V>(
189    classifier: &GraphClassifier<Extra, V>,
190    policy: &ReconciliationPolicy<V::MergeStrategy>,
191    p: Pattern<V>,
192    g: PatternGraph<Extra, V>,
193) -> PatternGraph<Extra, V>
194where
195    V: GraphValue<Id = Symbol>
196        + HasIdentity<V, Symbol>
197        + Mergeable
198        + Refinable
199        + PartialEq
200        + Clone
201        + 'static,
202    Extra: 'static,
203{
204    // Merge the single inner element first.
205    let g1 = if p.elements.len() == 1 {
206        let inner = p.elements[0].clone();
207        merge_with_policy(classifier, policy, inner, g)
208    } else {
209        g
210    };
211
212    let i = V::identity(&p.value).clone();
213    let mut g2 = g1;
214    match g2.pg_annotations.remove(&i) {
215        None => {
216            g2.pg_annotations.insert(i, p);
217        }
218        Some(existing) => {
219            let synthetic = two_occurrences(&existing, p.clone());
220            match crate::reconcile::reconcile(policy, &synthetic) {
221                Err(_) => {
222                    g2.pg_annotations.insert(i.clone(), existing);
223                    g2.pg_conflicts.entry(i).or_default().push(p);
224                }
225                Ok(merged) => {
226                    g2.pg_annotations.insert(i, merged);
227                }
228            }
229        }
230    }
231    g2
232}
233
234fn insert_other<Extra, V>(
235    policy: &ReconciliationPolicy<V::MergeStrategy>,
236    extra: Extra,
237    p: Pattern<V>,
238    mut g: PatternGraph<Extra, V>,
239) -> PatternGraph<Extra, V>
240where
241    V: GraphValue<Id = Symbol> + HasIdentity<V, Symbol> + Mergeable + Refinable + PartialEq + Clone,
242{
243    let i = V::identity(&p.value).clone();
244    match g.pg_other.remove(&i) {
245        None => {
246            g.pg_other.insert(i, (extra, p));
247        }
248        Some((existing_extra, existing)) => {
249            let synthetic = two_occurrences(&existing, p.clone());
250            match crate::reconcile::reconcile(policy, &synthetic) {
251                Err(_) => {
252                    g.pg_other.insert(i.clone(), (existing_extra, existing));
253                    g.pg_conflicts.entry(i).or_default().push(p);
254                }
255                Ok(merged) => {
256                    g.pg_other.insert(i, (existing_extra, merged));
257                }
258            }
259        }
260    }
261    g
262}
263
264// -----------------------------------------------------------------------------
265// Public API
266// -----------------------------------------------------------------------------
267
268/// Inserts one pattern using the given reconciliation policy.
269///
270/// Dispatches to the appropriate typed collection based on `classifier`.
271/// Sub-elements are recursively merged before the top-level pattern is inserted.
272pub fn merge_with_policy<Extra, V>(
273    classifier: &GraphClassifier<Extra, V>,
274    policy: &ReconciliationPolicy<V::MergeStrategy>,
275    p: Pattern<V>,
276    g: PatternGraph<Extra, V>,
277) -> PatternGraph<Extra, V>
278where
279    V: GraphValue<Id = Symbol>
280        + HasIdentity<V, Symbol>
281        + Mergeable
282        + Refinable
283        + PartialEq
284        + Clone
285        + 'static,
286    Extra: 'static,
287{
288    match (classifier.classify)(&p) {
289        GraphClass::GNode => insert_node(policy, p, g),
290        GraphClass::GRelationship => insert_relationship(classifier, policy, p, g),
291        GraphClass::GWalk => insert_walk(classifier, policy, p, g),
292        GraphClass::GAnnotation => insert_annotation(classifier, policy, p, g),
293        GraphClass::GOther(extra) => insert_other(policy, extra, p, g),
294    }
295}
296
297/// Inserts one pattern using `LastWriteWins` policy.
298pub fn merge<Extra, V>(
299    classifier: &GraphClassifier<Extra, V>,
300    p: Pattern<V>,
301    g: PatternGraph<Extra, V>,
302) -> PatternGraph<Extra, V>
303where
304    V: GraphValue<Id = Symbol>
305        + HasIdentity<V, Symbol>
306        + Mergeable
307        + Refinable
308        + PartialEq
309        + Clone
310        + 'static,
311    Extra: 'static,
312{
313    merge_with_policy(classifier, &ReconciliationPolicy::LastWriteWins, p, g)
314}
315
316/// Builds a graph from an iterable of patterns using `LastWriteWins`.
317pub fn from_patterns<Extra, V>(
318    classifier: &GraphClassifier<Extra, V>,
319    patterns: impl IntoIterator<Item = Pattern<V>>,
320) -> PatternGraph<Extra, V>
321where
322    V: GraphValue<Id = Symbol>
323        + HasIdentity<V, Symbol>
324        + Mergeable
325        + Refinable
326        + PartialEq
327        + Clone
328        + 'static,
329    Extra: 'static,
330{
331    from_patterns_with_policy(classifier, &ReconciliationPolicy::LastWriteWins, patterns)
332}
333
334// ============================================================================
335// GraphQuery constructor
336// ============================================================================
337
338/// Wraps a `PatternGraph` in a `GraphQuery<V>`.
339///
340/// All nine `GraphQuery` fields are implemented against the `PatternGraph` maps.
341///
342/// # Complexity
343///
344/// - `query_nodes` / `query_relationships`: O(n) / O(r) to collect values
345/// - `query_incident_rels`: O(r) scan of all relationships
346/// - `query_source` / `query_target`: O(1) element access
347/// - `query_degree`: O(r) scan
348/// - `query_node_by_id` / `query_relationship_by_id`: O(1) average HashMap lookup
349/// - `query_containers`: O(r + w + a) scan of relationships, walks, annotations
350///
351/// # Deferred
352///
353/// TODO: `from_graph_lens` — deferred until `GraphLens` type is available in pattern-rs.
354///
355/// Rc and Arc variants are intentionally separate (no macro): only one is compiled per build,
356/// trait bounds stay clear, and we avoid fragile abstraction over pointer types.
357#[cfg(not(feature = "thread-safe"))]
358pub fn from_pattern_graph<Extra, V>(graph: std::rc::Rc<PatternGraph<Extra, V>>) -> GraphQuery<V>
359where
360    Extra: 'static,
361    V: GraphValue + Clone + 'static,
362    V::Id: Clone + Eq + std::hash::Hash + 'static,
363{
364    use std::rc::Rc;
365
366    let g1 = Rc::clone(&graph);
367    let query_nodes = Rc::new(move || g1.pg_nodes.values().cloned().collect());
368
369    let g2 = Rc::clone(&graph);
370    let query_relationships = Rc::new(move || g2.pg_relationships.values().cloned().collect());
371
372    let g3 = Rc::clone(&graph);
373    let query_incident_rels = Rc::new(move |node: &Pattern<V>| {
374        let node_id = node.value.identify();
375        g3.pg_relationships
376            .values()
377            .filter(|rel| {
378                rel.elements.len() == 2
379                    && (rel.elements[0].value.identify() == node_id
380                        || rel.elements[1].value.identify() == node_id)
381            })
382            .cloned()
383            .collect()
384    });
385
386    // query_source and query_target read directly from relationship elements (O(1))
387    let query_source = Rc::new(|rel: &Pattern<V>| rel.elements.first().cloned());
388    let query_target = Rc::new(|rel: &Pattern<V>| rel.elements.get(1).cloned());
389
390    let g4 = Rc::clone(&graph);
391    let query_degree = Rc::new(move |node: &Pattern<V>| {
392        let node_id = node.value.identify();
393        g4.pg_relationships
394            .values()
395            .filter(|rel| {
396                rel.elements.len() == 2
397                    && (rel.elements[0].value.identify() == node_id
398                        || rel.elements[1].value.identify() == node_id)
399            })
400            .count()
401    });
402
403    let g5 = Rc::clone(&graph);
404    let query_node_by_id = Rc::new(move |id: &V::Id| g5.pg_nodes.get(id).cloned());
405
406    let g6 = Rc::clone(&graph);
407    let query_relationship_by_id = Rc::new(move |id: &V::Id| g6.pg_relationships.get(id).cloned());
408
409    let g7 = Rc::clone(&graph);
410    let query_containers = Rc::new(move |element: &Pattern<V>| {
411        let elem_id = element.value.identify();
412        let mut containers = Vec::new();
413
414        // Relationships: element is an endpoint (source or target)
415        for rel in g7.pg_relationships.values() {
416            if rel.elements.len() == 2
417                && (rel.elements[0].value.identify() == elem_id
418                    || rel.elements[1].value.identify() == elem_id)
419            {
420                containers.push(rel.clone());
421            }
422        }
423
424        // Walks: element is one of the walk's direct sub-elements
425        for walk in g7.pg_walks.values() {
426            if walk.elements.iter().any(|e| e.value.identify() == elem_id) {
427                containers.push(walk.clone());
428            }
429        }
430
431        // Annotations: element is the single inner element
432        for ann in g7.pg_annotations.values() {
433            if ann.elements.len() == 1 && ann.elements[0].value.identify() == elem_id {
434                containers.push(ann.clone());
435            }
436        }
437
438        containers
439    });
440
441    GraphQuery {
442        query_nodes,
443        query_relationships,
444        query_incident_rels,
445        query_source,
446        query_target,
447        query_degree,
448        query_node_by_id,
449        query_relationship_by_id,
450        query_containers,
451    }
452}
453
454#[cfg(feature = "thread-safe")]
455pub fn from_pattern_graph<Extra, V>(graph: std::sync::Arc<PatternGraph<Extra, V>>) -> GraphQuery<V>
456where
457    Extra: Send + Sync + 'static,
458    V: GraphValue + Clone + Send + Sync + 'static,
459    V::Id: Clone + Eq + std::hash::Hash + Send + Sync + 'static,
460{
461    use std::sync::Arc;
462
463    let g1 = Arc::clone(&graph);
464    let query_nodes = Arc::new(move || g1.pg_nodes.values().cloned().collect());
465
466    let g2 = Arc::clone(&graph);
467    let query_relationships = Arc::new(move || g2.pg_relationships.values().cloned().collect());
468
469    let g3 = Arc::clone(&graph);
470    let query_incident_rels = Arc::new(move |node: &Pattern<V>| {
471        let node_id = node.value.identify();
472        g3.pg_relationships
473            .values()
474            .filter(|rel| {
475                rel.elements.len() == 2
476                    && (rel.elements[0].value.identify() == node_id
477                        || rel.elements[1].value.identify() == node_id)
478            })
479            .cloned()
480            .collect()
481    });
482
483    let query_source = Arc::new(|rel: &Pattern<V>| rel.elements.first().cloned());
484    let query_target = Arc::new(|rel: &Pattern<V>| rel.elements.get(1).cloned());
485
486    let g4 = Arc::clone(&graph);
487    let query_degree = Arc::new(move |node: &Pattern<V>| {
488        let node_id = node.value.identify();
489        g4.pg_relationships
490            .values()
491            .filter(|rel| {
492                rel.elements.len() == 2
493                    && (rel.elements[0].value.identify() == node_id
494                        || rel.elements[1].value.identify() == node_id)
495            })
496            .count()
497    });
498
499    let g5 = Arc::clone(&graph);
500    let query_node_by_id = Arc::new(move |id: &V::Id| g5.pg_nodes.get(id).cloned());
501
502    let g6 = Arc::clone(&graph);
503    let query_relationship_by_id = Arc::new(move |id: &V::Id| g6.pg_relationships.get(id).cloned());
504
505    let g7 = Arc::clone(&graph);
506    let query_containers = Arc::new(move |element: &Pattern<V>| {
507        let elem_id = element.value.identify();
508        let mut containers = Vec::new();
509
510        for rel in g7.pg_relationships.values() {
511            if rel.elements.len() == 2
512                && (rel.elements[0].value.identify() == elem_id
513                    || rel.elements[1].value.identify() == elem_id)
514            {
515                containers.push(rel.clone());
516            }
517        }
518
519        for walk in g7.pg_walks.values() {
520            if walk.elements.iter().any(|e| e.value.identify() == elem_id) {
521                containers.push(walk.clone());
522            }
523        }
524
525        for ann in g7.pg_annotations.values() {
526            if ann.elements.len() == 1 && ann.elements[0].value.identify() == elem_id {
527                containers.push(ann.clone());
528            }
529        }
530
531        containers
532    });
533
534    GraphQuery {
535        query_nodes,
536        query_relationships,
537        query_incident_rels,
538        query_source,
539        query_target,
540        query_degree,
541        query_node_by_id,
542        query_relationship_by_id,
543        query_containers,
544    }
545}
546
547/// Builds a graph from an iterable of patterns using the given policy.
548pub fn from_patterns_with_policy<Extra, V>(
549    classifier: &GraphClassifier<Extra, V>,
550    policy: &ReconciliationPolicy<V::MergeStrategy>,
551    patterns: impl IntoIterator<Item = Pattern<V>>,
552) -> PatternGraph<Extra, V>
553where
554    V: GraphValue<Id = Symbol>
555        + HasIdentity<V, Symbol>
556        + Mergeable
557        + Refinable
558        + PartialEq
559        + Clone
560        + 'static,
561    Extra: 'static,
562{
563    patterns.into_iter().fold(PatternGraph::empty(), |g, p| {
564        merge_with_policy(classifier, policy, p, g)
565    })
566}