grapha 0.3.0

Blazingly fast code intelligence CLI and MCP server for Swift and Rust
Documentation
use std::{collections::HashMap, path::PathBuf};

use rusqlite::{Connection, params};

use grapha_core::graph::{EdgeKind, Graph, Node, NodeKind, NodeRole, Span, Visibility};

use super::{SqliteStore, compat, schema, str_to_enum};

pub(super) fn load_filtered(
    store: &SqliteStore,
    edge_kinds: Option<&[EdgeKind]>,
    metadata_key_prefix: Option<&str>,
) -> anyhow::Result<Graph> {
    let conn = store.open()?;
    let schema_version = schema::schema_version(&conn)?;
    schema::create_tables(&conn)?;

    let version = load_version(&conn)?;
    let nodes = load_nodes(&conn, metadata_key_prefix)?;
    let (edge_where, edge_kind_params) = build_edge_where(edge_kinds);
    let edges = compat::load_edges(
        &conn,
        schema_version.as_deref(),
        &edge_where,
        &edge_kind_params,
    )?;

    Ok(Graph {
        version,
        nodes,
        edges,
    })
}

pub(super) fn load_version(conn: &Connection) -> anyhow::Result<String> {
    Ok(conn
        .query_row("SELECT value FROM meta WHERE key = 'version'", [], |row| {
            row.get(0)
        })
        .unwrap_or_else(|_| "0.1.0".to_string()))
}

pub(super) fn load_nodes(
    conn: &Connection,
    metadata_key_prefix: Option<&str>,
) -> anyhow::Result<Vec<Node>> {
    let mut nodes = Vec::new();

    if let Some(prefix) = metadata_key_prefix {
        let mut stmt = conn.prepare(
            "SELECT id, kind, name, file,
                    span_start_line, span_start_col, span_end_line, span_end_col,
                    visibility,
                    CASE WHEN instr(metadata, ?1) > 0 THEN metadata ELSE '{}' END,
                    role, NULL, NULL, module, NULL
             FROM nodes",
        )?;
        let mut rows = stmt.query(params![prefix])?;
        while let Some(row) = rows.next()? {
            nodes.push(decode_node_row(row)?);
        }
    } else {
        let mut stmt = conn.prepare(
            "SELECT id, kind, name, file,
                    span_start_line, span_start_col, span_end_line, span_end_col,
                    visibility, metadata, role, signature, doc_comment, module, snippet
             FROM nodes",
        )?;
        let mut rows = stmt.query([])?;
        while let Some(row) = rows.next()? {
            nodes.push(decode_node_row(row)?);
        }
    }

    Ok(nodes)
}

pub(super) fn build_edge_where(edge_kinds: Option<&[EdgeKind]>) -> (String, Vec<&'static str>) {
    let Some(edge_kinds) = edge_kinds.filter(|kinds| !kinds.is_empty()) else {
        return (String::new(), Vec::new());
    };

    let placeholders = (1..=edge_kinds.len())
        .map(|index| format!("?{index}"))
        .collect::<Vec<_>>()
        .join(", ");
    let params = edge_kinds
        .iter()
        .map(super::edge_kind_str)
        .collect::<Vec<_>>();
    (format!(" WHERE kind IN ({placeholders})"), params)
}

fn decode_node_row(row: &rusqlite::Row<'_>) -> anyhow::Result<Node> {
    let kind_str: String = row.get(1)?;
    let kind: NodeKind = str_to_enum(&kind_str)
        .map_err(|e| anyhow::anyhow!("invalid node kind '{kind_str}': {e}"))?;
    let vis_str: String = row.get(8)?;
    let visibility: Visibility = str_to_enum(&vis_str)
        .map_err(|e| anyhow::anyhow!("invalid visibility '{vis_str}': {e}"))?;
    let meta_str: String = row.get(9)?;
    let metadata: HashMap<String, String> = serde_json::from_str(&meta_str)?;
    let role: Option<NodeRole> = row
        .get::<_, Option<String>>(10)?
        .map(|s| serde_json::from_str(&s))
        .transpose()
        .map_err(|e| anyhow::anyhow!("invalid node role: {e}"))?;

    Ok(Node {
        id: row.get(0)?,
        kind,
        name: row.get(2)?,
        file: PathBuf::from(row.get::<_, String>(3)?),
        span: Span {
            start: [
                row.get::<_, i64>(4)? as usize,
                row.get::<_, i64>(5)? as usize,
            ],
            end: [
                row.get::<_, i64>(6)? as usize,
                row.get::<_, i64>(7)? as usize,
            ],
        },
        visibility,
        metadata,
        role,
        signature: row.get(11)?,
        doc_comment: row.get(12)?,
        module: row.get(13)?,
        snippet: row.get(14)?,
    })
}