gobby-code 1.3.3

Fast Rust CLI for Gobby's code index — AST-aware search, symbol navigation, and dependency graph
Documentation
use super::{IndexRequest, index_files};
use crate::config::{Context, ProjectIndexScope};
use crate::db;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};

mod serial_db {
    use super::*;

    #[test]
    #[cfg_attr(
        not(gcode_postgres_tests),
        ignore = "requires a PostgreSQL test database URL"
    )]
    #[serial_test::serial(serial_db)]
    fn discovered_scan_deletes_stale_facts_when_ast_indexing_returns_none() {
        let (mut conn, database_url) = connect_test_db();
        let project_root = tempfile::tempdir().expect("project tempdir");
        let project_id = unique_test_project_id("gcode-discovered-skip-cleanup");
        cleanup_project(&mut conn, &project_id).expect("pre-clean project rows");
        let _cleanup = ProjectCleanup {
            database_url: database_url.clone(),
            project_id: project_id.clone(),
        };

        write_file(
            project_root.path(),
            "src/lib.rs",
            b"pub fn indexed() -> u8 { 1 }\n",
        );
        let ctx = test_context(
            database_url,
            project_root.path().to_path_buf(),
            project_id.clone(),
        );
        let initial = index_files(discovered_request(project_root.path(), true), &ctx)
            .expect("initial discovered index");
        assert_eq!(initial.indexed_files, 1);
        assert!(
            symbol_count(&mut conn, &project_id, "src/lib.rs") > 0,
            "initial discovered scan should index at least one symbol"
        );

        write_file(project_root.path(), "src/lib.rs", b"");
        let reindex = index_files(discovered_request(project_root.path(), false), &ctx)
            .expect("reindex discovered scan");

        assert_eq!(reindex.indexed_files, 0);
        assert_eq!(reindex.skipped_files, 1);
        assert_eq!(
            symbol_count(&mut conn, &project_id, "src/lib.rs"),
            0,
            "stale code_symbols rows should be deleted when AST indexing skips an indexed file"
        );
        assert_eq!(
            indexed_file_count(&mut conn, &project_id, "src/lib.rs"),
            0,
            "deleted file facts should not leave the file stale for every later scan"
        );
    }
}

fn connect_test_db() -> (postgres::Client, String) {
    let database_url = crate::test_env::postgres_test_database_url("stale cleanup tests");
    let conn = db::connect_readwrite(&database_url)
        .expect("connect stale cleanup PostgreSQL test database");
    (conn, database_url)
}

fn test_context(database_url: String, project_root: PathBuf, project_id: String) -> Context {
    Context {
        database_url,
        project_root,
        project_id,
        quiet: true,
        falkordb: None,
        qdrant: None,
        embedding: None,
        code_vectors: crate::config::CodeVectorSettings { vector_dim: None },
        indexing: gobby_core::config::IndexingConfig::default(),
        daemon_url: None,
        index_scope: ProjectIndexScope::Single,
    }
}

fn discovered_request(project_root: &Path, full: bool) -> IndexRequest {
    IndexRequest {
        project_root: project_root.to_path_buf(),
        path_filter: None,
        explicit_files: Vec::new(),
        full,
        require_cpp_semantics: false,
        sync_projections: false,
    }
}

fn write_file(root: &Path, rel: &str, contents: &[u8]) {
    let path = root.join(rel);
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent).expect("create parent");
    }
    std::fs::write(path, contents).expect("write file");
}

fn unique_test_project_id(prefix: &str) -> String {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("system time after epoch")
        .as_nanos();
    format!("{prefix}-{}-{nanos}", std::process::id())
}

struct ProjectCleanup {
    database_url: String,
    project_id: String,
}

impl Drop for ProjectCleanup {
    fn drop(&mut self) {
        if let Ok(mut conn) = db::connect_readwrite(&self.database_url) {
            let _ = cleanup_project(&mut conn, &self.project_id);
        }
    }
}

fn cleanup_project(conn: &mut postgres::Client, project_id: &str) -> anyhow::Result<()> {
    let mut tx = conn.transaction()?;
    tx.execute(
        "DELETE FROM code_calls WHERE project_id = $1",
        &[&project_id],
    )?;
    tx.execute(
        "DELETE FROM code_imports WHERE project_id = $1",
        &[&project_id],
    )?;
    tx.execute(
        "DELETE FROM code_content_chunks WHERE project_id = $1",
        &[&project_id],
    )?;
    tx.execute(
        "DELETE FROM code_symbols WHERE project_id = $1",
        &[&project_id],
    )?;
    tx.execute(
        "DELETE FROM code_indexed_files WHERE project_id = $1",
        &[&project_id],
    )?;
    tx.execute(
        "DELETE FROM code_indexed_projects WHERE id = $1",
        &[&project_id],
    )?;
    tx.commit()?;
    Ok(())
}

fn symbol_count(conn: &mut postgres::Client, project_id: &str, rel: &str) -> i64 {
    count_rows(
        conn,
        "SELECT COUNT(*) FROM code_symbols WHERE project_id = $1 AND file_path = $2",
        project_id,
        rel,
    )
}

fn indexed_file_count(conn: &mut postgres::Client, project_id: &str, rel: &str) -> i64 {
    count_rows(
        conn,
        "SELECT COUNT(*) FROM code_indexed_files WHERE project_id = $1 AND file_path = $2",
        project_id,
        rel,
    )
}

fn count_rows(conn: &mut postgres::Client, sql: &str, project_id: &str, rel: &str) -> i64 {
    conn.query_one(sql, &[&project_id, &rel])
        .expect("count rows")
        .try_get(0)
        .expect("row count")
}