1pub mod paths;
12
13#[cfg(feature = "backend-sqlite")]
15pub mod sqlite_backend;
16#[cfg(feature = "backend-native-v3")]
17pub mod kv_backend;
18#[cfg(feature = "backend-geometric")]
19pub mod geometric;
20
21#[cfg(all(feature = "sqlite", not(feature = "backend-sqlite")))]
23pub mod sqlite_backend;
24#[cfg(all(feature = "native-v3", not(feature = "backend-native-v3")))]
25pub mod kv_backend;
26#[cfg(all(feature = "geometric", not(feature = "backend-geometric")))]
27pub mod geometric;
28
29use anyhow::{Context, Result};
30use rusqlite::{Connection, OptionalExtension, params};
31use std::path::Path;
32
33use sqlitegraph::{GraphBackend, GraphConfig, SnapshotId, open_graph};
35
36#[cfg(feature = "backend-sqlite")]
47pub use sqlite_backend::SqliteStorage;
48#[cfg(feature = "backend-native-v3")]
49pub use kv_backend::KvStorage;
50#[cfg(feature = "backend-geometric")]
51pub use geometric::GeometricStorage;
52
53#[allow(unused_imports)]
57pub use paths::{
58 PathCache,
59 store_paths,
60 get_cached_paths,
61 invalidate_function_paths,
62 update_function_paths_if_changed,
63};
64
65pub trait StorageTrait {
94 fn get_cfg_blocks(&self, function_id: i64) -> Result<Vec<CfgBlockData>>;
109
110 fn get_entity(&self, entity_id: i64) -> Option<sqlitegraph::GraphEntity>;
123
124 fn get_cached_paths(&self, _function_id: i64) -> Result<Option<Vec<crate::cfg::Path>>> {
139 Ok(None) }
141}
142
143#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
149pub struct CfgBlockData {
150 pub id: i64,
152 pub kind: String,
154 pub terminator: String,
156 pub byte_start: u64,
158 pub byte_end: u64,
160 pub start_line: u64,
162 pub start_col: u64,
164 pub end_line: u64,
166 pub end_col: u64,
168}
169
170#[derive(Debug)]
177#[allow(clippy::large_enum_variant)] pub enum Backend {
179 #[cfg(feature = "backend-sqlite")]
181 Sqlite(SqliteStorage),
182 #[cfg(feature = "backend-native-v3")]
184 NativeV3(KvStorage),
185 #[cfg(feature = "backend-geometric")]
187 Geometric(GeometricStorage),
188}
189
190impl Backend {
191 pub fn detect_and_open(db_path: &Path) -> Result<Self> {
214 use magellan::migrate_backend_cmd::detect_backend_format;
215
216 #[cfg(feature = "backend-geometric")]
218 let is_geo = db_path.extension().and_then(|e| e.to_str()) == Some("geo");
219
220 #[cfg(feature = "backend-geometric")]
221 {
222 if is_geo {
223 return GeometricStorage::open(db_path).map(Backend::Geometric);
224 }
225 }
226
227 let sqlite_detected = detect_backend_format(db_path).is_ok();
229
230 #[cfg(feature = "backend-sqlite")]
231 {
232 if sqlite_detected {
233 return SqliteStorage::open(db_path).map(Backend::Sqlite);
234 } else {
235 return Err(anyhow::anyhow!("Native-V3 backend detected but SQLite feature enabled"));
236 }
237 }
238
239 #[cfg(feature = "backend-native-v3")]
240 {
241 if sqlite_detected {
242 return Err(anyhow::anyhow!("SQLite backend detected but Native-V3 feature enabled"));
243 } else {
244 return KvStorage::open(db_path).map(Backend::NativeV3);
245 }
246 }
247
248 #[cfg(not(any(feature = "backend-sqlite", feature = "backend-native-v3", feature = "backend-geometric")))]
249 {
250 Err(anyhow::anyhow!("No storage backend feature enabled"))
251 }
252 }
253
254 pub fn is_geometric(&self) -> bool {
256 match self {
257 #[cfg(feature = "backend-geometric")]
258 Backend::Geometric(_) => true,
259 _ => false,
260 }
261 }
262
263 pub fn is_sqlite(&self) -> bool {
265 match self {
266 #[cfg(feature = "backend-sqlite")]
267 Backend::Sqlite(_) => true,
268 #[cfg(not(feature = "backend-sqlite"))]
269 _ => false,
270 }
271 }
272
273 pub fn is_native_v3(&self) -> bool {
275 match self {
276 #[cfg(feature = "backend-native-v3")]
277 Backend::NativeV3(_) => true,
278 _ => false,
279 }
280 }
281
282 pub fn get_cfg_blocks(&self, function_id: i64) -> Result<Vec<CfgBlockData>> {
284 match self {
285 #[cfg(feature = "backend-sqlite")]
286 Backend::Sqlite(s) => s.get_cfg_blocks(function_id),
287 #[cfg(feature = "backend-native-v3")]
288 Backend::NativeV3(k) => k.get_cfg_blocks(function_id),
289 #[cfg(feature = "backend-geometric")]
290 Backend::Geometric(g) => g.get_cfg_blocks(function_id),
291 #[allow(unreachable_patterns)]
292 _ => Err(anyhow::anyhow!("No storage backend available")),
293 }
294 }
295
296 pub fn get_entity(&self, entity_id: i64) -> Option<sqlitegraph::GraphEntity> {
298 match self {
299 #[cfg(feature = "backend-sqlite")]
300 Backend::Sqlite(s) => s.get_entity(entity_id),
301 #[cfg(feature = "backend-native-v3")]
302 Backend::NativeV3(k) => k.get_entity(entity_id),
303 #[cfg(feature = "backend-geometric")]
304 Backend::Geometric(g) => g.get_entity(entity_id),
305 #[allow(unreachable_patterns)]
306 _ => None,
307 }
308 }
309
310 pub fn get_cached_paths(&self, function_id: i64) -> Result<Option<Vec<crate::cfg::Path>>> {
312 match self {
313 #[cfg(feature = "backend-sqlite")]
314 Backend::Sqlite(s) => s.get_cached_paths(function_id),
315 #[cfg(feature = "backend-native-v3")]
316 Backend::NativeV3(k) => k.get_cached_paths(function_id),
317 #[cfg(feature = "backend-geometric")]
318 Backend::Geometric(g) => g.get_cached_paths(function_id),
319 #[allow(unreachable_patterns)]
320 _ => Err(anyhow::anyhow!("No storage backend available")),
321 }
322 }
323}
324
325impl StorageTrait for Backend {
327 fn get_cfg_blocks(&self, function_id: i64) -> Result<Vec<CfgBlockData>> {
328 self.get_cfg_blocks(function_id)
329 }
330
331 fn get_entity(&self, entity_id: i64) -> Option<sqlitegraph::GraphEntity> {
332 self.get_entity(entity_id)
333 }
334
335 fn get_cached_paths(&self, function_id: i64) -> Result<Option<Vec<crate::cfg::Path>>> {
336 self.get_cached_paths(function_id)
337 }
338}
339
340#[derive(Debug, Clone, Copy, PartialEq, Eq)]
345pub enum BackendFormat {
346 SQLite,
348 NativeV3,
350 Geometric,
352 Unknown,
354}
355
356impl BackendFormat {
357 pub fn detect(path: &Path) -> Result<Self> {
365 if !path.exists() {
366 return Ok(BackendFormat::Unknown);
367 }
368
369 if path.extension().and_then(|e| e.to_str()) == Some("geo") {
371 return Ok(BackendFormat::Geometric);
372 }
373
374 let mut file = std::fs::File::open(path)?;
375 let mut header = [0u8; 16];
376 let bytes_read = std::io::Read::read(&mut file, &mut header)?;
377
378 if bytes_read < header.len() {
379 return Ok(BackendFormat::Unknown);
380 }
381
382 Ok(if &header[..15] == b"SQLite format 3" {
384 BackendFormat::SQLite
385 } else {
386 BackendFormat::NativeV3
388 })
389 }
390}
391
392pub const MIRAGE_SCHEMA_VERSION: i32 = 1;
394
395pub const MIN_MAGELLAN_SCHEMA_VERSION: i32 = 7;
398
399pub const TEST_MAGELLAN_SCHEMA_VERSION: i32 = MIN_MAGELLAN_SCHEMA_VERSION;
401
402pub const REQUIRED_MAGELLAN_SCHEMA_VERSION: i32 = TEST_MAGELLAN_SCHEMA_VERSION;
404
405pub const REQUIRED_SQLITEGRAPH_SCHEMA_VERSION: i32 = 3;
407
408pub struct MirageDb {
413 storage: Backend,
416
417 graph_backend: Box<dyn GraphBackend>,
420
421 snapshot_id: SnapshotId,
423
424 #[cfg(feature = "backend-sqlite")]
427 conn: Option<Connection>,
428}
429
430impl std::fmt::Debug for MirageDb {
431 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
432 f.debug_struct("MirageDb")
433 .field("snapshot_id", &self.snapshot_id)
434 .field("storage", &self.storage)
435 .field("graph_backend", &"<GraphBackend>")
436 .finish()
437 }
438}
439
440impl MirageDb {
441 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
450 let path = path.as_ref();
451 if !path.exists() {
452 anyhow::bail!("Database not found: {}", path.display());
453 }
454
455 let storage = Backend::detect_and_open(path)
457 .context("Failed to open storage backend")?;
458
459 let detected_backend = BackendFormat::detect(path)
461 .context("Failed to detect backend format")?;
462
463 #[cfg(feature = "backend-sqlite")]
465 {
466 if detected_backend == BackendFormat::NativeV3 {
467 anyhow::bail!(
468 "Database file '{}' uses native-v3 format, but this binary was built \
469 with SQLite backend. Rebuild with: cargo build --release --no-default-features --features native-v3",
470 path.display()
471 );
472 }
473 }
474
475 #[cfg(feature = "backend-native-v3")]
476 {
477 if detected_backend == BackendFormat::SQLite {
478 anyhow::bail!(
479 "Database file '{}' uses SQLite format, but this binary was built \
480 with native-v3 backend. Rebuild with: cargo build --release",
481 path.display()
482 );
483 }
484 }
485
486 #[cfg(feature = "backend-geometric")]
488 if detected_backend == BackendFormat::Geometric {
489 let snapshot_id = SnapshotId::current();
490
491 let graph_backend = create_geometric_stub_backend();
495
496 #[cfg(feature = "backend-sqlite")]
497 let conn = None;
498
499 return Ok(Self {
500 storage,
501 graph_backend,
502 snapshot_id,
503 #[cfg(feature = "backend-sqlite")]
504 conn,
505 });
506 }
507
508 let cfg = match detected_backend {
510 BackendFormat::SQLite => GraphConfig::sqlite(),
511 BackendFormat::NativeV3 => GraphConfig::native(),
512 BackendFormat::Geometric => {
513 GraphConfig::native()
515 }
516 BackendFormat::Unknown => {
517 anyhow::bail!(
518 "Unknown database format: {}. Cannot determine backend.",
519 path.display()
520 );
521 }
522 };
523
524 let graph_backend = open_graph(path, &cfg)
526 .context("Failed to open graph database")?;
527
528 let snapshot_id = SnapshotId::current();
529
530 #[cfg(feature = "backend-sqlite")]
532 let conn = {
533 let mut conn = Connection::open(path)
534 .context("Failed to open SQLite connection")?;
535 Self::validate_schema_sqlite(&mut conn, path)?;
536 Some(conn)
537 };
538
539 #[cfg(feature = "backend-native-v3")]
541 {
542 }
545
546 Ok(Self {
547 storage,
548 graph_backend,
549 snapshot_id,
550 #[cfg(feature = "backend-sqlite")]
551 conn,
552 })
553 }
554
555 #[cfg(feature = "backend-sqlite")]
557 fn validate_schema_sqlite(conn: &mut Connection, _path: &Path) -> Result<()> {
558 let mirage_meta_exists: bool = conn.query_row(
560 "SELECT 1 FROM sqlite_master WHERE type='table' AND name='mirage_meta'",
561 [],
562 |row| row.get(0),
563 ).optional()?.unwrap_or(0) == 1;
564
565 let mirage_version: i32 = if mirage_meta_exists {
567 conn.query_row(
568 "SELECT mirage_schema_version FROM mirage_meta WHERE id = 1",
569 [],
570 |row| row.get(0),
571 ).optional()?.flatten().unwrap_or(0)
572 } else {
573 0
574 };
575
576 if mirage_version > MIRAGE_SCHEMA_VERSION {
577 anyhow::bail!(
578 "Database schema version {} is newer than supported version {}.
579 Please update Mirage.",
580 mirage_version, MIRAGE_SCHEMA_VERSION
581 );
582 }
583
584 let magellan_version: i32 = conn.query_row(
586 "SELECT magellan_schema_version FROM magellan_meta WHERE id = 1",
587 [],
588 |row| row.get(0),
589 ).optional()?.flatten().unwrap_or(0);
590
591 if magellan_version < MIN_MAGELLAN_SCHEMA_VERSION {
592 anyhow::bail!(
593 "Magellan schema version {} is too old (minimum {}). \
594 Please update Magellan and run 'magellan watch' to rebuild CFGs.",
595 magellan_version, MIN_MAGELLAN_SCHEMA_VERSION
596 );
597 }
598
599 let cfg_blocks_exists: bool = conn.query_row(
601 "SELECT 1 FROM sqlite_master WHERE type='table' AND name='cfg_blocks'",
602 [],
603 |row| row.get(0),
604 ).optional()?.unwrap_or(0) == 1;
605
606 if !cfg_blocks_exists {
607 anyhow::bail!(
608 "CFG blocks table not found. Magellan schema v7+ required. \
609 Run 'magellan watch' to build CFGs."
610 );
611 }
612
613 if !mirage_meta_exists {
616 create_schema(conn, magellan_version)?;
617 } else if mirage_version < MIRAGE_SCHEMA_VERSION {
618 migrate_schema(conn)?;
619 }
620
621 Ok(())
622 }
623
624 #[cfg(feature = "backend-sqlite")]
630 pub fn conn(&self) -> Result<&Connection, anyhow::Error> {
631 self.conn.as_ref().ok_or_else(|| {
632 anyhow::anyhow!(
633 "Direct Connection access deprecated. Use storage() for CFG queries or backend() for entity queries."
634 )
635 })
636 }
637
638 #[cfg(feature = "backend-sqlite")]
644 pub fn conn_mut(&mut self) -> Result<&mut Connection, anyhow::Error> {
645 self.conn.as_mut().ok_or_else(|| {
646 anyhow::anyhow!(
647 "Direct Connection access deprecated. Use storage() for CFG queries or backend() for entity queries."
648 )
649 })
650 }
651
652 #[cfg(feature = "backend-native-v3")]
658 pub fn conn(&self) -> Result<&Connection, anyhow::Error> {
659 Err(anyhow::anyhow!(
660 "Direct Connection access deprecated. Use storage() for CFG queries or backend() for entity queries."
661 ))
662 }
663
664 #[cfg(feature = "backend-native-v3")]
670 pub fn conn_mut(&mut self) -> Result<&mut Connection, anyhow::Error> {
671 Err(anyhow::anyhow!(
672 "Direct Connection access deprecated. Use storage() for CFG queries or backend() for entity queries."
673 ))
674 }
675
676 pub fn storage(&self) -> &Backend {
683 &self.storage
684 }
685
686 pub fn backend(&self) -> &dyn GraphBackend {
692 self.graph_backend.as_ref()
693 }
694
695 #[cfg(feature = "backend-sqlite")]
700 pub fn is_sqlite(&self) -> bool {
701 self.conn.is_some()
702 }
703
704 #[cfg(feature = "backend-native-v3")]
708 pub fn is_sqlite(&self) -> bool {
709 false
710 }
711}
712
713#[cfg(feature = "backend-geometric")]
723fn create_geometric_stub_backend() -> Box<dyn GraphBackend> {
724 use sqlitegraph::{GraphBackend, GraphEntity, SqliteGraphError, SnapshotId};
725 use sqlitegraph::backend::{NodeSpec, EdgeSpec, NeighborQuery, BackendDirection};
726 use sqlitegraph::pattern::{PatternQuery, PatternMatch};
727 use sqlitegraph::multi_hop::ChainStep;
728
729 struct GeometricStubBackend;
732
733 impl GraphBackend for GeometricStubBackend {
734 fn insert_node(&self, _node: NodeSpec) -> Result<i64, SqliteGraphError> {
735 Err(SqliteGraphError::unsupported(
736 "GraphBackend operations not supported for geometric backend. Use GeometricBackend methods directly."
737 ))
738 }
739
740 fn insert_edge(&self, _edge: EdgeSpec) -> Result<i64, SqliteGraphError> {
741 Err(SqliteGraphError::unsupported(
742 "GraphBackend operations not supported for geometric backend. Use GeometricBackend methods directly."
743 ))
744 }
745
746 fn update_node(&self, _node_id: i64, _node: NodeSpec) -> Result<i64, SqliteGraphError> {
747 Err(SqliteGraphError::unsupported(
748 "GraphBackend operations not supported for geometric backend. Use GeometricBackend methods directly."
749 ))
750 }
751
752 fn delete_entity(&self, _id: i64) -> Result<(), SqliteGraphError> {
753 Err(SqliteGraphError::unsupported(
754 "GraphBackend operations not supported for geometric backend. Use GeometricBackend methods directly."
755 ))
756 }
757
758 fn entity_ids(&self) -> Result<Vec<i64>, SqliteGraphError> {
759 Ok(vec![])
761 }
762
763 fn get_node(&self, _snapshot_id: SnapshotId, _id: i64) -> Result<GraphEntity, SqliteGraphError> {
764 Err(SqliteGraphError::unsupported(
765 "GraphBackend operations not supported for geometric backend. Use GeometricBackend methods directly."
766 ))
767 }
768
769 fn neighbors(
770 &self,
771 _snapshot_id: SnapshotId,
772 _node: i64,
773 _query: NeighborQuery,
774 ) -> Result<Vec<i64>, SqliteGraphError> {
775 Ok(vec![])
777 }
778
779 fn bfs(
780 &self,
781 _snapshot_id: SnapshotId,
782 _start: i64,
783 _depth: u32,
784 ) -> Result<Vec<i64>, SqliteGraphError> {
785 Ok(vec![])
787 }
788
789 fn shortest_path(
790 &self,
791 _snapshot_id: SnapshotId,
792 _start: i64,
793 _end: i64,
794 ) -> Result<Option<Vec<i64>>, SqliteGraphError> {
795 Ok(None)
796 }
797
798 fn node_degree(
799 &self,
800 _snapshot_id: SnapshotId,
801 _node: i64,
802 ) -> Result<(usize, usize), SqliteGraphError> {
803 Ok((0, 0))
804 }
805
806 fn k_hop(
807 &self,
808 _snapshot_id: SnapshotId,
809 _start: i64,
810 _depth: u32,
811 _direction: BackendDirection,
812 ) -> Result<Vec<i64>, SqliteGraphError> {
813 Ok(vec![])
814 }
815
816 fn k_hop_filtered(
817 &self,
818 _snapshot_id: SnapshotId,
819 _start: i64,
820 _depth: u32,
821 _direction: BackendDirection,
822 _allowed_edge_types: &[&str],
823 ) -> Result<Vec<i64>, SqliteGraphError> {
824 Ok(vec![])
825 }
826
827 fn chain_query(
828 &self,
829 _snapshot_id: SnapshotId,
830 _start: i64,
831 _chain: &[ChainStep],
832 ) -> Result<Vec<i64>, SqliteGraphError> {
833 Ok(vec![])
834 }
835
836 fn pattern_search(
837 &self,
838 _snapshot_id: SnapshotId,
839 _start: i64,
840 _pattern: &PatternQuery,
841 ) -> Result<Vec<PatternMatch>, SqliteGraphError> {
842 Ok(vec![])
843 }
844
845 fn checkpoint(&self) -> Result<(), SqliteGraphError> {
846 Ok(())
847 }
848
849 fn flush(&self) -> Result<(), SqliteGraphError> {
850 Ok(())
851 }
852
853 fn backup(&self, _backup_dir: &std::path::Path) -> Result<sqlitegraph::backend::BackupResult, SqliteGraphError> {
854 Err(SqliteGraphError::unsupported(
855 "Backup not supported for geometric backend"
856 ))
857 }
858
859 fn snapshot_export(
860 &self,
861 _export_dir: &std::path::Path,
862 ) -> Result<sqlitegraph::backend::SnapshotMetadata, SqliteGraphError> {
863 Err(SqliteGraphError::unsupported(
864 "Snapshot export not supported for geometric backend"
865 ))
866 }
867
868 fn snapshot_import(
869 &self,
870 _import_dir: &std::path::Path,
871 ) -> Result<sqlitegraph::backend::ImportMetadata, SqliteGraphError> {
872 Err(SqliteGraphError::unsupported(
873 "Snapshot import not supported for geometric backend"
874 ))
875 }
876
877 fn query_nodes_by_kind(
878 &self,
879 _snapshot_id: SnapshotId,
880 _kind: &str,
881 ) -> Result<Vec<i64>, SqliteGraphError> {
882 Ok(vec![])
883 }
884
885 fn query_nodes_by_name_pattern(
886 &self,
887 _snapshot_id: SnapshotId,
888 _pattern: &str,
889 ) -> Result<Vec<i64>, SqliteGraphError> {
890 Ok(vec![])
891 }
892 }
893
894 Box::new(GeometricStubBackend)
895}
896
897struct Migration {
899 version: i32,
900 description: &'static str,
901 up: fn(&mut Connection) -> Result<()>,
902}
903
904fn migrations() -> Vec<Migration> {
906 vec![]
908}
909
910pub fn migrate_schema(conn: &mut Connection) -> Result<()> {
912 let current_version: i32 = conn.query_row(
913 "SELECT mirage_schema_version FROM mirage_meta WHERE id = 1",
914 [],
915 |row| row.get(0),
916 ).unwrap_or(0);
917
918 if current_version >= MIRAGE_SCHEMA_VERSION {
919 return Ok(());
921 }
922
923 let pending: Vec<_> = migrations()
925 .into_iter()
926 .filter(|m| m.version > current_version && m.version <= MIRAGE_SCHEMA_VERSION)
927 .collect();
928
929 for migration in pending {
930 (migration.up)(conn)
932 .with_context(|| format!("Failed to run migration v{}: {}", migration.version, migration.description))?;
933
934 conn.execute(
936 "UPDATE mirage_meta SET mirage_schema_version = ? WHERE id = 1",
937 params![migration.version],
938 )?;
939 }
940
941 if current_version < MIRAGE_SCHEMA_VERSION {
943 conn.execute(
944 "UPDATE mirage_meta SET mirage_schema_version = ? WHERE id = 1",
945 params![MIRAGE_SCHEMA_VERSION],
946 )?;
947 }
948
949 Ok(())
950}
951
952pub fn create_schema(conn: &mut Connection, _magellan_schema_version: i32) -> Result<()> {
957 conn.execute(
959 "CREATE TABLE IF NOT EXISTS mirage_meta (
960 id INTEGER PRIMARY KEY CHECK (id = 1),
961 mirage_schema_version INTEGER NOT NULL,
962 magellan_schema_version INTEGER NOT NULL,
963 compiler_version TEXT,
964 created_at INTEGER NOT NULL
965 )",
966 [],
967 )?;
968
969 conn.execute(
975 "CREATE TABLE IF NOT EXISTS cfg_blocks (
976 id INTEGER PRIMARY KEY AUTOINCREMENT,
977 function_id INTEGER NOT NULL,
978 kind TEXT NOT NULL,
979 terminator TEXT NOT NULL,
980 byte_start INTEGER,
981 byte_end INTEGER,
982 start_line INTEGER,
983 start_col INTEGER,
984 end_line INTEGER,
985 end_col INTEGER,
986 FOREIGN KEY (function_id) REFERENCES graph_entities(id)
987 )",
988 [],
989 )?;
990
991 conn.execute(
992 "CREATE INDEX IF NOT EXISTS idx_cfg_blocks_function ON cfg_blocks(function_id)",
993 [],
994 )?;
995
996 conn.execute(
999 "CREATE TABLE IF NOT EXISTS cfg_edges (
1000 from_id INTEGER NOT NULL,
1001 to_id INTEGER NOT NULL,
1002 edge_type TEXT NOT NULL,
1003 PRIMARY KEY (from_id, to_id, edge_type),
1004 FOREIGN KEY (from_id) REFERENCES cfg_blocks(id),
1005 FOREIGN KEY (to_id) REFERENCES cfg_blocks(id)
1006 )",
1007 [],
1008 )?;
1009
1010 conn.execute("CREATE INDEX IF NOT EXISTS idx_cfg_edges_from ON cfg_edges(from_id)", [])?;
1011 conn.execute("CREATE INDEX IF NOT EXISTS idx_cfg_edges_to ON cfg_edges(to_id)", [])?;
1012
1013 conn.execute(
1015 "CREATE TABLE IF NOT EXISTS cfg_paths (
1016 path_id TEXT PRIMARY KEY,
1017 function_id INTEGER NOT NULL,
1018 path_kind TEXT NOT NULL,
1019 entry_block INTEGER NOT NULL,
1020 exit_block INTEGER NOT NULL,
1021 length INTEGER NOT NULL,
1022 created_at INTEGER NOT NULL,
1023 FOREIGN KEY (function_id) REFERENCES graph_entities(id)
1024 )",
1025 [],
1026 )?;
1027
1028 conn.execute("CREATE INDEX IF NOT EXISTS idx_cfg_paths_function ON cfg_paths(function_id)", [])?;
1029 conn.execute("CREATE INDEX IF NOT EXISTS idx_cfg_paths_kind ON cfg_paths(path_kind)", [])?;
1030
1031 conn.execute(
1033 "CREATE TABLE IF NOT EXISTS cfg_path_elements (
1034 path_id TEXT NOT NULL,
1035 sequence_order INTEGER NOT NULL,
1036 block_id INTEGER NOT NULL,
1037 PRIMARY KEY (path_id, sequence_order),
1038 FOREIGN KEY (path_id) REFERENCES cfg_paths(path_id)
1039 )",
1040 [],
1041 )?;
1042
1043 conn.execute("CREATE INDEX IF NOT EXISTS cfg_path_elements_block ON cfg_path_elements(block_id)", [])?;
1044
1045 conn.execute(
1047 "CREATE TABLE IF NOT EXISTS cfg_dominators (
1048 block_id INTEGER NOT NULL,
1049 dominator_id INTEGER NOT NULL,
1050 is_strict BOOLEAN NOT NULL,
1051 PRIMARY KEY (block_id, dominator_id, is_strict),
1052 FOREIGN KEY (block_id) REFERENCES cfg_blocks(id),
1053 FOREIGN KEY (dominator_id) REFERENCES cfg_blocks(id)
1054 )",
1055 [],
1056 )?;
1057
1058 conn.execute(
1060 "CREATE TABLE IF NOT EXISTS cfg_post_dominators (
1061 block_id INTEGER NOT NULL,
1062 post_dominator_id INTEGER NOT NULL,
1063 is_strict BOOLEAN NOT NULL,
1064 PRIMARY KEY (block_id, post_dominator_id, is_strict),
1065 FOREIGN KEY (block_id) REFERENCES cfg_blocks(id),
1066 FOREIGN KEY (post_dominator_id) REFERENCES cfg_blocks(id)
1067 )",
1068 [],
1069 )?;
1070
1071 let now = chrono::Utc::now().timestamp();
1073 conn.execute(
1074 "INSERT OR REPLACE INTO mirage_meta (id, mirage_schema_version, magellan_schema_version, created_at)
1075 VALUES (1, ?, ?, ?)",
1076 params![MIRAGE_SCHEMA_VERSION, REQUIRED_MAGELLAN_SCHEMA_VERSION, now],
1077 )?;
1078
1079 Ok(())
1080}
1081
1082#[derive(Debug, Clone, serde::Serialize)]
1084pub struct DatabaseStatus {
1085 pub cfg_blocks: i64,
1086 #[deprecated(note = "Edges are now computed in memory, not stored")]
1087 pub cfg_edges: i64,
1088 pub cfg_paths: i64,
1089 pub cfg_dominators: i64,
1090 pub mirage_schema_version: i32,
1091 pub magellan_schema_version: i32,
1092}
1093
1094impl MirageDb {
1095 #[cfg(feature = "backend-sqlite")]
1100 pub fn status(&self) -> Result<DatabaseStatus> {
1101 match self.conn.as_ref() {
1103 Some(conn) => {
1104 let cfg_blocks: i64 = conn.query_row(
1106 "SELECT COUNT(*) FROM cfg_blocks",
1107 [],
1108 |row| row.get(0),
1109 ).unwrap_or(0);
1110
1111 let cfg_edges: i64 = conn.query_row(
1114 "SELECT COUNT(*) FROM cfg_edges",
1115 [],
1116 |row| row.get(0),
1117 ).unwrap_or(0);
1118
1119 let cfg_paths: i64 = conn.query_row(
1120 "SELECT COUNT(*) FROM cfg_paths",
1121 [],
1122 |row| row.get(0),
1123 ).unwrap_or(0);
1124
1125 let cfg_dominators: i64 = conn.query_row(
1126 "SELECT COUNT(*) FROM cfg_dominators",
1127 [],
1128 |row| row.get(0),
1129 ).unwrap_or(0);
1130
1131 let mirage_schema_version: i32 = conn.query_row(
1132 "SELECT mirage_schema_version FROM mirage_meta WHERE id = 1",
1133 [],
1134 |row| row.get(0),
1135 ).unwrap_or(0);
1136
1137 let magellan_schema_version: i32 = conn.query_row(
1138 "SELECT magellan_schema_version FROM magellan_meta WHERE id = 1",
1139 [],
1140 |row| row.get(0),
1141 ).unwrap_or(0);
1142
1143 #[allow(deprecated)]
1144 Ok(DatabaseStatus {
1145 cfg_blocks,
1146 cfg_edges,
1147 cfg_paths,
1148 cfg_dominators,
1149 mirage_schema_version,
1150 magellan_schema_version,
1151 })
1152 }
1153 None => {
1154 self.status_via_storage()
1156 }
1157 }
1158 }
1159
1160 #[cfg(feature = "backend-sqlite")]
1162 fn status_via_storage(&self) -> Result<DatabaseStatus> {
1163 #[cfg(feature = "backend-geometric")]
1165 {
1166 if let Backend::Geometric(ref geometric) = self.storage {
1167 let stats = geometric.get_stats()?;
1169 return Ok(DatabaseStatus {
1170 cfg_blocks: stats.cfg_block_count as i64,
1171 cfg_edges: 0, cfg_paths: 0, cfg_dominators: 0, mirage_schema_version: MIRAGE_SCHEMA_VERSION,
1175 magellan_schema_version: MIN_MAGELLAN_SCHEMA_VERSION,
1176 });
1177 }
1178 }
1179
1180 Ok(DatabaseStatus {
1182 cfg_blocks: 0,
1183 cfg_edges: 0,
1184 cfg_paths: 0,
1185 cfg_dominators: 0,
1186 mirage_schema_version: MIRAGE_SCHEMA_VERSION,
1187 magellan_schema_version: MIN_MAGELLAN_SCHEMA_VERSION,
1188 })
1189 }
1190
1191 #[cfg(feature = "backend-native-v3")]
1195 pub fn status(&self) -> Result<DatabaseStatus> {
1196 let _snapshot = SnapshotId::current();
1200
1201 let cfg_blocks_count: i64 = 0;
1204
1205 let cfg_edges: i64 = 0; let cfg_paths: i64 = 0; let cfg_dominators: i64 = 0; let mirage_schema_version = MIRAGE_SCHEMA_VERSION;
1214 let magellan_schema_version = MIN_MAGELLAN_SCHEMA_VERSION;
1215
1216 #[allow(deprecated)]
1217 Ok(DatabaseStatus {
1218 cfg_blocks: cfg_blocks_count,
1219 cfg_edges,
1220 cfg_paths,
1221 cfg_dominators,
1222 mirage_schema_version,
1223 magellan_schema_version,
1224 })
1225 }
1226
1227 #[cfg(all(feature = "backend-geometric", not(feature = "backend-sqlite"), not(feature = "backend-native-v3")))]
1231 pub fn status(&self) -> Result<DatabaseStatus> {
1232 let cfg_blocks: i64 = if let Backend::Geometric(ref geometric) = self.storage {
1235 0
1239 } else {
1240 0
1241 };
1242
1243 let cfg_edges: i64 = 0;
1245 let cfg_paths: i64 = 0;
1246 let cfg_dominators: i64 = 0;
1247
1248 let mirage_schema_version = MIRAGE_SCHEMA_VERSION;
1251 let magellan_schema_version = MIN_MAGELLAN_SCHEMA_VERSION;
1252
1253 #[allow(deprecated)]
1254 Ok(DatabaseStatus {
1255 cfg_blocks,
1256 cfg_edges,
1257 cfg_paths,
1258 cfg_dominators,
1259 mirage_schema_version,
1260 magellan_schema_version,
1261 })
1262 }
1263
1264 #[cfg(feature = "backend-sqlite")]
1292 pub fn resolve_function_name(&self, name_or_id: &str) -> Result<i64> {
1293 self.resolve_function_name_with_file(name_or_id, None)
1294 }
1295
1296 #[cfg(feature = "backend-sqlite")]
1312 pub fn resolve_function_name_with_file(&self, name_or_id: &str, file_filter: Option<&str>) -> Result<i64> {
1313 if let Ok(id) = name_or_id.parse::<i64>() {
1315 return Ok(id);
1316 }
1317
1318 if let Ok(conn) = self.conn() {
1320 resolve_function_name_sqlite(conn, name_or_id, file_filter)
1321 } else {
1322 #[cfg(feature = "backend-geometric")]
1324 {
1325 if let Backend::Geometric(ref geometric) = self.storage {
1326 return self.resolve_function_name_geometric(name_or_id);
1327 }
1328 }
1329 anyhow::bail!("No database connection available for function resolution")
1330 }
1331 }
1332
1333 #[cfg(feature = "backend-geometric")]
1336 fn normalize_path_for_dedup(path: &str) -> String {
1337 let path = path.replace('\\', "/");
1339 let path = path.strip_prefix("./").unwrap_or(&path);
1341 if let Some(idx) = path.find("/src/") {
1344 path[idx + 1..].to_string()
1346 } else {
1347 path.to_string()
1348 }
1349 }
1350
1351 #[cfg(feature = "backend-geometric")]
1358 fn resolve_function_name_geometric(&self, name_or_id: &str) -> Result<i64> {
1359 if let Ok(id) = name_or_id.parse::<i64>() {
1361 if let Backend::Geometric(ref geometric) = self.storage {
1363 if geometric.inner().find_symbol_by_id_info(id as u64).is_some() {
1364 return Ok(id);
1365 }
1366 }
1367 anyhow::bail!("Function with ID '{}' not found", id);
1368 }
1369
1370 if let Some(fqn_data) = Self::parse_fqn(name_or_id) {
1372 return self.resolve_function_by_fqn(fqn_data);
1373 }
1374
1375 if let Backend::Geometric(ref geometric) = self.storage {
1377 let all_symbols = geometric.find_symbols_by_name(name_or_id);
1379 if all_symbols.is_empty() {
1380 anyhow::bail!("Function '{}' not found", name_or_id);
1381 }
1382
1383 let mut unique_symbols: Vec<magellan::graph::geometric_backend::SymbolInfo> = Vec::new();
1387 let mut seen_ids: std::collections::HashSet<u64> = std::collections::HashSet::new();
1388
1389 for sym in all_symbols {
1390 if seen_ids.insert(sym.id) {
1391 unique_symbols.push(sym);
1392 }
1393 }
1394
1395 if unique_symbols.len() > 1 {
1397 let first = &unique_symbols[0];
1398 let first_path_normalized = Self::normalize_path_for_dedup(&first.file_path);
1399 let all_same_location = unique_symbols.iter().all(|sym| {
1400 let sym_path_normalized = Self::normalize_path_for_dedup(&sym.file_path);
1401 sym.name == first.name
1402 && sym_path_normalized == first_path_normalized
1403 && sym.start_line == first.start_line
1404 && sym.start_col == first.start_col
1405 });
1406
1407 if !all_same_location {
1408 anyhow::bail!(
1410 "Ambiguous function reference to '{}': {} unique candidates found\n\nCandidates:\n{}\n\nUse full qualified name: magellan::/path/to/file.rs::{}",
1411 name_or_id,
1412 unique_symbols.len(),
1413 unique_symbols.iter().map(|s| {
1414 format!(" - {} ({}:{}:{})", s.name, s.file_path, s.start_line, s.start_col)
1415 }).collect::<Vec<_>>().join("\n"),
1416 name_or_id
1417 );
1418 }
1419 }
1421 Ok(unique_symbols[0].id as i64)
1422 } else {
1423 anyhow::bail!("Geometric backend not available")
1424 }
1425 }
1426
1427 #[cfg(feature = "backend-geometric")]
1430 fn parse_fqn(name: &str) -> Option<(&str, &str)> {
1431 if !name.starts_with("magellan::") {
1434 return None;
1435 }
1436
1437 let after_prefix = &name[10..]; if let Some(last_sep_pos) = after_prefix.rfind("::") {
1442 let file_path = &after_prefix[..last_sep_pos];
1443 let name_part = &after_prefix[last_sep_pos + 2..];
1444
1445 let symbol_name = if let Some(space_pos) = name_part.find(' ') {
1448 &name_part[space_pos + 1..]
1449 } else {
1450 name_part
1451 };
1452
1453 if !file_path.is_empty() && !symbol_name.is_empty() {
1454 return Some((file_path, symbol_name));
1455 }
1456 }
1457
1458 None
1459 }
1460
1461 #[cfg(feature = "backend-geometric")]
1463 fn resolve_function_by_fqn(&self, fqn_data: (&str, &str)) -> Result<i64> {
1464 let (file_path, symbol_name) = fqn_data;
1465
1466 if let Backend::Geometric(ref geometric) = self.storage {
1467 match geometric.find_symbol_id_by_name_and_path(symbol_name, file_path) {
1469 Some(id) => Ok(id as i64),
1470 None => {
1471 let all_symbols = geometric.find_symbols_by_name(symbol_name);
1473 let normalized_target = Self::normalize_path_for_dedup(file_path);
1474
1475 let matching_symbols: Vec<_> = all_symbols
1476 .into_iter()
1477 .filter(|sym| {
1478 let sym_path_normalized = Self::normalize_path_for_dedup(&sym.file_path);
1479 sym_path_normalized == normalized_target
1480 })
1481 .collect();
1482
1483 if matching_symbols.is_empty() {
1484 anyhow::bail!("Function '{}' not found in file '{}'", symbol_name, file_path);
1485 } else {
1486 anyhow::bail!(
1488 "Multiple functions named '{}' found in file '{}' ({} matches). Use numeric ID instead.",
1489 symbol_name,
1490 file_path,
1491 matching_symbols.len()
1492 );
1493 }
1494 }
1495 }
1496 } else {
1497 anyhow::bail!("Geometric backend not available")
1498 }
1499 }
1500
1501 #[cfg(feature = "backend-native-v3")]
1506 pub fn resolve_function_name(&self, name_or_id: &str) -> Result<i64> {
1507 if let Ok(id) = name_or_id.parse::<i64>() {
1509 return Ok(id);
1510 }
1511
1512 use sqlitegraph::SnapshotId;
1514 let snapshot = SnapshotId::current();
1515
1516 let entity_ids = self.backend().entity_ids()
1518 .context("Failed to query entities from backend")?;
1519
1520 for entity_id in &entity_ids {
1522 if let Ok(entity) = self.backend().get_node(snapshot, *entity_id) {
1523 if entity.kind == "Symbol" {
1524 if let Some(symbol_id) = entity.data.get("symbol_id").and_then(|s| s.as_str()) {
1526 if symbol_id == name_or_id {
1527 if let Some(kind) = entity.data.get("kind").and_then(|k| k.as_str()) {
1529 if kind == "Function" {
1530 return Ok(*entity_id);
1531 }
1532 }
1533 }
1534 }
1535 }
1536 }
1537 }
1538
1539 for entity_id in &entity_ids {
1541 if let Ok(entity) = self.backend().get_node(snapshot, *entity_id) {
1542 if entity.kind == "Symbol" && entity.name == name_or_id {
1546 if let Some(kind) = entity.data.get("kind").and_then(|k| k.as_str()) {
1548 if kind == "Function" {
1549 return Ok(*entity_id);
1550 }
1551 }
1552 }
1553 }
1554 }
1555
1556 anyhow::bail!(
1557 "Function '{}' not found in database. Run 'magellan watch' to index functions.",
1558 name_or_id
1559 )
1560 }
1561
1562 #[cfg(feature = "backend-sqlite")]
1588 pub fn load_cfg(&self, function_id: i64) -> Result<crate::cfg::Cfg> {
1589 let blocks = self.storage().get_cfg_blocks(function_id)?;
1591
1592 if blocks.is_empty() {
1593 anyhow::bail!(
1594 "No CFG blocks found for function_id {}. Run 'magellan watch' to build CFGs.",
1595 function_id
1596 );
1597 }
1598
1599 let file_path = self.get_function_file(function_id);
1601
1602 let block_rows: Vec<(i64, String, Option<String>, Option<i64>, Option<i64>,
1604 Option<i64>, Option<i64>, Option<i64>, Option<i64>)> = blocks
1605 .into_iter()
1606 .enumerate()
1607 .map(|(idx, b)| (
1608 idx as i64, b.kind,
1610 Some(b.terminator),
1611 Some(b.byte_start as i64),
1612 Some(b.byte_end as i64),
1613 Some(b.start_line as i64),
1614 Some(b.start_col as i64),
1615 Some(b.end_line as i64),
1616 Some(b.end_col as i64),
1617 ))
1618 .collect();
1619
1620 load_cfg_from_rows(block_rows, file_path.map(std::path::PathBuf::from))
1621 }
1622
1623 #[cfg(feature = "backend-native-v3")]
1627 pub fn load_cfg(&self, function_id: i64) -> Result<crate::cfg::Cfg> {
1628 load_cfg_from_native_v3(self.backend(), function_id)
1629 }
1630
1631 pub fn get_function_name(&self, function_id: i64) -> Option<String> {
1646 let snapshot = SnapshotId::current();
1647 self.backend().get_node(snapshot, function_id)
1648 .ok()
1649 .and_then(|entity| {
1650 if entity.kind == "Symbol"
1652 && entity.data.get("kind").and_then(|v| v.as_str()) == Some("Function")
1653 {
1654 Some(entity.name)
1655 } else {
1656 None
1657 }
1658 })
1659 }
1660
1661 pub fn get_function_file(&self, function_id: i64) -> Option<String> {
1676 let snapshot = SnapshotId::current();
1677 self.backend().get_node(snapshot, function_id)
1678 .ok()
1679 .and_then(|entity| entity.file_path)
1680 }
1681
1682 #[cfg(feature = "backend-sqlite")]
1697 pub fn function_exists(&self, function_id: i64) -> bool {
1698 use crate::storage::function_exists;
1699 self.conn()
1700 .and_then(|conn| Ok(function_exists(conn, function_id)))
1701 .unwrap_or(false)
1702 }
1703
1704 #[cfg(feature = "backend-native-v3")]
1709 pub fn function_exists(&self, _function_id: i64) -> bool {
1710 false
1713 }
1714
1715 #[cfg(feature = "backend-sqlite")]
1730 pub fn get_function_hash(&self, function_id: i64) -> Option<String> {
1731 use crate::storage::get_function_hash;
1732 self.conn()
1733 .and_then(|conn| Ok(get_function_hash(conn, function_id)))
1734 .ok()
1735 .flatten()
1736 }
1737
1738 #[cfg(feature = "backend-native-v3")]
1742 pub fn get_function_hash(&self, _function_id: i64) -> Option<String> {
1743 None
1745 }
1746}
1747
1748#[cfg(feature = "backend-sqlite")]
1753fn resolve_function_name_sqlite(conn: &Connection, name_or_id: &str, file_filter: Option<&str>) -> Result<i64> {
1754 let function_id_by_symbol: Option<i64> = conn
1757 .query_row(
1758 "SELECT id FROM graph_entities
1759 WHERE kind = 'Symbol'
1760 AND json_extract(data, '$.kind') = 'Function'
1761 AND json_extract(data, '$.symbol_id') = ?
1762 LIMIT 1",
1763 params![name_or_id],
1764 |row| row.get(0),
1765 )
1766 .optional()
1767 .context(format!(
1768 "Failed to query function with symbol_id '{}'",
1769 name_or_id
1770 ))?;
1771
1772 if let Some(id) = function_id_by_symbol {
1773 return Ok(id);
1774 }
1775
1776 let function_id: Option<i64> = if let Some(file_path) = file_filter {
1778 let pattern = format!("%{}%", file_path);
1780 conn.query_row(
1781 "SELECT id FROM graph_entities
1782 WHERE kind = 'Symbol'
1783 AND json_extract(data, '$.kind') = 'Function'
1784 AND name = ?
1785 AND file_path LIKE ?
1786 LIMIT 1",
1787 params![name_or_id, pattern],
1788 |row| row.get(0),
1789 )
1790 .optional()
1791 .context(format!(
1792 "Failed to query function with name '{}' in file '{}'",
1793 name_or_id, file_path
1794 ))?
1795 } else {
1796 conn.query_row(
1798 "SELECT id FROM graph_entities
1799 WHERE kind = 'Symbol'
1800 AND json_extract(data, '$.kind') = 'Function'
1801 AND name = ?
1802 LIMIT 1",
1803 params![name_or_id],
1804 |row| row.get(0),
1805 )
1806 .optional()
1807 .context(format!(
1808 "Failed to query function with name '{}'",
1809 name_or_id
1810 ))?
1811 };
1812
1813 function_id.context(format!(
1814 "Function '{}' not found in database. Run 'magellan watch' to index functions.",
1815 name_or_id
1816 ))
1817}
1818
1819#[cfg(feature = "backend-sqlite")]
1823fn load_cfg_from_sqlite(conn: &Connection, function_id: i64) -> Result<crate::cfg::Cfg> {
1824 use std::path::PathBuf;
1825
1826 let file_path: Option<String> = conn
1828 .query_row(
1829 "SELECT file_path FROM graph_entities WHERE id = ?",
1830 params![function_id],
1831 |row| row.get(0),
1832 )
1833 .optional()
1834 .context("Failed to query file_path from graph_entities")?;
1835
1836 let file_path = file_path.map(PathBuf::from);
1837
1838 let mut stmt = conn.prepare_cached(
1841 "SELECT id, kind, terminator, byte_start, byte_end,
1842 start_line, start_col, end_line, end_col
1843 FROM cfg_blocks
1844 WHERE function_id = ?
1845 ORDER BY id ASC",
1846 ).context("Failed to prepare cfg_blocks query")?;
1847
1848 let block_rows: Vec<(i64, String, Option<String>, Option<i64>, Option<i64>,
1849 Option<i64>, Option<i64>, Option<i64>, Option<i64>)> = stmt
1850 .query_map(params![function_id], |row| {
1851 Ok((
1852 row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?, row.get(5)?, row.get(6)?, row.get(7)?, row.get(8)?, ))
1862 })
1863 .context("Failed to execute cfg_blocks query")?
1864 .collect::<Result<Vec<_>, _>>()
1865 .context("Failed to collect cfg_blocks rows")?;
1866
1867 if block_rows.is_empty() {
1868 anyhow::bail!(
1869 "No CFG blocks found for function_id {}. Run 'magellan watch' to build CFGs.",
1870 function_id
1871 );
1872 }
1873
1874 load_cfg_from_rows(block_rows, file_path)
1875}
1876
1877#[cfg(feature = "backend-native-v3")]
1882fn load_cfg_from_native_v3(
1883 _backend: &dyn GraphBackend,
1884 _function_id: i64,
1885) -> Result<crate::cfg::Cfg> {
1886 anyhow::bail!(
1889 "Native-V3 CFG loading is not yet fully implemented. \
1890 Please use the SQLite backend for now."
1891 )
1892}
1893
1894fn load_cfg_from_rows(
1899 block_rows: Vec<(i64, String, Option<String>, Option<i64>, Option<i64>,
1900 Option<i64>, Option<i64>, Option<i64>, Option<i64>)>,
1901 file_path: Option<std::path::PathBuf>,
1902) -> Result<crate::cfg::Cfg> {
1903 use crate::cfg::{BasicBlock, BlockKind, Cfg, Terminator};
1904 use crate::cfg::build_edges_from_terminators;
1905 use crate::cfg::source::SourceLocation;
1906 use std::collections::HashMap;
1907
1908 let mut db_id_to_node: HashMap<i64, usize> = HashMap::new();
1910 let mut graph = Cfg::new();
1911
1912 for (node_idx, (db_id, kind_str, terminator_str, byte_start, byte_end,
1914 start_line, start_col, end_line, end_col)) in
1915 block_rows.iter().enumerate()
1916 {
1917 let kind = match kind_str.as_str() {
1919 "entry" => BlockKind::Entry,
1920 "return" => BlockKind::Exit,
1921 "if" | "else" | "loop" | "while" | "for" | "match_arm" | "block" => BlockKind::Normal,
1922 _ => {
1923 BlockKind::Normal
1926 }
1927 };
1928
1929 let terminator = match terminator_str.as_deref() {
1931 Some("fallthrough") => Terminator::Goto { target: 0 }, Some("conditional") => Terminator::SwitchInt { targets: vec![], otherwise: 0 },
1933 Some("goto") => Terminator::Goto { target: 0 },
1934 Some("return") => Terminator::Return,
1935 Some("break") => Terminator::Abort("break".to_string()),
1936 Some("continue") => Terminator::Abort("continue".to_string()),
1937 Some("call") => Terminator::Call { target: None, unwind: None },
1938 Some("panic") => Terminator::Abort("panic".to_string()),
1939 Some(_) | None => Terminator::Unreachable,
1940 };
1941
1942 let source_location = if let Some(ref path) = file_path {
1944 let sl = start_line.and_then(|l| start_col.map(|c| (l as usize, c as usize)));
1946 let el = end_line.and_then(|l| end_col.map(|c| (l as usize, c as usize)));
1947
1948 match (sl, el, byte_start, byte_end) {
1949 (Some((start_l, start_c)), Some((end_l, end_c)), Some(bs), Some(be)) => {
1950 Some(SourceLocation {
1951 file_path: path.clone(),
1952 byte_start: *bs as usize,
1953 byte_end: *be as usize,
1954 start_line: start_l,
1955 start_column: start_c,
1956 end_line: end_l,
1957 end_column: end_c,
1958 })
1959 }
1960 _ => None,
1961 }
1962 } else {
1963 None
1964 };
1965
1966 let block = BasicBlock {
1967 id: node_idx,
1968 kind,
1969 statements: vec![], terminator,
1971 source_location,
1972 };
1973
1974 graph.add_node(block);
1975 db_id_to_node.insert(*db_id, node_idx);
1976 }
1977
1978 build_edges_from_terminators(&mut graph, &block_rows, &db_id_to_node)
1981 .context("Failed to build edges from terminator data")?;
1982
1983 Ok(graph)
1984}
1985
1986pub fn resolve_function_name(db: &MirageDb, name_or_id: &str) -> Result<i64> {
2016 db.resolve_function_name(name_or_id)
2017}
2018
2019pub fn resolve_function_name_with_file(db: &MirageDb, name_or_id: &str, file_filter: Option<&str>) -> Result<i64> {
2047 db.resolve_function_name_with_file(name_or_id, file_filter)
2048}
2049
2050pub fn get_function_name_db(db: &MirageDb, function_id: i64) -> Option<String> {
2078 db.get_function_name(function_id)
2079}
2080
2081pub fn get_function_file_db(db: &MirageDb, function_id: i64) -> Option<String> {
2109 db.get_function_file(function_id)
2110}
2111
2112pub fn get_function_hash_db(db: &MirageDb, function_id: i64) -> Option<String> {
2143 db.get_function_hash(function_id)
2144}
2145
2146#[cfg(feature = "backend-sqlite")]
2151pub fn resolve_function_name_with_conn(conn: &Connection, name_or_id: &str) -> Result<i64> {
2152 if let Ok(id) = name_or_id.parse::<i64>() {
2154 return Ok(id);
2155 }
2156
2157 let function_id: Option<i64> = conn
2160 .query_row(
2161 "SELECT id FROM graph_entities
2162 WHERE kind = 'Symbol'
2163 AND json_extract(data, '$.kind') = 'Function'
2164 AND name = ?
2165 LIMIT 1",
2166 params![name_or_id],
2167 |row| row.get(0),
2168 )
2169 .optional()
2170 .context(format!(
2171 "Failed to query function with name '{}'",
2172 name_or_id
2173 ))?;
2174
2175 function_id.context(format!(
2176 "Function '{}' not found in database. Run 'magellan watch' to index functions.",
2177 name_or_id
2178 ))
2179}
2180
2181pub fn load_cfg_from_db(db: &MirageDb, function_id: i64) -> Result<crate::cfg::Cfg> {
2213 db.load_cfg(function_id)
2214}
2215
2216#[cfg(feature = "backend-sqlite")]
2250pub fn load_cfg_from_db_with_conn(conn: &Connection, function_id: i64) -> Result<crate::cfg::Cfg> {
2251 load_cfg_from_sqlite(conn, function_id)
2252}
2253
2254#[deprecated(note = "Magellan handles CFG storage via cfg_blocks. Edges are computed in memory.")]
2286pub fn store_cfg(
2287 conn: &mut Connection,
2288 function_id: i64,
2289 _function_hash: &str, cfg: &crate::cfg::Cfg,
2291) -> Result<()> {
2292 use crate::cfg::{BlockKind, EdgeType, Terminator};
2293 use petgraph::visit::EdgeRef;
2294
2295 conn.execute("BEGIN IMMEDIATE TRANSACTION", [])
2296 .context("Failed to begin transaction")?;
2297
2298 conn.execute(
2300 "DELETE FROM cfg_edges WHERE from_id IN (
2301 SELECT id FROM cfg_blocks WHERE function_id = ?
2302 )",
2303 params![function_id],
2304 ).context("Failed to clear existing cfg_edges")?;
2305
2306 conn.execute(
2307 "DELETE FROM cfg_blocks WHERE function_id = ?",
2308 params![function_id],
2309 ).context("Failed to clear existing cfg_blocks")?;
2310
2311 let mut block_id_map: std::collections::HashMap<petgraph::graph::NodeIndex, i64> =
2313 std::collections::HashMap::new();
2314
2315 let mut insert_block = conn.prepare_cached(
2316 "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
2317 start_line, start_col, end_line, end_col)
2318 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
2319 ).context("Failed to prepare block insert statement")?;
2320
2321 for node_idx in cfg.node_indices() {
2322 let block = cfg.node_weight(node_idx)
2323 .context("CFG node has no weight")?;
2324
2325 let terminator_str = match &block.terminator {
2327 Terminator::Goto { .. } => "goto",
2328 Terminator::SwitchInt { .. } => "conditional",
2329 Terminator::Return => "return",
2330 Terminator::Call { .. } => "call",
2331 Terminator::Abort(msg) if msg == "break" => "break",
2332 Terminator::Abort(msg) if msg == "continue" => "continue",
2333 Terminator::Abort(msg) if msg == "panic" => "panic",
2334 _ => "fallthrough",
2335 };
2336
2337 let (byte_start, byte_end) = block.source_location.as_ref()
2339 .map(|loc| (Some(loc.byte_start as i64), Some(loc.byte_end as i64)))
2340 .unwrap_or((None, None));
2341
2342 let (start_line, start_col, end_line, end_col) = block.source_location.as_ref()
2343 .map(|loc| (
2344 Some(loc.start_line as i64),
2345 Some(loc.start_column as i64),
2346 Some(loc.end_line as i64),
2347 Some(loc.end_column as i64),
2348 ))
2349 .unwrap_or((None, None, None, None));
2350
2351 let kind = match block.kind {
2353 BlockKind::Entry => "entry",
2354 BlockKind::Normal => "block",
2355 BlockKind::Exit => "return",
2356 };
2357
2358 insert_block.execute(params![
2359 function_id,
2360 kind,
2361 terminator_str,
2362 byte_start,
2363 byte_end,
2364 start_line,
2365 start_col,
2366 end_line,
2367 end_col,
2368 ]).context("Failed to insert cfg_block")?;
2369
2370 let db_id = conn.last_insert_rowid();
2371 block_id_map.insert(node_idx, db_id);
2372 }
2373
2374 let mut insert_edge = conn.prepare_cached(
2376 "INSERT INTO cfg_edges (from_id, to_id, edge_type) VALUES (?, ?, ?)",
2377 ).context("Failed to prepare edge insert statement")?;
2378
2379 for edge in cfg.edge_references() {
2380 let from_db_id = block_id_map.get(&edge.source())
2381 .context("Edge source has no database ID")?;
2382 let to_db_id = block_id_map.get(&edge.target())
2383 .context("Edge target has no database ID")?;
2384
2385 let edge_type_str = match edge.weight() {
2386 EdgeType::Fallthrough => "Fallthrough",
2387 EdgeType::TrueBranch => "TrueBranch",
2388 EdgeType::FalseBranch => "FalseBranch",
2389 EdgeType::LoopBack => "LoopBack",
2390 EdgeType::LoopExit => "LoopExit",
2391 EdgeType::Call => "Call",
2392 EdgeType::Exception => "Exception",
2393 EdgeType::Return => "Return",
2394 };
2395
2396 insert_edge.execute(params![from_db_id, to_db_id, edge_type_str])
2397 .context("Failed to insert cfg_edge")?;
2398 }
2399
2400 conn.execute("COMMIT", [])
2401 .context("Failed to commit transaction")?;
2402
2403 Ok(())
2404}
2405
2406pub fn function_exists(conn: &Connection, function_id: i64) -> bool {
2418 conn.query_row(
2419 "SELECT COUNT(*) FROM cfg_blocks WHERE function_id = ?",
2420 params![function_id],
2421 |row| row.get::<_, i64>(0).map(|count| count > 0)
2422 ).optional().ok().flatten().unwrap_or(false)
2423}
2424
2425pub fn get_function_hash(conn: &Connection, function_id: i64) -> Option<String> {
2443 let cfg_hash: Option<String> = conn.query_row(
2445 "SELECT cfg_hash FROM cfg_blocks WHERE function_id = ? LIMIT 1",
2446 params![function_id],
2447 |row| row.get(0)
2448 ).optional().ok().flatten();
2449
2450 if cfg_hash.is_some() {
2451 return cfg_hash;
2452 }
2453
2454 conn.query_row(
2457 "SELECT json_extract(data, '$.symbol_id') FROM graph_entities WHERE id = ? LIMIT 1",
2458 params![function_id],
2459 |row| row.get::<_, Option<String>>(0)
2460 ).optional().ok().flatten().flatten()
2461}
2462
2463pub fn hash_changed(
2484 conn: &Connection,
2485 function_id: i64,
2486 _new_hash: &str,
2487) -> Result<bool> {
2488 let old_hash: Option<String> = conn.query_row(
2489 "SELECT function_hash FROM cfg_blocks WHERE function_id = ? LIMIT 1",
2490 params![function_id],
2491 |row| row.get(0)
2492 ).optional()?;
2493
2494 match old_hash {
2495 Some(old) => Ok(old != _new_hash),
2496 None => Ok(true), }
2498}
2499
2500pub fn get_changed_functions(
2520 conn: &Connection,
2521 project_path: &std::path::Path,
2522) -> Result<std::collections::HashSet<String>> {
2523 use std::collections::HashSet;
2524 use std::process::Command;
2525
2526 let mut changed = HashSet::new();
2527
2528 if let Ok(git_output) = Command::new("git")
2530 .args(["diff", "--name-only", "HEAD"])
2531 .current_dir(project_path)
2532 .output()
2533 {
2534 let git_files = String::from_utf8_lossy(&git_output.stdout);
2535
2536 let changed_rs_files: Vec<&str> = git_files
2538 .lines()
2539 .filter(|f| f.ends_with(".rs"))
2540 .collect();
2541
2542 if changed_rs_files.is_empty() {
2543 return Ok(changed);
2544 }
2545
2546 for file in changed_rs_files {
2548 let normalized_path = if file.starts_with('/') {
2550 file.trim_start_matches('/')
2551 } else {
2552 file
2553 };
2554
2555 let mut stmt = conn.prepare_cached(
2559 "SELECT name FROM graph_entities
2560 WHERE kind = 'function' AND (
2561 file_path = ? OR
2562 file_path = ? OR
2563 file_path LIKE '%' || ?
2564 )"
2565 ).context("Failed to prepare function lookup query")?;
2566
2567 let with_slash = format!("/{}", normalized_path);
2568
2569 let rows = stmt.query_map(
2570 params![normalized_path, &with_slash, normalized_path],
2571 |row| row.get::<_, String>(0)
2572 ).context("Failed to execute function lookup")?;
2573
2574 for row in rows {
2575 if let Ok(func_name) = row {
2576 changed.insert(func_name);
2577 }
2578 }
2579 }
2580 }
2581
2582 Ok(changed)
2583}
2584
2585pub fn get_function_file(
2598 conn: &Connection,
2599 function_name: &str,
2600) -> Result<Option<String>> {
2601 let file: Option<String> = conn.query_row(
2602 "SELECT file_path FROM graph_entities WHERE kind = 'function' AND name = ? LIMIT 1",
2603 params![function_name],
2604 |row| row.get(0)
2605 ).optional()?;
2606
2607 Ok(file)
2608}
2609
2610pub fn get_function_name(conn: &Connection, function_id: i64) -> Option<String> {
2622 conn.query_row(
2623 "SELECT name FROM graph_entities WHERE id = ?",
2624 params![function_id],
2625 |row| row.get(0)
2626 ).optional().ok().flatten()
2627}
2628
2629pub fn get_path_elements(conn: &Connection, path_id: &str) -> Result<Vec<crate::cfg::BlockId>> {
2641 let mut stmt = conn.prepare_cached(
2642 "SELECT block_id FROM cfg_path_elements
2643 WHERE path_id = ?
2644 ORDER BY sequence_order ASC",
2645 ).context("Failed to prepare path elements query")?;
2646
2647 let blocks: Vec<crate::cfg::BlockId> = stmt
2648 .query_map(params![path_id], |row| {
2649 Ok(row.get::<_, i64>(0)? as usize)
2650 })
2651 .context("Failed to execute path elements query")?
2652 .collect::<Result<Vec<_>, _>>()
2653 .context("Failed to collect path elements")?;
2654
2655 if blocks.is_empty() {
2656 anyhow::bail!("Path '{}' not found in cache", path_id);
2657 }
2658
2659 Ok(blocks)
2660}
2661
2662pub fn compute_path_impact_from_db(
2679 conn: &Connection,
2680 path_id: &str,
2681 cfg: &crate::cfg::Cfg,
2682 max_depth: Option<usize>,
2683) -> Result<crate::cfg::PathImpact> {
2684 let path_blocks = get_path_elements(conn, path_id)?;
2685
2686 let mut impact = crate::cfg::compute_path_impact(cfg, &path_blocks, max_depth);
2687 impact.path_id = path_id.to_string();
2688
2689 Ok(impact)
2690}
2691
2692pub fn create_minimal_database<P: AsRef<Path>>(path: P) -> Result<()> {
2707 let path = path.as_ref();
2708
2709 if path.exists() {
2711 anyhow::bail!("Database already exists: {}", path.display());
2712 }
2713
2714 let mut conn = Connection::open(path)
2715 .context("Failed to create database file")?;
2716
2717 conn.execute(
2719 "CREATE TABLE magellan_meta (
2720 id INTEGER PRIMARY KEY CHECK (id = 1),
2721 magellan_schema_version INTEGER NOT NULL,
2722 sqlitegraph_schema_version INTEGER NOT NULL,
2723 created_at INTEGER NOT NULL
2724 )",
2725 [],
2726 ).context("Failed to create magellan_meta table")?;
2727
2728 conn.execute(
2730 "CREATE TABLE graph_entities (
2731 id INTEGER PRIMARY KEY AUTOINCREMENT,
2732 kind TEXT NOT NULL,
2733 name TEXT NOT NULL,
2734 file_path TEXT,
2735 data TEXT NOT NULL
2736 )",
2737 [],
2738 ).context("Failed to create graph_entities table")?;
2739
2740 conn.execute(
2742 "CREATE INDEX idx_graph_entities_kind ON graph_entities(kind)",
2743 [],
2744 ).context("Failed to create index on graph_entities.kind")?;
2745
2746 conn.execute(
2747 "CREATE INDEX idx_graph_entities_name ON graph_entities(name)",
2748 [],
2749 ).context("Failed to create index on graph_entities.name")?;
2750
2751 let now = chrono::Utc::now().timestamp();
2753 conn.execute(
2754 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
2755 VALUES (1, ?, ?, ?)",
2756 params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, now],
2757 ).context("Failed to initialize magellan_meta")?;
2758
2759 create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).context("Failed to create Mirage schema")?;
2761
2762 Ok(())
2763}
2764
2765#[cfg(all(test, feature = "sqlite"))]
2766mod tests {
2767 use super::*;
2768
2769 #[test]
2770 fn test_create_schema() {
2771 let mut conn = Connection::open_in_memory().unwrap();
2772 conn.execute(
2774 "CREATE TABLE magellan_meta (
2775 id INTEGER PRIMARY KEY CHECK (id = 1),
2776 magellan_schema_version INTEGER NOT NULL,
2777 sqlitegraph_schema_version INTEGER NOT NULL,
2778 created_at INTEGER NOT NULL
2779 )",
2780 [],
2781 ).unwrap();
2782
2783 conn.execute(
2784 "CREATE TABLE graph_entities (
2785 id INTEGER PRIMARY KEY AUTOINCREMENT,
2786 kind TEXT NOT NULL,
2787 name TEXT NOT NULL,
2788 file_path TEXT,
2789 data TEXT NOT NULL
2790 )",
2791 [],
2792 ).unwrap();
2793
2794 conn.execute(
2796 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
2797 VALUES (1, ?, ?, ?)",
2798 params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
2799 ).unwrap();
2800
2801 create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
2803
2804 let table_count: i64 = conn.query_row(
2806 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name LIKE 'cfg_%'",
2807 [],
2808 |row| row.get(0),
2809 ).unwrap();
2810
2811 assert!(table_count >= 5); }
2813
2814 #[test]
2815 fn test_migrate_schema_from_version_0() {
2816 let mut conn = Connection::open_in_memory().unwrap();
2817
2818 conn.execute(
2820 "CREATE TABLE magellan_meta (
2821 id INTEGER PRIMARY KEY CHECK (id = 1),
2822 magellan_schema_version INTEGER NOT NULL,
2823 sqlitegraph_schema_version INTEGER NOT NULL,
2824 created_at INTEGER NOT NULL
2825 )",
2826 [],
2827 ).unwrap();
2828
2829 conn.execute(
2830 "CREATE TABLE graph_entities (
2831 id INTEGER PRIMARY KEY AUTOINCREMENT,
2832 kind TEXT NOT NULL,
2833 name TEXT NOT NULL,
2834 file_path TEXT,
2835 data TEXT NOT NULL
2836 )",
2837 [],
2838 ).unwrap();
2839
2840 conn.execute(
2841 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
2842 VALUES (1, ?, ?, ?)",
2843 params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
2844 ).unwrap();
2845
2846 create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
2848
2849 let version: i32 = conn.query_row(
2851 "SELECT mirage_schema_version FROM mirage_meta WHERE id = 1",
2852 [],
2853 |row| row.get(0),
2854 ).unwrap();
2855
2856 assert_eq!(version, MIRAGE_SCHEMA_VERSION);
2857 }
2858
2859 #[test]
2860 fn test_migrate_schema_no_op_when_current() {
2861 let mut conn = Connection::open_in_memory().unwrap();
2862
2863 conn.execute(
2865 "CREATE TABLE magellan_meta (
2866 id INTEGER PRIMARY KEY CHECK (id = 1),
2867 magellan_schema_version INTEGER NOT NULL,
2868 sqlitegraph_schema_version INTEGER NOT NULL,
2869 created_at INTEGER NOT NULL
2870 )",
2871 [],
2872 ).unwrap();
2873
2874 conn.execute(
2875 "CREATE TABLE graph_entities (
2876 id INTEGER PRIMARY KEY AUTOINCREMENT,
2877 kind TEXT NOT NULL,
2878 name TEXT NOT NULL,
2879 file_path TEXT,
2880 data TEXT NOT NULL
2881 )",
2882 [],
2883 ).unwrap();
2884
2885 conn.execute(
2886 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
2887 VALUES (1, ?, ?, ?)",
2888 params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
2889 ).unwrap();
2890
2891 create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
2893
2894 migrate_schema(&mut conn).unwrap();
2896
2897 let version: i32 = conn.query_row(
2899 "SELECT mirage_schema_version FROM mirage_meta WHERE id = 1",
2900 [],
2901 |row| row.get(0),
2902 ).unwrap();
2903
2904 assert_eq!(version, MIRAGE_SCHEMA_VERSION);
2905 }
2906
2907 #[test]
2908 fn test_fk_constraint_cfg_blocks() {
2909 let mut conn = Connection::open_in_memory().unwrap();
2910
2911 conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
2913
2914 conn.execute(
2916 "CREATE TABLE magellan_meta (
2917 id INTEGER PRIMARY KEY CHECK (id = 1),
2918 magellan_schema_version INTEGER NOT NULL,
2919 sqlitegraph_schema_version INTEGER NOT NULL,
2920 created_at INTEGER NOT NULL
2921 )",
2922 [],
2923 ).unwrap();
2924
2925 conn.execute(
2926 "CREATE TABLE graph_entities (
2927 id INTEGER PRIMARY KEY AUTOINCREMENT,
2928 kind TEXT NOT NULL,
2929 name TEXT NOT NULL,
2930 file_path TEXT,
2931 data TEXT NOT NULL
2932 )",
2933 [],
2934 ).unwrap();
2935
2936 conn.execute(
2937 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
2938 VALUES (1, ?, ?, ?)",
2939 params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
2940 ).unwrap();
2941
2942 create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
2944
2945 conn.execute(
2947 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
2948 params!("function", "test_func", "test.rs", "{}"),
2949 ).unwrap();
2950
2951 let function_id: i64 = conn.last_insert_rowid();
2952
2953 let invalid_result = conn.execute(
2955 "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
2956 start_line, start_col, end_line, end_col)
2957 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
2958 params!(9999, "entry", "return", 0, 10, 1, 0, 1, 10),
2959 );
2960
2961 assert!(invalid_result.is_err(), "Insert with invalid function_id should fail");
2963
2964 let valid_result = conn.execute(
2966 "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
2967 start_line, start_col, end_line, end_col)
2968 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
2969 params!(function_id, "entry", "return", 0, 10, 1, 0, 1, 10),
2970 );
2971
2972 assert!(valid_result.is_ok(), "Insert with valid function_id should succeed");
2973
2974 let count: i64 = conn.query_row(
2976 "SELECT COUNT(*) FROM cfg_blocks WHERE function_id = ?",
2977 params![function_id],
2978 |row| row.get(0),
2979 ).unwrap();
2980
2981 assert_eq!(count, 1, "Should have exactly one cfg_block entry");
2982 }
2983
2984 #[test]
2985 fn test_store_cfg_retrieves_correctly() {
2986 use crate::cfg::{BasicBlock, BlockKind, Cfg, EdgeType, Terminator};
2987
2988 let mut conn = Connection::open_in_memory().unwrap();
2989
2990 conn.execute(
2992 "CREATE TABLE magellan_meta (
2993 id INTEGER PRIMARY KEY CHECK (id = 1),
2994 magellan_schema_version INTEGER NOT NULL,
2995 sqlitegraph_schema_version INTEGER NOT NULL,
2996 created_at INTEGER NOT NULL
2997 )",
2998 [],
2999 ).unwrap();
3000
3001 conn.execute(
3002 "CREATE TABLE graph_entities (
3003 id INTEGER PRIMARY KEY AUTOINCREMENT,
3004 kind TEXT NOT NULL,
3005 name TEXT NOT NULL,
3006 file_path TEXT,
3007 data TEXT NOT NULL
3008 )",
3009 [],
3010 ).unwrap();
3011
3012 conn.execute(
3013 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3014 VALUES (1, ?, ?, ?)",
3015 params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
3016 ).unwrap();
3017
3018 create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
3020
3021 conn.execute(
3023 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3024 params!("function", "test_func", "test.rs", "{}"),
3025 ).unwrap();
3026
3027 let function_id: i64 = conn.last_insert_rowid();
3028
3029 let mut cfg = Cfg::new();
3031
3032 let b0 = cfg.add_node(BasicBlock {
3033 id: 0,
3034 kind: BlockKind::Entry,
3035 statements: vec!["let x = 1".to_string()],
3036 terminator: Terminator::Goto { target: 1 },
3037 source_location: None,
3038 });
3039
3040 let b1 = cfg.add_node(BasicBlock {
3041 id: 1,
3042 kind: BlockKind::Normal,
3043 statements: vec![],
3044 terminator: Terminator::Return,
3045 source_location: None,
3046 });
3047
3048 cfg.add_edge(b0, b1, EdgeType::Fallthrough);
3049
3050 store_cfg(&mut conn, function_id, "test_hash_123", &cfg).unwrap();
3052
3053 let block_count: i64 = conn.query_row(
3055 "SELECT COUNT(*) FROM cfg_blocks WHERE function_id = ?",
3056 params![function_id],
3057 |row| row.get(0),
3058 ).unwrap();
3059
3060 assert_eq!(block_count, 2, "Should have 2 blocks");
3061
3062 let edge_count: i64 = conn.query_row(
3064 "SELECT COUNT(*) FROM cfg_edges",
3065 [],
3066 |row| row.get(0),
3067 ).unwrap();
3068
3069 assert_eq!(edge_count, 1, "Should have 1 edge");
3070
3071 assert!(function_exists(&conn, function_id));
3076 assert!(!function_exists(&conn, 9999));
3077
3078 let loaded_cfg = load_cfg_from_db_with_conn(&conn, function_id).unwrap();
3080
3081 assert_eq!(loaded_cfg.node_count(), 2);
3082 assert_eq!(loaded_cfg.edge_count(), 1);
3083 }
3084
3085 #[test]
3086 fn test_store_cfg_incremental_update_clears_old_data() {
3087 use crate::cfg::{BasicBlock, BlockKind, Cfg, EdgeType, Terminator};
3088
3089 let mut conn = Connection::open_in_memory().unwrap();
3090
3091 conn.execute(
3093 "CREATE TABLE magellan_meta (
3094 id INTEGER PRIMARY KEY CHECK (id = 1),
3095 magellan_schema_version INTEGER NOT NULL,
3096 sqlitegraph_schema_version INTEGER NOT NULL,
3097 created_at INTEGER NOT NULL
3098 )",
3099 [],
3100 ).unwrap();
3101
3102 conn.execute(
3103 "CREATE TABLE graph_entities (
3104 id INTEGER PRIMARY KEY AUTOINCREMENT,
3105 kind TEXT NOT NULL,
3106 name TEXT NOT NULL,
3107 file_path TEXT,
3108 data TEXT NOT NULL
3109 )",
3110 [],
3111 ).unwrap();
3112
3113 conn.execute(
3114 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3115 VALUES (1, ?, ?, ?)",
3116 params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
3117 ).unwrap();
3118
3119 create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
3120
3121 conn.execute(
3122 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3123 params!("function", "test_func", "test.rs", "{}"),
3124 ).unwrap();
3125
3126 let function_id: i64 = conn.last_insert_rowid();
3127
3128 let mut cfg1 = Cfg::new();
3130 let b0 = cfg1.add_node(BasicBlock {
3131 id: 0,
3132 kind: BlockKind::Entry,
3133 statements: vec![],
3134 terminator: Terminator::Goto { target: 1 },
3135 source_location: None,
3136 });
3137 let b1 = cfg1.add_node(BasicBlock {
3138 id: 1,
3139 kind: BlockKind::Exit,
3140 statements: vec![],
3141 terminator: Terminator::Return,
3142 source_location: None,
3143 });
3144 cfg1.add_edge(b0, b1, EdgeType::Fallthrough);
3145
3146 store_cfg(&mut conn, function_id, "hash_v1", &cfg1).unwrap();
3147
3148 let block_count_v1: i64 = conn.query_row(
3149 "SELECT COUNT(*) FROM cfg_blocks WHERE function_id = ?",
3150 params![function_id],
3151 |row| row.get(0),
3152 ).unwrap();
3153
3154 assert_eq!(block_count_v1, 2);
3155
3156 let mut cfg2 = Cfg::new();
3158 let b0 = cfg2.add_node(BasicBlock {
3159 id: 0,
3160 kind: BlockKind::Entry,
3161 statements: vec![],
3162 terminator: Terminator::Goto { target: 1 },
3163 source_location: None,
3164 });
3165 let b1 = cfg2.add_node(BasicBlock {
3166 id: 1,
3167 kind: BlockKind::Normal,
3168 statements: vec![],
3169 terminator: Terminator::Goto { target: 2 },
3170 source_location: None,
3171 });
3172 let b2 = cfg2.add_node(BasicBlock {
3173 id: 2,
3174 kind: BlockKind::Exit,
3175 statements: vec![],
3176 terminator: Terminator::Return,
3177 source_location: None,
3178 });
3179 cfg2.add_edge(b0, b1, EdgeType::Fallthrough);
3180 cfg2.add_edge(b1, b2, EdgeType::Fallthrough);
3181
3182 store_cfg(&mut conn, function_id, "hash_v3", &cfg2).unwrap();
3183
3184 let block_count_v3: i64 = conn.query_row(
3185 "SELECT COUNT(*) FROM cfg_blocks WHERE function_id = ?",
3186 params![function_id],
3187 |row| row.get(0),
3188 ).unwrap();
3189
3190 assert_eq!(block_count_v3, 3);
3192
3193 }
3196
3197 fn create_test_db_with_schema() -> Connection {
3205 let mut conn = Connection::open_in_memory().unwrap();
3206
3207 conn.execute(
3209 "CREATE TABLE magellan_meta (
3210 id INTEGER PRIMARY KEY CHECK (id = 1),
3211 magellan_schema_version INTEGER NOT NULL,
3212 sqlitegraph_schema_version INTEGER NOT NULL,
3213 created_at INTEGER NOT NULL
3214 )",
3215 [],
3216 ).unwrap();
3217
3218 conn.execute(
3219 "CREATE TABLE graph_entities (
3220 id INTEGER PRIMARY KEY AUTOINCREMENT,
3221 kind TEXT NOT NULL,
3222 name TEXT NOT NULL,
3223 file_path TEXT,
3224 data TEXT NOT NULL
3225 )",
3226 [],
3227 ).unwrap();
3228
3229 conn.execute(
3231 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3232 VALUES (1, ?, ?, ?)",
3233 params![7, 3, 0], ).unwrap();
3235
3236 conn.execute(
3239 "CREATE TABLE cfg_blocks (
3240 id INTEGER PRIMARY KEY AUTOINCREMENT,
3241 function_id INTEGER NOT NULL,
3242 kind TEXT NOT NULL,
3243 terminator TEXT NOT NULL,
3244 byte_start INTEGER NOT NULL,
3245 byte_end INTEGER NOT NULL,
3246 start_line INTEGER NOT NULL,
3247 start_col INTEGER NOT NULL,
3248 end_line INTEGER NOT NULL,
3249 end_col INTEGER NOT NULL,
3250 FOREIGN KEY (function_id) REFERENCES graph_entities(id)
3251 )",
3252 [],
3253 ).unwrap();
3254
3255 conn.execute(
3257 "CREATE TABLE graph_edges (
3258 id INTEGER PRIMARY KEY AUTOINCREMENT,
3259 from_id INTEGER NOT NULL,
3260 to_id INTEGER NOT NULL,
3261 edge_type TEXT NOT NULL,
3262 data TEXT
3263 )",
3264 [],
3265 ).unwrap();
3266
3267 create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
3269
3270 conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
3272
3273 conn
3274 }
3275
3276 #[test]
3279 fn test_resolve_function_by_id() {
3280 let conn = create_test_db_with_schema();
3281
3282 conn.execute(
3284 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3285 params!("function", "my_func", "test.rs", "{}"),
3286 ).unwrap();
3287 let function_id: i64 = conn.last_insert_rowid();
3288
3289 let result = resolve_function_name_with_conn(&conn, &function_id.to_string()).unwrap();
3291 assert_eq!(result, function_id);
3292 }
3293
3294 #[test]
3295 fn test_resolve_function_by_name() {
3296 let conn = create_test_db_with_schema();
3297
3298 conn.execute(
3301 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3302 params!("Symbol", "test_function", "test.rs", r#"{"kind":"Function"}"#),
3303 ).unwrap();
3304 let function_id: i64 = conn.last_insert_rowid();
3305
3306 let result = resolve_function_name_with_conn(&conn, "test_function").unwrap();
3308 assert_eq!(result, function_id);
3309 }
3310
3311 #[test]
3312 fn test_resolve_function_not_found() {
3313 let conn = create_test_db_with_schema();
3314
3315 let result = resolve_function_name_with_conn(&conn, "nonexistent_func");
3317
3318 assert!(result.is_err(), "Should return error for non-existent function");
3319 let err_msg = result.unwrap_err().to_string();
3320 assert!(err_msg.contains("not found") || err_msg.contains("not found in database"));
3321 }
3322
3323 #[test]
3324 fn test_resolve_function_numeric_string() {
3325 let conn = create_test_db_with_schema();
3326
3327 conn.execute(
3329 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3330 params!("function", "func123", "test.rs", "{}"),
3331 ).unwrap();
3332
3333 let result = resolve_function_name_with_conn(&conn, "123").unwrap();
3335 assert_eq!(result, 123);
3336
3337 conn.execute(
3339 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3340 params!("function", "another_func", "test.rs", "{}"),
3341 ).unwrap();
3342 let _id_456 = conn.last_insert_rowid();
3343
3344 let result = resolve_function_name_with_conn(&conn, "999").unwrap();
3347 assert_eq!(result, 999, "Should return numeric ID directly");
3348 }
3349
3350 #[test]
3351 fn test_load_cfg_not_found() {
3352 let conn = create_test_db_with_schema();
3353
3354 let result = load_cfg_from_db_with_conn(&conn, 99999);
3356
3357 assert!(result.is_err(), "Should return error for function with no CFG");
3358 let err_msg = result.unwrap_err().to_string();
3359 assert!(err_msg.contains("No CFG blocks found") || err_msg.contains("not found"));
3360 }
3361
3362 #[test]
3363 fn test_load_cfg_empty_terminator() {
3364 use crate::cfg::Terminator;
3365
3366 let conn = create_test_db_with_schema();
3367
3368 conn.execute(
3370 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3371 params!("function", "empty_term_func", "test.rs", "{}"),
3372 ).unwrap();
3373 let function_id: i64 = conn.last_insert_rowid();
3374
3375 conn.execute(
3377 "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
3378 start_line, start_col, end_line, end_col)
3379 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
3380 params!(function_id, "return", "return", 0, 10, 1, 0, 1, 10),
3381 ).unwrap();
3382
3383 let cfg = load_cfg_from_db_with_conn(&conn, function_id).unwrap();
3385
3386 assert_eq!(cfg.node_count(), 1);
3387 let block = &cfg[petgraph::graph::NodeIndex::new(0)];
3388 assert!(matches!(block.terminator, Terminator::Return));
3389 }
3390
3391 #[test]
3392 fn test_load_cfg_with_multiple_edge_types() {
3393 use crate::cfg::EdgeType;
3394
3395 let conn = create_test_db_with_schema();
3396
3397 conn.execute(
3399 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3400 params!("function", "edge_types_func", "test.rs", "{}"),
3401 ).unwrap();
3402 let function_id: i64 = conn.last_insert_rowid();
3403
3404 conn.execute(
3406 "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
3407 start_line, start_col, end_line, end_col)
3408 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
3409 params!(function_id, "entry", "conditional", 0, 10, 1, 0, 1, 10),
3410 ).unwrap();
3411 let _block_0_id: i64 = conn.last_insert_rowid();
3412
3413 conn.execute(
3414 "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
3415 start_line, start_col, end_line, end_col)
3416 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
3417 params!(function_id, "block", "fallthrough", 10, 20, 2, 0, 2, 10),
3418 ).unwrap();
3419 let _block_1_id: i64 = conn.last_insert_rowid();
3420
3421 conn.execute(
3422 "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
3423 start_line, start_col, end_line, end_col)
3424 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
3425 params!(function_id, "block", "call", 20, 30, 3, 0, 3, 10),
3426 ).unwrap();
3427 let _block_2_id: i64 = conn.last_insert_rowid();
3428
3429 conn.execute(
3430 "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
3431 start_line, start_col, end_line, end_col)
3432 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
3433 params!(function_id, "return", "return", 30, 40, 4, 0, 4, 10),
3434 ).unwrap();
3435 let _block_3_id: i64 = conn.last_insert_rowid();
3436
3437 let cfg = load_cfg_from_db_with_conn(&conn, function_id).unwrap();
3439
3440 assert_eq!(cfg.node_count(), 4);
3441 assert_eq!(cfg.edge_count(), 4);
3442
3443 use petgraph::visit::EdgeRef;
3448 let edges: Vec<_> = cfg.edge_references().map(|e| {
3449 (e.source().index(), e.target().index(), *e.weight())
3450 }).collect();
3451
3452 assert!(edges.contains(&(0, 1, EdgeType::TrueBranch)));
3453 assert!(edges.contains(&(0, 2, EdgeType::FalseBranch)));
3454 assert!(edges.contains(&(1, 2, EdgeType::Fallthrough)));
3455 assert!(edges.contains(&(2, 3, EdgeType::Call)));
3456 }
3457
3458 #[test]
3459 fn test_get_function_name() {
3460 let conn = create_test_db_with_schema();
3461
3462 conn.execute(
3464 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3465 params!("function", "my_test_func", "test.rs", "{}"),
3466 ).unwrap();
3467 let function_id: i64 = conn.last_insert_rowid();
3468
3469 let name = get_function_name(&conn, function_id);
3471 assert_eq!(name, Some("my_test_func".to_string()));
3472
3473 let name = get_function_name(&conn, 9999);
3475 assert_eq!(name, None);
3476 }
3477
3478 #[test]
3479 fn test_get_path_elements() {
3480 let conn = create_test_db_with_schema();
3481
3482 conn.execute(
3484 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3485 params!("function", "path_test_func", "test.rs", "{}"),
3486 ).unwrap();
3487 let function_id: i64 = conn.last_insert_rowid();
3488
3489 conn.execute(
3491 "INSERT INTO cfg_paths (path_id, function_id, path_kind, entry_block, exit_block, length, created_at)
3492 VALUES (?, ?, ?, ?, ?, ?, ?)",
3493 params!("test_path_abc123", function_id, "normal", 0, 2, 3, 1000),
3494 ).unwrap();
3495
3496 conn.execute(
3498 "INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
3499 params!("test_path_abc123", 0, 0),
3500 ).unwrap();
3501 conn.execute(
3502 "INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
3503 params!("test_path_abc123", 1, 1),
3504 ).unwrap();
3505 conn.execute(
3506 "INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
3507 params!("test_path_abc123", 2, 2),
3508 ).unwrap();
3509
3510 let blocks = get_path_elements(&conn, "test_path_abc123").unwrap();
3512 assert_eq!(blocks, vec![0, 1, 2]);
3513
3514 let result = get_path_elements(&conn, "nonexistent_path");
3516 assert!(result.is_err());
3517 }
3518
3519 #[test]
3520 fn test_compute_path_impact_from_db() {
3521 use crate::cfg::{BasicBlock, BlockKind, Terminator};
3522
3523 let conn = create_test_db_with_schema();
3524
3525 conn.execute(
3527 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3528 params!("function", "impact_test_func", "test.rs", "{}"),
3529 ).unwrap();
3530 let function_id: i64 = conn.last_insert_rowid();
3531
3532 let mut cfg = crate::cfg::Cfg::new();
3534 let b0 = cfg.add_node(BasicBlock {
3535 id: 0,
3536 kind: BlockKind::Entry,
3537 statements: vec![],
3538 terminator: Terminator::Goto { target: 1 },
3539 source_location: None,
3540 });
3541 let b1 = cfg.add_node(BasicBlock {
3542 id: 1,
3543 kind: BlockKind::Normal,
3544 statements: vec![],
3545 terminator: Terminator::Goto { target: 2 },
3546 source_location: None,
3547 });
3548 let b2 = cfg.add_node(BasicBlock {
3549 id: 2,
3550 kind: BlockKind::Normal,
3551 statements: vec![],
3552 terminator: Terminator::Goto { target: 3 },
3553 source_location: None,
3554 });
3555 let b3 = cfg.add_node(BasicBlock {
3556 id: 3,
3557 kind: BlockKind::Exit,
3558 statements: vec![],
3559 terminator: Terminator::Return,
3560 source_location: None,
3561 });
3562 cfg.add_edge(b0, b1, crate::cfg::EdgeType::Fallthrough);
3563 cfg.add_edge(b1, b2, crate::cfg::EdgeType::Fallthrough);
3564 cfg.add_edge(b2, b3, crate::cfg::EdgeType::Fallthrough);
3565
3566 conn.execute(
3568 "INSERT INTO cfg_paths (path_id, function_id, path_kind, entry_block, exit_block, length, created_at)
3569 VALUES (?, ?, ?, ?, ?, ?, ?)",
3570 params!("impact_test_path", function_id, "normal", 0, 3, 3, 1000),
3571 ).unwrap();
3572
3573 conn.execute(
3574 "INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
3575 params!("impact_test_path", 0, 0),
3576 ).unwrap();
3577 conn.execute(
3578 "INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
3579 params!("impact_test_path", 1, 1),
3580 ).unwrap();
3581 conn.execute(
3582 "INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
3583 params!("impact_test_path", 2, 3),
3584 ).unwrap();
3585
3586 let impact = compute_path_impact_from_db(&conn, "impact_test_path", &cfg, None).unwrap();
3588
3589 assert_eq!(impact.path_id, "impact_test_path");
3590 assert_eq!(impact.path_length, 3);
3591 assert!(impact.unique_blocks_affected.contains(&2));
3593 }
3594
3595 #[test]
3598 fn test_load_cfg_missing_cfg_blocks_table() {
3599 let conn = Connection::open_in_memory().unwrap();
3600
3601 conn.execute(
3603 "CREATE TABLE magellan_meta (
3604 id INTEGER PRIMARY KEY CHECK (id = 1),
3605 magellan_schema_version INTEGER NOT NULL,
3606 sqlitegraph_schema_version INTEGER NOT NULL,
3607 created_at INTEGER NOT NULL
3608 )",
3609 [],
3610 ).unwrap();
3611
3612 conn.execute(
3613 "CREATE TABLE graph_entities (
3614 id INTEGER PRIMARY KEY AUTOINCREMENT,
3615 kind TEXT NOT NULL,
3616 name TEXT NOT NULL,
3617 file_path TEXT,
3618 data TEXT NOT NULL
3619 )",
3620 [],
3621 ).unwrap();
3622
3623 conn.execute(
3624 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3625 VALUES (1, ?, ?, ?)",
3626 params![6, 3, 0], ).unwrap();
3628
3629 conn.execute(
3631 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3632 params!("function", "test_func", "test.rs", "{}"),
3633 ).unwrap();
3634 let function_id: i64 = conn.last_insert_rowid();
3635
3636 let result = load_cfg_from_db_with_conn(&conn, function_id);
3638 assert!(result.is_err(), "Should fail when cfg_blocks table missing");
3639
3640 let err_msg = result.unwrap_err().to_string();
3641 assert!(err_msg.contains("cfg_blocks") || err_msg.contains("prepare"),
3643 "Error should mention cfg_blocks or prepare: {}", err_msg);
3644 }
3645
3646 #[test]
3647 fn test_load_cfg_function_not_found() {
3648 let conn = create_test_db_with_schema();
3649
3650 let result = load_cfg_from_db_with_conn(&conn, 99999);
3652 assert!(result.is_err(), "Should fail for non-existent function");
3653
3654 let err_msg = result.unwrap_err().to_string();
3655 assert!(err_msg.contains("No CFG blocks found") || err_msg.contains("not found"),
3656 "Error should mention missing CFG: {}", err_msg);
3657 assert!(err_msg.contains("magellan watch"),
3658 "Error should suggest running magellan watch: {}", err_msg);
3659 }
3660
3661 #[test]
3662 fn test_load_cfg_empty_blocks() {
3663 let conn = create_test_db_with_schema();
3664
3665 conn.execute(
3667 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3668 params!("function", "func_without_cfg", "test.rs", "{}"),
3669 ).unwrap();
3670 let function_id: i64 = conn.last_insert_rowid();
3671
3672 let result = load_cfg_from_db_with_conn(&conn, function_id);
3674 assert!(result.is_err(), "Should fail when no CFG blocks exist");
3675
3676 let err_msg = result.unwrap_err().to_string();
3677 assert!(err_msg.contains("No CFG blocks found"),
3678 "Error should mention no CFG blocks: {}", err_msg);
3679 assert!(err_msg.contains("magellan watch"),
3680 "Error should suggest running magellan watch: {}", err_msg);
3681 }
3682
3683 #[test]
3684 fn test_resolve_function_missing_with_helpful_message() {
3685 let conn = create_test_db_with_schema();
3686
3687 let result = resolve_function_name_with_conn(&conn, "nonexistent_function");
3689 assert!(result.is_err(), "Should fail for non-existent function");
3690
3691 let err_msg = result.unwrap_err().to_string();
3692 assert!(err_msg.contains("not found") || err_msg.contains("not found in database"),
3693 "Error should mention function not found: {}", err_msg);
3694 }
3695
3696 #[test]
3697 fn test_open_database_old_magellan_schema() {
3698 let conn = Connection::open_in_memory().unwrap();
3699
3700 conn.execute(
3702 "CREATE TABLE magellan_meta (
3703 id INTEGER PRIMARY KEY CHECK (id = 1),
3704 magellan_schema_version INTEGER NOT NULL,
3705 sqlitegraph_schema_version INTEGER NOT NULL,
3706 created_at INTEGER NOT NULL
3707 )",
3708 [],
3709 ).unwrap();
3710
3711 conn.execute(
3712 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3713 VALUES (1, 6, 3, 0)", [],
3715 ).unwrap();
3716
3717 conn.execute(
3719 "CREATE TABLE cfg_blocks (
3720 id INTEGER PRIMARY KEY AUTOINCREMENT,
3721 function_id INTEGER NOT NULL,
3722 kind TEXT NOT NULL,
3723 terminator TEXT NOT NULL,
3724 byte_start INTEGER NOT NULL,
3725 byte_end INTEGER NOT NULL,
3726 start_line INTEGER NOT NULL,
3727 start_col INTEGER NOT NULL,
3728 end_line INTEGER NOT NULL,
3729 end_col INTEGER NOT NULL,
3730 FOREIGN KEY (function_id) REFERENCES graph_entities(id)
3731 )",
3732 [],
3733 ).unwrap();
3734
3735 drop(conn);
3737 let db_file = tempfile::NamedTempFile::new().unwrap();
3738 {
3739 let conn = Connection::open(db_file.path()).unwrap();
3740 conn.execute(
3741 "CREATE TABLE magellan_meta (
3742 id INTEGER PRIMARY KEY CHECK (id = 1),
3743 magellan_schema_version INTEGER NOT NULL,
3744 sqlitegraph_schema_version INTEGER NOT NULL,
3745 created_at INTEGER NOT NULL
3746 )",
3747 [],
3748 ).unwrap();
3749 conn.execute(
3750 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3751 VALUES (1, 6, 3, 0)",
3752 [],
3753 ).unwrap();
3754 conn.execute(
3755 "CREATE TABLE graph_entities (
3756 id INTEGER PRIMARY KEY AUTOINCREMENT,
3757 kind TEXT NOT NULL,
3758 name TEXT NOT NULL,
3759 file_path TEXT,
3760 data TEXT NOT NULL
3761 )",
3762 [],
3763 ).unwrap();
3764 }
3765
3766 let result = MirageDb::open(db_file.path());
3767 assert!(result.is_err(), "Should fail with old Magellan schema");
3768
3769 let err_msg = result.unwrap_err().to_string();
3770 assert!(err_msg.contains("too old") || err_msg.contains("minimum"),
3771 "Error should mention schema too old: {}", err_msg);
3772 assert!(err_msg.contains("magellan watch"),
3773 "Error should suggest running magellan watch: {}", err_msg);
3774 }
3775
3776 #[test]
3779 fn test_backend_detect_sqlite_header() {
3780 use std::io::Write;
3781
3782 let temp_file = tempfile::NamedTempFile::new().unwrap();
3784 let mut file = std::fs::File::create(temp_file.path()).unwrap();
3785 file.write_all(b"SQLite format 3\0").unwrap();
3786 file.sync_all().unwrap();
3787
3788 let backend = BackendFormat::detect(temp_file.path()).unwrap();
3789 assert_eq!(backend, BackendFormat::SQLite, "Should detect SQLite format");
3790 }
3791
3792 #[test]
3793 fn test_backend_detect_native_v3_header() {
3794 use std::io::Write;
3795
3796 let temp_file = tempfile::NamedTempFile::new().unwrap();
3798 let mut file = std::fs::File::create(temp_file.path()).unwrap();
3799 file.write_all(b"MIRAGE-NATIVE-V3\0").unwrap();
3800 file.sync_all().unwrap();
3801
3802 let backend = BackendFormat::detect(temp_file.path()).unwrap();
3803 assert_eq!(backend, BackendFormat::NativeV3, "Should detect native-v3 format");
3804 }
3805
3806 #[test]
3807 fn test_backend_detect_nonexistent_file() {
3808 let backend = BackendFormat::detect(Path::new("/nonexistent/path/to/file.db")).unwrap();
3809 assert_eq!(backend, BackendFormat::Unknown, "Non-existent file should be Unknown");
3810 }
3811
3812 #[test]
3813 fn test_backend_detect_empty_file() {
3814 let temp_file = tempfile::NamedTempFile::new().unwrap();
3816 let backend = BackendFormat::detect(temp_file.path()).unwrap();
3819 assert_eq!(backend, BackendFormat::Unknown, "Empty file should be Unknown");
3820 }
3821
3822 #[test]
3823 fn test_backend_detect_partial_header() {
3824 use std::io::Write;
3825
3826 let temp_file = tempfile::NamedTempFile::new().unwrap();
3828 let mut file = std::fs::File::create(temp_file.path()).unwrap();
3829 file.write_all(b"SQLite").unwrap(); file.sync_all().unwrap();
3831
3832 let backend = BackendFormat::detect(temp_file.path()).unwrap();
3833 assert_eq!(backend, BackendFormat::Unknown, "Partial header should be Unknown");
3834 }
3835
3836 #[test]
3837 fn test_backend_equality() {
3838 assert_eq!(BackendFormat::SQLite, BackendFormat::SQLite);
3839 assert_eq!(BackendFormat::NativeV3, BackendFormat::NativeV3);
3840 assert_eq!(BackendFormat::Unknown, BackendFormat::Unknown);
3841
3842 assert_ne!(BackendFormat::SQLite, BackendFormat::NativeV3);
3843 assert_ne!(BackendFormat::SQLite, BackendFormat::Unknown);
3844 assert_ne!(BackendFormat::NativeV3, BackendFormat::Unknown);
3845 }
3846}