Skip to main content

mirage/storage/
mod.rs

1// Database storage layer extending Magellan's schema
2//
3// Mirage uses the same Magellan database and extends it with:
4// - cfg_blocks: Basic blocks within functions (managed by Magellan v7+)
5// - cfg_paths: Enumerated execution paths
6// - cfg_path_elements: Blocks in each path
7// - cfg_dominators: Dominance relationships
8// - cfg_post_dominators: Reverse dominance
9//
10// Note: cfg_edges table is managed by Magellan v11+; Mirage computes
11// edges in memory from terminator data and does not create/query this table.
12
13pub mod paths;
14
15// Backend-agnostic storage trait and implementations (Phase 069-01)
16#[cfg(feature = "backend-geometric")]
17pub mod geometric;
18#[cfg(feature = "backend-sqlite")]
19pub mod sqlite_backend;
20
21// Also support the aliased feature names for convenience
22#[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
31// GraphBackend imports for dual backend support
32use sqlitegraph::{open_graph, GraphBackend, GraphConfig, SnapshotId};
33
34// Note: We avoid importing BackendRouter here to prevent circular dependency
35// with crate::router which uses crate::storage. Instead, we use fully qualified
36// paths where needed.
37
38// Backend implementations (Phase 069-01)
39#[cfg(feature = "backend-geometric")]
40pub use geometric::GeometricStorage;
41#[cfg(feature = "backend-sqlite")]
42pub use sqlite_backend::SqliteStorage;
43
44// Re-export path caching functions
45// Note: Some exports like PathCache, store_paths, etc. are not currently used
46// but are kept for potential future use and API completeness
47#[allow(unused_imports)]
48pub use paths::{
49    get_cached_paths, invalidate_function_paths, store_paths, update_function_paths_if_changed,
50    PathCache,
51};
52
53// ============================================================================
54// Backend-Agnostic Storage Trait (Phase 069-01)
55// ============================================================================
56
57/// Backend-agnostic storage trait for CFG data
58///
59/// This trait abstracts over supported storage backends,
60/// enabling runtime backend detection and zero breaking changes.
61///
62/// # Design
63///
64/// - Follows llmgrep's Backend pattern for consistency
65/// - All methods take `&self` (not `&mut self`) to enable shared access
66/// - Errors are returned as `anyhow::Error` for flexibility
67///
68/// # Examples
69///
70/// ```ignore
71/// # use mirage_analyzer::storage::{StorageTrait, Backend};
72/// # fn main() -> anyhow::Result<()> {
73/// // Auto-detect and open backend
74/// let backend = Backend::detect_and_open("/path/to/db")?;
75///
76/// // Query CFG blocks
77/// let blocks = backend.get_cfg_blocks(123)?;
78/// # Ok(())
79/// # }
80/// ```
81pub trait StorageTrait {
82    /// Get CFG blocks for a function
83    ///
84    /// Returns all basic blocks for the given function_id.
85    /// For SQLite: queries cfg_blocks table
86    ///
87    /// # Arguments
88    ///
89    /// * `function_id` - ID of the function in graph_entities
90    ///
91    /// # Returns
92    ///
93    /// * `Ok(Vec<CfgBlockData>)` - Vector of CFG block data
94    /// * `Err(...)` - Error if query fails
95    fn get_cfg_blocks(&self, function_id: i64) -> Result<Vec<CfgBlockData>>;
96
97    /// Get entity by ID
98    ///
99    /// Returns the entity with the given ID from graph_entities.
100    ///
101    /// # Arguments
102    ///
103    /// * `entity_id` - ID of the entity
104    ///
105    /// # Returns
106    ///
107    /// * `Some(GraphEntity)` - Entity if found
108    /// * `None` - Entity not found
109    fn get_entity(&self, entity_id: i64) -> Option<sqlitegraph::GraphEntity>;
110
111    /// Get cached paths for a function (optional)
112    ///
113    /// Returns cached enumerated paths if available.
114    /// Default implementation returns None (no caching).
115    ///
116    /// # Arguments
117    ///
118    /// * `function_id` - ID of the function
119    ///
120    /// # Returns
121    ///
122    /// * `Ok(Some(paths))` - Cached paths if available
123    /// * `Ok(None)` - No cached paths
124    /// * `Err(...)` - Error if query fails
125    fn get_cached_paths(&self, _function_id: i64) -> Result<Option<Vec<crate::cfg::Path>>> {
126        Ok(None) // Default: no caching
127    }
128
129    /// Get callees (functions called by the given function)
130    ///
131    /// Returns IDs of all functions that this function calls, based on
132    /// call graph edges in the database.
133    ///
134    /// # Arguments
135    ///
136    /// * `function_id` - ID of the caller function
137    ///
138    /// # Returns
139    ///
140    /// * `Ok(Vec<i64>)` - Callee function IDs
141    /// * `Err(...)` - Error if query fails
142    ///
143    /// # Default
144    ///
145    /// Default implementation returns an empty vector.
146    fn get_callees(&self, _function_id: i64) -> Result<Vec<i64>> {
147        Ok(Vec::new())
148    }
149}
150
151/// CFG block data (backend-agnostic representation)
152///
153/// This struct represents the data returned by `StorageTrait::get_cfg_blocks`.
154/// It is a simplified version of Magellan's CfgBlock that contains only the
155/// fields needed by Mirage for CFG analysis.
156#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
157pub struct CfgBlockData {
158    /// Block ID (from cfg_blocks table)
159    pub id: i64,
160    /// Block kind (entry, conditional, loop, match, return, etc.)
161    pub kind: String,
162    /// Terminator kind (how control exits this block)
163    pub terminator: String,
164    /// Byte offset where block starts
165    pub byte_start: u64,
166    /// Byte offset where block ends
167    pub byte_end: u64,
168    /// Line where block starts (1-indexed)
169    pub start_line: u64,
170    /// Column where block starts (0-indexed)
171    pub start_col: u64,
172    /// Line where block ends (1-indexed)
173    pub end_line: u64,
174    /// Column where block ends (0-indexed)
175    pub end_col: u64,
176    /// 4D Spatial Coordinates
177    /// X coordinate: dominator depth (control flow hierarchy level)
178    pub coord_x: i64,
179    /// Y coordinate: loop nesting depth (how many loops surround this block)
180    pub coord_y: i64,
181    /// Z coordinate: branch distance from entry point
182    pub coord_z: i64,
183}
184
185/// Storage backend enum (Phase 069-01)
186///
187/// This enum wraps SqliteStorage or GeometricStorage and delegates
188/// StorageTrait methods to the appropriate implementation.
189///
190/// Follows llmgrep's Backend pattern for consistency across tools.
191#[derive(Debug)]
192#[allow(clippy::large_enum_variant)]
193pub enum Backend {
194    /// SQLite storage backend (traditional, always available)
195    #[cfg(feature = "backend-sqlite")]
196    Sqlite(SqliteStorage),
197    /// Geometric storage backend for .geo files (Magellan 3.0+)
198    #[cfg(feature = "backend-geometric")]
199    Geometric(GeometricStorage),
200}
201
202impl Backend {
203    /// Detect backend format from database file and open appropriate backend
204    ///
205    /// Uses file extension and magellan's detection for consistent backend detection.
206    ///
207    /// # Arguments
208    ///
209    /// * `db_path` - Path to the database file
210    ///
211    /// # Returns
212    ///
213    /// * `Ok(Backend)` - Appropriate backend variant
214    /// * `Err(...)` - Error if detection or opening fails
215    ///
216    /// # Examples
217    ///
218    /// ```ignore
219    /// # use mirage_analyzer::storage::Backend;
220    /// # fn main() -> anyhow::Result<()> {
221    /// let backend = Backend::detect_and_open("/path/to/codegraph.db")?;
222    /// # Ok(())
223    /// # }
224    /// ```
225    pub fn detect_and_open(db_path: &Path) -> Result<Self> {
226        use magellan::migrate_backend_cmd::detect_backend_format;
227
228        // Check for .geo extension first (Magellan 3.0+ geometric backend)
229        #[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        // For non-.geo files, use Magellan's SQLite detection.
240        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    /// Check if this is a Geometric backend
260    pub fn is_geometric(&self) -> bool {
261        match self {
262            #[cfg(feature = "backend-geometric")]
263            Backend::Geometric(_) => true,
264            _ => false,
265        }
266    }
267
268    /// Check if this is a SQLite backend
269    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    /// Delegate get_cfg_blocks to inner backend
281    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    /// Delegate get_entity to inner backend
293    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    /// Delegate get_cached_paths to inner backend
305    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    /// Delegate get_callees to inner backend
317    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
329// Implement StorageTrait for Backend (delegates to inner storage)
330impl 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/// Database backend format detected in a graph database file.
349///
350/// This is the legacy format detection enum. For new code, use the
351/// `Backend` enum (with StorageTrait) which provides full backend abstraction.
352#[derive(Debug, Clone, Copy, PartialEq, Eq)]
353pub enum BackendFormat {
354    /// SQLite-based backend (default, backward compatible)
355    SQLite,
356    /// Geometric backend (.geo files, Magellan 3.0+)
357    Geometric,
358    /// Unknown or unrecognized format
359    Unknown,
360}
361
362impl BackendFormat {
363    /// Detect which backend format a database file uses.
364    ///
365    /// Checks the file header to determine if the database is SQLite format.
366    /// Returns Unknown if the file doesn't exist or has an unrecognized header.
367    ///
368    /// **Deprecated:** Use `Backend::detect_and_open()` for new code which provides
369    /// full backend abstraction, not just format detection.
370    pub fn detect(path: &Path) -> Result<Self> {
371        if !path.exists() {
372            return Ok(BackendFormat::Unknown);
373        }
374
375        // Check for .geo extension first (Magellan 3.0+ geometric backend)
376        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        // SQLite databases start with "SQLite format 3"
389        Ok(if &header[..15] == b"SQLite format 3" {
390            BackendFormat::SQLite
391        } else {
392            BackendFormat::Unknown
393        })
394    }
395}
396
397/// Mirage schema version
398pub const MIRAGE_SCHEMA_VERSION: i32 = 1;
399
400/// Minimum Magellan schema version we require
401/// Magellan v7+ includes cfg_blocks table with AST-based CFG
402pub const MIN_MAGELLAN_SCHEMA_VERSION: i32 = 7;
403
404/// Magellan schema version used in tests (for consistency)
405pub const TEST_MAGELLAN_SCHEMA_VERSION: i32 = MIN_MAGELLAN_SCHEMA_VERSION;
406
407/// Alias for backward compatibility (same as TEST_MAGELLAN_SCHEMA_VERSION)
408pub const REQUIRED_MAGELLAN_SCHEMA_VERSION: i32 = TEST_MAGELLAN_SCHEMA_VERSION;
409
410/// SQLiteGraph schema version we require
411pub const REQUIRED_SQLITEGRAPH_SCHEMA_VERSION: i32 = 3;
412
413/// Database connection wrapper
414///
415/// Uses Backend enum for CFG queries (Phase 069-02) and GraphBackend for entity queries.
416/// This dual-backend approach allows gradual migration from direct Connection usage.
417pub struct MirageDb {
418    /// Storage backend for CFG queries (Phase 069-02)
419    /// Wraps either SqliteStorage or KvStorage for backend-agnostic CFG access.
420    storage: Backend,
421
422    /// Backend-agnostic graph interface for entity queries
423    /// Used for entity_ids(), get_node(), kv_get() and other GraphBackend operations.
424    graph_backend: Box<dyn GraphBackend>,
425
426    /// Snapshot ID for consistent reads
427    snapshot_id: SnapshotId,
428
429    // SQLite-specific connection (only available with sqlite feature)
430    // DEPRECATED: Use storage field instead for new code
431    #[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    /// Open database at the given path
447    ///
448    /// This can open:
449    /// - A Mirage database (with mirage_meta table)
450    /// - A Magellan database (extends it with Mirage tables)
451    ///
452    /// Phase 069-02: Uses Backend::detect_and_open() for CFG queries
453    /// and open_graph() for entity queries (GraphBackend).
454    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        // Phase 069-02: Use Backend::detect_and_open() for storage layer
461        let storage = Backend::detect_and_open(path).context("Failed to open storage backend")?;
462
463        // Detect backend format from file header for GraphBackend creation
464        let detected_backend =
465            BackendFormat::detect(path).context("Failed to detect backend format")?;
466
467        // Handle geometric backend specially - it doesn't use GraphBackend
468        #[cfg(feature = "backend-geometric")]
469        if detected_backend == BackendFormat::Geometric {
470            let snapshot_id = SnapshotId::current();
471
472            // For geometric backend, we don't have a traditional GraphBackend
473            // Instead, we use the GeometricStorage directly for all operations
474            // Create a stub GraphBackend that returns errors for unsupported operations
475            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        // Select appropriate GraphConfig based on detected backend
490        let cfg = match detected_backend {
491            BackendFormat::SQLite => GraphConfig::sqlite(),
492            BackendFormat::Geometric => {
493                // This case is handled above, but needed for match completeness
494                GraphConfig::native()
495            }
496            BackendFormat::Unknown => {
497                anyhow::bail!(
498                    "Unknown database format: {}. Cannot determine backend.",
499                    path.display()
500                );
501            }
502        };
503
504        // Use open_graph factory to create GraphBackend for entity queries
505        let graph_backend = open_graph(path, &cfg).context("Failed to open graph database")?;
506
507        let snapshot_id = SnapshotId::current();
508
509        // For SQLite backend, open Connection and validate schema
510        #[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    /// Validate database schema for SQLite backend
527    #[cfg(feature = "backend-sqlite")]
528    fn validate_schema_sqlite(conn: &mut Connection, _path: &Path) -> Result<()> {
529        // Check if mirage_meta table exists
530        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        // Get Mirage schema version (0 if table doesn't exist)
541        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        // Check Magellan schema compatibility
564        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        // Check for cfg_blocks table existence (Magellan v7+)
584        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 doesn't exist, this is a pure Magellan database.
602        // Initialize Mirage tables to extend it.
603        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    /// Get a reference to the underlying Connection (SQLite backend only)
613    ///
614    /// Phase 069-02: DEPRECATED - Use storage() for CFG queries, backend() for entity queries.
615    #[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    /// Get a mutable reference to the underlying Connection (SQLite backend only)
625    ///
626    /// Phase 069-02: DEPRECATED - Use storage() for CFG queries, backend() for entity queries.
627    #[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    /// Get a reference to the storage backend for CFG queries
637    ///
638    /// Phase 069-02: Use this to access CFG-specific storage operations
639    /// like get_cfg_blocks(), get_entity(), and get_cached_paths().
640    ///
641    /// This is the preferred way to access CFG data in new code.
642    pub fn storage(&self) -> &Backend {
643        &self.storage
644    }
645
646    /// Get a reference to the backend-agnostic GraphBackend interface
647    ///
648    /// Use this for entity queries (entity_ids, get_node, kv_get, etc.).
649    /// Phase 069-02: This now returns the GraphBackend used for entity queries,
650    /// while storage() provides the Backend enum for CFG queries.
651    pub fn backend(&self) -> &dyn GraphBackend {
652        self.graph_backend.as_ref()
653    }
654
655    /// Check if the database backend is SQLite
656    ///
657    /// This is useful for runtime checks when certain features
658    /// are only available with specific backends (e.g., path caching).
659    #[cfg(feature = "backend-sqlite")]
660    pub fn is_sqlite(&self) -> bool {
661        self.conn.is_some()
662    }
663}
664
665/// Create a stub GraphBackend for geometric backend
666///
667/// Geometric backend doesn't use sqlitegraph's GraphBackend trait.
668/// Instead, it provides its own query methods directly via GeometricBackend.
669/// This stub is used to satisfy the MirageDb struct's graph_backend field.
670///
671/// Any code that tries to use GraphBackend methods on a geometric database
672/// will get appropriate errors directing them to use the geometric-specific
673/// methods instead.
674#[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    /// Stub GraphBackend implementation for geometric backend
682    /// All methods return errors since geometric uses its own API
683    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            // Return empty list - geometric doesn't use entity_ids
712            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            // Return empty list - geometric doesn't use GraphBackend neighbors
732            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            // Return empty list - geometric has its own pathfinding
742            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
856/// A schema migration
857struct Migration {
858    version: i32,
859    description: &'static str,
860    up: fn(&mut Connection) -> Result<()>,
861}
862
863/// Get all registered migrations
864fn migrations() -> Vec<Migration> {
865    // No migrations yet - framework is ready for future schema changes
866    vec![]
867}
868
869/// Run schema migrations to bring database up to current version
870pub 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        // Already at or above current version
881        return Ok(());
882    }
883
884    // Get migrations that need to run
885    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        // Run migration
892        (migration.up)(conn).with_context(|| {
893            format!(
894                "Failed to run migration v{}: {}",
895                migration.version, migration.description
896            )
897        })?;
898
899        // Update version
900        conn.execute(
901            "UPDATE mirage_meta SET mirage_schema_version = ? WHERE id = 1",
902            params![migration.version],
903        )?;
904    }
905
906    // Ensure we're at the final version
907    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
917/// Create Mirage schema tables in an existing Magellan database
918///
919/// The magellan_schema_version parameter should be the actual version
920/// from the magellan_meta table, not MIN_MAGELLAN_SCHEMA_VERSION.
921pub fn create_schema(conn: &mut Connection, _magellan_schema_version: i32) -> Result<()> {
922    // Create mirage_meta table
923    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    // Create cfg_blocks table (Magellan v7+ schema)
935    // Note: Mirage now uses Magellan's cfg_blocks table as the source of truth
936    // This table is created by Magellan, but we include the CREATE here for:
937    // 1. Test database setup
938    // 2. Documentation of expected schema
939    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    // cfg_edges table is managed by Magellan v11+ with schema:
965    //   (id, function_id, source_idx, target_idx, edge_type)
966    // Mirage computes edges in memory via build_edges_from_terminators().
967
968    // Create cfg_paths table
969    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    // Create cfg_path_elements table
993    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    // Create cfg_dominators table
1010    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    // Create cfg_post_dominators table
1023    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    // Initialize mirage_meta
1036    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/// Database status information
1047#[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    /// Get database statistics
1060    ///
1061    /// Note: cfg_edges count is included for backward compatibility but edges
1062    /// are now computed in memory from terminator data, not stored.
1063    #[cfg(feature = "backend-sqlite")]
1064    pub fn status(&self) -> Result<DatabaseStatus> {
1065        // Check if we have a connection (SQLite backend) or need to use storage backend (geometric)
1066        match self.conn.as_ref() {
1067            Some(conn) => {
1068                // SQLite backend - use direct SQL queries
1069                let cfg_blocks: i64 = conn
1070                    .query_row("SELECT COUNT(*) FROM cfg_blocks", [], |row| row.get(0))
1071                    .unwrap_or(0);
1072
1073                // Edges are now computed in memory from terminator data (per RESEARCH.md Pattern 2)
1074                // This count is kept for backward compatibility but will always be 0 for new databases
1075                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                // No connection - use storage backend instead (geometric)
1115                self.status_via_storage()
1116            }
1117        }
1118    }
1119
1120    /// Helper function to get status via storage backend (for non-SQLite backends)
1121    #[cfg(feature = "backend-sqlite")]
1122    fn status_via_storage(&self) -> Result<DatabaseStatus> {
1123        // For geometric backend, query via GeometricStorage
1124        #[cfg(feature = "backend-geometric")]
1125        {
1126            if let Backend::Geometric(ref geometric) = self.storage {
1127                // Get real stats from geometric backend
1128                let stats = geometric.get_stats()?;
1129                return Ok(DatabaseStatus {
1130                    cfg_blocks: stats.cfg_block_count as i64,
1131                    cfg_edges: 0,      // Edges computed in memory
1132                    cfg_paths: 0,      // Paths computed on-demand
1133                    cfg_dominators: 0, // Dominators computed on-demand
1134                    mirage_schema_version: MIRAGE_SCHEMA_VERSION,
1135                    magellan_schema_version: MIN_MAGELLAN_SCHEMA_VERSION,
1136                });
1137            }
1138        }
1139
1140        // Fallback for other backends
1141        #[allow(deprecated)]
1142        // cfg_edges field is kept for backward compatibility with existing databases
1143        // Edges are now computed in memory from terminator data (per commit 20a28d4)
1144        Ok(DatabaseStatus {
1145            cfg_blocks: 0,
1146            cfg_edges: 0, // Always 0 for new databases, kept for backward compatibility
1147            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    /// Get database statistics (geometric backend)
1155    ///
1156    /// Uses GeometricBackend methods to query symbol and CFG data.
1157    #[cfg(all(feature = "backend-geometric", not(feature = "backend-sqlite")))]
1158    pub fn status(&self) -> Result<DatabaseStatus> {
1159        // For geometric backend, we need to query through the storage
1160        // Since we don't have direct SQLite access, use the GeometricStorage methods
1161        let cfg_blocks: i64 = if let Backend::Geometric(ref geometric) = self.storage {
1162            // Geometric doesn't have a direct count method, but we can estimate
1163            // from symbol count or return 0 for now
1164            // TODO: Add proper CFG block counting for geometric backend
1165            0
1166        } else {
1167            0
1168        };
1169
1170        // Geometric backend doesn't have these tables
1171        let cfg_edges: i64 = 0;
1172        let cfg_paths: i64 = 0;
1173        let cfg_dominators: i64 = 0;
1174
1175        // Geometric uses a different versioning scheme
1176        // Return constants for compatibility
1177        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    /// Resolve a function name or ID to a function_id (backend-agnostic)
1192    ///
1193    /// This method works with both SQLite and geometric backends.
1194    ///
1195    /// # Arguments
1196    ///
1197    /// * `name_or_id` - Function name (string) or function_id (numeric string)
1198    ///
1199    /// # Returns
1200    ///
1201    /// * `Ok(i64)` - The function_id if found
1202    /// * `Err(...)` - Error if function not found or query fails
1203    ///
1204    /// # Examples
1205    ///
1206    /// ```no_run
1207    /// # use mirage_analyzer::storage::MirageDb;
1208    /// # fn main() -> anyhow::Result<()> {
1209    /// # let db = MirageDb::open("test.db")?;
1210    /// // Resolve by numeric ID
1211    /// let func_id = db.resolve_function_name("123")?;
1212    ///
1213    /// // Resolve by function name
1214    /// let func_id = db.resolve_function_name("my_function")?;
1215    /// # Ok(())
1216    /// # }
1217    /// ```
1218    #[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    /// Resolve a function name or ID to a function_id with optional file filter
1224    ///
1225    /// This method works with both SQLite and geometric backends.
1226    /// For SQLite backend: queries the graph_entities table
1227    /// For geometric backend: uses GraphBackend::get_node
1228    ///
1229    /// # Arguments
1230    ///
1231    /// * `name_or_id` - Function name (string) or function_id (numeric string)
1232    /// * `file_filter` - Optional file path to disambiguate functions with same name
1233    ///
1234    /// # Returns
1235    ///
1236    /// * `Ok(i64)` - The function_id if found
1237    /// * `Err(...)` - Error if function not found or query fails
1238    #[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        // Try to parse as numeric ID first
1245        if let Ok(id) = name_or_id.parse::<i64>() {
1246            return Ok(id);
1247        }
1248
1249        // Check if we have a SQLite connection (geometric backend has conn=None)
1250        if let Ok(conn) = self.conn() {
1251            resolve_function_name_sqlite(conn, name_or_id, file_filter)
1252        } else {
1253            // For geometric backend, use the storage backend directly
1254            #[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    /// Normalize path for deduplication purposes
1265    /// Converts paths to a canonical form for comparison
1266    #[cfg(feature = "backend-geometric")]
1267    fn normalize_path_for_dedup(path: &str) -> String {
1268        // Normalize backslashes to forward slashes first
1269        let path = path.replace('\\', "/");
1270        // Remove leading "./" if present
1271        let path = path.strip_prefix("./").unwrap_or(&path);
1272        // For deduplication, we want to compare relative paths consistently
1273        // If path starts with the project root pattern, extract just src/ portion
1274        if let Some(idx) = path.find("/src/") {
1275            // Extract from src/ onwards for consistent comparison
1276            path[idx + 1..].to_string()
1277        } else {
1278            path.to_string()
1279        }
1280    }
1281
1282    /// Resolve function name for geometric backend
1283    ///
1284    /// Accepts:
1285    /// - Numeric ID (e.g., "12345")
1286    /// - Full Qualified Name (FQN): magellan::/path/to/file.rs::FunctionName
1287    /// - Simple name (e.g., "FunctionName") - must be unique
1288    #[cfg(feature = "backend-geometric")]
1289    fn resolve_function_name_geometric(&self, name_or_id: &str) -> Result<i64> {
1290        // Try to parse as numeric ID first
1291        if let Ok(id) = name_or_id.parse::<i64>() {
1292            // Verify the ID exists
1293            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        // Check if this is a Full Qualified Name (FQN) format: magellan::/path/to/file.rs::FunctionName
1306        if let Some(fqn_data) = Self::parse_fqn(name_or_id) {
1307            return self.resolve_function_by_fqn(fqn_data);
1308        }
1309
1310        // Use simple name resolution via geometric storage
1311        if let Backend::Geometric(ref geometric) = self.storage {
1312            // Find symbols by name
1313            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            // Deduplicate by symbol ID - the ID is the unique primary key in the database.
1319            // This handles cases where the same symbol may be indexed multiple times with
1320            // identical (name, file_path, location) data but different internal records.
1321            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            // Check if all candidates are at the same location (duplicates) or genuinely different
1332            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                    // Genuinely ambiguous - different functions with same name
1345                    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                // All same location - pick the first one (they're duplicates)
1356            }
1357            Ok(unique_symbols[0].id as i64)
1358        } else {
1359            anyhow::bail!("Geometric backend not available")
1360        }
1361    }
1362
1363    /// Parse FQN format: magellan::/path/to/file.rs::Function symbol_name
1364    /// Returns (file_path, symbol_name) if valid FQN
1365    #[cfg(feature = "backend-geometric")]
1366    fn parse_fqn(name: &str) -> Option<(&str, &str)> {
1367        // FQN format: magellan::<file_path>::<Kind> <symbol_name>
1368        // Example: magellan::/home/user/src/main.rs::Function main
1369        if !name.starts_with("magellan::") {
1370            return None;
1371        }
1372
1373        // Strip the prefix
1374        let after_prefix = &name[10..]; // Skip "magellan::"
1375
1376        // Find the last :: separator
1377        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            // The name_part may include a kind prefix like "Function ", "Struct ", etc.
1382            // Strip the kind prefix to get the actual symbol name
1383            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    /// Resolve function by FQN (file path + symbol name)
1398    #[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            // Use the direct lookup method (handles deduplication internally)
1404            match geometric.find_symbol_id_by_name_and_path(symbol_name, file_path) {
1405                Some(id) => Ok(id as i64),
1406                None => {
1407                    // Not found or ambiguous - try to get more details for error message
1408                    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                        // Multiple matches - report ambiguity
1428                        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    /// Load a CFG from the database (backend-agnostic)
1443    ///
1444    /// For SQLite backend: uses SQL query on cfg_blocks table
1445    ///
1446    /// # Arguments
1447    ///
1448    /// * `function_id` - ID of the function to load CFG for
1449    ///
1450    /// # Returns
1451    ///
1452    /// * `Ok(Cfg)` - The reconstructed control flow graph
1453    /// * `Err(...)` - Error if query fails or CFG data is invalid
1454    ///
1455    /// # Examples
1456    ///
1457    /// ```no_run
1458    /// # use mirage_analyzer::storage::MirageDb;
1459    /// # fn main() -> anyhow::Result<()> {
1460    /// # let db = MirageDb::open("test.db")?;
1461    /// let cfg = db.load_cfg(123)?;
1462    /// # Ok(())
1463    /// # }
1464    /// ```
1465    #[cfg(feature = "backend-sqlite")]
1466    pub fn load_cfg(&self, function_id: i64) -> Result<crate::cfg::Cfg> {
1467        // Phase 069-02: Use storage backend instead of direct Connection
1468        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        // Get file_path for this function
1478        let file_path = self.get_function_file(function_id);
1479
1480        // Convert CfgBlockData to the tuple format expected by load_cfg_from_rows
1481        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, // id (use index as id)
1500                    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        // Query cfg_edges from SQLite connection if available (Magellan v11+)
1516        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    /// Get the function name for a given function_id (backend-agnostic)
1545    ///
1546    /// For SQLite backend: queries the graph_entities table
1547    /// For Geometric backend: uses GraphBackend::get_node
1548    ///
1549    /// # Arguments
1550    ///
1551    /// * `function_id` - ID of the function
1552    ///
1553    /// # Returns
1554    ///
1555    /// * `Some(name)` - The function name if found
1556    /// * `None` - Function not found
1557    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                // Return the name if this is a function
1564                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    /// Get the file path for a given function_id (backend-agnostic)
1575    ///
1576    /// This method works with both SQLite and geometric backends.
1577    /// For SQLite backend: queries the graph_entities table
1578    /// For geometric backend: uses GraphBackend::get_node
1579    ///
1580    /// # Arguments
1581    ///
1582    /// * `function_id` - ID of the function
1583    ///
1584    /// # Returns
1585    ///
1586    /// * `Some(file_path)` - The file path if found
1587    /// * `None` - File path not available
1588    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    /// Check if a function has CFG blocks (SQLite backend)
1597    ///
1598    /// For SQLite backend: queries the cfg_blocks table
1599    ///
1600    /// # Arguments
1601    ///
1602    /// * `function_id` - ID of the function to check
1603    ///
1604    /// # Returns
1605    ///
1606    /// * `true` - Function has CFG blocks
1607    /// * `false` - Function not indexed or no CFG blocks
1608    #[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    /// Get the function hash for path caching (SQLite backend)
1617    ///
1618    /// For SQLite backend: queries the cfg_blocks table
1619    ///
1620    /// # Arguments
1621    ///
1622    /// * `function_id` - ID of the function
1623    ///
1624    /// # Returns
1625    ///
1626    /// * `Some(hash)` - The function hash if available
1627    /// * `None` - Hash not available
1628    #[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/// Resolve a function name or ID to a function_id (SQLite backend)
1639///
1640/// This is a helper function for the SQLite backend. For backend-agnostic
1641/// resolution, use `MirageDb::resolve_function_name` which takes `&MirageDb`.
1642#[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    // First try to look up by symbol_id (hex hash like 7ca9eebfa98204a5)
1649    // Magellan stores symbol_id inside the data JSON column
1650    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    // Then try to look up by function name, optionally filtered by file
1671    let function_id: Option<i64> = if let Some(file_path) = file_filter {
1672        // With file filter - use LIKE to match partial paths
1673        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        // Without file filter - original behavior
1691        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/// Load CFG blocks from SQLite backend
1714///
1715/// This helper function loads CFG blocks using SQL queries from the cfg_blocks table.
1716#[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    // Query file_path for this function from graph_entities
1721    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    // Query all blocks for this function from Magellan's cfg_blocks table
1733    // Magellan schema v7+ uses: kind (not block_kind), terminator as TEXT, and line/col columns
1734    // Also includes 4D spatial coordinates: coord_x (dominator depth), coord_y (loop nesting), coord_z (branch distance)
1735    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)?,  // id (database primary key)
1763                row.get(1)?,  // kind (Magellan's column name)
1764                row.get(2)?,  // terminator (plain TEXT, not JSON)
1765                row.get(3)?,  // byte_start
1766                row.get(4)?,  // byte_end
1767                row.get(5)?,  // start_line
1768                row.get(6)?,  // start_col
1769                row.get(7)?,  // end_line
1770                row.get(8)?,  // end_col
1771                row.get(9)?,  // coord_x (dominator depth)
1772                row.get(10)?, // coord_y (loop nesting depth)
1773                row.get(11)?, // coord_z (branch distance)
1774            ))
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    // Query cfg_edges for this function (Magellan v11+)
1788    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
1807/// Common CFG loading logic used by the SQLite backend
1808///
1809/// This function takes pre-fetched block rows and builds the CFG structure.
1810fn 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    // Build mapping from database block ID to graph node index
1834    let mut db_id_to_node: HashMap<i64, usize> = HashMap::new();
1835    let mut graph = Cfg::new();
1836
1837    // Add each block to the graph
1838    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        // Parse Magellan's block kind to Mirage's BlockKind
1857        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                // Fallback: treat unknown kinds as Normal
1863                // Magellan may have additional kinds we don't explicitly handle
1864                BlockKind::Normal
1865            }
1866        };
1867
1868        // Parse Magellan's terminator string to Mirage's Terminator enum
1869        let terminator = match terminator_str.as_deref() {
1870            Some("fallthrough") => Terminator::Goto { target: 0 }, // target will be resolved from edges
1871            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        // Construct source_location from Magellan's line/column data
1888        let source_location = if let Some(ref path) = file_path {
1889            // Use line/column data directly (Magellan v7+)
1890            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![], // Empty for now - future enhancement
1916            terminator,
1917            source_location,
1918            // 4D spatial coordinates from Magellan's cfg_blocks table
1919            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    // Build mapping from vector index to graph node index (for cfg_edges)
1929    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    // Use cfg_edges from Magellan if available, otherwise fall back to terminators
1937    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
1948/// Resolve a function name or ID to a function_id (backend-agnostic)
1949///
1950/// This is the main entry point for resolving function names. It works with both
1951/// SQLite and geometric backends.
1952///
1953/// # Arguments
1954///
1955/// * `db` - Database reference (works with both backends)
1956/// * `name_or_id` - Function name (string) or function_id (numeric string)
1957///
1958/// # Returns
1959///
1960/// * `Ok(i64)` - The function_id if found
1961/// * `Err(...)` - Error if function not found or query fails
1962///
1963/// # Examples
1964///
1965/// ```no_run
1966/// # use mirage_analyzer::storage::{resolve_function_name, MirageDb};
1967/// # fn main() -> anyhow::Result<()> {
1968/// # let db = MirageDb::open("test.db")?;
1969/// // Resolve by numeric ID
1970/// let func_id = resolve_function_name(&db, "123")?;
1971///
1972/// // Resolve by function name
1973/// let func_id = resolve_function_name(&db, "my_function")?;
1974/// # Ok(())
1975/// # }
1976/// ```
1977pub fn resolve_function_name(db: &MirageDb, name_or_id: &str) -> Result<i64> {
1978    db.resolve_function_name(name_or_id)
1979}
1980
1981/// Resolve a function name or ID to a function_id with optional file filter
1982///
1983/// This is a helper function that delegates to `MirageDb::resolve_function_name_with_file`.
1984/// Use this for a backend-agnostic API.
1985///
1986/// # Arguments
1987///
1988/// * `db` - Database reference (works with both backends)
1989/// * `name_or_id` - Function name or numeric ID string
1990/// * `file_filter` - Optional file path to disambiguate functions with same name
1991///
1992/// # Returns
1993///
1994/// * `Ok(i64)` - The function_id if found
1995/// * `Err(...)` - Error if function not found or query fails
1996///
1997/// # Examples
1998///
1999/// ```no_run
2000/// # use mirage_analyzer::storage::{resolve_function_name_with_file, MirageDb};
2001/// # fn main() -> anyhow::Result<()> {
2002/// # let db = MirageDb::open("test.db")?;
2003/// // Resolve with file filter to disambiguate
2004/// let func_id = resolve_function_name_with_file(&db, "process", Some("src/lib.rs"))?;
2005/// # Ok(())
2006/// # }
2007/// ```
2008pub 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
2016/// Get the function name for a given function_id (backend-agnostic)
2017///
2018/// This is the main entry point for getting function names. It works with both
2019/// SQLite and geometric backends.
2020///
2021/// # Arguments
2022///
2023/// * `db` - Database reference (works with both backends)
2024/// * `function_id` - ID of the function
2025///
2026/// # Returns
2027///
2028/// * `Some(name)` - The function name if found
2029/// * `None` - Function not found
2030///
2031/// # Examples
2032///
2033/// ```no_run
2034/// # use mirage_analyzer::storage::{get_function_name_db, MirageDb};
2035/// # fn main() -> anyhow::Result<()> {
2036/// # let db = MirageDb::open("test.db")?;
2037/// if let Some(name) = get_function_name_db(&db, 123) {
2038///     println!("Function: {}", name);
2039/// }
2040/// # Ok(())
2041/// # }
2042/// ```
2043pub fn get_function_name_db(db: &MirageDb, function_id: i64) -> Option<String> {
2044    db.get_function_name(function_id)
2045}
2046
2047/// Get the file path for a given function_id (backend-agnostic)
2048///
2049/// This is the main entry point for getting function file paths. It works with both
2050/// SQLite and geometric backends.
2051///
2052/// # Arguments
2053///
2054/// * `db` - Database reference (works with both backends)
2055/// * `function_id` - ID of the function
2056///
2057/// # Returns
2058///
2059/// * `Some(file_path)` - The file path if found
2060/// * `None` - File path not available
2061///
2062/// # Examples
2063///
2064/// ```no_run
2065/// # use mirage_analyzer::storage::{get_function_file_db, MirageDb};
2066/// # fn main() -> anyhow::Result<()> {
2067/// # let db = MirageDb::open("test.db")?;
2068/// if let Some(path) = get_function_file_db(&db, 123) {
2069///     println!("File: {}", path);
2070/// }
2071/// # Ok(())
2072/// # }
2073/// ```
2074pub fn get_function_file_db(db: &MirageDb, function_id: i64) -> Option<String> {
2075    db.get_function_file(function_id)
2076}
2077
2078/// Get the function hash for path caching (backend-agnostic)
2079///
2080/// This is the main entry point for getting function hashes. It works with both
2081/// SQLite and geometric backends.
2082///
2083/// For SQLite backend: returns the stored hash if available
2084/// For geometric backend: always returns None (Magellan manages its own caching)
2085///
2086/// # Arguments
2087///
2088/// * `db` - Database reference (works with both backends)
2089/// * `function_id` - ID of the function
2090///
2091/// # Returns
2092///
2093/// * `Some(hash)` - The function hash if available (SQLite only)
2094/// * `None` - Hash not available or geometric backend
2095///
2096/// # Examples
2097///
2098/// ```no_run
2099/// # use mirage_analyzer::storage::{get_function_hash_db, MirageDb};
2100/// # fn main() -> anyhow::Result<()> {
2101/// # let db = MirageDb::open("test.db")?;
2102/// if let Some(hash) = get_function_hash_db(&db, 123) {
2103///     println!("Hash: {}", hash);
2104/// }
2105/// # Ok(())
2106/// # }
2107/// ```
2108pub fn get_function_hash_db(db: &MirageDb, function_id: i64) -> Option<String> {
2109    db.get_function_hash(function_id)
2110}
2111
2112/// Resolve a function name or ID to a function_id (SQLite backend, legacy)
2113///
2114/// This is the legacy function that takes a direct Connection reference.
2115/// For new code supporting both backends, use `resolve_function_name` which takes `&MirageDb`.
2116#[cfg(feature = "backend-sqlite")]
2117pub fn resolve_function_name_with_conn(conn: &Connection, name_or_id: &str) -> Result<i64> {
2118    // Try to parse as numeric ID first
2119    if let Ok(id) = name_or_id.parse::<i64>() {
2120        return Ok(id);
2121    }
2122
2123    // Query by function name
2124    // Note: Magellan v7 stores functions as kind='Symbol' with data.kind='Function'
2125    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
2147/// Load a CFG from the database for a given function_id (backend-agnostic)
2148///
2149/// This is the main entry point for loading CFGs. It works with SQLite and geometric backends.
2150///
2151/// # Arguments
2152///
2153/// * `db` - Database reference (works with both backends)
2154/// * `function_id` - ID of the function to load CFG for
2155///
2156/// # Returns
2157///
2158/// * `Ok(Cfg)` - The reconstructed control flow graph
2159/// * `Err(...)` - Error if query fails or CFG data is invalid
2160///
2161/// # Examples
2162///
2163/// ```no_run
2164/// # use mirage_analyzer::storage::{load_cfg_from_db, MirageDb};
2165/// # fn main() -> anyhow::Result<()> {
2166/// # let db = MirageDb::open("test.db")?;
2167/// let cfg = load_cfg_from_db(&db, 123)?;
2168/// # Ok(())
2169/// # }
2170/// ```
2171///
2172/// # Notes
2173///
2174/// - For SQLite backend: uses SQL query on cfg_blocks table
2175/// - For geometric backend: uses Magellan's KV store via get_cfg_blocks_kv()
2176/// - Requires Magellan schema v7+ for cfg_blocks table
2177/// - Edges are constructed in memory from terminator data, not queried from cfg_edges table
2178pub fn load_cfg_from_db(db: &MirageDb, function_id: i64) -> Result<crate::cfg::Cfg> {
2179    db.load_cfg(function_id)
2180}
2181
2182/// Load a CFG from the database for a given function_id (SQLite backend)
2183///
2184/// This is the legacy function that takes a direct Connection reference.
2185/// For new code supporting both backends, use `load_cfg_from_db` which takes `&MirageDb`.
2186///
2187/// # Arguments
2188///
2189/// * `conn` - Database connection (SQLite only)
2190/// * `function_id` - ID of the function to load CFG for
2191///
2192/// # Returns
2193///
2194/// * `Ok(Cfg)` - The reconstructed control flow graph
2195/// * `Err(...)` - Error if query fails or CFG data is invalid
2196///
2197/// # Examples
2198///
2199/// ```no_run
2200/// # use mirage_analyzer::storage::load_cfg_from_db_with_conn;
2201/// # use rusqlite::Connection;
2202/// # fn main() -> anyhow::Result<()> {
2203/// # let conn = Connection::open_in_memory()?;
2204/// let cfg = load_cfg_from_db_with_conn(&conn, 123)?;
2205/// # Ok(())
2206/// # }
2207/// ```
2208///
2209/// # Notes
2210///
2211/// - This function only works with SQLite backend
2212/// - For backend-agnostic loading, use `load_cfg_from_db(&db, function_id)` instead
2213/// - Requires Magellan schema v7+ for cfg_blocks table
2214/// - Edges are constructed in memory from terminator data, not queried from cfg_edges table
2215#[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/// Store a CFG in the database for a given function
2221///
2222/// # Arguments
2223///
2224/// * `conn` - Database connection
2225/// * `function_id` - ID of the function in graph_entities
2226/// * `function_hash` - BLAKE3 hash of the function body for incremental updates
2227/// * `cfg` - The control flow graph to store
2228///
2229/// # Returns
2230///
2231/// * `Ok(())` - CFG stored successfully
2232/// * `Err(...)` - Error if storage fails
2233///
2234/// # Algorithm
2235///
2236/// 1. Begin IMMEDIATE transaction for atomicity
2237/// 2. Clear existing cfg_blocks for this function_id (incremental update)
2238/// 3. Insert each BasicBlock as a row in cfg_blocks:
2239///    - Serialize terminator as JSON string
2240///    - Store source location byte ranges if available
2241/// 4. Commit transaction
2242///
2243/// # Notes
2244///
2245/// - DEPRECATED: Magellan handles CFG storage via cfg_blocks. Edges are computed in memory.
2246/// - This function is kept for backward compatibility with existing tests.
2247/// - cfg_edges table is managed by Magellan v11+; Mirage does not create or query it.
2248/// - Uses BEGIN IMMEDIATE to acquire write lock early (prevents write conflicts)
2249/// - Existing blocks are cleared for incremental updates
2250/// - Block IDs are AUTOINCREMENT in the database
2251#[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, // Unused: Magellan manages its own caching
2256    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    // Clear existing blocks for this function (incremental update)
2264    // Note: cfg_edges is managed by Magellan v11+; Mirage does not maintain it.
2265    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    // Insert each block and collect database IDs
2272    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        // Convert terminator to Magellan's string format
2290        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        // Get location data from source_location
2302        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        // Convert BlockKind to Magellan's kind string
2322        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    // Note: cfg_edges table is managed by Magellan v11+.
2350    // Mirage computes edges in memory from terminator data and does not insert into this table.
2351
2352    conn.execute("COMMIT", [])
2353        .context("Failed to commit transaction")?;
2354
2355    Ok(())
2356}
2357
2358/// Check if a function is already indexed in the database
2359///
2360/// # Arguments
2361///
2362/// * `conn` - Database connection
2363/// * `function_id` - ID of the function to check
2364///
2365/// # Returns
2366///
2367/// * `true` - Function has CFG blocks stored
2368/// * `false` - Function not indexed
2369pub 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
2381/// Get the stored hash for a function
2382///
2383/// # Arguments
2384///
2385/// * `conn` - Database connection
2386/// * `function_id` - ID of the function
2387///
2388/// # Returns
2389///
2390/// * `Some(hash)` - The stored BLAKE3 hash if function exists
2391/// * `None` - Function not found or no hash stored
2392///
2393/// # Note
2394///
2395/// Magellan's cfg_blocks table doesn't store function_hash, so this function
2396/// always returns None when using Magellan's schema. The hash functionality
2397/// is only available when using Mirage's legacy schema.
2398pub fn get_function_hash(conn: &Connection, function_id: i64) -> Option<String> {
2399    // Try Magellan v8+ cfg_hash column first
2400    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    // Fallback: use symbol_id from graph_entities (Magellan v7 schema)
2415    // This provides a stable identifier for caching
2416    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
2427/// Compare two function hashes and return true if they differ
2428///
2429/// Used by the index command to decide whether to skip a function.
2430///
2431/// # Arguments
2432///
2433/// * `conn` - Database connection
2434/// * `function_id` - ID of the function
2435/// * `new_hash` - New hash to compare against stored hash
2436///
2437/// # Returns
2438///
2439/// * `Ok(true)` - Hashes differ or function is new (needs re-indexing)
2440/// * `Ok(false)` - Hashes match (can skip)
2441/// * `Err(...)` - Database query error
2442///
2443/// # Note
2444///
2445/// Compare stored cfg_hash against new hash to detect function changes.
2446/// Returns true if hashes differ or no hash is found (indicating re-indexing needed).
2447pub 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), // New function or no hash stored, always index
2459    }
2460}
2461
2462/// Compute the set of functions that need re-indexing based on git changes
2463///
2464/// This uses git diff to find changed Rust files, then queries the database
2465/// for functions defined in those files.
2466///
2467/// # Arguments
2468///
2469/// * `conn` - Database connection
2470/// * `project_path` - Path to the project being indexed
2471///
2472/// # Returns
2473///
2474/// Set of function names that should be re-indexed
2475///
2476/// # Notes
2477///
2478/// - Uses `git diff --name-only HEAD` to detect changed files
2479/// - Only considers .rs files
2480/// - Returns functions from changed files based on graph_entities table
2481pub 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    // Use git to find changed Rust files
2491    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        // Collect .rs files that changed
2499        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        // Build a list of file paths for the SQL query
2507        for file in changed_rs_files {
2508            // Normalize the file path relative to project root
2509            let normalized_path = if file.starts_with('/') {
2510                file.trim_start_matches('/')
2511            } else {
2512                file
2513            };
2514
2515            // Query for functions in this file
2516            // Note: file_path in graph_entities may be relative or absolute,
2517            // so we check both patterns
2518            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
2549/// Get the file containing a function
2550///
2551/// # Arguments
2552///
2553/// * `conn` - Database connection
2554/// * `function_name` - Name of the function
2555///
2556/// # Returns
2557///
2558/// * `Ok(Some(file_path))` - The file path if found
2559/// * `Ok(None)` - Function not found
2560/// * `Err(...)` - Database error
2561pub 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
2573/// Get the function name for a given block ID
2574///
2575/// # Arguments
2576///
2577/// * `conn` - Database connection
2578/// * `function_id` - ID of the function
2579///
2580/// # Returns
2581///
2582/// * `Some(name)` - The function name if found
2583/// * `None` - Function not found
2584pub 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
2595/// Get path elements (blocks in order) for a given path_id
2596///
2597/// # Arguments
2598///
2599/// * `conn` - Database connection
2600/// * `path_id` - The path ID to query
2601///
2602/// # Returns
2603///
2604/// * `Ok(Vec<BlockId>)` - Ordered list of block IDs in the path
2605/// * `Err(...)` - Error if query fails or path not found
2606pub 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
2628/// Compute path impact from the database
2629///
2630/// This loads the path's blocks from the database and computes
2631/// the impact by aggregating reachable blocks from each path block.
2632///
2633/// # Arguments
2634///
2635/// * `conn` - Database connection
2636/// * `path_id` - The path ID to analyze
2637/// * `cfg` - The control flow graph
2638/// * `max_depth` - Maximum depth for impact analysis
2639///
2640/// # Returns
2641///
2642/// * `Ok(PathImpact)` - Aggregated impact data
2643/// * `Err(...)` - Error if path not found or computation fails
2644pub 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
2658/// Create a minimal Magellan-compatible database at the given path
2659///
2660/// This creates a new database with the minimal Magellan schema required
2661/// for Mirage to store CFG data. For a full Magellan database, users
2662/// should run `magellan watch` on their project.
2663///
2664/// # Arguments
2665///
2666/// * `path` - Path where the database should be created
2667///
2668/// # Returns
2669///
2670/// * `Ok(())` - Database created successfully
2671/// * `Err(...)` - Error if creation fails
2672pub fn create_minimal_database<P: AsRef<Path>>(path: P) -> Result<()> {
2673    let path = path.as_ref();
2674
2675    // Don't overwrite existing database
2676    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    // Create Magellan meta table
2683    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    // Create graph_entities table (minimal schema)
2695    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    // Create indexes for graph_entities
2708    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    // Initialize Magellan meta
2721    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 Mirage schema
2729    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        // First create the Magellan tables (simplified)
2743        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        // Insert Magellan meta
2767        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 Mirage schema
2774        create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
2775
2776        // Verify tables exist
2777        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); // cfg_blocks, cfg_paths, cfg_path_elements, cfg_dominators (cfg_edges is managed by Magellan v11+)
2786    }
2787
2788    #[test]
2789    fn test_migrate_schema_from_version_0() {
2790        let mut conn = Connection::open_in_memory().unwrap();
2791
2792        // Create Magellan tables
2793        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 Mirage schema at version 0 (no mirage_meta yet)
2823        create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
2824
2825        // Verify version is 1
2826        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        // Create Magellan tables
2842        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 Mirage schema
2872        create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
2873
2874        // Migration should be a no-op - already at current version
2875        migrate_schema(&mut conn).unwrap();
2876
2877        // Verify version is still 1
2878        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        // Enable foreign key enforcement (SQLite requires this)
2894        conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
2895
2896        // Create Magellan tables
2897        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 Mirage schema
2927        create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
2928
2929        // Insert a graph entity (function)
2930        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        // Attempt to insert cfg_blocks with invalid function_id (should fail)
2939        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        // Should fail with foreign key constraint error
2947        assert!(
2948            invalid_result.is_err(),
2949            "Insert with invalid function_id should fail"
2950        );
2951
2952        // Insert valid cfg_blocks with correct function_id (should succeed)
2953        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        // Verify the insert worked
2966        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        // Create Magellan tables
2984        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 Mirage schema
3014        create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
3015
3016        // Insert a function entity
3017        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        // Create a simple test CFG
3026        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 the CFG
3049        store_cfg(&mut conn, function_id, "test_hash_123", &cfg).unwrap();
3050
3051        // Verify blocks were stored
3052        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        // Note: cfg_edges table is managed by Magellan v11+; Mirage does not create or query it.
3063        // Edges are computed in memory from terminator data via build_edges_from_terminators().
3064
3065        // Note: function_hash is not stored in Magellan's schema, so we skip that check
3066        // The hash functionality is only available with Mirage's legacy schema
3067
3068        // Verify function_exists
3069        assert!(function_exists(&conn, function_id));
3070        assert!(!function_exists(&conn, 9999));
3071
3072        // Load and verify the CFG
3073        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        // Create Magellan tables
3086        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        // Create initial CFG with 2 blocks
3126        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        // Create updated CFG with 3 blocks
3158        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        // Should have 3 blocks now (old ones cleared)
3197        assert_eq!(block_count_v3, 3);
3198
3199        // Note: function_hash is not stored in Magellan's schema
3200        // Hash verification is skipped for Magellan v7+ schema
3201    }
3202
3203    // Helper function to create a test database with Magellan + Mirage schema
3204    //
3205    // Creates a Magellan v7-compatible database with Mirage extensions.
3206    // The cfg_blocks table uses Magellan v7 schema:
3207    // - kind: TEXT (lowercase: "entry", "block", "return", "if", etc.)
3208    // - terminator: TEXT (lowercase: "fallthrough", "conditional", "return", etc.)
3209    // - Includes line/column fields for source locations
3210    fn create_test_db_with_schema() -> Connection {
3211        let mut conn = Connection::open_in_memory().unwrap();
3212
3213        // Create Magellan v7 tables
3214        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        // Insert Magellan v7 meta
3238        conn.execute(
3239            "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3240             VALUES (1, ?, ?, ?)",
3241            params![7, 3, 0],  // Magellan v7, sqlitegraph v3
3242        ).unwrap();
3243
3244        // Create Magellan's cfg_blocks table (v7 schema)
3245        // This is the authoritative table for CFG data in Magellan v7+
3246        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        // Create graph_edges for CFG edges
3268        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 Mirage schema (mirage_meta and additional tables)
3281        create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
3282
3283        // Enable foreign key enforcement for tests
3284        conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
3285
3286        conn
3287    }
3288
3289    // Tests for resolve_function_name and load_cfg_from_db (09-02)
3290
3291    #[test]
3292    fn test_resolve_function_by_id() {
3293        let conn = create_test_db_with_schema();
3294
3295        // Insert a test function
3296        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        // Resolve by numeric ID
3304        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        // Insert a test function with Magellan v7 schema
3313        // Magellan v7 stores functions as kind='Symbol' with data.kind='Function'
3314        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        // Resolve by name
3327        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        // Try to resolve a non-existent function
3336        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        // Insert a test function
3351        conn.execute(
3352            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3353            params!("function", "func123", "test.rs", "{}"),
3354        )
3355        .unwrap();
3356
3357        // Resolve by numeric string "123" - should parse as ID, not name
3358        let result = resolve_function_name_with_conn(&conn, "123").unwrap();
3359        assert_eq!(result, 123);
3360
3361        // Now insert a function with ID 456
3362        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        // If we query "456" it should try to parse as numeric ID
3370        // Since we just inserted and got some ID, let's verify numeric parsing works
3371        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        // Try to load CFG for non-existent function
3380        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        // Insert a test function
3397        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        // Create a block with NULL terminator (should default to Unreachable)
3405        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        // Load the CFG - should handle NULL terminator gracefully
3414        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        // Insert a test function
3428        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        // Create blocks with different edge types
3436        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        // Load the CFG - edges are now built from terminator data, not cfg_edges table
3473        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        // Verify edge types are built from terminators:
3479        // Block 0 (conditional) -> Block 1 (TrueBranch), Block 2 (FalseBranch)
3480        // Block 1 (fallthrough) -> Block 2 (Fallthrough)
3481        // Block 2 (call) -> Block 3 (Call)
3482        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        // Insert a test function
3499        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        // Get function name
3507        let name = get_function_name(&conn, function_id);
3508        assert_eq!(name, Some("my_test_func".to_string()));
3509
3510        // Non-existent function
3511        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        // Insert a test function and path
3520        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        // Insert a path
3528        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        // Insert path elements
3535        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        // Get path elements
3552        let blocks = get_path_elements(&conn, "test_path_abc123").unwrap();
3553        assert_eq!(blocks, vec![0, 1, 2]);
3554
3555        // Non-existent path
3556        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        // Insert a test function
3567        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        // Create a simple CFG: 0 -> 1 -> 2 -> 3
3575        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        // Insert a path: 0 -> 1 -> 3
3613        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        // Compute impact
3636        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        // Block 2 is not in the path but is reachable from block 1
3641        assert!(impact.unique_blocks_affected.contains(&2));
3642    }
3643
3644    // Graceful degradation tests for missing CFG data
3645
3646    #[test]
3647    fn test_load_cfg_missing_cfg_blocks_table() {
3648        let conn = Connection::open_in_memory().unwrap();
3649
3650        // Create Magellan tables WITHOUT cfg_blocks
3651        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],  // Magellan v6 (too old, no cfg_blocks)
3678        ).unwrap();
3679
3680        // Insert a function
3681        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        // Try to load CFG - should fail with helpful error
3689        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        // Error should mention the problem (either cfg_blocks or prepare failed)
3694        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        // Try to load CFG for non-existent function
3706        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        // Insert a function but no CFG blocks
3727        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        // Try to load CFG - should fail with helpful error
3735        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        // Try to resolve a non-existent function
3756        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        // Create Magellan v6 database (too old)
3772        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)",  // Magellan v6 < required v7
3786            [],
3787        ).unwrap();
3788
3789        // Create cfg_blocks table (but wrong schema version)
3790        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        // Try to open via MirageDb - should fail with schema version error
3809        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    // Backend detection tests (13-01)
3858
3859    #[test]
3860    fn test_backend_detect_sqlite_header() {
3861        use std::io::Write;
3862
3863        // Create a temporary file with SQLite header
3864        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        // Empty file has less than 16 bytes
3890        let temp_file = tempfile::NamedTempFile::new().unwrap();
3891        // File is empty (0 bytes)
3892
3893        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        // File with less than 16 bytes but not SQLite
3906        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(); // Only 7 bytes
3909        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}