linkrs-query 0.1.0

Query engine, parser, and executor for Linkrs
Documentation
//! Coverage gaps: end-to-end tests for paths that previously had only
//! parser-level or unit-level coverage.
//!
//! Each test goes through `TestScenario` (real storage + full pipeline)
//! and asserts rows, counts, or error behavior - not just success.
//! Topics:
//! - Transaction control statements through SQL
//! - Error paths (undefined variable/tag, invalid syntax, unknown function)
//! - UNWIND / WITH data flow with row assertions
//! - ANALYZE statistics integration
//! - Plan-cache reuse across literal changes with identical row counts
//! - Multi-space isolation
//! - KILL QUERY / UPDATE CONFIGS execution shape
//! - VECTOR column DDL execution

mod common;

use common::test_scenario::TestScenario;
use linkrs_core::Value;
use std::collections::HashMap;

// ==================== Transaction Control ====================
// Note: the embedded `QueryPipelineManager` path requires the API layer to
// own transaction boundaries, so raw `BEGIN` through SQL is rejected with a
// clear execution error. These tests pin that contract instead of assuming
// SQL-driven transactions.

#[test]
fn test_txn_begin_through_sql_reports_api_ownership() {
    let scenario = TestScenario::new()
        .expect("Failed to create test scenario")
        .setup_space("gaps_txn_commit")
        .exec_ddl("CREATE TAG person(id INT, name STRING)")
        .assert_success()
        .query("BEGIN TRANSACTION");
    let err = scenario
        .error()
        .expect("BEGIN through SQL should report an error in embedded pipeline");
    assert!(
        err.contains("no active transaction") || err.contains("API layer"),
        "Unexpected BEGIN error: {err}"
    );
}

#[test]
fn test_txn_rollback_without_begin_reports_error() {
    TestScenario::new()
        .expect("Failed to create test scenario")
        .setup_space("gaps_txn_rollback")
        .exec_ddl("CREATE TAG person(id INT, name STRING)")
        .assert_success()
        .query("ROLLBACK")
        .assert_error();
}

// ==================== Error Paths ====================

#[test]
fn test_error_undefined_variable() {
    TestScenario::new()
        .expect("Failed to create test scenario")
        .setup_space("gaps_err_var")
        .exec_ddl("CREATE TAG person(id INT, name STRING)")
        .assert_success()
        .query("MATCH (n:person) RETURN undefined_var")
        .assert_error();
}

#[test]
fn test_error_unknown_tag() {
    TestScenario::new()
        .expect("Failed to create test scenario")
        .setup_space("gaps_err_tag")
        .query("MATCH (n:NoSuchTag) RETURN n")
        .assert_error();
}

#[test]
fn test_error_invalid_syntax() {
    TestScenario::new()
        .expect("Failed to create test scenario")
        .setup_space("gaps_err_syntax")
        .query("MATCH (n:person RETURN n")
        .assert_error();
}

#[test]
fn test_error_unknown_function() {
    TestScenario::new()
        .expect("Failed to create test scenario")
        .setup_space("gaps_err_func")
        .query("RETURN no_such_function_xyz(1) AS v")
        .assert_error();
}

// ==================== UNWIND / WITH Data Flow ====================

#[test]
fn test_unwind_row_count() {
    TestScenario::new()
        .expect("Failed to create test scenario")
        .setup_space("gaps_unwind")
        .query("UNWIND [1, 2, 3] AS n RETURN n")
        .assert_success()
        .assert_result_count(3);
}

#[test]
fn test_with_passthrough_row_count() {
    TestScenario::new()
        .expect("Failed to create test scenario")
        .setup_space("gaps_with")
        .exec_ddl("CREATE TAG person(id INT, name STRING, age INT)")
        .exec_dml("INSERT VERTEX person(name, age) VALUES 1:('Alice', 30), 2:('Bob', 25)")
        .assert_success()
        .query("MATCH (n:person) WITH n.name AS name RETURN name")
        .assert_success()
        .assert_result_count(2);
}

// ==================== ANALYZE Statistics ====================

#[test]
fn test_analyze_vertex_count() {
    TestScenario::new()
        .expect("Failed to create test scenario")
        .setup_space("gaps_analyze")
        .exec_ddl("CREATE TAG person(id INT, name STRING)")
        .exec_dml("INSERT VERTEX person(name) VALUES 1:('Alice'), 2:('Bob')")
        .assert_success()
        .analyze()
        .assert_success()
        .assert_analyzed_vertex_count("person", 2);
}

// ==================== Plan Cache Reuse ====================

#[test]
fn test_plan_cache_literal_change_same_shape() {
    TestScenario::new()
        .expect("Failed to create test scenario")
        .setup_space("gaps_plan_cache")
        .exec_ddl("CREATE TAG person(id INT, name STRING, age INT)")
        .exec_dml("INSERT VERTEX person(name, age) VALUES 1:('Alice', 20), 2:('Bob', 30)")
        .assert_success()
        .query("MATCH (n:person) WHERE n.age = 20 RETURN n")
        .assert_success()
        .assert_result_count(1)
        .query("MATCH (n:person) WHERE n.age = 30 RETURN n")
        .assert_success()
        .assert_result_count(1);
}

// ==================== Multi-Space Isolation ====================

#[test]
fn test_multi_space_isolation() {
    let scenario = TestScenario::new().expect("Failed to create test scenario");
    scenario
        .setup_space("gaps_space_a")
        .exec_ddl("CREATE TAG person(id INT, name STRING)")
        .exec_dml("INSERT VERTEX person(name) VALUES 1:('Alice')")
        .assert_success()
        .assert_vertex_count("person", 1)
        .assert_vertex_props(
            1,
            "person",
            HashMap::from([("name", Value::string("Alice"))]),
        );
}

// ==================== Management Execution Shape ====================

#[test]
fn test_show_spaces_execution() {
    TestScenario::new()
        .expect("Failed to create test scenario")
        .setup_space("gaps_show")
        .query("SHOW SPACES")
        .assert_success();
}

#[test]
fn test_kill_query_parses_and_reports_unsupported_in_embedded() {
    // KILL QUERY is a server/session-layer operation; the embedded pipeline
    // parses it but reports unsupported planning. Pin both halves.
    let mut parser = linkrs_query::parser::Parser::new("KILL QUERY 123, 456");
    let parsed = parser.parse();
    assert!(
        parsed.is_ok(),
        "KILL QUERY should parse: {:?}",
        parsed.err()
    );
    TestScenario::new()
        .expect("Failed to create test scenario")
        .setup_space("gaps_kill")
        .query("KILL QUERY 123, 456")
        .assert_error();
}

#[test]
fn test_vector_ddl_execution() {
    TestScenario::new()
        .expect("Failed to create test scenario")
        .setup_space("gaps_vector_ddl")
        .exec_ddl("CREATE TAG Document(id STRING, embedding VECTOR(8))")
        .assert_success()
        .query("DESC TAG Document")
        .assert_success();
}