sqry-cli 6.0.17

CLI for sqry - semantic code search
Documentation
//! Integration scaffold for SQL graph coverage (Phase 3 Sprint 2).
//!
//! These tests are marked `#[ignore]` until the SQL `GraphBuilder` lands. They
//! already wire in the curated fixtures so we can flip them on once graph
//! extraction is functional.

mod common;
use common::sqry_bin;

use assert_cmd::Command;
use serde_json::Value;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use tempfile::TempDir;

fn sqry_cmd() -> Command {
    let path = sqry_bin();
    Command::new(path)
}

fn index_project(path: &Path) {
    sqry_cmd().arg("index").arg(path).assert().success();
}

fn copy_fixture_dir(relative: &str) -> TempDir {
    let binding = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    let root = binding.parent().expect("workspace root");
    let source = root.join(relative);
    assert!(source.exists(), "fixture directory {source:?} not found");

    let temp = TempDir::new().expect("create temp dir");
    copy_dir_all(&source, temp.path()).expect("copy fixture into temp dir");
    temp
}

fn copy_dir_all(src: &Path, dst: &Path) -> io::Result<()> {
    fs::create_dir_all(dst)?;
    for entry in fs::read_dir(src)? {
        let entry = entry?;
        let ty = entry.file_type()?;
        let dest_path = dst.join(entry.file_name());
        if ty.is_dir() {
            copy_dir_all(&entry.path(), &dest_path)?;
        } else if ty.is_file() {
            fs::copy(entry.path(), dest_path)?;
        }
    }
    Ok(())
}

#[test]
fn sql_graph_stats_reports_functions_triggers_and_table_operations() {
    let project = copy_fixture_dir("tests/fixtures/sql_banking");
    index_project(project.path());

    let output = sqry_cmd()
        .arg("graph")
        .arg("--path")
        .arg(project.path())
        .arg("--format")
        .arg("json")
        .arg("stats")
        .arg("--by-language")
        .arg("--by-file")
        .output()
        .expect("run sqry graph stats");
    assert!(
        output.status.success(),
        "graph stats command failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let stats: Value =
        serde_json::from_slice(&output.stdout).expect("stats output should be valid JSON");

    // SQL GraphBuilder should register SQL nodes in language summary
    let language_summary = stats["nodes_by_language"]
        .as_object()
        .expect("nodes_by_language map missing");
    assert!(
        language_summary
            .keys()
            .any(|lang| lang.eq("Sql") || lang.eq("SQL")),
        "expected SQL nodes in language summary, got {language_summary:?}"
    );

    // The schema.sql file should contain graph nodes
    let file_summary = stats["nodes_by_file"]
        .as_object()
        .expect("nodes_by_file map missing");
    assert!(
        file_summary.keys().any(|file| file.ends_with("schema.sql")),
        "expected schema.sql in nodes_by_file summary, got {file_summary:?}"
    );

    // Edge kind counts should reflect SQL relationships:
    // - TriggeredBy (trigger -> table)
    // - TableRead (function -> table for SELECT)
    // - TableWrite (function -> table for INSERT/UPDATE/DELETE)
    let edge_summary = stats["edges_by_kind"]
        .as_object()
        .expect("edges_by_kind map missing");

    // Verify we have table operation edges (TableRead and/or TableWrite)
    let has_table_edges = edge_summary.keys().any(|kind| {
        kind.contains("TableRead") || kind.contains("TableWrite") || kind.contains("TriggeredBy")
    });
    assert!(
        has_table_edges,
        "expected table operation edges (TableRead/TableWrite/TriggeredBy), got {edge_summary:?}"
    );
}

#[test]
fn sql_trace_path_from_function_to_table() {
    let project = copy_fixture_dir("tests/fixtures/sql_banking");
    index_project(project.path());

    // Trace path from close_account function to accounts table
    // The function performs INSERT (to archived_accounts) and DELETE (from accounts)
    let output = sqry_cmd()
        .arg("graph")
        .arg("--path")
        .arg(project.path())
        .arg("--format")
        .arg("text")
        .arg("trace-path")
        .arg("close_account")
        .arg("accounts")
        .output()
        .expect("run sqry graph trace-path");

    // Note: Trace-path may not find a path if symbols aren't in the same execution flow.
    // The main validation is that SQL GraphBuilder is working (tested in first test).
    // This test verifies the trace-path command doesn't crash on SQL input.
    // A missing path or command failure is acceptable for this fixture since:
    // - close_account and accounts are related via table operations, not call chains
    // - tree-sitter-sequel has grammar limitations (no CALL statement support)
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    // Command completed (success or expected failure with message)
    let completed_ok = output.status.success()
        || stderr.contains("No path found")
        || stderr.contains("Graph command failed")
        || stderr.contains("not found");

    assert!(
        completed_ok,
        "Expected command to complete (with or without finding path). stdout:\n{stdout}\nstderr:\n{stderr}"
    );
}