#![doc = ""]
#![doc = include_str!("../README.md")]
use anyhow::{Context, Result};
use rusqlite::ffi::sqlite3_auto_extension;
use rusqlite::{params, Connection, OptionalExtension};
use serde::Serialize;
use sqlite_vec::sqlite3_vec_init;
use tracing::{info, warn};
use cartog_core::{Edge, EdgeKind, EdgeProvenance, FileInfo, Symbol, SymbolKind, Visibility};
#[derive(Debug, thiserror::Error)]
pub enum DbError {
#[error("failed to open database at {path}: {source}")]
Open {
path: std::path::PathBuf,
#[source]
source: rusqlite::Error,
},
#[error("failed to prepare database directory {path}: {source}")]
PrepareDir {
path: std::path::PathBuf,
#[source]
source: std::io::Error,
},
#[error("failed to set startup pragmas: {0}")]
Pragma(#[source] rusqlite::Error),
#[error("failed to create schema: {0}")]
Schema(#[source] rusqlite::Error),
#[error("failed to create RAG schema: {0}")]
RagSchema(#[source] rusqlite::Error),
#[error("failed to back up database before destructive migration to {path}: {source}")]
BackupFailed {
path: std::path::PathBuf,
#[source]
source: rusqlite::Error,
},
#[error("embedding dimension migration failed: {0}")]
EmbeddingDimension(#[source] rusqlite::Error),
#[error(
"schema_version mismatch: this binary expects {expected}, DB has {stored} \
(a different cartog process upgraded the schema; restart this session)"
)]
SchemaDrift { expected: u32, stored: u32 },
#[error(transparent)]
Sqlite(#[from] rusqlite::Error),
}
pub type DbResult<T> = std::result::Result<T, DbError>;
const SQL_INSERT_SYMBOL: &str = "INSERT OR REPLACE INTO symbols
(id, name, kind, file_path, start_line, end_line, start_byte, end_byte,
parent_id, signature, visibility, is_async, docstring, content_hash, subtree_hash)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)";
const SQL_INSERT_EDGE: &str = "INSERT INTO edges
(source_id, target_name, target_id, kind, file_path, line, resolution_state, resolution_source)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)";
const SCHEMA: &str = r#"
CREATE TABLE IF NOT EXISTS symbols (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
kind TEXT NOT NULL,
file_path TEXT NOT NULL,
start_line INTEGER,
end_line INTEGER,
start_byte INTEGER,
end_byte INTEGER,
parent_id TEXT,
signature TEXT,
visibility TEXT,
is_async BOOLEAN DEFAULT FALSE,
docstring TEXT,
in_degree INTEGER DEFAULT 0,
content_hash TEXT,
subtree_hash TEXT
);
CREATE TABLE IF NOT EXISTS edges (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_id TEXT NOT NULL,
target_name TEXT NOT NULL,
target_id TEXT,
kind TEXT NOT NULL,
file_path TEXT NOT NULL,
line INTEGER,
-- 0 = unresolved (heuristic + LSP not yet definitive), 1 = resolved,
-- 2 = unresolvable (LSP definitively returned no definition: typo, dyn dispatch, macro),
-- 3 = external (LSP located the target outside the indexed root: stdlib, deps, node_modules).
resolution_state INTEGER NOT NULL DEFAULT 0,
-- Which tier/source resolved target_id (EdgeProvenance::as_str), or NULL for
-- unresolved edges and rows resolved before provenance tracking existed.
resolution_source TEXT,
FOREIGN KEY (source_id) REFERENCES symbols(id)
);
CREATE TABLE IF NOT EXISTS files (
path TEXT PRIMARY KEY,
last_modified REAL,
hash TEXT,
language TEXT,
num_symbols INTEGER DEFAULT 0
);
CREATE TABLE IF NOT EXISTS metadata (
key TEXT PRIMARY KEY,
value TEXT
);
-- query_log feeds `cartog stats --savings` / `cartog savings`. One row per
-- successful read tool call (CLI or MCP). No query payload is stored — just
-- which tool, when, and the call surface — to keep the local-first promise.
CREATE TABLE IF NOT EXISTS query_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tool TEXT NOT NULL,
source TEXT NOT NULL,
ts INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_query_log_tool ON query_log(tool);
CREATE INDEX IF NOT EXISTS idx_query_log_ts ON query_log(ts);
CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind);
CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_path);
CREATE INDEX IF NOT EXISTS idx_symbols_parent ON symbols(parent_id);
-- Composite: speeds up same-directory edge resolution
-- (WHERE name = ? AND file_path LIKE ?) in `resolve_edges_pass`.
CREATE INDEX IF NOT EXISTS idx_symbols_name_file ON symbols(name, file_path);
CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_id);
CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_name);
CREATE INDEX IF NOT EXISTS idx_edges_target_id ON edges(target_id);
CREATE INDEX IF NOT EXISTS idx_edges_kind ON edges(kind);
-- Per-file edge delete (clear_file_data_in_tx); without it the DELETE full-scans
-- edges per file, making --force/first-index O(files×edges). idx_edges_unresolved
-- is partial (state=0) so it can't serve deletes of resolved edges.
CREATE INDEX IF NOT EXISTS idx_edges_file ON edges(file_path);
-- Tier-2 import-path lookups; kind-only index scans all imports edges per call (#109).
CREATE INDEX IF NOT EXISTS idx_edges_kind_target ON edges(kind, target_name);
-- idx_edges_unresolved (partial index on resolution_state=0) is created
-- post-migration in Database::open so pre-v4 DBs without the column don't
-- blow up at SCHEMA-load time.
"#;
const RAG_SCHEMA: &str = r#"
CREATE TABLE IF NOT EXISTS symbol_content (
symbol_id TEXT PRIMARY KEY,
content TEXT NOT NULL,
header TEXT NOT NULL,
normalized_name TEXT NOT NULL DEFAULT ''
);
CREATE VIRTUAL TABLE IF NOT EXISTS symbol_fts USING fts5(
symbol_name,
normalized_name,
content,
content=symbol_content,
content_rowid=rowid
);
-- Triggers to keep FTS5 in sync with symbol_content
CREATE TRIGGER IF NOT EXISTS symbol_content_ai AFTER INSERT ON symbol_content BEGIN
INSERT INTO symbol_fts(rowid, symbol_name, normalized_name, content)
VALUES (new.rowid, (SELECT name FROM symbols WHERE id = new.symbol_id), new.normalized_name, new.content);
END;
CREATE TRIGGER IF NOT EXISTS symbol_content_ad AFTER DELETE ON symbol_content BEGIN
INSERT INTO symbol_fts(symbol_fts, rowid, symbol_name, normalized_name, content)
VALUES ('delete', old.rowid, (SELECT name FROM symbols WHERE id = old.symbol_id), old.normalized_name, old.content);
END;
CREATE TABLE IF NOT EXISTS symbol_embedding_map (
id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol_id TEXT NOT NULL UNIQUE
);
CREATE INDEX IF NOT EXISTS idx_embedding_map_symbol ON symbol_embedding_map(symbol_id);
"#;
pub const DEFAULT_EMBEDDING_DIM: usize = 384;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EmbeddingFingerprint {
pub provider: String,
pub model: String,
pub dimension: usize,
}
const EMBED_PROVIDER_KEY: &str = "embedding_provider";
const EMBED_MODEL_KEY: &str = "embedding_model";
fn rag_vec_schema(dim: usize) -> String {
format!("CREATE VIRTUAL TABLE IF NOT EXISTS symbol_vec USING vec0(embedding float[{dim}])")
}
pub const DB_DIR: &str = ".cartog";
pub const DB_FILENAME: &str = "db.sqlite";
pub const LEGACY_DB_FILE: &str = ".cartog.db";
pub const BUSY_TIMEOUT_MS: u32 = 5000;
#[cfg(test)]
thread_local! {
static RECONCILE_FAIL_AFTER_MODEL: std::sync::atomic::AtomicBool =
const { std::sync::atomic::AtomicBool::new(false) };
}
pub fn checkpoint_wal(path: &std::path::Path) -> anyhow::Result<()> {
use anyhow::Context;
if !path.exists() {
return Ok(());
}
let conn = Connection::open(path)
.with_context(|| format!("open {} for WAL checkpoint", path.display()))?;
conn.execute_batch(&format!(
"PRAGMA busy_timeout={BUSY_TIMEOUT_MS};
PRAGMA wal_checkpoint(TRUNCATE);"
))
.with_context(|| format!("PRAGMA wal_checkpoint(TRUNCATE) on {}", path.display()))?;
Ok(())
}
pub const MAX_SEARCH_LIMIT: u32 = 100;
pub fn normalize_symbol_name(name: &str) -> String {
let mut words = Vec::new();
let mut current = String::new();
let chars: Vec<char> = name.chars().collect();
let len = chars.len();
for i in 0..len {
let c = chars[i];
if c == '_' || c == '-' {
if !current.is_empty() {
words.push(std::mem::take(&mut current));
}
continue;
}
if c.is_uppercase() {
let next_is_lower = i + 1 < len && chars[i + 1].is_lowercase();
let prev_is_lower = !current.is_empty() && chars[i - 1].is_lowercase();
if prev_is_lower {
words.push(std::mem::take(&mut current));
} else if !current.is_empty() && next_is_lower {
words.push(std::mem::take(&mut current));
}
current.extend(c.to_lowercase());
} else if c.is_alphanumeric() {
current.extend(c.to_lowercase());
} else {
if !current.is_empty() {
words.push(std::mem::take(&mut current));
}
}
}
if !current.is_empty() {
words.push(current);
}
words.join(" ")
}
pub struct Database {
conn: Connection,
pinned: Option<PinnedAttach>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PinnedAttach {
pub schema_version: u32,
pub embedding: Option<EmbeddingFingerprint>,
}
impl std::fmt::Debug for Database {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Database").finish_non_exhaustive()
}
}
pub fn register_sqlite_vec() {
use std::sync::Once;
static INIT: Once = Once::new();
INIT.call_once(|| unsafe {
#[allow(clippy::missing_transmute_annotations)]
sqlite3_auto_extension(Some(std::mem::transmute(sqlite3_vec_init as *const ())));
});
}
const SCHEMA_VERSION: u32 = 7;
pub const CURRENT_SCHEMA_VERSION: u32 = SCHEMA_VERSION;
pub fn read_schema_version_at(path: &std::path::Path) -> anyhow::Result<u32> {
use anyhow::Context;
let conn = Connection::open_with_flags(
path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_URI,
)
.with_context(|| format!("open {} read-only for schema check", path.display()))?;
Ok(read_schema_version(&conn)?)
}
pub fn read_metadata_at(path: &std::path::Path, key: &str) -> anyhow::Result<Option<String>> {
use anyhow::Context;
let conn = Connection::open_with_flags(
path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_URI,
)
.with_context(|| format!("open {} read-only for metadata read", path.display()))?;
match conn.query_row(
"SELECT value FROM metadata WHERE key = ?1",
rusqlite::params![key],
|row| row.get::<_, Option<String>>(0),
) {
Ok(v) => Ok(v),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(rusqlite::Error::SqliteFailure(_, Some(ref msg)))
if msg.contains("no such table: metadata") =>
{
Ok(None)
}
Err(e) => Err(e).with_context(|| format!("read metadata[{key}] from {}", path.display())),
}
}
fn symbol_vec_exists(conn: &Connection) -> std::result::Result<bool, rusqlite::Error> {
conn.query_row(
"SELECT 1 FROM sqlite_master WHERE type IN ('table','view') AND name='symbol_vec'",
[],
|row| row.get::<_, i64>(0),
)
.optional()
.map(|v| v.is_some())
}
fn read_schema_version(conn: &Connection) -> std::result::Result<u32, DbError> {
match conn.query_row(
"SELECT CAST(value AS INTEGER) FROM metadata WHERE key = 'schema_version'",
[],
|row| row.get::<_, u32>(0),
) {
Ok(v) => Ok(v),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(0),
Err(rusqlite::Error::SqliteFailure(_, Some(ref msg)))
if msg.contains("no such table: metadata") =>
{
Ok(0)
}
Err(e) => Err(DbError::Sqlite(e)),
}
}
fn migrate(conn: &Connection) {
let current: u32 = conn
.query_row(
"SELECT CAST(value AS INTEGER) FROM metadata WHERE key = 'schema_version'",
[],
|row| row.get(0),
)
.unwrap_or(1);
let has_hash_cols = conn
.prepare("SELECT content_hash FROM symbols LIMIT 0")
.is_ok();
let has_resolution_state = conn
.prepare("SELECT resolution_state FROM edges LIMIT 0")
.is_ok();
let has_query_log = conn.prepare("SELECT 1 FROM query_log LIMIT 0").is_ok();
let has_resolution_source = conn
.prepare("SELECT resolution_source FROM edges LIMIT 0")
.is_ok();
if current >= SCHEMA_VERSION
&& has_hash_cols
&& has_resolution_state
&& has_query_log
&& has_resolution_source
{
return;
}
let no_version_row = conn
.query_row(
"SELECT 1 FROM metadata WHERE key = 'schema_version'",
[],
|_| Ok(()),
)
.is_err();
let symbols_empty = conn
.query_row("SELECT COUNT(*) FROM symbols", [], |r| r.get::<_, i64>(0))
.map(|c| c == 0)
.unwrap_or(false);
if no_version_row
&& symbols_empty
&& has_hash_cols
&& has_resolution_state
&& has_query_log
&& has_resolution_source
{
if let Err(e) = conn.execute(
"INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', ?1)",
params![SCHEMA_VERSION.to_string()],
) {
warn!(error = %e, "failed to stamp fresh-DB schema version");
}
return;
}
if current < 2 {
let _ = conn.execute(
"ALTER TABLE symbols ADD COLUMN in_degree INTEGER DEFAULT 0",
[],
);
}
if current < 3 || !has_hash_cols {
info!("schema v3: stable symbol IDs — clearing index for full rebuild");
let _ = conn.execute("ALTER TABLE symbols ADD COLUMN content_hash TEXT", []);
let _ = conn.execute("ALTER TABLE symbols ADD COLUMN subtree_hash TEXT", []);
for table in &["symbol_content", "edges", "symbols", "files"] {
let _ = conn.execute(&format!("DELETE FROM {table}"), []);
}
let _ = conn.execute("DELETE FROM symbol_vec", []);
let _ = conn.execute("DELETE FROM symbol_embedding_map", []);
let _ = conn.execute("DELETE FROM metadata WHERE key = 'last_commit'", []);
}
if current < 4 || !has_resolution_state {
info!("schema v4: adding edges.resolution_state column");
let _ = conn.execute(
"ALTER TABLE edges ADD COLUMN resolution_state INTEGER NOT NULL DEFAULT 0",
[],
);
let _ = conn.execute(
"UPDATE edges SET resolution_state = 1 WHERE target_id IS NOT NULL",
[],
);
}
if current < 5 || !has_query_log {
info!("schema v5: query_log table");
let _ = conn.execute(
"CREATE TABLE IF NOT EXISTS query_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tool TEXT NOT NULL,
source TEXT NOT NULL,
ts INTEGER NOT NULL
)",
[],
);
let _ = conn.execute(
"CREATE INDEX IF NOT EXISTS idx_query_log_tool ON query_log(tool)",
[],
);
let _ = conn.execute(
"CREATE INDEX IF NOT EXISTS idx_query_log_ts ON query_log(ts)",
[],
);
}
if current < 6 || !has_resolution_source {
info!("schema v6: adding edges.resolution_source column");
if let Err(e) = conn.execute("ALTER TABLE edges ADD COLUMN resolution_source TEXT", []) {
warn!(error = %e, "failed to add edges.resolution_source column");
}
}
if current < 7 {
info!("schema v7: symbol-ID escaping — clearing index for full rebuild");
for table in &["symbol_content", "edges", "symbols", "files"] {
let _ = conn.execute(&format!("DELETE FROM {table}"), []);
}
let _ = conn.execute("DELETE FROM symbol_vec", []);
let _ = conn.execute("DELETE FROM symbol_embedding_map", []);
let _ = conn.execute("DELETE FROM metadata WHERE key = 'last_commit'", []);
}
if let Err(e) = conn.execute(
"INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', ?1)",
params![SCHEMA_VERSION.to_string()],
) {
warn!(error = %e, "failed to store schema version");
}
}
const MIGRATION_RETRY_BACKOFF_MS: &[u64] = &[50, 100, 250, 500, 1000];
fn retry_busy<T, F>(mut op: F) -> std::result::Result<T, rusqlite::Error>
where
F: FnMut() -> std::result::Result<T, rusqlite::Error>,
{
let mut attempt = 0usize;
loop {
match op() {
Ok(v) => return Ok(v),
Err(e) => {
let busy = matches!(
e,
rusqlite::Error::SqliteFailure(
rusqlite::ffi::Error {
code: rusqlite::ErrorCode::DatabaseBusy
| rusqlite::ErrorCode::DatabaseLocked,
..
},
_
)
);
if !busy || attempt >= MIGRATION_RETRY_BACKOFF_MS.len() {
return Err(e);
}
let delay_ms = MIGRATION_RETRY_BACKOFF_MS[attempt];
tracing::debug!(
attempt = attempt + 1,
delay_ms,
"retrying embedding-dimension write after SQLITE_BUSY"
);
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
attempt += 1;
}
}
}
}
fn handle_embedding_dimension(
conn: &Connection,
requested_dim: usize,
) -> std::result::Result<(), rusqlite::Error> {
let stored_dim: Option<usize> = conn
.query_row(
"SELECT CAST(value AS INTEGER) FROM metadata WHERE key = 'embedding_dimension'",
[],
|row| row.get::<_, i64>(0).map(|v| v as usize),
)
.ok();
let effective_dim = match stored_dim {
Some(old) if requested_dim == DEFAULT_EMBEDDING_DIM && old != DEFAULT_EMBEDDING_DIM => old,
_ => requested_dim,
};
if stored_dim == Some(effective_dim) && symbol_vec_exists(conn)? {
return Ok(());
}
let schema = rag_vec_schema(effective_dim);
let needs_wipe = stored_dim.is_some();
retry_busy(|| {
let tx = conn.unchecked_transaction()?;
if needs_wipe {
let old_dim = stored_dim.unwrap_or(0);
tracing::warn!(
old = old_dim,
new = effective_dim,
"Embedding dimension changed — clearing vector index. Run `cartog rag index` to re-embed."
);
tx.execute("DROP TABLE IF EXISTS symbol_vec", [])?;
tx.execute("DELETE FROM symbol_embedding_map", [])?;
}
tx.execute_batch(&schema)?;
tx.execute(
"INSERT OR REPLACE INTO metadata (key, value) VALUES ('embedding_dimension', ?1)",
params![effective_dim.to_string()],
)?;
tx.commit()
})?;
Ok(())
}
fn backup_before_destructive_migration(
conn: &Connection,
db_path: &std::path::Path,
) -> DbResult<()> {
let current: u32 = conn
.query_row(
"SELECT CAST(value AS INTEGER) FROM metadata WHERE key = 'schema_version'",
[],
|row| row.get(0),
)
.unwrap_or(1);
let has_hash_cols = conn
.prepare("SELECT content_hash FROM symbols LIMIT 0")
.is_ok();
let will_wipe = current < 7 || !has_hash_cols;
if !will_wipe {
return Ok(());
}
let has_rows = |table: &str| -> bool {
conn.query_row(&format!("SELECT EXISTS(SELECT 1 FROM {table})"), [], |r| {
r.get::<_, bool>(0)
})
.unwrap_or(false)
};
let any_indexed = [
"symbols",
"edges",
"files",
"symbol_content",
"symbol_embedding_map",
]
.iter()
.any(|t| has_rows(t));
if !any_indexed {
return Ok(());
}
let path_str = db_path.to_string_lossy();
if path_str.is_empty() || path_str == ":memory:" || path_str.starts_with("file:") {
return Ok(());
}
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let mut backup_os = db_path.as_os_str().to_os_string();
backup_os.push(format!(".pre-v{current}-{ts}.bak"));
let backup_path = std::path::PathBuf::from(backup_os);
let escaped = backup_path.to_string_lossy().replace('\'', "''");
conn.execute(&format!("VACUUM INTO '{escaped}'"), [])
.map_err(|source| DbError::BackupFailed {
path: backup_path.clone(),
source,
})?;
let symbol_count: i64 = conn
.query_row("SELECT COUNT(*) FROM symbols", [], |row| row.get(0))
.unwrap_or(0);
info!(
backup = %backup_path.display(),
old_version = current,
new_version = SCHEMA_VERSION,
symbols = symbol_count,
"schema migration will clear indexed data — created backup"
);
Ok(())
}
mod store;
pub use store::queries::PathHop;
pub use store::rag::KindScope;
#[derive(Debug, Clone)]
pub struct UnresolvedEdge {
pub edge_id: i64,
pub target_name: String,
pub file_path: String,
pub line: u32,
}
#[derive(Debug, Clone, Serialize, schemars::JsonSchema)]
pub struct IndexStats {
pub num_files: u32,
pub num_symbols: u32,
pub num_edges: u32,
pub num_resolved: u32,
pub num_unresolvable: u32,
pub num_external: u32,
pub languages: Vec<(String, u32)>,
pub symbol_kinds: Vec<(String, u32)>,
}
#[derive(Debug, Clone, Serialize)]
pub struct SavingsReport {
pub by_tool: Vec<(String, u64)>,
pub by_source: Vec<(String, u64)>,
pub total_queries: u64,
pub tokens_used_cartog: u64,
pub tokens_used_grep: u64,
pub estimated_tokens_saved: u64,
pub percent_saved: u8,
pub baseline_delta: u32,
}
pub const TOKENS_PER_QUERY_CARTOG: u32 = 280;
pub const TOKENS_PER_QUERY_GREP: u32 = 1_700;
pub const TOKENS_SAVED_PER_QUERY: u32 = TOKENS_PER_QUERY_GREP - TOKENS_PER_QUERY_CARTOG;
static LOG_QUERY_FAILURE_REPORTED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
fn empty_savings_report() -> SavingsReport {
SavingsReport {
by_tool: Vec::new(),
by_source: Vec::new(),
total_queries: 0,
tokens_used_cartog: 0,
tokens_used_grep: 0,
estimated_tokens_saved: 0,
percent_saved: 0,
baseline_delta: TOKENS_SAVED_PER_QUERY,
}
}
fn is_no_such_table(e: &rusqlite::Error) -> bool {
matches!(
e,
rusqlite::Error::SqliteFailure(_, Some(msg)) if msg.contains("no such table")
)
}
fn row_to_symbol(row: &rusqlite::Row<'_>) -> rusqlite::Result<Symbol> {
row_to_symbol_offset(row, 0)
}
fn row_to_symbol_offset(row: &rusqlite::Row<'_>, off: usize) -> rusqlite::Result<Symbol> {
let kind_str = row.get::<_, String>(off + 2)?;
let kind = kind_str.parse().unwrap_or_else(|_| {
warn!(kind = %kind_str, "unknown symbol kind, defaulting to variable");
SymbolKind::Variable
});
let vis_str = row.get::<_, Option<String>>(off + 10)?.unwrap_or_default();
Ok(Symbol {
id: row.get(off)?,
name: row.get(off + 1)?,
kind,
file_path: row.get(off + 3)?,
start_line: row.get(off + 4)?,
end_line: row.get(off + 5)?,
start_byte: row.get(off + 6)?,
end_byte: row.get(off + 7)?,
parent_id: row.get(off + 8)?,
signature: row.get(off + 9)?,
visibility: Visibility::from_str_lossy(&vis_str),
is_async: row.get(off + 11)?,
docstring: row.get(off + 12)?,
in_degree: row.get(off + 13).unwrap_or(0),
content_hash: row.get(off + 14).unwrap_or(None),
subtree_hash: row.get(off + 15).unwrap_or(None),
})
}
fn disambiguate_two<'a>(a: &'a (String, String), b: &'a (String, String)) -> Option<&'a String> {
match kind_priority(&a.1).cmp(&kind_priority(&b.1)) {
std::cmp::Ordering::Greater => Some(&a.0),
std::cmp::Ordering::Less => Some(&b.0),
std::cmp::Ordering::Equal => None,
}
}
fn kind_priority(kind: &str) -> u8 {
match kind {
"class" | "interface" | "enum" | "type_alias" | "trait" => 3,
"function" => 2,
"method" => 1,
_ => 0,
}
}
fn edge_from_row(row: &rusqlite::Row<'_>, base: usize) -> rusqlite::Result<Edge> {
let kind_str = row.get::<_, String>(base + 3)?;
let kind = kind_str.parse().unwrap_or_else(|_| {
warn!(kind = %kind_str, "unknown edge kind, defaulting to references");
EdgeKind::References
});
let provenance = match row.get::<_, Option<String>>(base + 6)? {
Some(s) => s.parse::<EdgeProvenance>().ok().or_else(|| {
warn!(source = %s, "unknown edge provenance, dropping to None");
None
}),
None => None,
};
Ok(Edge {
source_id: row.get(base)?,
target_name: row.get(base + 1)?,
target_id: row.get(base + 2)?,
kind,
file_path: row.get(base + 4)?,
line: row.get(base + 5)?,
provenance,
})
}
fn row_to_edge(row: &rusqlite::Row<'_>) -> rusqlite::Result<Edge> {
edge_from_row(row, 1)
}
#[cfg(test)]
mod tests;