use super::*;
impl Database {
pub fn upsert_symbol_content(
&self,
symbol_id: &str,
symbol_name: &str,
content: &str,
header: &str,
) -> Result<()> {
let normalized = normalize_symbol_name(symbol_name);
self.conn.execute(
"DELETE FROM symbol_content WHERE symbol_id = ?1",
params![symbol_id],
)?;
self.conn.execute(
"INSERT INTO symbol_content (symbol_id, content, header, normalized_name)
VALUES (?1, ?2, ?3, ?4)",
params![symbol_id, content, header, normalized],
)?;
Ok(())
}
pub fn insert_symbol_contents(&self, items: &[(String, String, String, String)]) -> Result<()> {
let tx = self.conn.unchecked_transaction()?;
self.insert_symbol_contents_in_tx(items)?;
tx.commit()?;
Ok(())
}
pub fn insert_symbol_contents_in_tx(
&self,
items: &[(String, String, String, String)],
) -> Result<()> {
let mut del = self
.conn
.prepare_cached("DELETE FROM symbol_content WHERE symbol_id = ?1")?;
let mut ins = self.conn.prepare_cached(
"INSERT INTO symbol_content (symbol_id, content, header, normalized_name)
VALUES (?1, ?2, ?3, ?4)",
)?;
for (symbol_id, name, content, header) in items {
let normalized = normalize_symbol_name(name);
del.execute(params![symbol_id])?;
ins.execute(params![symbol_id, content, header, normalized])?;
}
Ok(())
}
pub fn clear_symbol_content_for_file(&self, file_path: &str) -> Result<()> {
self.conn.execute(
"DELETE FROM symbol_content WHERE symbol_id IN
(SELECT id FROM symbols WHERE file_path = ?1)",
params![file_path],
)?;
Ok(())
}
pub fn get_symbol_content(&self, symbol_id: &str) -> Result<Option<(String, String)>> {
self.conn
.query_row(
"SELECT content, header FROM symbol_content WHERE symbol_id = ?1",
params![symbol_id],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.optional()
.context("Failed to query symbol content")
}
pub fn get_symbol_contents_batch(
&self,
symbol_ids: &[String],
) -> Result<std::collections::HashMap<String, (String, String)>> {
let mut result = std::collections::HashMap::with_capacity(symbol_ids.len());
if symbol_ids.is_empty() {
return Ok(result);
}
for chunk in symbol_ids.chunks(Self::FILE_CHUNK_SIZE) {
let placeholders: Vec<&str> = chunk.iter().map(|_| "?").collect();
let sql = format!(
"SELECT symbol_id, content, header FROM symbol_content WHERE symbol_id IN ({})",
placeholders.join(",")
);
let mut stmt = self.conn.prepare(&sql)?;
let params: Vec<Box<dyn rusqlite::types::ToSql>> = chunk
.iter()
.map(|id| Box::new(id.clone()) as Box<dyn rusqlite::types::ToSql>)
.collect();
let param_refs: Vec<&dyn rusqlite::types::ToSql> =
params.iter().map(|p| p.as_ref()).collect();
let rows = stmt
.query_map(param_refs.as_slice(), |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
))
})?
.collect::<std::result::Result<Vec<_>, _>>()?;
for (id, content, header) in rows {
result.insert(id, (content, header));
}
}
Ok(result)
}
pub fn fts5_search(&self, query: &str, limit: u32) -> Result<Vec<String>> {
let mut stmt = self.conn.prepare(
"SELECT sc.symbol_id
FROM symbol_fts f
JOIN symbol_content sc ON sc.rowid = f.rowid
WHERE symbol_fts MATCH ?1
ORDER BY rank
LIMIT ?2",
)?;
let rows = stmt
.query_map(params![query, limit], |row| row.get(0))?
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(rows)
}
pub fn get_or_create_embedding_id(&self, symbol_id: &str) -> Result<i64> {
let existing: Option<i64> = self
.conn
.query_row(
"SELECT id FROM symbol_embedding_map WHERE symbol_id = ?1",
params![symbol_id],
|row| row.get(0),
)
.optional()?;
if let Some(id) = existing {
return Ok(id);
}
self.conn.execute(
"INSERT INTO symbol_embedding_map (symbol_id) VALUES (?1)",
params![symbol_id],
)?;
Ok(self.conn.last_insert_rowid())
}
pub fn symbol_id_for_embedding(&self, embedding_id: i64) -> Result<Option<String>> {
self.conn
.query_row(
"SELECT symbol_id FROM symbol_embedding_map WHERE id = ?1",
params![embedding_id],
|row| row.get(0),
)
.optional()
.context("Failed to query embedding map")
}
pub fn symbol_ids_for_embeddings(&self, embedding_ids: &[i64]) -> Result<Vec<(i64, String)>> {
if embedding_ids.is_empty() {
return Ok(Vec::new());
}
let mut all_results = Vec::with_capacity(embedding_ids.len());
for chunk in embedding_ids.chunks(Self::FILE_CHUNK_SIZE) {
let placeholders: Vec<String> = chunk.iter().map(|_| "?".to_string()).collect();
let sql = format!(
"SELECT id, symbol_id FROM symbol_embedding_map WHERE id IN ({})",
placeholders.join(",")
);
let mut stmt = self.conn.prepare(&sql)?;
let params: Vec<Box<dyn rusqlite::types::ToSql>> = chunk
.iter()
.map(|id| Box::new(*id) as Box<dyn rusqlite::types::ToSql>)
.collect();
let param_refs: Vec<&dyn rusqlite::types::ToSql> =
params.iter().map(|p| p.as_ref()).collect();
let rows = stmt
.query_map(param_refs.as_slice(), |row| Ok((row.get(0)?, row.get(1)?)))?
.collect::<std::result::Result<Vec<_>, _>>()?;
all_results.extend(rows);
}
Ok(all_results)
}
pub fn upsert_embedding(&self, embedding_id: i64, embedding: &[u8]) -> Result<()> {
self.conn.execute(
"DELETE FROM symbol_vec WHERE rowid = ?1",
params![embedding_id],
)?;
self.conn.execute(
"INSERT INTO symbol_vec (rowid, embedding) VALUES (?1, ?2)",
params![embedding_id, embedding],
)?;
Ok(())
}
pub fn insert_embeddings(&self, items: &[(i64, Vec<u8>)]) -> Result<()> {
let tx = self.conn.unchecked_transaction()?;
for (id, embedding) in items {
self.conn
.execute("DELETE FROM symbol_vec WHERE rowid = ?1", params![id])?;
self.conn.execute(
"INSERT INTO symbol_vec (rowid, embedding) VALUES (?1, ?2)",
params![id, embedding],
)?;
}
tx.commit()?;
Ok(())
}
pub fn vector_search(&self, query_embedding: &[u8], limit: u32) -> Result<Vec<(i64, f64)>> {
let mut stmt = self.conn.prepare(
"SELECT rowid, distance
FROM symbol_vec
WHERE embedding MATCH ?1
ORDER BY distance
LIMIT ?2",
)?;
let rows = stmt
.query_map(params![query_embedding, limit], |row| {
Ok((row.get(0)?, row.get(1)?))
})?
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(rows)
}
pub fn embedding_count(&self) -> Result<u32> {
Ok(self
.conn
.query_row("SELECT COUNT(*) FROM symbol_embedding_map", [], |row| {
row.get(0)
})?)
}
pub fn has_embedding(&self, symbol_id: &str) -> Result<bool> {
let map_id: Option<i64> = self
.conn
.query_row(
"SELECT id FROM symbol_embedding_map WHERE symbol_id = ?1",
params![symbol_id],
|row| row.get(0),
)
.optional()?;
if let Some(id) = map_id {
let exists: bool = self.conn.query_row(
"SELECT EXISTS(SELECT 1 FROM symbol_vec WHERE rowid = ?1)",
params![id],
|row| row.get(0),
)?;
Ok(exists)
} else {
Ok(false)
}
}
pub fn clear_rag_data_for_file(&self, file_path: &str) -> Result<()> {
self.conn.execute(
"DELETE FROM symbol_vec WHERE rowid IN
(SELECT em.id FROM symbol_embedding_map em
JOIN symbols s ON em.symbol_id = s.id
WHERE s.file_path = ?1)",
params![file_path],
)?;
self.conn.execute(
"DELETE FROM symbol_embedding_map WHERE symbol_id IN
(SELECT id FROM symbols WHERE file_path = ?1)",
params![file_path],
)?;
self.clear_symbol_content_for_file(file_path)?;
Ok(())
}
pub fn get_symbol(&self, id: &str) -> Result<Option<Symbol>> {
self.conn
.query_row(
"SELECT id, name, kind, file_path, start_line, end_line, start_byte, end_byte,
parent_id, signature, visibility, is_async, docstring, in_degree,
content_hash, subtree_hash
FROM symbols WHERE id = ?1",
params![id],
row_to_symbol,
)
.optional()
.context("Failed to query symbol")
}
pub fn get_symbols_by_ids(&self, ids: &[String]) -> Result<Vec<Symbol>> {
if ids.is_empty() {
return Ok(Vec::new());
}
let placeholders: Vec<&str> = ids.iter().map(|_| "?").collect();
let sql = format!(
"SELECT id, name, kind, file_path, start_line, end_line, start_byte, end_byte,
parent_id, signature, visibility, is_async, docstring, in_degree,
content_hash, subtree_hash
FROM symbols WHERE id IN ({})",
placeholders.join(",")
);
let mut stmt = self.conn.prepare(&sql)?;
let params: Vec<Box<dyn rusqlite::types::ToSql>> = ids
.iter()
.map(|id| Box::new(id.clone()) as Box<dyn rusqlite::types::ToSql>)
.collect();
let param_refs: Vec<&dyn rusqlite::types::ToSql> =
params.iter().map(|p| p.as_ref()).collect();
let rows: std::collections::HashMap<String, Symbol> = stmt
.query_map(param_refs.as_slice(), row_to_symbol)?
.filter_map(|r| r.ok())
.map(|s| (s.id.clone(), s))
.collect();
Ok(ids.iter().filter_map(|id| rows.get(id).cloned()).collect())
}
pub fn symbols_needing_embeddings(&self) -> Result<Vec<String>> {
let mut stmt = self.conn.prepare(
"SELECT sc.symbol_id FROM symbol_content sc
JOIN symbols s ON s.id = sc.symbol_id
WHERE s.kind NOT IN (?1, ?2)
AND NOT EXISTS (
SELECT 1 FROM symbol_embedding_map em
JOIN symbol_vec sv ON sv.rowid = em.id
WHERE em.symbol_id = sc.symbol_id
)",
)?;
let rows = stmt
.query_map(
params![SymbolKind::Variable.as_str(), SymbolKind::Import.as_str(),],
|row| row.get(0),
)?
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(rows)
}
pub fn symbol_content_count(&self) -> Result<u32> {
Ok(self
.conn
.query_row("SELECT COUNT(*) FROM symbol_content", [], |row| row.get(0))?)
}
pub fn all_content_symbol_ids(&self) -> Result<Vec<String>> {
let mut stmt = self.conn.prepare(
"SELECT sc.symbol_id FROM symbol_content sc
JOIN symbols s ON s.id = sc.symbol_id
WHERE s.kind NOT IN (?1, ?2)
ORDER BY sc.symbol_id",
)?;
let rows = stmt
.query_map(
params![SymbolKind::Variable.as_str(), SymbolKind::Import.as_str(),],
|row| row.get(0),
)?
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(rows)
}
pub fn clear_all_embeddings(&self) -> Result<()> {
self.conn.execute("DELETE FROM symbol_vec", [])?;
self.conn.execute("DELETE FROM symbol_embedding_map", [])?;
Ok(())
}
}