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}