use super::*;
impl Database {
pub async fn insert_node(&self, node: &Node) -> Result<()> {
self.conn()
.execute(
"INSERT OR REPLACE INTO nodes
(id, kind, name, qualified_name, file_path,
start_line, end_line, start_column, end_column,
docstring, signature, visibility, is_async,
branches, loops, returns, max_nesting,
unsafe_blocks, unchecked_calls, assertions, updated_at, attrs_start_line, parent_id, cognitive_complexity, distinct_operators, distinct_operands, total_operators, total_operands, search_terms)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29)",
params![
node.id.as_str(),
node.kind.as_str(),
node.name.as_str(),
node.qualified_name.as_str(),
node.file_path.as_str(),
i64::from(node.start_line),
i64::from(node.end_line),
i64::from(node.start_column),
i64::from(node.end_column),
opt_str(node.docstring.as_deref()),
opt_str(node.signature.as_deref()),
node.visibility.as_str(),
i64::from(node.is_async),
i64::from(node.branches),
i64::from(node.loops),
i64::from(node.returns),
i64::from(node.max_nesting),
i64::from(node.unsafe_blocks),
i64::from(node.unchecked_calls),
i64::from(node.assertions),
node.updated_at as i64,
i64::from(node.attrs_start_line),
opt_str(node.parent_id.as_deref()),
i64::from(node.cognitive_complexity),
i64::from(node.distinct_operators),
i64::from(node.distinct_operands),
i64::from(node.total_operators),
i64::from(node.total_operands),
crate::text::search_terms(&node.name, &node.qualified_name),
],
)
.await
.map_err(|e| TokenSaveError::Database {
message: format!("failed to insert node: {e}"),
operation: "insert_node".to_string(),
})?;
Ok(())
}
pub async fn insert_all(
&self,
nodes: &[Node],
edges: &[Edge],
files: &[FileRecord],
) -> Result<()> {
let mut parent_map: std::collections::HashMap<&str, &str> =
std::collections::HashMap::new();
let mut surviving_edges: Vec<&Edge> = Vec::with_capacity(edges.len());
for edge in edges {
if edge.kind == crate::types::EdgeKind::Contains {
parent_map
.entry(edge.target.as_str())
.or_insert(edge.source.as_str());
} else {
surviving_edges.push(edge);
}
}
let nodes_owned: Vec<Node>;
let nodes_ref: &[Node] = if parent_map.is_empty() {
nodes
} else {
nodes_owned = nodes
.iter()
.map(|n| {
if let Some(parent) = parent_map.get(n.id.as_str()) {
let mut copy = n.clone();
copy.parent_id = Some((*parent).to_string());
copy
} else {
n.clone()
}
})
.collect();
&nodes_owned
};
let mut sql = String::with_capacity(
nodes_ref.len() * 400 + surviving_edges.len() * 120 + files.len() * 120,
);
sql.push_str("BEGIN;\n");
for chunk in nodes_ref.chunks(200) {
sql.push_str(
"INSERT OR REPLACE INTO nodes \
(id,kind,name,qualified_name,file_path,\
start_line,end_line,start_column,end_column,\
docstring,signature,visibility,is_async,\
branches,loops,returns,max_nesting,\
unsafe_blocks,unchecked_calls,assertions,updated_at,attrs_start_line,parent_id,cognitive_complexity,distinct_operators,distinct_operands,total_operators,total_operands,search_terms) VALUES ",
);
for (i, node) in chunk.iter().enumerate() {
if i > 0 {
sql.push(',');
}
sql.push('(');
push_quoted(&mut sql, &node.id);
sql.push(',');
push_quoted(&mut sql, node.kind.as_str());
sql.push(',');
push_quoted(&mut sql, &node.name);
sql.push(',');
push_quoted(&mut sql, &node.qualified_name);
sql.push(',');
push_quoted(&mut sql, &node.file_path);
sql.push(',');
push_int(&mut sql, i64::from(node.start_line));
sql.push(',');
push_int(&mut sql, i64::from(node.end_line));
sql.push(',');
push_int(&mut sql, i64::from(node.start_column));
sql.push(',');
push_int(&mut sql, i64::from(node.end_column));
sql.push(',');
push_opt_quoted(&mut sql, node.docstring.as_deref());
sql.push(',');
push_opt_quoted(&mut sql, node.signature.as_deref());
sql.push(',');
push_quoted(&mut sql, node.visibility.as_str());
sql.push(',');
push_int(&mut sql, i64::from(node.is_async));
sql.push(',');
push_int(&mut sql, i64::from(node.branches));
sql.push(',');
push_int(&mut sql, i64::from(node.loops));
sql.push(',');
push_int(&mut sql, i64::from(node.returns));
sql.push(',');
push_int(&mut sql, i64::from(node.max_nesting));
sql.push(',');
push_int(&mut sql, i64::from(node.unsafe_blocks));
sql.push(',');
push_int(&mut sql, i64::from(node.unchecked_calls));
sql.push(',');
push_int(&mut sql, i64::from(node.assertions));
sql.push(',');
push_int(&mut sql, node.updated_at as i64);
sql.push(',');
push_int(&mut sql, i64::from(node.attrs_start_line));
sql.push(',');
push_opt_quoted(&mut sql, node.parent_id.as_deref());
sql.push(',');
push_int(&mut sql, i64::from(node.cognitive_complexity));
sql.push(',');
push_int(&mut sql, i64::from(node.distinct_operators));
sql.push(',');
push_int(&mut sql, i64::from(node.distinct_operands));
sql.push(',');
push_int(&mut sql, i64::from(node.total_operators));
sql.push(',');
push_int(&mut sql, i64::from(node.total_operands));
sql.push(',');
push_quoted(
&mut sql,
&crate::text::search_terms(&node.name, &node.qualified_name),
);
sql.push(')');
}
sql.push_str(";\n");
}
for chunk in surviving_edges.chunks(500) {
sql.push_str("INSERT OR IGNORE INTO edges (source,target,kind,line) VALUES ");
for (i, edge) in chunk.iter().enumerate() {
if i > 0 {
sql.push(',');
}
sql.push('(');
push_quoted(&mut sql, &edge.source);
sql.push(',');
push_quoted(&mut sql, &edge.target);
sql.push(',');
push_quoted(&mut sql, edge.kind.as_str());
sql.push(',');
match edge.line {
Some(l) => push_int(&mut sql, i64::from(l)),
None => sql.push_str("NULL"),
}
sql.push(')');
}
sql.push_str(";\n");
}
for chunk in files.chunks(500) {
sql.push_str(
"INSERT OR REPLACE INTO files \
(path,content_hash,size,modified_at,indexed_at,node_count) VALUES ",
);
for (i, file) in chunk.iter().enumerate() {
if i > 0 {
sql.push(',');
}
sql.push('(');
push_quoted(&mut sql, &file.path);
sql.push(',');
push_quoted(&mut sql, &file.content_hash);
sql.push(',');
push_int(&mut sql, file.size as i64);
sql.push(',');
push_int(&mut sql, file.modified_at);
sql.push(',');
push_int(&mut sql, file.indexed_at);
sql.push(',');
push_int(&mut sql, i64::from(file.node_count));
sql.push(')');
}
sql.push_str(";\n");
}
sql.push_str("COMMIT;\n");
self.conn()
.execute_batch(&sql)
.await
.map_err(|e| TokenSaveError::Database {
message: format!("failed to bulk insert: {e}"),
operation: "insert_all".to_string(),
})?;
Ok(())
}
pub async fn insert_nodes(&self, nodes: &[Node]) -> Result<()> {
if nodes.is_empty() {
return Ok(());
}
self.conn()
.execute("BEGIN", ())
.await
.map_err(|e| TokenSaveError::Database {
message: format!("failed to begin: {e}"),
operation: "insert_nodes".to_string(),
})?;
let stmt = self.conn()
.prepare(
"INSERT OR REPLACE INTO nodes \
(id,kind,name,qualified_name,file_path,\
start_line,end_line,start_column,end_column,\
docstring,signature,visibility,is_async,\
branches,loops,returns,max_nesting,\
unsafe_blocks,unchecked_calls,assertions,updated_at,attrs_start_line,parent_id,cognitive_complexity,distinct_operators,distinct_operands,total_operators,total_operands,search_terms) \
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19,?20,?21,?22,?23,?24,?25,?26,?27,?28,?29)"
)
.await
.map_err(|e| TokenSaveError::Database {
message: format!("failed to prepare: {e}"),
operation: "insert_nodes".to_string(),
})?;
for node in nodes {
stmt.execute(params![
node.id.as_str(),
node.kind.as_str(),
node.name.as_str(),
node.qualified_name.as_str(),
node.file_path.as_str(),
i64::from(node.start_line),
i64::from(node.end_line),
i64::from(node.start_column),
i64::from(node.end_column),
opt_str(node.docstring.as_deref()),
opt_str(node.signature.as_deref()),
node.visibility.as_str(),
i64::from(node.is_async),
i64::from(node.branches),
i64::from(node.loops),
i64::from(node.returns),
i64::from(node.max_nesting),
i64::from(node.unsafe_blocks),
i64::from(node.unchecked_calls),
i64::from(node.assertions),
node.updated_at as i64,
i64::from(node.attrs_start_line),
opt_str(node.parent_id.as_deref()),
i64::from(node.cognitive_complexity),
i64::from(node.distinct_operators),
i64::from(node.distinct_operands),
i64::from(node.total_operators),
i64::from(node.total_operands),
crate::text::search_terms(&node.name, &node.qualified_name),
])
.await
.map_err(|e| TokenSaveError::Database {
message: format!("failed to insert node: {e}"),
operation: "insert_nodes".to_string(),
})?;
stmt.reset();
}
self.conn()
.execute("COMMIT", ())
.await
.map_err(|e| TokenSaveError::Database {
message: format!("failed to commit: {e}"),
operation: "insert_nodes".to_string(),
})?;
Ok(())
}
pub async fn get_node_by_id(&self, id: &str) -> Result<Option<Node>> {
let mut rows = self
.conn()
.query(
"SELECT id, kind, name, qualified_name, file_path,
start_line, end_line, start_column, end_column,
docstring, signature, visibility, is_async, branches, loops, returns, max_nesting, unsafe_blocks, unchecked_calls, assertions, updated_at, attrs_start_line, parent_id, cognitive_complexity, distinct_operators, distinct_operands, total_operators, total_operands
FROM nodes WHERE id = ?1",
params![id],
)
.await
.map_err(|e| TokenSaveError::Database {
message: format!("failed to query node by id: {e}"),
operation: "get_node_by_id".to_string(),
})?;
match rows.next().await.map_err(|e| TokenSaveError::Database {
message: format!("failed to read node row: {e}"),
operation: "get_node_by_id".to_string(),
})? {
Some(row) => {
let node = row_to_node(&row).map_err(|e| TokenSaveError::Database {
message: format!("failed to map node row: {e}"),
operation: "get_node_by_id".to_string(),
})?;
Ok(Some(node))
}
None => Ok(None),
}
}
pub async fn get_nodes_by_ids(&self, ids: &[String]) -> Result<Vec<Node>> {
if ids.is_empty() {
return Ok(Vec::new());
}
let placeholders = build_qmark_placeholders(ids.len());
let sql = format!(
"SELECT id, kind, name, qualified_name, file_path,
start_line, end_line, start_column, end_column,
docstring, signature, visibility, is_async, branches, loops, returns, max_nesting, unsafe_blocks, unchecked_calls, assertions, updated_at, attrs_start_line, parent_id, cognitive_complexity, distinct_operators, distinct_operands, total_operators, total_operands
FROM nodes WHERE id IN ({placeholders})",
);
let param_values: Vec<libsql::Value> = ids
.iter()
.map(|id| libsql::Value::Text(id.clone()))
.collect();
let mut rows = self
.conn()
.query(&sql, libsql::params_from_iter(param_values))
.await
.map_err(|e| TokenSaveError::Database {
message: format!("failed to batch query nodes: {e}"),
operation: "get_nodes_by_ids".to_string(),
})?;
collect_rows(&mut rows, row_to_node, "get_nodes_by_ids").await
}
pub async fn get_nodes_by_file(&self, file_path: &str) -> Result<Vec<Node>> {
let mut rows = self
.conn()
.query(
"SELECT id, kind, name, qualified_name, file_path,
start_line, end_line, start_column, end_column,
docstring, signature, visibility, is_async, branches, loops, returns, max_nesting, unsafe_blocks, unchecked_calls, assertions, updated_at, attrs_start_line, parent_id, cognitive_complexity, distinct_operators, distinct_operands, total_operators, total_operands
FROM nodes WHERE file_path = ?1 ORDER BY start_line",
params![file_path],
)
.await
.map_err(|e| TokenSaveError::Database {
message: format!("failed to query nodes by file: {e}"),
operation: "get_nodes_by_file".to_string(),
})?;
collect_rows(&mut rows, row_to_node, "get_nodes_by_file").await
}
pub async fn get_children_of(&self, parent_id: &str) -> Result<Vec<Node>> {
let mut rows = self
.conn()
.query(
"SELECT id, kind, name, qualified_name, file_path,
start_line, end_line, start_column, end_column,
docstring, signature, visibility, is_async, branches, loops, returns, max_nesting, unsafe_blocks, unchecked_calls, assertions, updated_at, attrs_start_line, parent_id, cognitive_complexity, distinct_operators, distinct_operands, total_operators, total_operands
FROM nodes WHERE parent_id = ?1 ORDER BY start_line",
params![parent_id],
)
.await
.map_err(|e| TokenSaveError::Database {
message: format!("failed to query children: {e}"),
operation: "get_children_of".to_string(),
})?;
collect_rows(&mut rows, row_to_node, "get_children_of").await
}
pub async fn get_children_of_many(&self, parent_ids: &[String]) -> Result<Vec<Node>> {
if parent_ids.is_empty() {
return Ok(Vec::new());
}
let placeholders = build_qmark_placeholders(parent_ids.len());
let sql = format!(
"SELECT id, kind, name, qualified_name, file_path,
start_line, end_line, start_column, end_column,
docstring, signature, visibility, is_async, branches, loops, returns, max_nesting, unsafe_blocks, unchecked_calls, assertions, updated_at, attrs_start_line, parent_id, cognitive_complexity, distinct_operators, distinct_operands, total_operators, total_operands
FROM nodes WHERE parent_id IN ({placeholders}) ORDER BY file_path, start_line"
);
let values = parent_ids
.iter()
.cloned()
.map(libsql::Value::Text)
.collect::<Vec<_>>();
let mut rows = self
.conn()
.query(&sql, libsql::params_from_iter(values))
.await
.map_err(|e| TokenSaveError::Database {
message: format!("failed to query children for parent set: {e}"),
operation: "get_children_of_many".to_string(),
})?;
collect_rows(&mut rows, row_to_node, "get_children_of_many").await
}
pub async fn get_trait_methods_for_impl_method(
&self,
impl_id: &str,
method_name: &str,
) -> Result<Vec<Node>> {
let mut rows = self
.conn()
.query(
"SELECT method.id, method.kind, method.name, method.qualified_name, method.file_path,
method.start_line, method.end_line, method.start_column, method.end_column,
method.docstring, method.signature, method.visibility, method.is_async,
method.branches, method.loops, method.returns, method.max_nesting,
method.unsafe_blocks, method.unchecked_calls, method.assertions,
method.updated_at, method.attrs_start_line, method.parent_id,
method.cognitive_complexity, method.distinct_operators,
method.distinct_operands, method.total_operators, method.total_operands
FROM edges dispatch
JOIN nodes trait ON trait.id = dispatch.target AND trait.kind = 'trait'
JOIN nodes method ON method.parent_id = trait.id
WHERE dispatch.source = ?1
AND dispatch.kind = 'implements'
AND method.name = ?2
AND method.kind IN ('method', 'function')
ORDER BY method.file_path, method.start_line",
params![impl_id, method_name],
)
.await
.map_err(|e| TokenSaveError::Database {
message: format!("failed to resolve reverse trait dispatch: {e}"),
operation: "get_trait_methods_for_impl_method".to_string(),
})?;
collect_rows(&mut rows, row_to_node, "get_trait_methods_for_impl_method").await
}
pub async fn get_nodes_by_kind(&self, kind: NodeKind) -> Result<Vec<Node>> {
let mut rows = self
.conn()
.query(
"SELECT id, kind, name, qualified_name, file_path,
start_line, end_line, start_column, end_column,
docstring, signature, visibility, is_async, branches, loops, returns, max_nesting, unsafe_blocks, unchecked_calls, assertions, updated_at, attrs_start_line, parent_id, cognitive_complexity, distinct_operators, distinct_operands, total_operators, total_operands
FROM nodes WHERE kind = ?1",
params![kind.as_str()],
)
.await
.map_err(|e| TokenSaveError::Database {
message: format!("failed to query nodes by kind: {e}"),
operation: "get_nodes_by_kind".to_string(),
})?;
collect_rows(&mut rows, row_to_node, "get_nodes_by_kind").await
}
pub async fn get_all_nodes(&self) -> Result<Vec<Node>> {
let mut rows = self
.conn()
.query(
"SELECT id, kind, name, qualified_name, file_path,
start_line, end_line, start_column, end_column,
docstring, signature, visibility, is_async, branches, loops, returns, max_nesting, unsafe_blocks, unchecked_calls, assertions, updated_at, attrs_start_line, parent_id, cognitive_complexity, distinct_operators, distinct_operands, total_operators, total_operands
FROM nodes",
(),
)
.await
.map_err(|e| TokenSaveError::Database {
message: format!("failed to query all nodes: {e}"),
operation: "get_all_nodes".to_string(),
})?;
collect_rows(&mut rows, row_to_node, "get_all_nodes").await
}
pub async fn delete_nodes_by_file(&self, file_path: &str) -> Result<()> {
debug_assert!(
!file_path.is_empty(),
"delete_nodes_by_file called with empty file_path"
);
debug_assert!(
!file_path.starts_with('/'),
"delete_nodes_by_file expects relative path, got absolute"
);
self.conn()
.execute(
"DELETE FROM executable_body_fts WHERE file_path = ?1",
params![file_path],
)
.await
.map_err(|e| TokenSaveError::Database {
message: format!("failed to delete executable body documents: {e}"),
operation: "delete_nodes_by_file".to_string(),
})?;
let node_ids: Vec<String> = {
let mut rows = self
.conn()
.query(
"SELECT id FROM nodes WHERE file_path = ?1",
params![file_path],
)
.await
.map_err(|e| TokenSaveError::Database {
message: format!("failed to query node ids: {e}"),
operation: "delete_nodes_by_file".to_string(),
})?;
let mut ids = Vec::new();
while let Some(row) = rows.next().await.map_err(|e| TokenSaveError::Database {
message: format!("failed to read node id: {e}"),
operation: "delete_nodes_by_file".to_string(),
})? {
ids.push(row.get::<String>(0).map_err(|e| TokenSaveError::Database {
message: format!("failed to read node id value: {e}"),
operation: "delete_nodes_by_file".to_string(),
})?);
}
ids
};
if node_ids.is_empty() {
return Ok(());
}
let tx = self
.conn()
.transaction()
.await
.map_err(|e| TokenSaveError::Database {
message: format!("failed to begin transaction: {e}"),
operation: "delete_nodes_by_file".to_string(),
})?;
for id in &node_ids {
tx.execute(
"DELETE FROM edges WHERE source = ?1 OR target = ?1",
params![id.as_str()],
)
.await
.map_err(|e| TokenSaveError::Database {
message: format!("failed to delete edges: {e}"),
operation: "delete_nodes_by_file".to_string(),
})?;
tx.execute(
"DELETE FROM unresolved_refs WHERE from_node_id = ?1",
params![id.as_str()],
)
.await
.map_err(|e| TokenSaveError::Database {
message: format!("failed to delete unresolved refs: {e}"),
operation: "delete_nodes_by_file".to_string(),
})?;
tx.execute(
"DELETE FROM vectors WHERE node_id = ?1",
params![id.as_str()],
)
.await
.map_err(|e| TokenSaveError::Database {
message: format!("failed to delete vectors: {e}"),
operation: "delete_nodes_by_file".to_string(),
})?;
}
tx.execute("DELETE FROM nodes WHERE file_path = ?1", params![file_path])
.await
.map_err(|e| TokenSaveError::Database {
message: format!("failed to delete nodes: {e}"),
operation: "delete_nodes_by_file".to_string(),
})?;
tx.commit().await.map_err(|e| TokenSaveError::Database {
message: format!("failed to commit transaction: {e}"),
operation: "delete_nodes_by_file".to_string(),
})
}
}