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