Skip to main content

codemem_storage/
cross_repo.rs

1//! Cross-repo linking: package registry, unresolved references, and API endpoints.
2
3use crate::{MapStorageErr, Storage};
4use codemem_core::{CodememError, Edge};
5use rusqlite::params;
6
7/// A row in the `package_registry` table.
8#[derive(Debug, Clone)]
9pub struct PackageRegistryEntry {
10    pub package_name: String,
11    pub namespace: String,
12    pub version: String,
13    pub manifest: String,
14}
15
16/// A row in the `unresolved_refs` table.
17#[derive(Debug, Clone)]
18pub struct UnresolvedRefEntry {
19    pub id: String,
20    pub namespace: String,
21    pub source_node: String,
22    pub target_name: String,
23    pub package_hint: Option<String>,
24    pub ref_kind: String,
25    pub file_path: Option<String>,
26    pub line: Option<i64>,
27    pub created_at: i64,
28}
29
30/// A row in the `api_endpoints` table.
31#[derive(Debug, Clone)]
32pub struct ApiEndpointEntry {
33    pub id: String,
34    pub namespace: String,
35    pub method: Option<String>,
36    pub path: String,
37    pub handler: Option<String>,
38    pub schema: String,
39}
40
41/// A row in the `api_client_calls` table.
42#[derive(Debug, Clone)]
43pub struct ApiClientCallEntry {
44    pub id: String,
45    pub namespace: String,
46    pub method: Option<String>,
47    pub target: String,
48    pub caller: String,
49    pub library: String,
50}
51
52impl Storage {
53    // ── Package Registry ─────────────────────────────────────────────────
54
55    /// Insert or update a package registry entry.
56    pub fn upsert_package_registry(
57        &self,
58        package_name: &str,
59        namespace: &str,
60        version: &str,
61        manifest: &str,
62    ) -> Result<(), CodememError> {
63        let conn = self.conn()?;
64        conn.execute(
65            "INSERT OR REPLACE INTO package_registry (package_name, namespace, version, manifest)
66             VALUES (?1, ?2, ?3, ?4)",
67            params![package_name, namespace, version, manifest],
68        )
69        .storage_err()?;
70        Ok(())
71    }
72
73    /// Get all packages registered in a namespace.
74    pub fn get_packages_for_namespace(
75        &self,
76        namespace: &str,
77    ) -> Result<Vec<PackageRegistryEntry>, CodememError> {
78        let conn = self.conn()?;
79        let mut stmt = conn
80            .prepare(
81                "SELECT package_name, namespace, version, manifest
82                 FROM package_registry WHERE namespace = ?1",
83            )
84            .storage_err()?;
85        let rows = stmt
86            .query_map(params![namespace], |row| {
87                Ok(PackageRegistryEntry {
88                    package_name: row.get(0)?,
89                    namespace: row.get(1)?,
90                    version: row.get(2)?,
91                    manifest: row.get(3)?,
92                })
93            })
94            .storage_err()?;
95        let mut entries = Vec::new();
96        for row in rows {
97            entries.push(row.storage_err()?);
98        }
99        Ok(entries)
100    }
101
102    /// Find all namespaces that provide a given package.
103    pub fn find_namespace_for_package(
104        &self,
105        package_name: &str,
106    ) -> Result<Vec<PackageRegistryEntry>, CodememError> {
107        let conn = self.conn()?;
108        let mut stmt = conn
109            .prepare(
110                "SELECT package_name, namespace, version, manifest
111                 FROM package_registry WHERE package_name = ?1",
112            )
113            .storage_err()?;
114        let rows = stmt
115            .query_map(params![package_name], |row| {
116                Ok(PackageRegistryEntry {
117                    package_name: row.get(0)?,
118                    namespace: row.get(1)?,
119                    version: row.get(2)?,
120                    manifest: row.get(3)?,
121                })
122            })
123            .storage_err()?;
124        let mut entries = Vec::new();
125        for row in rows {
126            entries.push(row.storage_err()?);
127        }
128        Ok(entries)
129    }
130
131    /// Delete all package registry entries for a namespace. Returns count deleted.
132    pub fn delete_package_registry_for_namespace(
133        &self,
134        namespace: &str,
135    ) -> Result<usize, CodememError> {
136        let conn = self.conn()?;
137        let deleted = conn
138            .execute(
139                "DELETE FROM package_registry WHERE namespace = ?1",
140                params![namespace],
141            )
142            .storage_err()?;
143        Ok(deleted)
144    }
145
146    // ── Unresolved Refs ──────────────────────────────────────────────────
147
148    /// Insert a single unresolved reference.
149    pub fn insert_unresolved_ref(&self, entry: &UnresolvedRefEntry) -> Result<(), CodememError> {
150        let conn = self.conn()?;
151        conn.execute(
152            "INSERT OR REPLACE INTO unresolved_refs
153             (id, namespace, source_node, target_name, package_hint, ref_kind, file_path, line, created_at)
154             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
155            params![
156                entry.id,
157                entry.namespace,
158                entry.source_node,
159                entry.target_name,
160                entry.package_hint,
161                entry.ref_kind,
162                entry.file_path,
163                entry.line,
164                entry.created_at,
165            ],
166        )
167        .storage_err()?;
168        Ok(())
169    }
170
171    /// Batch insert unresolved references, respecting SQLite 999-param limit.
172    pub fn insert_unresolved_refs_batch(
173        &self,
174        refs: &[UnresolvedRefEntry],
175    ) -> Result<(), CodememError> {
176        if refs.is_empty() {
177            return Ok(());
178        }
179        let conn = self.conn()?;
180        let tx = conn.unchecked_transaction().storage_err()?;
181
182        const COLS: usize = 9;
183        const BATCH: usize = 999 / COLS; // 111
184
185        for chunk in refs.chunks(BATCH) {
186            let mut placeholders = String::new();
187            for (r, _) in chunk.iter().enumerate() {
188                if r > 0 {
189                    placeholders.push(',');
190                }
191                placeholders.push('(');
192                for c in 0..COLS {
193                    if c > 0 {
194                        placeholders.push(',');
195                    }
196                    placeholders.push('?');
197                    placeholders.push_str(&(r * COLS + c + 1).to_string());
198                }
199                placeholders.push(')');
200            }
201
202            let sql = format!(
203                "INSERT OR REPLACE INTO unresolved_refs
204                 (id, namespace, source_node, target_name, package_hint, ref_kind, file_path, line, created_at)
205                 VALUES {placeholders}"
206            );
207
208            let mut param_values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
209            for entry in chunk {
210                param_values.push(Box::new(entry.id.clone()));
211                param_values.push(Box::new(entry.namespace.clone()));
212                param_values.push(Box::new(entry.source_node.clone()));
213                param_values.push(Box::new(entry.target_name.clone()));
214                param_values.push(Box::new(entry.package_hint.clone()));
215                param_values.push(Box::new(entry.ref_kind.clone()));
216                param_values.push(Box::new(entry.file_path.clone()));
217                param_values.push(Box::new(entry.line));
218                param_values.push(Box::new(entry.created_at));
219            }
220            let param_refs: Vec<&dyn rusqlite::types::ToSql> =
221                param_values.iter().map(|p| p.as_ref()).collect();
222
223            tx.execute(&sql, param_refs.as_slice()).storage_err()?;
224        }
225
226        tx.commit().storage_err()?;
227        Ok(())
228    }
229
230    /// Get all unresolved references for a namespace.
231    pub fn get_unresolved_refs_for_namespace(
232        &self,
233        namespace: &str,
234    ) -> Result<Vec<UnresolvedRefEntry>, CodememError> {
235        let conn = self.conn()?;
236        let mut stmt = conn
237            .prepare(
238                "SELECT id, namespace, source_node, target_name, package_hint, ref_kind, file_path, line, created_at
239                 FROM unresolved_refs WHERE namespace = ?1",
240            )
241            .storage_err()?;
242        let rows = stmt
243            .query_map(params![namespace], |row| {
244                Ok(UnresolvedRefEntry {
245                    id: row.get(0)?,
246                    namespace: row.get(1)?,
247                    source_node: row.get(2)?,
248                    target_name: row.get(3)?,
249                    package_hint: row.get(4)?,
250                    ref_kind: row.get(5)?,
251                    file_path: row.get(6)?,
252                    line: row.get(7)?,
253                    created_at: row.get(8)?,
254                })
255            })
256            .storage_err()?;
257        let mut entries = Vec::new();
258        for row in rows {
259            entries.push(row.storage_err()?);
260        }
261        Ok(entries)
262    }
263
264    /// Get all unresolved references with a given package hint.
265    pub fn get_unresolved_refs_for_package_hint(
266        &self,
267        package_hint: &str,
268    ) -> Result<Vec<UnresolvedRefEntry>, CodememError> {
269        let conn = self.conn()?;
270        let mut stmt = conn
271            .prepare(
272                "SELECT id, namespace, source_node, target_name, package_hint, ref_kind, file_path, line, created_at
273                 FROM unresolved_refs WHERE package_hint = ?1",
274            )
275            .storage_err()?;
276        let rows = stmt
277            .query_map(params![package_hint], |row| {
278                Ok(UnresolvedRefEntry {
279                    id: row.get(0)?,
280                    namespace: row.get(1)?,
281                    source_node: row.get(2)?,
282                    target_name: row.get(3)?,
283                    package_hint: row.get(4)?,
284                    ref_kind: row.get(5)?,
285                    file_path: row.get(6)?,
286                    line: row.get(7)?,
287                    created_at: row.get(8)?,
288                })
289            })
290            .storage_err()?;
291        let mut entries = Vec::new();
292        for row in rows {
293            entries.push(row.storage_err()?);
294        }
295        Ok(entries)
296    }
297
298    /// Delete a single unresolved reference by ID.
299    pub fn delete_unresolved_ref(&self, id: &str) -> Result<(), CodememError> {
300        let conn = self.conn()?;
301        conn.execute("DELETE FROM unresolved_refs WHERE id = ?1", params![id])
302            .storage_err()?;
303        Ok(())
304    }
305
306    /// Batch delete unresolved references by IDs.
307    pub fn delete_unresolved_refs_batch(&self, ids: &[String]) -> Result<(), CodememError> {
308        if ids.is_empty() {
309            return Ok(());
310        }
311        let conn = self.conn()?;
312        let tx = conn.unchecked_transaction().storage_err()?;
313
314        // 1 param per id, batch by 999
315        for chunk in ids.chunks(999) {
316            let placeholders: Vec<String> = (1..=chunk.len()).map(|i| format!("?{i}")).collect();
317            let sql = format!(
318                "DELETE FROM unresolved_refs WHERE id IN ({})",
319                placeholders.join(",")
320            );
321            let param_refs: Vec<&dyn rusqlite::types::ToSql> = chunk
322                .iter()
323                .map(|s| s as &dyn rusqlite::types::ToSql)
324                .collect();
325            tx.execute(&sql, param_refs.as_slice()).storage_err()?;
326        }
327
328        tx.commit().storage_err()?;
329        Ok(())
330    }
331
332    /// Delete all unresolved references for a namespace. Returns count deleted.
333    pub fn delete_unresolved_refs_for_namespace(
334        &self,
335        namespace: &str,
336    ) -> Result<usize, CodememError> {
337        let conn = self.conn()?;
338        let deleted = conn
339            .execute(
340                "DELETE FROM unresolved_refs WHERE namespace = ?1",
341                params![namespace],
342            )
343            .storage_err()?;
344        Ok(deleted)
345    }
346
347    // ── API Endpoints ────────────────────────────────────────────────────
348
349    /// Insert or update an API endpoint.
350    pub fn upsert_api_endpoint(&self, endpoint: &ApiEndpointEntry) -> Result<(), CodememError> {
351        let conn = self.conn()?;
352        conn.execute(
353            "INSERT OR REPLACE INTO api_endpoints (id, namespace, method, path, handler, schema)
354             VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
355            params![
356                endpoint.id,
357                endpoint.namespace,
358                endpoint.method,
359                endpoint.path,
360                endpoint.handler,
361                endpoint.schema,
362            ],
363        )
364        .storage_err()?;
365        Ok(())
366    }
367
368    /// Get all API endpoints for a namespace.
369    pub fn get_api_endpoints_for_namespace(
370        &self,
371        namespace: &str,
372    ) -> Result<Vec<ApiEndpointEntry>, CodememError> {
373        let conn = self.conn()?;
374        let mut stmt = conn
375            .prepare(
376                "SELECT id, namespace, method, path, handler, schema
377                 FROM api_endpoints WHERE namespace = ?1",
378            )
379            .storage_err()?;
380        let rows = stmt
381            .query_map(params![namespace], |row| {
382                Ok(ApiEndpointEntry {
383                    id: row.get(0)?,
384                    namespace: row.get(1)?,
385                    method: row.get(2)?,
386                    path: row.get(3)?,
387                    handler: row.get(4)?,
388                    schema: row.get(5)?,
389                })
390            })
391            .storage_err()?;
392        let mut entries = Vec::new();
393        for row in rows {
394            entries.push(row.storage_err()?);
395        }
396        Ok(entries)
397    }
398
399    /// Get all API endpoints with an exact path match.
400    pub fn get_api_endpoints_for_path(
401        &self,
402        path: &str,
403    ) -> Result<Vec<ApiEndpointEntry>, CodememError> {
404        let conn = self.conn()?;
405        let mut stmt = conn
406            .prepare(
407                "SELECT id, namespace, method, path, handler, schema
408                 FROM api_endpoints WHERE path = ?1",
409            )
410            .storage_err()?;
411        let rows = stmt
412            .query_map(params![path], |row| {
413                Ok(ApiEndpointEntry {
414                    id: row.get(0)?,
415                    namespace: row.get(1)?,
416                    method: row.get(2)?,
417                    path: row.get(3)?,
418                    handler: row.get(4)?,
419                    schema: row.get(5)?,
420                })
421            })
422            .storage_err()?;
423        let mut entries = Vec::new();
424        for row in rows {
425            entries.push(row.storage_err()?);
426        }
427        Ok(entries)
428    }
429
430    /// Find API endpoints whose path matches a LIKE pattern.
431    pub fn find_api_endpoints_by_path_pattern(
432        &self,
433        path_pattern: &str,
434    ) -> Result<Vec<ApiEndpointEntry>, CodememError> {
435        let conn = self.conn()?;
436        let mut stmt = conn
437            .prepare(
438                "SELECT id, namespace, method, path, handler, schema
439                 FROM api_endpoints WHERE path LIKE ?1",
440            )
441            .storage_err()?;
442        let rows = stmt
443            .query_map(params![path_pattern], |row| {
444                Ok(ApiEndpointEntry {
445                    id: row.get(0)?,
446                    namespace: row.get(1)?,
447                    method: row.get(2)?,
448                    path: row.get(3)?,
449                    handler: row.get(4)?,
450                    schema: row.get(5)?,
451                })
452            })
453            .storage_err()?;
454        let mut entries = Vec::new();
455        for row in rows {
456            entries.push(row.storage_err()?);
457        }
458        Ok(entries)
459    }
460
461    /// Delete all API endpoints for a namespace. Returns count deleted.
462    pub fn delete_api_endpoints_for_namespace(
463        &self,
464        namespace: &str,
465    ) -> Result<usize, CodememError> {
466        let conn = self.conn()?;
467        let deleted = conn
468            .execute(
469                "DELETE FROM api_endpoints WHERE namespace = ?1",
470                params![namespace],
471            )
472            .storage_err()?;
473        Ok(deleted)
474    }
475
476    // ── API Client Calls ─────────────────────────────────────────────────
477
478    /// Insert or update an API client call.
479    pub fn upsert_api_client_call(
480        &self,
481        id: &str,
482        namespace: &str,
483        method: Option<&str>,
484        target: &str,
485        caller: &str,
486        library: &str,
487    ) -> Result<(), CodememError> {
488        let conn = self.conn()?;
489        conn.execute(
490            "INSERT OR REPLACE INTO api_client_calls (id, namespace, method, target, caller, library)
491             VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
492            params![id, namespace, method, target, caller, library],
493        )
494        .storage_err()?;
495        Ok(())
496    }
497
498    /// Get all API client calls for a namespace.
499    pub fn get_api_client_calls_for_namespace(
500        &self,
501        namespace: &str,
502    ) -> Result<Vec<ApiClientCallEntry>, CodememError> {
503        let conn = self.conn()?;
504        let mut stmt = conn
505            .prepare(
506                "SELECT id, namespace, method, target, caller, library
507                 FROM api_client_calls WHERE namespace = ?1",
508            )
509            .storage_err()?;
510        let rows = stmt
511            .query_map(params![namespace], |row| {
512                Ok(ApiClientCallEntry {
513                    id: row.get(0)?,
514                    namespace: row.get(1)?,
515                    method: row.get(2)?,
516                    target: row.get(3)?,
517                    caller: row.get(4)?,
518                    library: row.get(5)?,
519                })
520            })
521            .storage_err()?;
522        let mut entries = Vec::new();
523        for row in rows {
524            entries.push(row.storage_err()?);
525        }
526        Ok(entries)
527    }
528
529    // ── Cross-namespace Edge Queries ─────────────────────────────────────
530
531    /// Get edges where at least one endpoint (src or dst) belongs to the given
532    /// namespace and the edge has `cross_namespace = true` in its properties.
533    /// This is semantically equivalent to `graph_edges_for_namespace_with_cross(ns, true)`
534    /// but additionally filters for edges explicitly marked as cross-namespace.
535    pub fn get_cross_namespace_edges(&self, namespace: &str) -> Result<Vec<Edge>, CodememError> {
536        // Delegate to the unified method and filter for cross_namespace property.
537        let all_edges = self.graph_edges_for_namespace_with_cross(namespace, true)?;
538        Ok(all_edges
539            .into_iter()
540            .filter(|e| {
541                e.properties
542                    .get("cross_namespace")
543                    .and_then(|v| v.as_bool())
544                    .unwrap_or(false)
545            })
546            .collect())
547    }
548}
549
550#[cfg(test)]
551#[path = "tests/cross_repo_tests.rs"]
552mod tests;