Skip to main content

sqry_core/workspace/
index.rs

1//! `WorkspaceIndex`: multi-repo search orchestration using `SessionManager`.
2
3use std::path::{Path, PathBuf};
4
5use crate::graph::unified::NodeKind;
6use crate::query::QueryExecutor;
7use crate::session::SessionManager;
8
9use super::error::{WorkspaceError, WorkspaceResult};
10use super::registry::{WorkspaceRegistry, WorkspaceRepository};
11
12/// A workspace-level index that orchestrates queries across multiple repositories.
13///
14/// `WorkspaceIndex` delegates to `SessionManager` for individual repository queries,
15/// leveraging its caching and lazy-loading capabilities. Results are aggregated
16/// and tagged with repository metadata.
17pub struct WorkspaceIndex {
18    registry: WorkspaceRegistry,
19    session: SessionManager,
20    workspace_root: PathBuf,
21}
22
23impl WorkspaceIndex {
24    /// Open a workspace index from the registry file at the given path.
25    ///
26    /// # Arguments
27    ///
28    /// * `workspace_root` - Root directory of the workspace
29    /// * `registry_path` - Path to the .sqry-workspace registry file
30    ///
31    /// # Example
32    ///
33    /// ```no_run
34    /// use sqry_core::workspace::WorkspaceIndex;
35    /// use std::path::Path;
36    ///
37    /// let workspace = Path::new("/path/to/workspace");
38    /// let registry = workspace.join(".sqry-workspace");
39    /// let mut index = WorkspaceIndex::open(workspace, &registry).unwrap();
40    /// ```
41    ///
42    /// # Errors
43    ///
44    /// Returns [`WorkspaceError`] when registry loading or session creation fails.
45    pub fn open(workspace_root: impl Into<PathBuf>, registry_path: &Path) -> WorkspaceResult<Self> {
46        let workspace_root = workspace_root.into();
47        let registry = WorkspaceRegistry::load(registry_path)?;
48        let session = SessionManager::new().map_err(|e| WorkspaceError::QueryParsing {
49            message: format!("Failed to create SessionManager: {e}"),
50        })?;
51
52        Ok(Self {
53            registry,
54            session,
55            workspace_root,
56        })
57    }
58
59    /// Create a new workspace index with an existing registry and session.
60    ///
61    /// Useful for testing or when you want to provide a custom `SessionManager`.
62    pub fn new(
63        workspace_root: impl Into<PathBuf>,
64        registry: WorkspaceRegistry,
65        session: SessionManager,
66    ) -> Self {
67        Self {
68            registry,
69            session,
70            workspace_root: workspace_root.into(),
71        }
72    }
73
74    /// Execute a query across all repositories in the workspace.
75    ///
76    /// This method:
77    /// 1. Parses the query into AST and extracts repo filter
78    /// 2. Filters repositories based on the repo predicates
79    /// 3. Executes the normalized query (repo predicates stripped) against each matching repository
80    /// 4. Aggregates results with repository metadata
81    ///
82    /// # Arguments
83    ///
84    /// * `query` - The query string to execute (may include repo: predicates)
85    ///
86    /// # Returns
87    ///
88    /// A list of matches with their associated repository information.
89    ///
90    /// # Example
91    ///
92    /// ```no_run
93    /// use sqry_core::workspace::WorkspaceIndex;
94    /// use std::path::Path;
95    ///
96    /// let workspace = Path::new("/path/to/workspace");
97    /// let registry = workspace.join(".sqry-workspace");
98    /// let mut index = WorkspaceIndex::open(workspace, &registry).unwrap();
99    ///
100    /// let results = index.query("kind:function AND repo:backend-*").unwrap();
101    /// ```
102    ///
103    /// # Errors
104    ///
105    /// Returns [`WorkspaceError`] when parsing the query or executing repository queries fails.
106    pub fn query(&mut self, query: &str) -> WorkspaceResult<Vec<NodeWithRepo>> {
107        // Parse query into AST to extract repo filter.
108        let executor = QueryExecutor::new();
109        let parsed = executor
110            .parse_query_ast(query)
111            .map_err(|e| WorkspaceError::QueryParsing {
112                message: format!("Failed to parse query: {e}"),
113            })?;
114
115        // Filter repositories based on the repo predicates
116        let repos: Vec<&WorkspaceRepository> = self
117            .registry
118            .repositories
119            .iter()
120            .filter(|repo| parsed.repo_filter.matches(&repo.name))
121            .collect();
122
123        if repos.is_empty() {
124            return Ok(Vec::new());
125        }
126
127        // Use normalized query string (repo predicates stripped) for repository execution
128        // The repo filtering has already been done at the workspace level above
129        let query_str = parsed.normalized.as_ref();
130
131        // Execute query against each repository and aggregate results
132        let mut all_results = Vec::new();
133
134        for repo in repos {
135            // Resolve absolute path for the repository
136            let repo_path = if repo.root.is_absolute() {
137                repo.root.clone()
138            } else {
139                self.workspace_root.join(&repo.root)
140            };
141
142            // Query the repository via SessionManager
143            // SessionManager will parse the query again and execute it against the single-repo index
144            match self.session.query(&repo_path, query_str) {
145                Ok(results) => {
146                    // Tag each result with repo metadata
147                    for m in results.iter() {
148                        let match_info = MatchInfo {
149                            name: m.name().map(|s| s.to_string()).unwrap_or_default(),
150                            qualified_name: m.qualified_name().map(|s| s.to_string()),
151                            kind: m.kind(),
152                            language: m.language().map(|lang| lang.to_string()),
153                            file_path: m.relative_path().unwrap_or_default(),
154                            start_line: m.start_line(),
155                            start_column: m.start_column(),
156                            end_line: m.end_line(),
157                            end_column: m.end_column(),
158                            is_static: m.is_static(),
159                            signature: m.signature().map(|s| s.to_string()),
160                            doc: m.doc().map(|s| s.to_string()),
161                        };
162                        all_results.push(NodeWithRepo {
163                            match_info,
164                            repo_name: repo.name.clone(),
165                            repo_id: repo.id.clone(),
166                            repo_path: repo_path.clone(),
167                        });
168                    }
169                }
170                Err(err) => {
171                    // Log error but continue with other repos
172                    eprintln!(
173                        "Warning: Failed to query repository '{}': {}",
174                        repo.name, err
175                    );
176                }
177            }
178        }
179
180        Ok(all_results)
181    }
182
183    /// Get workspace-level statistics.
184    ///
185    /// Returns aggregated stats across all repositories in the workspace.
186    #[must_use]
187    pub fn stats(&self) -> WorkspaceStats {
188        let total_repos = self.registry.repositories.len();
189        let indexed_repos = self
190            .registry
191            .repositories
192            .iter()
193            .filter(|r| r.last_indexed_at.is_some())
194            .count();
195        let total_symbols = self
196            .registry
197            .repositories
198            .iter()
199            .filter_map(|r| r.symbol_count)
200            .sum();
201
202        WorkspaceStats {
203            total_repos,
204            indexed_repos,
205            total_symbols,
206        }
207    }
208
209    /// Get detailed workspace statistics including staleness tracking.
210    ///
211    /// Returns comprehensive stats with freshness buckets, health scores,
212    /// and other detailed metrics.
213    #[must_use]
214    pub fn detailed_stats(&self) -> super::stats::DetailedWorkspaceStats {
215        super::stats::DetailedWorkspaceStats::from_registry(&self.registry)
216    }
217
218    /// Get a reference to the underlying registry.
219    #[must_use]
220    pub fn registry(&self) -> &WorkspaceRegistry {
221        &self.registry
222    }
223
224    /// Get a mutable reference to the underlying registry.
225    pub fn registry_mut(&mut self) -> &mut WorkspaceRegistry {
226        &mut self.registry
227    }
228
229    /// Get the workspace root directory.
230    #[must_use]
231    pub fn workspace_root(&self) -> &Path {
232        &self.workspace_root
233    }
234}
235
236/// Query match information from `CodeGraph`.
237#[derive(Debug, Clone)]
238pub struct MatchInfo {
239    /// Node name
240    pub name: String,
241    /// Canonical qualified name, if available.
242    pub qualified_name: Option<String>,
243    /// Node kind (function, class, etc.)
244    pub kind: NodeKind,
245    /// Language derived from the backing file, if available.
246    pub language: Option<String>,
247    /// File path where the node is defined (relative to repo root)
248    pub file_path: PathBuf,
249    /// Starting line number (1-based)
250    pub start_line: u32,
251    /// Starting column (1-based)
252    pub start_column: u32,
253    /// Ending line number (1-based)
254    pub end_line: u32,
255    /// Ending column (1-based)
256    pub end_column: u32,
257    /// Whether this match represents a static member.
258    pub is_static: bool,
259    /// Optional signature
260    pub signature: Option<String>,
261    /// Optional documentation
262    pub doc: Option<String>,
263}
264
265/// A query match tagged with repository metadata for workspace-level queries.
266#[derive(Debug, Clone)]
267pub struct NodeWithRepo {
268    /// The match info from the query.
269    pub match_info: MatchInfo,
270    /// The name of the repository containing this node.
271    pub repo_name: String,
272    /// The unique identifier of the repository containing this node.
273    pub repo_id: super::registry::WorkspaceRepoId,
274    /// Absolute path to the repository containing this node.
275    pub repo_path: PathBuf,
276}
277
278/// Aggregated statistics for a workspace.
279#[derive(Debug, Clone)]
280pub struct WorkspaceStats {
281    /// Total number of repositories in the workspace.
282    pub total_repos: usize,
283    /// Number of repositories that have been indexed.
284    pub indexed_repos: usize,
285    /// Total symbol count across all indexed repositories.
286    pub total_symbols: u64,
287}