use anyhow::{Context, Result};
use rusqlite::{params, Connection, OptionalExtension};
use super::MirageDb;
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 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 func_name in rows.flatten() {
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 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(all(test, feature = "sqlite"))]
mod tests {
use super::*;
use rusqlite::Connection;
fn create_test_db_with_schema() -> Connection {
crate::storage::schema::create_test_db_with_schema()
}
fn test_get_function_name() {
let conn = create_test_db_with_schema();
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
rusqlite::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 (?, ?, ?, ?)",
rusqlite::params!("function", "path_test_func", "test.rs", "{}"),
)
.unwrap();
let function_id: i64 = conn.last_insert_rowid();
conn.execute(
"INSERT INTO cfgPaths (path_id, function_id, path_kind, entry_block, exit_block, length, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)",
rusqlite::params!("test_path_abc123", function_id, "normal", 0, 2, 3, 1000),
).unwrap_or_else(|_e| {
conn.execute(
"INSERT INTO cfg_paths (path_id, function_id, path_kind, entry_block, exit_block, length, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)",
rusqlite::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 (?, ?, ?)",
rusqlite::params!("test_path_abc123", 0, 0),
)
.unwrap();
conn.execute(
"INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
rusqlite::params!("test_path_abc123", 1, 1),
)
.unwrap();
conn.execute(
"INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
rusqlite::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 (?, ?, ?, ?)",
rusqlite::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 (?, ?, ?, ?, ?, ?, ?)",
rusqlite::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 (?, ?, ?)",
rusqlite::params!("impact_test_path", 0, 0),
)
.unwrap();
conn.execute(
"INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
rusqlite::params!("impact_test_path", 1, 1),
)
.unwrap();
conn.execute(
"INSERT INTO cfg_path_elements (path_id, sequence_order, block_id) VALUES (?, ?, ?)",
rusqlite::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));
}
}