1pub mod paths;
14
15#[cfg(feature = "backend-geometric")]
17pub mod geometric;
18#[cfg(feature = "backend-sqlite")]
19pub mod sqlite_backend;
20
21#[cfg(all(feature = "geometric", not(feature = "backend-geometric")))]
23pub mod geometric;
24#[cfg(all(feature = "sqlite", not(feature = "backend-sqlite")))]
25pub mod sqlite_backend;
26
27use anyhow::{Context, Result};
28use rusqlite::{params, Connection, OptionalExtension};
29use std::path::Path;
30
31use sqlitegraph::{open_graph, GraphBackend, GraphConfig, SnapshotId};
33
34#[cfg(feature = "backend-geometric")]
40pub use geometric::GeometricStorage;
41#[cfg(feature = "backend-sqlite")]
42pub use sqlite_backend::SqliteStorage;
43
44#[allow(unused_imports)]
48pub use paths::{
49 get_cached_paths, invalidate_function_paths, store_paths, update_function_paths_if_changed,
50 PathCache,
51};
52
53pub trait StorageTrait {
82 fn get_cfg_blocks(&self, function_id: i64) -> Result<Vec<CfgBlockData>>;
96
97 fn get_entity(&self, entity_id: i64) -> Option<sqlitegraph::GraphEntity>;
110
111 fn get_cached_paths(&self, _function_id: i64) -> Result<Option<Vec<crate::cfg::Path>>> {
126 Ok(None) }
128
129 fn get_callees(&self, _function_id: i64) -> Result<Vec<i64>> {
147 Ok(Vec::new())
148 }
149}
150
151#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
157pub struct CfgBlockData {
158 pub id: i64,
160 pub kind: String,
162 pub terminator: String,
164 pub byte_start: u64,
166 pub byte_end: u64,
168 pub start_line: u64,
170 pub start_col: u64,
172 pub end_line: u64,
174 pub end_col: u64,
176 pub coord_x: i64,
179 pub coord_y: i64,
181 pub coord_z: i64,
183}
184
185#[derive(Debug)]
192#[allow(clippy::large_enum_variant)]
193pub enum Backend {
194 #[cfg(feature = "backend-sqlite")]
196 Sqlite(SqliteStorage),
197 #[cfg(feature = "backend-geometric")]
199 Geometric(GeometricStorage),
200}
201
202impl Backend {
203 pub fn detect_and_open(db_path: &Path) -> Result<Self> {
226 use magellan::migrate_backend_cmd::detect_backend_format;
227
228 #[cfg(feature = "backend-geometric")]
230 let is_geo = db_path.extension().and_then(|e| e.to_str()) == Some("geo");
231
232 #[cfg(feature = "backend-geometric")]
233 {
234 if is_geo {
235 return GeometricStorage::open(db_path).map(Backend::Geometric);
236 }
237 }
238
239 let sqlite_detected = detect_backend_format(db_path).is_ok();
241
242 #[cfg(feature = "backend-sqlite")]
243 {
244 if sqlite_detected {
245 return SqliteStorage::open(db_path).map(Backend::Sqlite);
246 } else {
247 return Err(anyhow::anyhow!(
248 "Unsupported database format; use a SQLite .db"
249 ));
250 }
251 }
252
253 #[cfg(not(any(feature = "backend-sqlite", feature = "backend-geometric")))]
254 {
255 Err(anyhow::anyhow!("No storage backend feature enabled"))
256 }
257 }
258
259 pub fn is_geometric(&self) -> bool {
261 match self {
262 #[cfg(feature = "backend-geometric")]
263 Backend::Geometric(_) => true,
264 _ => false,
265 }
266 }
267
268 pub fn is_sqlite(&self) -> bool {
270 match self {
271 #[cfg(feature = "backend-sqlite")]
272 Backend::Sqlite(_) => true,
273 #[cfg(feature = "backend-geometric")]
274 Backend::Geometric(_) => false,
275 #[cfg(not(feature = "backend-sqlite"))]
276 _ => false,
277 }
278 }
279
280 pub fn get_cfg_blocks(&self, function_id: i64) -> Result<Vec<CfgBlockData>> {
282 match self {
283 #[cfg(feature = "backend-sqlite")]
284 Backend::Sqlite(s) => s.get_cfg_blocks(function_id),
285 #[cfg(feature = "backend-geometric")]
286 Backend::Geometric(g) => g.get_cfg_blocks(function_id),
287 #[allow(unreachable_patterns)]
288 _ => Err(anyhow::anyhow!("No storage backend available")),
289 }
290 }
291
292 pub fn get_entity(&self, entity_id: i64) -> Option<sqlitegraph::GraphEntity> {
294 match self {
295 #[cfg(feature = "backend-sqlite")]
296 Backend::Sqlite(s) => s.get_entity(entity_id),
297 #[cfg(feature = "backend-geometric")]
298 Backend::Geometric(g) => g.get_entity(entity_id),
299 #[allow(unreachable_patterns)]
300 _ => None,
301 }
302 }
303
304 pub fn get_cached_paths(&self, function_id: i64) -> Result<Option<Vec<crate::cfg::Path>>> {
306 match self {
307 #[cfg(feature = "backend-sqlite")]
308 Backend::Sqlite(s) => s.get_cached_paths(function_id),
309 #[cfg(feature = "backend-geometric")]
310 Backend::Geometric(g) => g.get_cached_paths(function_id),
311 #[allow(unreachable_patterns)]
312 _ => Err(anyhow::anyhow!("No storage backend available")),
313 }
314 }
315
316 pub fn get_callees(&self, function_id: i64) -> Result<Vec<i64>> {
318 match self {
319 #[cfg(feature = "backend-sqlite")]
320 Backend::Sqlite(s) => s.get_callees(function_id),
321 #[cfg(feature = "backend-geometric")]
322 Backend::Geometric(g) => g.get_callees(function_id),
323 #[allow(unreachable_patterns)]
324 _ => Ok(Vec::new()),
325 }
326 }
327}
328
329impl StorageTrait for Backend {
331 fn get_cfg_blocks(&self, function_id: i64) -> Result<Vec<CfgBlockData>> {
332 self.get_cfg_blocks(function_id)
333 }
334
335 fn get_entity(&self, entity_id: i64) -> Option<sqlitegraph::GraphEntity> {
336 self.get_entity(entity_id)
337 }
338
339 fn get_cached_paths(&self, function_id: i64) -> Result<Option<Vec<crate::cfg::Path>>> {
340 self.get_cached_paths(function_id)
341 }
342
343 fn get_callees(&self, function_id: i64) -> Result<Vec<i64>> {
344 self.get_callees(function_id)
345 }
346}
347
348#[derive(Debug, Clone, Copy, PartialEq, Eq)]
353pub enum BackendFormat {
354 SQLite,
356 Geometric,
358 Unknown,
360}
361
362impl BackendFormat {
363 pub fn detect(path: &Path) -> Result<Self> {
371 if !path.exists() {
372 return Ok(BackendFormat::Unknown);
373 }
374
375 if path.extension().and_then(|e| e.to_str()) == Some("geo") {
377 return Ok(BackendFormat::Geometric);
378 }
379
380 let mut file = std::fs::File::open(path)?;
381 let mut header = [0u8; 16];
382 let bytes_read = std::io::Read::read(&mut file, &mut header)?;
383
384 if bytes_read < header.len() {
385 return Ok(BackendFormat::Unknown);
386 }
387
388 Ok(if &header[..15] == b"SQLite format 3" {
390 BackendFormat::SQLite
391 } else {
392 BackendFormat::Unknown
393 })
394 }
395}
396
397pub const MIRAGE_SCHEMA_VERSION: i32 = 1;
399
400pub const MIN_MAGELLAN_SCHEMA_VERSION: i32 = 7;
403
404pub const TEST_MAGELLAN_SCHEMA_VERSION: i32 = MIN_MAGELLAN_SCHEMA_VERSION;
406
407pub const REQUIRED_MAGELLAN_SCHEMA_VERSION: i32 = TEST_MAGELLAN_SCHEMA_VERSION;
409
410pub const REQUIRED_SQLITEGRAPH_SCHEMA_VERSION: i32 = 3;
412
413pub struct MirageDb {
418 storage: Backend,
421
422 graph_backend: Box<dyn GraphBackend>,
425
426 snapshot_id: SnapshotId,
428
429 #[cfg(feature = "backend-sqlite")]
432 conn: Option<Connection>,
433}
434
435impl std::fmt::Debug for MirageDb {
436 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
437 f.debug_struct("MirageDb")
438 .field("snapshot_id", &self.snapshot_id)
439 .field("storage", &self.storage)
440 .field("graph_backend", &"<GraphBackend>")
441 .finish()
442 }
443}
444
445impl MirageDb {
446 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
455 let path = path.as_ref();
456 if !path.exists() {
457 anyhow::bail!("Database not found: {}", path.display());
458 }
459
460 let storage = Backend::detect_and_open(path).context("Failed to open storage backend")?;
462
463 let detected_backend =
465 BackendFormat::detect(path).context("Failed to detect backend format")?;
466
467 #[cfg(feature = "backend-geometric")]
469 if detected_backend == BackendFormat::Geometric {
470 let snapshot_id = SnapshotId::current();
471
472 let graph_backend = create_geometric_stub_backend();
476
477 #[cfg(feature = "backend-sqlite")]
478 let conn = None;
479
480 return Ok(Self {
481 storage,
482 graph_backend,
483 snapshot_id,
484 #[cfg(feature = "backend-sqlite")]
485 conn,
486 });
487 }
488
489 let cfg = match detected_backend {
491 BackendFormat::SQLite => GraphConfig::sqlite(),
492 BackendFormat::Geometric => {
493 GraphConfig::native()
495 }
496 BackendFormat::Unknown => {
497 anyhow::bail!(
498 "Unknown database format: {}. Cannot determine backend.",
499 path.display()
500 );
501 }
502 };
503
504 let graph_backend = open_graph(path, &cfg).context("Failed to open graph database")?;
506
507 let snapshot_id = SnapshotId::current();
508
509 #[cfg(feature = "backend-sqlite")]
511 let conn = {
512 let mut conn = Connection::open(path).context("Failed to open SQLite connection")?;
513 Self::validate_schema_sqlite(&mut conn, path)?;
514 Some(conn)
515 };
516
517 Ok(Self {
518 storage,
519 graph_backend,
520 snapshot_id,
521 #[cfg(feature = "backend-sqlite")]
522 conn,
523 })
524 }
525
526 #[cfg(feature = "backend-sqlite")]
528 fn validate_schema_sqlite(conn: &mut Connection, _path: &Path) -> Result<()> {
529 let mirage_meta_exists: bool = conn
531 .query_row(
532 "SELECT 1 FROM sqlite_master WHERE type='table' AND name='mirage_meta'",
533 [],
534 |row| row.get(0),
535 )
536 .optional()?
537 .unwrap_or(0)
538 == 1;
539
540 let mirage_version: i32 = if mirage_meta_exists {
542 conn.query_row(
543 "SELECT mirage_schema_version FROM mirage_meta WHERE id = 1",
544 [],
545 |row| row.get(0),
546 )
547 .optional()?
548 .flatten()
549 .unwrap_or(0)
550 } else {
551 0
552 };
553
554 if mirage_version > MIRAGE_SCHEMA_VERSION {
555 anyhow::bail!(
556 "Database schema version {} is newer than supported version {}.
557 Please update Mirage.",
558 mirage_version,
559 MIRAGE_SCHEMA_VERSION
560 );
561 }
562
563 let magellan_version: i32 = conn
565 .query_row(
566 "SELECT magellan_schema_version FROM magellan_meta WHERE id = 1",
567 [],
568 |row| row.get(0),
569 )
570 .optional()?
571 .flatten()
572 .unwrap_or(0);
573
574 if magellan_version < MIN_MAGELLAN_SCHEMA_VERSION {
575 anyhow::bail!(
576 "Magellan schema version {} is too old (minimum {}). \
577 Please update Magellan and run 'magellan watch' to rebuild CFGs.",
578 magellan_version,
579 MIN_MAGELLAN_SCHEMA_VERSION
580 );
581 }
582
583 let cfg_blocks_exists: bool = conn
585 .query_row(
586 "SELECT 1 FROM sqlite_master WHERE type='table' AND name='cfg_blocks'",
587 [],
588 |row| row.get(0),
589 )
590 .optional()?
591 .unwrap_or(0)
592 == 1;
593
594 if !cfg_blocks_exists {
595 anyhow::bail!(
596 "CFG blocks table not found. Magellan schema v7+ required. \
597 Run 'magellan watch' to build CFGs."
598 );
599 }
600
601 if !mirage_meta_exists {
604 create_schema(conn, magellan_version)?;
605 } else if mirage_version < MIRAGE_SCHEMA_VERSION {
606 migrate_schema(conn)?;
607 }
608
609 Ok(())
610 }
611
612 #[cfg(feature = "backend-sqlite")]
616 pub fn conn(&self) -> Result<&Connection, anyhow::Error> {
617 self.conn.as_ref().ok_or_else(|| {
618 anyhow::anyhow!(
619 "Direct Connection access deprecated. Use storage() for CFG queries or backend() for entity queries."
620 )
621 })
622 }
623
624 #[cfg(feature = "backend-sqlite")]
628 pub fn conn_mut(&mut self) -> Result<&mut Connection, anyhow::Error> {
629 self.conn.as_mut().ok_or_else(|| {
630 anyhow::anyhow!(
631 "Direct Connection access deprecated. Use storage() for CFG queries or backend() for entity queries."
632 )
633 })
634 }
635
636 pub fn storage(&self) -> &Backend {
643 &self.storage
644 }
645
646 pub fn backend(&self) -> &dyn GraphBackend {
652 self.graph_backend.as_ref()
653 }
654
655 #[cfg(feature = "backend-sqlite")]
660 pub fn is_sqlite(&self) -> bool {
661 self.conn.is_some()
662 }
663}
664
665#[cfg(feature = "backend-geometric")]
675fn create_geometric_stub_backend() -> Box<dyn GraphBackend> {
676 use sqlitegraph::backend::{BackendDirection, EdgeSpec, NeighborQuery, NodeSpec};
677 use sqlitegraph::multi_hop::ChainStep;
678 use sqlitegraph::pattern::{PatternMatch, PatternQuery};
679 use sqlitegraph::{GraphBackend, GraphEntity, SnapshotId, SqliteGraphError};
680
681 struct GeometricStubBackend;
684
685 impl GraphBackend for GeometricStubBackend {
686 fn insert_node(&self, _node: NodeSpec) -> Result<i64, SqliteGraphError> {
687 Err(SqliteGraphError::unsupported(
688 "GraphBackend operations not supported for geometric backend. Use GeometricBackend methods directly."
689 ))
690 }
691
692 fn insert_edge(&self, _edge: EdgeSpec) -> Result<i64, SqliteGraphError> {
693 Err(SqliteGraphError::unsupported(
694 "GraphBackend operations not supported for geometric backend. Use GeometricBackend methods directly."
695 ))
696 }
697
698 fn update_node(&self, _node_id: i64, _node: NodeSpec) -> Result<i64, SqliteGraphError> {
699 Err(SqliteGraphError::unsupported(
700 "GraphBackend operations not supported for geometric backend. Use GeometricBackend methods directly."
701 ))
702 }
703
704 fn delete_entity(&self, _id: i64) -> Result<(), SqliteGraphError> {
705 Err(SqliteGraphError::unsupported(
706 "GraphBackend operations not supported for geometric backend. Use GeometricBackend methods directly."
707 ))
708 }
709
710 fn entity_ids(&self) -> Result<Vec<i64>, SqliteGraphError> {
711 Ok(vec![])
713 }
714
715 fn get_node(
716 &self,
717 _snapshot_id: SnapshotId,
718 _id: i64,
719 ) -> Result<GraphEntity, SqliteGraphError> {
720 Err(SqliteGraphError::unsupported(
721 "GraphBackend operations not supported for geometric backend. Use GeometricBackend methods directly."
722 ))
723 }
724
725 fn neighbors(
726 &self,
727 _snapshot_id: SnapshotId,
728 _node: i64,
729 _query: NeighborQuery,
730 ) -> Result<Vec<i64>, SqliteGraphError> {
731 Ok(vec![])
733 }
734
735 fn bfs(
736 &self,
737 _snapshot_id: SnapshotId,
738 _start: i64,
739 _depth: u32,
740 ) -> Result<Vec<i64>, SqliteGraphError> {
741 Ok(vec![])
743 }
744
745 fn shortest_path(
746 &self,
747 _snapshot_id: SnapshotId,
748 _start: i64,
749 _end: i64,
750 ) -> Result<Option<Vec<i64>>, SqliteGraphError> {
751 Ok(None)
752 }
753
754 fn node_degree(
755 &self,
756 _snapshot_id: SnapshotId,
757 _node: i64,
758 ) -> Result<(usize, usize), SqliteGraphError> {
759 Ok((0, 0))
760 }
761
762 fn k_hop(
763 &self,
764 _snapshot_id: SnapshotId,
765 _start: i64,
766 _depth: u32,
767 _direction: BackendDirection,
768 ) -> Result<Vec<i64>, SqliteGraphError> {
769 Ok(vec![])
770 }
771
772 fn k_hop_filtered(
773 &self,
774 _snapshot_id: SnapshotId,
775 _start: i64,
776 _depth: u32,
777 _direction: BackendDirection,
778 _allowed_edge_types: &[&str],
779 ) -> Result<Vec<i64>, SqliteGraphError> {
780 Ok(vec![])
781 }
782
783 fn chain_query(
784 &self,
785 _snapshot_id: SnapshotId,
786 _start: i64,
787 _chain: &[ChainStep],
788 ) -> Result<Vec<i64>, SqliteGraphError> {
789 Ok(vec![])
790 }
791
792 fn pattern_search(
793 &self,
794 _snapshot_id: SnapshotId,
795 _start: i64,
796 _pattern: &PatternQuery,
797 ) -> Result<Vec<PatternMatch>, SqliteGraphError> {
798 Ok(vec![])
799 }
800
801 fn checkpoint(&self) -> Result<(), SqliteGraphError> {
802 Ok(())
803 }
804
805 fn flush(&self) -> Result<(), SqliteGraphError> {
806 Ok(())
807 }
808
809 fn backup(
810 &self,
811 _backup_dir: &std::path::Path,
812 ) -> Result<sqlitegraph::backend::BackupResult, SqliteGraphError> {
813 Err(SqliteGraphError::unsupported(
814 "Backup not supported for geometric backend",
815 ))
816 }
817
818 fn snapshot_export(
819 &self,
820 _export_dir: &std::path::Path,
821 ) -> Result<sqlitegraph::backend::SnapshotMetadata, SqliteGraphError> {
822 Err(SqliteGraphError::unsupported(
823 "Snapshot export not supported for geometric backend",
824 ))
825 }
826
827 fn snapshot_import(
828 &self,
829 _import_dir: &std::path::Path,
830 ) -> Result<sqlitegraph::backend::ImportMetadata, SqliteGraphError> {
831 Err(SqliteGraphError::unsupported(
832 "Snapshot import not supported for geometric backend",
833 ))
834 }
835
836 fn query_nodes_by_kind(
837 &self,
838 _snapshot_id: SnapshotId,
839 _kind: &str,
840 ) -> Result<Vec<i64>, SqliteGraphError> {
841 Ok(vec![])
842 }
843
844 fn query_nodes_by_name_pattern(
845 &self,
846 _snapshot_id: SnapshotId,
847 _pattern: &str,
848 ) -> Result<Vec<i64>, SqliteGraphError> {
849 Ok(vec![])
850 }
851 }
852
853 Box::new(GeometricStubBackend)
854}
855
856struct Migration {
858 version: i32,
859 description: &'static str,
860 up: fn(&mut Connection) -> Result<()>,
861}
862
863fn migrations() -> Vec<Migration> {
865 vec![]
867}
868
869pub fn migrate_schema(conn: &mut Connection) -> Result<()> {
871 let current_version: i32 = conn
872 .query_row(
873 "SELECT mirage_schema_version FROM mirage_meta WHERE id = 1",
874 [],
875 |row| row.get(0),
876 )
877 .unwrap_or(0);
878
879 if current_version >= MIRAGE_SCHEMA_VERSION {
880 return Ok(());
882 }
883
884 let pending: Vec<_> = migrations()
886 .into_iter()
887 .filter(|m| m.version > current_version && m.version <= MIRAGE_SCHEMA_VERSION)
888 .collect();
889
890 for migration in pending {
891 (migration.up)(conn).with_context(|| {
893 format!(
894 "Failed to run migration v{}: {}",
895 migration.version, migration.description
896 )
897 })?;
898
899 conn.execute(
901 "UPDATE mirage_meta SET mirage_schema_version = ? WHERE id = 1",
902 params![migration.version],
903 )?;
904 }
905
906 if current_version < MIRAGE_SCHEMA_VERSION {
908 conn.execute(
909 "UPDATE mirage_meta SET mirage_schema_version = ? WHERE id = 1",
910 params![MIRAGE_SCHEMA_VERSION],
911 )?;
912 }
913
914 Ok(())
915}
916
917pub fn create_schema(conn: &mut Connection, _magellan_schema_version: i32) -> Result<()> {
922 conn.execute(
924 "CREATE TABLE IF NOT EXISTS mirage_meta (
925 id INTEGER PRIMARY KEY CHECK (id = 1),
926 mirage_schema_version INTEGER NOT NULL,
927 magellan_schema_version INTEGER NOT NULL,
928 compiler_version TEXT,
929 created_at INTEGER NOT NULL
930 )",
931 [],
932 )?;
933
934 conn.execute(
940 "CREATE TABLE IF NOT EXISTS cfg_blocks (
941 id INTEGER PRIMARY KEY AUTOINCREMENT,
942 function_id INTEGER NOT NULL,
943 kind TEXT NOT NULL,
944 terminator TEXT NOT NULL,
945 byte_start INTEGER,
946 byte_end INTEGER,
947 start_line INTEGER,
948 start_col INTEGER,
949 end_line INTEGER,
950 end_col INTEGER,
951 coord_x INTEGER NOT NULL DEFAULT 0,
952 coord_y INTEGER NOT NULL DEFAULT 0,
953 coord_z INTEGER NOT NULL DEFAULT 0,
954 FOREIGN KEY (function_id) REFERENCES graph_entities(id)
955 )",
956 [],
957 )?;
958
959 conn.execute(
960 "CREATE INDEX IF NOT EXISTS idx_cfg_blocks_function ON cfg_blocks(function_id)",
961 [],
962 )?;
963
964 conn.execute(
970 "CREATE TABLE IF NOT EXISTS cfg_paths (
971 path_id TEXT PRIMARY KEY,
972 function_id INTEGER NOT NULL,
973 path_kind TEXT NOT NULL,
974 entry_block INTEGER NOT NULL,
975 exit_block INTEGER NOT NULL,
976 length INTEGER NOT NULL,
977 created_at INTEGER NOT NULL,
978 FOREIGN KEY (function_id) REFERENCES graph_entities(id)
979 )",
980 [],
981 )?;
982
983 conn.execute(
984 "CREATE INDEX IF NOT EXISTS idx_cfg_paths_function ON cfg_paths(function_id)",
985 [],
986 )?;
987 conn.execute(
988 "CREATE INDEX IF NOT EXISTS idx_cfg_paths_kind ON cfg_paths(path_kind)",
989 [],
990 )?;
991
992 conn.execute(
994 "CREATE TABLE IF NOT EXISTS cfg_path_elements (
995 path_id TEXT NOT NULL,
996 sequence_order INTEGER NOT NULL,
997 block_id INTEGER NOT NULL,
998 PRIMARY KEY (path_id, sequence_order),
999 FOREIGN KEY (path_id) REFERENCES cfg_paths(path_id)
1000 )",
1001 [],
1002 )?;
1003
1004 conn.execute(
1005 "CREATE INDEX IF NOT EXISTS cfg_path_elements_block ON cfg_path_elements(block_id)",
1006 [],
1007 )?;
1008
1009 conn.execute(
1011 "CREATE TABLE IF NOT EXISTS cfg_dominators (
1012 block_id INTEGER NOT NULL,
1013 dominator_id INTEGER NOT NULL,
1014 is_strict BOOLEAN NOT NULL,
1015 PRIMARY KEY (block_id, dominator_id, is_strict),
1016 FOREIGN KEY (block_id) REFERENCES cfg_blocks(id),
1017 FOREIGN KEY (dominator_id) REFERENCES cfg_blocks(id)
1018 )",
1019 [],
1020 )?;
1021
1022 conn.execute(
1024 "CREATE TABLE IF NOT EXISTS cfg_post_dominators (
1025 block_id INTEGER NOT NULL,
1026 post_dominator_id INTEGER NOT NULL,
1027 is_strict BOOLEAN NOT NULL,
1028 PRIMARY KEY (block_id, post_dominator_id, is_strict),
1029 FOREIGN KEY (block_id) REFERENCES cfg_blocks(id),
1030 FOREIGN KEY (post_dominator_id) REFERENCES cfg_blocks(id)
1031 )",
1032 [],
1033 )?;
1034
1035 let now = chrono::Utc::now().timestamp();
1037 conn.execute(
1038 "INSERT OR REPLACE INTO mirage_meta (id, mirage_schema_version, magellan_schema_version, created_at)
1039 VALUES (1, ?, ?, ?)",
1040 params![MIRAGE_SCHEMA_VERSION, REQUIRED_MAGELLAN_SCHEMA_VERSION, now],
1041 )?;
1042
1043 Ok(())
1044}
1045
1046#[derive(Debug, Clone, serde::Serialize)]
1048pub struct DatabaseStatus {
1049 pub cfg_blocks: i64,
1050 #[deprecated(note = "Edges are now computed in memory, not stored")]
1051 pub cfg_edges: i64,
1052 pub cfg_paths: i64,
1053 pub cfg_dominators: i64,
1054 pub mirage_schema_version: i32,
1055 pub magellan_schema_version: i32,
1056}
1057
1058impl MirageDb {
1059 #[cfg(feature = "backend-sqlite")]
1064 pub fn status(&self) -> Result<DatabaseStatus> {
1065 match self.conn.as_ref() {
1067 Some(conn) => {
1068 let cfg_blocks: i64 = conn
1070 .query_row("SELECT COUNT(*) FROM cfg_blocks", [], |row| row.get(0))
1071 .unwrap_or(0);
1072
1073 let cfg_edges: i64 = conn
1076 .query_row("SELECT COUNT(*) FROM cfg_edges", [], |row| row.get(0))
1077 .unwrap_or(0);
1078
1079 let cfg_paths: i64 = conn
1080 .query_row("SELECT COUNT(*) FROM cfg_paths", [], |row| row.get(0))
1081 .unwrap_or(0);
1082
1083 let cfg_dominators: i64 = conn
1084 .query_row("SELECT COUNT(*) FROM cfg_dominators", [], |row| row.get(0))
1085 .unwrap_or(0);
1086
1087 let mirage_schema_version: i32 = conn
1088 .query_row(
1089 "SELECT mirage_schema_version FROM mirage_meta WHERE id = 1",
1090 [],
1091 |row| row.get(0),
1092 )
1093 .unwrap_or(0);
1094
1095 let magellan_schema_version: i32 = conn
1096 .query_row(
1097 "SELECT magellan_schema_version FROM magellan_meta WHERE id = 1",
1098 [],
1099 |row| row.get(0),
1100 )
1101 .unwrap_or(0);
1102
1103 #[allow(deprecated)]
1104 Ok(DatabaseStatus {
1105 cfg_blocks,
1106 cfg_edges,
1107 cfg_paths,
1108 cfg_dominators,
1109 mirage_schema_version,
1110 magellan_schema_version,
1111 })
1112 }
1113 None => {
1114 self.status_via_storage()
1116 }
1117 }
1118 }
1119
1120 #[cfg(feature = "backend-sqlite")]
1122 fn status_via_storage(&self) -> Result<DatabaseStatus> {
1123 #[cfg(feature = "backend-geometric")]
1125 {
1126 if let Backend::Geometric(ref geometric) = self.storage {
1127 let stats = geometric.get_stats()?;
1129 return Ok(DatabaseStatus {
1130 cfg_blocks: stats.cfg_block_count as i64,
1131 cfg_edges: 0, cfg_paths: 0, cfg_dominators: 0, mirage_schema_version: MIRAGE_SCHEMA_VERSION,
1135 magellan_schema_version: MIN_MAGELLAN_SCHEMA_VERSION,
1136 });
1137 }
1138 }
1139
1140 #[allow(deprecated)]
1142 Ok(DatabaseStatus {
1145 cfg_blocks: 0,
1146 cfg_edges: 0, cfg_paths: 0,
1148 cfg_dominators: 0,
1149 mirage_schema_version: MIRAGE_SCHEMA_VERSION,
1150 magellan_schema_version: MIN_MAGELLAN_SCHEMA_VERSION,
1151 })
1152 }
1153
1154 #[cfg(all(feature = "backend-geometric", not(feature = "backend-sqlite")))]
1158 pub fn status(&self) -> Result<DatabaseStatus> {
1159 let cfg_blocks: i64 = if let Backend::Geometric(ref geometric) = self.storage {
1162 0
1166 } else {
1167 0
1168 };
1169
1170 let cfg_edges: i64 = 0;
1172 let cfg_paths: i64 = 0;
1173 let cfg_dominators: i64 = 0;
1174
1175 let mirage_schema_version = MIRAGE_SCHEMA_VERSION;
1178 let magellan_schema_version = MIN_MAGELLAN_SCHEMA_VERSION;
1179
1180 #[allow(deprecated)]
1181 Ok(DatabaseStatus {
1182 cfg_blocks,
1183 cfg_edges,
1184 cfg_paths,
1185 cfg_dominators,
1186 mirage_schema_version,
1187 magellan_schema_version,
1188 })
1189 }
1190
1191 #[cfg(feature = "backend-sqlite")]
1219 pub fn resolve_function_name(&self, name_or_id: &str) -> Result<i64> {
1220 self.resolve_function_name_with_file(name_or_id, None)
1221 }
1222
1223 #[cfg(feature = "backend-sqlite")]
1239 pub fn resolve_function_name_with_file(
1240 &self,
1241 name_or_id: &str,
1242 file_filter: Option<&str>,
1243 ) -> Result<i64> {
1244 if let Ok(id) = name_or_id.parse::<i64>() {
1246 return Ok(id);
1247 }
1248
1249 if let Ok(conn) = self.conn() {
1251 resolve_function_name_sqlite(conn, name_or_id, file_filter)
1252 } else {
1253 #[cfg(feature = "backend-geometric")]
1255 {
1256 if let Backend::Geometric(ref geometric) = self.storage {
1257 return self.resolve_function_name_geometric(name_or_id);
1258 }
1259 }
1260 anyhow::bail!("No database connection available for function resolution")
1261 }
1262 }
1263
1264 #[cfg(feature = "backend-geometric")]
1267 fn normalize_path_for_dedup(path: &str) -> String {
1268 let path = path.replace('\\', "/");
1270 let path = path.strip_prefix("./").unwrap_or(&path);
1272 if let Some(idx) = path.find("/src/") {
1275 path[idx + 1..].to_string()
1277 } else {
1278 path.to_string()
1279 }
1280 }
1281
1282 #[cfg(feature = "backend-geometric")]
1289 fn resolve_function_name_geometric(&self, name_or_id: &str) -> Result<i64> {
1290 if let Ok(id) = name_or_id.parse::<i64>() {
1292 if let Backend::Geometric(ref geometric) = self.storage {
1294 if geometric
1295 .inner()
1296 .find_symbol_by_id_info(id as u64)
1297 .is_some()
1298 {
1299 return Ok(id);
1300 }
1301 }
1302 anyhow::bail!("Function with ID '{}' not found", id);
1303 }
1304
1305 if let Some(fqn_data) = Self::parse_fqn(name_or_id) {
1307 return self.resolve_function_by_fqn(fqn_data);
1308 }
1309
1310 if let Backend::Geometric(ref geometric) = self.storage {
1312 let all_symbols = geometric.find_symbols_by_name(name_or_id);
1314 if all_symbols.is_empty() {
1315 anyhow::bail!("Function '{}' not found", name_or_id);
1316 }
1317
1318 let mut unique_symbols: Vec<magellan::graph::geometric_backend::SymbolInfo> =
1322 Vec::new();
1323 let mut seen_ids: std::collections::HashSet<u64> = std::collections::HashSet::new();
1324
1325 for sym in all_symbols {
1326 if seen_ids.insert(sym.id) {
1327 unique_symbols.push(sym);
1328 }
1329 }
1330
1331 if unique_symbols.len() > 1 {
1333 let first = &unique_symbols[0];
1334 let first_path_normalized = Self::normalize_path_for_dedup(&first.file_path);
1335 let all_same_location = unique_symbols.iter().all(|sym| {
1336 let sym_path_normalized = Self::normalize_path_for_dedup(&sym.file_path);
1337 sym.name == first.name
1338 && sym_path_normalized == first_path_normalized
1339 && sym.start_line == first.start_line
1340 && sym.start_col == first.start_col
1341 });
1342
1343 if !all_same_location {
1344 anyhow::bail!(
1346 "Ambiguous function reference to '{}': {} unique candidates found\n\nCandidates:\n{}\n\nUse full qualified name: magellan::/path/to/file.rs::{}",
1347 name_or_id,
1348 unique_symbols.len(),
1349 unique_symbols.iter().map(|s| {
1350 format!(" - {} ({}:{}:{})", s.name, s.file_path, s.start_line, s.start_col)
1351 }).collect::<Vec<_>>().join("\n"),
1352 name_or_id
1353 );
1354 }
1355 }
1357 Ok(unique_symbols[0].id as i64)
1358 } else {
1359 anyhow::bail!("Geometric backend not available")
1360 }
1361 }
1362
1363 #[cfg(feature = "backend-geometric")]
1366 fn parse_fqn(name: &str) -> Option<(&str, &str)> {
1367 if !name.starts_with("magellan::") {
1370 return None;
1371 }
1372
1373 let after_prefix = &name[10..]; if let Some(last_sep_pos) = after_prefix.rfind("::") {
1378 let file_path = &after_prefix[..last_sep_pos];
1379 let name_part = &after_prefix[last_sep_pos + 2..];
1380
1381 let symbol_name = if let Some(space_pos) = name_part.find(' ') {
1384 &name_part[space_pos + 1..]
1385 } else {
1386 name_part
1387 };
1388
1389 if !file_path.is_empty() && !symbol_name.is_empty() {
1390 return Some((file_path, symbol_name));
1391 }
1392 }
1393
1394 None
1395 }
1396
1397 #[cfg(feature = "backend-geometric")]
1399 fn resolve_function_by_fqn(&self, fqn_data: (&str, &str)) -> Result<i64> {
1400 let (file_path, symbol_name) = fqn_data;
1401
1402 if let Backend::Geometric(ref geometric) = self.storage {
1403 match geometric.find_symbol_id_by_name_and_path(symbol_name, file_path) {
1405 Some(id) => Ok(id as i64),
1406 None => {
1407 let all_symbols = geometric.find_symbols_by_name(symbol_name);
1409 let normalized_target = Self::normalize_path_for_dedup(file_path);
1410
1411 let matching_symbols: Vec<_> = all_symbols
1412 .into_iter()
1413 .filter(|sym| {
1414 let sym_path_normalized =
1415 Self::normalize_path_for_dedup(&sym.file_path);
1416 sym_path_normalized == normalized_target
1417 })
1418 .collect();
1419
1420 if matching_symbols.is_empty() {
1421 anyhow::bail!(
1422 "Function '{}' not found in file '{}'",
1423 symbol_name,
1424 file_path
1425 );
1426 } else {
1427 anyhow::bail!(
1429 "Multiple functions named '{}' found in file '{}' ({} matches). Use numeric ID instead.",
1430 symbol_name,
1431 file_path,
1432 matching_symbols.len()
1433 );
1434 }
1435 }
1436 }
1437 } else {
1438 anyhow::bail!("Geometric backend not available")
1439 }
1440 }
1441
1442 #[cfg(feature = "backend-sqlite")]
1466 pub fn load_cfg(&self, function_id: i64) -> Result<crate::cfg::Cfg> {
1467 let blocks = self.storage().get_cfg_blocks(function_id)?;
1469
1470 if blocks.is_empty() {
1471 anyhow::bail!(
1472 "No CFG blocks found for function_id {}. Run 'magellan watch' to build CFGs.",
1473 function_id
1474 );
1475 }
1476
1477 let file_path = self.get_function_file(function_id);
1479
1480 let block_rows: Vec<(
1482 i64,
1483 String,
1484 Option<String>,
1485 Option<i64>,
1486 Option<i64>,
1487 Option<i64>,
1488 Option<i64>,
1489 Option<i64>,
1490 Option<i64>,
1491 Option<i64>,
1492 Option<i64>,
1493 Option<i64>,
1494 )> = blocks
1495 .into_iter()
1496 .enumerate()
1497 .map(|(idx, b)| {
1498 (
1499 idx as i64, b.kind,
1501 Some(b.terminator),
1502 Some(b.byte_start as i64),
1503 Some(b.byte_end as i64),
1504 Some(b.start_line as i64),
1505 Some(b.start_col as i64),
1506 Some(b.end_line as i64),
1507 Some(b.end_col as i64),
1508 Some(b.coord_x),
1509 Some(b.coord_y),
1510 Some(b.coord_z),
1511 )
1512 })
1513 .collect();
1514
1515 let cfg_edges: Vec<(i64, i64, String)> = if let Ok(conn) = self.conn() {
1517 match conn.prepare_cached(
1518 "SELECT source_idx, target_idx, edge_type
1519 FROM cfg_edges
1520 WHERE function_id = ?
1521 ORDER BY source_idx, target_idx",
1522 ) {
1523 Ok(mut stmt) => {
1524 match stmt.query_map(params![function_id], |row| {
1525 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
1526 }) {
1527 Ok(rows) => rows.collect::<Result<Vec<_>, _>>().unwrap_or_default(),
1528 Err(_) => vec![],
1529 }
1530 }
1531 Err(_) => vec![],
1532 }
1533 } else {
1534 vec![]
1535 };
1536
1537 load_cfg_from_rows(
1538 block_rows,
1539 file_path.map(std::path::PathBuf::from),
1540 cfg_edges,
1541 )
1542 }
1543
1544 pub fn get_function_name(&self, function_id: i64) -> Option<String> {
1558 let snapshot = SnapshotId::current();
1559 self.backend()
1560 .get_node(snapshot, function_id)
1561 .ok()
1562 .and_then(|entity| {
1563 if entity.kind == "Symbol"
1565 && entity.data.get("kind").and_then(|v| v.as_str()) == Some("Function")
1566 {
1567 Some(entity.name)
1568 } else {
1569 None
1570 }
1571 })
1572 }
1573
1574 pub fn get_function_file(&self, function_id: i64) -> Option<String> {
1589 let snapshot = SnapshotId::current();
1590 self.backend()
1591 .get_node(snapshot, function_id)
1592 .ok()
1593 .and_then(|entity| entity.file_path)
1594 }
1595
1596 #[cfg(feature = "backend-sqlite")]
1609 pub fn function_exists(&self, function_id: i64) -> bool {
1610 use crate::storage::function_exists;
1611 self.conn()
1612 .and_then(|conn| Ok(function_exists(conn, function_id)))
1613 .unwrap_or(false)
1614 }
1615
1616 #[cfg(feature = "backend-sqlite")]
1629 pub fn get_function_hash(&self, function_id: i64) -> Option<String> {
1630 use crate::storage::get_function_hash;
1631 self.conn()
1632 .and_then(|conn| Ok(get_function_hash(conn, function_id)))
1633 .ok()
1634 .flatten()
1635 }
1636}
1637
1638#[cfg(feature = "backend-sqlite")]
1643fn resolve_function_name_sqlite(
1644 conn: &Connection,
1645 name_or_id: &str,
1646 file_filter: Option<&str>,
1647) -> Result<i64> {
1648 let function_id_by_symbol: Option<i64> = conn
1651 .query_row(
1652 "SELECT id FROM graph_entities
1653 WHERE kind = 'Symbol'
1654 AND json_extract(data, '$.kind') = 'Function'
1655 AND json_extract(data, '$.symbol_id') = ?
1656 LIMIT 1",
1657 params![name_or_id],
1658 |row| row.get(0),
1659 )
1660 .optional()
1661 .context(format!(
1662 "Failed to query function with symbol_id '{}'",
1663 name_or_id
1664 ))?;
1665
1666 if let Some(id) = function_id_by_symbol {
1667 return Ok(id);
1668 }
1669
1670 let function_id: Option<i64> = if let Some(file_path) = file_filter {
1672 let pattern = format!("%{}%", file_path);
1674 conn.query_row(
1675 "SELECT id FROM graph_entities
1676 WHERE kind = 'Symbol'
1677 AND json_extract(data, '$.kind') = 'Function'
1678 AND name = ?
1679 AND file_path LIKE ?
1680 LIMIT 1",
1681 params![name_or_id, pattern],
1682 |row| row.get(0),
1683 )
1684 .optional()
1685 .context(format!(
1686 "Failed to query function with name '{}' in file '{}'",
1687 name_or_id, file_path
1688 ))?
1689 } else {
1690 conn.query_row(
1692 "SELECT id FROM graph_entities
1693 WHERE kind = 'Symbol'
1694 AND json_extract(data, '$.kind') = 'Function'
1695 AND name = ?
1696 LIMIT 1",
1697 params![name_or_id],
1698 |row| row.get(0),
1699 )
1700 .optional()
1701 .context(format!(
1702 "Failed to query function with name '{}'",
1703 name_or_id
1704 ))?
1705 };
1706
1707 function_id.context(format!(
1708 "Function '{}' not found in database. Run 'magellan watch' to index functions.",
1709 name_or_id
1710 ))
1711}
1712
1713#[cfg(feature = "backend-sqlite")]
1717fn load_cfg_from_sqlite(conn: &Connection, function_id: i64) -> Result<crate::cfg::Cfg> {
1718 use std::path::PathBuf;
1719
1720 let file_path: Option<String> = conn
1722 .query_row(
1723 "SELECT file_path FROM graph_entities WHERE id = ?",
1724 params![function_id],
1725 |row| row.get(0),
1726 )
1727 .optional()
1728 .context("Failed to query file_path from graph_entities")?;
1729
1730 let file_path = file_path.map(PathBuf::from);
1731
1732 let mut stmt = conn
1736 .prepare_cached(
1737 "SELECT id, kind, terminator, byte_start, byte_end,
1738 start_line, start_col, end_line, end_col,
1739 coord_x, coord_y, coord_z
1740 FROM cfg_blocks
1741 WHERE function_id = ?
1742 ORDER BY id ASC",
1743 )
1744 .context("Failed to prepare cfg_blocks query")?;
1745
1746 let block_rows: Vec<(
1747 i64,
1748 String,
1749 Option<String>,
1750 Option<i64>,
1751 Option<i64>,
1752 Option<i64>,
1753 Option<i64>,
1754 Option<i64>,
1755 Option<i64>,
1756 Option<i64>,
1757 Option<i64>,
1758 Option<i64>,
1759 )> = stmt
1760 .query_map(params![function_id], |row| {
1761 Ok((
1762 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)?, row.get(9)?, row.get(10)?, row.get(11)?, ))
1775 })
1776 .context("Failed to execute cfg_blocks query")?
1777 .collect::<Result<Vec<_>, _>>()
1778 .context("Failed to collect cfg_blocks rows")?;
1779
1780 if block_rows.is_empty() {
1781 anyhow::bail!(
1782 "No CFG blocks found for function_id {}. Run 'magellan watch' to build CFGs.",
1783 function_id
1784 );
1785 }
1786
1787 let edges: Vec<(i64, i64, String)> = match conn.prepare_cached(
1789 "SELECT source_idx, target_idx, edge_type
1790 FROM cfg_edges
1791 WHERE function_id = ?
1792 ORDER BY source_idx, target_idx",
1793 ) {
1794 Ok(mut stmt) => stmt
1795 .query_map(params![function_id], |row| {
1796 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
1797 })
1798 .context("Failed to query cfg_edges")?
1799 .collect::<Result<Vec<_>, _>>()
1800 .context("Failed to collect cfg_edges rows")?,
1801 Err(_) => Vec::new(),
1802 };
1803
1804 load_cfg_from_rows(block_rows, file_path, edges)
1805}
1806
1807fn load_cfg_from_rows(
1811 block_rows: Vec<(
1812 i64,
1813 String,
1814 Option<String>,
1815 Option<i64>,
1816 Option<i64>,
1817 Option<i64>,
1818 Option<i64>,
1819 Option<i64>,
1820 Option<i64>,
1821 Option<i64>,
1822 Option<i64>,
1823 Option<i64>,
1824 )>,
1825 file_path: Option<std::path::PathBuf>,
1826 cfg_edges: Vec<(i64, i64, String)>,
1827) -> Result<crate::cfg::Cfg> {
1828 use crate::cfg::source::SourceLocation;
1829 use crate::cfg::{build_edges_from_cfg_edges, build_edges_from_terminators};
1830 use crate::cfg::{BasicBlock, BlockKind, Cfg, Terminator};
1831 use std::collections::HashMap;
1832
1833 let mut db_id_to_node: HashMap<i64, usize> = HashMap::new();
1835 let mut graph = Cfg::new();
1836
1837 for (
1839 node_idx,
1840 (
1841 db_id,
1842 kind_str,
1843 terminator_str,
1844 byte_start,
1845 byte_end,
1846 start_line,
1847 start_col,
1848 end_line,
1849 end_col,
1850 coord_x,
1851 coord_y,
1852 coord_z,
1853 ),
1854 ) in block_rows.iter().enumerate()
1855 {
1856 let kind = match kind_str.as_str() {
1858 "entry" => BlockKind::Entry,
1859 "return" => BlockKind::Exit,
1860 "if" | "else" | "loop" | "while" | "for" | "match_arm" | "block" => BlockKind::Normal,
1861 _ => {
1862 BlockKind::Normal
1865 }
1866 };
1867
1868 let terminator = match terminator_str.as_deref() {
1870 Some("fallthrough") => Terminator::Goto { target: 0 }, Some("conditional") => Terminator::SwitchInt {
1872 targets: vec![],
1873 otherwise: 0,
1874 },
1875 Some("goto") => Terminator::Goto { target: 0 },
1876 Some("return") => Terminator::Return,
1877 Some("break") => Terminator::Abort("break".to_string()),
1878 Some("continue") => Terminator::Abort("continue".to_string()),
1879 Some("call") => Terminator::Call {
1880 target: None,
1881 unwind: None,
1882 },
1883 Some("panic") => Terminator::Abort("panic".to_string()),
1884 Some(_) | None => Terminator::Unreachable,
1885 };
1886
1887 let source_location = if let Some(ref path) = file_path {
1889 let sl = start_line.and_then(|l| start_col.map(|c| (l as usize, c as usize)));
1891 let el = end_line.and_then(|l| end_col.map(|c| (l as usize, c as usize)));
1892
1893 match (sl, el, byte_start, byte_end) {
1894 (Some((start_l, start_c)), Some((end_l, end_c)), Some(bs), Some(be)) => {
1895 Some(SourceLocation {
1896 file_path: path.clone(),
1897 byte_start: *bs as usize,
1898 byte_end: *be as usize,
1899 start_line: start_l,
1900 start_column: start_c,
1901 end_line: end_l,
1902 end_column: end_c,
1903 })
1904 }
1905 _ => None,
1906 }
1907 } else {
1908 None
1909 };
1910
1911 let block = BasicBlock {
1912 id: node_idx,
1913 db_id: Some(*db_id),
1914 kind,
1915 statements: vec![], terminator,
1917 source_location,
1918 coord_x: coord_x.unwrap_or(0),
1920 coord_y: coord_y.unwrap_or(0),
1921 coord_z: coord_z.unwrap_or(0),
1922 };
1923
1924 graph.add_node(block);
1925 db_id_to_node.insert(*db_id, node_idx);
1926 }
1927
1928 let mut index_to_node: HashMap<usize, usize> = HashMap::new();
1930 for (idx, (db_id, _, _, _, _, _, _, _, _, _, _, _)) in block_rows.iter().enumerate() {
1931 if let Some(&node_idx) = db_id_to_node.get(db_id) {
1932 index_to_node.insert(idx, node_idx);
1933 }
1934 }
1935
1936 if !cfg_edges.is_empty() {
1938 build_edges_from_cfg_edges(&mut graph, &cfg_edges, &index_to_node)
1939 .context("Failed to build edges from cfg_edges")?;
1940 } else {
1941 build_edges_from_terminators(&mut graph, &block_rows, &db_id_to_node)
1942 .context("Failed to build edges from terminator data")?;
1943 }
1944
1945 Ok(graph)
1946}
1947
1948pub fn resolve_function_name(db: &MirageDb, name_or_id: &str) -> Result<i64> {
1978 db.resolve_function_name(name_or_id)
1979}
1980
1981pub fn resolve_function_name_with_file(
2009 db: &MirageDb,
2010 name_or_id: &str,
2011 file_filter: Option<&str>,
2012) -> Result<i64> {
2013 db.resolve_function_name_with_file(name_or_id, file_filter)
2014}
2015
2016pub fn get_function_name_db(db: &MirageDb, function_id: i64) -> Option<String> {
2044 db.get_function_name(function_id)
2045}
2046
2047pub fn get_function_file_db(db: &MirageDb, function_id: i64) -> Option<String> {
2075 db.get_function_file(function_id)
2076}
2077
2078pub fn get_function_hash_db(db: &MirageDb, function_id: i64) -> Option<String> {
2109 db.get_function_hash(function_id)
2110}
2111
2112#[cfg(feature = "backend-sqlite")]
2117pub fn resolve_function_name_with_conn(conn: &Connection, name_or_id: &str) -> Result<i64> {
2118 if let Ok(id) = name_or_id.parse::<i64>() {
2120 return Ok(id);
2121 }
2122
2123 let function_id: Option<i64> = conn
2126 .query_row(
2127 "SELECT id FROM graph_entities
2128 WHERE kind = 'Symbol'
2129 AND json_extract(data, '$.kind') = 'Function'
2130 AND name = ?
2131 LIMIT 1",
2132 params![name_or_id],
2133 |row| row.get(0),
2134 )
2135 .optional()
2136 .context(format!(
2137 "Failed to query function with name '{}'",
2138 name_or_id
2139 ))?;
2140
2141 function_id.context(format!(
2142 "Function '{}' not found in database. Run 'magellan watch' to index functions.",
2143 name_or_id
2144 ))
2145}
2146
2147pub fn load_cfg_from_db(db: &MirageDb, function_id: i64) -> Result<crate::cfg::Cfg> {
2179 db.load_cfg(function_id)
2180}
2181
2182#[cfg(feature = "backend-sqlite")]
2216pub fn load_cfg_from_db_with_conn(conn: &Connection, function_id: i64) -> Result<crate::cfg::Cfg> {
2217 load_cfg_from_sqlite(conn, function_id)
2218}
2219
2220#[deprecated(note = "Magellan handles CFG storage via cfg_blocks. Edges are computed in memory.")]
2252pub fn store_cfg(
2253 conn: &mut Connection,
2254 function_id: i64,
2255 _function_hash: &str, cfg: &crate::cfg::Cfg,
2257) -> Result<()> {
2258 use crate::cfg::{BlockKind, Terminator};
2259
2260 conn.execute("BEGIN IMMEDIATE TRANSACTION", [])
2261 .context("Failed to begin transaction")?;
2262
2263 conn.execute(
2266 "DELETE FROM cfg_blocks WHERE function_id = ?",
2267 params![function_id],
2268 )
2269 .context("Failed to clear existing cfg_blocks")?;
2270
2271 let mut block_id_map: std::collections::HashMap<petgraph::graph::NodeIndex, i64> =
2273 std::collections::HashMap::new();
2274
2275 let mut insert_block = conn
2276 .prepare_cached(
2277 "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
2278 start_line, start_col, end_line, end_col,
2279 coord_x, coord_y, coord_z)
2280 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
2281 )
2282 .context("Failed to prepare block insert statement")?;
2283
2284 for node_idx in cfg.node_indices() {
2285 let block = cfg
2286 .node_weight(node_idx)
2287 .context("CFG node has no weight")?;
2288
2289 let terminator_str = match &block.terminator {
2291 Terminator::Goto { .. } => "goto",
2292 Terminator::SwitchInt { .. } => "conditional",
2293 Terminator::Return => "return",
2294 Terminator::Call { .. } => "call",
2295 Terminator::Abort(msg) if msg == "break" => "break",
2296 Terminator::Abort(msg) if msg == "continue" => "continue",
2297 Terminator::Abort(msg) if msg == "panic" => "panic",
2298 _ => "fallthrough",
2299 };
2300
2301 let (byte_start, byte_end) = block
2303 .source_location
2304 .as_ref()
2305 .map(|loc| (Some(loc.byte_start as i64), Some(loc.byte_end as i64)))
2306 .unwrap_or((None, None));
2307
2308 let (start_line, start_col, end_line, end_col) = block
2309 .source_location
2310 .as_ref()
2311 .map(|loc| {
2312 (
2313 Some(loc.start_line as i64),
2314 Some(loc.start_column as i64),
2315 Some(loc.end_line as i64),
2316 Some(loc.end_column as i64),
2317 )
2318 })
2319 .unwrap_or((None, None, None, None));
2320
2321 let kind = match block.kind {
2323 BlockKind::Entry => "entry",
2324 BlockKind::Normal => "block",
2325 BlockKind::Exit => "return",
2326 };
2327
2328 insert_block
2329 .execute(params![
2330 function_id,
2331 kind,
2332 terminator_str,
2333 byte_start,
2334 byte_end,
2335 start_line,
2336 start_col,
2337 end_line,
2338 end_col,
2339 block.coord_x,
2340 block.coord_y,
2341 block.coord_z,
2342 ])
2343 .context("Failed to insert cfg_block")?;
2344
2345 let db_id = conn.last_insert_rowid();
2346 block_id_map.insert(node_idx, db_id);
2347 }
2348
2349 conn.execute("COMMIT", [])
2353 .context("Failed to commit transaction")?;
2354
2355 Ok(())
2356}
2357
2358pub fn function_exists(conn: &Connection, function_id: i64) -> bool {
2370 conn.query_row(
2371 "SELECT COUNT(*) FROM cfg_blocks WHERE function_id = ?",
2372 params![function_id],
2373 |row| row.get::<_, i64>(0).map(|count| count > 0),
2374 )
2375 .optional()
2376 .ok()
2377 .flatten()
2378 .unwrap_or(false)
2379}
2380
2381pub fn get_function_hash(conn: &Connection, function_id: i64) -> Option<String> {
2399 let cfg_hash: Option<String> = conn
2401 .query_row(
2402 "SELECT cfg_hash FROM cfg_blocks WHERE function_id = ? LIMIT 1",
2403 params![function_id],
2404 |row| row.get(0),
2405 )
2406 .optional()
2407 .ok()
2408 .flatten();
2409
2410 if cfg_hash.is_some() {
2411 return cfg_hash;
2412 }
2413
2414 conn.query_row(
2417 "SELECT json_extract(data, '$.symbol_id') FROM graph_entities WHERE id = ? LIMIT 1",
2418 params![function_id],
2419 |row| row.get::<_, Option<String>>(0),
2420 )
2421 .optional()
2422 .ok()
2423 .flatten()
2424 .flatten()
2425}
2426
2427pub fn hash_changed(conn: &Connection, function_id: i64, _new_hash: &str) -> Result<bool> {
2448 let old_hash: Option<String> = conn
2449 .query_row(
2450 "SELECT cfg_hash FROM cfg_blocks WHERE function_id = ? LIMIT 1",
2451 params![function_id],
2452 |row| row.get(0),
2453 )
2454 .optional()?;
2455
2456 match old_hash {
2457 Some(old) => Ok(old != _new_hash),
2458 None => Ok(true), }
2460}
2461
2462pub fn get_changed_functions(
2482 conn: &Connection,
2483 project_path: &std::path::Path,
2484) -> Result<std::collections::HashSet<String>> {
2485 use std::collections::HashSet;
2486 use std::process::Command;
2487
2488 let mut changed = HashSet::new();
2489
2490 if let Ok(git_output) = Command::new("git")
2492 .args(["diff", "--name-only", "HEAD"])
2493 .current_dir(project_path)
2494 .output()
2495 {
2496 let git_files = String::from_utf8_lossy(&git_output.stdout);
2497
2498 let changed_rs_files: Vec<&str> =
2500 git_files.lines().filter(|f| f.ends_with(".rs")).collect();
2501
2502 if changed_rs_files.is_empty() {
2503 return Ok(changed);
2504 }
2505
2506 for file in changed_rs_files {
2508 let normalized_path = if file.starts_with('/') {
2510 file.trim_start_matches('/')
2511 } else {
2512 file
2513 };
2514
2515 let mut stmt = conn
2519 .prepare_cached(
2520 "SELECT name FROM graph_entities
2521 WHERE kind = 'function' AND (
2522 file_path = ? OR
2523 file_path = ? OR
2524 file_path LIKE '%' || ?
2525 )",
2526 )
2527 .context("Failed to prepare function lookup query")?;
2528
2529 let with_slash = format!("/{}", normalized_path);
2530
2531 let rows = stmt
2532 .query_map(
2533 params![normalized_path, &with_slash, normalized_path],
2534 |row| row.get::<_, String>(0),
2535 )
2536 .context("Failed to execute function lookup")?;
2537
2538 for row in rows {
2539 if let Ok(func_name) = row {
2540 changed.insert(func_name);
2541 }
2542 }
2543 }
2544 }
2545
2546 Ok(changed)
2547}
2548
2549pub fn get_function_file(conn: &Connection, function_name: &str) -> Result<Option<String>> {
2562 let file: Option<String> = conn
2563 .query_row(
2564 "SELECT file_path FROM graph_entities WHERE kind = 'function' AND name = ? LIMIT 1",
2565 params![function_name],
2566 |row| row.get(0),
2567 )
2568 .optional()?;
2569
2570 Ok(file)
2571}
2572
2573pub fn get_function_name(conn: &Connection, function_id: i64) -> Option<String> {
2585 conn.query_row(
2586 "SELECT name FROM graph_entities WHERE id = ?",
2587 params![function_id],
2588 |row| row.get(0),
2589 )
2590 .optional()
2591 .ok()
2592 .flatten()
2593}
2594
2595pub fn get_path_elements(conn: &Connection, path_id: &str) -> Result<Vec<crate::cfg::BlockId>> {
2607 let mut stmt = conn
2608 .prepare_cached(
2609 "SELECT block_id FROM cfg_path_elements
2610 WHERE path_id = ?
2611 ORDER BY sequence_order ASC",
2612 )
2613 .context("Failed to prepare path elements query")?;
2614
2615 let blocks: Vec<crate::cfg::BlockId> = stmt
2616 .query_map(params![path_id], |row| Ok(row.get::<_, i64>(0)? as usize))
2617 .context("Failed to execute path elements query")?
2618 .collect::<Result<Vec<_>, _>>()
2619 .context("Failed to collect path elements")?;
2620
2621 if blocks.is_empty() {
2622 anyhow::bail!("Path '{}' not found in cache", path_id);
2623 }
2624
2625 Ok(blocks)
2626}
2627
2628pub fn compute_path_impact_from_db(
2645 conn: &Connection,
2646 path_id: &str,
2647 cfg: &crate::cfg::Cfg,
2648 max_depth: Option<usize>,
2649) -> Result<crate::cfg::PathImpact> {
2650 let path_blocks = get_path_elements(conn, path_id)?;
2651
2652 let mut impact = crate::cfg::compute_path_impact(cfg, &path_blocks, max_depth);
2653 impact.path_id = path_id.to_string();
2654
2655 Ok(impact)
2656}
2657
2658pub fn create_minimal_database<P: AsRef<Path>>(path: P) -> Result<()> {
2673 let path = path.as_ref();
2674
2675 if path.exists() {
2677 anyhow::bail!("Database already exists: {}", path.display());
2678 }
2679
2680 let mut conn = Connection::open(path).context("Failed to create database file")?;
2681
2682 conn.execute(
2684 "CREATE TABLE magellan_meta (
2685 id INTEGER PRIMARY KEY CHECK (id = 1),
2686 magellan_schema_version INTEGER NOT NULL,
2687 sqlitegraph_schema_version INTEGER NOT NULL,
2688 created_at INTEGER NOT NULL
2689 )",
2690 [],
2691 )
2692 .context("Failed to create magellan_meta table")?;
2693
2694 conn.execute(
2696 "CREATE TABLE graph_entities (
2697 id INTEGER PRIMARY KEY AUTOINCREMENT,
2698 kind TEXT NOT NULL,
2699 name TEXT NOT NULL,
2700 file_path TEXT,
2701 data TEXT NOT NULL
2702 )",
2703 [],
2704 )
2705 .context("Failed to create graph_entities table")?;
2706
2707 conn.execute(
2709 "CREATE INDEX idx_graph_entities_kind ON graph_entities(kind)",
2710 [],
2711 )
2712 .context("Failed to create index on graph_entities.kind")?;
2713
2714 conn.execute(
2715 "CREATE INDEX idx_graph_entities_name ON graph_entities(name)",
2716 [],
2717 )
2718 .context("Failed to create index on graph_entities.name")?;
2719
2720 let now = chrono::Utc::now().timestamp();
2722 conn.execute(
2723 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
2724 VALUES (1, ?, ?, ?)",
2725 params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, now],
2726 ).context("Failed to initialize magellan_meta")?;
2727
2728 create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION)
2730 .context("Failed to create Mirage schema")?;
2731
2732 Ok(())
2733}
2734
2735#[cfg(all(test, feature = "sqlite"))]
2736mod tests {
2737 use super::*;
2738
2739 #[test]
2740 fn test_create_schema() {
2741 let mut conn = Connection::open_in_memory().unwrap();
2742 conn.execute(
2744 "CREATE TABLE magellan_meta (
2745 id INTEGER PRIMARY KEY CHECK (id = 1),
2746 magellan_schema_version INTEGER NOT NULL,
2747 sqlitegraph_schema_version INTEGER NOT NULL,
2748 created_at INTEGER NOT NULL
2749 )",
2750 [],
2751 )
2752 .unwrap();
2753
2754 conn.execute(
2755 "CREATE TABLE graph_entities (
2756 id INTEGER PRIMARY KEY AUTOINCREMENT,
2757 kind TEXT NOT NULL,
2758 name TEXT NOT NULL,
2759 file_path TEXT,
2760 data TEXT NOT NULL
2761 )",
2762 [],
2763 )
2764 .unwrap();
2765
2766 conn.execute(
2768 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
2769 VALUES (1, ?, ?, ?)",
2770 params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
2771 ).unwrap();
2772
2773 create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
2775
2776 let table_count: i64 = conn
2778 .query_row(
2779 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name LIKE 'cfg_%'",
2780 [],
2781 |row| row.get(0),
2782 )
2783 .unwrap();
2784
2785 assert!(table_count >= 4); }
2787
2788 #[test]
2789 fn test_migrate_schema_from_version_0() {
2790 let mut conn = Connection::open_in_memory().unwrap();
2791
2792 conn.execute(
2794 "CREATE TABLE magellan_meta (
2795 id INTEGER PRIMARY KEY CHECK (id = 1),
2796 magellan_schema_version INTEGER NOT NULL,
2797 sqlitegraph_schema_version INTEGER NOT NULL,
2798 created_at INTEGER NOT NULL
2799 )",
2800 [],
2801 )
2802 .unwrap();
2803
2804 conn.execute(
2805 "CREATE TABLE graph_entities (
2806 id INTEGER PRIMARY KEY AUTOINCREMENT,
2807 kind TEXT NOT NULL,
2808 name TEXT NOT NULL,
2809 file_path TEXT,
2810 data TEXT NOT NULL
2811 )",
2812 [],
2813 )
2814 .unwrap();
2815
2816 conn.execute(
2817 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
2818 VALUES (1, ?, ?, ?)",
2819 params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
2820 ).unwrap();
2821
2822 create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
2824
2825 let version: i32 = conn
2827 .query_row(
2828 "SELECT mirage_schema_version FROM mirage_meta WHERE id = 1",
2829 [],
2830 |row| row.get(0),
2831 )
2832 .unwrap();
2833
2834 assert_eq!(version, MIRAGE_SCHEMA_VERSION);
2835 }
2836
2837 #[test]
2838 fn test_migrate_schema_no_op_when_current() {
2839 let mut conn = Connection::open_in_memory().unwrap();
2840
2841 conn.execute(
2843 "CREATE TABLE magellan_meta (
2844 id INTEGER PRIMARY KEY CHECK (id = 1),
2845 magellan_schema_version INTEGER NOT NULL,
2846 sqlitegraph_schema_version INTEGER NOT NULL,
2847 created_at INTEGER NOT NULL
2848 )",
2849 [],
2850 )
2851 .unwrap();
2852
2853 conn.execute(
2854 "CREATE TABLE graph_entities (
2855 id INTEGER PRIMARY KEY AUTOINCREMENT,
2856 kind TEXT NOT NULL,
2857 name TEXT NOT NULL,
2858 file_path TEXT,
2859 data TEXT NOT NULL
2860 )",
2861 [],
2862 )
2863 .unwrap();
2864
2865 conn.execute(
2866 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
2867 VALUES (1, ?, ?, ?)",
2868 params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
2869 ).unwrap();
2870
2871 create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
2873
2874 migrate_schema(&mut conn).unwrap();
2876
2877 let version: i32 = conn
2879 .query_row(
2880 "SELECT mirage_schema_version FROM mirage_meta WHERE id = 1",
2881 [],
2882 |row| row.get(0),
2883 )
2884 .unwrap();
2885
2886 assert_eq!(version, MIRAGE_SCHEMA_VERSION);
2887 }
2888
2889 #[test]
2890 fn test_fk_constraint_cfg_blocks() {
2891 let mut conn = Connection::open_in_memory().unwrap();
2892
2893 conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
2895
2896 conn.execute(
2898 "CREATE TABLE magellan_meta (
2899 id INTEGER PRIMARY KEY CHECK (id = 1),
2900 magellan_schema_version INTEGER NOT NULL,
2901 sqlitegraph_schema_version INTEGER NOT NULL,
2902 created_at INTEGER NOT NULL
2903 )",
2904 [],
2905 )
2906 .unwrap();
2907
2908 conn.execute(
2909 "CREATE TABLE graph_entities (
2910 id INTEGER PRIMARY KEY AUTOINCREMENT,
2911 kind TEXT NOT NULL,
2912 name TEXT NOT NULL,
2913 file_path TEXT,
2914 data TEXT NOT NULL
2915 )",
2916 [],
2917 )
2918 .unwrap();
2919
2920 conn.execute(
2921 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
2922 VALUES (1, ?, ?, ?)",
2923 params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
2924 ).unwrap();
2925
2926 create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
2928
2929 conn.execute(
2931 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
2932 params!("function", "test_func", "test.rs", "{}"),
2933 )
2934 .unwrap();
2935
2936 let function_id: i64 = conn.last_insert_rowid();
2937
2938 let invalid_result = conn.execute(
2940 "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
2941 start_line, start_col, end_line, end_col)
2942 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
2943 params!(9999, "entry", "return", 0, 10, 1, 0, 1, 10),
2944 );
2945
2946 assert!(
2948 invalid_result.is_err(),
2949 "Insert with invalid function_id should fail"
2950 );
2951
2952 let valid_result = conn.execute(
2954 "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
2955 start_line, start_col, end_line, end_col)
2956 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
2957 params!(function_id, "entry", "return", 0, 10, 1, 0, 1, 10),
2958 );
2959
2960 assert!(
2961 valid_result.is_ok(),
2962 "Insert with valid function_id should succeed"
2963 );
2964
2965 let count: i64 = conn
2967 .query_row(
2968 "SELECT COUNT(*) FROM cfg_blocks WHERE function_id = ?",
2969 params![function_id],
2970 |row| row.get(0),
2971 )
2972 .unwrap();
2973
2974 assert_eq!(count, 1, "Should have exactly one cfg_block entry");
2975 }
2976
2977 #[test]
2978 fn test_store_cfg_retrieves_correctly() {
2979 use crate::cfg::{BasicBlock, BlockKind, Cfg, EdgeType, Terminator};
2980
2981 let mut conn = Connection::open_in_memory().unwrap();
2982
2983 conn.execute(
2985 "CREATE TABLE magellan_meta (
2986 id INTEGER PRIMARY KEY CHECK (id = 1),
2987 magellan_schema_version INTEGER NOT NULL,
2988 sqlitegraph_schema_version INTEGER NOT NULL,
2989 created_at INTEGER NOT NULL
2990 )",
2991 [],
2992 )
2993 .unwrap();
2994
2995 conn.execute(
2996 "CREATE TABLE graph_entities (
2997 id INTEGER PRIMARY KEY AUTOINCREMENT,
2998 kind TEXT NOT NULL,
2999 name TEXT NOT NULL,
3000 file_path TEXT,
3001 data TEXT NOT NULL
3002 )",
3003 [],
3004 )
3005 .unwrap();
3006
3007 conn.execute(
3008 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3009 VALUES (1, ?, ?, ?)",
3010 params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
3011 ).unwrap();
3012
3013 create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
3015
3016 conn.execute(
3018 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3019 params!("function", "test_func", "test.rs", "{}"),
3020 )
3021 .unwrap();
3022
3023 let function_id: i64 = conn.last_insert_rowid();
3024
3025 let mut cfg = Cfg::new();
3027
3028 let b0 = cfg.add_node(BasicBlock {
3029 id: 0,
3030 db_id: None,
3031 kind: BlockKind::Entry,
3032 statements: vec!["let x = 1".to_string()],
3033 terminator: Terminator::Goto { target: 1 },
3034 source_location: None,
3035 });
3036
3037 let b1 = cfg.add_node(BasicBlock {
3038 id: 1,
3039 db_id: None,
3040 kind: BlockKind::Normal,
3041 statements: vec![],
3042 terminator: Terminator::Return,
3043 source_location: None,
3044 });
3045
3046 cfg.add_edge(b0, b1, EdgeType::Fallthrough);
3047
3048 store_cfg(&mut conn, function_id, "test_hash_123", &cfg).unwrap();
3050
3051 let block_count: i64 = conn
3053 .query_row(
3054 "SELECT COUNT(*) FROM cfg_blocks WHERE function_id = ?",
3055 params![function_id],
3056 |row| row.get(0),
3057 )
3058 .unwrap();
3059
3060 assert_eq!(block_count, 2, "Should have 2 blocks");
3061
3062 assert!(function_exists(&conn, function_id));
3070 assert!(!function_exists(&conn, 9999));
3071
3072 let loaded_cfg = load_cfg_from_db_with_conn(&conn, function_id).unwrap();
3074
3075 assert_eq!(loaded_cfg.node_count(), 2);
3076 assert_eq!(loaded_cfg.edge_count(), 1);
3077 }
3078
3079 #[test]
3080 fn test_store_cfg_incremental_update_clears_old_data() {
3081 use crate::cfg::{BasicBlock, BlockKind, Cfg, EdgeType, Terminator};
3082
3083 let mut conn = Connection::open_in_memory().unwrap();
3084
3085 conn.execute(
3087 "CREATE TABLE magellan_meta (
3088 id INTEGER PRIMARY KEY CHECK (id = 1),
3089 magellan_schema_version INTEGER NOT NULL,
3090 sqlitegraph_schema_version INTEGER NOT NULL,
3091 created_at INTEGER NOT NULL
3092 )",
3093 [],
3094 )
3095 .unwrap();
3096
3097 conn.execute(
3098 "CREATE TABLE graph_entities (
3099 id INTEGER PRIMARY KEY AUTOINCREMENT,
3100 kind TEXT NOT NULL,
3101 name TEXT NOT NULL,
3102 file_path TEXT,
3103 data TEXT NOT NULL
3104 )",
3105 [],
3106 )
3107 .unwrap();
3108
3109 conn.execute(
3110 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3111 VALUES (1, ?, ?, ?)",
3112 params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
3113 ).unwrap();
3114
3115 create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
3116
3117 conn.execute(
3118 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3119 params!("function", "test_func", "test.rs", "{}"),
3120 )
3121 .unwrap();
3122
3123 let function_id: i64 = conn.last_insert_rowid();
3124
3125 let mut cfg1 = Cfg::new();
3127 let b0 = cfg1.add_node(BasicBlock {
3128 id: 0,
3129 db_id: None,
3130 kind: BlockKind::Entry,
3131 statements: vec![],
3132 terminator: Terminator::Goto { target: 1 },
3133 source_location: None,
3134 });
3135 let b1 = cfg1.add_node(BasicBlock {
3136 id: 1,
3137 db_id: None,
3138 kind: BlockKind::Exit,
3139 statements: vec![],
3140 terminator: Terminator::Return,
3141 source_location: None,
3142 });
3143 cfg1.add_edge(b0, b1, EdgeType::Fallthrough);
3144
3145 store_cfg(&mut conn, function_id, "hash_v1", &cfg1).unwrap();
3146
3147 let block_count_v1: i64 = conn
3148 .query_row(
3149 "SELECT COUNT(*) FROM cfg_blocks WHERE function_id = ?",
3150 params![function_id],
3151 |row| row.get(0),
3152 )
3153 .unwrap();
3154
3155 assert_eq!(block_count_v1, 2);
3156
3157 let mut cfg2 = Cfg::new();
3159 let b0 = cfg2.add_node(BasicBlock {
3160 id: 0,
3161 db_id: None,
3162 kind: BlockKind::Entry,
3163 statements: vec![],
3164 terminator: Terminator::Goto { target: 1 },
3165 source_location: None,
3166 });
3167 let b1 = cfg2.add_node(BasicBlock {
3168 id: 1,
3169 db_id: None,
3170 kind: BlockKind::Normal,
3171 statements: vec![],
3172 terminator: Terminator::Goto { target: 2 },
3173 source_location: None,
3174 });
3175 let b2 = cfg2.add_node(BasicBlock {
3176 id: 2,
3177 db_id: None,
3178 kind: BlockKind::Exit,
3179 statements: vec![],
3180 terminator: Terminator::Return,
3181 source_location: None,
3182 });
3183 cfg2.add_edge(b0, b1, EdgeType::Fallthrough);
3184 cfg2.add_edge(b1, b2, EdgeType::Fallthrough);
3185
3186 store_cfg(&mut conn, function_id, "hash_v3", &cfg2).unwrap();
3187
3188 let block_count_v3: i64 = conn
3189 .query_row(
3190 "SELECT COUNT(*) FROM cfg_blocks WHERE function_id = ?",
3191 params![function_id],
3192 |row| row.get(0),
3193 )
3194 .unwrap();
3195
3196 assert_eq!(block_count_v3, 3);
3198
3199 }
3202
3203 fn create_test_db_with_schema() -> Connection {
3211 let mut conn = Connection::open_in_memory().unwrap();
3212
3213 conn.execute(
3215 "CREATE TABLE magellan_meta (
3216 id INTEGER PRIMARY KEY CHECK (id = 1),
3217 magellan_schema_version INTEGER NOT NULL,
3218 sqlitegraph_schema_version INTEGER NOT NULL,
3219 created_at INTEGER NOT NULL
3220 )",
3221 [],
3222 )
3223 .unwrap();
3224
3225 conn.execute(
3226 "CREATE TABLE graph_entities (
3227 id INTEGER PRIMARY KEY AUTOINCREMENT,
3228 kind TEXT NOT NULL,
3229 name TEXT NOT NULL,
3230 file_path TEXT,
3231 data TEXT NOT NULL
3232 )",
3233 [],
3234 )
3235 .unwrap();
3236
3237 conn.execute(
3239 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3240 VALUES (1, ?, ?, ?)",
3241 params![7, 3, 0], ).unwrap();
3243
3244 conn.execute(
3247 "CREATE TABLE cfg_blocks (
3248 id INTEGER PRIMARY KEY AUTOINCREMENT,
3249 function_id INTEGER NOT NULL,
3250 kind TEXT NOT NULL,
3251 terminator TEXT NOT NULL,
3252 byte_start INTEGER NOT NULL,
3253 byte_end INTEGER NOT NULL,
3254 start_line INTEGER NOT NULL,
3255 start_col INTEGER NOT NULL,
3256 end_line INTEGER NOT NULL,
3257 end_col INTEGER NOT NULL,
3258 coord_x INTEGER NOT NULL DEFAULT 0,
3259 coord_y INTEGER NOT NULL DEFAULT 0,
3260 coord_z INTEGER NOT NULL DEFAULT 0,
3261 FOREIGN KEY (function_id) REFERENCES graph_entities(id)
3262 )",
3263 [],
3264 )
3265 .unwrap();
3266
3267 conn.execute(
3269 "CREATE TABLE graph_edges (
3270 id INTEGER PRIMARY KEY AUTOINCREMENT,
3271 from_id INTEGER NOT NULL,
3272 to_id INTEGER NOT NULL,
3273 edge_type TEXT NOT NULL,
3274 data TEXT
3275 )",
3276 [],
3277 )
3278 .unwrap();
3279
3280 create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
3282
3283 conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
3285
3286 conn
3287 }
3288
3289 #[test]
3292 fn test_resolve_function_by_id() {
3293 let conn = create_test_db_with_schema();
3294
3295 conn.execute(
3297 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3298 params!("function", "my_func", "test.rs", "{}"),
3299 )
3300 .unwrap();
3301 let function_id: i64 = conn.last_insert_rowid();
3302
3303 let result = resolve_function_name_with_conn(&conn, &function_id.to_string()).unwrap();
3305 assert_eq!(result, function_id);
3306 }
3307
3308 #[test]
3309 fn test_resolve_function_by_name() {
3310 let conn = create_test_db_with_schema();
3311
3312 conn.execute(
3315 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3316 params!(
3317 "Symbol",
3318 "test_function",
3319 "test.rs",
3320 r#"{"kind":"Function"}"#
3321 ),
3322 )
3323 .unwrap();
3324 let function_id: i64 = conn.last_insert_rowid();
3325
3326 let result = resolve_function_name_with_conn(&conn, "test_function").unwrap();
3328 assert_eq!(result, function_id);
3329 }
3330
3331 #[test]
3332 fn test_resolve_function_not_found() {
3333 let conn = create_test_db_with_schema();
3334
3335 let result = resolve_function_name_with_conn(&conn, "nonexistent_func");
3337
3338 assert!(
3339 result.is_err(),
3340 "Should return error for non-existent function"
3341 );
3342 let err_msg = result.unwrap_err().to_string();
3343 assert!(err_msg.contains("not found") || err_msg.contains("not found in database"));
3344 }
3345
3346 #[test]
3347 fn test_resolve_function_numeric_string() {
3348 let conn = create_test_db_with_schema();
3349
3350 conn.execute(
3352 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3353 params!("function", "func123", "test.rs", "{}"),
3354 )
3355 .unwrap();
3356
3357 let result = resolve_function_name_with_conn(&conn, "123").unwrap();
3359 assert_eq!(result, 123);
3360
3361 conn.execute(
3363 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3364 params!("function", "another_func", "test.rs", "{}"),
3365 )
3366 .unwrap();
3367 let _id_456 = conn.last_insert_rowid();
3368
3369 let result = resolve_function_name_with_conn(&conn, "999").unwrap();
3372 assert_eq!(result, 999, "Should return numeric ID directly");
3373 }
3374
3375 #[test]
3376 fn test_load_cfg_not_found() {
3377 let conn = create_test_db_with_schema();
3378
3379 let result = load_cfg_from_db_with_conn(&conn, 99999);
3381
3382 assert!(
3383 result.is_err(),
3384 "Should return error for function with no CFG"
3385 );
3386 let err_msg = result.unwrap_err().to_string();
3387 assert!(err_msg.contains("No CFG blocks found") || err_msg.contains("not found"));
3388 }
3389
3390 #[test]
3391 fn test_load_cfg_empty_terminator() {
3392 use crate::cfg::Terminator;
3393
3394 let conn = create_test_db_with_schema();
3395
3396 conn.execute(
3398 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3399 params!("function", "empty_term_func", "test.rs", "{}"),
3400 )
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, "return", "return", 0, 10, 1, 0, 1, 10),
3410 )
3411 .unwrap();
3412
3413 let cfg = load_cfg_from_db_with_conn(&conn, function_id).unwrap();
3415
3416 assert_eq!(cfg.node_count(), 1);
3417 let block = &cfg[petgraph::graph::NodeIndex::new(0)];
3418 assert!(matches!(block.terminator, Terminator::Return));
3419 }
3420
3421 #[test]
3422 fn test_load_cfg_with_multiple_edge_types() {
3423 use crate::cfg::EdgeType;
3424
3425 let conn = create_test_db_with_schema();
3426
3427 conn.execute(
3429 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3430 params!("function", "edge_types_func", "test.rs", "{}"),
3431 )
3432 .unwrap();
3433 let function_id: i64 = conn.last_insert_rowid();
3434
3435 conn.execute(
3437 "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
3438 start_line, start_col, end_line, end_col)
3439 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
3440 params!(function_id, "entry", "conditional", 0, 10, 1, 0, 1, 10),
3441 )
3442 .unwrap();
3443 let _block_0_id: i64 = conn.last_insert_rowid();
3444
3445 conn.execute(
3446 "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
3447 start_line, start_col, end_line, end_col)
3448 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
3449 params!(function_id, "block", "fallthrough", 10, 20, 2, 0, 2, 10),
3450 )
3451 .unwrap();
3452 let _block_1_id: i64 = conn.last_insert_rowid();
3453
3454 conn.execute(
3455 "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
3456 start_line, start_col, end_line, end_col)
3457 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
3458 params!(function_id, "block", "call", 20, 30, 3, 0, 3, 10),
3459 )
3460 .unwrap();
3461 let _block_2_id: i64 = conn.last_insert_rowid();
3462
3463 conn.execute(
3464 "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
3465 start_line, start_col, end_line, end_col)
3466 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
3467 params!(function_id, "return", "return", 30, 40, 4, 0, 4, 10),
3468 )
3469 .unwrap();
3470 let _block_3_id: i64 = conn.last_insert_rowid();
3471
3472 let cfg = load_cfg_from_db_with_conn(&conn, function_id).unwrap();
3474
3475 assert_eq!(cfg.node_count(), 4);
3476 assert_eq!(cfg.edge_count(), 4);
3477
3478 use petgraph::visit::EdgeRef;
3483 let edges: Vec<_> = cfg
3484 .edge_references()
3485 .map(|e| (e.source().index(), e.target().index(), *e.weight()))
3486 .collect();
3487
3488 assert!(edges.contains(&(0, 1, EdgeType::TrueBranch)));
3489 assert!(edges.contains(&(0, 2, EdgeType::FalseBranch)));
3490 assert!(edges.contains(&(1, 2, EdgeType::Fallthrough)));
3491 assert!(edges.contains(&(2, 3, EdgeType::Call)));
3492 }
3493
3494 #[test]
3495 fn test_get_function_name() {
3496 let conn = create_test_db_with_schema();
3497
3498 conn.execute(
3500 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3501 params!("function", "my_test_func", "test.rs", "{}"),
3502 )
3503 .unwrap();
3504 let function_id: i64 = conn.last_insert_rowid();
3505
3506 let name = get_function_name(&conn, function_id);
3508 assert_eq!(name, Some("my_test_func".to_string()));
3509
3510 let name = get_function_name(&conn, 9999);
3512 assert_eq!(name, None);
3513 }
3514
3515 #[test]
3516 fn test_get_path_elements() {
3517 let conn = create_test_db_with_schema();
3518
3519 conn.execute(
3521 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3522 params!("function", "path_test_func", "test.rs", "{}"),
3523 )
3524 .unwrap();
3525 let function_id: i64 = conn.last_insert_rowid();
3526
3527 conn.execute(
3529 "INSERT INTO cfg_paths (path_id, function_id, path_kind, entry_block, exit_block, length, created_at)
3530 VALUES (?, ?, ?, ?, ?, ?, ?)",
3531 params!("test_path_abc123", function_id, "normal", 0, 2, 3, 1000),
3532 ).unwrap();
3533
3534 conn.execute(
3536 "INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
3537 params!("test_path_abc123", 0, 0),
3538 )
3539 .unwrap();
3540 conn.execute(
3541 "INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
3542 params!("test_path_abc123", 1, 1),
3543 )
3544 .unwrap();
3545 conn.execute(
3546 "INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
3547 params!("test_path_abc123", 2, 2),
3548 )
3549 .unwrap();
3550
3551 let blocks = get_path_elements(&conn, "test_path_abc123").unwrap();
3553 assert_eq!(blocks, vec![0, 1, 2]);
3554
3555 let result = get_path_elements(&conn, "nonexistent_path");
3557 assert!(result.is_err());
3558 }
3559
3560 #[test]
3561 fn test_compute_path_impact_from_db() {
3562 use crate::cfg::{BasicBlock, BlockKind, Terminator};
3563
3564 let conn = create_test_db_with_schema();
3565
3566 conn.execute(
3568 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3569 params!("function", "impact_test_func", "test.rs", "{}"),
3570 )
3571 .unwrap();
3572 let function_id: i64 = conn.last_insert_rowid();
3573
3574 let mut cfg = crate::cfg::Cfg::new();
3576 let b0 = cfg.add_node(BasicBlock {
3577 id: 0,
3578 db_id: None,
3579 kind: BlockKind::Entry,
3580 statements: vec![],
3581 terminator: Terminator::Goto { target: 1 },
3582 source_location: None,
3583 });
3584 let b1 = cfg.add_node(BasicBlock {
3585 id: 1,
3586 db_id: None,
3587 kind: BlockKind::Normal,
3588 statements: vec![],
3589 terminator: Terminator::Goto { target: 2 },
3590 source_location: None,
3591 });
3592 let b2 = cfg.add_node(BasicBlock {
3593 id: 2,
3594 db_id: None,
3595 kind: BlockKind::Normal,
3596 statements: vec![],
3597 terminator: Terminator::Goto { target: 3 },
3598 source_location: None,
3599 });
3600 let b3 = cfg.add_node(BasicBlock {
3601 id: 3,
3602 db_id: None,
3603 kind: BlockKind::Exit,
3604 statements: vec![],
3605 terminator: Terminator::Return,
3606 source_location: None,
3607 });
3608 cfg.add_edge(b0, b1, crate::cfg::EdgeType::Fallthrough);
3609 cfg.add_edge(b1, b2, crate::cfg::EdgeType::Fallthrough);
3610 cfg.add_edge(b2, b3, crate::cfg::EdgeType::Fallthrough);
3611
3612 conn.execute(
3614 "INSERT INTO cfg_paths (path_id, function_id, path_kind, entry_block, exit_block, length, created_at)
3615 VALUES (?, ?, ?, ?, ?, ?, ?)",
3616 params!("impact_test_path", function_id, "normal", 0, 3, 3, 1000),
3617 ).unwrap();
3618
3619 conn.execute(
3620 "INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
3621 params!("impact_test_path", 0, 0),
3622 )
3623 .unwrap();
3624 conn.execute(
3625 "INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
3626 params!("impact_test_path", 1, 1),
3627 )
3628 .unwrap();
3629 conn.execute(
3630 "INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
3631 params!("impact_test_path", 2, 3),
3632 )
3633 .unwrap();
3634
3635 let impact = compute_path_impact_from_db(&conn, "impact_test_path", &cfg, None).unwrap();
3637
3638 assert_eq!(impact.path_id, "impact_test_path");
3639 assert_eq!(impact.path_length, 3);
3640 assert!(impact.unique_blocks_affected.contains(&2));
3642 }
3643
3644 #[test]
3647 fn test_load_cfg_missing_cfg_blocks_table() {
3648 let conn = Connection::open_in_memory().unwrap();
3649
3650 conn.execute(
3652 "CREATE TABLE magellan_meta (
3653 id INTEGER PRIMARY KEY CHECK (id = 1),
3654 magellan_schema_version INTEGER NOT NULL,
3655 sqlitegraph_schema_version INTEGER NOT NULL,
3656 created_at INTEGER NOT NULL
3657 )",
3658 [],
3659 )
3660 .unwrap();
3661
3662 conn.execute(
3663 "CREATE TABLE graph_entities (
3664 id INTEGER PRIMARY KEY AUTOINCREMENT,
3665 kind TEXT NOT NULL,
3666 name TEXT NOT NULL,
3667 file_path TEXT,
3668 data TEXT NOT NULL
3669 )",
3670 [],
3671 )
3672 .unwrap();
3673
3674 conn.execute(
3675 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3676 VALUES (1, ?, ?, ?)",
3677 params![6, 3, 0], ).unwrap();
3679
3680 conn.execute(
3682 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3683 params!("function", "test_func", "test.rs", "{}"),
3684 )
3685 .unwrap();
3686 let function_id: i64 = conn.last_insert_rowid();
3687
3688 let result = load_cfg_from_db_with_conn(&conn, function_id);
3690 assert!(result.is_err(), "Should fail when cfg_blocks table missing");
3691
3692 let err_msg = result.unwrap_err().to_string();
3693 assert!(
3695 err_msg.contains("cfg_blocks") || err_msg.contains("prepare"),
3696 "Error should mention cfg_blocks or prepare: {}",
3697 err_msg
3698 );
3699 }
3700
3701 #[test]
3702 fn test_load_cfg_function_not_found() {
3703 let conn = create_test_db_with_schema();
3704
3705 let result = load_cfg_from_db_with_conn(&conn, 99999);
3707 assert!(result.is_err(), "Should fail for non-existent function");
3708
3709 let err_msg = result.unwrap_err().to_string();
3710 assert!(
3711 err_msg.contains("No CFG blocks found") || err_msg.contains("not found"),
3712 "Error should mention missing CFG: {}",
3713 err_msg
3714 );
3715 assert!(
3716 err_msg.contains("magellan watch"),
3717 "Error should suggest running magellan watch: {}",
3718 err_msg
3719 );
3720 }
3721
3722 #[test]
3723 fn test_load_cfg_empty_blocks() {
3724 let conn = create_test_db_with_schema();
3725
3726 conn.execute(
3728 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3729 params!("function", "func_without_cfg", "test.rs", "{}"),
3730 )
3731 .unwrap();
3732 let function_id: i64 = conn.last_insert_rowid();
3733
3734 let result = load_cfg_from_db_with_conn(&conn, function_id);
3736 assert!(result.is_err(), "Should fail when no CFG blocks exist");
3737
3738 let err_msg = result.unwrap_err().to_string();
3739 assert!(
3740 err_msg.contains("No CFG blocks found"),
3741 "Error should mention no CFG blocks: {}",
3742 err_msg
3743 );
3744 assert!(
3745 err_msg.contains("magellan watch"),
3746 "Error should suggest running magellan watch: {}",
3747 err_msg
3748 );
3749 }
3750
3751 #[test]
3752 fn test_resolve_function_missing_with_helpful_message() {
3753 let conn = create_test_db_with_schema();
3754
3755 let result = resolve_function_name_with_conn(&conn, "nonexistent_function");
3757 assert!(result.is_err(), "Should fail for non-existent function");
3758
3759 let err_msg = result.unwrap_err().to_string();
3760 assert!(
3761 err_msg.contains("not found") || err_msg.contains("not found in database"),
3762 "Error should mention function not found: {}",
3763 err_msg
3764 );
3765 }
3766
3767 #[test]
3768 fn test_open_database_old_magellan_schema() {
3769 let conn = Connection::open_in_memory().unwrap();
3770
3771 conn.execute(
3773 "CREATE TABLE magellan_meta (
3774 id INTEGER PRIMARY KEY CHECK (id = 1),
3775 magellan_schema_version INTEGER NOT NULL,
3776 sqlitegraph_schema_version INTEGER NOT NULL,
3777 created_at INTEGER NOT NULL
3778 )",
3779 [],
3780 )
3781 .unwrap();
3782
3783 conn.execute(
3784 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3785 VALUES (1, 6, 3, 0)", [],
3787 ).unwrap();
3788
3789 conn.execute(
3791 "CREATE TABLE cfg_blocks (
3792 id INTEGER PRIMARY KEY AUTOINCREMENT,
3793 function_id INTEGER NOT NULL,
3794 kind TEXT NOT NULL,
3795 terminator TEXT NOT NULL,
3796 byte_start INTEGER NOT NULL,
3797 byte_end INTEGER NOT NULL,
3798 start_line INTEGER NOT NULL,
3799 start_col INTEGER NOT NULL,
3800 end_line INTEGER NOT NULL,
3801 end_col INTEGER NOT NULL,
3802 FOREIGN KEY (function_id) REFERENCES graph_entities(id)
3803 )",
3804 [],
3805 )
3806 .unwrap();
3807
3808 drop(conn);
3810 let db_file = tempfile::NamedTempFile::new().unwrap();
3811 {
3812 let conn = Connection::open(db_file.path()).unwrap();
3813 conn.execute(
3814 "CREATE TABLE magellan_meta (
3815 id INTEGER PRIMARY KEY CHECK (id = 1),
3816 magellan_schema_version INTEGER NOT NULL,
3817 sqlitegraph_schema_version INTEGER NOT NULL,
3818 created_at INTEGER NOT NULL
3819 )",
3820 [],
3821 )
3822 .unwrap();
3823 conn.execute(
3824 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3825 VALUES (1, 6, 3, 0)",
3826 [],
3827 ).unwrap();
3828 conn.execute(
3829 "CREATE TABLE graph_entities (
3830 id INTEGER PRIMARY KEY AUTOINCREMENT,
3831 kind TEXT NOT NULL,
3832 name TEXT NOT NULL,
3833 file_path TEXT,
3834 data TEXT NOT NULL
3835 )",
3836 [],
3837 )
3838 .unwrap();
3839 }
3840
3841 let result = MirageDb::open(db_file.path());
3842 assert!(result.is_err(), "Should fail with old Magellan schema");
3843
3844 let err_msg = result.unwrap_err().to_string();
3845 assert!(
3846 err_msg.contains("too old") || err_msg.contains("minimum"),
3847 "Error should mention schema too old: {}",
3848 err_msg
3849 );
3850 assert!(
3851 err_msg.contains("magellan watch"),
3852 "Error should suggest running magellan watch: {}",
3853 err_msg
3854 );
3855 }
3856
3857 #[test]
3860 fn test_backend_detect_sqlite_header() {
3861 use std::io::Write;
3862
3863 let temp_file = tempfile::NamedTempFile::new().unwrap();
3865 let mut file = std::fs::File::create(temp_file.path()).unwrap();
3866 file.write_all(b"SQLite format 3\0").unwrap();
3867 file.sync_all().unwrap();
3868
3869 let backend = BackendFormat::detect(temp_file.path()).unwrap();
3870 assert_eq!(
3871 backend,
3872 BackendFormat::SQLite,
3873 "Should detect SQLite format"
3874 );
3875 }
3876
3877 #[test]
3878 fn test_backend_detect_nonexistent_file() {
3879 let backend = BackendFormat::detect(Path::new("/nonexistent/path/to/file.db")).unwrap();
3880 assert_eq!(
3881 backend,
3882 BackendFormat::Unknown,
3883 "Non-existent file should be Unknown"
3884 );
3885 }
3886
3887 #[test]
3888 fn test_backend_detect_empty_file() {
3889 let temp_file = tempfile::NamedTempFile::new().unwrap();
3891 let backend = BackendFormat::detect(temp_file.path()).unwrap();
3894 assert_eq!(
3895 backend,
3896 BackendFormat::Unknown,
3897 "Empty file should be Unknown"
3898 );
3899 }
3900
3901 #[test]
3902 fn test_backend_detect_partial_header() {
3903 use std::io::Write;
3904
3905 let temp_file = tempfile::NamedTempFile::new().unwrap();
3907 let mut file = std::fs::File::create(temp_file.path()).unwrap();
3908 file.write_all(b"SQLite").unwrap(); file.sync_all().unwrap();
3910
3911 let backend = BackendFormat::detect(temp_file.path()).unwrap();
3912 assert_eq!(
3913 backend,
3914 BackendFormat::Unknown,
3915 "Partial header should be Unknown"
3916 );
3917 }
3918
3919 #[test]
3920 fn test_backend_equality() {
3921 assert_eq!(BackendFormat::SQLite, BackendFormat::SQLite);
3922 assert_eq!(BackendFormat::Unknown, BackendFormat::Unknown);
3923
3924 assert_ne!(BackendFormat::SQLite, BackendFormat::Unknown);
3925 }
3926}