pub mod paths;
#[cfg(feature = "backend-geometric")]
pub mod geometric;
#[cfg(feature = "backend-sqlite")]
pub mod sqlite_backend;
#[cfg(all(feature = "geometric", not(feature = "backend-geometric")))]
pub mod geometric;
#[cfg(all(feature = "sqlite", not(feature = "backend-sqlite")))]
pub mod sqlite_backend;
use anyhow::{Context, Result};
use rusqlite::{params, Connection, OptionalExtension};
use std::path::Path;
use sqlitegraph::{open_graph, GraphBackend, GraphConfig, SnapshotId};
#[cfg(feature = "backend-geometric")]
pub use geometric::GeometricStorage;
#[cfg(feature = "backend-sqlite")]
pub use sqlite_backend::SqliteStorage;
#[allow(unused_imports)]
pub use paths::{
get_cached_paths, invalidate_function_paths, store_paths, update_function_paths_if_changed,
PathCache,
};
pub trait StorageTrait {
fn get_cfg_blocks(&self, function_id: i64) -> Result<Vec<CfgBlockData>>;
fn get_entity(&self, entity_id: i64) -> Option<sqlitegraph::GraphEntity>;
fn get_cached_paths(&self, _function_id: i64) -> Result<Option<Vec<crate::cfg::Path>>> {
Ok(None) }
fn get_callees(&self, _function_id: i64) -> Result<Vec<i64>> {
Ok(Vec::new())
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CfgBlockData {
pub id: i64,
pub kind: String,
pub terminator: String,
pub byte_start: u64,
pub byte_end: u64,
pub start_line: u64,
pub start_col: u64,
pub end_line: u64,
pub end_col: u64,
pub coord_x: i64,
pub coord_y: i64,
pub coord_z: i64,
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum Backend {
#[cfg(feature = "backend-sqlite")]
Sqlite(SqliteStorage),
#[cfg(feature = "backend-geometric")]
Geometric(GeometricStorage),
}
impl Backend {
pub fn detect_and_open(db_path: &Path) -> Result<Self> {
use magellan::migrate_backend_cmd::detect_backend_format;
#[cfg(feature = "backend-geometric")]
let is_geo = db_path.extension().and_then(|e| e.to_str()) == Some("geo");
#[cfg(feature = "backend-geometric")]
{
if is_geo {
return GeometricStorage::open(db_path).map(Backend::Geometric);
}
}
let sqlite_detected = detect_backend_format(db_path).is_ok();
#[cfg(feature = "backend-sqlite")]
{
if sqlite_detected {
return SqliteStorage::open(db_path).map(Backend::Sqlite);
} else {
return Err(anyhow::anyhow!("Unsupported database format; use a SQLite .db"));
}
}
#[cfg(not(any(feature = "backend-sqlite", feature = "backend-geometric")))]
{
Err(anyhow::anyhow!("No storage backend feature enabled"))
}
}
pub fn is_geometric(&self) -> bool {
match self {
#[cfg(feature = "backend-geometric")]
Backend::Geometric(_) => true,
_ => false,
}
}
pub fn is_sqlite(&self) -> bool {
match self {
#[cfg(feature = "backend-sqlite")]
Backend::Sqlite(_) => true,
#[cfg(feature = "backend-geometric")]
Backend::Geometric(_) => false,
#[cfg(not(feature = "backend-sqlite"))]
_ => false,
}
}
pub fn get_cfg_blocks(&self, function_id: i64) -> Result<Vec<CfgBlockData>> {
match self {
#[cfg(feature = "backend-sqlite")]
Backend::Sqlite(s) => s.get_cfg_blocks(function_id),
#[cfg(feature = "backend-geometric")]
Backend::Geometric(g) => g.get_cfg_blocks(function_id),
#[allow(unreachable_patterns)]
_ => Err(anyhow::anyhow!("No storage backend available")),
}
}
pub fn get_entity(&self, entity_id: i64) -> Option<sqlitegraph::GraphEntity> {
match self {
#[cfg(feature = "backend-sqlite")]
Backend::Sqlite(s) => s.get_entity(entity_id),
#[cfg(feature = "backend-geometric")]
Backend::Geometric(g) => g.get_entity(entity_id),
#[allow(unreachable_patterns)]
_ => None,
}
}
pub fn get_cached_paths(&self, function_id: i64) -> Result<Option<Vec<crate::cfg::Path>>> {
match self {
#[cfg(feature = "backend-sqlite")]
Backend::Sqlite(s) => s.get_cached_paths(function_id),
#[cfg(feature = "backend-geometric")]
Backend::Geometric(g) => g.get_cached_paths(function_id),
#[allow(unreachable_patterns)]
_ => Err(anyhow::anyhow!("No storage backend available")),
}
}
pub fn get_callees(&self, function_id: i64) -> Result<Vec<i64>> {
match self {
#[cfg(feature = "backend-sqlite")]
Backend::Sqlite(s) => s.get_callees(function_id),
#[cfg(feature = "backend-geometric")]
Backend::Geometric(g) => g.get_callees(function_id),
#[allow(unreachable_patterns)]
_ => Ok(Vec::new()),
}
}
}
impl StorageTrait for Backend {
fn get_cfg_blocks(&self, function_id: i64) -> Result<Vec<CfgBlockData>> {
self.get_cfg_blocks(function_id)
}
fn get_entity(&self, entity_id: i64) -> Option<sqlitegraph::GraphEntity> {
self.get_entity(entity_id)
}
fn get_cached_paths(&self, function_id: i64) -> Result<Option<Vec<crate::cfg::Path>>> {
self.get_cached_paths(function_id)
}
fn get_callees(&self, function_id: i64) -> Result<Vec<i64>> {
self.get_callees(function_id)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackendFormat {
SQLite,
Geometric,
Unknown,
}
impl BackendFormat {
pub fn detect(path: &Path) -> Result<Self> {
if !path.exists() {
return Ok(BackendFormat::Unknown);
}
if path.extension().and_then(|e| e.to_str()) == Some("geo") {
return Ok(BackendFormat::Geometric);
}
let mut file = std::fs::File::open(path)?;
let mut header = [0u8; 16];
let bytes_read = std::io::Read::read(&mut file, &mut header)?;
if bytes_read < header.len() {
return Ok(BackendFormat::Unknown);
}
Ok(if &header[..15] == b"SQLite format 3" {
BackendFormat::SQLite
} else {
BackendFormat::Unknown
})
}
}
pub const MIRAGE_SCHEMA_VERSION: i32 = 1;
pub const MIN_MAGELLAN_SCHEMA_VERSION: i32 = 7;
pub const TEST_MAGELLAN_SCHEMA_VERSION: i32 = MIN_MAGELLAN_SCHEMA_VERSION;
pub const REQUIRED_MAGELLAN_SCHEMA_VERSION: i32 = TEST_MAGELLAN_SCHEMA_VERSION;
pub const REQUIRED_SQLITEGRAPH_SCHEMA_VERSION: i32 = 3;
pub struct MirageDb {
storage: Backend,
graph_backend: Box<dyn GraphBackend>,
snapshot_id: SnapshotId,
#[cfg(feature = "backend-sqlite")]
conn: Option<Connection>,
}
impl std::fmt::Debug for MirageDb {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MirageDb")
.field("snapshot_id", &self.snapshot_id)
.field("storage", &self.storage)
.field("graph_backend", &"<GraphBackend>")
.finish()
}
}
impl MirageDb {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
if !path.exists() {
anyhow::bail!("Database not found: {}", path.display());
}
let storage = Backend::detect_and_open(path).context("Failed to open storage backend")?;
let detected_backend =
BackendFormat::detect(path).context("Failed to detect backend format")?;
#[cfg(feature = "backend-geometric")]
if detected_backend == BackendFormat::Geometric {
let snapshot_id = SnapshotId::current();
let graph_backend = create_geometric_stub_backend();
#[cfg(feature = "backend-sqlite")]
let conn = None;
return Ok(Self {
storage,
graph_backend,
snapshot_id,
#[cfg(feature = "backend-sqlite")]
conn,
});
}
let cfg = match detected_backend {
BackendFormat::SQLite => GraphConfig::sqlite(),
BackendFormat::Geometric => {
GraphConfig::native()
}
BackendFormat::Unknown => {
anyhow::bail!(
"Unknown database format: {}. Cannot determine backend.",
path.display()
);
}
};
let graph_backend = open_graph(path, &cfg).context("Failed to open graph database")?;
let snapshot_id = SnapshotId::current();
#[cfg(feature = "backend-sqlite")]
let conn = {
let mut conn = Connection::open(path).context("Failed to open SQLite connection")?;
Self::validate_schema_sqlite(&mut conn, path)?;
Some(conn)
};
Ok(Self {
storage,
graph_backend,
snapshot_id,
#[cfg(feature = "backend-sqlite")]
conn,
})
}
#[cfg(feature = "backend-sqlite")]
fn validate_schema_sqlite(conn: &mut Connection, _path: &Path) -> Result<()> {
let mirage_meta_exists: bool = conn
.query_row(
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='mirage_meta'",
[],
|row| row.get(0),
)
.optional()?
.unwrap_or(0)
== 1;
let mirage_version: i32 = if mirage_meta_exists {
conn.query_row(
"SELECT mirage_schema_version FROM mirage_meta WHERE id = 1",
[],
|row| row.get(0),
)
.optional()?
.flatten()
.unwrap_or(0)
} else {
0
};
if mirage_version > MIRAGE_SCHEMA_VERSION {
anyhow::bail!(
"Database schema version {} is newer than supported version {}.
Please update Mirage.",
mirage_version,
MIRAGE_SCHEMA_VERSION
);
}
let magellan_version: i32 = conn
.query_row(
"SELECT magellan_schema_version FROM magellan_meta WHERE id = 1",
[],
|row| row.get(0),
)
.optional()?
.flatten()
.unwrap_or(0);
if magellan_version < MIN_MAGELLAN_SCHEMA_VERSION {
anyhow::bail!(
"Magellan schema version {} is too old (minimum {}). \
Please update Magellan and run 'magellan watch' to rebuild CFGs.",
magellan_version,
MIN_MAGELLAN_SCHEMA_VERSION
);
}
let cfg_blocks_exists: bool = conn
.query_row(
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='cfg_blocks'",
[],
|row| row.get(0),
)
.optional()?
.unwrap_or(0)
== 1;
if !cfg_blocks_exists {
anyhow::bail!(
"CFG blocks table not found. Magellan schema v7+ required. \
Run 'magellan watch' to build CFGs."
);
}
if !mirage_meta_exists {
create_schema(conn, magellan_version)?;
} else if mirage_version < MIRAGE_SCHEMA_VERSION {
migrate_schema(conn)?;
}
Ok(())
}
#[cfg(feature = "backend-sqlite")]
pub fn conn(&self) -> Result<&Connection, anyhow::Error> {
self.conn.as_ref().ok_or_else(|| {
anyhow::anyhow!(
"Direct Connection access deprecated. Use storage() for CFG queries or backend() for entity queries."
)
})
}
#[cfg(feature = "backend-sqlite")]
pub fn conn_mut(&mut self) -> Result<&mut Connection, anyhow::Error> {
self.conn.as_mut().ok_or_else(|| {
anyhow::anyhow!(
"Direct Connection access deprecated. Use storage() for CFG queries or backend() for entity queries."
)
})
}
pub fn storage(&self) -> &Backend {
&self.storage
}
pub fn backend(&self) -> &dyn GraphBackend {
self.graph_backend.as_ref()
}
#[cfg(feature = "backend-sqlite")]
pub fn is_sqlite(&self) -> bool {
self.conn.is_some()
}
}
#[cfg(feature = "backend-geometric")]
fn create_geometric_stub_backend() -> Box<dyn GraphBackend> {
use sqlitegraph::backend::{BackendDirection, EdgeSpec, NeighborQuery, NodeSpec};
use sqlitegraph::multi_hop::ChainStep;
use sqlitegraph::pattern::{PatternMatch, PatternQuery};
use sqlitegraph::{GraphBackend, GraphEntity, SnapshotId, SqliteGraphError};
struct GeometricStubBackend;
impl GraphBackend for GeometricStubBackend {
fn insert_node(&self, _node: NodeSpec) -> Result<i64, SqliteGraphError> {
Err(SqliteGraphError::unsupported(
"GraphBackend operations not supported for geometric backend. Use GeometricBackend methods directly."
))
}
fn insert_edge(&self, _edge: EdgeSpec) -> Result<i64, SqliteGraphError> {
Err(SqliteGraphError::unsupported(
"GraphBackend operations not supported for geometric backend. Use GeometricBackend methods directly."
))
}
fn update_node(&self, _node_id: i64, _node: NodeSpec) -> Result<i64, SqliteGraphError> {
Err(SqliteGraphError::unsupported(
"GraphBackend operations not supported for geometric backend. Use GeometricBackend methods directly."
))
}
fn delete_entity(&self, _id: i64) -> Result<(), SqliteGraphError> {
Err(SqliteGraphError::unsupported(
"GraphBackend operations not supported for geometric backend. Use GeometricBackend methods directly."
))
}
fn entity_ids(&self) -> Result<Vec<i64>, SqliteGraphError> {
Ok(vec![])
}
fn get_node(
&self,
_snapshot_id: SnapshotId,
_id: i64,
) -> Result<GraphEntity, SqliteGraphError> {
Err(SqliteGraphError::unsupported(
"GraphBackend operations not supported for geometric backend. Use GeometricBackend methods directly."
))
}
fn neighbors(
&self,
_snapshot_id: SnapshotId,
_node: i64,
_query: NeighborQuery,
) -> Result<Vec<i64>, SqliteGraphError> {
Ok(vec![])
}
fn bfs(
&self,
_snapshot_id: SnapshotId,
_start: i64,
_depth: u32,
) -> Result<Vec<i64>, SqliteGraphError> {
Ok(vec![])
}
fn shortest_path(
&self,
_snapshot_id: SnapshotId,
_start: i64,
_end: i64,
) -> Result<Option<Vec<i64>>, SqliteGraphError> {
Ok(None)
}
fn node_degree(
&self,
_snapshot_id: SnapshotId,
_node: i64,
) -> Result<(usize, usize), SqliteGraphError> {
Ok((0, 0))
}
fn k_hop(
&self,
_snapshot_id: SnapshotId,
_start: i64,
_depth: u32,
_direction: BackendDirection,
) -> Result<Vec<i64>, SqliteGraphError> {
Ok(vec![])
}
fn k_hop_filtered(
&self,
_snapshot_id: SnapshotId,
_start: i64,
_depth: u32,
_direction: BackendDirection,
_allowed_edge_types: &[&str],
) -> Result<Vec<i64>, SqliteGraphError> {
Ok(vec![])
}
fn chain_query(
&self,
_snapshot_id: SnapshotId,
_start: i64,
_chain: &[ChainStep],
) -> Result<Vec<i64>, SqliteGraphError> {
Ok(vec![])
}
fn pattern_search(
&self,
_snapshot_id: SnapshotId,
_start: i64,
_pattern: &PatternQuery,
) -> Result<Vec<PatternMatch>, SqliteGraphError> {
Ok(vec![])
}
fn checkpoint(&self) -> Result<(), SqliteGraphError> {
Ok(())
}
fn flush(&self) -> Result<(), SqliteGraphError> {
Ok(())
}
fn backup(
&self,
_backup_dir: &std::path::Path,
) -> Result<sqlitegraph::backend::BackupResult, SqliteGraphError> {
Err(SqliteGraphError::unsupported(
"Backup not supported for geometric backend",
))
}
fn snapshot_export(
&self,
_export_dir: &std::path::Path,
) -> Result<sqlitegraph::backend::SnapshotMetadata, SqliteGraphError> {
Err(SqliteGraphError::unsupported(
"Snapshot export not supported for geometric backend",
))
}
fn snapshot_import(
&self,
_import_dir: &std::path::Path,
) -> Result<sqlitegraph::backend::ImportMetadata, SqliteGraphError> {
Err(SqliteGraphError::unsupported(
"Snapshot import not supported for geometric backend",
))
}
fn query_nodes_by_kind(
&self,
_snapshot_id: SnapshotId,
_kind: &str,
) -> Result<Vec<i64>, SqliteGraphError> {
Ok(vec![])
}
fn query_nodes_by_name_pattern(
&self,
_snapshot_id: SnapshotId,
_pattern: &str,
) -> Result<Vec<i64>, SqliteGraphError> {
Ok(vec![])
}
}
Box::new(GeometricStubBackend)
}
struct Migration {
version: i32,
description: &'static str,
up: fn(&mut Connection) -> Result<()>,
}
fn migrations() -> Vec<Migration> {
vec![]
}
pub fn migrate_schema(conn: &mut Connection) -> Result<()> {
let current_version: i32 = conn
.query_row(
"SELECT mirage_schema_version FROM mirage_meta WHERE id = 1",
[],
|row| row.get(0),
)
.unwrap_or(0);
if current_version >= MIRAGE_SCHEMA_VERSION {
return Ok(());
}
let pending: Vec<_> = migrations()
.into_iter()
.filter(|m| m.version > current_version && m.version <= MIRAGE_SCHEMA_VERSION)
.collect();
for migration in pending {
(migration.up)(conn).with_context(|| {
format!(
"Failed to run migration v{}: {}",
migration.version, migration.description
)
})?;
conn.execute(
"UPDATE mirage_meta SET mirage_schema_version = ? WHERE id = 1",
params![migration.version],
)?;
}
if current_version < MIRAGE_SCHEMA_VERSION {
conn.execute(
"UPDATE mirage_meta SET mirage_schema_version = ? WHERE id = 1",
params![MIRAGE_SCHEMA_VERSION],
)?;
}
Ok(())
}
pub fn create_schema(conn: &mut Connection, _magellan_schema_version: i32) -> Result<()> {
conn.execute(
"CREATE TABLE IF NOT EXISTS mirage_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
mirage_schema_version INTEGER NOT NULL,
magellan_schema_version INTEGER NOT NULL,
compiler_version TEXT,
created_at INTEGER NOT NULL
)",
[],
)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS cfg_blocks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
function_id INTEGER NOT NULL,
kind TEXT NOT NULL,
terminator TEXT NOT NULL,
byte_start INTEGER,
byte_end INTEGER,
start_line INTEGER,
start_col INTEGER,
end_line INTEGER,
end_col INTEGER,
coord_x INTEGER NOT NULL DEFAULT 0,
coord_y INTEGER NOT NULL DEFAULT 0,
coord_z INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (function_id) REFERENCES graph_entities(id)
)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_cfg_blocks_function ON cfg_blocks(function_id)",
[],
)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS cfg_paths (
path_id TEXT PRIMARY KEY,
function_id INTEGER NOT NULL,
path_kind TEXT NOT NULL,
entry_block INTEGER NOT NULL,
exit_block INTEGER NOT NULL,
length INTEGER NOT NULL,
created_at INTEGER NOT NULL,
FOREIGN KEY (function_id) REFERENCES graph_entities(id)
)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_cfg_paths_function ON cfg_paths(function_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_cfg_paths_kind ON cfg_paths(path_kind)",
[],
)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS cfg_path_elements (
path_id TEXT NOT NULL,
sequence_order INTEGER NOT NULL,
block_id INTEGER NOT NULL,
PRIMARY KEY (path_id, sequence_order),
FOREIGN KEY (path_id) REFERENCES cfg_paths(path_id)
)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS cfg_path_elements_block ON cfg_path_elements(block_id)",
[],
)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS cfg_dominators (
block_id INTEGER NOT NULL,
dominator_id INTEGER NOT NULL,
is_strict BOOLEAN NOT NULL,
PRIMARY KEY (block_id, dominator_id, is_strict),
FOREIGN KEY (block_id) REFERENCES cfg_blocks(id),
FOREIGN KEY (dominator_id) REFERENCES cfg_blocks(id)
)",
[],
)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS cfg_post_dominators (
block_id INTEGER NOT NULL,
post_dominator_id INTEGER NOT NULL,
is_strict BOOLEAN NOT NULL,
PRIMARY KEY (block_id, post_dominator_id, is_strict),
FOREIGN KEY (block_id) REFERENCES cfg_blocks(id),
FOREIGN KEY (post_dominator_id) REFERENCES cfg_blocks(id)
)",
[],
)?;
let now = chrono::Utc::now().timestamp();
conn.execute(
"INSERT OR REPLACE INTO mirage_meta (id, mirage_schema_version, magellan_schema_version, created_at)
VALUES (1, ?, ?, ?)",
params![MIRAGE_SCHEMA_VERSION, REQUIRED_MAGELLAN_SCHEMA_VERSION, now],
)?;
Ok(())
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct DatabaseStatus {
pub cfg_blocks: i64,
#[deprecated(note = "Edges are now computed in memory, not stored")]
pub cfg_edges: i64,
pub cfg_paths: i64,
pub cfg_dominators: i64,
pub mirage_schema_version: i32,
pub magellan_schema_version: i32,
}
impl MirageDb {
#[cfg(feature = "backend-sqlite")]
pub fn status(&self) -> Result<DatabaseStatus> {
match self.conn.as_ref() {
Some(conn) => {
let cfg_blocks: i64 = conn
.query_row("SELECT COUNT(*) FROM cfg_blocks", [], |row| row.get(0))
.unwrap_or(0);
let cfg_edges: i64 = conn
.query_row("SELECT COUNT(*) FROM cfg_edges", [], |row| row.get(0))
.unwrap_or(0);
let cfg_paths: i64 = conn
.query_row("SELECT COUNT(*) FROM cfg_paths", [], |row| row.get(0))
.unwrap_or(0);
let cfg_dominators: i64 = conn
.query_row("SELECT COUNT(*) FROM cfg_dominators", [], |row| row.get(0))
.unwrap_or(0);
let mirage_schema_version: i32 = conn
.query_row(
"SELECT mirage_schema_version FROM mirage_meta WHERE id = 1",
[],
|row| row.get(0),
)
.unwrap_or(0);
let magellan_schema_version: i32 = conn
.query_row(
"SELECT magellan_schema_version FROM magellan_meta WHERE id = 1",
[],
|row| row.get(0),
)
.unwrap_or(0);
#[allow(deprecated)]
Ok(DatabaseStatus {
cfg_blocks,
cfg_edges,
cfg_paths,
cfg_dominators,
mirage_schema_version,
magellan_schema_version,
})
}
None => {
self.status_via_storage()
}
}
}
#[cfg(feature = "backend-sqlite")]
fn status_via_storage(&self) -> Result<DatabaseStatus> {
#[cfg(feature = "backend-geometric")]
{
if let Backend::Geometric(ref geometric) = self.storage {
let stats = geometric.get_stats()?;
return Ok(DatabaseStatus {
cfg_blocks: stats.cfg_block_count as i64,
cfg_edges: 0, cfg_paths: 0, cfg_dominators: 0, mirage_schema_version: MIRAGE_SCHEMA_VERSION,
magellan_schema_version: MIN_MAGELLAN_SCHEMA_VERSION,
});
}
}
#[allow(deprecated)]
Ok(DatabaseStatus {
cfg_blocks: 0,
cfg_edges: 0, cfg_paths: 0,
cfg_dominators: 0,
mirage_schema_version: MIRAGE_SCHEMA_VERSION,
magellan_schema_version: MIN_MAGELLAN_SCHEMA_VERSION,
})
}
#[cfg(all(
feature = "backend-geometric",
not(feature = "backend-sqlite")
))]
pub fn status(&self) -> Result<DatabaseStatus> {
let cfg_blocks: i64 = if let Backend::Geometric(ref geometric) = self.storage {
0
} else {
0
};
let cfg_edges: i64 = 0;
let cfg_paths: i64 = 0;
let cfg_dominators: i64 = 0;
let mirage_schema_version = MIRAGE_SCHEMA_VERSION;
let magellan_schema_version = MIN_MAGELLAN_SCHEMA_VERSION;
#[allow(deprecated)]
Ok(DatabaseStatus {
cfg_blocks,
cfg_edges,
cfg_paths,
cfg_dominators,
mirage_schema_version,
magellan_schema_version,
})
}
#[cfg(feature = "backend-sqlite")]
pub fn resolve_function_name(&self, name_or_id: &str) -> Result<i64> {
self.resolve_function_name_with_file(name_or_id, None)
}
#[cfg(feature = "backend-sqlite")]
pub fn resolve_function_name_with_file(
&self,
name_or_id: &str,
file_filter: Option<&str>,
) -> Result<i64> {
if let Ok(id) = name_or_id.parse::<i64>() {
return Ok(id);
}
if let Ok(conn) = self.conn() {
resolve_function_name_sqlite(conn, name_or_id, file_filter)
} else {
#[cfg(feature = "backend-geometric")]
{
if let Backend::Geometric(ref geometric) = self.storage {
return self.resolve_function_name_geometric(name_or_id);
}
}
anyhow::bail!("No database connection available for function resolution")
}
}
#[cfg(feature = "backend-geometric")]
fn normalize_path_for_dedup(path: &str) -> String {
let path = path.replace('\\', "/");
let path = path.strip_prefix("./").unwrap_or(&path);
if let Some(idx) = path.find("/src/") {
path[idx + 1..].to_string()
} else {
path.to_string()
}
}
#[cfg(feature = "backend-geometric")]
fn resolve_function_name_geometric(&self, name_or_id: &str) -> Result<i64> {
if let Ok(id) = name_or_id.parse::<i64>() {
if let Backend::Geometric(ref geometric) = self.storage {
if geometric
.inner()
.find_symbol_by_id_info(id as u64)
.is_some()
{
return Ok(id);
}
}
anyhow::bail!("Function with ID '{}' not found", id);
}
if let Some(fqn_data) = Self::parse_fqn(name_or_id) {
return self.resolve_function_by_fqn(fqn_data);
}
if let Backend::Geometric(ref geometric) = self.storage {
let all_symbols = geometric.find_symbols_by_name(name_or_id);
if all_symbols.is_empty() {
anyhow::bail!("Function '{}' not found", name_or_id);
}
let mut unique_symbols: Vec<magellan::graph::geometric_backend::SymbolInfo> =
Vec::new();
let mut seen_ids: std::collections::HashSet<u64> = std::collections::HashSet::new();
for sym in all_symbols {
if seen_ids.insert(sym.id) {
unique_symbols.push(sym);
}
}
if unique_symbols.len() > 1 {
let first = &unique_symbols[0];
let first_path_normalized = Self::normalize_path_for_dedup(&first.file_path);
let all_same_location = unique_symbols.iter().all(|sym| {
let sym_path_normalized = Self::normalize_path_for_dedup(&sym.file_path);
sym.name == first.name
&& sym_path_normalized == first_path_normalized
&& sym.start_line == first.start_line
&& sym.start_col == first.start_col
});
if !all_same_location {
anyhow::bail!(
"Ambiguous function reference to '{}': {} unique candidates found\n\nCandidates:\n{}\n\nUse full qualified name: magellan::/path/to/file.rs::{}",
name_or_id,
unique_symbols.len(),
unique_symbols.iter().map(|s| {
format!(" - {} ({}:{}:{})", s.name, s.file_path, s.start_line, s.start_col)
}).collect::<Vec<_>>().join("\n"),
name_or_id
);
}
}
Ok(unique_symbols[0].id as i64)
} else {
anyhow::bail!("Geometric backend not available")
}
}
#[cfg(feature = "backend-geometric")]
fn parse_fqn(name: &str) -> Option<(&str, &str)> {
if !name.starts_with("magellan::") {
return None;
}
let after_prefix = &name[10..];
if let Some(last_sep_pos) = after_prefix.rfind("::") {
let file_path = &after_prefix[..last_sep_pos];
let name_part = &after_prefix[last_sep_pos + 2..];
let symbol_name = if let Some(space_pos) = name_part.find(' ') {
&name_part[space_pos + 1..]
} else {
name_part
};
if !file_path.is_empty() && !symbol_name.is_empty() {
return Some((file_path, symbol_name));
}
}
None
}
#[cfg(feature = "backend-geometric")]
fn resolve_function_by_fqn(&self, fqn_data: (&str, &str)) -> Result<i64> {
let (file_path, symbol_name) = fqn_data;
if let Backend::Geometric(ref geometric) = self.storage {
match geometric.find_symbol_id_by_name_and_path(symbol_name, file_path) {
Some(id) => Ok(id as i64),
None => {
let all_symbols = geometric.find_symbols_by_name(symbol_name);
let normalized_target = Self::normalize_path_for_dedup(file_path);
let matching_symbols: Vec<_> = all_symbols
.into_iter()
.filter(|sym| {
let sym_path_normalized =
Self::normalize_path_for_dedup(&sym.file_path);
sym_path_normalized == normalized_target
})
.collect();
if matching_symbols.is_empty() {
anyhow::bail!(
"Function '{}' not found in file '{}'",
symbol_name,
file_path
);
} else {
anyhow::bail!(
"Multiple functions named '{}' found in file '{}' ({} matches). Use numeric ID instead.",
symbol_name,
file_path,
matching_symbols.len()
);
}
}
}
} else {
anyhow::bail!("Geometric backend not available")
}
}
#[cfg(feature = "backend-sqlite")]
pub fn load_cfg(&self, function_id: i64) -> Result<crate::cfg::Cfg> {
let blocks = self.storage().get_cfg_blocks(function_id)?;
if blocks.is_empty() {
anyhow::bail!(
"No CFG blocks found for function_id {}. Run 'magellan watch' to build CFGs.",
function_id
);
}
let file_path = self.get_function_file(function_id);
let block_rows: Vec<(
i64,
String,
Option<String>,
Option<i64>,
Option<i64>,
Option<i64>,
Option<i64>,
Option<i64>,
Option<i64>,
Option<i64>,
Option<i64>,
Option<i64>,
)> = blocks
.into_iter()
.enumerate()
.map(|(idx, b)| {
(
idx as i64, b.kind,
Some(b.terminator),
Some(b.byte_start as i64),
Some(b.byte_end as i64),
Some(b.start_line as i64),
Some(b.start_col as i64),
Some(b.end_line as i64),
Some(b.end_col as i64),
Some(b.coord_x),
Some(b.coord_y),
Some(b.coord_z),
)
})
.collect();
let cfg_edges: Vec<(i64, i64, String)> = if let Ok(conn) = self.conn() {
match conn.prepare_cached(
"SELECT source_idx, target_idx, edge_type
FROM cfg_edges
WHERE function_id = ?
ORDER BY source_idx, target_idx",
) {
Ok(mut stmt) => {
match stmt.query_map(params![function_id], |row| {
Ok((row.get(0)?, row.get(1)?, row.get(2)?))
}) {
Ok(rows) => rows.collect::<Result<Vec<_>, _>>().unwrap_or_default(),
Err(_) => vec![],
}
}
Err(_) => vec![],
}
} else {
vec![]
};
load_cfg_from_rows(
block_rows,
file_path.map(std::path::PathBuf::from),
cfg_edges,
)
}
pub fn get_function_name(&self, function_id: i64) -> Option<String> {
let snapshot = SnapshotId::current();
self.backend()
.get_node(snapshot, function_id)
.ok()
.and_then(|entity| {
if entity.kind == "Symbol"
&& entity.data.get("kind").and_then(|v| v.as_str()) == Some("Function")
{
Some(entity.name)
} else {
None
}
})
}
pub fn get_function_file(&self, function_id: i64) -> Option<String> {
let snapshot = SnapshotId::current();
self.backend()
.get_node(snapshot, function_id)
.ok()
.and_then(|entity| entity.file_path)
}
#[cfg(feature = "backend-sqlite")]
pub fn function_exists(&self, function_id: i64) -> bool {
use crate::storage::function_exists;
self.conn()
.and_then(|conn| Ok(function_exists(conn, function_id)))
.unwrap_or(false)
}
#[cfg(feature = "backend-sqlite")]
pub fn get_function_hash(&self, function_id: i64) -> Option<String> {
use crate::storage::get_function_hash;
self.conn()
.and_then(|conn| Ok(get_function_hash(conn, function_id)))
.ok()
.flatten()
}
}
#[cfg(feature = "backend-sqlite")]
fn resolve_function_name_sqlite(
conn: &Connection,
name_or_id: &str,
file_filter: Option<&str>,
) -> Result<i64> {
let function_id_by_symbol: Option<i64> = conn
.query_row(
"SELECT id FROM graph_entities
WHERE kind = 'Symbol'
AND json_extract(data, '$.kind') = 'Function'
AND json_extract(data, '$.symbol_id') = ?
LIMIT 1",
params![name_or_id],
|row| row.get(0),
)
.optional()
.context(format!(
"Failed to query function with symbol_id '{}'",
name_or_id
))?;
if let Some(id) = function_id_by_symbol {
return Ok(id);
}
let function_id: Option<i64> = if let Some(file_path) = file_filter {
let pattern = format!("%{}%", file_path);
conn.query_row(
"SELECT id FROM graph_entities
WHERE kind = 'Symbol'
AND json_extract(data, '$.kind') = 'Function'
AND name = ?
AND file_path LIKE ?
LIMIT 1",
params![name_or_id, pattern],
|row| row.get(0),
)
.optional()
.context(format!(
"Failed to query function with name '{}' in file '{}'",
name_or_id, file_path
))?
} else {
conn.query_row(
"SELECT id FROM graph_entities
WHERE kind = 'Symbol'
AND json_extract(data, '$.kind') = 'Function'
AND name = ?
LIMIT 1",
params![name_or_id],
|row| row.get(0),
)
.optional()
.context(format!(
"Failed to query function with name '{}'",
name_or_id
))?
};
function_id.context(format!(
"Function '{}' not found in database. Run 'magellan watch' to index functions.",
name_or_id
))
}
#[cfg(feature = "backend-sqlite")]
fn load_cfg_from_sqlite(conn: &Connection, function_id: i64) -> Result<crate::cfg::Cfg> {
use std::path::PathBuf;
let file_path: Option<String> = conn
.query_row(
"SELECT file_path FROM graph_entities WHERE id = ?",
params![function_id],
|row| row.get(0),
)
.optional()
.context("Failed to query file_path from graph_entities")?;
let file_path = file_path.map(PathBuf::from);
let mut stmt = conn
.prepare_cached(
"SELECT id, kind, terminator, byte_start, byte_end,
start_line, start_col, end_line, end_col,
coord_x, coord_y, coord_z
FROM cfg_blocks
WHERE function_id = ?
ORDER BY id ASC",
)
.context("Failed to prepare cfg_blocks query")?;
let block_rows: Vec<(
i64,
String,
Option<String>,
Option<i64>,
Option<i64>,
Option<i64>,
Option<i64>,
Option<i64>,
Option<i64>,
Option<i64>,
Option<i64>,
Option<i64>,
)> = stmt
.query_map(params![function_id], |row| {
Ok((
row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?, row.get(5)?, row.get(6)?, row.get(7)?, row.get(8)?, row.get(9)?, row.get(10)?, row.get(11)?, ))
})
.context("Failed to execute cfg_blocks query")?
.collect::<Result<Vec<_>, _>>()
.context("Failed to collect cfg_blocks rows")?;
if block_rows.is_empty() {
anyhow::bail!(
"No CFG blocks found for function_id {}. Run 'magellan watch' to build CFGs.",
function_id
);
}
let edges: Vec<(i64, i64, String)> = match conn.prepare_cached(
"SELECT source_idx, target_idx, edge_type
FROM cfg_edges
WHERE function_id = ?
ORDER BY source_idx, target_idx",
) {
Ok(mut stmt) => stmt
.query_map(params![function_id], |row| {
Ok((row.get(0)?, row.get(1)?, row.get(2)?))
})
.context("Failed to query cfg_edges")?
.collect::<Result<Vec<_>, _>>()
.context("Failed to collect cfg_edges rows")?,
Err(_) => Vec::new(),
};
load_cfg_from_rows(block_rows, file_path, edges)
}
fn load_cfg_from_rows(
block_rows: Vec<(
i64,
String,
Option<String>,
Option<i64>,
Option<i64>,
Option<i64>,
Option<i64>,
Option<i64>,
Option<i64>,
Option<i64>,
Option<i64>,
Option<i64>,
)>,
file_path: Option<std::path::PathBuf>,
cfg_edges: Vec<(i64, i64, String)>,
) -> Result<crate::cfg::Cfg> {
use crate::cfg::source::SourceLocation;
use crate::cfg::{build_edges_from_cfg_edges, build_edges_from_terminators};
use crate::cfg::{BasicBlock, BlockKind, Cfg, Terminator};
use std::collections::HashMap;
let mut db_id_to_node: HashMap<i64, usize> = HashMap::new();
let mut graph = Cfg::new();
for (
node_idx,
(
db_id,
kind_str,
terminator_str,
byte_start,
byte_end,
start_line,
start_col,
end_line,
end_col,
coord_x,
coord_y,
coord_z,
),
) in block_rows.iter().enumerate()
{
let kind = match kind_str.as_str() {
"entry" => BlockKind::Entry,
"return" => BlockKind::Exit,
"if" | "else" | "loop" | "while" | "for" | "match_arm" | "block" => BlockKind::Normal,
_ => {
BlockKind::Normal
}
};
let terminator = match terminator_str.as_deref() {
Some("fallthrough") => Terminator::Goto { target: 0 }, Some("conditional") => Terminator::SwitchInt {
targets: vec![],
otherwise: 0,
},
Some("goto") => Terminator::Goto { target: 0 },
Some("return") => Terminator::Return,
Some("break") => Terminator::Abort("break".to_string()),
Some("continue") => Terminator::Abort("continue".to_string()),
Some("call") => Terminator::Call {
target: None,
unwind: None,
},
Some("panic") => Terminator::Abort("panic".to_string()),
Some(_) | None => Terminator::Unreachable,
};
let source_location = if let Some(ref path) = file_path {
let sl = start_line.and_then(|l| start_col.map(|c| (l as usize, c as usize)));
let el = end_line.and_then(|l| end_col.map(|c| (l as usize, c as usize)));
match (sl, el, byte_start, byte_end) {
(Some((start_l, start_c)), Some((end_l, end_c)), Some(bs), Some(be)) => {
Some(SourceLocation {
file_path: path.clone(),
byte_start: *bs as usize,
byte_end: *be as usize,
start_line: start_l,
start_column: start_c,
end_line: end_l,
end_column: end_c,
})
}
_ => None,
}
} else {
None
};
let block = BasicBlock {
id: node_idx,
db_id: Some(*db_id),
kind,
statements: vec![], terminator,
source_location,
coord_x: coord_x.unwrap_or(0),
coord_y: coord_y.unwrap_or(0),
coord_z: coord_z.unwrap_or(0),
};
graph.add_node(block);
db_id_to_node.insert(*db_id, node_idx);
}
let mut index_to_node: HashMap<usize, usize> = HashMap::new();
for (idx, (db_id, _, _, _, _, _, _, _, _, _, _, _)) in block_rows.iter().enumerate() {
if let Some(&node_idx) = db_id_to_node.get(db_id) {
index_to_node.insert(idx, node_idx);
}
}
if !cfg_edges.is_empty() {
build_edges_from_cfg_edges(&mut graph, &cfg_edges, &index_to_node)
.context("Failed to build edges from cfg_edges")?;
} else {
build_edges_from_terminators(&mut graph, &block_rows, &db_id_to_node)
.context("Failed to build edges from terminator data")?;
}
Ok(graph)
}
pub fn resolve_function_name(db: &MirageDb, name_or_id: &str) -> Result<i64> {
db.resolve_function_name(name_or_id)
}
pub fn resolve_function_name_with_file(
db: &MirageDb,
name_or_id: &str,
file_filter: Option<&str>,
) -> Result<i64> {
db.resolve_function_name_with_file(name_or_id, file_filter)
}
pub fn get_function_name_db(db: &MirageDb, function_id: i64) -> Option<String> {
db.get_function_name(function_id)
}
pub fn get_function_file_db(db: &MirageDb, function_id: i64) -> Option<String> {
db.get_function_file(function_id)
}
pub fn get_function_hash_db(db: &MirageDb, function_id: i64) -> Option<String> {
db.get_function_hash(function_id)
}
#[cfg(feature = "backend-sqlite")]
pub fn resolve_function_name_with_conn(conn: &Connection, name_or_id: &str) -> Result<i64> {
if let Ok(id) = name_or_id.parse::<i64>() {
return Ok(id);
}
let function_id: Option<i64> = conn
.query_row(
"SELECT id FROM graph_entities
WHERE kind = 'Symbol'
AND json_extract(data, '$.kind') = 'Function'
AND name = ?
LIMIT 1",
params![name_or_id],
|row| row.get(0),
)
.optional()
.context(format!(
"Failed to query function with name '{}'",
name_or_id
))?;
function_id.context(format!(
"Function '{}' not found in database. Run 'magellan watch' to index functions.",
name_or_id
))
}
pub fn load_cfg_from_db(db: &MirageDb, function_id: i64) -> Result<crate::cfg::Cfg> {
db.load_cfg(function_id)
}
#[cfg(feature = "backend-sqlite")]
pub fn load_cfg_from_db_with_conn(conn: &Connection, function_id: i64) -> Result<crate::cfg::Cfg> {
load_cfg_from_sqlite(conn, function_id)
}
#[deprecated(note = "Magellan handles CFG storage via cfg_blocks. Edges are computed in memory.")]
pub fn store_cfg(
conn: &mut Connection,
function_id: i64,
_function_hash: &str, cfg: &crate::cfg::Cfg,
) -> Result<()> {
use crate::cfg::{BlockKind, Terminator};
conn.execute("BEGIN IMMEDIATE TRANSACTION", [])
.context("Failed to begin transaction")?;
conn.execute(
"DELETE FROM cfg_blocks WHERE function_id = ?",
params![function_id],
)
.context("Failed to clear existing cfg_blocks")?;
let mut block_id_map: std::collections::HashMap<petgraph::graph::NodeIndex, i64> =
std::collections::HashMap::new();
let mut insert_block = conn
.prepare_cached(
"INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
start_line, start_col, end_line, end_col,
coord_x, coord_y, coord_z)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
)
.context("Failed to prepare block insert statement")?;
for node_idx in cfg.node_indices() {
let block = cfg
.node_weight(node_idx)
.context("CFG node has no weight")?;
let terminator_str = match &block.terminator {
Terminator::Goto { .. } => "goto",
Terminator::SwitchInt { .. } => "conditional",
Terminator::Return => "return",
Terminator::Call { .. } => "call",
Terminator::Abort(msg) if msg == "break" => "break",
Terminator::Abort(msg) if msg == "continue" => "continue",
Terminator::Abort(msg) if msg == "panic" => "panic",
_ => "fallthrough",
};
let (byte_start, byte_end) = block
.source_location
.as_ref()
.map(|loc| (Some(loc.byte_start as i64), Some(loc.byte_end as i64)))
.unwrap_or((None, None));
let (start_line, start_col, end_line, end_col) = block
.source_location
.as_ref()
.map(|loc| {
(
Some(loc.start_line as i64),
Some(loc.start_column as i64),
Some(loc.end_line as i64),
Some(loc.end_column as i64),
)
})
.unwrap_or((None, None, None, None));
let kind = match block.kind {
BlockKind::Entry => "entry",
BlockKind::Normal => "block",
BlockKind::Exit => "return",
};
insert_block
.execute(params![
function_id,
kind,
terminator_str,
byte_start,
byte_end,
start_line,
start_col,
end_line,
end_col,
block.coord_x,
block.coord_y,
block.coord_z,
])
.context("Failed to insert cfg_block")?;
let db_id = conn.last_insert_rowid();
block_id_map.insert(node_idx, db_id);
}
conn.execute("COMMIT", [])
.context("Failed to commit transaction")?;
Ok(())
}
pub fn function_exists(conn: &Connection, function_id: i64) -> bool {
conn.query_row(
"SELECT COUNT(*) FROM cfg_blocks WHERE function_id = ?",
params![function_id],
|row| row.get::<_, i64>(0).map(|count| count > 0),
)
.optional()
.ok()
.flatten()
.unwrap_or(false)
}
pub fn get_function_hash(conn: &Connection, function_id: i64) -> Option<String> {
let cfg_hash: Option<String> = conn
.query_row(
"SELECT cfg_hash FROM cfg_blocks WHERE function_id = ? LIMIT 1",
params![function_id],
|row| row.get(0),
)
.optional()
.ok()
.flatten();
if cfg_hash.is_some() {
return cfg_hash;
}
conn.query_row(
"SELECT json_extract(data, '$.symbol_id') FROM graph_entities WHERE id = ? LIMIT 1",
params![function_id],
|row| row.get::<_, Option<String>>(0),
)
.optional()
.ok()
.flatten()
.flatten()
}
pub fn hash_changed(conn: &Connection, function_id: i64, _new_hash: &str) -> Result<bool> {
let old_hash: Option<String> = conn
.query_row(
"SELECT cfg_hash FROM cfg_blocks WHERE function_id = ? LIMIT 1",
params![function_id],
|row| row.get(0),
)
.optional()?;
match old_hash {
Some(old) => Ok(old != _new_hash),
None => Ok(true), }
}
pub fn get_changed_functions(
conn: &Connection,
project_path: &std::path::Path,
) -> Result<std::collections::HashSet<String>> {
use std::collections::HashSet;
use std::process::Command;
let mut changed = HashSet::new();
if let Ok(git_output) = Command::new("git")
.args(["diff", "--name-only", "HEAD"])
.current_dir(project_path)
.output()
{
let git_files = String::from_utf8_lossy(&git_output.stdout);
let changed_rs_files: Vec<&str> =
git_files.lines().filter(|f| f.ends_with(".rs")).collect();
if changed_rs_files.is_empty() {
return Ok(changed);
}
for file in changed_rs_files {
let normalized_path = if file.starts_with('/') {
file.trim_start_matches('/')
} else {
file
};
let mut stmt = conn
.prepare_cached(
"SELECT name FROM graph_entities
WHERE kind = 'function' AND (
file_path = ? OR
file_path = ? OR
file_path LIKE '%' || ?
)",
)
.context("Failed to prepare function lookup query")?;
let with_slash = format!("/{}", normalized_path);
let rows = stmt
.query_map(
params![normalized_path, &with_slash, normalized_path],
|row| row.get::<_, String>(0),
)
.context("Failed to execute function lookup")?;
for row in rows {
if let Ok(func_name) = row {
changed.insert(func_name);
}
}
}
}
Ok(changed)
}
pub fn get_function_file(conn: &Connection, function_name: &str) -> Result<Option<String>> {
let file: Option<String> = conn
.query_row(
"SELECT file_path FROM graph_entities WHERE kind = 'function' AND name = ? LIMIT 1",
params![function_name],
|row| row.get(0),
)
.optional()?;
Ok(file)
}
pub fn get_function_name(conn: &Connection, function_id: i64) -> Option<String> {
conn.query_row(
"SELECT name FROM graph_entities WHERE id = ?",
params![function_id],
|row| row.get(0),
)
.optional()
.ok()
.flatten()
}
pub fn get_path_elements(conn: &Connection, path_id: &str) -> Result<Vec<crate::cfg::BlockId>> {
let mut stmt = conn
.prepare_cached(
"SELECT block_id FROM cfg_path_elements
WHERE path_id = ?
ORDER BY sequence_order ASC",
)
.context("Failed to prepare path elements query")?;
let blocks: Vec<crate::cfg::BlockId> = stmt
.query_map(params![path_id], |row| Ok(row.get::<_, i64>(0)? as usize))
.context("Failed to execute path elements query")?
.collect::<Result<Vec<_>, _>>()
.context("Failed to collect path elements")?;
if blocks.is_empty() {
anyhow::bail!("Path '{}' not found in cache", path_id);
}
Ok(blocks)
}
pub fn compute_path_impact_from_db(
conn: &Connection,
path_id: &str,
cfg: &crate::cfg::Cfg,
max_depth: Option<usize>,
) -> Result<crate::cfg::PathImpact> {
let path_blocks = get_path_elements(conn, path_id)?;
let mut impact = crate::cfg::compute_path_impact(cfg, &path_blocks, max_depth);
impact.path_id = path_id.to_string();
Ok(impact)
}
pub fn create_minimal_database<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref();
if path.exists() {
anyhow::bail!("Database already exists: {}", path.display());
}
let mut conn = Connection::open(path).context("Failed to create database file")?;
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)
.context("Failed to create magellan_meta table")?;
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
name TEXT NOT NULL,
file_path TEXT,
data TEXT NOT NULL
)",
[],
)
.context("Failed to create graph_entities table")?;
conn.execute(
"CREATE INDEX idx_graph_entities_kind ON graph_entities(kind)",
[],
)
.context("Failed to create index on graph_entities.kind")?;
conn.execute(
"CREATE INDEX idx_graph_entities_name ON graph_entities(name)",
[],
)
.context("Failed to create index on graph_entities.name")?;
let now = chrono::Utc::now().timestamp();
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, ?, ?, ?)",
params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, now],
).context("Failed to initialize magellan_meta")?;
create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION)
.context("Failed to create Mirage schema")?;
Ok(())
}
#[cfg(all(test, feature = "sqlite"))]
mod tests {
use super::*;
#[test]
fn test_create_schema() {
let mut conn = Connection::open_in_memory().unwrap();
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
name TEXT NOT NULL,
file_path TEXT,
data TEXT NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, ?, ?, ?)",
params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
).unwrap();
create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
let table_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name LIKE 'cfg_%'",
[],
|row| row.get(0),
)
.unwrap();
assert!(table_count >= 4); }
#[test]
fn test_migrate_schema_from_version_0() {
let mut conn = Connection::open_in_memory().unwrap();
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
name TEXT NOT NULL,
file_path TEXT,
data TEXT NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, ?, ?, ?)",
params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
).unwrap();
create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
let version: i32 = conn
.query_row(
"SELECT mirage_schema_version FROM mirage_meta WHERE id = 1",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(version, MIRAGE_SCHEMA_VERSION);
}
#[test]
fn test_migrate_schema_no_op_when_current() {
let mut conn = Connection::open_in_memory().unwrap();
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
name TEXT NOT NULL,
file_path TEXT,
data TEXT NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, ?, ?, ?)",
params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
).unwrap();
create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
migrate_schema(&mut conn).unwrap();
let version: i32 = conn
.query_row(
"SELECT mirage_schema_version FROM mirage_meta WHERE id = 1",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(version, MIRAGE_SCHEMA_VERSION);
}
#[test]
fn test_fk_constraint_cfg_blocks() {
let mut conn = Connection::open_in_memory().unwrap();
conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
name TEXT NOT NULL,
file_path TEXT,
data TEXT NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, ?, ?, ?)",
params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
).unwrap();
create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
params!("function", "test_func", "test.rs", "{}"),
)
.unwrap();
let function_id: i64 = conn.last_insert_rowid();
let invalid_result = conn.execute(
"INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
start_line, start_col, end_line, end_col)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
params!(9999, "entry", "return", 0, 10, 1, 0, 1, 10),
);
assert!(
invalid_result.is_err(),
"Insert with invalid function_id should fail"
);
let valid_result = conn.execute(
"INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
start_line, start_col, end_line, end_col)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
params!(function_id, "entry", "return", 0, 10, 1, 0, 1, 10),
);
assert!(
valid_result.is_ok(),
"Insert with valid function_id should succeed"
);
let count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM cfg_blocks WHERE function_id = ?",
params![function_id],
|row| row.get(0),
)
.unwrap();
assert_eq!(count, 1, "Should have exactly one cfg_block entry");
}
#[test]
fn test_store_cfg_retrieves_correctly() {
use crate::cfg::{BasicBlock, BlockKind, Cfg, EdgeType, Terminator};
let mut conn = Connection::open_in_memory().unwrap();
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
name TEXT NOT NULL,
file_path TEXT,
data TEXT NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, ?, ?, ?)",
params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
).unwrap();
create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
params!("function", "test_func", "test.rs", "{}"),
)
.unwrap();
let function_id: i64 = conn.last_insert_rowid();
let mut cfg = Cfg::new();
let b0 = cfg.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec!["let x = 1".to_string()],
terminator: Terminator::Goto { target: 1 },
source_location: None,
});
let b1 = cfg.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
});
cfg.add_edge(b0, b1, EdgeType::Fallthrough);
store_cfg(&mut conn, function_id, "test_hash_123", &cfg).unwrap();
let block_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM cfg_blocks WHERE function_id = ?",
params![function_id],
|row| row.get(0),
)
.unwrap();
assert_eq!(block_count, 2, "Should have 2 blocks");
assert!(function_exists(&conn, function_id));
assert!(!function_exists(&conn, 9999));
let loaded_cfg = load_cfg_from_db_with_conn(&conn, function_id).unwrap();
assert_eq!(loaded_cfg.node_count(), 2);
assert_eq!(loaded_cfg.edge_count(), 1);
}
#[test]
fn test_store_cfg_incremental_update_clears_old_data() {
use crate::cfg::{BasicBlock, BlockKind, Cfg, EdgeType, Terminator};
let mut conn = Connection::open_in_memory().unwrap();
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
name TEXT NOT NULL,
file_path TEXT,
data TEXT NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, ?, ?, ?)",
params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
).unwrap();
create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
params!("function", "test_func", "test.rs", "{}"),
)
.unwrap();
let function_id: i64 = conn.last_insert_rowid();
let mut cfg1 = Cfg::new();
let b0 = cfg1.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
});
let b1 = cfg1.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
});
cfg1.add_edge(b0, b1, EdgeType::Fallthrough);
store_cfg(&mut conn, function_id, "hash_v1", &cfg1).unwrap();
let block_count_v1: i64 = conn
.query_row(
"SELECT COUNT(*) FROM cfg_blocks WHERE function_id = ?",
params![function_id],
|row| row.get(0),
)
.unwrap();
assert_eq!(block_count_v1, 2);
let mut cfg2 = Cfg::new();
let b0 = cfg2.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
});
let b1 = cfg2.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 2 },
source_location: None,
});
let b2 = cfg2.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
});
cfg2.add_edge(b0, b1, EdgeType::Fallthrough);
cfg2.add_edge(b1, b2, EdgeType::Fallthrough);
store_cfg(&mut conn, function_id, "hash_v3", &cfg2).unwrap();
let block_count_v3: i64 = conn
.query_row(
"SELECT COUNT(*) FROM cfg_blocks WHERE function_id = ?",
params![function_id],
|row| row.get(0),
)
.unwrap();
assert_eq!(block_count_v3, 3);
}
fn create_test_db_with_schema() -> Connection {
let mut conn = Connection::open_in_memory().unwrap();
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
name TEXT NOT NULL,
file_path TEXT,
data TEXT NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, ?, ?, ?)",
params![7, 3, 0], ).unwrap();
conn.execute(
"CREATE TABLE cfg_blocks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
function_id INTEGER NOT NULL,
kind TEXT NOT NULL,
terminator TEXT NOT NULL,
byte_start INTEGER NOT NULL,
byte_end INTEGER NOT NULL,
start_line INTEGER NOT NULL,
start_col INTEGER NOT NULL,
end_line INTEGER NOT NULL,
end_col INTEGER NOT NULL,
coord_x INTEGER NOT NULL DEFAULT 0,
coord_y INTEGER NOT NULL DEFAULT 0,
coord_z INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (function_id) REFERENCES graph_entities(id)
)",
[],
)
.unwrap();
conn.execute(
"CREATE TABLE graph_edges (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_id INTEGER NOT NULL,
to_id INTEGER NOT NULL,
edge_type TEXT NOT NULL,
data TEXT
)",
[],
)
.unwrap();
create_schema(&mut conn, TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
conn
}
#[test]
fn test_resolve_function_by_id() {
let conn = create_test_db_with_schema();
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
params!("function", "my_func", "test.rs", "{}"),
)
.unwrap();
let function_id: i64 = conn.last_insert_rowid();
let result = resolve_function_name_with_conn(&conn, &function_id.to_string()).unwrap();
assert_eq!(result, function_id);
}
#[test]
fn test_resolve_function_by_name() {
let conn = create_test_db_with_schema();
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
params!(
"Symbol",
"test_function",
"test.rs",
r#"{"kind":"Function"}"#
),
)
.unwrap();
let function_id: i64 = conn.last_insert_rowid();
let result = resolve_function_name_with_conn(&conn, "test_function").unwrap();
assert_eq!(result, function_id);
}
#[test]
fn test_resolve_function_not_found() {
let conn = create_test_db_with_schema();
let result = resolve_function_name_with_conn(&conn, "nonexistent_func");
assert!(
result.is_err(),
"Should return error for non-existent function"
);
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("not found") || err_msg.contains("not found in database"));
}
#[test]
fn test_resolve_function_numeric_string() {
let conn = create_test_db_with_schema();
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
params!("function", "func123", "test.rs", "{}"),
)
.unwrap();
let result = resolve_function_name_with_conn(&conn, "123").unwrap();
assert_eq!(result, 123);
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
params!("function", "another_func", "test.rs", "{}"),
)
.unwrap();
let _id_456 = conn.last_insert_rowid();
let result = resolve_function_name_with_conn(&conn, "999").unwrap();
assert_eq!(result, 999, "Should return numeric ID directly");
}
#[test]
fn test_load_cfg_not_found() {
let conn = create_test_db_with_schema();
let result = load_cfg_from_db_with_conn(&conn, 99999);
assert!(
result.is_err(),
"Should return error for function with no CFG"
);
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("No CFG blocks found") || err_msg.contains("not found"));
}
#[test]
fn test_load_cfg_empty_terminator() {
use crate::cfg::Terminator;
let conn = create_test_db_with_schema();
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
params!("function", "empty_term_func", "test.rs", "{}"),
)
.unwrap();
let function_id: i64 = conn.last_insert_rowid();
conn.execute(
"INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
start_line, start_col, end_line, end_col)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
params!(function_id, "return", "return", 0, 10, 1, 0, 1, 10),
)
.unwrap();
let cfg = load_cfg_from_db_with_conn(&conn, function_id).unwrap();
assert_eq!(cfg.node_count(), 1);
let block = &cfg[petgraph::graph::NodeIndex::new(0)];
assert!(matches!(block.terminator, Terminator::Return));
}
#[test]
fn test_load_cfg_with_multiple_edge_types() {
use crate::cfg::EdgeType;
let conn = create_test_db_with_schema();
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
params!("function", "edge_types_func", "test.rs", "{}"),
)
.unwrap();
let function_id: i64 = conn.last_insert_rowid();
conn.execute(
"INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
start_line, start_col, end_line, end_col)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
params!(function_id, "entry", "conditional", 0, 10, 1, 0, 1, 10),
)
.unwrap();
let _block_0_id: i64 = conn.last_insert_rowid();
conn.execute(
"INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
start_line, start_col, end_line, end_col)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
params!(function_id, "block", "fallthrough", 10, 20, 2, 0, 2, 10),
)
.unwrap();
let _block_1_id: i64 = conn.last_insert_rowid();
conn.execute(
"INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
start_line, start_col, end_line, end_col)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
params!(function_id, "block", "call", 20, 30, 3, 0, 3, 10),
)
.unwrap();
let _block_2_id: i64 = conn.last_insert_rowid();
conn.execute(
"INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end,
start_line, start_col, end_line, end_col)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
params!(function_id, "return", "return", 30, 40, 4, 0, 4, 10),
)
.unwrap();
let _block_3_id: i64 = conn.last_insert_rowid();
let cfg = load_cfg_from_db_with_conn(&conn, function_id).unwrap();
assert_eq!(cfg.node_count(), 4);
assert_eq!(cfg.edge_count(), 4);
use petgraph::visit::EdgeRef;
let edges: Vec<_> = cfg
.edge_references()
.map(|e| (e.source().index(), e.target().index(), *e.weight()))
.collect();
assert!(edges.contains(&(0, 1, EdgeType::TrueBranch)));
assert!(edges.contains(&(0, 2, EdgeType::FalseBranch)));
assert!(edges.contains(&(1, 2, EdgeType::Fallthrough)));
assert!(edges.contains(&(2, 3, EdgeType::Call)));
}
#[test]
fn test_get_function_name() {
let conn = create_test_db_with_schema();
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
params!("function", "my_test_func", "test.rs", "{}"),
)
.unwrap();
let function_id: i64 = conn.last_insert_rowid();
let name = get_function_name(&conn, function_id);
assert_eq!(name, Some("my_test_func".to_string()));
let name = get_function_name(&conn, 9999);
assert_eq!(name, None);
}
#[test]
fn test_get_path_elements() {
let conn = create_test_db_with_schema();
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
params!("function", "path_test_func", "test.rs", "{}"),
)
.unwrap();
let function_id: i64 = conn.last_insert_rowid();
conn.execute(
"INSERT INTO cfg_paths (path_id, function_id, path_kind, entry_block, exit_block, length, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)",
params!("test_path_abc123", function_id, "normal", 0, 2, 3, 1000),
).unwrap();
conn.execute(
"INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
params!("test_path_abc123", 0, 0),
)
.unwrap();
conn.execute(
"INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
params!("test_path_abc123", 1, 1),
)
.unwrap();
conn.execute(
"INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
params!("test_path_abc123", 2, 2),
)
.unwrap();
let blocks = get_path_elements(&conn, "test_path_abc123").unwrap();
assert_eq!(blocks, vec![0, 1, 2]);
let result = get_path_elements(&conn, "nonexistent_path");
assert!(result.is_err());
}
#[test]
fn test_compute_path_impact_from_db() {
use crate::cfg::{BasicBlock, BlockKind, Terminator};
let conn = create_test_db_with_schema();
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
params!("function", "impact_test_func", "test.rs", "{}"),
)
.unwrap();
let function_id: i64 = conn.last_insert_rowid();
let mut cfg = crate::cfg::Cfg::new();
let b0 = cfg.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
});
let b1 = cfg.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 2 },
source_location: None,
});
let b2 = cfg.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 3 },
source_location: None,
});
let b3 = cfg.add_node(BasicBlock {
id: 3,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
});
cfg.add_edge(b0, b1, crate::cfg::EdgeType::Fallthrough);
cfg.add_edge(b1, b2, crate::cfg::EdgeType::Fallthrough);
cfg.add_edge(b2, b3, crate::cfg::EdgeType::Fallthrough);
conn.execute(
"INSERT INTO cfg_paths (path_id, function_id, path_kind, entry_block, exit_block, length, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)",
params!("impact_test_path", function_id, "normal", 0, 3, 3, 1000),
).unwrap();
conn.execute(
"INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
params!("impact_test_path", 0, 0),
)
.unwrap();
conn.execute(
"INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
params!("impact_test_path", 1, 1),
)
.unwrap();
conn.execute(
"INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
params!("impact_test_path", 2, 3),
)
.unwrap();
let impact = compute_path_impact_from_db(&conn, "impact_test_path", &cfg, None).unwrap();
assert_eq!(impact.path_id, "impact_test_path");
assert_eq!(impact.path_length, 3);
assert!(impact.unique_blocks_affected.contains(&2));
}
#[test]
fn test_load_cfg_missing_cfg_blocks_table() {
let conn = Connection::open_in_memory().unwrap();
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
name TEXT NOT NULL,
file_path TEXT,
data TEXT NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, ?, ?, ?)",
params![6, 3, 0], ).unwrap();
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
params!("function", "test_func", "test.rs", "{}"),
)
.unwrap();
let function_id: i64 = conn.last_insert_rowid();
let result = load_cfg_from_db_with_conn(&conn, function_id);
assert!(result.is_err(), "Should fail when cfg_blocks table missing");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("cfg_blocks") || err_msg.contains("prepare"),
"Error should mention cfg_blocks or prepare: {}",
err_msg
);
}
#[test]
fn test_load_cfg_function_not_found() {
let conn = create_test_db_with_schema();
let result = load_cfg_from_db_with_conn(&conn, 99999);
assert!(result.is_err(), "Should fail for non-existent function");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("No CFG blocks found") || err_msg.contains("not found"),
"Error should mention missing CFG: {}",
err_msg
);
assert!(
err_msg.contains("magellan watch"),
"Error should suggest running magellan watch: {}",
err_msg
);
}
#[test]
fn test_load_cfg_empty_blocks() {
let conn = create_test_db_with_schema();
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
params!("function", "func_without_cfg", "test.rs", "{}"),
)
.unwrap();
let function_id: i64 = conn.last_insert_rowid();
let result = load_cfg_from_db_with_conn(&conn, function_id);
assert!(result.is_err(), "Should fail when no CFG blocks exist");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("No CFG blocks found"),
"Error should mention no CFG blocks: {}",
err_msg
);
assert!(
err_msg.contains("magellan watch"),
"Error should suggest running magellan watch: {}",
err_msg
);
}
#[test]
fn test_resolve_function_missing_with_helpful_message() {
let conn = create_test_db_with_schema();
let result = resolve_function_name_with_conn(&conn, "nonexistent_function");
assert!(result.is_err(), "Should fail for non-existent function");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("not found") || err_msg.contains("not found in database"),
"Error should mention function not found: {}",
err_msg
);
}
#[test]
fn test_open_database_old_magellan_schema() {
let conn = Connection::open_in_memory().unwrap();
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, 6, 3, 0)", [],
).unwrap();
conn.execute(
"CREATE TABLE cfg_blocks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
function_id INTEGER NOT NULL,
kind TEXT NOT NULL,
terminator TEXT NOT NULL,
byte_start INTEGER NOT NULL,
byte_end INTEGER NOT NULL,
start_line INTEGER NOT NULL,
start_col INTEGER NOT NULL,
end_line INTEGER NOT NULL,
end_col INTEGER NOT NULL,
FOREIGN KEY (function_id) REFERENCES graph_entities(id)
)",
[],
)
.unwrap();
drop(conn);
let db_file = tempfile::NamedTempFile::new().unwrap();
{
let conn = Connection::open(db_file.path()).unwrap();
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, 6, 3, 0)",
[],
).unwrap();
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
name TEXT NOT NULL,
file_path TEXT,
data TEXT NOT NULL
)",
[],
)
.unwrap();
}
let result = MirageDb::open(db_file.path());
assert!(result.is_err(), "Should fail with old Magellan schema");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("too old") || err_msg.contains("minimum"),
"Error should mention schema too old: {}",
err_msg
);
assert!(
err_msg.contains("magellan watch"),
"Error should suggest running magellan watch: {}",
err_msg
);
}
#[test]
fn test_backend_detect_sqlite_header() {
use std::io::Write;
let temp_file = tempfile::NamedTempFile::new().unwrap();
let mut file = std::fs::File::create(temp_file.path()).unwrap();
file.write_all(b"SQLite format 3\0").unwrap();
file.sync_all().unwrap();
let backend = BackendFormat::detect(temp_file.path()).unwrap();
assert_eq!(
backend,
BackendFormat::SQLite,
"Should detect SQLite format"
);
}
#[test]
fn test_backend_detect_nonexistent_file() {
let backend = BackendFormat::detect(Path::new("/nonexistent/path/to/file.db")).unwrap();
assert_eq!(
backend,
BackendFormat::Unknown,
"Non-existent file should be Unknown"
);
}
#[test]
fn test_backend_detect_empty_file() {
let temp_file = tempfile::NamedTempFile::new().unwrap();
let backend = BackendFormat::detect(temp_file.path()).unwrap();
assert_eq!(
backend,
BackendFormat::Unknown,
"Empty file should be Unknown"
);
}
#[test]
fn test_backend_detect_partial_header() {
use std::io::Write;
let temp_file = tempfile::NamedTempFile::new().unwrap();
let mut file = std::fs::File::create(temp_file.path()).unwrap();
file.write_all(b"SQLite").unwrap(); file.sync_all().unwrap();
let backend = BackendFormat::detect(temp_file.path()).unwrap();
assert_eq!(
backend,
BackendFormat::Unknown,
"Partial header should be Unknown"
);
}
#[test]
fn test_backend_equality() {
assert_eq!(BackendFormat::SQLite, BackendFormat::SQLite);
assert_eq!(BackendFormat::Unknown, BackendFormat::Unknown);
assert_ne!(BackendFormat::SQLite, BackendFormat::Unknown);
}
}