use anyhow::Result;
use rusqlite::Connection;
use serde::Serialize;
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize)]
pub struct AstContext {
pub ast_id: i64,
pub kind: String,
pub parent_id: Option<i64>,
pub byte_start: u64,
pub byte_end: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub depth: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_kind: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub children_count_by_kind: Option<HashMap<String, u64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub decision_points: Option<u64>,
}
pub fn check_ast_table_exists(conn: &Connection) -> Result<bool> {
let mut stmt =
conn.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='ast_nodes'")?;
Ok(stmt.exists([])?)
}
pub const fn ast_nodes_table_schema() -> &'static str {
"CREATE TABLE ast_nodes (
id INTEGER PRIMARY KEY,
parent_id INTEGER,
kind TEXT NOT NULL,
byte_start INTEGER NOT NULL,
byte_end INTEGER NOT NULL
)"
}
pub fn calculate_ast_depth(conn: &Connection, ast_id: i64) -> Result<Option<u64>> {
let sql = r#"
WITH RECURSIVE node_ancestry AS (
-- Base case: root nodes (parent_id IS NULL)
SELECT id, parent_id, 0 as depth
FROM ast_nodes
WHERE parent_id IS NULL
UNION ALL
-- Recursive case: add 1 to parent depth
SELECT a.id, a.parent_id, na.depth + 1
FROM ast_nodes a
JOIN node_ancestry na ON a.parent_id = na.id
)
SELECT depth FROM node_ancestry WHERE id = ?
"#;
match conn.query_row(sql, [ast_id], |row| row.get::<_, u64>(0)) {
Ok(depth) => Ok(Some(depth)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}
pub fn calculate_decision_depth(conn: &Connection, ast_id: i64) -> Result<Option<u64>> {
let sql = r#"
WITH RECURSIVE decision_ancestry AS (
-- Base case: start from the node itself, count 1 if it's a decision point
SELECT id, parent_id,
CASE WHEN kind IN (
'if_expression', 'match_expression', 'for_expression',
'while_expression', 'loop_expression'
) THEN 1 ELSE 0 END as depth
FROM ast_nodes
WHERE id = ?
UNION ALL
-- Recursive case: traverse to parent, add 1 if parent is a decision point
SELECT a.id, a.parent_id,
da.depth + CASE WHEN a.kind IN (
'if_expression', 'match_expression', 'for_expression',
'while_expression', 'loop_expression'
) THEN 1 ELSE 0 END
FROM ast_nodes a
JOIN decision_ancestry da ON a.id = da.parent_id
WHERE a.parent_id IS NOT NULL
)
SELECT MAX(depth) FROM decision_ancestry
"#;
match conn.query_row(sql, [ast_id], |row| row.get::<_, u64>(0)) {
Ok(depth) => Ok(Some(depth)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}
pub fn get_parent_kind(conn: &Connection, parent_id: Option<i64>) -> Result<Option<String>> {
let Some(pid) = parent_id else {
return Ok(None);
};
let sql = "SELECT kind FROM ast_nodes WHERE id = ?";
match conn.query_row(sql, [pid], |row| row.get::<_, String>(0)) {
Ok(kind) => Ok(Some(kind)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}
pub fn count_children_by_kind(conn: &Connection, ast_id: i64) -> Result<HashMap<String, u64>> {
let sql = r#"
SELECT kind, COUNT(*) as count
FROM ast_nodes
WHERE parent_id = ?
GROUP BY kind
"#;
let mut stmt = conn.prepare(sql)?;
let rows = stmt.query_map([ast_id], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, u64>(1)?))
})?;
let mut counts = HashMap::new();
for row in rows {
let (kind, count) = row?;
counts.insert(kind, count);
}
Ok(counts)
}
pub fn count_decision_points(conn: &Connection, ast_id: i64) -> Result<u64> {
let sql = r#"
SELECT COUNT(*) FROM ast_nodes
WHERE parent_id = ?
AND kind IN (
'if_expression', 'match_expression', 'while_expression',
'for_expression', 'loop_expression', 'conditional_expression'
)
"#;
conn.query_row(sql, [ast_id], |row| row.get(0))
.map_err(Into::into)
}
pub fn get_ast_context_for_symbol(
conn: &Connection,
_file_path: &str,
byte_start: u64,
byte_end: u64,
include_enriched: bool,
) -> Result<Option<AstContext>> {
get_ast_context_for_symbol_with_preference(
conn,
_file_path,
byte_start,
byte_end,
include_enriched,
&[],
)
}
pub fn get_ast_context_for_symbol_with_preference(
conn: &Connection,
_file_path: &str,
byte_start: u64,
byte_end: u64,
include_enriched: bool,
preferred_kinds: &[String],
) -> Result<Option<AstContext>> {
let (ast_id, parent_id, kind, ast_byte_start, ast_byte_end) = if !preferred_kinds.is_empty() {
let placeholders = preferred_kinds
.iter()
.map(|_| "?")
.collect::<Vec<_>>()
.join(",");
let sql = format!(
"SELECT id, parent_id, kind, byte_start, byte_end
FROM ast_nodes
WHERE byte_start <= ? AND byte_end >= ? AND kind IN ({})
ORDER BY ABS(byte_start - ?) + ABS(byte_end - ?)
LIMIT 1",
placeholders
);
let byte_end_i64 = byte_end as i64;
let byte_start_i64 = byte_start as i64;
let mut params: Vec<&dyn rusqlite::ToSql> = vec![&byte_end_i64, &byte_start_i64];
for kind in preferred_kinds {
params.push(kind);
}
params.push(&byte_start_i64);
params.push(&byte_end_i64);
match conn.query_row(&sql, params.as_slice(), |row| {
Ok((
row.get::<_, i64>(0)?,
row.get::<_, Option<i64>>(1)?,
row.get::<_, String>(2)?,
row.get::<_, u64>(3)?,
row.get::<_, u64>(4)?,
))
}) {
Ok(result) => result,
Err(rusqlite::Error::QueryReturnedNoRows) => {
let fallback_sql = r#"
SELECT id, parent_id, kind, byte_start, byte_end
FROM ast_nodes
WHERE byte_start <= ? AND byte_end >= ?
ORDER BY ABS(byte_start - ?) + ABS(byte_end - ?)
LIMIT 1
"#;
match conn.query_row(
fallback_sql,
[
byte_end as i64,
byte_start as i64,
byte_start as i64,
byte_end as i64,
],
|row| {
Ok((
row.get::<_, i64>(0)?,
row.get::<_, Option<i64>>(1)?,
row.get::<_, String>(2)?,
row.get::<_, u64>(3)?,
row.get::<_, u64>(4)?,
))
},
) {
Ok(result) => result,
Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None),
Err(e) => return Err(e.into()),
}
}
Err(e) => return Err(e.into()),
}
} else {
let sql = r#"
SELECT id, parent_id, kind, byte_start, byte_end
FROM ast_nodes
WHERE byte_start <= ? AND byte_end >= ?
ORDER BY
CASE WHEN byte_start <= ? AND byte_end >= ? THEN 0 ELSE 1 END ASC,
(byte_end - byte_start) ASC
LIMIT 1
"#;
match conn.query_row(
sql,
[
byte_end as i64,
byte_start as i64,
byte_start as i64,
byte_end as i64,
],
|row| {
Ok((
row.get::<_, i64>(0)?,
row.get::<_, Option<i64>>(1)?,
row.get::<_, String>(2)?,
row.get::<_, u64>(3)?,
row.get::<_, u64>(4)?,
))
},
) {
Ok(result) => result,
Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None),
Err(e) => return Err(e.into()),
}
};
let mut ctx = AstContext {
ast_id,
kind,
parent_id,
byte_start: ast_byte_start,
byte_end: ast_byte_end,
depth: None,
parent_kind: None,
children_count_by_kind: None,
decision_points: None,
};
if include_enriched {
ctx.depth = Some(calculate_ast_depth(conn, ast_id)?.unwrap_or(0));
ctx.parent_kind = get_parent_kind(conn, parent_id)?;
ctx.children_count_by_kind = Some(count_children_by_kind(conn, ast_id)?);
ctx.decision_points = Some(count_decision_points(conn, ast_id)?);
}
Ok(Some(ctx))
}
pub use language::{
expand_shorthand, expand_shorthand_with_language, expand_shorthands,
get_node_kinds_for_language, get_supported_languages, LanguageNodeKinds, AST_SHORTHANDS,
JAVASCRIPT_NODE_KINDS, PYTHON_NODE_KINDS, TYPESCRIPT_NODE_KINDS,
};
mod language;
#[cfg(test)]
mod tests;