use rusqlite::{Connection, OptionalExtension, params};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NodeKind {
File,
Symbol,
Module,
Commit,
Test,
CIRun,
Knowledge,
Issue,
}
impl NodeKind {
pub fn as_str(&self) -> &'static str {
match self {
Self::File => "file",
Self::Symbol => "symbol",
Self::Module => "module",
Self::Commit => "commit",
Self::Test => "test",
Self::CIRun => "ci_run",
Self::Knowledge => "knowledge",
Self::Issue => "issue",
}
}
pub fn parse(s: &str) -> Self {
match s {
"symbol" => Self::Symbol,
"module" => Self::Module,
"commit" => Self::Commit,
"test" => Self::Test,
"ci_run" => Self::CIRun,
"knowledge" => Self::Knowledge,
"issue" => Self::Issue,
_ => Self::File,
}
}
}
#[derive(Debug, Clone)]
pub struct Node {
pub id: Option<i64>,
pub kind: NodeKind,
pub name: String,
pub file_path: String,
pub line_start: Option<usize>,
pub line_end: Option<usize>,
pub metadata: Option<String>,
}
impl Node {
pub fn file(path: &str) -> Self {
Self {
id: None,
kind: NodeKind::File,
name: path.to_string(),
file_path: path.to_string(),
line_start: None,
line_end: None,
metadata: None,
}
}
pub fn symbol(name: &str, file_path: &str, kind: NodeKind) -> Self {
Self {
id: None,
kind,
name: name.to_string(),
file_path: file_path.to_string(),
line_start: None,
line_end: None,
metadata: None,
}
}
pub fn with_lines(mut self, start: usize, end: usize) -> Self {
self.line_start = Some(start);
self.line_end = Some(end);
self
}
pub fn with_metadata(mut self, meta: &str) -> Self {
self.metadata = Some(meta.to_string());
self
}
pub fn commit(hash: &str, message: &str) -> Self {
Self {
id: None,
kind: NodeKind::Commit,
name: hash.to_string(),
file_path: String::new(),
line_start: None,
line_end: None,
metadata: Some(message.to_string()),
}
}
pub fn test(path: &str, test_name: &str) -> Self {
Self {
id: None,
kind: NodeKind::Test,
name: test_name.to_string(),
file_path: path.to_string(),
line_start: None,
line_end: None,
metadata: None,
}
}
pub fn knowledge(id: &str, summary: &str) -> Self {
Self {
id: None,
kind: NodeKind::Knowledge,
name: id.to_string(),
file_path: String::new(),
line_start: None,
line_end: None,
metadata: Some(summary.to_string()),
}
}
pub fn issue(id: &str, title: &str) -> Self {
Self {
id: None,
kind: NodeKind::Issue,
name: id.to_string(),
file_path: String::new(),
line_start: None,
line_end: None,
metadata: Some(title.to_string()),
}
}
}
pub(super) fn upsert(conn: &Connection, node: &Node) -> anyhow::Result<i64> {
let file_id = super::path_id::intern(conn, &node.file_path)?;
conn.execute(
"INSERT INTO nodes (kind, name, file_id, line_start, line_end, metadata)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
ON CONFLICT(kind, name, file_id) DO UPDATE SET
line_start = excluded.line_start,
line_end = excluded.line_end,
metadata = excluded.metadata",
params![
node.kind.as_str(),
node.name,
file_id.raw(),
node.line_start.map(|v| v as i64),
node.line_end.map(|v| v as i64),
node.metadata,
],
)?;
let id: i64 = conn.query_row(
"SELECT id FROM nodes WHERE kind = ?1 AND name = ?2 AND file_id = ?3",
params![node.kind.as_str(), node.name, file_id.raw()],
|row| row.get(0),
)?;
Ok(id)
}
pub(super) fn get_by_path(conn: &Connection, file_path: &str) -> anyhow::Result<Option<Node>> {
let result = conn
.query_row(
"SELECT n.id, n.kind, n.name, p.path, n.line_start, n.line_end, n.metadata
FROM nodes n JOIN paths p ON p.id = n.file_id
WHERE n.kind = 'file' AND p.path = ?1",
params![file_path],
|row| {
Ok(Node {
id: Some(row.get(0)?),
kind: NodeKind::parse(&row.get::<_, String>(1)?),
name: row.get(2)?,
file_path: row.get(3)?,
line_start: row.get::<_, Option<i64>>(4)?.map(|v| v as usize),
line_end: row.get::<_, Option<i64>>(5)?.map(|v| v as usize),
metadata: row.get(6)?,
})
},
)
.optional()?;
Ok(result)
}
pub(super) fn get_by_symbol(
conn: &Connection,
name: &str,
file_path: &str,
) -> anyhow::Result<Option<Node>> {
let result = conn
.query_row(
"SELECT n.id, n.kind, n.name, p.path, n.line_start, n.line_end, n.metadata
FROM nodes n JOIN paths p ON p.id = n.file_id
WHERE n.name = ?1 AND p.path = ?2 AND n.kind != 'file'",
params![name, file_path],
|row| {
Ok(Node {
id: Some(row.get(0)?),
kind: NodeKind::parse(&row.get::<_, String>(1)?),
name: row.get(2)?,
file_path: row.get(3)?,
line_start: row.get::<_, Option<i64>>(4)?.map(|v| v as usize),
line_end: row.get::<_, Option<i64>>(5)?.map(|v| v as usize),
metadata: row.get(6)?,
})
},
)
.optional()?;
Ok(result)
}
pub(super) fn remove_by_file(conn: &Connection, file_path: &str) -> anyhow::Result<()> {
conn.execute(
"DELETE FROM edges WHERE source_id IN (
SELECT n.id FROM nodes n JOIN paths p ON p.id = n.file_id WHERE p.path = ?1
) OR target_id IN (
SELECT n.id FROM nodes n JOIN paths p ON p.id = n.file_id WHERE p.path = ?1
)",
params![file_path],
)?;
conn.execute(
"DELETE FROM nodes WHERE file_id = (SELECT id FROM paths WHERE path = ?1)",
params![file_path],
)?;
Ok(())
}
pub(super) fn find_symbols(
conn: &Connection,
name: &str,
file_filter: Option<&str>,
kind_filter: Option<&str>,
) -> anyhow::Result<Vec<Node>> {
let name_lower = name.to_lowercase();
let mut sql = String::from(
"SELECT n.id, n.kind, n.name, p.path, n.line_start, n.line_end, n.metadata
FROM nodes n JOIN paths p ON p.id = n.file_id WHERE n.kind != 'file'
AND LOWER(name) LIKE '%' || ?1 || '%'",
);
let mut param_idx = 2;
if file_filter.is_some() {
sql.push_str(&format!(" AND p.path LIKE '%' || ?{param_idx} || '%'"));
param_idx += 1;
}
if kind_filter.is_some() {
sql.push_str(&format!(" AND kind = ?{param_idx}"));
}
sql.push_str(" ORDER BY p.path, n.line_start LIMIT 100");
let mut stmt = conn.prepare(&sql)?;
let params_vec: Vec<Box<dyn rusqlite::types::ToSql>> = {
let mut v: Vec<Box<dyn rusqlite::types::ToSql>> = vec![Box::new(name_lower)];
if let Some(f) = file_filter {
v.push(Box::new(f.to_string()));
}
if let Some(k) = kind_filter {
v.push(Box::new(k.to_string()));
}
v
};
let refs: Vec<&dyn rusqlite::types::ToSql> =
params_vec.iter().map(std::convert::AsRef::as_ref).collect();
let rows = stmt.query_map(refs.as_slice(), |row| {
Ok(Node {
id: Some(row.get(0)?),
kind: NodeKind::parse(&row.get::<_, String>(1)?),
name: row.get(2)?,
file_path: row.get(3)?,
line_start: row.get::<_, Option<i64>>(4)?.map(|v| v as usize),
line_end: row.get::<_, Option<i64>>(5)?.map(|v| v as usize),
metadata: row.get(6)?,
})
})?;
let mut results = Vec::new();
for r in rows {
results.push(r?);
}
Ok(results)
}
pub(super) fn resolve_symbol_def_files(
conn: &Connection,
name: &str,
) -> anyhow::Result<Vec<String>> {
let mut stmt = conn.prepare(
"SELECT DISTINCT p.path FROM nodes n JOIN paths p ON p.id = n.file_id
WHERE n.kind = 'symbol' AND n.name = ?1 AND p.path != ''
ORDER BY p.path",
)?;
let rows = stmt.query_map(params![name], |row| row.get::<_, String>(0))?;
let mut out = Vec::new();
for r in rows {
out.push(r?);
}
Ok(out)
}
pub(super) fn all_symbols(conn: &Connection) -> anyhow::Result<Vec<Node>> {
let mut stmt = conn.prepare(
"SELECT n.id, n.kind, n.name, p.path, n.line_start, n.line_end, n.metadata
FROM nodes n JOIN paths p ON p.id = n.file_id WHERE n.kind != 'file'
ORDER BY p.path, n.line_start",
)?;
let rows = stmt.query_map([], |row| {
Ok(Node {
id: Some(row.get(0)?),
kind: NodeKind::parse(&row.get::<_, String>(1)?),
name: row.get(2)?,
file_path: row.get(3)?,
line_start: row.get::<_, Option<i64>>(4)?.map(|v| v as usize),
line_end: row.get::<_, Option<i64>>(5)?.map(|v| v as usize),
metadata: row.get(6)?,
})
})?;
let mut results = Vec::new();
for r in rows {
results.push(r?);
}
Ok(results)
}
pub(super) fn symbol_count(conn: &Connection) -> anyhow::Result<usize> {
let c: i64 = conn.query_row(
"SELECT COUNT(*) FROM nodes WHERE kind != 'file'",
[],
|row| row.get(0),
)?;
Ok(c as usize)
}
pub(super) fn file_count(conn: &Connection) -> anyhow::Result<usize> {
let c: i64 = conn.query_row(
"SELECT COUNT(*) FROM nodes WHERE kind = 'file'",
[],
|row| row.get(0),
)?;
Ok(c as usize)
}
pub(super) fn all_edges_flat(
conn: &Connection,
) -> anyhow::Result<Vec<(String, String, String, f64)>> {
let mut stmt = conn.prepare(
"SELECT p1.path, p2.path, e.kind
FROM edges e
JOIN nodes n1 ON e.source_id = n1.id
JOIN nodes n2 ON e.target_id = n2.id
JOIN paths p1 ON p1.id = n1.file_id
JOIN paths p2 ON p2.id = n2.file_id
WHERE n1.kind = 'file' AND n2.kind = 'file'",
)?;
let rows = stmt.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
))
})?;
let mut result = Vec::new();
for r in rows {
let (from, to, kind) = r?;
let weight = super::queries::edge_weight(&kind);
result.push((from, to, kind, weight));
}
Ok(result)
}
pub(super) fn count(conn: &Connection) -> anyhow::Result<usize> {
let c: i64 = conn.query_row("SELECT COUNT(*) FROM nodes", [], |row| row.get(0))?;
Ok(c as usize)
}