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