Skip to main content

pattern_core/graph/
standard.rs

1//! StandardGraph: ergonomic wrapper around PatternGraph<(), Subject>.
2//!
3//! Provides zero-configuration graph construction and querying for the common
4//! case of building graph structures from atomic patterns (nodes, relationships,
5//! walks, annotations).
6
7use std::collections::HashMap;
8
9use crate::graph::graph_classifier::{canonical_classifier, GraphValue};
10use crate::graph::graph_query::GraphQuery;
11use crate::graph::graph_view::GraphView;
12use crate::pattern::Pattern;
13use crate::pattern_graph::PatternGraph;
14use crate::subject::{Subject, Symbol};
15
16/// A concrete, ergonomic graph type wrapping `PatternGraph<(), Subject>`.
17///
18/// StandardGraph eliminates the type parameters and classifier/policy boilerplate
19/// needed when working with `PatternGraph` directly. It provides fluent construction
20/// methods and graph-native queries.
21///
22/// # Examples
23///
24/// ```rust
25/// use pattern_core::graph::StandardGraph;
26/// use pattern_core::subject::Subject;
27///
28/// let mut g = StandardGraph::new();
29/// let alice = Subject::build("alice").label("Person").done();
30/// let bob   = Subject::build("bob").label("Person").done();
31/// g.add_node(alice.clone());
32/// g.add_node(bob.clone());
33/// g.add_relationship(Subject::build("r1").label("KNOWS").done(), &alice, &bob);
34/// assert_eq!(g.node_count(), 2);
35/// assert_eq!(g.relationship_count(), 1);
36/// ```
37pub struct StandardGraph {
38    inner: PatternGraph<(), Subject>,
39}
40
41impl StandardGraph {
42    /// Creates an empty StandardGraph.
43    pub fn new() -> Self {
44        StandardGraph {
45            inner: PatternGraph::empty(),
46        }
47    }
48
49    // ========================================================================
50    // Atomic element addition (Phase 3: US1)
51    // ========================================================================
52
53    /// Adds a node to the graph.
54    ///
55    /// The subject becomes an atomic pattern (no elements). If a node with the
56    /// same identity already exists, it is replaced (last-write-wins).
57    pub fn add_node(&mut self, subject: Subject) -> &mut Self {
58        let id = subject.identity.clone();
59        let pattern = Pattern::point(subject);
60        self.inner.pg_nodes.insert(id, pattern);
61        self
62    }
63
64    /// Adds a relationship to the graph.
65    ///
66    /// Creates a 2-element pattern with the source and target nodes as elements.
67    /// If the source or target nodes don't exist yet, minimal placeholder nodes
68    /// are created automatically.
69    ///
70    /// Pass the actual `Subject` objects for source and target when you have them;
71    /// use `Subject::from_id("id")` as a lightweight reference when you only have
72    /// an identity string.
73    pub fn add_relationship(
74        &mut self,
75        subject: Subject,
76        source: &Subject,
77        target: &Subject,
78    ) -> &mut Self {
79        let source_pattern = self.get_or_create_placeholder_node(&source.identity);
80        let target_pattern = self.get_or_create_placeholder_node(&target.identity);
81
82        let id = subject.identity.clone();
83        let pattern = Pattern::pattern(subject, vec![source_pattern, target_pattern]);
84        self.inner.pg_relationships.insert(id, pattern);
85        self
86    }
87
88    /// Adds a walk to the graph.
89    ///
90    /// Creates an N-element pattern where each element is a relationship pattern.
91    /// If referenced relationships don't exist, minimal placeholders are created.
92    ///
93    /// Pass the actual `Subject` objects for relationships when you have them;
94    /// use `Subject::from_id("id")` as a lightweight reference when you only have
95    /// an identity string.
96    pub fn add_walk(&mut self, subject: Subject, relationships: &[Subject]) -> &mut Self {
97        let rel_patterns: Vec<Pattern<Subject>> = relationships
98            .iter()
99            .map(|rel| self.get_or_create_placeholder_relationship(&rel.identity))
100            .collect();
101
102        let id = subject.identity.clone();
103        let pattern = Pattern::pattern(subject, rel_patterns);
104        self.inner.pg_walks.insert(id, pattern);
105        self
106    }
107
108    /// Adds an annotation to the graph.
109    ///
110    /// Creates a 1-element pattern wrapping the referenced element.
111    /// If the referenced element doesn't exist, a minimal placeholder node is created.
112    ///
113    /// Pass the actual `Subject` when you have it; use `Subject::from_id("id")` as
114    /// a lightweight reference when you only have an identity string.
115    pub fn add_annotation(&mut self, subject: Subject, element: &Subject) -> &mut Self {
116        let element_id = &element.identity;
117        let element_pattern = if let Some(existing) = self.find_element(element_id) {
118            existing
119        } else {
120            // Insert placeholder into pg_nodes for consistency with add_relationship
121            let placeholder = Self::make_placeholder_node(element_id);
122            self.inner
123                .pg_nodes
124                .insert(element_id.clone(), placeholder.clone());
125            placeholder
126        };
127
128        let id = subject.identity.clone();
129        let pattern = Pattern::pattern(subject, vec![element_pattern]);
130        self.inner.pg_annotations.insert(id, pattern);
131        self
132    }
133
134    // ========================================================================
135    // Pattern ingestion (Phase 4: US3)
136    // ========================================================================
137
138    /// Adds a single pattern, classifying it by shape and inserting into the
139    /// appropriate bucket.
140    ///
141    /// When a node with the same identity already exists, labels and properties
142    /// are merged using union semantics rather than overwriting. This preserves
143    /// labels declared in earlier patterns when a later back-reference omits them.
144    pub fn add_pattern(&mut self, pattern: Pattern<Subject>) -> &mut Self {
145        let classifier = canonical_classifier();
146        let policy = pattern_merge_policy();
147        self.inner = crate::pattern_graph::merge_with_policy(
148            &classifier,
149            &policy,
150            pattern,
151            std::mem::replace(&mut self.inner, PatternGraph::empty()),
152        );
153        self
154    }
155
156    /// Adds multiple patterns, classifying each by shape.
157    ///
158    /// When a node with the same identity already exists, labels and properties
159    /// are merged using union semantics rather than overwriting. This preserves
160    /// labels declared in earlier patterns when a later back-reference omits them.
161    pub fn add_patterns(
162        &mut self,
163        patterns: impl IntoIterator<Item = Pattern<Subject>>,
164    ) -> &mut Self {
165        let classifier = canonical_classifier();
166        let policy = pattern_merge_policy();
167        let mut graph = std::mem::replace(&mut self.inner, PatternGraph::empty());
168        for pattern in patterns {
169            graph = crate::pattern_graph::merge_with_policy(&classifier, &policy, pattern, graph);
170        }
171        self.inner = graph;
172        self
173    }
174
175    /// Creates a StandardGraph from an iterator of patterns.
176    ///
177    /// When the same identity appears multiple times (back-references), labels and
178    /// properties are merged using union semantics so that labels declared in an
179    /// earlier pattern are preserved even when a later occurrence omits them.
180    pub fn from_patterns(patterns: impl IntoIterator<Item = Pattern<Subject>>) -> Self {
181        let classifier = canonical_classifier();
182        let policy = pattern_merge_policy();
183        let inner = crate::pattern_graph::from_patterns_with_policy(&classifier, &policy, patterns);
184        StandardGraph { inner }
185    }
186
187    /// Creates a StandardGraph by wrapping an existing PatternGraph directly.
188    pub fn from_pattern_graph(graph: PatternGraph<(), Subject>) -> Self {
189        StandardGraph { inner: graph }
190    }
191
192    // ========================================================================
193    // Element access (Phase 3: US1)
194    // ========================================================================
195
196    /// Returns the node with the given identity.
197    pub fn node(&self, id: &Symbol) -> Option<&Pattern<Subject>> {
198        self.inner.pg_nodes.get(id)
199    }
200
201    /// Returns the relationship with the given identity.
202    pub fn relationship(&self, id: &Symbol) -> Option<&Pattern<Subject>> {
203        self.inner.pg_relationships.get(id)
204    }
205
206    /// Returns the walk with the given identity.
207    pub fn walk(&self, id: &Symbol) -> Option<&Pattern<Subject>> {
208        self.inner.pg_walks.get(id)
209    }
210
211    /// Returns the annotation with the given identity.
212    pub fn annotation(&self, id: &Symbol) -> Option<&Pattern<Subject>> {
213        self.inner.pg_annotations.get(id)
214    }
215
216    // ========================================================================
217    // Counts and health (Phase 3: US1)
218    // ========================================================================
219
220    /// Returns the number of nodes.
221    pub fn node_count(&self) -> usize {
222        self.inner.pg_nodes.len()
223    }
224
225    /// Returns the number of relationships.
226    pub fn relationship_count(&self) -> usize {
227        self.inner.pg_relationships.len()
228    }
229
230    /// Returns the number of walks.
231    pub fn walk_count(&self) -> usize {
232        self.inner.pg_walks.len()
233    }
234
235    /// Returns the number of annotations.
236    pub fn annotation_count(&self) -> usize {
237        self.inner.pg_annotations.len()
238    }
239
240    /// Returns true if the graph has no elements in any bucket.
241    pub fn is_empty(&self) -> bool {
242        self.inner.pg_nodes.is_empty()
243            && self.inner.pg_relationships.is_empty()
244            && self.inner.pg_walks.is_empty()
245            && self.inner.pg_annotations.is_empty()
246            && self.inner.pg_other.is_empty()
247            && self.inner.pg_conflicts.is_empty()
248    }
249
250    /// Returns true if any reconciliation conflicts have been recorded.
251    pub fn has_conflicts(&self) -> bool {
252        !self.inner.pg_conflicts.is_empty()
253    }
254
255    /// Returns the conflict map (identity → conflicting patterns).
256    pub fn conflicts(&self) -> &HashMap<Symbol, Vec<Pattern<Subject>>> {
257        &self.inner.pg_conflicts
258    }
259
260    /// Returns the "other" bucket (unclassifiable patterns).
261    pub fn other(&self) -> &HashMap<Symbol, ((), Pattern<Subject>)> {
262        &self.inner.pg_other
263    }
264
265    // ========================================================================
266    // Iterators (Phase 5: US4)
267    // ========================================================================
268
269    /// Iterates over all nodes.
270    pub fn nodes(&self) -> impl Iterator<Item = (&Symbol, &Pattern<Subject>)> {
271        self.inner.pg_nodes.iter()
272    }
273
274    /// Iterates over all relationships.
275    pub fn relationships(&self) -> impl Iterator<Item = (&Symbol, &Pattern<Subject>)> {
276        self.inner.pg_relationships.iter()
277    }
278
279    /// Iterates over all walks.
280    pub fn walks(&self) -> impl Iterator<Item = (&Symbol, &Pattern<Subject>)> {
281        self.inner.pg_walks.iter()
282    }
283
284    /// Iterates over all annotations.
285    pub fn annotations(&self) -> impl Iterator<Item = (&Symbol, &Pattern<Subject>)> {
286        self.inner.pg_annotations.iter()
287    }
288
289    // ========================================================================
290    // Graph-native queries (Phase 5: US4)
291    // ========================================================================
292
293    /// Returns the source node of a relationship.
294    pub fn source(&self, rel_id: &Symbol) -> Option<&Pattern<Subject>> {
295        self.inner
296            .pg_relationships
297            .get(rel_id)
298            .and_then(|rel| rel.elements.first())
299    }
300
301    /// Returns the target node of a relationship.
302    pub fn target(&self, rel_id: &Symbol) -> Option<&Pattern<Subject>> {
303        self.inner
304            .pg_relationships
305            .get(rel_id)
306            .and_then(|rel| rel.elements.get(1))
307    }
308
309    /// Returns all neighbor nodes of the given node (both directions).
310    pub fn neighbors(&self, node_id: &Symbol) -> Vec<&Pattern<Subject>> {
311        let mut result = Vec::new();
312        for rel in self.inner.pg_relationships.values() {
313            if rel.elements.len() == 2 {
314                let src_id = rel.elements[0].value.identify();
315                let tgt_id = rel.elements[1].value.identify();
316                if src_id == node_id {
317                    result.push(&rel.elements[1]);
318                } else if tgt_id == node_id {
319                    result.push(&rel.elements[0]);
320                }
321            }
322        }
323        result
324    }
325
326    /// Returns the degree of a node (number of incident relationships, both directions).
327    pub fn degree(&self, node_id: &Symbol) -> usize {
328        self.inner
329            .pg_relationships
330            .values()
331            .filter(|rel| {
332                rel.elements.len() == 2
333                    && (rel.elements[0].value.identify() == node_id
334                        || rel.elements[1].value.identify() == node_id)
335            })
336            .count()
337    }
338
339    // ========================================================================
340    // Escape hatches (Phase 6: US5)
341    // ========================================================================
342
343    /// Returns a reference to the inner PatternGraph.
344    pub fn as_pattern_graph(&self) -> &PatternGraph<(), Subject> {
345        &self.inner
346    }
347
348    /// Consumes the StandardGraph and returns the inner PatternGraph.
349    pub fn into_pattern_graph(self) -> PatternGraph<(), Subject> {
350        self.inner
351    }
352
353    /// Creates a GraphQuery from this graph.
354    #[cfg(not(feature = "thread-safe"))]
355    pub fn as_query(&self) -> GraphQuery<Subject> {
356        use std::rc::Rc;
357        let graph = Rc::new(PatternGraph {
358            pg_nodes: self.inner.pg_nodes.clone(),
359            pg_relationships: self.inner.pg_relationships.clone(),
360            pg_walks: self.inner.pg_walks.clone(),
361            pg_annotations: self.inner.pg_annotations.clone(),
362            pg_other: self.inner.pg_other.clone(),
363            pg_conflicts: self.inner.pg_conflicts.clone(),
364        });
365        crate::pattern_graph::from_pattern_graph(graph)
366    }
367
368    /// Creates a GraphQuery from this graph.
369    #[cfg(feature = "thread-safe")]
370    pub fn as_query(&self) -> GraphQuery<Subject> {
371        use std::sync::Arc;
372        let graph = Arc::new(PatternGraph {
373            pg_nodes: self.inner.pg_nodes.clone(),
374            pg_relationships: self.inner.pg_relationships.clone(),
375            pg_walks: self.inner.pg_walks.clone(),
376            pg_annotations: self.inner.pg_annotations.clone(),
377            pg_other: self.inner.pg_other.clone(),
378            pg_conflicts: self.inner.pg_conflicts.clone(),
379        });
380        crate::pattern_graph::from_pattern_graph(graph)
381    }
382
383    /// Creates a GraphView snapshot from this graph.
384    pub fn as_snapshot(&self) -> GraphView<(), Subject> {
385        let classifier = canonical_classifier();
386        crate::graph::graph_view::from_pattern_graph(&classifier, &self.inner)
387    }
388
389    // ========================================================================
390    // Private helpers
391    // ========================================================================
392
393    fn make_placeholder_node(id: &Symbol) -> Pattern<Subject> {
394        Pattern::point(Subject {
395            identity: id.clone(),
396            labels: std::collections::HashSet::new(),
397            properties: HashMap::new(),
398        })
399    }
400
401    fn get_or_create_placeholder_node(&mut self, id: &Symbol) -> Pattern<Subject> {
402        if let Some(node) = self.inner.pg_nodes.get(id) {
403            node.clone()
404        } else {
405            let placeholder = Self::make_placeholder_node(id);
406            self.inner.pg_nodes.insert(id.clone(), placeholder.clone());
407            placeholder
408        }
409    }
410
411    fn get_or_create_placeholder_relationship(&self, id: &Symbol) -> Pattern<Subject> {
412        if let Some(rel) = self.inner.pg_relationships.get(id) {
413            rel.clone()
414        } else {
415            // Create a minimal placeholder relationship (just a point with the id)
416            Pattern::point(Subject {
417                identity: id.clone(),
418                labels: std::collections::HashSet::new(),
419                properties: HashMap::new(),
420            })
421        }
422    }
423
424    fn find_element(&self, id: &Symbol) -> Option<Pattern<Subject>> {
425        self.inner
426            .pg_nodes
427            .get(id)
428            .or_else(|| self.inner.pg_relationships.get(id))
429            .or_else(|| self.inner.pg_walks.get(id))
430            .or_else(|| self.inner.pg_annotations.get(id))
431            .cloned()
432    }
433}
434
435impl Default for StandardGraph {
436    fn default() -> Self {
437        Self::new()
438    }
439}
440
441/// Returns the reconciliation policy used by `add_pattern`, `add_patterns`, and
442/// `from_patterns`.
443///
444/// Union label semantics ensure that back-references (node occurrences without
445/// labels that refer to an earlier labelled declaration) do not silently discard
446/// the labels established by the first declaration.
447fn pattern_merge_policy(
448) -> crate::reconcile::ReconciliationPolicy<crate::reconcile::SubjectMergeStrategy> {
449    crate::reconcile::ReconciliationPolicy::Merge(
450        crate::reconcile::ElementMergeStrategy::UnionElements,
451        crate::reconcile::default_subject_merge_strategy(),
452    )
453}