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    pub fn add_pattern(&mut self, pattern: Pattern<Subject>) -> &mut Self {
141        let classifier = canonical_classifier();
142        self.inner = crate::pattern_graph::merge(
143            &classifier,
144            pattern,
145            std::mem::replace(&mut self.inner, PatternGraph::empty()),
146        );
147        self
148    }
149
150    /// Adds multiple patterns, classifying each by shape.
151    pub fn add_patterns(
152        &mut self,
153        patterns: impl IntoIterator<Item = Pattern<Subject>>,
154    ) -> &mut Self {
155        let classifier = canonical_classifier();
156        let mut graph = std::mem::replace(&mut self.inner, PatternGraph::empty());
157        for pattern in patterns {
158            graph = crate::pattern_graph::merge(&classifier, pattern, graph);
159        }
160        self.inner = graph;
161        self
162    }
163
164    /// Creates a StandardGraph from an iterator of patterns.
165    pub fn from_patterns(patterns: impl IntoIterator<Item = Pattern<Subject>>) -> Self {
166        let classifier = canonical_classifier();
167        let inner = crate::pattern_graph::from_patterns(&classifier, patterns);
168        StandardGraph { inner }
169    }
170
171    /// Creates a StandardGraph by wrapping an existing PatternGraph directly.
172    pub fn from_pattern_graph(graph: PatternGraph<(), Subject>) -> Self {
173        StandardGraph { inner: graph }
174    }
175
176    // ========================================================================
177    // Element access (Phase 3: US1)
178    // ========================================================================
179
180    /// Returns the node with the given identity.
181    pub fn node(&self, id: &Symbol) -> Option<&Pattern<Subject>> {
182        self.inner.pg_nodes.get(id)
183    }
184
185    /// Returns the relationship with the given identity.
186    pub fn relationship(&self, id: &Symbol) -> Option<&Pattern<Subject>> {
187        self.inner.pg_relationships.get(id)
188    }
189
190    /// Returns the walk with the given identity.
191    pub fn walk(&self, id: &Symbol) -> Option<&Pattern<Subject>> {
192        self.inner.pg_walks.get(id)
193    }
194
195    /// Returns the annotation with the given identity.
196    pub fn annotation(&self, id: &Symbol) -> Option<&Pattern<Subject>> {
197        self.inner.pg_annotations.get(id)
198    }
199
200    // ========================================================================
201    // Counts and health (Phase 3: US1)
202    // ========================================================================
203
204    /// Returns the number of nodes.
205    pub fn node_count(&self) -> usize {
206        self.inner.pg_nodes.len()
207    }
208
209    /// Returns the number of relationships.
210    pub fn relationship_count(&self) -> usize {
211        self.inner.pg_relationships.len()
212    }
213
214    /// Returns the number of walks.
215    pub fn walk_count(&self) -> usize {
216        self.inner.pg_walks.len()
217    }
218
219    /// Returns the number of annotations.
220    pub fn annotation_count(&self) -> usize {
221        self.inner.pg_annotations.len()
222    }
223
224    /// Returns true if the graph has no elements in any bucket.
225    pub fn is_empty(&self) -> bool {
226        self.inner.pg_nodes.is_empty()
227            && self.inner.pg_relationships.is_empty()
228            && self.inner.pg_walks.is_empty()
229            && self.inner.pg_annotations.is_empty()
230            && self.inner.pg_other.is_empty()
231            && self.inner.pg_conflicts.is_empty()
232    }
233
234    /// Returns true if any reconciliation conflicts have been recorded.
235    pub fn has_conflicts(&self) -> bool {
236        !self.inner.pg_conflicts.is_empty()
237    }
238
239    /// Returns the conflict map (identity → conflicting patterns).
240    pub fn conflicts(&self) -> &HashMap<Symbol, Vec<Pattern<Subject>>> {
241        &self.inner.pg_conflicts
242    }
243
244    /// Returns the "other" bucket (unclassifiable patterns).
245    pub fn other(&self) -> &HashMap<Symbol, ((), Pattern<Subject>)> {
246        &self.inner.pg_other
247    }
248
249    // ========================================================================
250    // Iterators (Phase 5: US4)
251    // ========================================================================
252
253    /// Iterates over all nodes.
254    pub fn nodes(&self) -> impl Iterator<Item = (&Symbol, &Pattern<Subject>)> {
255        self.inner.pg_nodes.iter()
256    }
257
258    /// Iterates over all relationships.
259    pub fn relationships(&self) -> impl Iterator<Item = (&Symbol, &Pattern<Subject>)> {
260        self.inner.pg_relationships.iter()
261    }
262
263    /// Iterates over all walks.
264    pub fn walks(&self) -> impl Iterator<Item = (&Symbol, &Pattern<Subject>)> {
265        self.inner.pg_walks.iter()
266    }
267
268    /// Iterates over all annotations.
269    pub fn annotations(&self) -> impl Iterator<Item = (&Symbol, &Pattern<Subject>)> {
270        self.inner.pg_annotations.iter()
271    }
272
273    // ========================================================================
274    // Graph-native queries (Phase 5: US4)
275    // ========================================================================
276
277    /// Returns the source node of a relationship.
278    pub fn source(&self, rel_id: &Symbol) -> Option<&Pattern<Subject>> {
279        self.inner
280            .pg_relationships
281            .get(rel_id)
282            .and_then(|rel| rel.elements.first())
283    }
284
285    /// Returns the target node of a relationship.
286    pub fn target(&self, rel_id: &Symbol) -> Option<&Pattern<Subject>> {
287        self.inner
288            .pg_relationships
289            .get(rel_id)
290            .and_then(|rel| rel.elements.get(1))
291    }
292
293    /// Returns all neighbor nodes of the given node (both directions).
294    pub fn neighbors(&self, node_id: &Symbol) -> Vec<&Pattern<Subject>> {
295        let mut result = Vec::new();
296        for rel in self.inner.pg_relationships.values() {
297            if rel.elements.len() == 2 {
298                let src_id = rel.elements[0].value.identify();
299                let tgt_id = rel.elements[1].value.identify();
300                if src_id == node_id {
301                    result.push(&rel.elements[1]);
302                } else if tgt_id == node_id {
303                    result.push(&rel.elements[0]);
304                }
305            }
306        }
307        result
308    }
309
310    /// Returns the degree of a node (number of incident relationships, both directions).
311    pub fn degree(&self, node_id: &Symbol) -> usize {
312        self.inner
313            .pg_relationships
314            .values()
315            .filter(|rel| {
316                rel.elements.len() == 2
317                    && (rel.elements[0].value.identify() == node_id
318                        || rel.elements[1].value.identify() == node_id)
319            })
320            .count()
321    }
322
323    // ========================================================================
324    // Escape hatches (Phase 6: US5)
325    // ========================================================================
326
327    /// Returns a reference to the inner PatternGraph.
328    pub fn as_pattern_graph(&self) -> &PatternGraph<(), Subject> {
329        &self.inner
330    }
331
332    /// Consumes the StandardGraph and returns the inner PatternGraph.
333    pub fn into_pattern_graph(self) -> PatternGraph<(), Subject> {
334        self.inner
335    }
336
337    /// Creates a GraphQuery from this graph.
338    #[cfg(not(feature = "thread-safe"))]
339    pub fn as_query(&self) -> GraphQuery<Subject> {
340        use std::rc::Rc;
341        let graph = Rc::new(PatternGraph {
342            pg_nodes: self.inner.pg_nodes.clone(),
343            pg_relationships: self.inner.pg_relationships.clone(),
344            pg_walks: self.inner.pg_walks.clone(),
345            pg_annotations: self.inner.pg_annotations.clone(),
346            pg_other: self.inner.pg_other.clone(),
347            pg_conflicts: self.inner.pg_conflicts.clone(),
348        });
349        crate::pattern_graph::from_pattern_graph(graph)
350    }
351
352    /// Creates a GraphQuery from this graph.
353    #[cfg(feature = "thread-safe")]
354    pub fn as_query(&self) -> GraphQuery<Subject> {
355        use std::sync::Arc;
356        let graph = Arc::new(PatternGraph {
357            pg_nodes: self.inner.pg_nodes.clone(),
358            pg_relationships: self.inner.pg_relationships.clone(),
359            pg_walks: self.inner.pg_walks.clone(),
360            pg_annotations: self.inner.pg_annotations.clone(),
361            pg_other: self.inner.pg_other.clone(),
362            pg_conflicts: self.inner.pg_conflicts.clone(),
363        });
364        crate::pattern_graph::from_pattern_graph(graph)
365    }
366
367    /// Creates a GraphView snapshot from this graph.
368    pub fn as_snapshot(&self) -> GraphView<(), Subject> {
369        let classifier = canonical_classifier();
370        crate::graph::graph_view::from_pattern_graph(&classifier, &self.inner)
371    }
372
373    // ========================================================================
374    // Private helpers
375    // ========================================================================
376
377    fn make_placeholder_node(id: &Symbol) -> Pattern<Subject> {
378        Pattern::point(Subject {
379            identity: id.clone(),
380            labels: std::collections::HashSet::new(),
381            properties: HashMap::new(),
382        })
383    }
384
385    fn get_or_create_placeholder_node(&mut self, id: &Symbol) -> Pattern<Subject> {
386        if let Some(node) = self.inner.pg_nodes.get(id) {
387            node.clone()
388        } else {
389            let placeholder = Self::make_placeholder_node(id);
390            self.inner.pg_nodes.insert(id.clone(), placeholder.clone());
391            placeholder
392        }
393    }
394
395    fn get_or_create_placeholder_relationship(&self, id: &Symbol) -> Pattern<Subject> {
396        if let Some(rel) = self.inner.pg_relationships.get(id) {
397            rel.clone()
398        } else {
399            // Create a minimal placeholder relationship (just a point with the id)
400            Pattern::point(Subject {
401                identity: id.clone(),
402                labels: std::collections::HashSet::new(),
403                properties: HashMap::new(),
404            })
405        }
406    }
407
408    fn find_element(&self, id: &Symbol) -> Option<Pattern<Subject>> {
409        self.inner
410            .pg_nodes
411            .get(id)
412            .or_else(|| self.inner.pg_relationships.get(id))
413            .or_else(|| self.inner.pg_walks.get(id))
414            .or_else(|| self.inner.pg_annotations.get(id))
415            .cloned()
416    }
417}
418
419impl Default for StandardGraph {
420    fn default() -> Self {
421        Self::new()
422    }
423}