Skip to main content

mirage_analyzer/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
5// - cfg_edges: Control flow between blocks
6// - cfg_paths: Enumerated execution paths
7// - cfg_path_elements: Blocks in each path
8// - cfg_dominators: Dominance relationships
9// - cfg_post_dominators: Reverse dominance
10
11pub mod paths;
12
13// Backend-agnostic storage trait and implementations (Phase 069-01)
14#[cfg(feature = "backend-sqlite")]
15pub mod sqlite_backend;
16#[cfg(feature = "backend-native-v3")]
17pub mod kv_backend;
18#[cfg(feature = "backend-geometric")]
19pub mod geometric;
20
21// Also support the aliased feature names for convenience
22#[cfg(all(feature = "sqlite", not(feature = "backend-sqlite")))]
23pub mod sqlite_backend;
24#[cfg(all(feature = "native-v3", not(feature = "backend-native-v3")))]
25pub mod kv_backend;
26#[cfg(all(feature = "geometric", not(feature = "backend-geometric")))]
27pub mod geometric;
28
29use anyhow::{Context, Result};
30use rusqlite::{Connection, OptionalExtension, params};
31use std::path::Path;
32
33// GraphBackend imports for dual backend support
34use sqlitegraph::{GraphBackend, GraphConfig, SnapshotId, open_graph};
35
36// Note: We avoid importing BackendRouter here to prevent circular dependency
37// with crate::router which uses crate::storage. Instead, we use fully qualified
38// paths where needed.
39
40// Note: Magellan 2.4.x doesn't provide get_cfg_blocks_kv
41// CFG data should be retrieved through alternative methods for native-v3
42// #[cfg(feature = "backend-native-v3")]
43// use magellan::graph::get_cfg_blocks_kv;
44
45// Backend implementations (Phase 069-01)
46#[cfg(feature = "backend-sqlite")]
47pub use sqlite_backend::SqliteStorage;
48#[cfg(feature = "backend-native-v3")]
49pub use kv_backend::KvStorage;
50#[cfg(feature = "backend-geometric")]
51pub use geometric::GeometricStorage;
52
53// Re-export path caching functions
54// Note: Some exports like PathCache, store_paths, etc. are not currently used
55// but are kept for potential future use and API completeness
56#[allow(unused_imports)]
57pub use paths::{
58    PathCache,
59    store_paths,
60    get_cached_paths,
61    invalidate_function_paths,
62    update_function_paths_if_changed,
63};
64
65// ============================================================================
66// Backend-Agnostic Storage Trait (Phase 069-01)
67// ============================================================================
68
69/// Backend-agnostic storage trait for CFG data
70///
71/// This trait abstracts over SQLite and Native-V3 storage backends,
72/// enabling runtime backend detection and zero breaking changes.
73///
74/// # Design
75///
76/// - Follows llmgrep's Backend pattern for consistency
77/// - All methods take `&self` (not `&mut self`) to enable shared access
78/// - Errors are returned as `anyhow::Error` for flexibility
79///
80/// # Examples
81///
82/// ```ignore
83/// # use mirage_analyzer::storage::{StorageTrait, Backend};
84/// # fn main() -> anyhow::Result<()> {
85/// // Auto-detect and open backend
86/// let backend = Backend::detect_and_open("/path/to/db")?;
87///
88/// // Query CFG blocks (works with both SQLite and native-v3)
89/// let blocks = backend.get_cfg_blocks(123)?;
90/// # Ok(())
91/// # }
92/// ```
93pub trait StorageTrait {
94    /// Get CFG blocks for a function
95    ///
96    /// Returns all basic blocks for the given function_id.
97    /// For SQLite: queries cfg_blocks table
98    /// For native-v3: uses KV store with key "cfg:func:{function_id}"
99    ///
100    /// # Arguments
101    ///
102    /// * `function_id` - ID of the function in graph_entities
103    ///
104    /// # Returns
105    ///
106    /// * `Ok(Vec<CfgBlockData>)` - Vector of CFG block data
107    /// * `Err(...)` - Error if query fails
108    fn get_cfg_blocks(&self, function_id: i64) -> Result<Vec<CfgBlockData>>;
109
110    /// Get entity by ID
111    ///
112    /// Returns the entity with the given ID from graph_entities.
113    ///
114    /// # Arguments
115    ///
116    /// * `entity_id` - ID of the entity
117    ///
118    /// # Returns
119    ///
120    /// * `Some(GraphEntity)` - Entity if found
121    /// * `None` - Entity not found
122    fn get_entity(&self, entity_id: i64) -> Option<sqlitegraph::GraphEntity>;
123
124    /// Get cached paths for a function (optional)
125    ///
126    /// Returns cached enumerated paths if available.
127    /// Default implementation returns None (no caching).
128    ///
129    /// # Arguments
130    ///
131    /// * `function_id` - ID of the function
132    ///
133    /// # Returns
134    ///
135    /// * `Ok(Some(paths))` - Cached paths if available
136    /// * `Ok(None)` - No cached paths
137    /// * `Err(...)` - Error if query fails
138    fn get_cached_paths(&self, _function_id: i64) -> Result<Option<Vec<crate::cfg::Path>>> {
139        Ok(None) // Default: no caching
140    }
141}
142
143/// CFG block data (backend-agnostic representation)
144///
145/// This struct represents the data returned by `StorageTrait::get_cfg_blocks`.
146/// It is a simplified version of Magellan's CfgBlock that contains only the
147/// fields needed by Mirage for CFG analysis.
148#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
149pub struct CfgBlockData {
150    /// Block ID (from cfg_blocks table)
151    pub id: i64,
152    /// Block kind (entry, conditional, loop, match, return, etc.)
153    pub kind: String,
154    /// Terminator kind (how control exits this block)
155    pub terminator: String,
156    /// Byte offset where block starts
157    pub byte_start: u64,
158    /// Byte offset where block ends
159    pub byte_end: u64,
160    /// Line where block starts (1-indexed)
161    pub start_line: u64,
162    /// Column where block starts (0-indexed)
163    pub start_col: u64,
164    /// Line where block ends (1-indexed)
165    pub end_line: u64,
166    /// Column where block ends (0-indexed)
167    pub end_col: u64,
168}
169
170/// Storage backend enum (Phase 069-01)
171///
172/// This enum wraps SqliteStorage, KvStorage, or GeometricStorage and delegates
173/// StorageTrait methods to the appropriate implementation.
174///
175/// Follows llmgrep's Backend pattern for consistency across tools.
176#[derive(Debug)]
177#[allow(clippy::large_enum_variant)] // KvStorage may contain CodeGraph
178pub enum Backend {
179    /// SQLite storage backend (traditional, always available)
180    #[cfg(feature = "backend-sqlite")]
181    Sqlite(SqliteStorage),
182    /// Native-V3 storage backend (high-performance, requires native-v3 feature)
183    #[cfg(feature = "backend-native-v3")]
184    NativeV3(KvStorage),
185    /// Geometric storage backend for .geo files (Magellan 3.0+)
186    #[cfg(feature = "backend-geometric")]
187    Geometric(GeometricStorage),
188}
189
190impl Backend {
191    /// Detect backend format from database file and open appropriate backend
192    ///
193    /// Uses file extension and magellan's detection for consistent backend detection.
194    ///
195    /// # Arguments
196    ///
197    /// * `db_path` - Path to the database file
198    ///
199    /// # Returns
200    ///
201    /// * `Ok(Backend)` - Appropriate backend variant
202    /// * `Err(...)` - Error if detection or opening fails
203    ///
204    /// # Examples
205    ///
206    /// ```ignore
207    /// # use mirage_analyzer::storage::Backend;
208    /// # fn main() -> anyhow::Result<()> {
209    /// let backend = Backend::detect_and_open("/path/to/codegraph.db")?;
210    /// # Ok(())
211    /// # }
212    /// ```
213    pub fn detect_and_open(db_path: &Path) -> Result<Self> {
214        use magellan::migrate_backend_cmd::detect_backend_format;
215
216        // Check for .geo extension first (Magellan 3.0+ geometric backend)
217        #[cfg(feature = "backend-geometric")]
218        let is_geo = db_path.extension().and_then(|e| e.to_str()) == Some("geo");
219
220        #[cfg(feature = "backend-geometric")]
221        {
222            if is_geo {
223                return GeometricStorage::open(db_path).map(Backend::Geometric);
224            }
225        }
226
227        // For non-.geo files, use magellan's detection (SQLite vs Native-V3)
228        let sqlite_detected = detect_backend_format(db_path).is_ok();
229
230        #[cfg(feature = "backend-sqlite")]
231        {
232            if sqlite_detected {
233                return SqliteStorage::open(db_path).map(Backend::Sqlite);
234            } else {
235                return Err(anyhow::anyhow!("Native-V3 backend detected but SQLite feature enabled"));
236            }
237        }
238
239        #[cfg(feature = "backend-native-v3")]
240        {
241            if sqlite_detected {
242                return Err(anyhow::anyhow!("SQLite backend detected but Native-V3 feature enabled"));
243            } else {
244                return KvStorage::open(db_path).map(Backend::NativeV3);
245            }
246        }
247
248        #[cfg(not(any(feature = "backend-sqlite", feature = "backend-native-v3", feature = "backend-geometric")))]
249        {
250        Err(anyhow::anyhow!("No storage backend feature enabled"))
251        }
252    }
253
254    /// Check if this is a Geometric backend
255    pub fn is_geometric(&self) -> bool {
256        match self {
257            #[cfg(feature = "backend-geometric")]
258            Backend::Geometric(_) => true,
259            _ => false,
260        }
261    }
262
263    /// Check if this is a SQLite backend
264    pub fn is_sqlite(&self) -> bool {
265        match self {
266            #[cfg(feature = "backend-sqlite")]
267            Backend::Sqlite(_) => true,
268            #[cfg(not(feature = "backend-sqlite"))]
269            _ => false,
270        }
271    }
272
273    /// Check if this is a Native-V3 backend
274    pub fn is_native_v3(&self) -> bool {
275        match self {
276            #[cfg(feature = "backend-native-v3")]
277            Backend::NativeV3(_) => true,
278            _ => false,
279        }
280    }
281
282    /// Delegate get_cfg_blocks to inner backend
283    pub fn get_cfg_blocks(&self, function_id: i64) -> Result<Vec<CfgBlockData>> {
284        match self {
285            #[cfg(feature = "backend-sqlite")]
286            Backend::Sqlite(s) => s.get_cfg_blocks(function_id),
287            #[cfg(feature = "backend-native-v3")]
288            Backend::NativeV3(k) => k.get_cfg_blocks(function_id),
289            #[cfg(feature = "backend-geometric")]
290            Backend::Geometric(g) => g.get_cfg_blocks(function_id),
291            #[allow(unreachable_patterns)]
292            _ => Err(anyhow::anyhow!("No storage backend available")),
293        }
294    }
295
296    /// Delegate get_entity to inner backend
297    pub fn get_entity(&self, entity_id: i64) -> Option<sqlitegraph::GraphEntity> {
298        match self {
299            #[cfg(feature = "backend-sqlite")]
300            Backend::Sqlite(s) => s.get_entity(entity_id),
301            #[cfg(feature = "backend-native-v3")]
302            Backend::NativeV3(k) => k.get_entity(entity_id),
303            #[cfg(feature = "backend-geometric")]
304            Backend::Geometric(g) => g.get_entity(entity_id),
305            #[allow(unreachable_patterns)]
306            _ => None,
307        }
308    }
309
310    /// Delegate get_cached_paths to inner backend
311    pub fn get_cached_paths(&self, function_id: i64) -> Result<Option<Vec<crate::cfg::Path>>> {
312        match self {
313            #[cfg(feature = "backend-sqlite")]
314            Backend::Sqlite(s) => s.get_cached_paths(function_id),
315            #[cfg(feature = "backend-native-v3")]
316            Backend::NativeV3(k) => k.get_cached_paths(function_id),
317            #[cfg(feature = "backend-geometric")]
318            Backend::Geometric(g) => g.get_cached_paths(function_id),
319            #[allow(unreachable_patterns)]
320            _ => Err(anyhow::anyhow!("No storage backend available")),
321        }
322    }
323}
324
325// Implement StorageTrait for Backend (delegates to inner storage)
326impl StorageTrait for Backend {
327    fn get_cfg_blocks(&self, function_id: i64) -> Result<Vec<CfgBlockData>> {
328        self.get_cfg_blocks(function_id)
329    }
330
331    fn get_entity(&self, entity_id: i64) -> Option<sqlitegraph::GraphEntity> {
332        self.get_entity(entity_id)
333    }
334
335    fn get_cached_paths(&self, function_id: i64) -> Result<Option<Vec<crate::cfg::Path>>> {
336        self.get_cached_paths(function_id)
337    }
338}
339
340/// Database backend format detected in a graph database file.
341///
342/// This is the legacy format detection enum. For new code, use the
343/// `Backend` enum (with StorageTrait) which provides full backend abstraction.
344#[derive(Debug, Clone, Copy, PartialEq, Eq)]
345pub enum BackendFormat {
346    /// SQLite-based backend (default, backward compatible)
347    SQLite,
348    /// Native-v3 backend (requires native-v3 feature)
349    NativeV3,
350    /// Geometric backend (.geo files, Magellan 3.0+)
351    Geometric,
352    /// Unknown or unrecognized format
353    Unknown,
354}
355
356impl BackendFormat {
357    /// Detect which backend format a database file uses.
358    ///
359    /// Checks the file header to determine if the database is SQLite or native-v3 format.
360    /// Returns Unknown if the file doesn't exist or has an unrecognized header.
361    ///
362    /// **Deprecated:** Use `Backend::detect_and_open()` for new code which provides
363    /// full backend abstraction, not just format detection.
364    pub fn detect(path: &Path) -> Result<Self> {
365        if !path.exists() {
366            return Ok(BackendFormat::Unknown);
367        }
368
369        // Check for .geo extension first (Magellan 3.0+ geometric backend)
370        if path.extension().and_then(|e| e.to_str()) == Some("geo") {
371            return Ok(BackendFormat::Geometric);
372        }
373
374        let mut file = std::fs::File::open(path)?;
375        let mut header = [0u8; 16];
376        let bytes_read = std::io::Read::read(&mut file, &mut header)?;
377
378        if bytes_read < header.len() {
379            return Ok(BackendFormat::Unknown);
380        }
381
382        // SQLite databases start with "SQLite format 3"
383        Ok(if &header[..15] == b"SQLite format 3" {
384            BackendFormat::SQLite
385        } else {
386            // If it exists but isn't SQLite, assume native-v3
387            BackendFormat::NativeV3
388        })
389    }
390}
391
392/// Mirage schema version
393pub const MIRAGE_SCHEMA_VERSION: i32 = 1;
394
395/// Minimum Magellan schema version we require
396/// Magellan v7+ includes cfg_blocks table with AST-based CFG
397pub const MIN_MAGELLAN_SCHEMA_VERSION: i32 = 7;
398
399/// Magellan schema version used in tests (for consistency)
400pub const TEST_MAGELLAN_SCHEMA_VERSION: i32 = MIN_MAGELLAN_SCHEMA_VERSION;
401
402/// Alias for backward compatibility (same as TEST_MAGELLAN_SCHEMA_VERSION)
403pub const REQUIRED_MAGELLAN_SCHEMA_VERSION: i32 = TEST_MAGELLAN_SCHEMA_VERSION;
404
405/// SQLiteGraph schema version we require
406pub const REQUIRED_SQLITEGRAPH_SCHEMA_VERSION: i32 = 3;
407
408/// Database connection wrapper
409///
410/// Uses Backend enum for CFG queries (Phase 069-02) and GraphBackend for entity queries.
411/// This dual-backend approach allows gradual migration from direct Connection usage.
412pub struct MirageDb {
413    /// Storage backend for CFG queries (Phase 069-02)
414    /// Wraps either SqliteStorage or KvStorage for backend-agnostic CFG access.
415    storage: Backend,
416
417    /// Backend-agnostic graph interface for entity queries
418    /// Used for entity_ids(), get_node(), kv_get() and other GraphBackend operations.
419    graph_backend: Box<dyn GraphBackend>,
420
421    /// Snapshot ID for consistent reads
422    snapshot_id: SnapshotId,
423
424    // SQLite-specific connection (only available with sqlite feature)
425    // DEPRECATED: Use storage field instead for new code
426    #[cfg(feature = "backend-sqlite")]
427    conn: Option<Connection>,
428}
429
430impl std::fmt::Debug for MirageDb {
431    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
432        f.debug_struct("MirageDb")
433            .field("snapshot_id", &self.snapshot_id)
434            .field("storage", &self.storage)
435            .field("graph_backend", &"<GraphBackend>")
436            .finish()
437    }
438}
439
440impl MirageDb {
441    /// Open database at the given path
442    ///
443    /// This can open:
444    /// - A Mirage database (with mirage_meta table)
445    /// - A Magellan database (extends it with Mirage tables)
446    ///
447    /// Phase 069-02: Uses Backend::detect_and_open() for CFG queries
448    /// and open_graph() for entity queries (GraphBackend).
449    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
450        let path = path.as_ref();
451        if !path.exists() {
452            anyhow::bail!("Database not found: {}", path.display());
453        }
454
455        // Phase 069-02: Use Backend::detect_and_open() for storage layer
456        let storage = Backend::detect_and_open(path)
457            .context("Failed to open storage backend")?;
458
459        // Detect backend format from file header for GraphBackend creation
460        let detected_backend = BackendFormat::detect(path)
461            .context("Failed to detect backend format")?;
462
463        // Validate that detected backend matches compile-time feature
464        #[cfg(feature = "backend-sqlite")]
465        {
466            if detected_backend == BackendFormat::NativeV3 {
467                anyhow::bail!(
468                    "Database file '{}' uses native-v3 format, but this binary was built \
469                     with SQLite backend. Rebuild with: cargo build --release --no-default-features --features native-v3",
470                    path.display()
471                );
472            }
473        }
474
475        #[cfg(feature = "backend-native-v3")]
476        {
477            if detected_backend == BackendFormat::SQLite {
478                anyhow::bail!(
479                    "Database file '{}' uses SQLite format, but this binary was built \
480                     with native-v3 backend. Rebuild with: cargo build --release",
481                    path.display()
482                );
483            }
484        }
485
486        // Handle geometric backend specially - it doesn't use GraphBackend
487        #[cfg(feature = "backend-geometric")]
488        if detected_backend == BackendFormat::Geometric {
489            let snapshot_id = SnapshotId::current();
490            
491            // For geometric backend, we don't have a traditional GraphBackend
492            // Instead, we use the GeometricStorage directly for all operations
493            // Create a stub GraphBackend that returns errors for unsupported operations
494            let graph_backend = create_geometric_stub_backend();
495
496            #[cfg(feature = "backend-sqlite")]
497            let conn = None;
498
499            return Ok(Self {
500                storage,
501                graph_backend,
502                snapshot_id,
503                #[cfg(feature = "backend-sqlite")]
504                conn,
505            });
506        }
507
508        // Select appropriate GraphConfig based on detected backend
509        let cfg = match detected_backend {
510            BackendFormat::SQLite => GraphConfig::sqlite(),
511            BackendFormat::NativeV3 => GraphConfig::native(),
512            BackendFormat::Geometric => {
513                // This case is handled above, but needed for match completeness
514                GraphConfig::native()
515            }
516            BackendFormat::Unknown => {
517                anyhow::bail!(
518                    "Unknown database format: {}. Cannot determine backend.",
519                    path.display()
520                );
521            }
522        };
523
524        // Use open_graph factory to create GraphBackend for entity queries
525        let graph_backend = open_graph(path, &cfg)
526            .context("Failed to open graph database")?;
527
528        let snapshot_id = SnapshotId::current();
529
530        // For SQLite backend, open Connection and validate schema
531        #[cfg(feature = "backend-sqlite")]
532        let conn = {
533            let mut conn = Connection::open(path)
534                .context("Failed to open SQLite connection")?;
535            Self::validate_schema_sqlite(&mut conn, path)?;
536            Some(conn)
537        };
538
539        // For native-v3 backend, schema validation will be added in future plans
540        #[cfg(feature = "backend-native-v3")]
541        {
542            // TODO: Add native-v3 schema validation via GraphBackend methods
543            // For now, we trust the native-v3 backend has the required tables
544        }
545
546        Ok(Self {
547            storage,
548            graph_backend,
549            snapshot_id,
550            #[cfg(feature = "backend-sqlite")]
551            conn,
552        })
553    }
554
555    /// Validate database schema for SQLite backend
556    #[cfg(feature = "backend-sqlite")]
557    fn validate_schema_sqlite(conn: &mut Connection, _path: &Path) -> Result<()> {
558        // Check if mirage_meta table exists
559        let mirage_meta_exists: bool = conn.query_row(
560            "SELECT 1 FROM sqlite_master WHERE type='table' AND name='mirage_meta'",
561            [],
562            |row| row.get(0),
563        ).optional()?.unwrap_or(0) == 1;
564
565        // Get Mirage schema version (0 if table doesn't exist)
566        let mirage_version: i32 = if mirage_meta_exists {
567            conn.query_row(
568                "SELECT mirage_schema_version FROM mirage_meta WHERE id = 1",
569                [],
570                |row| row.get(0),
571            ).optional()?.flatten().unwrap_or(0)
572        } else {
573            0
574        };
575
576        if mirage_version > MIRAGE_SCHEMA_VERSION {
577            anyhow::bail!(
578                "Database schema version {} is newer than supported version {}.
579                 Please update Mirage.",
580                mirage_version, MIRAGE_SCHEMA_VERSION
581            );
582        }
583
584        // Check Magellan schema compatibility
585        let magellan_version: i32 = conn.query_row(
586            "SELECT magellan_schema_version FROM magellan_meta WHERE id = 1",
587            [],
588            |row| row.get(0),
589        ).optional()?.flatten().unwrap_or(0);
590
591        if magellan_version < MIN_MAGELLAN_SCHEMA_VERSION {
592            anyhow::bail!(
593                "Magellan schema version {} is too old (minimum {}). \
594                 Please update Magellan and run 'magellan watch' to rebuild CFGs.",
595                magellan_version, MIN_MAGELLAN_SCHEMA_VERSION
596            );
597        }
598
599        // Check for cfg_blocks table existence (Magellan v7+)
600        let cfg_blocks_exists: bool = conn.query_row(
601            "SELECT 1 FROM sqlite_master WHERE type='table' AND name='cfg_blocks'",
602            [],
603            |row| row.get(0),
604        ).optional()?.unwrap_or(0) == 1;
605
606        if !cfg_blocks_exists {
607            anyhow::bail!(
608                "CFG blocks table not found. Magellan schema v7+ required. \
609                 Run 'magellan watch' to build CFGs."
610            );
611        }
612
613        // If mirage_meta doesn't exist, this is a pure Magellan database.
614        // Initialize Mirage tables to extend it.
615        if !mirage_meta_exists {
616            create_schema(conn, magellan_version)?;
617        } else if mirage_version < MIRAGE_SCHEMA_VERSION {
618            migrate_schema(conn)?;
619        }
620
621        Ok(())
622    }
623
624    /// Get a reference to the underlying Connection (SQLite backend only)
625    ///
626    /// Phase 069-02: DEPRECATED - Use storage() for CFG queries, backend() for entity queries.
627    /// For SQLite backend, returns the Connection directly.
628    /// For native-v3 backend, returns an error.
629    #[cfg(feature = "backend-sqlite")]
630    pub fn conn(&self) -> Result<&Connection, anyhow::Error> {
631        self.conn.as_ref().ok_or_else(|| {
632            anyhow::anyhow!(
633                "Direct Connection access deprecated. Use storage() for CFG queries or backend() for entity queries."
634            )
635        })
636    }
637
638    /// Get a mutable reference to the underlying Connection (SQLite backend only)
639    ///
640    /// Phase 069-02: DEPRECATED - Use storage() for CFG queries, backend() for entity queries.
641    /// For SQLite backend, returns the Connection directly.
642    /// For native-v3 backend, returns an error.
643    #[cfg(feature = "backend-sqlite")]
644    pub fn conn_mut(&mut self) -> Result<&mut Connection, anyhow::Error> {
645        self.conn.as_mut().ok_or_else(|| {
646            anyhow::anyhow!(
647                "Direct Connection access deprecated. Use storage() for CFG queries or backend() for entity queries."
648            )
649        })
650    }
651
652    /// Get a reference to the underlying Connection (native-v3 backend)
653    ///
654    /// Phase 069-02: DEPRECATED - Use storage() for CFG queries, backend() for entity queries.
655    /// For native-v3 backend, this always returns an error since Connection
656    /// is only available with SQLite backend.
657    #[cfg(feature = "backend-native-v3")]
658    pub fn conn(&self) -> Result<&Connection, anyhow::Error> {
659        Err(anyhow::anyhow!(
660            "Direct Connection access deprecated. Use storage() for CFG queries or backend() for entity queries."
661        ))
662    }
663
664    /// Get a mutable reference to the underlying Connection (native-v3 backend)
665    ///
666    /// Phase 069-02: DEPRECATED - Use storage() for CFG queries, backend() for entity queries.
667    /// For native-v3 backend, this always returns an error since Connection
668    /// is only available with SQLite backend.
669    #[cfg(feature = "backend-native-v3")]
670    pub fn conn_mut(&mut self) -> Result<&mut Connection, anyhow::Error> {
671        Err(anyhow::anyhow!(
672            "Direct Connection access deprecated. Use storage() for CFG queries or backend() for entity queries."
673        ))
674    }
675
676    /// Get a reference to the storage backend for CFG queries
677    ///
678    /// Phase 069-02: Use this to access CFG-specific storage operations
679    /// like get_cfg_blocks(), get_entity(), and get_cached_paths().
680    ///
681    /// This is the preferred way to access CFG data in new code.
682    pub fn storage(&self) -> &Backend {
683        &self.storage
684    }
685
686    /// Get a reference to the backend-agnostic GraphBackend interface
687    ///
688    /// Use this for entity queries (entity_ids, get_node, kv_get, etc.).
689    /// Phase 069-02: This now returns the GraphBackend used for entity queries,
690    /// while storage() provides the Backend enum for CFG queries.
691    pub fn backend(&self) -> &dyn GraphBackend {
692        self.graph_backend.as_ref()
693    }
694
695    /// Check if the database backend is SQLite
696    ///
697    /// This is useful for runtime checks when certain features
698    /// are only available with specific backends (e.g., path caching).
699    #[cfg(feature = "backend-sqlite")]
700    pub fn is_sqlite(&self) -> bool {
701        self.conn.is_some()
702    }
703
704    /// Check if the database backend is SQLite
705    ///
706    /// For native-v3, this always returns false.
707    #[cfg(feature = "backend-native-v3")]
708    pub fn is_sqlite(&self) -> bool {
709        false
710    }
711}
712
713/// Create a stub GraphBackend for geometric backend
714/// 
715/// Geometric backend doesn't use sqlitegraph's GraphBackend trait.
716/// Instead, it provides its own query methods directly via GeometricBackend.
717/// This stub is used to satisfy the MirageDb struct's graph_backend field.
718/// 
719/// Any code that tries to use GraphBackend methods on a geometric database
720/// will get appropriate errors directing them to use the geometric-specific
721/// methods instead.
722#[cfg(feature = "backend-geometric")]
723fn create_geometric_stub_backend() -> Box<dyn GraphBackend> {
724    use sqlitegraph::{GraphBackend, GraphEntity, SqliteGraphError, SnapshotId};
725    use sqlitegraph::backend::{NodeSpec, EdgeSpec, NeighborQuery, BackendDirection};
726    use sqlitegraph::pattern::{PatternQuery, PatternMatch};
727    use sqlitegraph::multi_hop::ChainStep;
728    
729    /// Stub GraphBackend implementation for geometric backend
730    /// All methods return errors since geometric uses its own API
731    struct GeometricStubBackend;
732    
733    impl GraphBackend for GeometricStubBackend {
734        fn insert_node(&self, _node: NodeSpec) -> Result<i64, SqliteGraphError> {
735            Err(SqliteGraphError::unsupported(
736                "GraphBackend operations not supported for geometric backend. Use GeometricBackend methods directly."
737            ))
738        }
739        
740        fn insert_edge(&self, _edge: EdgeSpec) -> Result<i64, SqliteGraphError> {
741            Err(SqliteGraphError::unsupported(
742                "GraphBackend operations not supported for geometric backend. Use GeometricBackend methods directly."
743            ))
744        }
745        
746        fn update_node(&self, _node_id: i64, _node: NodeSpec) -> Result<i64, SqliteGraphError> {
747            Err(SqliteGraphError::unsupported(
748                "GraphBackend operations not supported for geometric backend. Use GeometricBackend methods directly."
749            ))
750        }
751        
752        fn delete_entity(&self, _id: i64) -> Result<(), SqliteGraphError> {
753            Err(SqliteGraphError::unsupported(
754                "GraphBackend operations not supported for geometric backend. Use GeometricBackend methods directly."
755            ))
756        }
757        
758        fn entity_ids(&self) -> Result<Vec<i64>, SqliteGraphError> {
759            // Return empty list - geometric doesn't use entity_ids
760            Ok(vec![])
761        }
762        
763        fn get_node(&self, _snapshot_id: SnapshotId, _id: i64) -> Result<GraphEntity, SqliteGraphError> {
764            Err(SqliteGraphError::unsupported(
765                "GraphBackend operations not supported for geometric backend. Use GeometricBackend methods directly."
766            ))
767        }
768        
769        fn neighbors(
770            &self,
771            _snapshot_id: SnapshotId,
772            _node: i64,
773            _query: NeighborQuery,
774        ) -> Result<Vec<i64>, SqliteGraphError> {
775            // Return empty list - geometric doesn't use GraphBackend neighbors
776            Ok(vec![])
777        }
778        
779        fn bfs(
780            &self,
781            _snapshot_id: SnapshotId,
782            _start: i64,
783            _depth: u32,
784        ) -> Result<Vec<i64>, SqliteGraphError> {
785            // Return empty list - geometric has its own pathfinding
786            Ok(vec![])
787        }
788        
789        fn shortest_path(
790            &self,
791            _snapshot_id: SnapshotId,
792            _start: i64,
793            _end: i64,
794        ) -> Result<Option<Vec<i64>>, SqliteGraphError> {
795            Ok(None)
796        }
797        
798        fn node_degree(
799            &self,
800            _snapshot_id: SnapshotId,
801            _node: i64,
802        ) -> Result<(usize, usize), SqliteGraphError> {
803            Ok((0, 0))
804        }
805        
806        fn k_hop(
807            &self,
808            _snapshot_id: SnapshotId,
809            _start: i64,
810            _depth: u32,
811            _direction: BackendDirection,
812        ) -> Result<Vec<i64>, SqliteGraphError> {
813            Ok(vec![])
814        }
815        
816        fn k_hop_filtered(
817            &self,
818            _snapshot_id: SnapshotId,
819            _start: i64,
820            _depth: u32,
821            _direction: BackendDirection,
822            _allowed_edge_types: &[&str],
823        ) -> Result<Vec<i64>, SqliteGraphError> {
824            Ok(vec![])
825        }
826        
827        fn chain_query(
828            &self,
829            _snapshot_id: SnapshotId,
830            _start: i64,
831            _chain: &[ChainStep],
832        ) -> Result<Vec<i64>, SqliteGraphError> {
833            Ok(vec![])
834        }
835        
836        fn pattern_search(
837            &self,
838            _snapshot_id: SnapshotId,
839            _start: i64,
840            _pattern: &PatternQuery,
841        ) -> Result<Vec<PatternMatch>, SqliteGraphError> {
842            Ok(vec![])
843        }
844        
845        fn checkpoint(&self) -> Result<(), SqliteGraphError> {
846            Ok(())
847        }
848        
849        fn flush(&self) -> Result<(), SqliteGraphError> {
850            Ok(())
851        }
852        
853        fn backup(&self, _backup_dir: &std::path::Path) -> Result<sqlitegraph::backend::BackupResult, SqliteGraphError> {
854            Err(SqliteGraphError::unsupported(
855                "Backup not supported for geometric backend"
856            ))
857        }
858        
859        fn snapshot_export(
860            &self,
861            _export_dir: &std::path::Path,
862        ) -> Result<sqlitegraph::backend::SnapshotMetadata, SqliteGraphError> {
863            Err(SqliteGraphError::unsupported(
864                "Snapshot export not supported for geometric backend"
865            ))
866        }
867        
868        fn snapshot_import(
869            &self,
870            _import_dir: &std::path::Path,
871        ) -> Result<sqlitegraph::backend::ImportMetadata, SqliteGraphError> {
872            Err(SqliteGraphError::unsupported(
873                "Snapshot import not supported for geometric backend"
874            ))
875        }
876        
877        fn query_nodes_by_kind(
878            &self,
879            _snapshot_id: SnapshotId,
880            _kind: &str,
881        ) -> Result<Vec<i64>, SqliteGraphError> {
882            Ok(vec![])
883        }
884        
885        fn query_nodes_by_name_pattern(
886            &self,
887            _snapshot_id: SnapshotId,
888            _pattern: &str,
889        ) -> Result<Vec<i64>, SqliteGraphError> {
890            Ok(vec![])
891        }
892    }
893    
894    Box::new(GeometricStubBackend)
895}
896
897/// A schema migration
898struct Migration {
899    version: i32,
900    description: &'static str,
901    up: fn(&mut Connection) -> Result<()>,
902}
903
904/// Get all registered migrations
905fn migrations() -> Vec<Migration> {
906    // No migrations yet - framework is ready for future schema changes
907    vec![]
908}
909
910/// Run schema migrations to bring database up to current version
911pub fn migrate_schema(conn: &mut Connection) -> Result<()> {
912    let current_version: i32 = conn.query_row(
913        "SELECT mirage_schema_version FROM mirage_meta WHERE id = 1",
914        [],
915        |row| row.get(0),
916    ).unwrap_or(0);
917
918    if current_version >= MIRAGE_SCHEMA_VERSION {
919        // Already at or above current version
920        return Ok(());
921    }
922
923    // Get migrations that need to run
924    let pending: Vec<_> = migrations()
925        .into_iter()
926        .filter(|m| m.version > current_version && m.version <= MIRAGE_SCHEMA_VERSION)
927        .collect();
928
929    for migration in pending {
930        // Run migration
931        (migration.up)(conn)
932            .with_context(|| format!("Failed to run migration v{}: {}", migration.version, migration.description))?;
933
934        // Update version
935        conn.execute(
936            "UPDATE mirage_meta SET mirage_schema_version = ? WHERE id = 1",
937            params![migration.version],
938        )?;
939    }
940
941    // Ensure we're at the final version
942    if current_version < MIRAGE_SCHEMA_VERSION {
943        conn.execute(
944            "UPDATE mirage_meta SET mirage_schema_version = ? WHERE id = 1",
945            params![MIRAGE_SCHEMA_VERSION],
946        )?;
947    }
948
949    Ok(())
950}
951
952/// Create Mirage schema tables in an existing Magellan database
953///
954/// The magellan_schema_version parameter should be the actual version
955/// from the magellan_meta table, not MIN_MAGELLAN_SCHEMA_VERSION.
956pub fn create_schema(conn: &mut Connection, _magellan_schema_version: i32) -> Result<()> {
957    // Create mirage_meta table
958    conn.execute(
959        "CREATE TABLE IF NOT EXISTS mirage_meta (
960            id INTEGER PRIMARY KEY CHECK (id = 1),
961            mirage_schema_version INTEGER NOT NULL,
962            magellan_schema_version INTEGER NOT NULL,
963            compiler_version TEXT,
964            created_at INTEGER NOT NULL
965        )",
966        [],
967    )?;
968
969    // Create cfg_blocks table (Magellan v7+ schema)
970    // Note: Mirage now uses Magellan's cfg_blocks table as the source of truth
971    // This table is created by Magellan, but we include the CREATE here for:
972    // 1. Test database setup
973    // 2. Documentation of expected schema
974    conn.execute(
975        "CREATE TABLE IF NOT EXISTS cfg_blocks (
976            id INTEGER PRIMARY KEY AUTOINCREMENT,
977            function_id INTEGER NOT NULL,
978            kind TEXT NOT NULL,
979            terminator TEXT NOT NULL,
980            byte_start INTEGER,
981            byte_end INTEGER,
982            start_line INTEGER,
983            start_col INTEGER,
984            end_line INTEGER,
985            end_col INTEGER,
986            FOREIGN KEY (function_id) REFERENCES graph_entities(id)
987        )",
988        [],
989    )?;
990
991    conn.execute(
992        "CREATE INDEX IF NOT EXISTS idx_cfg_blocks_function ON cfg_blocks(function_id)",
993        [],
994    )?;
995
996    // Create cfg_edges table (kept for backward compatibility with tests and existing databases)
997    // Note: New code should compute edges in memory using build_edges_from_terminators()
998    conn.execute(
999        "CREATE TABLE IF NOT EXISTS cfg_edges (
1000            from_id INTEGER NOT NULL,
1001            to_id INTEGER NOT NULL,
1002            edge_type TEXT NOT NULL,
1003            PRIMARY KEY (from_id, to_id, edge_type),
1004            FOREIGN KEY (from_id) REFERENCES cfg_blocks(id),
1005            FOREIGN KEY (to_id) REFERENCES cfg_blocks(id)
1006        )",
1007        [],
1008    )?;
1009
1010    conn.execute("CREATE INDEX IF NOT EXISTS idx_cfg_edges_from ON cfg_edges(from_id)", [])?;
1011    conn.execute("CREATE INDEX IF NOT EXISTS idx_cfg_edges_to ON cfg_edges(to_id)", [])?;
1012
1013    // Create cfg_paths table
1014    conn.execute(
1015        "CREATE TABLE IF NOT EXISTS cfg_paths (
1016            path_id TEXT PRIMARY KEY,
1017            function_id INTEGER NOT NULL,
1018            path_kind TEXT NOT NULL,
1019            entry_block INTEGER NOT NULL,
1020            exit_block INTEGER NOT NULL,
1021            length INTEGER NOT NULL,
1022            created_at INTEGER NOT NULL,
1023            FOREIGN KEY (function_id) REFERENCES graph_entities(id)
1024        )",
1025        [],
1026    )?;
1027
1028    conn.execute("CREATE INDEX IF NOT EXISTS idx_cfg_paths_function ON cfg_paths(function_id)", [])?;
1029    conn.execute("CREATE INDEX IF NOT EXISTS idx_cfg_paths_kind ON cfg_paths(path_kind)", [])?;
1030
1031    // Create cfg_path_elements table
1032    conn.execute(
1033        "CREATE TABLE IF NOT EXISTS cfg_path_elements (
1034            path_id TEXT NOT NULL,
1035            sequence_order INTEGER NOT NULL,
1036            block_id INTEGER NOT NULL,
1037            PRIMARY KEY (path_id, sequence_order),
1038            FOREIGN KEY (path_id) REFERENCES cfg_paths(path_id)
1039        )",
1040        [],
1041    )?;
1042
1043    conn.execute("CREATE INDEX IF NOT EXISTS cfg_path_elements_block ON cfg_path_elements(block_id)", [])?;
1044
1045    // Create cfg_dominators table
1046    conn.execute(
1047        "CREATE TABLE IF NOT EXISTS cfg_dominators (
1048            block_id INTEGER NOT NULL,
1049            dominator_id INTEGER NOT NULL,
1050            is_strict BOOLEAN NOT NULL,
1051            PRIMARY KEY (block_id, dominator_id, is_strict),
1052            FOREIGN KEY (block_id) REFERENCES cfg_blocks(id),
1053            FOREIGN KEY (dominator_id) REFERENCES cfg_blocks(id)
1054        )",
1055        [],
1056    )?;
1057
1058    // Create cfg_post_dominators table
1059    conn.execute(
1060        "CREATE TABLE IF NOT EXISTS cfg_post_dominators (
1061            block_id INTEGER NOT NULL,
1062            post_dominator_id INTEGER NOT NULL,
1063            is_strict BOOLEAN NOT NULL,
1064            PRIMARY KEY (block_id, post_dominator_id, is_strict),
1065            FOREIGN KEY (block_id) REFERENCES cfg_blocks(id),
1066            FOREIGN KEY (post_dominator_id) REFERENCES cfg_blocks(id)
1067        )",
1068        [],
1069    )?;
1070
1071    // Initialize mirage_meta
1072    let now = chrono::Utc::now().timestamp();
1073    conn.execute(
1074        "INSERT OR REPLACE INTO mirage_meta (id, mirage_schema_version, magellan_schema_version, created_at)
1075         VALUES (1, ?, ?, ?)",
1076        params![MIRAGE_SCHEMA_VERSION, REQUIRED_MAGELLAN_SCHEMA_VERSION, now],
1077    )?;
1078
1079    Ok(())
1080}
1081
1082/// Database status information
1083#[derive(Debug, Clone, serde::Serialize)]
1084pub struct DatabaseStatus {
1085    pub cfg_blocks: i64,
1086    #[deprecated(note = "Edges are now computed in memory, not stored")]
1087    pub cfg_edges: i64,
1088    pub cfg_paths: i64,
1089    pub cfg_dominators: i64,
1090    pub mirage_schema_version: i32,
1091    pub magellan_schema_version: i32,
1092}
1093
1094impl MirageDb {
1095    /// Get database statistics
1096    ///
1097    /// Note: cfg_edges count is included for backward compatibility but edges
1098    /// are now computed in memory from terminator data, not stored.
1099    #[cfg(feature = "backend-sqlite")]
1100    pub fn status(&self) -> Result<DatabaseStatus> {
1101        // Check if we have a connection (SQLite backend) or need to use storage backend (geometric)
1102        match self.conn.as_ref() {
1103            Some(conn) => {
1104                // SQLite backend - use direct SQL queries
1105                let cfg_blocks: i64 = conn.query_row(
1106                    "SELECT COUNT(*) FROM cfg_blocks",
1107                    [],
1108                    |row| row.get(0),
1109                ).unwrap_or(0);
1110
1111                // Edges are now computed in memory from terminator data (per RESEARCH.md Pattern 2)
1112                // This count is kept for backward compatibility but will always be 0 for new databases
1113                let cfg_edges: i64 = conn.query_row(
1114                    "SELECT COUNT(*) FROM cfg_edges",
1115                    [],
1116                    |row| row.get(0),
1117                ).unwrap_or(0);
1118
1119                let cfg_paths: i64 = conn.query_row(
1120                    "SELECT COUNT(*) FROM cfg_paths",
1121                    [],
1122                    |row| row.get(0),
1123                ).unwrap_or(0);
1124
1125                let cfg_dominators: i64 = conn.query_row(
1126                    "SELECT COUNT(*) FROM cfg_dominators",
1127                    [],
1128                    |row| row.get(0),
1129                ).unwrap_or(0);
1130
1131                let mirage_schema_version: i32 = conn.query_row(
1132                    "SELECT mirage_schema_version FROM mirage_meta WHERE id = 1",
1133                    [],
1134                    |row| row.get(0),
1135                ).unwrap_or(0);
1136
1137                let magellan_schema_version: i32 = conn.query_row(
1138                    "SELECT magellan_schema_version FROM magellan_meta WHERE id = 1",
1139                    [],
1140                    |row| row.get(0),
1141                ).unwrap_or(0);
1142
1143                #[allow(deprecated)]
1144                Ok(DatabaseStatus {
1145                    cfg_blocks,
1146                    cfg_edges,
1147                    cfg_paths,
1148                    cfg_dominators,
1149                    mirage_schema_version,
1150                    magellan_schema_version,
1151                })
1152            }
1153            None => {
1154                // No connection - use storage backend instead (geometric or native-v3)
1155                self.status_via_storage()
1156            }
1157        }
1158    }
1159    
1160    /// Helper function to get status via storage backend (for non-SQLite backends)
1161    #[cfg(feature = "backend-sqlite")]
1162    fn status_via_storage(&self) -> Result<DatabaseStatus> {
1163        // For geometric backend, query via GeometricStorage
1164        #[cfg(feature = "backend-geometric")]
1165        {
1166            if let Backend::Geometric(ref geometric) = self.storage {
1167                // Get real stats from geometric backend
1168                let stats = geometric.get_stats()?;
1169                return Ok(DatabaseStatus {
1170                    cfg_blocks: stats.cfg_block_count as i64,
1171                    cfg_edges: 0, // Edges computed in memory
1172                    cfg_paths: 0, // Paths computed on-demand
1173                    cfg_dominators: 0, // Dominators computed on-demand
1174                    mirage_schema_version: MIRAGE_SCHEMA_VERSION,
1175                    magellan_schema_version: MIN_MAGELLAN_SCHEMA_VERSION,
1176                });
1177            }
1178        }
1179        
1180        // For native-v3 or other backends
1181        Ok(DatabaseStatus {
1182            cfg_blocks: 0,
1183            cfg_edges: 0,
1184            cfg_paths: 0,
1185            cfg_dominators: 0,
1186            mirage_schema_version: MIRAGE_SCHEMA_VERSION,
1187            magellan_schema_version: MIN_MAGELLAN_SCHEMA_VERSION,
1188        })
1189    }
1190
1191    /// Get database statistics (native-v3 backend)
1192    ///
1193    /// Uses GraphBackend methods to query entity and KV store data.
1194    #[cfg(feature = "backend-native-v3")]
1195    pub fn status(&self) -> Result<DatabaseStatus> {
1196        // For native-v3, CFG blocks are stored in the KV store
1197        // Counting them requires iterating through all function entities
1198        // and checking for CFG data in the KV store
1199        let _snapshot = SnapshotId::current();
1200        
1201        // TODO: Implement CFG block counting for native-v3 using kv_prefix_scan
1202        // For now, return 0 as the implementation requires sqlitegraph native-v3 APIs
1203        let cfg_blocks_count: i64 = 0;
1204
1205        // For native-v3, these counts are 0 as they're not stored in KV
1206        // cfg_paths and cfg_dominators are Mirage-specific tables not in native-v3
1207        let cfg_edges: i64 = 0; // Edges computed in memory
1208        let cfg_paths: i64 = 0; // Path caching not yet implemented for native-v3
1209        let cfg_dominators: i64 = 0; // Dominator caching not yet implemented for native-v3
1210
1211        // Schema versions: use constants (native-v3 doesn't have meta tables)
1212        // In the future, these could be stored in KV with well-known keys
1213        let mirage_schema_version = MIRAGE_SCHEMA_VERSION;
1214        let magellan_schema_version = MIN_MAGELLAN_SCHEMA_VERSION;
1215
1216        #[allow(deprecated)]
1217        Ok(DatabaseStatus {
1218            cfg_blocks: cfg_blocks_count,
1219            cfg_edges,
1220            cfg_paths,
1221            cfg_dominators,
1222            mirage_schema_version,
1223            magellan_schema_version,
1224        })
1225    }
1226
1227    /// Get database statistics (geometric backend)
1228    ///
1229    /// Uses GeometricBackend methods to query symbol and CFG data.
1230    #[cfg(all(feature = "backend-geometric", not(feature = "backend-sqlite"), not(feature = "backend-native-v3")))]
1231    pub fn status(&self) -> Result<DatabaseStatus> {
1232        // For geometric backend, we need to query through the storage
1233        // Since we don't have direct SQLite access, use the GeometricStorage methods
1234        let cfg_blocks: i64 = if let Backend::Geometric(ref geometric) = self.storage {
1235            // Geometric doesn't have a direct count method, but we can estimate
1236            // from symbol count or return 0 for now
1237            // TODO: Add proper CFG block counting for geometric backend
1238            0
1239        } else {
1240            0
1241        };
1242
1243        // Geometric backend doesn't have these tables
1244        let cfg_edges: i64 = 0;
1245        let cfg_paths: i64 = 0;
1246        let cfg_dominators: i64 = 0;
1247
1248        // Geometric uses a different versioning scheme
1249        // Return constants for compatibility
1250        let mirage_schema_version = MIRAGE_SCHEMA_VERSION;
1251        let magellan_schema_version = MIN_MAGELLAN_SCHEMA_VERSION;
1252
1253        #[allow(deprecated)]
1254        Ok(DatabaseStatus {
1255            cfg_blocks,
1256            cfg_edges,
1257            cfg_paths,
1258            cfg_dominators,
1259            mirage_schema_version,
1260            magellan_schema_version,
1261        })
1262    }
1263
1264    /// Resolve a function name or ID to a function_id (backend-agnostic)
1265    ///
1266    /// This method works with both SQLite and native-v3 backends.
1267    ///
1268    /// # Arguments
1269    ///
1270    /// * `name_or_id` - Function name (string) or function_id (numeric string)
1271    ///
1272    /// # Returns
1273    ///
1274    /// * `Ok(i64)` - The function_id if found
1275    /// * `Err(...)` - Error if function not found or query fails
1276    ///
1277    /// # Examples
1278    ///
1279    /// ```no_run
1280    /// # use mirage_analyzer::storage::MirageDb;
1281    /// # fn main() -> anyhow::Result<()> {
1282    /// # let db = MirageDb::open("test.db")?;
1283    /// // Resolve by numeric ID
1284    /// let func_id = db.resolve_function_name("123")?;
1285    ///
1286    /// // Resolve by function name
1287    /// let func_id = db.resolve_function_name("my_function")?;
1288    /// # Ok(())
1289    /// # }
1290    /// ```
1291    #[cfg(feature = "backend-sqlite")]
1292    pub fn resolve_function_name(&self, name_or_id: &str) -> Result<i64> {
1293        self.resolve_function_name_with_file(name_or_id, None)
1294    }
1295
1296    /// Resolve a function name or ID to a function_id with optional file filter
1297    ///
1298    /// This method works with both SQLite and geometric backends.
1299    /// For SQLite backend: queries the graph_entities table
1300    /// For geometric backend: uses GraphBackend::get_node
1301    ///
1302    /// # Arguments
1303    ///
1304    /// * `name_or_id` - Function name (string) or function_id (numeric string)
1305    /// * `file_filter` - Optional file path to disambiguate functions with same name
1306    ///
1307    /// # Returns
1308    ///
1309    /// * `Ok(i64)` - The function_id if found
1310    /// * `Err(...)` - Error if function not found or query fails
1311    #[cfg(feature = "backend-sqlite")]
1312    pub fn resolve_function_name_with_file(&self, name_or_id: &str, file_filter: Option<&str>) -> Result<i64> {
1313        // Try to parse as numeric ID first
1314        if let Ok(id) = name_or_id.parse::<i64>() {
1315            return Ok(id);
1316        }
1317
1318        // Check if we have a SQLite connection (geometric backend has conn=None)
1319        if let Ok(conn) = self.conn() {
1320            resolve_function_name_sqlite(conn, name_or_id, file_filter)
1321        } else {
1322            // For geometric backend, use the storage backend directly
1323            #[cfg(feature = "backend-geometric")]
1324            {
1325                if let Backend::Geometric(ref geometric) = self.storage {
1326                    return self.resolve_function_name_geometric(name_or_id);
1327                }
1328            }
1329            anyhow::bail!("No database connection available for function resolution")
1330        }
1331    }
1332
1333    /// Normalize path for deduplication purposes
1334    /// Converts paths to a canonical form for comparison
1335    #[cfg(feature = "backend-geometric")]
1336    fn normalize_path_for_dedup(path: &str) -> String {
1337        // Normalize backslashes to forward slashes first
1338        let path = path.replace('\\', "/");
1339        // Remove leading "./" if present
1340        let path = path.strip_prefix("./").unwrap_or(&path);
1341        // For deduplication, we want to compare relative paths consistently
1342        // If path starts with the project root pattern, extract just src/ portion
1343        if let Some(idx) = path.find("/src/") {
1344            // Extract from src/ onwards for consistent comparison
1345            path[idx + 1..].to_string()
1346        } else {
1347            path.to_string()
1348        }
1349    }
1350    
1351    /// Resolve function name for geometric backend
1352    ///
1353    /// Accepts:
1354    /// - Numeric ID (e.g., "12345")
1355    /// - Full Qualified Name (FQN): magellan::/path/to/file.rs::FunctionName
1356    /// - Simple name (e.g., "FunctionName") - must be unique
1357    #[cfg(feature = "backend-geometric")]
1358    fn resolve_function_name_geometric(&self, name_or_id: &str) -> Result<i64> {
1359        // Try to parse as numeric ID first
1360        if let Ok(id) = name_or_id.parse::<i64>() {
1361            // Verify the ID exists
1362            if let Backend::Geometric(ref geometric) = self.storage {
1363                if geometric.inner().find_symbol_by_id_info(id as u64).is_some() {
1364                    return Ok(id);
1365                }
1366            }
1367            anyhow::bail!("Function with ID '{}' not found", id);
1368        }
1369        
1370        // Check if this is a Full Qualified Name (FQN) format: magellan::/path/to/file.rs::FunctionName
1371        if let Some(fqn_data) = Self::parse_fqn(name_or_id) {
1372            return self.resolve_function_by_fqn(fqn_data);
1373        }
1374        
1375        // Use simple name resolution via geometric storage
1376        if let Backend::Geometric(ref geometric) = self.storage {
1377            // Find symbols by name
1378            let all_symbols = geometric.find_symbols_by_name(name_or_id);
1379            if all_symbols.is_empty() {
1380                anyhow::bail!("Function '{}' not found", name_or_id);
1381            }
1382            
1383            // Deduplicate by symbol ID - the ID is the unique primary key in the database.
1384            // This handles cases where the same symbol may be indexed multiple times with
1385            // identical (name, file_path, location) data but different internal records.
1386            let mut unique_symbols: Vec<magellan::graph::geometric_backend::SymbolInfo> = Vec::new();
1387            let mut seen_ids: std::collections::HashSet<u64> = std::collections::HashSet::new();
1388            
1389            for sym in all_symbols {
1390                if seen_ids.insert(sym.id) {
1391                    unique_symbols.push(sym);
1392                }
1393            }
1394            
1395            // Check if all candidates are at the same location (duplicates) or genuinely different
1396            if unique_symbols.len() > 1 {
1397                let first = &unique_symbols[0];
1398                let first_path_normalized = Self::normalize_path_for_dedup(&first.file_path);
1399                let all_same_location = unique_symbols.iter().all(|sym| {
1400                    let sym_path_normalized = Self::normalize_path_for_dedup(&sym.file_path);
1401                    sym.name == first.name 
1402                        && sym_path_normalized == first_path_normalized
1403                        && sym.start_line == first.start_line 
1404                        && sym.start_col == first.start_col
1405                });
1406                
1407                if !all_same_location {
1408                    // Genuinely ambiguous - different functions with same name
1409                    anyhow::bail!(
1410                        "Ambiguous function reference to '{}': {} unique candidates found\n\nCandidates:\n{}\n\nUse full qualified name: magellan::/path/to/file.rs::{}",
1411                        name_or_id,
1412                        unique_symbols.len(),
1413                        unique_symbols.iter().map(|s| {
1414                            format!("  - {} ({}:{}:{})", s.name, s.file_path, s.start_line, s.start_col)
1415                        }).collect::<Vec<_>>().join("\n"),
1416                        name_or_id
1417                    );
1418                }
1419                // All same location - pick the first one (they're duplicates)
1420            }
1421            Ok(unique_symbols[0].id as i64)
1422        } else {
1423            anyhow::bail!("Geometric backend not available")
1424        }
1425    }
1426    
1427    /// Parse FQN format: magellan::/path/to/file.rs::Function symbol_name
1428    /// Returns (file_path, symbol_name) if valid FQN
1429    #[cfg(feature = "backend-geometric")]
1430    fn parse_fqn(name: &str) -> Option<(&str, &str)> {
1431        // FQN format: magellan::<file_path>::<Kind> <symbol_name>
1432        // Example: magellan::/home/user/src/main.rs::Function main
1433        if !name.starts_with("magellan::") {
1434            return None;
1435        }
1436        
1437        // Strip the prefix
1438        let after_prefix = &name[10..]; // Skip "magellan::"
1439        
1440        // Find the last :: separator
1441        if let Some(last_sep_pos) = after_prefix.rfind("::") {
1442            let file_path = &after_prefix[..last_sep_pos];
1443            let name_part = &after_prefix[last_sep_pos + 2..];
1444            
1445            // The name_part may include a kind prefix like "Function ", "Struct ", etc.
1446            // Strip the kind prefix to get the actual symbol name
1447            let symbol_name = if let Some(space_pos) = name_part.find(' ') {
1448                &name_part[space_pos + 1..]
1449            } else {
1450                name_part
1451            };
1452            
1453            if !file_path.is_empty() && !symbol_name.is_empty() {
1454                return Some((file_path, symbol_name));
1455            }
1456        }
1457        
1458        None
1459    }
1460    
1461    /// Resolve function by FQN (file path + symbol name)
1462    #[cfg(feature = "backend-geometric")]
1463    fn resolve_function_by_fqn(&self, fqn_data: (&str, &str)) -> Result<i64> {
1464        let (file_path, symbol_name) = fqn_data;
1465        
1466        if let Backend::Geometric(ref geometric) = self.storage {
1467            // Use the direct lookup method (handles deduplication internally)
1468            match geometric.find_symbol_id_by_name_and_path(symbol_name, file_path) {
1469                Some(id) => Ok(id as i64),
1470                None => {
1471                    // Not found or ambiguous - try to get more details for error message
1472                    let all_symbols = geometric.find_symbols_by_name(symbol_name);
1473                    let normalized_target = Self::normalize_path_for_dedup(file_path);
1474                    
1475                    let matching_symbols: Vec<_> = all_symbols
1476                        .into_iter()
1477                        .filter(|sym| {
1478                            let sym_path_normalized = Self::normalize_path_for_dedup(&sym.file_path);
1479                            sym_path_normalized == normalized_target
1480                        })
1481                        .collect();
1482                    
1483                    if matching_symbols.is_empty() {
1484                        anyhow::bail!("Function '{}' not found in file '{}'", symbol_name, file_path);
1485                    } else {
1486                        // Multiple matches - report ambiguity
1487                        anyhow::bail!(
1488                            "Multiple functions named '{}' found in file '{}' ({} matches). Use numeric ID instead.",
1489                            symbol_name,
1490                            file_path,
1491                            matching_symbols.len()
1492                        );
1493                    }
1494                }
1495            }
1496        } else {
1497            anyhow::bail!("Geometric backend not available")
1498        }
1499    }
1500
1501    /// Resolve a function name or ID to a function_id (native-v3 backend)
1502    ///
1503    /// This method uses the native-v3 backend to resolve function names.
1504    /// Matches Magellan's storage format: entity.kind = 'Symbol' with data.kind = 'Function'
1505    #[cfg(feature = "backend-native-v3")]
1506    pub fn resolve_function_name(&self, name_or_id: &str) -> Result<i64> {
1507        // Try to parse as numeric ID first
1508        if let Ok(id) = name_or_id.parse::<i64>() {
1509            return Ok(id);
1510        }
1511
1512        // For native-v3, query using GraphBackend
1513        use sqlitegraph::SnapshotId;
1514        let snapshot = SnapshotId::current();
1515
1516        // Get all entities and filter for functions
1517        let entity_ids = self.backend().entity_ids()
1518            .context("Failed to query entities from backend")?;
1519
1520        // First pass: look for matching symbol_id (hex hash like 7ca9eebfa98204a5)
1521        for entity_id in &entity_ids {
1522            if let Ok(entity) = self.backend().get_node(snapshot, *entity_id) {
1523                if entity.kind == "Symbol" {
1524                    // Check if symbol_id matches
1525                    if let Some(symbol_id) = entity.data.get("symbol_id").and_then(|s| s.as_str()) {
1526                        if symbol_id == name_or_id {
1527                            // Check data.kind for Function type
1528                            if let Some(kind) = entity.data.get("kind").and_then(|k| k.as_str()) {
1529                                if kind == "Function" {
1530                                    return Ok(*entity_id);
1531                                }
1532                            }
1533                        }
1534                    }
1535                }
1536            }
1537        }
1538
1539        // Second pass: look for matching name
1540        for entity_id in &entity_ids {
1541            if let Ok(entity) = self.backend().get_node(snapshot, *entity_id) {
1542                // Check if this is a Symbol entity with matching name
1543                // Magellan stores symbols with entity.kind = 'Symbol' 
1544                // and the actual symbol kind (Function, Struct, etc.) in data.kind
1545                if entity.kind == "Symbol" && entity.name == name_or_id {
1546                    // Check data.kind for Function type
1547                    if let Some(kind) = entity.data.get("kind").and_then(|k| k.as_str()) {
1548                        if kind == "Function" {
1549                            return Ok(*entity_id);
1550                        }
1551                    }
1552                }
1553            }
1554        }
1555
1556        anyhow::bail!(
1557            "Function '{}' not found in database. Run 'magellan watch' to index functions.",
1558            name_or_id
1559        )
1560    }
1561
1562    /// Load a CFG from the database (backend-agnostic)
1563    ///
1564    /// This method works with both SQLite and native-v3 backends.
1565    /// For SQLite backend: uses SQL query on cfg_blocks table
1566    /// For native-v3 backend: uses Magellan's KV store via get_cfg_blocks_kv()
1567    ///
1568    /// # Arguments
1569    ///
1570    /// * `function_id` - ID of the function to load CFG for
1571    ///
1572    /// # Returns
1573    ///
1574    /// * `Ok(Cfg)` - The reconstructed control flow graph
1575    /// * `Err(...)` - Error if query fails or CFG data is invalid
1576    ///
1577    /// # Examples
1578    ///
1579    /// ```no_run
1580    /// # use mirage_analyzer::storage::MirageDb;
1581    /// # fn main() -> anyhow::Result<()> {
1582    /// # let db = MirageDb::open("test.db")?;
1583    /// let cfg = db.load_cfg(123)?;
1584    /// # Ok(())
1585    /// # }
1586    /// ```
1587    #[cfg(feature = "backend-sqlite")]
1588    pub fn load_cfg(&self, function_id: i64) -> Result<crate::cfg::Cfg> {
1589        // Phase 069-02: Use storage backend instead of direct Connection
1590        let blocks = self.storage().get_cfg_blocks(function_id)?;
1591
1592        if blocks.is_empty() {
1593            anyhow::bail!(
1594                "No CFG blocks found for function_id {}. Run 'magellan watch' to build CFGs.",
1595                function_id
1596            );
1597        }
1598
1599        // Get file_path for this function
1600        let file_path = self.get_function_file(function_id);
1601
1602        // Convert CfgBlockData to the tuple format expected by load_cfg_from_rows
1603        let block_rows: Vec<(i64, String, Option<String>, Option<i64>, Option<i64>,
1604                              Option<i64>, Option<i64>, Option<i64>, Option<i64>)> = blocks
1605            .into_iter()
1606            .enumerate()
1607            .map(|(idx, b)| (
1608                idx as i64,  // id (use index as id)
1609                b.kind,
1610                Some(b.terminator),
1611                Some(b.byte_start as i64),
1612                Some(b.byte_end as i64),
1613                Some(b.start_line as i64),
1614                Some(b.start_col as i64),
1615                Some(b.end_line as i64),
1616                Some(b.end_col as i64),
1617            ))
1618            .collect();
1619
1620        load_cfg_from_rows(block_rows, file_path.map(std::path::PathBuf::from))
1621    }
1622
1623    /// Load a CFG from the database (native-v3 backend)
1624    ///
1625    /// This method uses the native-v3 KV store to load CFG data.
1626    #[cfg(feature = "backend-native-v3")]
1627    pub fn load_cfg(&self, function_id: i64) -> Result<crate::cfg::Cfg> {
1628        load_cfg_from_native_v3(self.backend(), function_id)
1629    }
1630
1631    /// Get the function name for a given function_id (backend-agnostic)
1632    ///
1633    /// This method works with both SQLite and native-v3 backends.
1634    /// For SQLite backend: queries the graph_entities table
1635    /// For native-v3 backend: uses GraphBackend::get_node
1636    ///
1637    /// # Arguments
1638    ///
1639    /// * `function_id` - ID of the function
1640    ///
1641    /// # Returns
1642    ///
1643    /// * `Some(name)` - The function name if found
1644    /// * `None` - Function not found
1645    pub fn get_function_name(&self, function_id: i64) -> Option<String> {
1646        let snapshot = SnapshotId::current();
1647        self.backend().get_node(snapshot, function_id)
1648            .ok()
1649            .and_then(|entity| {
1650                // Return the name if this is a function
1651                if entity.kind == "Symbol"
1652                    && entity.data.get("kind").and_then(|v| v.as_str()) == Some("Function")
1653                {
1654                    Some(entity.name)
1655                } else {
1656                    None
1657                }
1658            })
1659    }
1660
1661    /// Get the file path for a given function_id (backend-agnostic)
1662    ///
1663    /// This method works with both SQLite and native-v3 backends.
1664    /// For SQLite backend: queries the graph_entities table
1665    /// For native-v3 backend: uses GraphBackend::get_node
1666    ///
1667    /// # Arguments
1668    ///
1669    /// * `function_id` - ID of the function
1670    ///
1671    /// # Returns
1672    ///
1673    /// * `Some(file_path)` - The file path if found
1674    /// * `None` - File path not available
1675    pub fn get_function_file(&self, function_id: i64) -> Option<String> {
1676        let snapshot = SnapshotId::current();
1677        self.backend().get_node(snapshot, function_id)
1678            .ok()
1679            .and_then(|entity| entity.file_path)
1680    }
1681
1682    /// Check if a function has CFG blocks (backend-agnostic)
1683    ///
1684    /// This method works with both SQLite and native-v3 backends.
1685    /// For SQLite backend: queries the cfg_blocks table
1686    /// For native-v3 backend: checks KV store for cfg:func:{function_id}
1687    ///
1688    /// # Arguments
1689    ///
1690    /// * `function_id` - ID of the function to check
1691    ///
1692    /// # Returns
1693    ///
1694    /// * `true` - Function has CFG blocks
1695    /// * `false` - Function not indexed or no CFG blocks
1696    #[cfg(feature = "backend-sqlite")]
1697    pub fn function_exists(&self, function_id: i64) -> bool {
1698        use crate::storage::function_exists;
1699        self.conn()
1700            .and_then(|conn| Ok(function_exists(conn, function_id)))
1701            .unwrap_or(false)
1702    }
1703
1704    /// Check if a function has CFG blocks (native-v3 backend)
1705    ///
1706    /// For native-v3, checks the KV store for CFG blocks.
1707    /// TODO: Implement using sqlitegraph native-v3 KV APIs
1708    #[cfg(feature = "backend-native-v3")]
1709    pub fn function_exists(&self, _function_id: i64) -> bool {
1710        // TODO: Implement native-v3 function existence check
1711        // This requires using sqlitegraph native-v3 KV store APIs
1712        false
1713    }
1714
1715    /// Get the function hash for path caching (backend-agnostic)
1716    ///
1717    /// This method works with both SQLite and native-v3 backends.
1718    /// For SQLite backend: queries the cfg_blocks table
1719    /// For native-v3 backend: returns None (Magellan manages its own caching)
1720    ///
1721    /// # Arguments
1722    ///
1723    /// * `function_id` - ID of the function
1724    ///
1725    /// # Returns
1726    ///
1727    /// * `Some(hash)` - The function hash if available (SQLite only)
1728    /// * `None` - Hash not available or native-v3 backend
1729    #[cfg(feature = "backend-sqlite")]
1730    pub fn get_function_hash(&self, function_id: i64) -> Option<String> {
1731        use crate::storage::get_function_hash;
1732        self.conn()
1733            .and_then(|conn| Ok(get_function_hash(conn, function_id)))
1734            .ok()
1735            .flatten()
1736    }
1737
1738    /// Get the function hash for path caching (native-v3 backend)
1739    ///
1740    /// For native-v3, always returns None since Magellan manages its own caching.
1741    #[cfg(feature = "backend-native-v3")]
1742    pub fn get_function_hash(&self, _function_id: i64) -> Option<String> {
1743        // Magellan manages its own caching, so Mirage's hash-based caching is not used
1744        None
1745    }
1746}
1747
1748/// Resolve a function name or ID to a function_id (SQLite backend)
1749///
1750/// This is a helper function for the SQLite backend. For backend-agnostic
1751/// resolution, use `MirageDb::resolve_function_name` which takes `&MirageDb`.
1752#[cfg(feature = "backend-sqlite")]
1753fn resolve_function_name_sqlite(conn: &Connection, name_or_id: &str, file_filter: Option<&str>) -> Result<i64> {
1754    // First try to look up by symbol_id (hex hash like 7ca9eebfa98204a5)
1755    // Magellan stores symbol_id inside the data JSON column
1756    let function_id_by_symbol: Option<i64> = conn
1757        .query_row(
1758            "SELECT id FROM graph_entities
1759             WHERE kind = 'Symbol'
1760             AND json_extract(data, '$.kind') = 'Function'
1761             AND json_extract(data, '$.symbol_id') = ?
1762             LIMIT 1",
1763            params![name_or_id],
1764            |row| row.get(0),
1765        )
1766        .optional()
1767        .context(format!(
1768            "Failed to query function with symbol_id '{}'",
1769            name_or_id
1770        ))?;
1771
1772    if let Some(id) = function_id_by_symbol {
1773        return Ok(id);
1774    }
1775
1776    // Then try to look up by function name, optionally filtered by file
1777    let function_id: Option<i64> = if let Some(file_path) = file_filter {
1778        // With file filter - use LIKE to match partial paths
1779        let pattern = format!("%{}%", file_path);
1780        conn.query_row(
1781            "SELECT id FROM graph_entities
1782             WHERE kind = 'Symbol'
1783             AND json_extract(data, '$.kind') = 'Function'
1784             AND name = ?
1785             AND file_path LIKE ?
1786             LIMIT 1",
1787            params![name_or_id, pattern],
1788            |row| row.get(0),
1789        )
1790        .optional()
1791        .context(format!(
1792            "Failed to query function with name '{}' in file '{}'",
1793            name_or_id, file_path
1794        ))?
1795    } else {
1796        // Without file filter - original behavior
1797        conn.query_row(
1798            "SELECT id FROM graph_entities
1799             WHERE kind = 'Symbol'
1800             AND json_extract(data, '$.kind') = 'Function'
1801             AND name = ?
1802             LIMIT 1",
1803            params![name_or_id],
1804            |row| row.get(0),
1805        )
1806        .optional()
1807        .context(format!(
1808            "Failed to query function with name '{}'",
1809            name_or_id
1810        ))?
1811    };
1812
1813    function_id.context(format!(
1814        "Function '{}' not found in database. Run 'magellan watch' to index functions.",
1815        name_or_id
1816    ))
1817}
1818
1819/// Load CFG blocks from SQLite backend
1820///
1821/// This helper function loads CFG blocks using SQL queries from the cfg_blocks table.
1822#[cfg(feature = "backend-sqlite")]
1823fn load_cfg_from_sqlite(conn: &Connection, function_id: i64) -> Result<crate::cfg::Cfg> {
1824    use std::path::PathBuf;
1825
1826    // Query file_path for this function from graph_entities
1827    let file_path: Option<String> = conn
1828        .query_row(
1829            "SELECT file_path FROM graph_entities WHERE id = ?",
1830            params![function_id],
1831            |row| row.get(0),
1832        )
1833        .optional()
1834        .context("Failed to query file_path from graph_entities")?;
1835
1836    let file_path = file_path.map(PathBuf::from);
1837
1838    // Query all blocks for this function from Magellan's cfg_blocks table
1839    // Magellan schema v7+ uses: kind (not block_kind), terminator as TEXT, and line/col columns
1840    let mut stmt = conn.prepare_cached(
1841        "SELECT id, kind, terminator, byte_start, byte_end,
1842                start_line, start_col, end_line, end_col
1843         FROM cfg_blocks
1844         WHERE function_id = ?
1845         ORDER BY id ASC",
1846    ).context("Failed to prepare cfg_blocks query")?;
1847
1848    let block_rows: Vec<(i64, String, Option<String>, Option<i64>, Option<i64>,
1849                          Option<i64>, Option<i64>, Option<i64>, Option<i64>)> = stmt
1850        .query_map(params![function_id], |row| {
1851            Ok((
1852                row.get(0)?,     // id (database primary key)
1853                row.get(1)?,     // kind (Magellan's column name)
1854                row.get(2)?,     // terminator (plain TEXT, not JSON)
1855                row.get(3)?,     // byte_start
1856                row.get(4)?,     // byte_end
1857                row.get(5)?,     // start_line
1858                row.get(6)?,     // start_col
1859                row.get(7)?,     // end_line
1860                row.get(8)?,     // end_col
1861            ))
1862        })
1863        .context("Failed to execute cfg_blocks query")?
1864        .collect::<Result<Vec<_>, _>>()
1865        .context("Failed to collect cfg_blocks rows")?;
1866
1867    if block_rows.is_empty() {
1868        anyhow::bail!(
1869            "No CFG blocks found for function_id {}. Run 'magellan watch' to build CFGs.",
1870            function_id
1871        );
1872    }
1873
1874    load_cfg_from_rows(block_rows, file_path)
1875}
1876
1877/// Load CFG from native-v3 backend
1878///
1879/// This helper function loads CFG blocks from the native-v3 KV store.
1880/// TODO: Implement using sqlitegraph native-v3 APIs.
1881#[cfg(feature = "backend-native-v3")]
1882fn load_cfg_from_native_v3(
1883    _backend: &dyn GraphBackend,
1884    _function_id: i64,
1885) -> Result<crate::cfg::Cfg> {
1886    // TODO: Implement native-v3 CFG loading using sqlitegraph APIs
1887    // This requires using the KV store interface from sqlitegraph native-v3
1888    anyhow::bail!(
1889        "Native-V3 CFG loading is not yet fully implemented. \
1890         Please use the SQLite backend for now."
1891    )
1892}
1893
1894/// Common CFG loading logic used by both SQLite and native-v3 backends
1895///
1896/// This function takes pre-fetched block rows and builds the CFG structure.
1897/// It is shared between both backend implementations to ensure consistency.
1898fn load_cfg_from_rows(
1899    block_rows: Vec<(i64, String, Option<String>, Option<i64>, Option<i64>,
1900                     Option<i64>, Option<i64>, Option<i64>, Option<i64>)>,
1901    file_path: Option<std::path::PathBuf>,
1902) -> Result<crate::cfg::Cfg> {
1903    use crate::cfg::{BasicBlock, BlockKind, Cfg, Terminator};
1904    use crate::cfg::build_edges_from_terminators;
1905    use crate::cfg::source::SourceLocation;
1906    use std::collections::HashMap;
1907
1908    // Build mapping from database block ID to graph node index
1909    let mut db_id_to_node: HashMap<i64, usize> = HashMap::new();
1910    let mut graph = Cfg::new();
1911
1912    // Add each block to the graph
1913    for (node_idx, (db_id, kind_str, terminator_str, byte_start, byte_end,
1914                     start_line, start_col, end_line, end_col)) in
1915        block_rows.iter().enumerate()
1916    {
1917        // Parse Magellan's block kind to Mirage's BlockKind
1918        let kind = match kind_str.as_str() {
1919            "entry" => BlockKind::Entry,
1920            "return" => BlockKind::Exit,
1921            "if" | "else" | "loop" | "while" | "for" | "match_arm" | "block" => BlockKind::Normal,
1922            _ => {
1923                // Fallback: treat unknown kinds as Normal
1924                // Magellan may have additional kinds we don't explicitly handle
1925                BlockKind::Normal
1926            }
1927        };
1928
1929        // Parse Magellan's terminator string to Mirage's Terminator enum
1930        let terminator = match terminator_str.as_deref() {
1931            Some("fallthrough") => Terminator::Goto { target: 0 }, // target will be resolved from edges
1932            Some("conditional") => Terminator::SwitchInt { targets: vec![], otherwise: 0 },
1933            Some("goto") => Terminator::Goto { target: 0 },
1934            Some("return") => Terminator::Return,
1935            Some("break") => Terminator::Abort("break".to_string()),
1936            Some("continue") => Terminator::Abort("continue".to_string()),
1937            Some("call") => Terminator::Call { target: None, unwind: None },
1938            Some("panic") => Terminator::Abort("panic".to_string()),
1939            Some(_) | None => Terminator::Unreachable,
1940        };
1941
1942        // Construct source_location from Magellan's line/column data
1943        let source_location = if let Some(ref path) = file_path {
1944            // Use line/column data directly (Magellan v7+)
1945            let sl = start_line.and_then(|l| start_col.map(|c| (l as usize, c as usize)));
1946            let el = end_line.and_then(|l| end_col.map(|c| (l as usize, c as usize)));
1947
1948            match (sl, el, byte_start, byte_end) {
1949                (Some((start_l, start_c)), Some((end_l, end_c)), Some(bs), Some(be)) => {
1950                    Some(SourceLocation {
1951                        file_path: path.clone(),
1952                        byte_start: *bs as usize,
1953                        byte_end: *be as usize,
1954                        start_line: start_l,
1955                        start_column: start_c,
1956                        end_line: end_l,
1957                        end_column: end_c,
1958                    })
1959                }
1960                _ => None,
1961            }
1962        } else {
1963            None
1964        };
1965
1966        let block = BasicBlock {
1967            id: node_idx,
1968            kind,
1969            statements: vec![], // Empty for now - future enhancement
1970            terminator,
1971            source_location,
1972        };
1973
1974        graph.add_node(block);
1975        db_id_to_node.insert(*db_id, node_idx);
1976    }
1977
1978    // Build edges from terminator data (per RESEARCH.md Pattern 2)
1979    // Edges are derived in memory by analyzing terminators, not queried from cfg_edges table
1980    build_edges_from_terminators(&mut graph, &block_rows, &db_id_to_node)
1981        .context("Failed to build edges from terminator data")?;
1982
1983    Ok(graph)
1984}
1985
1986/// Resolve a function name or ID to a function_id (backend-agnostic)
1987///
1988/// This is the main entry point for resolving function names. It works with both
1989/// SQLite and native-v3 backends.
1990///
1991/// # Arguments
1992///
1993/// * `db` - Database reference (works with both backends)
1994/// * `name_or_id` - Function name (string) or function_id (numeric string)
1995///
1996/// # Returns
1997///
1998/// * `Ok(i64)` - The function_id if found
1999/// * `Err(...)` - Error if function not found or query fails
2000///
2001/// # Examples
2002///
2003/// ```no_run
2004/// # use mirage_analyzer::storage::{resolve_function_name, MirageDb};
2005/// # fn main() -> anyhow::Result<()> {
2006/// # let db = MirageDb::open("test.db")?;
2007/// // Resolve by numeric ID
2008/// let func_id = resolve_function_name(&db, "123")?;
2009///
2010/// // Resolve by function name
2011/// let func_id = resolve_function_name(&db, "my_function")?;
2012/// # Ok(())
2013/// # }
2014/// ```
2015pub fn resolve_function_name(db: &MirageDb, name_or_id: &str) -> Result<i64> {
2016    db.resolve_function_name(name_or_id)
2017}
2018
2019/// Resolve a function name or ID to a function_id with optional file filter
2020///
2021/// This is a helper function that delegates to `MirageDb::resolve_function_name_with_file`.
2022/// Use this for a backend-agnostic API.
2023///
2024/// # Arguments
2025///
2026/// * `db` - Database reference (works with both backends)
2027/// * `name_or_id` - Function name or numeric ID string
2028/// * `file_filter` - Optional file path to disambiguate functions with same name
2029///
2030/// # Returns
2031///
2032/// * `Ok(i64)` - The function_id if found
2033/// * `Err(...)` - Error if function not found or query fails
2034///
2035/// # Examples
2036///
2037/// ```no_run
2038/// # use mirage_analyzer::storage::{resolve_function_name_with_file, MirageDb};
2039/// # fn main() -> anyhow::Result<()> {
2040/// # let db = MirageDb::open("test.db")?;
2041/// // Resolve with file filter to disambiguate
2042/// let func_id = resolve_function_name_with_file(&db, "process", Some("src/lib.rs"))?;
2043/// # Ok(())
2044/// # }
2045/// ```
2046pub fn resolve_function_name_with_file(db: &MirageDb, name_or_id: &str, file_filter: Option<&str>) -> Result<i64> {
2047    db.resolve_function_name_with_file(name_or_id, file_filter)
2048}
2049
2050/// Get the function name for a given function_id (backend-agnostic)
2051///
2052/// This is the main entry point for getting function names. It works with both
2053/// SQLite and native-v3 backends.
2054///
2055/// # Arguments
2056///
2057/// * `db` - Database reference (works with both backends)
2058/// * `function_id` - ID of the function
2059///
2060/// # Returns
2061///
2062/// * `Some(name)` - The function name if found
2063/// * `None` - Function not found
2064///
2065/// # Examples
2066///
2067/// ```no_run
2068/// # use mirage_analyzer::storage::{get_function_name_db, MirageDb};
2069/// # fn main() -> anyhow::Result<()> {
2070/// # let db = MirageDb::open("test.db")?;
2071/// if let Some(name) = get_function_name_db(&db, 123) {
2072///     println!("Function: {}", name);
2073/// }
2074/// # Ok(())
2075/// # }
2076/// ```
2077pub fn get_function_name_db(db: &MirageDb, function_id: i64) -> Option<String> {
2078    db.get_function_name(function_id)
2079}
2080
2081/// Get the file path for a given function_id (backend-agnostic)
2082///
2083/// This is the main entry point for getting function file paths. It works with both
2084/// SQLite and native-v3 backends.
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(file_path)` - The file path if found
2094/// * `None` - File path not available
2095///
2096/// # Examples
2097///
2098/// ```no_run
2099/// # use mirage_analyzer::storage::{get_function_file_db, MirageDb};
2100/// # fn main() -> anyhow::Result<()> {
2101/// # let db = MirageDb::open("test.db")?;
2102/// if let Some(path) = get_function_file_db(&db, 123) {
2103///     println!("File: {}", path);
2104/// }
2105/// # Ok(())
2106/// # }
2107/// ```
2108pub fn get_function_file_db(db: &MirageDb, function_id: i64) -> Option<String> {
2109    db.get_function_file(function_id)
2110}
2111
2112/// Get the function hash for path caching (backend-agnostic)
2113///
2114/// This is the main entry point for getting function hashes. It works with both
2115/// SQLite and native-v3 backends.
2116///
2117/// For SQLite backend: returns the stored hash if available
2118/// For native-v3 backend: always returns None (Magellan manages its own caching)
2119///
2120/// # Arguments
2121///
2122/// * `db` - Database reference (works with both backends)
2123/// * `function_id` - ID of the function
2124///
2125/// # Returns
2126///
2127/// * `Some(hash)` - The function hash if available (SQLite only)
2128/// * `None` - Hash not available or native-v3 backend
2129///
2130/// # Examples
2131///
2132/// ```no_run
2133/// # use mirage_analyzer::storage::{get_function_hash_db, MirageDb};
2134/// # fn main() -> anyhow::Result<()> {
2135/// # let db = MirageDb::open("test.db")?;
2136/// if let Some(hash) = get_function_hash_db(&db, 123) {
2137///     println!("Hash: {}", hash);
2138/// }
2139/// # Ok(())
2140/// # }
2141/// ```
2142pub fn get_function_hash_db(db: &MirageDb, function_id: i64) -> Option<String> {
2143    db.get_function_hash(function_id)
2144}
2145
2146/// Resolve a function name or ID to a function_id (SQLite backend, legacy)
2147///
2148/// This is the legacy function that takes a direct Connection reference.
2149/// For new code supporting both backends, use `resolve_function_name` which takes `&MirageDb`.
2150#[cfg(feature = "backend-sqlite")]
2151pub fn resolve_function_name_with_conn(conn: &Connection, name_or_id: &str) -> Result<i64> {
2152    // Try to parse as numeric ID first
2153    if let Ok(id) = name_or_id.parse::<i64>() {
2154        return Ok(id);
2155    }
2156
2157    // Query by function name
2158    // Note: Magellan v7 stores functions as kind='Symbol' with data.kind='Function'
2159    let function_id: Option<i64> = conn
2160        .query_row(
2161            "SELECT id FROM graph_entities
2162             WHERE kind = 'Symbol'
2163             AND json_extract(data, '$.kind') = 'Function'
2164             AND name = ?
2165             LIMIT 1",
2166            params![name_or_id],
2167            |row| row.get(0),
2168        )
2169        .optional()
2170        .context(format!(
2171            "Failed to query function with name '{}'",
2172            name_or_id
2173        ))?;
2174
2175    function_id.context(format!(
2176        "Function '{}' not found in database. Run 'magellan watch' to index functions.",
2177        name_or_id
2178    ))
2179}
2180
2181/// Load a CFG from the database for a given function_id (backend-agnostic)
2182///
2183/// This is the main entry point for loading CFGs. It works with both SQLite and native-v3 backends.
2184///
2185/// # Arguments
2186///
2187/// * `db` - Database reference (works with both backends)
2188/// * `function_id` - ID of the function to load CFG for
2189///
2190/// # Returns
2191///
2192/// * `Ok(Cfg)` - The reconstructed control flow graph
2193/// * `Err(...)` - Error if query fails or CFG data is invalid
2194///
2195/// # Examples
2196///
2197/// ```no_run
2198/// # use mirage_analyzer::storage::{load_cfg_from_db, MirageDb};
2199/// # fn main() -> anyhow::Result<()> {
2200/// # let db = MirageDb::open("test.db")?;
2201/// let cfg = load_cfg_from_db(&db, 123)?;
2202/// # Ok(())
2203/// # }
2204/// ```
2205///
2206/// # Notes
2207///
2208/// - For SQLite backend: uses SQL query on cfg_blocks table
2209/// - For native-v3 backend: uses Magellan's KV store via get_cfg_blocks_kv()
2210/// - Requires Magellan schema v7+ for cfg_blocks table
2211/// - Edges are constructed in memory from terminator data, not queried from cfg_edges table
2212pub fn load_cfg_from_db(db: &MirageDb, function_id: i64) -> Result<crate::cfg::Cfg> {
2213    db.load_cfg(function_id)
2214}
2215
2216/// Load a CFG from the database for a given function_id (SQLite backend)
2217///
2218/// This is the legacy function that takes a direct Connection reference.
2219/// For new code supporting both backends, use `load_cfg_from_db` which takes `&MirageDb`.
2220///
2221/// # Arguments
2222///
2223/// * `conn` - Database connection (SQLite only)
2224/// * `function_id` - ID of the function to load CFG for
2225///
2226/// # Returns
2227///
2228/// * `Ok(Cfg)` - The reconstructed control flow graph
2229/// * `Err(...)` - Error if query fails or CFG data is invalid
2230///
2231/// # Examples
2232///
2233/// ```no_run
2234/// # use mirage_analyzer::storage::load_cfg_from_db_with_conn;
2235/// # use rusqlite::Connection;
2236/// # fn main() -> anyhow::Result<()> {
2237/// # let conn = Connection::open_in_memory()?;
2238/// let cfg = load_cfg_from_db_with_conn(&conn, 123)?;
2239/// # Ok(())
2240/// # }
2241/// ```
2242///
2243/// # Notes
2244///
2245/// - This function only works with SQLite backend
2246/// - For backend-agnostic loading, use `load_cfg_from_db(&db, function_id)` instead
2247/// - Requires Magellan schema v7+ for cfg_blocks table
2248/// - Edges are constructed in memory from terminator data, not queried from cfg_edges table
2249#[cfg(feature = "backend-sqlite")]
2250pub fn load_cfg_from_db_with_conn(conn: &Connection, function_id: i64) -> Result<crate::cfg::Cfg> {
2251    load_cfg_from_sqlite(conn, function_id)
2252}
2253
2254/// Store a CFG in the database for a given function
2255///
2256/// # Arguments
2257///
2258/// * `conn` - Database connection
2259/// * `function_id` - ID of the function in graph_entities
2260/// * `function_hash` - BLAKE3 hash of the function body for incremental updates
2261/// * `cfg` - The control flow graph to store
2262///
2263/// # Returns
2264///
2265/// * `Ok(())` - CFG stored successfully
2266/// * `Err(...)` - Error if storage fails
2267///
2268/// # Algorithm
2269///
2270/// 1. Begin IMMEDIATE transaction for atomicity
2271/// 2. Clear existing cfg_blocks and cfg_edges for this function_id (incremental update)
2272/// 3. Insert each BasicBlock as a row in cfg_blocks:
2273///    - Serialize terminator as JSON string
2274///    - Store source location byte ranges if available
2275/// 4. Insert each edge as a row in cfg_edges (for backward compatibility)
2276/// 5. Commit transaction
2277///
2278/// # Notes
2279///
2280/// - DEPRECATED: Magellan handles CFG storage via cfg_blocks. Edges are now computed in memory.
2281/// - This function is kept for backward compatibility with existing tests.
2282/// - Uses BEGIN IMMEDIATE to acquire write lock early (prevents write conflicts)
2283/// - Existing blocks/edges are cleared for incremental updates
2284/// - Block IDs are AUTOINCREMENT in the database
2285#[deprecated(note = "Magellan handles CFG storage via cfg_blocks. Edges are computed in memory.")]
2286pub fn store_cfg(
2287    conn: &mut Connection,
2288    function_id: i64,
2289    _function_hash: &str,  // Unused: Magellan manages its own caching
2290    cfg: &crate::cfg::Cfg,
2291) -> Result<()> {
2292    use crate::cfg::{BlockKind, EdgeType, Terminator};
2293    use petgraph::visit::EdgeRef;
2294
2295    conn.execute("BEGIN IMMEDIATE TRANSACTION", [])
2296        .context("Failed to begin transaction")?;
2297
2298    // Clear existing blocks and edges for this function (incremental update)
2299    conn.execute(
2300        "DELETE FROM cfg_edges WHERE from_id IN (
2301            SELECT id FROM cfg_blocks WHERE function_id = ?
2302         )",
2303        params![function_id],
2304    ).context("Failed to clear existing cfg_edges")?;
2305
2306    conn.execute(
2307        "DELETE FROM cfg_blocks WHERE function_id = ?",
2308        params![function_id],
2309    ).context("Failed to clear existing cfg_blocks")?;
2310
2311    // Insert each block and collect database IDs
2312    let mut block_id_map: std::collections::HashMap<petgraph::graph::NodeIndex, i64> =
2313        std::collections::HashMap::new();
2314
2315    let mut insert_block = conn.prepare_cached(
2316        "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
2317                                  start_line, start_col, end_line, end_col)
2318         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
2319    ).context("Failed to prepare block insert statement")?;
2320
2321    for node_idx in cfg.node_indices() {
2322        let block = cfg.node_weight(node_idx)
2323            .context("CFG node has no weight")?;
2324
2325        // Convert terminator to Magellan's string format
2326        let terminator_str = match &block.terminator {
2327            Terminator::Goto { .. } => "goto",
2328            Terminator::SwitchInt { .. } => "conditional",
2329            Terminator::Return => "return",
2330            Terminator::Call { .. } => "call",
2331            Terminator::Abort(msg) if msg == "break" => "break",
2332            Terminator::Abort(msg) if msg == "continue" => "continue",
2333            Terminator::Abort(msg) if msg == "panic" => "panic",
2334            _ => "fallthrough",
2335        };
2336
2337        // Get location data from source_location
2338        let (byte_start, byte_end) = block.source_location.as_ref()
2339            .map(|loc| (Some(loc.byte_start as i64), Some(loc.byte_end as i64)))
2340            .unwrap_or((None, None));
2341
2342        let (start_line, start_col, end_line, end_col) = block.source_location.as_ref()
2343            .map(|loc| (
2344                Some(loc.start_line as i64),
2345                Some(loc.start_column as i64),
2346                Some(loc.end_line as i64),
2347                Some(loc.end_column as i64),
2348            ))
2349            .unwrap_or((None, None, None, None));
2350
2351        // Convert BlockKind to Magellan's kind string
2352        let kind = match block.kind {
2353            BlockKind::Entry => "entry",
2354            BlockKind::Normal => "block",
2355            BlockKind::Exit => "return",
2356        };
2357
2358        insert_block.execute(params![
2359            function_id,
2360            kind,
2361            terminator_str,
2362            byte_start,
2363            byte_end,
2364            start_line,
2365            start_col,
2366            end_line,
2367            end_col,
2368        ]).context("Failed to insert cfg_block")?;
2369
2370        let db_id = conn.last_insert_rowid();
2371        block_id_map.insert(node_idx, db_id);
2372    }
2373
2374    // Insert each edge (for backward compatibility, though edges are now computed in memory)
2375    let mut insert_edge = conn.prepare_cached(
2376        "INSERT INTO cfg_edges (from_id, to_id, edge_type) VALUES (?, ?, ?)",
2377    ).context("Failed to prepare edge insert statement")?;
2378
2379    for edge in cfg.edge_references() {
2380        let from_db_id = block_id_map.get(&edge.source())
2381            .context("Edge source has no database ID")?;
2382        let to_db_id = block_id_map.get(&edge.target())
2383            .context("Edge target has no database ID")?;
2384
2385        let edge_type_str = match edge.weight() {
2386            EdgeType::Fallthrough => "Fallthrough",
2387            EdgeType::TrueBranch => "TrueBranch",
2388            EdgeType::FalseBranch => "FalseBranch",
2389            EdgeType::LoopBack => "LoopBack",
2390            EdgeType::LoopExit => "LoopExit",
2391            EdgeType::Call => "Call",
2392            EdgeType::Exception => "Exception",
2393            EdgeType::Return => "Return",
2394        };
2395
2396        insert_edge.execute(params![from_db_id, to_db_id, edge_type_str])
2397            .context("Failed to insert cfg_edge")?;
2398    }
2399
2400    conn.execute("COMMIT", [])
2401        .context("Failed to commit transaction")?;
2402
2403    Ok(())
2404}
2405
2406/// Check if a function is already indexed in the database
2407///
2408/// # Arguments
2409///
2410/// * `conn` - Database connection
2411/// * `function_id` - ID of the function to check
2412///
2413/// # Returns
2414///
2415/// * `true` - Function has CFG blocks stored
2416/// * `false` - Function not indexed
2417pub fn function_exists(conn: &Connection, function_id: i64) -> bool {
2418    conn.query_row(
2419        "SELECT COUNT(*) FROM cfg_blocks WHERE function_id = ?",
2420        params![function_id],
2421        |row| row.get::<_, i64>(0).map(|count| count > 0)
2422    ).optional().ok().flatten().unwrap_or(false)
2423}
2424
2425/// Get the stored hash for a function
2426///
2427/// # Arguments
2428///
2429/// * `conn` - Database connection
2430/// * `function_id` - ID of the function
2431///
2432/// # Returns
2433///
2434/// * `Some(hash)` - The stored BLAKE3 hash if function exists
2435/// * `None` - Function not found or no hash stored
2436///
2437/// # Note
2438///
2439/// Magellan's cfg_blocks table doesn't store function_hash, so this function
2440/// always returns None when using Magellan's schema. The hash functionality
2441/// is only available when using Mirage's legacy schema.
2442pub fn get_function_hash(conn: &Connection, function_id: i64) -> Option<String> {
2443    // Try Magellan v8+ cfg_hash column first
2444    let cfg_hash: Option<String> = conn.query_row(
2445        "SELECT cfg_hash FROM cfg_blocks WHERE function_id = ? LIMIT 1",
2446        params![function_id],
2447        |row| row.get(0)
2448    ).optional().ok().flatten();
2449    
2450    if cfg_hash.is_some() {
2451        return cfg_hash;
2452    }
2453    
2454    // Fallback: use symbol_id from graph_entities (Magellan v7 schema)
2455    // This provides a stable identifier for caching
2456    conn.query_row(
2457        "SELECT json_extract(data, '$.symbol_id') FROM graph_entities WHERE id = ? LIMIT 1",
2458        params![function_id],
2459        |row| row.get::<_, Option<String>>(0)
2460    ).optional().ok().flatten().flatten()
2461}
2462
2463/// Compare two function hashes and return true if they differ
2464///
2465/// Used by the index command to decide whether to skip a function.
2466///
2467/// # Arguments
2468///
2469/// * `conn` - Database connection
2470/// * `function_id` - ID of the function
2471/// * `new_hash` - New hash to compare against stored hash
2472///
2473/// # Returns
2474///
2475/// * `Ok(true)` - Hashes differ or function is new (needs re-indexing)
2476/// * `Ok(false)` - Hashes match (can skip)
2477/// * `Err(...)` - Database query error
2478///
2479/// # Note
2480///
2481/// Magellan's cfg_blocks table doesn't store function_hash, so this function
2482/// always returns true (indicating re-indexing needed) when using Magellan's schema.
2483pub fn hash_changed(
2484    conn: &Connection,
2485    function_id: i64,
2486    _new_hash: &str,
2487) -> Result<bool> {
2488    let old_hash: Option<String> = conn.query_row(
2489        "SELECT function_hash FROM cfg_blocks WHERE function_id = ? LIMIT 1",
2490        params![function_id],
2491        |row| row.get(0)
2492    ).optional()?;
2493
2494    match old_hash {
2495        Some(old) => Ok(old != _new_hash),
2496        None => Ok(true),  // New function or no hash stored, always index
2497    }
2498}
2499
2500/// Compute the set of functions that need re-indexing based on git changes
2501///
2502/// This uses git diff to find changed Rust files, then queries the database
2503/// for functions defined in those files.
2504///
2505/// # Arguments
2506///
2507/// * `conn` - Database connection
2508/// * `project_path` - Path to the project being indexed
2509///
2510/// # Returns
2511///
2512/// Set of function names that should be re-indexed
2513///
2514/// # Notes
2515///
2516/// - Uses `git diff --name-only HEAD` to detect changed files
2517/// - Only considers .rs files
2518/// - Returns functions from changed files based on graph_entities table
2519pub fn get_changed_functions(
2520    conn: &Connection,
2521    project_path: &std::path::Path,
2522) -> Result<std::collections::HashSet<String>> {
2523    use std::collections::HashSet;
2524    use std::process::Command;
2525
2526    let mut changed = HashSet::new();
2527
2528    // Use git to find changed Rust files
2529    if let Ok(git_output) = Command::new("git")
2530        .args(["diff", "--name-only", "HEAD"])
2531        .current_dir(project_path)
2532        .output()
2533    {
2534        let git_files = String::from_utf8_lossy(&git_output.stdout);
2535
2536        // Collect .rs files that changed
2537        let changed_rs_files: Vec<&str> = git_files
2538            .lines()
2539            .filter(|f| f.ends_with(".rs"))
2540            .collect();
2541
2542        if changed_rs_files.is_empty() {
2543            return Ok(changed);
2544        }
2545
2546        // Build a list of file paths for the SQL query
2547        for file in changed_rs_files {
2548            // Normalize the file path relative to project root
2549            let normalized_path = if file.starts_with('/') {
2550                file.trim_start_matches('/')
2551            } else {
2552                file
2553            };
2554
2555            // Query for functions in this file
2556            // Note: file_path in graph_entities may be relative or absolute,
2557            // so we check both patterns
2558            let mut stmt = conn.prepare_cached(
2559                "SELECT name FROM graph_entities
2560                 WHERE kind = 'function' AND (
2561                     file_path = ? OR
2562                     file_path = ? OR
2563                     file_path LIKE '%' || ?
2564                 )"
2565            ).context("Failed to prepare function lookup query")?;
2566
2567            let with_slash = format!("/{}", normalized_path);
2568
2569            let rows = stmt.query_map(
2570                params![normalized_path, &with_slash, normalized_path],
2571                |row| row.get::<_, String>(0)
2572            ).context("Failed to execute function lookup")?;
2573
2574            for row in rows {
2575                if let Ok(func_name) = row {
2576                    changed.insert(func_name);
2577                }
2578            }
2579        }
2580    }
2581
2582    Ok(changed)
2583}
2584
2585/// Get the file containing a function
2586///
2587/// # Arguments
2588///
2589/// * `conn` - Database connection
2590/// * `function_name` - Name of the function
2591///
2592/// # Returns
2593///
2594/// * `Ok(Some(file_path))` - The file path if found
2595/// * `Ok(None)` - Function not found
2596/// * `Err(...)` - Database error
2597pub fn get_function_file(
2598    conn: &Connection,
2599    function_name: &str,
2600) -> Result<Option<String>> {
2601    let file: Option<String> = conn.query_row(
2602        "SELECT file_path FROM graph_entities WHERE kind = 'function' AND name = ? LIMIT 1",
2603        params![function_name],
2604        |row| row.get(0)
2605    ).optional()?;
2606
2607    Ok(file)
2608}
2609
2610/// Get the function name for a given block ID
2611///
2612/// # Arguments
2613///
2614/// * `conn` - Database connection
2615/// * `function_id` - ID of the function
2616///
2617/// # Returns
2618///
2619/// * `Some(name)` - The function name if found
2620/// * `None` - Function not found
2621pub fn get_function_name(conn: &Connection, function_id: i64) -> Option<String> {
2622    conn.query_row(
2623        "SELECT name FROM graph_entities WHERE id = ?",
2624        params![function_id],
2625        |row| row.get(0)
2626    ).optional().ok().flatten()
2627}
2628
2629/// Get path elements (blocks in order) for a given path_id
2630///
2631/// # Arguments
2632///
2633/// * `conn` - Database connection
2634/// * `path_id` - The path ID to query
2635///
2636/// # Returns
2637///
2638/// * `Ok(Vec<BlockId>)` - Ordered list of block IDs in the path
2639/// * `Err(...)` - Error if query fails or path not found
2640pub fn get_path_elements(conn: &Connection, path_id: &str) -> Result<Vec<crate::cfg::BlockId>> {
2641    let mut stmt = conn.prepare_cached(
2642        "SELECT block_id FROM cfg_path_elements
2643         WHERE path_id = ?
2644         ORDER BY sequence_order ASC",
2645    ).context("Failed to prepare path elements query")?;
2646
2647    let blocks: Vec<crate::cfg::BlockId> = stmt
2648        .query_map(params![path_id], |row| {
2649            Ok(row.get::<_, i64>(0)? as usize)
2650        })
2651        .context("Failed to execute path elements query")?
2652        .collect::<Result<Vec<_>, _>>()
2653        .context("Failed to collect path elements")?;
2654
2655    if blocks.is_empty() {
2656        anyhow::bail!("Path '{}' not found in cache", path_id);
2657    }
2658
2659    Ok(blocks)
2660}
2661
2662/// Compute path impact from the database
2663///
2664/// This loads the path's blocks from the database and computes
2665/// the impact by aggregating reachable blocks from each path block.
2666///
2667/// # Arguments
2668///
2669/// * `conn` - Database connection
2670/// * `path_id` - The path ID to analyze
2671/// * `cfg` - The control flow graph
2672/// * `max_depth` - Maximum depth for impact analysis
2673///
2674/// # Returns
2675///
2676/// * `Ok(PathImpact)` - Aggregated impact data
2677/// * `Err(...)` - Error if path not found or computation fails
2678pub fn compute_path_impact_from_db(
2679    conn: &Connection,
2680    path_id: &str,
2681    cfg: &crate::cfg::Cfg,
2682    max_depth: Option<usize>,
2683) -> Result<crate::cfg::PathImpact> {
2684    let path_blocks = get_path_elements(conn, path_id)?;
2685
2686    let mut impact = crate::cfg::compute_path_impact(cfg, &path_blocks, max_depth);
2687    impact.path_id = path_id.to_string();
2688
2689    Ok(impact)
2690}
2691
2692/// Create a minimal Magellan-compatible database at the given path
2693///
2694/// This creates a new database with the minimal Magellan schema required
2695/// for Mirage to store CFG data. For a full Magellan database, users
2696/// should run `magellan watch` on their project.
2697///
2698/// # Arguments
2699///
2700/// * `path` - Path where the database should be created
2701///
2702/// # Returns
2703///
2704/// * `Ok(())` - Database created successfully
2705/// * `Err(...)` - Error if creation fails
2706pub fn create_minimal_database<P: AsRef<Path>>(path: P) -> Result<()> {
2707    let path = path.as_ref();
2708
2709    // Don't overwrite existing database
2710    if path.exists() {
2711        anyhow::bail!("Database already exists: {}", path.display());
2712    }
2713
2714    let mut conn = Connection::open(path)
2715        .context("Failed to create database file")?;
2716
2717    // Create Magellan meta table
2718    conn.execute(
2719        "CREATE TABLE magellan_meta (
2720            id INTEGER PRIMARY KEY CHECK (id = 1),
2721            magellan_schema_version INTEGER NOT NULL,
2722            sqlitegraph_schema_version INTEGER NOT NULL,
2723            created_at INTEGER NOT NULL
2724        )",
2725        [],
2726    ).context("Failed to create magellan_meta table")?;
2727
2728    // Create graph_entities table (minimal schema)
2729    conn.execute(
2730        "CREATE TABLE graph_entities (
2731            id INTEGER PRIMARY KEY AUTOINCREMENT,
2732            kind TEXT NOT NULL,
2733            name TEXT NOT NULL,
2734            file_path TEXT,
2735            data TEXT NOT NULL
2736        )",
2737        [],
2738    ).context("Failed to create graph_entities table")?;
2739
2740    // Create indexes for graph_entities
2741    conn.execute(
2742        "CREATE INDEX idx_graph_entities_kind ON graph_entities(kind)",
2743        [],
2744    ).context("Failed to create index on graph_entities.kind")?;
2745
2746    conn.execute(
2747        "CREATE INDEX idx_graph_entities_name ON graph_entities(name)",
2748        [],
2749    ).context("Failed to create index on graph_entities.name")?;
2750
2751    // Initialize Magellan meta
2752    let now = chrono::Utc::now().timestamp();
2753    conn.execute(
2754        "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
2755         VALUES (1, ?, ?, ?)",
2756        params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, now],
2757    ).context("Failed to initialize magellan_meta")?;
2758
2759    // Create Mirage schema
2760    create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).context("Failed to create Mirage schema")?;
2761
2762    Ok(())
2763}
2764
2765#[cfg(all(test, feature = "sqlite"))]
2766mod tests {
2767    use super::*;
2768
2769    #[test]
2770    fn test_create_schema() {
2771        let mut conn = Connection::open_in_memory().unwrap();
2772        // First create the Magellan tables (simplified)
2773        conn.execute(
2774            "CREATE TABLE magellan_meta (
2775                id INTEGER PRIMARY KEY CHECK (id = 1),
2776                magellan_schema_version INTEGER NOT NULL,
2777                sqlitegraph_schema_version INTEGER NOT NULL,
2778                created_at INTEGER NOT NULL
2779            )",
2780            [],
2781        ).unwrap();
2782
2783        conn.execute(
2784            "CREATE TABLE graph_entities (
2785                id INTEGER PRIMARY KEY AUTOINCREMENT,
2786                kind TEXT NOT NULL,
2787                name TEXT NOT NULL,
2788                file_path TEXT,
2789                data TEXT NOT NULL
2790            )",
2791            [],
2792        ).unwrap();
2793
2794        // Insert Magellan meta
2795        conn.execute(
2796            "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
2797             VALUES (1, ?, ?, ?)",
2798            params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
2799        ).unwrap();
2800
2801        // Create Mirage schema
2802        create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
2803
2804        // Verify tables exist
2805        let table_count: i64 = conn.query_row(
2806            "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name LIKE 'cfg_%'",
2807            [],
2808            |row| row.get(0),
2809        ).unwrap();
2810
2811        assert!(table_count >= 5); // cfg_blocks, cfg_edges, cfg_paths, cfg_path_elements, cfg_dominators
2812    }
2813
2814    #[test]
2815    fn test_migrate_schema_from_version_0() {
2816        let mut conn = Connection::open_in_memory().unwrap();
2817
2818        // Create Magellan tables
2819        conn.execute(
2820            "CREATE TABLE magellan_meta (
2821                id INTEGER PRIMARY KEY CHECK (id = 1),
2822                magellan_schema_version INTEGER NOT NULL,
2823                sqlitegraph_schema_version INTEGER NOT NULL,
2824                created_at INTEGER NOT NULL
2825            )",
2826            [],
2827        ).unwrap();
2828
2829        conn.execute(
2830            "CREATE TABLE graph_entities (
2831                id INTEGER PRIMARY KEY AUTOINCREMENT,
2832                kind TEXT NOT NULL,
2833                name TEXT NOT NULL,
2834                file_path TEXT,
2835                data TEXT NOT NULL
2836            )",
2837            [],
2838        ).unwrap();
2839
2840        conn.execute(
2841            "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
2842             VALUES (1, ?, ?, ?)",
2843            params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
2844        ).unwrap();
2845
2846        // Create Mirage schema at version 0 (no mirage_meta yet)
2847        create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
2848
2849        // Verify version is 1
2850        let version: i32 = conn.query_row(
2851            "SELECT mirage_schema_version FROM mirage_meta WHERE id = 1",
2852            [],
2853            |row| row.get(0),
2854        ).unwrap();
2855
2856        assert_eq!(version, MIRAGE_SCHEMA_VERSION);
2857    }
2858
2859    #[test]
2860    fn test_migrate_schema_no_op_when_current() {
2861        let mut conn = Connection::open_in_memory().unwrap();
2862
2863        // Create Magellan tables
2864        conn.execute(
2865            "CREATE TABLE magellan_meta (
2866                id INTEGER PRIMARY KEY CHECK (id = 1),
2867                magellan_schema_version INTEGER NOT NULL,
2868                sqlitegraph_schema_version INTEGER NOT NULL,
2869                created_at INTEGER NOT NULL
2870            )",
2871            [],
2872        ).unwrap();
2873
2874        conn.execute(
2875            "CREATE TABLE graph_entities (
2876                id INTEGER PRIMARY KEY AUTOINCREMENT,
2877                kind TEXT NOT NULL,
2878                name TEXT NOT NULL,
2879                file_path TEXT,
2880                data TEXT NOT NULL
2881            )",
2882            [],
2883        ).unwrap();
2884
2885        conn.execute(
2886            "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
2887             VALUES (1, ?, ?, ?)",
2888            params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
2889        ).unwrap();
2890
2891        // Create Mirage schema
2892        create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
2893
2894        // Migration should be a no-op - already at current version
2895        migrate_schema(&mut conn).unwrap();
2896
2897        // Verify version is still 1
2898        let version: i32 = conn.query_row(
2899            "SELECT mirage_schema_version FROM mirage_meta WHERE id = 1",
2900            [],
2901            |row| row.get(0),
2902        ).unwrap();
2903
2904        assert_eq!(version, MIRAGE_SCHEMA_VERSION);
2905    }
2906
2907    #[test]
2908    fn test_fk_constraint_cfg_blocks() {
2909        let mut conn = Connection::open_in_memory().unwrap();
2910
2911        // Enable foreign key enforcement (SQLite requires this)
2912        conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
2913
2914        // Create Magellan tables
2915        conn.execute(
2916            "CREATE TABLE magellan_meta (
2917                id INTEGER PRIMARY KEY CHECK (id = 1),
2918                magellan_schema_version INTEGER NOT NULL,
2919                sqlitegraph_schema_version INTEGER NOT NULL,
2920                created_at INTEGER NOT NULL
2921            )",
2922            [],
2923        ).unwrap();
2924
2925        conn.execute(
2926            "CREATE TABLE graph_entities (
2927                id INTEGER PRIMARY KEY AUTOINCREMENT,
2928                kind TEXT NOT NULL,
2929                name TEXT NOT NULL,
2930                file_path TEXT,
2931                data TEXT NOT NULL
2932            )",
2933            [],
2934        ).unwrap();
2935
2936        conn.execute(
2937            "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
2938             VALUES (1, ?, ?, ?)",
2939            params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
2940        ).unwrap();
2941
2942        // Create Mirage schema
2943        create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
2944
2945        // Insert a graph entity (function)
2946        conn.execute(
2947            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
2948            params!("function", "test_func", "test.rs", "{}"),
2949        ).unwrap();
2950
2951        let function_id: i64 = conn.last_insert_rowid();
2952
2953        // Attempt to insert cfg_blocks with invalid function_id (should fail)
2954        let invalid_result = conn.execute(
2955            "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
2956                                     start_line, start_col, end_line, end_col)
2957             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
2958            params!(9999, "entry", "return", 0, 10, 1, 0, 1, 10),
2959        );
2960
2961        // Should fail with foreign key constraint error
2962        assert!(invalid_result.is_err(), "Insert with invalid function_id should fail");
2963
2964        // Insert valid cfg_blocks with correct function_id (should succeed)
2965        let valid_result = conn.execute(
2966            "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
2967                                     start_line, start_col, end_line, end_col)
2968             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
2969            params!(function_id, "entry", "return", 0, 10, 1, 0, 1, 10),
2970        );
2971
2972        assert!(valid_result.is_ok(), "Insert with valid function_id should succeed");
2973
2974        // Verify the insert worked
2975        let count: i64 = conn.query_row(
2976            "SELECT COUNT(*) FROM cfg_blocks WHERE function_id = ?",
2977            params![function_id],
2978            |row| row.get(0),
2979        ).unwrap();
2980
2981        assert_eq!(count, 1, "Should have exactly one cfg_block entry");
2982    }
2983
2984    #[test]
2985    fn test_store_cfg_retrieves_correctly() {
2986        use crate::cfg::{BasicBlock, BlockKind, Cfg, EdgeType, Terminator};
2987
2988        let mut conn = Connection::open_in_memory().unwrap();
2989
2990        // Create Magellan tables
2991        conn.execute(
2992            "CREATE TABLE magellan_meta (
2993                id INTEGER PRIMARY KEY CHECK (id = 1),
2994                magellan_schema_version INTEGER NOT NULL,
2995                sqlitegraph_schema_version INTEGER NOT NULL,
2996                created_at INTEGER NOT NULL
2997            )",
2998            [],
2999        ).unwrap();
3000
3001        conn.execute(
3002            "CREATE TABLE graph_entities (
3003                id INTEGER PRIMARY KEY AUTOINCREMENT,
3004                kind TEXT NOT NULL,
3005                name TEXT NOT NULL,
3006                file_path TEXT,
3007                data TEXT NOT NULL
3008            )",
3009            [],
3010        ).unwrap();
3011
3012        conn.execute(
3013            "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3014             VALUES (1, ?, ?, ?)",
3015            params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
3016        ).unwrap();
3017
3018        // Create Mirage schema
3019        create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
3020
3021        // Insert a function entity
3022        conn.execute(
3023            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3024            params!("function", "test_func", "test.rs", "{}"),
3025        ).unwrap();
3026
3027        let function_id: i64 = conn.last_insert_rowid();
3028
3029        // Create a simple test CFG
3030        let mut cfg = Cfg::new();
3031
3032        let b0 = cfg.add_node(BasicBlock {
3033            id: 0,
3034            kind: BlockKind::Entry,
3035            statements: vec!["let x = 1".to_string()],
3036            terminator: Terminator::Goto { target: 1 },
3037            source_location: None,
3038        });
3039
3040        let b1 = cfg.add_node(BasicBlock {
3041            id: 1,
3042            kind: BlockKind::Normal,
3043            statements: vec![],
3044            terminator: Terminator::Return,
3045            source_location: None,
3046        });
3047
3048        cfg.add_edge(b0, b1, EdgeType::Fallthrough);
3049
3050        // Store the CFG
3051        store_cfg(&mut conn, function_id, "test_hash_123", &cfg).unwrap();
3052
3053        // Verify blocks were stored
3054        let block_count: i64 = conn.query_row(
3055            "SELECT COUNT(*) FROM cfg_blocks WHERE function_id = ?",
3056            params![function_id],
3057            |row| row.get(0),
3058        ).unwrap();
3059
3060        assert_eq!(block_count, 2, "Should have 2 blocks");
3061
3062        // Verify edges were stored
3063        let edge_count: i64 = conn.query_row(
3064            "SELECT COUNT(*) FROM cfg_edges",
3065            [],
3066            |row| row.get(0),
3067        ).unwrap();
3068
3069        assert_eq!(edge_count, 1, "Should have 1 edge");
3070
3071        // Note: function_hash is not stored in Magellan's schema, so we skip that check
3072        // The hash functionality is only available with Mirage's legacy schema
3073
3074        // Verify function_exists
3075        assert!(function_exists(&conn, function_id));
3076        assert!(!function_exists(&conn, 9999));
3077
3078        // Load and verify the CFG
3079        let loaded_cfg = load_cfg_from_db_with_conn(&conn, function_id).unwrap();
3080
3081        assert_eq!(loaded_cfg.node_count(), 2);
3082        assert_eq!(loaded_cfg.edge_count(), 1);
3083    }
3084
3085    #[test]
3086    fn test_store_cfg_incremental_update_clears_old_data() {
3087        use crate::cfg::{BasicBlock, BlockKind, Cfg, EdgeType, Terminator};
3088
3089        let mut conn = Connection::open_in_memory().unwrap();
3090
3091        // Create Magellan tables
3092        conn.execute(
3093            "CREATE TABLE magellan_meta (
3094                id INTEGER PRIMARY KEY CHECK (id = 1),
3095                magellan_schema_version INTEGER NOT NULL,
3096                sqlitegraph_schema_version INTEGER NOT NULL,
3097                created_at INTEGER NOT NULL
3098            )",
3099            [],
3100        ).unwrap();
3101
3102        conn.execute(
3103            "CREATE TABLE graph_entities (
3104                id INTEGER PRIMARY KEY AUTOINCREMENT,
3105                kind TEXT NOT NULL,
3106                name TEXT NOT NULL,
3107                file_path TEXT,
3108                data TEXT NOT NULL
3109            )",
3110            [],
3111        ).unwrap();
3112
3113        conn.execute(
3114            "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3115             VALUES (1, ?, ?, ?)",
3116            params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
3117        ).unwrap();
3118
3119        create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
3120
3121        conn.execute(
3122            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3123            params!("function", "test_func", "test.rs", "{}"),
3124        ).unwrap();
3125
3126        let function_id: i64 = conn.last_insert_rowid();
3127
3128        // Create initial CFG with 2 blocks
3129        let mut cfg1 = Cfg::new();
3130        let b0 = cfg1.add_node(BasicBlock {
3131            id: 0,
3132            kind: BlockKind::Entry,
3133            statements: vec![],
3134            terminator: Terminator::Goto { target: 1 },
3135            source_location: None,
3136        });
3137        let b1 = cfg1.add_node(BasicBlock {
3138            id: 1,
3139            kind: BlockKind::Exit,
3140            statements: vec![],
3141            terminator: Terminator::Return,
3142            source_location: None,
3143        });
3144        cfg1.add_edge(b0, b1, EdgeType::Fallthrough);
3145
3146        store_cfg(&mut conn, function_id, "hash_v1", &cfg1).unwrap();
3147
3148        let block_count_v1: i64 = conn.query_row(
3149            "SELECT COUNT(*) FROM cfg_blocks WHERE function_id = ?",
3150            params![function_id],
3151            |row| row.get(0),
3152        ).unwrap();
3153
3154        assert_eq!(block_count_v1, 2);
3155
3156        // Create updated CFG with 3 blocks
3157        let mut cfg2 = Cfg::new();
3158        let b0 = cfg2.add_node(BasicBlock {
3159            id: 0,
3160            kind: BlockKind::Entry,
3161            statements: vec![],
3162            terminator: Terminator::Goto { target: 1 },
3163            source_location: None,
3164        });
3165        let b1 = cfg2.add_node(BasicBlock {
3166            id: 1,
3167            kind: BlockKind::Normal,
3168            statements: vec![],
3169            terminator: Terminator::Goto { target: 2 },
3170            source_location: None,
3171        });
3172        let b2 = cfg2.add_node(BasicBlock {
3173            id: 2,
3174            kind: BlockKind::Exit,
3175            statements: vec![],
3176            terminator: Terminator::Return,
3177            source_location: None,
3178        });
3179        cfg2.add_edge(b0, b1, EdgeType::Fallthrough);
3180        cfg2.add_edge(b1, b2, EdgeType::Fallthrough);
3181
3182        store_cfg(&mut conn, function_id, "hash_v3", &cfg2).unwrap();
3183
3184        let block_count_v3: i64 = conn.query_row(
3185            "SELECT COUNT(*) FROM cfg_blocks WHERE function_id = ?",
3186            params![function_id],
3187            |row| row.get(0),
3188        ).unwrap();
3189
3190        // Should have 3 blocks now (old ones cleared)
3191        assert_eq!(block_count_v3, 3);
3192
3193        // Note: function_hash is not stored in Magellan's schema
3194        // Hash verification is skipped for Magellan v7+ schema
3195    }
3196
3197    // Helper function to create a test database with Magellan + Mirage schema
3198    //
3199    // Creates a Magellan v7-compatible database with Mirage extensions.
3200    // The cfg_blocks table uses Magellan v7 schema:
3201    // - kind: TEXT (lowercase: "entry", "block", "return", "if", etc.)
3202    // - terminator: TEXT (lowercase: "fallthrough", "conditional", "return", etc.)
3203    // - Includes line/column fields for source locations
3204    fn create_test_db_with_schema() -> Connection {
3205        let mut conn = Connection::open_in_memory().unwrap();
3206
3207        // Create Magellan v7 tables
3208        conn.execute(
3209            "CREATE TABLE magellan_meta (
3210                id INTEGER PRIMARY KEY CHECK (id = 1),
3211                magellan_schema_version INTEGER NOT NULL,
3212                sqlitegraph_schema_version INTEGER NOT NULL,
3213                created_at INTEGER NOT NULL
3214            )",
3215            [],
3216        ).unwrap();
3217
3218        conn.execute(
3219            "CREATE TABLE graph_entities (
3220                id INTEGER PRIMARY KEY AUTOINCREMENT,
3221                kind TEXT NOT NULL,
3222                name TEXT NOT NULL,
3223                file_path TEXT,
3224                data TEXT NOT NULL
3225            )",
3226            [],
3227        ).unwrap();
3228
3229        // Insert Magellan v7 meta
3230        conn.execute(
3231            "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3232             VALUES (1, ?, ?, ?)",
3233            params![7, 3, 0],  // Magellan v7, sqlitegraph v3
3234        ).unwrap();
3235
3236        // Create Magellan's cfg_blocks table (v7 schema)
3237        // This is the authoritative table for CFG data in Magellan v7+
3238        conn.execute(
3239            "CREATE TABLE cfg_blocks (
3240                id INTEGER PRIMARY KEY AUTOINCREMENT,
3241                function_id INTEGER NOT NULL,
3242                kind TEXT NOT NULL,
3243                terminator TEXT NOT NULL,
3244                byte_start INTEGER NOT NULL,
3245                byte_end INTEGER NOT NULL,
3246                start_line INTEGER NOT NULL,
3247                start_col INTEGER NOT NULL,
3248                end_line INTEGER NOT NULL,
3249                end_col INTEGER NOT NULL,
3250                FOREIGN KEY (function_id) REFERENCES graph_entities(id)
3251            )",
3252            [],
3253        ).unwrap();
3254
3255        // Create graph_edges for CFG edges
3256        conn.execute(
3257            "CREATE TABLE graph_edges (
3258                id INTEGER PRIMARY KEY AUTOINCREMENT,
3259                from_id INTEGER NOT NULL,
3260                to_id INTEGER NOT NULL,
3261                edge_type TEXT NOT NULL,
3262                data TEXT
3263            )",
3264            [],
3265        ).unwrap();
3266
3267        // Create Mirage schema (mirage_meta and additional tables)
3268        create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
3269
3270        // Enable foreign key enforcement for tests
3271        conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
3272
3273        conn
3274    }
3275
3276    // Tests for resolve_function_name and load_cfg_from_db (09-02)
3277
3278    #[test]
3279    fn test_resolve_function_by_id() {
3280        let conn = create_test_db_with_schema();
3281
3282        // Insert a test function
3283        conn.execute(
3284            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3285            params!("function", "my_func", "test.rs", "{}"),
3286        ).unwrap();
3287        let function_id: i64 = conn.last_insert_rowid();
3288
3289        // Resolve by numeric ID
3290        let result = resolve_function_name_with_conn(&conn, &function_id.to_string()).unwrap();
3291        assert_eq!(result, function_id);
3292    }
3293
3294    #[test]
3295    fn test_resolve_function_by_name() {
3296        let conn = create_test_db_with_schema();
3297
3298        // Insert a test function with Magellan v7 schema
3299        // Magellan v7 stores functions as kind='Symbol' with data.kind='Function'
3300        conn.execute(
3301            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3302            params!("Symbol", "test_function", "test.rs", r#"{"kind":"Function"}"#),
3303        ).unwrap();
3304        let function_id: i64 = conn.last_insert_rowid();
3305
3306        // Resolve by name
3307        let result = resolve_function_name_with_conn(&conn, "test_function").unwrap();
3308        assert_eq!(result, function_id);
3309    }
3310
3311    #[test]
3312    fn test_resolve_function_not_found() {
3313        let conn = create_test_db_with_schema();
3314
3315        // Try to resolve a non-existent function
3316        let result = resolve_function_name_with_conn(&conn, "nonexistent_func");
3317
3318        assert!(result.is_err(), "Should return error for non-existent function");
3319        let err_msg = result.unwrap_err().to_string();
3320        assert!(err_msg.contains("not found") || err_msg.contains("not found in database"));
3321    }
3322
3323    #[test]
3324    fn test_resolve_function_numeric_string() {
3325        let conn = create_test_db_with_schema();
3326
3327        // Insert a test function
3328        conn.execute(
3329            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3330            params!("function", "func123", "test.rs", "{}"),
3331        ).unwrap();
3332
3333        // Resolve by numeric string "123" - should parse as ID, not name
3334        let result = resolve_function_name_with_conn(&conn, "123").unwrap();
3335        assert_eq!(result, 123);
3336
3337        // Now insert a function with ID 456
3338        conn.execute(
3339            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3340            params!("function", "another_func", "test.rs", "{}"),
3341        ).unwrap();
3342        let _id_456 = conn.last_insert_rowid();
3343
3344        // If we query "456" it should try to parse as numeric ID
3345        // Since we just inserted and got some ID, let's verify numeric parsing works
3346        let result = resolve_function_name_with_conn(&conn, "999").unwrap();
3347        assert_eq!(result, 999, "Should return numeric ID directly");
3348    }
3349
3350    #[test]
3351    fn test_load_cfg_not_found() {
3352        let conn = create_test_db_with_schema();
3353
3354        // Try to load CFG for non-existent function
3355        let result = load_cfg_from_db_with_conn(&conn, 99999);
3356
3357        assert!(result.is_err(), "Should return error for function with no CFG");
3358        let err_msg = result.unwrap_err().to_string();
3359        assert!(err_msg.contains("No CFG blocks found") || err_msg.contains("not found"));
3360    }
3361
3362    #[test]
3363    fn test_load_cfg_empty_terminator() {
3364        use crate::cfg::Terminator;
3365
3366        let conn = create_test_db_with_schema();
3367
3368        // Insert a test function
3369        conn.execute(
3370            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3371            params!("function", "empty_term_func", "test.rs", "{}"),
3372        ).unwrap();
3373        let function_id: i64 = conn.last_insert_rowid();
3374
3375        // Create a block with NULL terminator (should default to Unreachable)
3376        conn.execute(
3377            "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
3378                                     start_line, start_col, end_line, end_col)
3379             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
3380            params!(function_id, "return", "return", 0, 10, 1, 0, 1, 10),
3381        ).unwrap();
3382
3383        // Load the CFG - should handle NULL terminator gracefully
3384        let cfg = load_cfg_from_db_with_conn(&conn, function_id).unwrap();
3385
3386        assert_eq!(cfg.node_count(), 1);
3387        let block = &cfg[petgraph::graph::NodeIndex::new(0)];
3388        assert!(matches!(block.terminator, Terminator::Return));
3389    }
3390
3391    #[test]
3392    fn test_load_cfg_with_multiple_edge_types() {
3393        use crate::cfg::EdgeType;
3394
3395        let conn = create_test_db_with_schema();
3396
3397        // Insert a test function
3398        conn.execute(
3399            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3400            params!("function", "edge_types_func", "test.rs", "{}"),
3401        ).unwrap();
3402        let function_id: i64 = conn.last_insert_rowid();
3403
3404        // Create blocks with different edge types
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, "entry", "conditional", 0, 10, 1, 0, 1, 10),
3410        ).unwrap();
3411        let _block_0_id: i64 = conn.last_insert_rowid();
3412
3413        conn.execute(
3414            "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
3415                                     start_line, start_col, end_line, end_col)
3416             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
3417            params!(function_id, "block", "fallthrough", 10, 20, 2, 0, 2, 10),
3418        ).unwrap();
3419        let _block_1_id: i64 = conn.last_insert_rowid();
3420
3421        conn.execute(
3422            "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
3423                                     start_line, start_col, end_line, end_col)
3424             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
3425            params!(function_id, "block", "call", 20, 30, 3, 0, 3, 10),
3426        ).unwrap();
3427        let _block_2_id: i64 = conn.last_insert_rowid();
3428
3429        conn.execute(
3430            "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
3431                                     start_line, start_col, end_line, end_col)
3432             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
3433            params!(function_id, "return", "return", 30, 40, 4, 0, 4, 10),
3434        ).unwrap();
3435        let _block_3_id: i64 = conn.last_insert_rowid();
3436
3437        // Load the CFG - edges are now built from terminator data, not cfg_edges table
3438        let cfg = load_cfg_from_db_with_conn(&conn, function_id).unwrap();
3439
3440        assert_eq!(cfg.node_count(), 4);
3441        assert_eq!(cfg.edge_count(), 4);
3442
3443        // Verify edge types are built from terminators:
3444        // Block 0 (conditional) -> Block 1 (TrueBranch), Block 2 (FalseBranch)
3445        // Block 1 (fallthrough) -> Block 2 (Fallthrough)
3446        // Block 2 (call) -> Block 3 (Call)
3447        use petgraph::visit::EdgeRef;
3448        let edges: Vec<_> = cfg.edge_references().map(|e| {
3449            (e.source().index(), e.target().index(), *e.weight())
3450        }).collect();
3451
3452        assert!(edges.contains(&(0, 1, EdgeType::TrueBranch)));
3453        assert!(edges.contains(&(0, 2, EdgeType::FalseBranch)));
3454        assert!(edges.contains(&(1, 2, EdgeType::Fallthrough)));
3455        assert!(edges.contains(&(2, 3, EdgeType::Call)));
3456    }
3457
3458    #[test]
3459    fn test_get_function_name() {
3460        let conn = create_test_db_with_schema();
3461
3462        // Insert a test function
3463        conn.execute(
3464            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3465            params!("function", "my_test_func", "test.rs", "{}"),
3466        ).unwrap();
3467        let function_id: i64 = conn.last_insert_rowid();
3468
3469        // Get function name
3470        let name = get_function_name(&conn, function_id);
3471        assert_eq!(name, Some("my_test_func".to_string()));
3472
3473        // Non-existent function
3474        let name = get_function_name(&conn, 9999);
3475        assert_eq!(name, None);
3476    }
3477
3478    #[test]
3479    fn test_get_path_elements() {
3480        let conn = create_test_db_with_schema();
3481
3482        // Insert a test function and path
3483        conn.execute(
3484            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3485            params!("function", "path_test_func", "test.rs", "{}"),
3486        ).unwrap();
3487        let function_id: i64 = conn.last_insert_rowid();
3488
3489        // Insert a path
3490        conn.execute(
3491            "INSERT INTO cfg_paths (path_id, function_id, path_kind, entry_block, exit_block, length, created_at)
3492             VALUES (?, ?, ?, ?, ?, ?, ?)",
3493            params!("test_path_abc123", function_id, "normal", 0, 2, 3, 1000),
3494        ).unwrap();
3495
3496        // Insert path elements
3497        conn.execute(
3498            "INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
3499            params!("test_path_abc123", 0, 0),
3500        ).unwrap();
3501        conn.execute(
3502            "INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
3503            params!("test_path_abc123", 1, 1),
3504        ).unwrap();
3505        conn.execute(
3506            "INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
3507            params!("test_path_abc123", 2, 2),
3508        ).unwrap();
3509
3510        // Get path elements
3511        let blocks = get_path_elements(&conn, "test_path_abc123").unwrap();
3512        assert_eq!(blocks, vec![0, 1, 2]);
3513
3514        // Non-existent path
3515        let result = get_path_elements(&conn, "nonexistent_path");
3516        assert!(result.is_err());
3517    }
3518
3519    #[test]
3520    fn test_compute_path_impact_from_db() {
3521        use crate::cfg::{BasicBlock, BlockKind, Terminator};
3522
3523        let conn = create_test_db_with_schema();
3524
3525        // Insert a test function
3526        conn.execute(
3527            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3528            params!("function", "impact_test_func", "test.rs", "{}"),
3529        ).unwrap();
3530        let function_id: i64 = conn.last_insert_rowid();
3531
3532        // Create a simple CFG: 0 -> 1 -> 2 -> 3
3533        let mut cfg = crate::cfg::Cfg::new();
3534        let b0 = cfg.add_node(BasicBlock {
3535            id: 0,
3536            kind: BlockKind::Entry,
3537            statements: vec![],
3538            terminator: Terminator::Goto { target: 1 },
3539            source_location: None,
3540        });
3541        let b1 = cfg.add_node(BasicBlock {
3542            id: 1,
3543            kind: BlockKind::Normal,
3544            statements: vec![],
3545            terminator: Terminator::Goto { target: 2 },
3546            source_location: None,
3547        });
3548        let b2 = cfg.add_node(BasicBlock {
3549            id: 2,
3550            kind: BlockKind::Normal,
3551            statements: vec![],
3552            terminator: Terminator::Goto { target: 3 },
3553            source_location: None,
3554        });
3555        let b3 = cfg.add_node(BasicBlock {
3556            id: 3,
3557            kind: BlockKind::Exit,
3558            statements: vec![],
3559            terminator: Terminator::Return,
3560            source_location: None,
3561        });
3562        cfg.add_edge(b0, b1, crate::cfg::EdgeType::Fallthrough);
3563        cfg.add_edge(b1, b2, crate::cfg::EdgeType::Fallthrough);
3564        cfg.add_edge(b2, b3, crate::cfg::EdgeType::Fallthrough);
3565
3566        // Insert a path: 0 -> 1 -> 3
3567        conn.execute(
3568            "INSERT INTO cfg_paths (path_id, function_id, path_kind, entry_block, exit_block, length, created_at)
3569             VALUES (?, ?, ?, ?, ?, ?, ?)",
3570            params!("impact_test_path", function_id, "normal", 0, 3, 3, 1000),
3571        ).unwrap();
3572
3573        conn.execute(
3574            "INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
3575            params!("impact_test_path", 0, 0),
3576        ).unwrap();
3577        conn.execute(
3578            "INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
3579            params!("impact_test_path", 1, 1),
3580        ).unwrap();
3581        conn.execute(
3582            "INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
3583            params!("impact_test_path", 2, 3),
3584        ).unwrap();
3585
3586        // Compute impact
3587        let impact = compute_path_impact_from_db(&conn, "impact_test_path", &cfg, None).unwrap();
3588
3589        assert_eq!(impact.path_id, "impact_test_path");
3590        assert_eq!(impact.path_length, 3);
3591        // Block 2 is not in the path but is reachable from block 1
3592        assert!(impact.unique_blocks_affected.contains(&2));
3593    }
3594
3595    // Graceful degradation tests for missing CFG data
3596
3597    #[test]
3598    fn test_load_cfg_missing_cfg_blocks_table() {
3599        let conn = Connection::open_in_memory().unwrap();
3600
3601        // Create Magellan tables WITHOUT cfg_blocks
3602        conn.execute(
3603            "CREATE TABLE magellan_meta (
3604                id INTEGER PRIMARY KEY CHECK (id = 1),
3605                magellan_schema_version INTEGER NOT NULL,
3606                sqlitegraph_schema_version INTEGER NOT NULL,
3607                created_at INTEGER NOT NULL
3608            )",
3609            [],
3610        ).unwrap();
3611
3612        conn.execute(
3613            "CREATE TABLE graph_entities (
3614                id INTEGER PRIMARY KEY AUTOINCREMENT,
3615                kind TEXT NOT NULL,
3616                name TEXT NOT NULL,
3617                file_path TEXT,
3618                data TEXT NOT NULL
3619            )",
3620            [],
3621        ).unwrap();
3622
3623        conn.execute(
3624            "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3625             VALUES (1, ?, ?, ?)",
3626            params![6, 3, 0],  // Magellan v6 (too old, no cfg_blocks)
3627        ).unwrap();
3628
3629        // Insert a function
3630        conn.execute(
3631            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3632            params!("function", "test_func", "test.rs", "{}"),
3633        ).unwrap();
3634        let function_id: i64 = conn.last_insert_rowid();
3635
3636        // Try to load CFG - should fail with helpful error
3637        let result = load_cfg_from_db_with_conn(&conn, function_id);
3638        assert!(result.is_err(), "Should fail when cfg_blocks table missing");
3639
3640        let err_msg = result.unwrap_err().to_string();
3641        // Error should mention the problem (either cfg_blocks or prepare failed)
3642        assert!(err_msg.contains("cfg_blocks") || err_msg.contains("prepare"),
3643                "Error should mention cfg_blocks or prepare: {}", err_msg);
3644    }
3645
3646    #[test]
3647    fn test_load_cfg_function_not_found() {
3648        let conn = create_test_db_with_schema();
3649
3650        // Try to load CFG for non-existent function
3651        let result = load_cfg_from_db_with_conn(&conn, 99999);
3652        assert!(result.is_err(), "Should fail for non-existent function");
3653
3654        let err_msg = result.unwrap_err().to_string();
3655        assert!(err_msg.contains("No CFG blocks found") || err_msg.contains("not found"),
3656                "Error should mention missing CFG: {}", err_msg);
3657        assert!(err_msg.contains("magellan watch"),
3658                "Error should suggest running magellan watch: {}", err_msg);
3659    }
3660
3661    #[test]
3662    fn test_load_cfg_empty_blocks() {
3663        let conn = create_test_db_with_schema();
3664
3665        // Insert a function but no CFG blocks
3666        conn.execute(
3667            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3668            params!("function", "func_without_cfg", "test.rs", "{}"),
3669        ).unwrap();
3670        let function_id: i64 = conn.last_insert_rowid();
3671
3672        // Try to load CFG - should fail with helpful error
3673        let result = load_cfg_from_db_with_conn(&conn, function_id);
3674        assert!(result.is_err(), "Should fail when no CFG blocks exist");
3675
3676        let err_msg = result.unwrap_err().to_string();
3677        assert!(err_msg.contains("No CFG blocks found"),
3678                "Error should mention no CFG blocks: {}", err_msg);
3679        assert!(err_msg.contains("magellan watch"),
3680                "Error should suggest running magellan watch: {}", err_msg);
3681    }
3682
3683    #[test]
3684    fn test_resolve_function_missing_with_helpful_message() {
3685        let conn = create_test_db_with_schema();
3686
3687        // Try to resolve a non-existent function
3688        let result = resolve_function_name_with_conn(&conn, "nonexistent_function");
3689        assert!(result.is_err(), "Should fail for non-existent function");
3690
3691        let err_msg = result.unwrap_err().to_string();
3692        assert!(err_msg.contains("not found") || err_msg.contains("not found in database"),
3693                "Error should mention function not found: {}", err_msg);
3694    }
3695
3696    #[test]
3697    fn test_open_database_old_magellan_schema() {
3698        let conn = Connection::open_in_memory().unwrap();
3699
3700        // Create Magellan v6 database (too old)
3701        conn.execute(
3702            "CREATE TABLE magellan_meta (
3703                id INTEGER PRIMARY KEY CHECK (id = 1),
3704                magellan_schema_version INTEGER NOT NULL,
3705                sqlitegraph_schema_version INTEGER NOT NULL,
3706                created_at INTEGER NOT NULL
3707            )",
3708            [],
3709        ).unwrap();
3710
3711        conn.execute(
3712            "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3713             VALUES (1, 6, 3, 0)",  // Magellan v6 < required v7
3714            [],
3715        ).unwrap();
3716
3717        // Create cfg_blocks table (but wrong schema version)
3718        conn.execute(
3719            "CREATE TABLE cfg_blocks (
3720                id INTEGER PRIMARY KEY AUTOINCREMENT,
3721                function_id INTEGER NOT NULL,
3722                kind TEXT NOT NULL,
3723                terminator TEXT NOT NULL,
3724                byte_start INTEGER NOT NULL,
3725                byte_end INTEGER NOT NULL,
3726                start_line INTEGER NOT NULL,
3727                start_col INTEGER NOT NULL,
3728                end_line INTEGER NOT NULL,
3729                end_col INTEGER NOT NULL,
3730                FOREIGN KEY (function_id) REFERENCES graph_entities(id)
3731            )",
3732            [],
3733        ).unwrap();
3734
3735        // Try to open via MirageDb - should fail with schema version error
3736        drop(conn);
3737        let db_file = tempfile::NamedTempFile::new().unwrap();
3738        {
3739            let conn = Connection::open(db_file.path()).unwrap();
3740            conn.execute(
3741                "CREATE TABLE magellan_meta (
3742                    id INTEGER PRIMARY KEY CHECK (id = 1),
3743                    magellan_schema_version INTEGER NOT NULL,
3744                    sqlitegraph_schema_version INTEGER NOT NULL,
3745                    created_at INTEGER NOT NULL
3746                )",
3747                [],
3748            ).unwrap();
3749            conn.execute(
3750                "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3751                 VALUES (1, 6, 3, 0)",
3752                [],
3753            ).unwrap();
3754            conn.execute(
3755                "CREATE TABLE graph_entities (
3756                    id INTEGER PRIMARY KEY AUTOINCREMENT,
3757                    kind TEXT NOT NULL,
3758                    name TEXT NOT NULL,
3759                    file_path TEXT,
3760                    data TEXT NOT NULL
3761                )",
3762                [],
3763            ).unwrap();
3764        }
3765
3766        let result = MirageDb::open(db_file.path());
3767        assert!(result.is_err(), "Should fail with old Magellan schema");
3768
3769        let err_msg = result.unwrap_err().to_string();
3770        assert!(err_msg.contains("too old") || err_msg.contains("minimum"),
3771                "Error should mention schema too old: {}", err_msg);
3772        assert!(err_msg.contains("magellan watch"),
3773                "Error should suggest running magellan watch: {}", err_msg);
3774    }
3775
3776    // Backend detection tests (13-01)
3777
3778    #[test]
3779    fn test_backend_detect_sqlite_header() {
3780        use std::io::Write;
3781
3782        // Create a temporary file with SQLite header
3783        let temp_file = tempfile::NamedTempFile::new().unwrap();
3784        let mut file = std::fs::File::create(temp_file.path()).unwrap();
3785        file.write_all(b"SQLite format 3\0").unwrap();
3786        file.sync_all().unwrap();
3787
3788        let backend = BackendFormat::detect(temp_file.path()).unwrap();
3789        assert_eq!(backend, BackendFormat::SQLite, "Should detect SQLite format");
3790    }
3791
3792    #[test]
3793    fn test_backend_detect_native_v3_header() {
3794        use std::io::Write;
3795
3796        // Create a temporary file with custom header (not SQLite)
3797        let temp_file = tempfile::NamedTempFile::new().unwrap();
3798        let mut file = std::fs::File::create(temp_file.path()).unwrap();
3799        file.write_all(b"MIRAGE-NATIVE-V3\0").unwrap();
3800        file.sync_all().unwrap();
3801
3802        let backend = BackendFormat::detect(temp_file.path()).unwrap();
3803        assert_eq!(backend, BackendFormat::NativeV3, "Should detect native-v3 format");
3804    }
3805
3806    #[test]
3807    fn test_backend_detect_nonexistent_file() {
3808        let backend = BackendFormat::detect(Path::new("/nonexistent/path/to/file.db")).unwrap();
3809        assert_eq!(backend, BackendFormat::Unknown, "Non-existent file should be Unknown");
3810    }
3811
3812    #[test]
3813    fn test_backend_detect_empty_file() {
3814        // Empty file has less than 16 bytes
3815        let temp_file = tempfile::NamedTempFile::new().unwrap();
3816        // File is empty (0 bytes)
3817
3818        let backend = BackendFormat::detect(temp_file.path()).unwrap();
3819        assert_eq!(backend, BackendFormat::Unknown, "Empty file should be Unknown");
3820    }
3821
3822    #[test]
3823    fn test_backend_detect_partial_header() {
3824        use std::io::Write;
3825
3826        // File with less than 16 bytes but not SQLite
3827        let temp_file = tempfile::NamedTempFile::new().unwrap();
3828        let mut file = std::fs::File::create(temp_file.path()).unwrap();
3829        file.write_all(b"SQLite").unwrap(); // Only 7 bytes
3830        file.sync_all().unwrap();
3831
3832        let backend = BackendFormat::detect(temp_file.path()).unwrap();
3833        assert_eq!(backend, BackendFormat::Unknown, "Partial header should be Unknown");
3834    }
3835
3836    #[test]
3837    fn test_backend_equality() {
3838        assert_eq!(BackendFormat::SQLite, BackendFormat::SQLite);
3839        assert_eq!(BackendFormat::NativeV3, BackendFormat::NativeV3);
3840        assert_eq!(BackendFormat::Unknown, BackendFormat::Unknown);
3841
3842        assert_ne!(BackendFormat::SQLite, BackendFormat::NativeV3);
3843        assert_ne!(BackendFormat::SQLite, BackendFormat::Unknown);
3844        assert_ne!(BackendFormat::NativeV3, BackendFormat::Unknown);
3845    }
3846}