velesdb-core 1.13.1

High-performance vector database engine written in Rust
Documentation
//! Tests for `ast` module

use super::ast::*;

#[test]
fn test_column_new() {
    let col = Column::new("id");
    assert_eq!(col.name, "id");
    assert!(col.alias.is_none());
}

#[test]
fn test_column_with_alias() {
    let col = Column::with_alias("payload.title", "title");
    assert_eq!(col.name, "payload.title");
    assert_eq!(col.alias, Some("title".to_string()));
}

#[test]
fn test_value_from_integer() {
    let v: Value = 42i64.into();
    assert_eq!(v, Value::Integer(42));
}

#[test]
fn test_value_from_float() {
    let v: Value = 2.5f64.into();
    assert_eq!(v, Value::Float(2.5));
}

#[test]
fn test_value_from_string() {
    let v: Value = "hello".into();
    assert_eq!(v, Value::String("hello".to_string()));
}

#[test]
fn test_value_from_bool() {
    let v: Value = true.into();
    assert_eq!(v, Value::Boolean(true));
}

#[test]
fn test_query_serialization() {
    let query = Query {
        let_bindings: vec![],
        compound: None,
        match_clause: None,
        dml: None,
        train: None,
        ddl: None,
        introspection: None,
        admin: None,
        select: SelectStatement {
            distinct: crate::velesql::DistinctMode::None,
            columns: SelectColumns::All,
            from: "documents".to_string(),
            from_alias: vec![],
            joins: vec![],
            where_clause: None,
            order_by: None,
            limit: Some(10),
            offset: None,
            with_clause: None,
            group_by: None,
            having: None,
            fusion_clause: None,
        },
    };

    let json = serde_json::to_string(&query).unwrap();
    let parsed: Query = serde_json::from_str(&json).unwrap();
    assert_eq!(query, parsed);
}

// ============================================================================
// Bug 4 regression: Display for ArithmeticOp and ArithmeticExpr
// ============================================================================

#[test]
fn test_arithmetic_op_display_add() {
    assert_eq!(format!("{}", ArithmeticOp::Add), "+");
}

#[test]
fn test_arithmetic_op_display_sub() {
    assert_eq!(format!("{}", ArithmeticOp::Sub), "-");
}

#[test]
fn test_arithmetic_op_display_mul() {
    assert_eq!(format!("{}", ArithmeticOp::Mul), "*");
}

#[test]
fn test_arithmetic_op_display_div() {
    assert_eq!(format!("{}", ArithmeticOp::Div), "/");
}

#[test]
fn test_arithmetic_expr_display_literal() {
    let expr = ArithmeticExpr::Literal(0.7);
    assert_eq!(format!("{expr}"), "0.7");
}

#[test]
fn test_arithmetic_expr_display_variable() {
    let expr = ArithmeticExpr::Variable("vector_score".to_string());
    assert_eq!(format!("{expr}"), "vector_score");
}

#[test]
fn test_arithmetic_expr_display_similarity_bare() {
    let expr = ArithmeticExpr::Similarity(Box::new(OrderByExpr::SimilarityBare));
    assert_eq!(format!("{expr}"), "similarity()");
}

#[test]
fn test_arithmetic_expr_display_binary_op() {
    let expr = ArithmeticExpr::BinaryOp {
        left: Box::new(ArithmeticExpr::Literal(0.7)),
        op: ArithmeticOp::Mul,
        right: Box::new(ArithmeticExpr::Variable("vector_score".to_string())),
    };
    assert_eq!(format!("{expr}"), "(0.7 * vector_score)");
}

#[test]
fn test_arithmetic_expr_display_complex() {
    // 0.7 * vector_score + 0.3 * graph_score
    let expr = ArithmeticExpr::BinaryOp {
        left: Box::new(ArithmeticExpr::BinaryOp {
            left: Box::new(ArithmeticExpr::Literal(0.7)),
            op: ArithmeticOp::Mul,
            right: Box::new(ArithmeticExpr::Variable("vector_score".to_string())),
        }),
        op: ArithmeticOp::Add,
        right: Box::new(ArithmeticExpr::BinaryOp {
            left: Box::new(ArithmeticExpr::Literal(0.3)),
            op: ArithmeticOp::Mul,
            right: Box::new(ArithmeticExpr::Variable("graph_score".to_string())),
        }),
    };
    assert_eq!(
        format!("{expr}"),
        "((0.7 * vector_score) + (0.3 * graph_score))"
    );
}

#[test]
fn test_arithmetic_expr_display_similarity_parameterized() {
    let expr = ArithmeticExpr::Similarity(Box::new(OrderByExpr::Similarity(SimilarityOrderBy {
        field: "embedding".to_string(),
        vector: VectorExpr::Parameter("alt_vec".to_string()),
    })));
    assert_eq!(format!("{expr}"), "similarity(embedding, $alt_vec)");
}

// ============================================================================
// Issue #486: Value::UnsignedInteger support
// ============================================================================

#[test]
fn test_value_from_u64() {
    let v: Value = 42u64.into();
    assert_eq!(v, Value::UnsignedInteger(42));
}

#[test]
fn test_value_unsigned_integer_to_json() {
    let v = Value::UnsignedInteger(42);
    assert_eq!(v.to_json(), serde_json::json!(42));
}

#[test]
fn test_value_unsigned_integer_to_json_large() {
    // i64::MAX + 1 — must not lose precision
    let large = 9_223_372_036_854_775_808_u64;
    let v = Value::UnsignedInteger(large);
    let json = v.to_json();
    assert_eq!(json.as_u64(), Some(large));
}

#[test]
fn test_value_unsigned_integer_to_json_max() {
    let v = Value::UnsignedInteger(u64::MAX);
    let json = v.to_json();
    assert_eq!(json.as_u64(), Some(u64::MAX));
}

#[test]
fn test_value_unsigned_integer_serialization_roundtrip() {
    let v = Value::UnsignedInteger(9_223_372_036_854_775_808);
    let json_str = serde_json::to_string(&v).expect("test: serialize");
    let parsed: Value = serde_json::from_str(&json_str).expect("test: deserialize");
    assert_eq!(v, parsed);
}

// ---------------------------------------------------------------------------
// `SelectColumns::to_display_names` — every SELECT-list variant must
// contribute a display name. The Python/WASM bindings consume this list as
// the column-metadata contract, so omitting a variant silently drops columns
// from their returned schema. These tests pin the full contract.
// ---------------------------------------------------------------------------

#[test]
fn test_display_names_all_wildcard() {
    let sc = SelectColumns::All;
    assert_eq!(sc.to_display_names(), vec!["*".to_string()]);
}

#[test]
fn test_display_names_columns_only() {
    let sc = SelectColumns::Columns(vec![Column::new("title"), Column::new("score")]);
    assert_eq!(
        sc.to_display_names(),
        vec!["title".to_string(), "score".to_string()]
    );
}

#[test]
fn test_display_names_similarity_score_with_alias() {
    let sc = SelectColumns::SimilarityScore(SimilarityScoreExpr {
        alias: Some("relevance".to_string()),
    });
    assert_eq!(sc.to_display_names(), vec!["relevance".to_string()]);
}

#[test]
fn test_display_names_similarity_score_no_alias() {
    let sc = SelectColumns::SimilarityScore(SimilarityScoreExpr { alias: None });
    assert_eq!(sc.to_display_names(), vec!["similarity".to_string()]);
}

#[test]
fn test_display_names_qualified_wildcard() {
    let sc = SelectColumns::QualifiedWildcard("ctx".to_string());
    assert_eq!(sc.to_display_names(), vec!["ctx.*".to_string()]);
}

/// Zero-tech-debt regression: the `Mixed` variant used to silently drop
/// `similarity_scores` and `qualified_wildcards` from its display names,
/// so bindings showed incomplete column lists for queries that combined
/// those features with regular columns.
#[test]
fn test_display_names_mixed_includes_all_variants() {
    let sc = SelectColumns::Mixed {
        columns: vec![Column::new("title"), Column::new("author")],
        aggregations: Vec::new(),
        similarity_scores: vec![SimilarityScoreExpr {
            alias: Some("score".to_string()),
        }],
        qualified_wildcards: vec!["ctx".to_string()],
        window_functions: vec![WindowFunction {
            function_type: WindowFunctionType::RowNumber,
            over_clause: OverClause {
                partition_by: Vec::new(),
                order_by: Vec::new(),
            },
            alias: Some("rn".to_string()),
        }],
    };

    // Expected order (mirrors the SELECT-list grammar):
    //   columns → aggregations → similarity_scores → qualified_wildcards → windows
    assert_eq!(
        sc.to_display_names(),
        vec![
            "title".to_string(),
            "author".to_string(),
            "score".to_string(),
            "ctx.*".to_string(),
            "rn".to_string(),
        ]
    );
}

/// Edge case: Mixed with only a similarity score and a qualified wildcard
/// (no regular columns). Pre-fix, this would return an empty `Vec`,
/// completely hiding the SELECT list from bindings.
#[test]
fn test_display_names_mixed_without_columns_still_exposes_score_and_wildcard() {
    let sc = SelectColumns::Mixed {
        columns: Vec::new(),
        aggregations: Vec::new(),
        similarity_scores: vec![SimilarityScoreExpr { alias: None }],
        qualified_wildcards: vec!["docs".to_string()],
        window_functions: Vec::new(),
    };

    assert_eq!(
        sc.to_display_names(),
        vec!["similarity".to_string(), "docs.*".to_string()]
    );
}