Skip to main content

sqry_db/comparative/
mod.rs

1//! `ComparativeQueryDb` for cross-snapshot operations.
2//!
3//! `semantic_diff` operates across two separate snapshots (different git refs
4//! or worktrees). This does not fit the single-snapshot `QueryDb` model because
5//! cross-snapshot results cannot be meaningfully invalidated.
6//!
7//! `ComparativeQueryDb` is a lightweight wrapper holding two snapshots. It
8//! bypasses the `ShardedCache` entirely — comparative queries are inherently
9//! one-shot, uncached operations.
10//!
11//! # DB20 design choice (Option A)
12//!
13//! The `diff` computation is exposed as an inherent method on
14//! `ComparativeQueryDb` rather than as a `DerivedQuery` (which would require
15//! a single-snapshot key) or as a free function with separate snapshots. This:
16//!
17//! 1. Keeps the public surface minimal (one wrapper, one method).
18//! 2. Co-locates cross-snapshot logic with the type that owns the two
19//!    snapshots.
20//! 3. Avoids an awkward "uncached `DerivedQuery`" abstraction that would
21//!    contradict the three-tier cache invariants.
22//!
23//! See `docs/superpowers/specs/2026-04-12-derived-analysis-db-query-planner-design.md`
24//! section M6 for the rationale.
25
26use std::sync::Arc;
27
28use sqry_core::graph::unified::concurrent::GraphSnapshot;
29
30pub mod diff;
31
32pub use diff::{ChangeType, DiffOptions, DiffOutput, DiffSummary, NodeChange, NodeLocation};
33
34/// A lightweight wrapper holding two `GraphSnapshot`s for cross-snapshot
35/// operations like `semantic_diff`.
36///
37/// Comparative queries are NOT cached (they are inherently one-shot
38/// cross-snapshot operations), so this type bypasses the `ShardedCache`
39/// entirely.
40///
41/// # Usage
42///
43/// ```rust,ignore
44/// use sqry_db::comparative::{ComparativeQueryDb, DiffOptions};
45///
46/// let cmp = ComparativeQueryDb::new(old_snapshot, new_snapshot);
47/// let out = cmp.diff(&DiffOptions::default()); // uncached, one-shot
48/// ```
49pub struct ComparativeQueryDb {
50    /// The "before" snapshot (e.g., older commit).
51    old: Arc<GraphSnapshot>,
52    /// The "after" snapshot (e.g., newer commit).
53    new: Arc<GraphSnapshot>,
54}
55
56impl ComparativeQueryDb {
57    /// Creates a new comparative DB from two snapshots.
58    #[must_use]
59    pub fn new(old: Arc<GraphSnapshot>, new: Arc<GraphSnapshot>) -> Self {
60        Self { old, new }
61    }
62
63    /// Returns the "before" snapshot.
64    #[inline]
65    #[must_use]
66    pub fn old(&self) -> &GraphSnapshot {
67        &self.old
68    }
69
70    /// Returns the "after" snapshot.
71    #[inline]
72    #[must_use]
73    pub fn new_snapshot(&self) -> &GraphSnapshot {
74        &self.new
75    }
76
77    /// Returns the "before" snapshot as an `Arc`.
78    #[inline]
79    #[must_use]
80    pub fn old_arc(&self) -> Arc<GraphSnapshot> {
81        Arc::clone(&self.old)
82    }
83
84    /// Returns the "after" snapshot as an `Arc`.
85    #[inline]
86    #[must_use]
87    pub fn new_arc(&self) -> Arc<GraphSnapshot> {
88        Arc::clone(&self.new)
89    }
90
91    /// Computes the semantic diff between the two snapshots.
92    ///
93    /// This is an uncached, one-shot operation. Results are NOT memoized by
94    /// the `ShardedCache` because cross-snapshot results have no meaningful
95    /// invalidation criterion (the two snapshots are immutable, and no
96    /// single-snapshot dependency bump would ever reinvalidate their diff).
97    ///
98    /// Pass the worktree roots via [`DiffOptions`] so per-file paths are
99    /// prefixed back to absolute worktree locations. Callers that do not
100    /// need worktree prefixing (e.g. unit tests or CLI callers that already
101    /// hold workspace-relative paths) may use [`Self::diff_default`].
102    #[must_use]
103    pub fn diff(&self, opts: &DiffOptions) -> DiffOutput {
104        diff::compute_diff(&self.old, &self.new, opts)
105    }
106
107    /// Computes the semantic diff using default options (no worktree
108    /// prefixing). See [`Self::diff`] for the full semantics.
109    #[must_use]
110    pub fn diff_default(&self) -> DiffOutput {
111        self.diff(&DiffOptions::default())
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    // ComparativeQueryDb is a pure wrapper — construction and accessor tests
120    // are covered in integration tests that build real graph snapshots.
121    // This module verifies the type is Send + Sync (required for cross-thread
122    // usage in MCP handlers).
123    #[test]
124    fn comparative_query_db_is_send_sync() {
125        fn assert_send_sync<T: Send + Sync>() {}
126        assert_send_sync::<ComparativeQueryDb>();
127    }
128}