use std::collections::{HashMap, HashSet};
use libsql::params;
use super::connection::Database;
use crate::errors::{Result, TokenSaveError};
use crate::types::*;
mod clear;
mod edges;
mod files;
mod fingerprints;
mod metadata;
mod nodes;
mod search;
mod stats;
mod unresolved;
pub(crate) fn build_qmark_placeholders(n: usize) -> String {
debug_assert!(n > 0, "build_qmark_placeholders called with n == 0");
let mut s = String::with_capacity(n * 3);
for i in 0..n {
if i > 0 {
s.push_str(", ");
}
s.push('?');
}
s
}
pub(crate) fn row_to_node(row: &libsql::Row) -> std::result::Result<Node, libsql::Error> {
let kind_str = get_string_lossy(row, 1)?;
let vis_str = get_string_lossy(row, 11)?;
let is_async_int = row.get::<i64>(12)?;
let start_line = row.get::<u32>(5)?;
let attrs_raw = row.get::<u32>(21).unwrap_or(0);
let attrs_start_line = if attrs_raw == 0 {
start_line
} else {
attrs_raw
};
let parent_id = get_opt_string_lossy(row, 22).ok().flatten();
Ok(Node {
id: get_string_lossy(row, 0)?,
kind: NodeKind::from_str(&kind_str).unwrap_or(NodeKind::Function),
name: get_string_lossy(row, 2)?,
qualified_name: get_string_lossy(row, 3)?,
file_path: get_string_lossy(row, 4)?,
start_line,
attrs_start_line,
end_line: row.get::<u32>(6)?,
start_column: row.get::<u32>(7)?,
end_column: row.get::<u32>(8)?,
signature: get_opt_string_lossy(row, 10)?,
docstring: get_opt_string_lossy(row, 9)?,
visibility: Visibility::from_str(&vis_str).unwrap_or_default(),
is_async: is_async_int != 0,
branches: row.get::<u32>(13)?,
loops: row.get::<u32>(14)?,
returns: row.get::<u32>(15)?,
max_nesting: row.get::<u32>(16)?,
unsafe_blocks: row.get::<u32>(17)?,
unchecked_calls: row.get::<u32>(18)?,
assertions: row.get::<u32>(19)?,
cognitive_complexity: row.get::<u32>(23).unwrap_or(0),
distinct_operators: row.get::<u32>(24).unwrap_or(0),
distinct_operands: row.get::<u32>(25).unwrap_or(0),
total_operators: row.get::<u32>(26).unwrap_or(0),
total_operands: row.get::<u32>(27).unwrap_or(0),
updated_at: row.get::<u64>(20)?,
parent_id,
})
}
pub(crate) fn get_string_lossy(
row: &libsql::Row,
idx: i32,
) -> std::result::Result<String, libsql::Error> {
let val = row.get::<libsql::Value>(idx)?;
match val {
libsql::Value::Text(s) => Ok(s),
libsql::Value::Blob(bytes) => Ok(String::from_utf8_lossy(&bytes).into_owned()),
libsql::Value::Null => Ok(String::new()),
libsql::Value::Integer(i) => Ok(i.to_string()),
libsql::Value::Real(f) => Ok(f.to_string()),
}
}
pub(crate) fn get_opt_string_lossy(
row: &libsql::Row,
idx: i32,
) -> std::result::Result<Option<String>, libsql::Error> {
let val = row.get::<libsql::Value>(idx)?;
match val {
libsql::Value::Null => Ok(None),
libsql::Value::Text(s) => Ok(Some(s)),
libsql::Value::Blob(bytes) => Ok(Some(String::from_utf8_lossy(&bytes).into_owned())),
libsql::Value::Integer(i) => Ok(Some(i.to_string())),
libsql::Value::Real(f) => Ok(Some(f.to_string())),
}
}
pub(crate) fn row_to_edge(row: &libsql::Row) -> std::result::Result<Edge, libsql::Error> {
let kind_str = row.get::<String>(2)?;
let line = row.get::<Option<u32>>(3)?;
Ok(Edge {
source: row.get::<String>(0)?,
target: row.get::<String>(1)?,
kind: EdgeKind::from_str(&kind_str).unwrap_or(EdgeKind::Uses),
line,
})
}
pub(crate) fn row_to_file(row: &libsql::Row) -> std::result::Result<FileRecord, libsql::Error> {
Ok(FileRecord {
path: row.get::<String>(0)?,
content_hash: row.get::<String>(1)?,
size: row.get::<u64>(2)?,
modified_at: row.get::<i64>(3)?,
indexed_at: row.get::<i64>(4)?,
node_count: row.get::<u32>(5)?,
})
}
pub(crate) fn row_to_unresolved_ref(
row: &libsql::Row,
) -> std::result::Result<UnresolvedRef, libsql::Error> {
let kind_str = row.get::<String>(2)?;
Ok(UnresolvedRef {
from_node_id: row.get::<String>(0)?,
reference_name: row.get::<String>(1)?,
reference_kind: EdgeKind::from_str(&kind_str).unwrap_or(EdgeKind::Uses),
line: row.get::<u32>(3)?,
column: row.get::<u32>(4)?,
file_path: row.get::<String>(5)?,
})
}
pub(crate) fn opt_str(opt: Option<&str>) -> libsql::Value {
match opt {
Some(s) => libsql::Value::Text(s.to_string()),
None => libsql::Value::Null,
}
}
pub(crate) fn push_quoted(buf: &mut String, s: &str) {
buf.push('\'');
for ch in s.chars() {
if ch == '\'' {
buf.push_str("''");
} else {
buf.push(ch);
}
}
buf.push('\'');
}
pub(crate) fn push_opt_quoted(buf: &mut String, opt: Option<&str>) {
match opt {
Some(s) => push_quoted(buf, s),
None => buf.push_str("NULL"),
}
}
pub(crate) fn push_int(buf: &mut String, val: i64) {
use std::fmt::Write;
let _ = write!(buf, "{val}");
}
pub(crate) async fn collect_rows<T>(
rows: &mut libsql::Rows,
map_fn: fn(&libsql::Row) -> std::result::Result<T, libsql::Error>,
operation: &str,
) -> Result<Vec<T>> {
let mut items = Vec::new();
while let Some(row) = rows.next().await.map_err(|e| TokenSaveError::Database {
message: format!("failed to read row: {e}"),
operation: operation.to_string(),
})? {
items.push(map_fn(&row).map_err(|e| TokenSaveError::Database {
message: format!("failed to map row: {e}"),
operation: operation.to_string(),
})?);
}
Ok(items)
}
pub(crate) fn display_language_for_path(path: &str) -> &'static str {
let basename = path.rsplit('/').next().unwrap_or(path);
let lower = basename.to_ascii_lowercase();
if lower == "dockerfile" || lower.starts_with("dockerfile.") {
return "Dockerfile";
}
if lower == "makefile" {
return "Makefile";
}
match path
.rsplit('.')
.next()
.unwrap_or("")
.to_ascii_lowercase()
.as_str()
{
"rs" => "Rust",
"go" => "Go",
"py" | "pyi" | "pyx" => "Python",
"js" | "jsx" | "mjs" | "cjs" => "JavaScript",
"ts" | "tsx" | "mts" | "cts" => "TypeScript",
"java" => "Java",
"kt" | "kts" => "Kotlin",
"scala" | "sc" => "Scala",
"swift" => "Swift",
"as" => "ActionScript",
"c" | "h" => "C",
"cpp" | "cc" | "cxx" | "hpp" | "hxx" | "hh" => "C++",
"cs" => "C#",
"fs" | "fsi" | "fsx" => "F#",
"fst" | "fsti" => "F*",
"rb" => "Ruby",
"php" => "PHP",
"dart" => "Dart",
"lua" => "Lua",
"pl" | "pm" => "Perl",
"sh" | "bash" => "Bash",
"ps1" | "psm1" => "PowerShell",
"nix" => "Nix",
"zig" => "Zig",
"proto" => "Protobuf",
"toml" => "TOML",
"sql" => "SQL",
"r" => "R",
"jl" => "Julia",
"ex" | "exs" => "Elixir",
"erl" | "hrl" => "Erlang",
"hs" => "Haskell",
"clj" | "cljs" | "cljc" | "edn" => "Clojure",
"ml" | "mli" => "OCaml",
"lean" => "Lean",
"m" | "mm" => "Objective-C",
"f" | "f90" | "f95" | "f03" | "f08" | "for" => "Fortran",
"cbl" | "cob" | "cpy" => "COBOL",
"pas" | "pp" | "dpr" => "Pascal",
"vb" => "VB.NET",
"bas" => "BASIC",
"bat" | "cmd" => "Batch",
"glsl" | "vert" | "frag" | "comp" | "geom" | "tesc" | "tese" => "GLSL",
"qnt" => "Quint",
"gd" => "GDScript",
_ => "Other",
}
}
pub(crate) async fn query_kind_counts(
conn: &libsql::Connection,
sql: &str,
) -> Result<HashMap<String, u64>> {
let mut map = HashMap::new();
let mut rows = conn
.query(sql, ())
.await
.map_err(|e| TokenSaveError::Database {
message: format!("failed to query kind counts: {e}"),
operation: "get_stats".to_string(),
})?;
while let Some(row) = rows.next().await.map_err(|e| TokenSaveError::Database {
message: format!("failed to read kind count row: {e}"),
operation: "get_stats".to_string(),
})? {
let kind: String = row
.get::<Option<String>>(0)
.map_err(|e| TokenSaveError::Database {
message: format!("failed to read kind: {e}"),
operation: "get_stats".to_string(),
})?
.unwrap_or_else(|| "unknown".to_string());
let count: i64 = row.get(1).map_err(|e| TokenSaveError::Database {
message: format!("failed to read count: {e}"),
operation: "get_stats".to_string(),
})?;
if count > 0 {
*map.entry(kind).or_insert(0) += count as u64;
}
}
Ok(map)
}
pub(crate) async fn query_scalar_i64(
conn: &libsql::Connection,
sql: &str,
operation: &str,
) -> Result<i64> {
let mut rows = conn
.query(sql, ())
.await
.map_err(|e| TokenSaveError::Database {
message: format!("failed to execute scalar query: {e}"),
operation: operation.to_string(),
})?;
let row = rows
.next()
.await
.map_err(|e| TokenSaveError::Database {
message: format!("failed to read scalar row: {e}"),
operation: operation.to_string(),
})?
.ok_or_else(|| TokenSaveError::Database {
message: "no result from scalar query".to_string(),
operation: operation.to_string(),
})?;
row.get::<i64>(0).map_err(|e| TokenSaveError::Database {
message: format!("failed to read scalar value: {e}"),
operation: operation.to_string(),
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::display_language_for_path;
#[test]
fn maps_common_extensions_to_named_languages() {
assert_eq!(display_language_for_path("src/main.rs"), "Rust");
assert_eq!(display_language_for_path("a/b/foo.py"), "Python");
assert_eq!(display_language_for_path("foo.pyi"), "Python");
assert_eq!(display_language_for_path("foo.tsx"), "TypeScript");
assert_eq!(display_language_for_path("foo.cs"), "C#");
assert_eq!(display_language_for_path("foo.fst"), "F*");
assert_eq!(display_language_for_path("foo.fsti"), "F*");
assert_eq!(display_language_for_path("foo.cpp"), "C++");
assert_eq!(
display_language_for_path("com/example/Game.as"),
"ActionScript"
);
assert_eq!(display_language_for_path("Dockerfile"), "Dockerfile");
assert_eq!(
display_language_for_path("docker/Dockerfile.prod"),
"Dockerfile"
);
assert_eq!(display_language_for_path("Makefile"), "Makefile");
assert_eq!(display_language_for_path("readme.txt"), "Other");
assert_eq!(display_language_for_path("noext"), "Other");
assert_eq!(
display_language_for_path("player.gd"),
"GDScript",
"GDScript files must not fall into the Other bucket in `status`'s files_by_language"
);
}
#[test]
fn extension_match_is_case_insensitive() {
assert_eq!(display_language_for_path("Foo.RS"), "Rust");
assert_eq!(display_language_for_path("Foo.PY"), "Python");
}
#[tokio::test]
async fn query_kind_counts_coalesces_null_kind() {
let db = libsql::Builder::new_local(":memory:")
.build()
.await
.expect("build in-memory db");
let conn = db.connect().expect("connect");
conn.execute("CREATE TABLE t (kind TEXT)", ())
.await
.unwrap();
conn.execute("INSERT INTO t (kind) VALUES ('function'), (NULL)", ())
.await
.unwrap();
let counts = super::query_kind_counts(&conn, "SELECT kind, COUNT(*) FROM t GROUP BY kind")
.await
.expect("get_stats must not hard-fail on a NULL kind row");
assert_eq!(counts.get("function"), Some(&1));
assert_eq!(counts.get("unknown"), Some(&1), "NULL kind → \"unknown\"");
}
}