Skip to main content

sqry_core/graph/unified/concurrent/
graph.rs

1//! `CodeGraph` and `ConcurrentCodeGraph` implementations.
2//!
3//! This module provides the core graph types with thread-safe access:
4//!
5//! - [`CodeGraph`]: Arc-wrapped internals for O(1) `CoW` snapshots
6//! - [`ConcurrentCodeGraph`]: `RwLock` wrapper with epoch versioning
7//! - [`GraphSnapshot`]: Immutable snapshot for long-running queries
8
9use std::collections::HashMap;
10use std::fmt;
11use std::sync::Arc;
12use std::sync::atomic::{AtomicU64, Ordering};
13
14use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
15
16use crate::confidence::ConfidenceMetadata;
17use crate::graph::unified::edge::bidirectional::BidirectionalEdgeStore;
18use crate::graph::unified::storage::arena::NodeArena;
19use crate::graph::unified::storage::indices::AuxiliaryIndices;
20use crate::graph::unified::storage::interner::StringInterner;
21use crate::graph::unified::storage::metadata::NodeMetadataStore;
22use crate::graph::unified::storage::registry::FileRegistry;
23
24/// Core graph with Arc-wrapped internals for O(1) `CoW` snapshots.
25///
26/// `CodeGraph` uses `Arc` for all internal components, enabling:
27/// - O(1) snapshot creation via Arc cloning
28/// - Copy-on-write semantics via `Arc::make_mut`
29/// - Memory-efficient sharing between snapshots
30///
31/// # Design
32///
33/// The Arc wrapping enables the MVCC pattern:
34/// - Readers see a consistent snapshot at the time they acquired access
35/// - Writers use `Arc::make_mut` to get exclusive copies only when mutating
36/// - Multiple snapshots can coexist without copying data
37///
38/// # Performance
39///
40/// - Snapshot creation: O(5) Arc clones ≈ <1μs
41/// - Read access: Direct Arc dereference, no locking
42/// - Write access: `Arc::make_mut` clones only if refcount > 1
43#[derive(Clone)]
44pub struct CodeGraph {
45    /// Node storage with generational indices.
46    nodes: Arc<NodeArena>,
47    /// Bidirectional edge storage (forward + reverse).
48    edges: Arc<BidirectionalEdgeStore>,
49    /// String interner for symbol names.
50    strings: Arc<StringInterner>,
51    /// File registry for path deduplication.
52    files: Arc<FileRegistry>,
53    /// Auxiliary indices for fast lookup.
54    indices: Arc<AuxiliaryIndices>,
55    /// Sparse macro boundary metadata (keyed by full NodeId).
56    macro_metadata: Arc<NodeMetadataStore>,
57    /// Epoch for version tracking.
58    epoch: u64,
59    /// Per-language confidence metadata collected during build.
60    /// Maps language name (e.g., "rust") to aggregated confidence.
61    confidence: HashMap<String, ConfidenceMetadata>,
62}
63
64impl CodeGraph {
65    /// Creates a new empty `CodeGraph`.
66    ///
67    /// # Example
68    ///
69    /// ```rust
70    /// use sqry_core::graph::unified::concurrent::CodeGraph;
71    ///
72    /// let graph = CodeGraph::new();
73    /// assert_eq!(graph.epoch(), 0);
74    /// ```
75    #[must_use]
76    pub fn new() -> Self {
77        Self {
78            nodes: Arc::new(NodeArena::new()),
79            edges: Arc::new(BidirectionalEdgeStore::new()),
80            strings: Arc::new(StringInterner::new()),
81            files: Arc::new(FileRegistry::new()),
82            indices: Arc::new(AuxiliaryIndices::new()),
83            macro_metadata: Arc::new(NodeMetadataStore::new()),
84            epoch: 0,
85            confidence: HashMap::new(),
86        }
87    }
88
89    /// Creates a `CodeGraph` from existing components.
90    ///
91    /// This is useful when building a graph from external data or
92    /// reconstructing from serialized state.
93    #[must_use]
94    pub fn from_components(
95        nodes: NodeArena,
96        edges: BidirectionalEdgeStore,
97        strings: StringInterner,
98        files: FileRegistry,
99        indices: AuxiliaryIndices,
100        macro_metadata: NodeMetadataStore,
101    ) -> Self {
102        Self {
103            nodes: Arc::new(nodes),
104            edges: Arc::new(edges),
105            strings: Arc::new(strings),
106            files: Arc::new(files),
107            indices: Arc::new(indices),
108            macro_metadata: Arc::new(macro_metadata),
109            epoch: 0,
110            confidence: HashMap::new(),
111        }
112    }
113
114    /// Creates a cheap snapshot of the graph.
115    ///
116    /// This operation is O(5) Arc clones, which completes in <1μs.
117    /// The snapshot is isolated from future mutations to the original graph.
118    ///
119    /// # Example
120    ///
121    /// ```rust
122    /// use sqry_core::graph::unified::concurrent::CodeGraph;
123    ///
124    /// let graph = CodeGraph::new();
125    /// let snapshot = graph.snapshot();
126    /// // snapshot is independent of future mutations to graph
127    /// ```
128    #[must_use]
129    pub fn snapshot(&self) -> GraphSnapshot {
130        GraphSnapshot {
131            nodes: Arc::clone(&self.nodes),
132            edges: Arc::clone(&self.edges),
133            strings: Arc::clone(&self.strings),
134            files: Arc::clone(&self.files),
135            indices: Arc::clone(&self.indices),
136            macro_metadata: Arc::clone(&self.macro_metadata),
137            epoch: self.epoch,
138        }
139    }
140
141    /// Returns a reference to the node arena.
142    #[inline]
143    #[must_use]
144    pub fn nodes(&self) -> &NodeArena {
145        &self.nodes
146    }
147
148    /// Returns a reference to the bidirectional edge store.
149    #[inline]
150    #[must_use]
151    pub fn edges(&self) -> &BidirectionalEdgeStore {
152        &self.edges
153    }
154
155    /// Returns a reference to the string interner.
156    #[inline]
157    #[must_use]
158    pub fn strings(&self) -> &StringInterner {
159        &self.strings
160    }
161
162    /// Returns a reference to the file registry.
163    #[inline]
164    #[must_use]
165    pub fn files(&self) -> &FileRegistry {
166        &self.files
167    }
168
169    /// Returns a reference to the auxiliary indices.
170    #[inline]
171    #[must_use]
172    pub fn indices(&self) -> &AuxiliaryIndices {
173        &self.indices
174    }
175
176    /// Returns a reference to the macro boundary metadata store.
177    #[inline]
178    #[must_use]
179    pub fn macro_metadata(&self) -> &NodeMetadataStore {
180        &self.macro_metadata
181    }
182
183    /// Returns the current epoch.
184    #[inline]
185    #[must_use]
186    pub fn epoch(&self) -> u64 {
187        self.epoch
188    }
189
190    /// Returns a mutable reference to the node arena.
191    ///
192    /// Uses `Arc::make_mut` for copy-on-write semantics: if other
193    /// references exist (e.g., snapshots), the data is cloned.
194    #[inline]
195    pub fn nodes_mut(&mut self) -> &mut NodeArena {
196        Arc::make_mut(&mut self.nodes)
197    }
198
199    /// Returns a mutable reference to the bidirectional edge store.
200    ///
201    /// Uses `Arc::make_mut` for copy-on-write semantics.
202    #[inline]
203    pub fn edges_mut(&mut self) -> &mut BidirectionalEdgeStore {
204        Arc::make_mut(&mut self.edges)
205    }
206
207    /// Returns a mutable reference to the string interner.
208    ///
209    /// Uses `Arc::make_mut` for copy-on-write semantics.
210    #[inline]
211    pub fn strings_mut(&mut self) -> &mut StringInterner {
212        Arc::make_mut(&mut self.strings)
213    }
214
215    /// Returns a mutable reference to the file registry.
216    ///
217    /// Uses `Arc::make_mut` for copy-on-write semantics.
218    #[inline]
219    pub fn files_mut(&mut self) -> &mut FileRegistry {
220        Arc::make_mut(&mut self.files)
221    }
222
223    /// Returns a mutable reference to the auxiliary indices.
224    ///
225    /// Uses `Arc::make_mut` for copy-on-write semantics.
226    #[inline]
227    pub fn indices_mut(&mut self) -> &mut AuxiliaryIndices {
228        Arc::make_mut(&mut self.indices)
229    }
230
231    /// Returns a mutable reference to the macro boundary metadata store.
232    ///
233    /// Uses `Arc::make_mut` for copy-on-write semantics.
234    #[inline]
235    pub fn macro_metadata_mut(&mut self) -> &mut NodeMetadataStore {
236        Arc::make_mut(&mut self.macro_metadata)
237    }
238
239    /// Returns mutable references to both the node arena and the string interner.
240    ///
241    /// This avoids the borrow-conflict that arises when calling `nodes_mut()` and
242    /// `strings_mut()` separately on `&mut self`.
243    #[inline]
244    pub fn nodes_and_strings_mut(&mut self) -> (&mut NodeArena, &mut StringInterner) {
245        (
246            Arc::make_mut(&mut self.nodes),
247            Arc::make_mut(&mut self.strings),
248        )
249    }
250
251    /// Rebuilds auxiliary indices from the current node arena.
252    ///
253    /// This avoids the borrow conflict that arises when calling `nodes()` and
254    /// `indices_mut()` separately on `&mut self`. Uses disjoint field borrowing
255    /// to access `nodes` (shared) and `indices` (mutable) simultaneously.
256    /// Internally calls `AuxiliaryIndices::build_from_arena` which clears
257    /// existing indices and rebuilds in a single pass without per-element
258    /// duplicate checking.
259    pub fn rebuild_indices(&mut self) {
260        let nodes = &self.nodes;
261        Arc::make_mut(&mut self.indices).build_from_arena(nodes);
262    }
263
264    /// Increments the epoch counter and returns the new value.
265    ///
266    /// Called automatically by `ConcurrentCodeGraph::write()`.
267    #[inline]
268    pub fn bump_epoch(&mut self) -> u64 {
269        self.epoch = self.epoch.wrapping_add(1);
270        self.epoch
271    }
272
273    /// Sets the epoch to a specific value.
274    ///
275    /// This is primarily for testing or reconstruction from serialized state.
276    #[inline]
277    pub fn set_epoch(&mut self, epoch: u64) {
278        self.epoch = epoch;
279    }
280
281    /// Returns the number of nodes in the graph.
282    ///
283    /// This is a convenience method that delegates to `nodes().len()`.
284    ///
285    /// # Example
286    ///
287    /// ```rust
288    /// use sqry_core::graph::unified::concurrent::CodeGraph;
289    ///
290    /// let graph = CodeGraph::new();
291    /// assert_eq!(graph.node_count(), 0);
292    /// ```
293    #[inline]
294    #[must_use]
295    pub fn node_count(&self) -> usize {
296        self.nodes.len()
297    }
298
299    /// Returns the number of edges in the graph (forward direction).
300    ///
301    /// This counts edges in the forward store, including both CSR and delta edges.
302    ///
303    /// # Example
304    ///
305    /// ```rust
306    /// use sqry_core::graph::unified::concurrent::CodeGraph;
307    ///
308    /// let graph = CodeGraph::new();
309    /// assert_eq!(graph.edge_count(), 0);
310    /// ```
311    #[inline]
312    #[must_use]
313    pub fn edge_count(&self) -> usize {
314        let stats = self.edges.stats();
315        stats.forward.csr_edge_count + stats.forward.delta_edge_count
316    }
317
318    /// Returns true if the graph contains no nodes.
319    ///
320    /// This is a convenience method that delegates to `nodes().is_empty()`.
321    ///
322    /// # Example
323    ///
324    /// ```rust
325    /// use sqry_core::graph::unified::concurrent::CodeGraph;
326    ///
327    /// let graph = CodeGraph::new();
328    /// assert!(graph.is_empty());
329    /// ```
330    #[inline]
331    #[must_use]
332    pub fn is_empty(&self) -> bool {
333        self.nodes.is_empty()
334    }
335
336    /// Returns an iterator over all indexed file paths.
337    ///
338    /// This is useful for enumerating all files that have been processed
339    /// and added to the graph.
340    ///
341    /// # Example
342    ///
343    /// ```rust
344    /// use sqry_core::graph::unified::concurrent::CodeGraph;
345    ///
346    /// let graph = CodeGraph::new();
347    /// for (file_id, path) in graph.indexed_files() {
348    ///     println!("File {}: {}", file_id.index(), path.display());
349    /// }
350    /// ```
351    #[inline]
352    pub fn indexed_files(
353        &self,
354    ) -> impl Iterator<Item = (crate::graph::unified::file::FileId, &std::path::Path)> {
355        self.files
356            .iter()
357            .map(|(id, arc_path)| (id, arc_path.as_ref()))
358    }
359
360    /// Returns the per-language confidence metadata.
361    ///
362    /// This contains analysis confidence information collected during graph build,
363    /// primarily used by language plugins (e.g., Rust) to track analysis quality.
364    #[inline]
365    #[must_use]
366    pub fn confidence(&self) -> &HashMap<String, ConfidenceMetadata> {
367        &self.confidence
368    }
369
370    /// Merges confidence metadata for a language.
371    ///
372    /// If confidence already exists for the language, this merges the new
373    /// metadata (taking the lower confidence level and combining limitations).
374    /// Otherwise, it inserts the new confidence.
375    pub fn merge_confidence(&mut self, language: &str, metadata: ConfidenceMetadata) {
376        use crate::confidence::ConfidenceLevel;
377
378        self.confidence
379            .entry(language.to_string())
380            .and_modify(|existing| {
381                // Take the lower confidence level (more conservative)
382                let new_level = match (&existing.level, &metadata.level) {
383                    (ConfidenceLevel::Verified, other) | (other, ConfidenceLevel::Verified) => {
384                        *other
385                    }
386                    (ConfidenceLevel::Partial, ConfidenceLevel::AstOnly)
387                    | (ConfidenceLevel::AstOnly, ConfidenceLevel::Partial) => {
388                        ConfidenceLevel::AstOnly
389                    }
390                    (level, _) => *level,
391                };
392                existing.level = new_level;
393
394                // Merge limitations (deduplicated)
395                for limitation in &metadata.limitations {
396                    if !existing.limitations.contains(limitation) {
397                        existing.limitations.push(limitation.clone());
398                    }
399                }
400
401                // Merge unavailable features (deduplicated)
402                for feature in &metadata.unavailable_features {
403                    if !existing.unavailable_features.contains(feature) {
404                        existing.unavailable_features.push(feature.clone());
405                    }
406                }
407            })
408            .or_insert(metadata);
409    }
410
411    /// Sets the confidence metadata map directly.
412    ///
413    /// This is primarily used when loading a graph from serialized state.
414    pub fn set_confidence(&mut self, confidence: HashMap<String, ConfidenceMetadata>) {
415        self.confidence = confidence;
416    }
417}
418
419impl Default for CodeGraph {
420    fn default() -> Self {
421        Self::new()
422    }
423}
424
425impl fmt::Debug for CodeGraph {
426    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
427        f.debug_struct("CodeGraph")
428            .field("nodes", &self.nodes.len())
429            .field("epoch", &self.epoch)
430            .finish_non_exhaustive()
431    }
432}
433
434/// Thread-safe wrapper for `CodeGraph` with epoch versioning.
435///
436/// `ConcurrentCodeGraph` provides MVCC-style concurrency:
437/// - Multiple readers can access the graph simultaneously
438/// - Only one writer can hold the lock at a time
439/// - Each write operation increments the epoch for cursor invalidation
440///
441/// # Design
442///
443/// The wrapper uses `parking_lot::RwLock` for efficient locking:
444/// - Fair scheduling prevents writer starvation
445/// - No poisoning (unlike `std::sync::RwLock`)
446/// - Faster lock/unlock operations
447///
448/// # Usage
449///
450/// ```rust
451/// use sqry_core::graph::unified::concurrent::ConcurrentCodeGraph;
452///
453/// let graph = ConcurrentCodeGraph::new();
454///
455/// // Read access (multiple readers allowed)
456/// {
457///     let guard = graph.read();
458///     let _nodes = guard.nodes();
459/// }
460///
461/// // Write access (exclusive)
462/// {
463///     let mut guard = graph.write();
464///     let _nodes = guard.nodes_mut();
465/// }
466///
467/// // Snapshot for long queries
468/// let snapshot = graph.snapshot();
469/// ```
470pub struct ConcurrentCodeGraph {
471    /// The underlying code graph protected by a read-write lock.
472    inner: RwLock<CodeGraph>,
473    /// Global epoch counter for cursor validation.
474    epoch: AtomicU64,
475}
476
477impl ConcurrentCodeGraph {
478    /// Creates a new empty `ConcurrentCodeGraph`.
479    #[must_use]
480    pub fn new() -> Self {
481        Self {
482            inner: RwLock::new(CodeGraph::new()),
483            epoch: AtomicU64::new(0),
484        }
485    }
486
487    /// Creates a `ConcurrentCodeGraph` from an existing `CodeGraph`.
488    #[must_use]
489    pub fn from_graph(graph: CodeGraph) -> Self {
490        let epoch = graph.epoch();
491        Self {
492            inner: RwLock::new(graph),
493            epoch: AtomicU64::new(epoch),
494        }
495    }
496
497    /// Acquires a read lock on the graph.
498    ///
499    /// Multiple readers can hold the lock simultaneously.
500    /// This does not increment the epoch.
501    #[inline]
502    pub fn read(&self) -> RwLockReadGuard<'_, CodeGraph> {
503        self.inner.read()
504    }
505
506    /// Acquires a write lock on the graph.
507    ///
508    /// Only one writer can hold the lock at a time.
509    /// This increments the global epoch counter.
510    #[inline]
511    pub fn write(&self) -> RwLockWriteGuard<'_, CodeGraph> {
512        // Increment the global epoch
513        self.epoch.fetch_add(1, Ordering::SeqCst);
514        let mut guard = self.inner.write();
515        // Sync the inner graph's epoch with the global epoch
516        guard.set_epoch(self.epoch.load(Ordering::SeqCst));
517        guard
518    }
519
520    /// Returns the current global epoch.
521    ///
522    /// This can be used to detect if the graph has been modified
523    /// since a previous operation (cursor invalidation).
524    #[inline]
525    #[must_use]
526    pub fn epoch(&self) -> u64 {
527        self.epoch.load(Ordering::SeqCst)
528    }
529
530    /// Creates a cheap snapshot of the graph.
531    ///
532    /// This acquires a brief read lock to clone the Arc references.
533    /// The snapshot is isolated from future mutations.
534    #[must_use]
535    pub fn snapshot(&self) -> GraphSnapshot {
536        self.inner.read().snapshot()
537    }
538
539    /// Attempts to acquire a read lock without blocking.
540    ///
541    /// Returns `None` if the lock is currently held exclusively.
542    #[inline]
543    #[must_use]
544    pub fn try_read(&self) -> Option<RwLockReadGuard<'_, CodeGraph>> {
545        self.inner.try_read()
546    }
547
548    /// Attempts to acquire a write lock without blocking.
549    ///
550    /// Returns `None` if the lock is currently held by another thread.
551    /// If successful, increments the epoch.
552    #[inline]
553    pub fn try_write(&self) -> Option<RwLockWriteGuard<'_, CodeGraph>> {
554        self.inner.try_write().map(|mut guard| {
555            self.epoch.fetch_add(1, Ordering::SeqCst);
556            guard.set_epoch(self.epoch.load(Ordering::SeqCst));
557            guard
558        })
559    }
560}
561
562impl Default for ConcurrentCodeGraph {
563    fn default() -> Self {
564        Self::new()
565    }
566}
567
568impl fmt::Debug for ConcurrentCodeGraph {
569    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
570        f.debug_struct("ConcurrentCodeGraph")
571            .field("epoch", &self.epoch.load(Ordering::SeqCst))
572            .finish_non_exhaustive()
573    }
574}
575
576/// Immutable snapshot of a `CodeGraph` for long-running queries.
577///
578/// `GraphSnapshot` holds Arc references to the graph components,
579/// providing a consistent view that is isolated from concurrent mutations.
580///
581/// # Design
582///
583/// Snapshots are created via `CodeGraph::snapshot()` or
584/// `ConcurrentCodeGraph::snapshot()`. They are:
585///
586/// - **Immutable**: No mutation methods available
587/// - **Isolated**: Independent of future graph mutations
588/// - **Cheap**: Only Arc clones, no data copying
589/// - **Self-contained**: Can outlive the original graph/lock
590///
591/// # Usage
592///
593/// ```rust
594/// use sqry_core::graph::unified::concurrent::{ConcurrentCodeGraph, GraphSnapshot};
595///
596/// let graph = ConcurrentCodeGraph::new();
597///
598/// // Create snapshot for a long query
599/// let snapshot: GraphSnapshot = graph.snapshot();
600///
601/// // Snapshot can be used independently
602/// let _epoch = snapshot.epoch();
603/// ```
604#[derive(Clone)]
605pub struct GraphSnapshot {
606    /// Node storage snapshot.
607    nodes: Arc<NodeArena>,
608    /// Edge storage snapshot.
609    edges: Arc<BidirectionalEdgeStore>,
610    /// String interner snapshot.
611    strings: Arc<StringInterner>,
612    /// File registry snapshot.
613    files: Arc<FileRegistry>,
614    /// Auxiliary indices snapshot.
615    indices: Arc<AuxiliaryIndices>,
616    /// Sparse macro boundary metadata snapshot.
617    macro_metadata: Arc<NodeMetadataStore>,
618    /// Epoch at snapshot time (for cursor validation).
619    epoch: u64,
620}
621
622impl GraphSnapshot {
623    /// Returns a reference to the node arena.
624    #[inline]
625    #[must_use]
626    pub fn nodes(&self) -> &NodeArena {
627        &self.nodes
628    }
629
630    /// Returns a reference to the bidirectional edge store.
631    #[inline]
632    #[must_use]
633    pub fn edges(&self) -> &BidirectionalEdgeStore {
634        &self.edges
635    }
636
637    /// Returns a reference to the string interner.
638    #[inline]
639    #[must_use]
640    pub fn strings(&self) -> &StringInterner {
641        &self.strings
642    }
643
644    /// Returns a reference to the file registry.
645    #[inline]
646    #[must_use]
647    pub fn files(&self) -> &FileRegistry {
648        &self.files
649    }
650
651    /// Returns a reference to the auxiliary indices.
652    #[inline]
653    #[must_use]
654    pub fn indices(&self) -> &AuxiliaryIndices {
655        &self.indices
656    }
657
658    /// Returns a reference to the macro boundary metadata store.
659    #[inline]
660    #[must_use]
661    pub fn macro_metadata(&self) -> &NodeMetadataStore {
662        &self.macro_metadata
663    }
664
665    /// Returns the epoch at which this snapshot was taken.
666    ///
667    /// This can be compared against the current graph epoch to
668    /// detect if the graph has changed since the snapshot.
669    #[inline]
670    #[must_use]
671    pub fn epoch(&self) -> u64 {
672        self.epoch
673    }
674
675    /// Returns `true` if this snapshot's epoch matches the given epoch.
676    ///
677    /// Use this to validate cursors before continuing pagination.
678    #[inline]
679    #[must_use]
680    pub fn epoch_matches(&self, other_epoch: u64) -> bool {
681        self.epoch == other_epoch
682    }
683
684    // ============================================================================
685    // Query Methods
686    // ============================================================================
687
688    /// Finds nodes matching a pattern.
689    ///
690    /// Performs a simple substring match on node names and qualified names.
691    /// Returns all matching node IDs.
692    ///
693    /// # Performance
694    ///
695    /// Optimized to iterate over unique strings in the interner (smaller set)
696    /// rather than all nodes in the arena.
697    ///
698    /// # Arguments
699    ///
700    /// * `pattern` - The pattern to match (substring search)
701    ///
702    /// # Returns
703    ///
704    /// A vector of `NodeIds` for all matching nodes.
705    #[must_use]
706    pub fn find_by_pattern(&self, pattern: &str) -> Vec<crate::graph::unified::node::NodeId> {
707        let mut matches = Vec::new();
708
709        // 1. Scan unique strings in interner for matches
710        for (str_id, s) in self.strings.iter() {
711            if s.contains(pattern) {
712                // 2. If string matches, look up all nodes with this name
713                // Check qualified name index
714                matches.extend_from_slice(self.indices.by_qualified_name(str_id));
715                // Check simple name index
716                matches.extend_from_slice(self.indices.by_name(str_id));
717            }
718        }
719
720        // Deduplicate matches (a node might match both qualified and simple name)
721        matches.sort_unstable();
722        matches.dedup();
723
724        matches
725    }
726
727    /// Gets all callees of a node (functions called by this node).
728    ///
729    /// Queries the forward edge store for all Calls edges from this node.
730    ///
731    /// # Arguments
732    ///
733    /// * `node` - The node ID to query
734    ///
735    /// # Returns
736    ///
737    /// A vector of `NodeIds` representing functions called by this node.
738    #[must_use]
739    pub fn get_callees(
740        &self,
741        node: crate::graph::unified::node::NodeId,
742    ) -> Vec<crate::graph::unified::node::NodeId> {
743        use crate::graph::unified::edge::EdgeKind;
744
745        self.edges
746            .edges_from(node)
747            .into_iter()
748            .filter(|edge| matches!(edge.kind, EdgeKind::Calls { .. }))
749            .map(|edge| edge.target)
750            .collect()
751    }
752
753    /// Gets all callers of a node (functions that call this node).
754    ///
755    /// Queries the reverse edge store for all Calls edges to this node.
756    ///
757    /// # Arguments
758    ///
759    /// * `node` - The node ID to query
760    ///
761    /// # Returns
762    ///
763    /// A vector of `NodeIds` representing functions that call this node.
764    #[must_use]
765    pub fn get_callers(
766        &self,
767        node: crate::graph::unified::node::NodeId,
768    ) -> Vec<crate::graph::unified::node::NodeId> {
769        use crate::graph::unified::edge::EdgeKind;
770
771        self.edges
772            .edges_to(node)
773            .into_iter()
774            .filter(|edge| matches!(edge.kind, EdgeKind::Calls { .. }))
775            .map(|edge| edge.source)
776            .collect()
777    }
778
779    /// Iterates over all nodes in the graph.
780    ///
781    /// Returns an iterator yielding (`NodeId`, &`NodeEntry`) pairs for all
782    /// occupied slots in the arena.
783    ///
784    /// # Returns
785    ///
786    /// An iterator over (`NodeId`, &`NodeEntry`) pairs.
787    pub fn iter_nodes(
788        &self,
789    ) -> impl Iterator<
790        Item = (
791            crate::graph::unified::node::NodeId,
792            &crate::graph::unified::storage::arena::NodeEntry,
793        ),
794    > {
795        self.nodes.iter()
796    }
797
798    /// Iterates over all edges in the graph.
799    ///
800    /// Returns an iterator yielding (source, target, `EdgeKind`) tuples for
801    /// all edges in the forward edge store.
802    ///
803    /// # Returns
804    ///
805    /// An iterator over edge tuples.
806    pub fn iter_edges(
807        &self,
808    ) -> impl Iterator<
809        Item = (
810            crate::graph::unified::node::NodeId,
811            crate::graph::unified::node::NodeId,
812            crate::graph::unified::edge::EdgeKind,
813        ),
814    > + '_ {
815        // Iterate over all nodes in the arena and get their outgoing edges
816        self.nodes.iter().flat_map(move |(node_id, _entry)| {
817            // Get all edges from this node
818            self.edges
819                .edges_from(node_id)
820                .into_iter()
821                .map(move |edge| (node_id, edge.target, edge.kind))
822        })
823    }
824
825    /// Gets a node entry by ID.
826    ///
827    /// Returns a reference to the `NodeEntry` if the ID is valid, or None
828    /// if the ID is invalid or stale.
829    ///
830    /// # Arguments
831    ///
832    /// * `id` - The node ID to look up
833    ///
834    /// # Returns
835    ///
836    /// A reference to the `NodeEntry`, or None if not found.
837    #[must_use]
838    pub fn get_node(
839        &self,
840        id: crate::graph::unified::node::NodeId,
841    ) -> Option<&crate::graph::unified::storage::arena::NodeEntry> {
842        self.nodes.get(id)
843    }
844}
845
846impl fmt::Debug for GraphSnapshot {
847    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
848        f.debug_struct("GraphSnapshot")
849            .field("nodes", &self.nodes.len())
850            .field("epoch", &self.epoch)
851            .finish_non_exhaustive()
852    }
853}
854
855#[cfg(test)]
856mod tests {
857    use super::*;
858    use crate::graph::unified::{
859        FileScope, NodeId, ResolutionMode, SymbolCandidateOutcome, SymbolQuery,
860        SymbolResolutionOutcome,
861    };
862
863    fn resolve_symbol_strict(snapshot: &GraphSnapshot, symbol: &str) -> Option<NodeId> {
864        match snapshot.resolve_symbol(&SymbolQuery {
865            symbol,
866            file_scope: FileScope::Any,
867            mode: ResolutionMode::Strict,
868        }) {
869            SymbolResolutionOutcome::Resolved(node_id) => Some(node_id),
870            SymbolResolutionOutcome::NotFound
871            | SymbolResolutionOutcome::FileNotIndexed
872            | SymbolResolutionOutcome::Ambiguous(_) => None,
873        }
874    }
875
876    fn candidate_nodes(snapshot: &GraphSnapshot, symbol: &str) -> Vec<NodeId> {
877        match snapshot.find_symbol_candidates(&SymbolQuery {
878            symbol,
879            file_scope: FileScope::Any,
880            mode: ResolutionMode::AllowSuffixCandidates,
881        }) {
882            SymbolCandidateOutcome::Candidates(candidates) => candidates,
883            SymbolCandidateOutcome::NotFound | SymbolCandidateOutcome::FileNotIndexed => Vec::new(),
884        }
885    }
886
887    #[test]
888    fn test_code_graph_new() {
889        let graph = CodeGraph::new();
890        assert_eq!(graph.epoch(), 0);
891        assert_eq!(graph.nodes().len(), 0);
892    }
893
894    #[test]
895    fn test_code_graph_default() {
896        let graph = CodeGraph::default();
897        assert_eq!(graph.epoch(), 0);
898    }
899
900    #[test]
901    fn test_code_graph_snapshot() {
902        let graph = CodeGraph::new();
903        let snapshot = graph.snapshot();
904        assert_eq!(snapshot.epoch(), 0);
905        assert_eq!(snapshot.nodes().len(), 0);
906    }
907
908    #[test]
909    fn test_code_graph_bump_epoch() {
910        let mut graph = CodeGraph::new();
911        assert_eq!(graph.epoch(), 0);
912        assert_eq!(graph.bump_epoch(), 1);
913        assert_eq!(graph.epoch(), 1);
914        assert_eq!(graph.bump_epoch(), 2);
915        assert_eq!(graph.epoch(), 2);
916    }
917
918    #[test]
919    fn test_code_graph_set_epoch() {
920        let mut graph = CodeGraph::new();
921        graph.set_epoch(42);
922        assert_eq!(graph.epoch(), 42);
923    }
924
925    #[test]
926    fn test_code_graph_from_components() {
927        let nodes = NodeArena::new();
928        let edges = BidirectionalEdgeStore::new();
929        let strings = StringInterner::new();
930        let files = FileRegistry::new();
931        let indices = AuxiliaryIndices::new();
932        let macro_metadata = NodeMetadataStore::new();
933
934        let graph =
935            CodeGraph::from_components(nodes, edges, strings, files, indices, macro_metadata);
936        assert_eq!(graph.epoch(), 0);
937    }
938
939    #[test]
940    fn test_code_graph_mut_accessors() {
941        let mut graph = CodeGraph::new();
942
943        // Access mutable references - should not panic
944        let _nodes = graph.nodes_mut();
945        let _edges = graph.edges_mut();
946        let _strings = graph.strings_mut();
947        let _files = graph.files_mut();
948        let _indices = graph.indices_mut();
949    }
950
951    #[test]
952    fn test_code_graph_snapshot_isolation() {
953        let mut graph = CodeGraph::new();
954        let snapshot1 = graph.snapshot();
955
956        // Mutate the graph
957        graph.bump_epoch();
958
959        let snapshot2 = graph.snapshot();
960
961        // Snapshots should have different epochs
962        assert_eq!(snapshot1.epoch(), 0);
963        assert_eq!(snapshot2.epoch(), 1);
964    }
965
966    #[test]
967    fn test_code_graph_debug() {
968        let graph = CodeGraph::new();
969        let debug_str = format!("{graph:?}");
970        assert!(debug_str.contains("CodeGraph"));
971        assert!(debug_str.contains("epoch"));
972    }
973
974    #[test]
975    fn test_concurrent_code_graph_new() {
976        let graph = ConcurrentCodeGraph::new();
977        assert_eq!(graph.epoch(), 0);
978    }
979
980    #[test]
981    fn test_concurrent_code_graph_default() {
982        let graph = ConcurrentCodeGraph::default();
983        assert_eq!(graph.epoch(), 0);
984    }
985
986    #[test]
987    fn test_concurrent_code_graph_from_graph() {
988        let mut inner = CodeGraph::new();
989        inner.set_epoch(10);
990        let graph = ConcurrentCodeGraph::from_graph(inner);
991        assert_eq!(graph.epoch(), 10);
992    }
993
994    #[test]
995    fn test_concurrent_code_graph_read() {
996        let graph = ConcurrentCodeGraph::new();
997        let guard = graph.read();
998        assert_eq!(guard.epoch(), 0);
999        assert_eq!(guard.nodes().len(), 0);
1000    }
1001
1002    #[test]
1003    fn test_concurrent_code_graph_write_increments_epoch() {
1004        let graph = ConcurrentCodeGraph::new();
1005        assert_eq!(graph.epoch(), 0);
1006
1007        {
1008            let guard = graph.write();
1009            assert_eq!(guard.epoch(), 1);
1010        }
1011
1012        assert_eq!(graph.epoch(), 1);
1013
1014        {
1015            let _guard = graph.write();
1016        }
1017
1018        assert_eq!(graph.epoch(), 2);
1019    }
1020
1021    #[test]
1022    fn test_concurrent_code_graph_snapshot() {
1023        let graph = ConcurrentCodeGraph::new();
1024
1025        {
1026            let _guard = graph.write();
1027        }
1028
1029        let snapshot = graph.snapshot();
1030        assert_eq!(snapshot.epoch(), 1);
1031    }
1032
1033    #[test]
1034    fn test_concurrent_code_graph_try_read() {
1035        let graph = ConcurrentCodeGraph::new();
1036        let guard = graph.try_read();
1037        assert!(guard.is_some());
1038    }
1039
1040    #[test]
1041    fn test_concurrent_code_graph_try_write() {
1042        let graph = ConcurrentCodeGraph::new();
1043        let guard = graph.try_write();
1044        assert!(guard.is_some());
1045        assert_eq!(graph.epoch(), 1);
1046    }
1047
1048    #[test]
1049    fn test_concurrent_code_graph_debug() {
1050        let graph = ConcurrentCodeGraph::new();
1051        let debug_str = format!("{graph:?}");
1052        assert!(debug_str.contains("ConcurrentCodeGraph"));
1053        assert!(debug_str.contains("epoch"));
1054    }
1055
1056    #[test]
1057    fn test_graph_snapshot_accessors() {
1058        let graph = CodeGraph::new();
1059        let snapshot = graph.snapshot();
1060
1061        // All accessors should work
1062        let _nodes = snapshot.nodes();
1063        let _edges = snapshot.edges();
1064        let _strings = snapshot.strings();
1065        let _files = snapshot.files();
1066        let _indices = snapshot.indices();
1067        let _epoch = snapshot.epoch();
1068    }
1069
1070    #[test]
1071    fn test_graph_snapshot_epoch_matches() {
1072        let graph = CodeGraph::new();
1073        let snapshot = graph.snapshot();
1074
1075        assert!(snapshot.epoch_matches(0));
1076        assert!(!snapshot.epoch_matches(1));
1077    }
1078
1079    #[test]
1080    fn test_graph_snapshot_clone() {
1081        let graph = CodeGraph::new();
1082        let snapshot1 = graph.snapshot();
1083        let snapshot2 = snapshot1.clone();
1084
1085        assert_eq!(snapshot1.epoch(), snapshot2.epoch());
1086    }
1087
1088    #[test]
1089    fn test_graph_snapshot_debug() {
1090        let graph = CodeGraph::new();
1091        let snapshot = graph.snapshot();
1092        let debug_str = format!("{snapshot:?}");
1093        assert!(debug_str.contains("GraphSnapshot"));
1094        assert!(debug_str.contains("epoch"));
1095    }
1096
1097    #[test]
1098    fn test_multiple_readers() {
1099        let graph = ConcurrentCodeGraph::new();
1100
1101        // Multiple readers should be able to acquire locks simultaneously
1102        let guard1 = graph.read();
1103        let guard2 = graph.read();
1104        let guard3 = graph.read();
1105
1106        assert_eq!(guard1.epoch(), 0);
1107        assert_eq!(guard2.epoch(), 0);
1108        assert_eq!(guard3.epoch(), 0);
1109    }
1110
1111    #[test]
1112    fn test_code_graph_clone() {
1113        let mut graph = CodeGraph::new();
1114        graph.bump_epoch();
1115
1116        let cloned = graph.clone();
1117        assert_eq!(cloned.epoch(), 1);
1118    }
1119
1120    #[test]
1121    fn test_epoch_wrapping() {
1122        let mut graph = CodeGraph::new();
1123        graph.set_epoch(u64::MAX);
1124        let new_epoch = graph.bump_epoch();
1125        assert_eq!(new_epoch, 0); // Should wrap around
1126    }
1127
1128    // ============================================================================
1129    // Query method tests
1130    // ============================================================================
1131
1132    #[test]
1133    fn test_snapshot_resolve_symbol() {
1134        use crate::graph::unified::node::NodeKind;
1135        use crate::graph::unified::storage::arena::NodeEntry;
1136        use std::path::Path;
1137
1138        let mut graph = CodeGraph::new();
1139
1140        // Add some nodes with qualified names
1141        let name_id = graph.strings_mut().intern("test_func").unwrap();
1142        let qual_name_id = graph.strings_mut().intern("module::test_func").unwrap();
1143        let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
1144
1145        let entry =
1146            NodeEntry::new(NodeKind::Function, name_id, file_id).with_qualified_name(qual_name_id);
1147
1148        let node_id = graph.nodes_mut().alloc(entry).unwrap();
1149        graph.indices_mut().add(
1150            node_id,
1151            NodeKind::Function,
1152            name_id,
1153            Some(qual_name_id),
1154            file_id,
1155        );
1156
1157        let snapshot = graph.snapshot();
1158
1159        // Find by qualified name
1160        let found = resolve_symbol_strict(&snapshot, "module::test_func");
1161        assert_eq!(found, Some(node_id));
1162
1163        // Find by exact simple name
1164        let found2 = resolve_symbol_strict(&snapshot, "test_func");
1165        assert_eq!(found2, Some(node_id));
1166
1167        // Not found
1168        assert!(resolve_symbol_strict(&snapshot, "nonexistent").is_none());
1169    }
1170
1171    #[test]
1172    fn test_snapshot_find_by_pattern() {
1173        use crate::graph::unified::node::NodeKind;
1174        use crate::graph::unified::storage::arena::NodeEntry;
1175        use std::path::Path;
1176
1177        let mut graph = CodeGraph::new();
1178
1179        // Add nodes with different names
1180        let name1 = graph.strings_mut().intern("foo_bar").unwrap();
1181        let name2 = graph.strings_mut().intern("baz_bar").unwrap();
1182        let name3 = graph.strings_mut().intern("qux_test").unwrap();
1183        let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
1184
1185        let node1 = graph
1186            .nodes_mut()
1187            .alloc(NodeEntry::new(NodeKind::Function, name1, file_id))
1188            .unwrap();
1189        let node2 = graph
1190            .nodes_mut()
1191            .alloc(NodeEntry::new(NodeKind::Function, name2, file_id))
1192            .unwrap();
1193        let node3 = graph
1194            .nodes_mut()
1195            .alloc(NodeEntry::new(NodeKind::Function, name3, file_id))
1196            .unwrap();
1197
1198        graph
1199            .indices_mut()
1200            .add(node1, NodeKind::Function, name1, None, file_id);
1201        graph
1202            .indices_mut()
1203            .add(node2, NodeKind::Function, name2, None, file_id);
1204        graph
1205            .indices_mut()
1206            .add(node3, NodeKind::Function, name3, None, file_id);
1207
1208        let snapshot = graph.snapshot();
1209
1210        // Find by pattern
1211        let matches = snapshot.find_by_pattern("bar");
1212        assert_eq!(matches.len(), 2);
1213        assert!(matches.contains(&node1));
1214        assert!(matches.contains(&node2));
1215
1216        // Find single match
1217        let matches = snapshot.find_by_pattern("qux");
1218        assert_eq!(matches.len(), 1);
1219        assert_eq!(matches[0], node3);
1220
1221        // No matches
1222        let matches = snapshot.find_by_pattern("nonexistent");
1223        assert!(matches.is_empty());
1224    }
1225
1226    #[test]
1227    fn test_snapshot_get_callees() {
1228        use crate::graph::unified::edge::EdgeKind;
1229        use crate::graph::unified::node::NodeKind;
1230        use crate::graph::unified::storage::arena::NodeEntry;
1231        use std::path::Path;
1232
1233        let mut graph = CodeGraph::new();
1234
1235        // Create caller and callee nodes
1236        let caller_name = graph.strings_mut().intern("caller").unwrap();
1237        let callee1_name = graph.strings_mut().intern("callee1").unwrap();
1238        let callee2_name = graph.strings_mut().intern("callee2").unwrap();
1239        let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
1240
1241        let caller_id = graph
1242            .nodes_mut()
1243            .alloc(NodeEntry::new(NodeKind::Function, caller_name, file_id))
1244            .unwrap();
1245        let callee1_id = graph
1246            .nodes_mut()
1247            .alloc(NodeEntry::new(NodeKind::Function, callee1_name, file_id))
1248            .unwrap();
1249        let callee2_id = graph
1250            .nodes_mut()
1251            .alloc(NodeEntry::new(NodeKind::Function, callee2_name, file_id))
1252            .unwrap();
1253
1254        // Add call edges
1255        graph.edges_mut().add_edge(
1256            caller_id,
1257            callee1_id,
1258            EdgeKind::Calls {
1259                argument_count: 0,
1260                is_async: false,
1261            },
1262            file_id,
1263        );
1264        graph.edges_mut().add_edge(
1265            caller_id,
1266            callee2_id,
1267            EdgeKind::Calls {
1268                argument_count: 0,
1269                is_async: false,
1270            },
1271            file_id,
1272        );
1273
1274        let snapshot = graph.snapshot();
1275
1276        // Query callees
1277        let callees = snapshot.get_callees(caller_id);
1278        assert_eq!(callees.len(), 2);
1279        assert!(callees.contains(&callee1_id));
1280        assert!(callees.contains(&callee2_id));
1281
1282        // Node with no callees
1283        let callees = snapshot.get_callees(callee1_id);
1284        assert!(callees.is_empty());
1285    }
1286
1287    #[test]
1288    fn test_snapshot_get_callers() {
1289        use crate::graph::unified::edge::EdgeKind;
1290        use crate::graph::unified::node::NodeKind;
1291        use crate::graph::unified::storage::arena::NodeEntry;
1292        use std::path::Path;
1293
1294        let mut graph = CodeGraph::new();
1295
1296        // Create caller and callee nodes
1297        let caller1_name = graph.strings_mut().intern("caller1").unwrap();
1298        let caller2_name = graph.strings_mut().intern("caller2").unwrap();
1299        let callee_name = graph.strings_mut().intern("callee").unwrap();
1300        let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
1301
1302        let caller1_id = graph
1303            .nodes_mut()
1304            .alloc(NodeEntry::new(NodeKind::Function, caller1_name, file_id))
1305            .unwrap();
1306        let caller2_id = graph
1307            .nodes_mut()
1308            .alloc(NodeEntry::new(NodeKind::Function, caller2_name, file_id))
1309            .unwrap();
1310        let callee_id = graph
1311            .nodes_mut()
1312            .alloc(NodeEntry::new(NodeKind::Function, callee_name, file_id))
1313            .unwrap();
1314
1315        // Add call edges
1316        graph.edges_mut().add_edge(
1317            caller1_id,
1318            callee_id,
1319            EdgeKind::Calls {
1320                argument_count: 0,
1321                is_async: false,
1322            },
1323            file_id,
1324        );
1325        graph.edges_mut().add_edge(
1326            caller2_id,
1327            callee_id,
1328            EdgeKind::Calls {
1329                argument_count: 0,
1330                is_async: false,
1331            },
1332            file_id,
1333        );
1334
1335        let snapshot = graph.snapshot();
1336
1337        // Query callers
1338        let callers = snapshot.get_callers(callee_id);
1339        assert_eq!(callers.len(), 2);
1340        assert!(callers.contains(&caller1_id));
1341        assert!(callers.contains(&caller2_id));
1342
1343        // Node with no callers
1344        let callers = snapshot.get_callers(caller1_id);
1345        assert!(callers.is_empty());
1346    }
1347
1348    #[test]
1349    fn test_snapshot_find_symbol_candidates() {
1350        use crate::graph::unified::node::NodeKind;
1351        use crate::graph::unified::storage::arena::NodeEntry;
1352        use std::path::Path;
1353
1354        let mut graph = CodeGraph::new();
1355
1356        // Add nodes with same symbol name but different qualified names
1357        let symbol_name = graph.strings_mut().intern("test").unwrap();
1358        let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
1359
1360        let node1 = graph
1361            .nodes_mut()
1362            .alloc(NodeEntry::new(NodeKind::Function, symbol_name, file_id))
1363            .unwrap();
1364        let node2 = graph
1365            .nodes_mut()
1366            .alloc(NodeEntry::new(NodeKind::Method, symbol_name, file_id))
1367            .unwrap();
1368
1369        // Add a different symbol
1370        let other_name = graph.strings_mut().intern("other").unwrap();
1371        let node3 = graph
1372            .nodes_mut()
1373            .alloc(NodeEntry::new(NodeKind::Function, other_name, file_id))
1374            .unwrap();
1375
1376        graph
1377            .indices_mut()
1378            .add(node1, NodeKind::Function, symbol_name, None, file_id);
1379        graph
1380            .indices_mut()
1381            .add(node2, NodeKind::Method, symbol_name, None, file_id);
1382        graph
1383            .indices_mut()
1384            .add(node3, NodeKind::Function, other_name, None, file_id);
1385
1386        let snapshot = graph.snapshot();
1387
1388        // Find by symbol
1389        let matches = candidate_nodes(&snapshot, "test");
1390        assert_eq!(matches.len(), 2);
1391        assert!(matches.contains(&node1));
1392        assert!(matches.contains(&node2));
1393
1394        // Find other symbol
1395        let matches = candidate_nodes(&snapshot, "other");
1396        assert_eq!(matches.len(), 1);
1397        assert_eq!(matches[0], node3);
1398
1399        // No matches
1400        let matches = candidate_nodes(&snapshot, "nonexistent");
1401        assert!(matches.is_empty());
1402    }
1403
1404    #[test]
1405    fn test_snapshot_iter_nodes() {
1406        use crate::graph::unified::node::NodeKind;
1407        use crate::graph::unified::storage::arena::NodeEntry;
1408        use std::path::Path;
1409
1410        let mut graph = CodeGraph::new();
1411
1412        // Add some nodes
1413        let name1 = graph.strings_mut().intern("func1").unwrap();
1414        let name2 = graph.strings_mut().intern("func2").unwrap();
1415        let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
1416
1417        let node1 = graph
1418            .nodes_mut()
1419            .alloc(NodeEntry::new(NodeKind::Function, name1, file_id))
1420            .unwrap();
1421        let node2 = graph
1422            .nodes_mut()
1423            .alloc(NodeEntry::new(NodeKind::Function, name2, file_id))
1424            .unwrap();
1425
1426        let snapshot = graph.snapshot();
1427
1428        // Iterate nodes
1429        let snapshot_nodes: Vec<_> = snapshot.iter_nodes().collect();
1430        assert_eq!(snapshot_nodes.len(), 2);
1431
1432        let node_ids: Vec<_> = snapshot_nodes.iter().map(|(id, _)| *id).collect();
1433        assert!(node_ids.contains(&node1));
1434        assert!(node_ids.contains(&node2));
1435    }
1436
1437    #[test]
1438    fn test_snapshot_iter_edges() {
1439        use crate::graph::unified::edge::EdgeKind;
1440        use crate::graph::unified::node::NodeKind;
1441        use crate::graph::unified::storage::arena::NodeEntry;
1442        use std::path::Path;
1443
1444        let mut graph = CodeGraph::new();
1445
1446        // Create nodes
1447        let name1 = graph.strings_mut().intern("func1").unwrap();
1448        let name2 = graph.strings_mut().intern("func2").unwrap();
1449        let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
1450
1451        let node1 = graph
1452            .nodes_mut()
1453            .alloc(NodeEntry::new(NodeKind::Function, name1, file_id))
1454            .unwrap();
1455        let node2 = graph
1456            .nodes_mut()
1457            .alloc(NodeEntry::new(NodeKind::Function, name2, file_id))
1458            .unwrap();
1459
1460        // Add edges
1461        graph.edges_mut().add_edge(
1462            node1,
1463            node2,
1464            EdgeKind::Calls {
1465                argument_count: 0,
1466                is_async: false,
1467            },
1468            file_id,
1469        );
1470
1471        let snapshot = graph.snapshot();
1472
1473        // Iterate edges
1474        let edges: Vec<_> = snapshot.iter_edges().collect();
1475        assert_eq!(edges.len(), 1);
1476
1477        let (src, tgt, kind) = &edges[0];
1478        assert_eq!(*src, node1);
1479        assert_eq!(*tgt, node2);
1480        assert!(matches!(
1481            kind,
1482            EdgeKind::Calls {
1483                argument_count: 0,
1484                is_async: false
1485            }
1486        ));
1487    }
1488
1489    #[test]
1490    fn test_snapshot_get_node() {
1491        use crate::graph::unified::node::NodeId;
1492        use crate::graph::unified::node::NodeKind;
1493        use crate::graph::unified::storage::arena::NodeEntry;
1494        use std::path::Path;
1495
1496        let mut graph = CodeGraph::new();
1497
1498        // Add a node
1499        let name = graph.strings_mut().intern("test_func").unwrap();
1500        let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
1501
1502        let node_id = graph
1503            .nodes_mut()
1504            .alloc(NodeEntry::new(NodeKind::Function, name, file_id))
1505            .unwrap();
1506
1507        let snapshot = graph.snapshot();
1508
1509        // Get node
1510        let entry = snapshot.get_node(node_id);
1511        assert!(entry.is_some());
1512        assert_eq!(entry.unwrap().kind, NodeKind::Function);
1513
1514        // Invalid node
1515        let invalid_id = NodeId::INVALID;
1516        assert!(snapshot.get_node(invalid_id).is_none());
1517    }
1518
1519    #[test]
1520    fn test_snapshot_query_empty_graph() {
1521        use crate::graph::unified::node::NodeId;
1522
1523        let graph = CodeGraph::new();
1524        let snapshot = graph.snapshot();
1525
1526        // All queries should return empty on empty graph
1527        assert!(resolve_symbol_strict(&snapshot, "test").is_none());
1528        assert!(snapshot.find_by_pattern("test").is_empty());
1529        assert!(candidate_nodes(&snapshot, "test").is_empty());
1530
1531        let dummy_id = NodeId::new(0, 1);
1532        assert!(snapshot.get_callees(dummy_id).is_empty());
1533        assert!(snapshot.get_callers(dummy_id).is_empty());
1534
1535        assert_eq!(snapshot.iter_nodes().count(), 0);
1536        assert_eq!(snapshot.iter_edges().count(), 0);
1537    }
1538
1539    #[test]
1540    fn test_snapshot_edge_filtering_by_kind() {
1541        use crate::graph::unified::edge::EdgeKind;
1542        use crate::graph::unified::node::NodeKind;
1543        use crate::graph::unified::storage::arena::NodeEntry;
1544        use std::path::Path;
1545
1546        let mut graph = CodeGraph::new();
1547
1548        // Create nodes
1549        let name1 = graph.strings_mut().intern("func1").unwrap();
1550        let name2 = graph.strings_mut().intern("func2").unwrap();
1551        let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
1552
1553        let node1 = graph
1554            .nodes_mut()
1555            .alloc(NodeEntry::new(NodeKind::Function, name1, file_id))
1556            .unwrap();
1557        let node2 = graph
1558            .nodes_mut()
1559            .alloc(NodeEntry::new(NodeKind::Function, name2, file_id))
1560            .unwrap();
1561
1562        // Add different kinds of edges
1563        graph.edges_mut().add_edge(
1564            node1,
1565            node2,
1566            EdgeKind::Calls {
1567                argument_count: 0,
1568                is_async: false,
1569            },
1570            file_id,
1571        );
1572        graph
1573            .edges_mut()
1574            .add_edge(node1, node2, EdgeKind::References, file_id);
1575
1576        let snapshot = graph.snapshot();
1577
1578        // get_callees should only return Calls edges
1579        let callees = snapshot.get_callees(node1);
1580        assert_eq!(callees.len(), 1);
1581        assert_eq!(callees[0], node2);
1582
1583        // iter_edges returns all edges regardless of kind
1584        let edges: Vec<_> = snapshot.iter_edges().collect();
1585        assert_eq!(edges.len(), 2);
1586    }
1587}