tellaro-query-language 2.0.0

A flexible, human-friendly query language for searching and filtering structured data
Documentation
//! Tests for empty array operator semantics in TQL.
//!
//! This module tests how TQL operators behave with empty arrays to ensure
//! consistent and predictable behavior across all operators.
//!
//! Key differences from Python:
//! - Direct comparison like `tags = 'python'` on array fields doesn't check elements
//! - Use `any tags eq 'python'` to check if any element matches
//! - Use `tags contains 'python'` to check if any element contains the string

use serde_json::json;
use tellaro_query_language::evaluator::TqlEvaluator;
use tellaro_query_language::parser::TqlParser;

/// Helper function to evaluate a query against a record
fn evaluate_query(query: &str, record: &serde_json::Value) -> bool {
    let parser = TqlParser::new();
    let evaluator = TqlEvaluator::new();

    let ast = parser.parse(query).expect("Failed to parse query");
    evaluator
        .evaluate(&ast, record)
        .expect("Failed to evaluate")
}

/// Helper function to filter records matching a query
fn filter_records(query: &str, records: &[serde_json::Value]) -> Vec<serde_json::Value> {
    records
        .iter()
        .filter(|r| evaluate_query(query, r))
        .cloned()
        .collect()
}

fn setup_tag_records() -> Vec<serde_json::Value> {
    vec![
        json!({"id": 1, "tags": []}),
        json!({"id": 2, "tags": ["python", "rust"]}),
        json!({"id": 3, "tags": ["javascript"]}),
    ]
}

fn setup_score_records() -> Vec<serde_json::Value> {
    vec![
        json!({"id": 1, "scores": []}),
        json!({"id": 2, "scores": [50, 75]}),
        json!({"id": 3, "scores": [25]}),
    ]
}

/// Test ANY operator with empty array
#[test]
fn test_empty_array_any_operator() {
    let data = setup_tag_records();

    // ANY checks if any element matches - empty array should not match
    let results = filter_records("any tags eq 'python'", &data);

    assert_eq!(results.len(), 1);
    assert_eq!(results[0].get("id").and_then(|v| v.as_i64()), Some(2));
}

/// Test ALL operator with empty array
#[test]
fn test_empty_array_all_operator() {
    let data = setup_tag_records();

    // ALL checks if all elements match
    // Empty array behavior: returns false (no elements to satisfy condition)
    let results = filter_records("all tags eq 'python'", &data);

    // Only record 2 has python tags, but also has rust, so ALL doesn't match
    // Record 3 has javascript, not python
    // Record 1 is empty - behavior depends on implementation
    assert_eq!(results.len(), 0);
}

/// Test NOT ANY (NONE) operator with empty array
#[test]
fn test_empty_array_not_any_operator() {
    let data = setup_tag_records();

    // NONE checks if no element matches
    // Empty array: true (no elements satisfy condition)
    let results = filter_records("none tags eq 'python'", &data);

    assert_eq!(results.len(), 2);
    let ids: Vec<i64> = results
        .iter()
        .filter_map(|r| r.get("id").and_then(|v| v.as_i64()))
        .collect();
    assert!(ids.contains(&1)); // Empty array
    assert!(ids.contains(&3)); // javascript only
}

/// Test NOT ALL operator with empty array
#[test]
fn test_empty_array_not_all_operator() {
    let data = setup_tag_records();

    // NOT ALL: true if not all elements match
    let results = filter_records("not all tags eq 'python'", &data);

    assert_eq!(results.len(), 3); // All records match
}

/// Test exists operator with empty array
#[test]
fn test_empty_array_exists() {
    let data = setup_tag_records();

    // Field exists even if it's an empty array
    let results = filter_records("tags exists", &data);

    assert_eq!(results.len(), 3);
}

/// Test greater-than operator with array elements using ANY
#[test]
fn test_empty_array_greater_than() {
    let data = setup_score_records();

    // Use ANY to check if any score > 60
    let results = filter_records("any scores gt 60", &data);

    // Only record 2 has scores > 60 (75)
    assert_eq!(results.len(), 1);
    assert_eq!(results[0].get("id").and_then(|v| v.as_i64()), Some(2));
}

/// Test less-than operator with array elements using ANY
#[test]
fn test_empty_array_less_than() {
    let data = setup_score_records();

    // Use ANY to check if any score < 60
    let results = filter_records("any scores lt 60", &data);

    // Record 2 (50) and Record 3 (25) have scores < 60
    assert_eq!(results.len(), 2);
    let ids: Vec<i64> = results
        .iter()
        .filter_map(|r| r.get("id").and_then(|v| v.as_i64()))
        .collect();
    assert!(ids.contains(&2));
    assert!(ids.contains(&3));
}

/// Test contains operator with array - checks substring in any element
#[test]
fn test_empty_array_contains_with_any() {
    let data = setup_tag_records();

    // Use any + contains to check if any tag contains 'py'
    let results = filter_records("any tags contains 'py'", &data);

    // Only record 2 has python (contains 'py')
    assert_eq!(results.len(), 1);
    assert_eq!(results[0].get("id").and_then(|v| v.as_i64()), Some(2));
}

/// Test startswith operator with array using ANY
#[test]
fn test_empty_array_startswith_with_any() {
    let data = setup_tag_records();

    // Check if any tag starts with 'py'
    let results = filter_records("any tags startswith 'py'", &data);

    // Only record 2 has python
    assert_eq!(results.len(), 1);
    assert_eq!(results[0].get("id").and_then(|v| v.as_i64()), Some(2));
}

/// Test endswith operator with array using ANY
#[test]
fn test_empty_array_endswith_with_any() {
    let data = setup_tag_records();

    // Check if any tag ends with 'on' (python)
    let results = filter_records("any tags endswith 'on'", &data);

    // Only record 2 has python (ends with 'on')
    assert_eq!(results.len(), 1);
    assert_eq!(results[0].get("id").and_then(|v| v.as_i64()), Some(2));
}

/// Test query with multiple records having empty arrays
#[test]
fn test_multiple_empty_arrays_with_any() {
    let data = vec![
        json!({"id": 1, "tags": []}),
        json!({"id": 2, "tags": []}),
        json!({"id": 3, "tags": ["python"]}),
    ];

    let results = filter_records("any tags eq 'python'", &data);

    // Only id=3 matches
    assert_eq!(results.len(), 1);
    assert_eq!(results[0].get("id").and_then(|v| v.as_i64()), Some(3));
}

/// Test empty array in compound logical expression
#[test]
fn test_empty_array_with_compound_expression() {
    let data = setup_tag_records();

    let results = filter_records("id > 1 AND any tags eq 'python'", &data);

    // id > 1: records 2, 3
    // any tags eq 'python': record 2
    // Combined: record 2
    assert_eq!(results.len(), 1);
    assert_eq!(results[0].get("id").and_then(|v| v.as_i64()), Some(2));
}

/// Test NONE with empty arrays
#[test]
fn test_none_with_empty_arrays() {
    let data = vec![
        json!({"id": 1, "tags": []}),
        json!({"id": 2, "tags": ["python"]}),
        json!({"id": 3, "tags": ["java"]}),
    ];

    // NONE tags eq 'python' - no element equals python
    let results = filter_records("none tags eq 'python'", &data);

    // Record 1 (empty) and Record 3 (java) match
    assert_eq!(results.len(), 2);
    let ids: Vec<i64> = results
        .iter()
        .filter_map(|r| r.get("id").and_then(|v| v.as_i64()))
        .collect();
    assert!(ids.contains(&1));
    assert!(ids.contains(&3));
}

/// Test ALL with numeric comparison
#[test]
fn test_all_with_numeric_comparison() {
    let data = vec![
        json!({"id": 1, "scores": []}),
        json!({"id": 2, "scores": [80, 90]}),
        json!({"id": 3, "scores": [50, 60]}),
    ];

    // ALL scores > 70
    let results = filter_records("all scores gt 70", &data);

    // Only record 2 has all scores > 70
    assert_eq!(results.len(), 1);
    assert_eq!(results[0].get("id").and_then(|v| v.as_i64()), Some(2));
}