icydb-core 0.180.20

IcyDB — A schema-first typed query engine and persistence runtime for Internet Computer canisters
Documentation
use crate::{
    db::predicate::{
        CoercionId, CoercionSpec, CompareOp, ComparePredicate, Predicate, normalize,
        normalize::{normalize_compare_value_for_kind, normalize_value_for_kind},
    },
    model::field::FieldKind,
    value::Value,
};

#[test]
fn normalize_and_dedups_identical_children_and_collapses_to_singleton() {
    let duplicated = Predicate::And(vec![
        Predicate::eq("rank".to_string(), Value::Nat64(7)),
        Predicate::eq("rank".to_string(), Value::Nat64(7)),
    ]);

    let normalized = normalize(&duplicated);

    assert_eq!(
        normalized,
        Predicate::Compare(ComparePredicate::eq("rank".to_string(), Value::Nat64(7))),
        "identical AND children should collapse to one predicate",
    );
}

#[test]
fn normalize_or_dedups_identical_children_and_collapses_to_singleton() {
    let duplicated = Predicate::Or(vec![
        Predicate::eq("rank".to_string(), Value::Nat64(7)),
        Predicate::eq("rank".to_string(), Value::Nat64(7)),
    ]);

    let normalized = normalize(&duplicated);

    assert_eq!(
        normalized,
        Predicate::Compare(ComparePredicate::eq("rank".to_string(), Value::Nat64(7))),
        "identical OR children should collapse to one predicate",
    );
}

#[test]
fn normalize_and_orders_cheaper_predicates_before_text_contains() {
    let mixed = Predicate::And(vec![
        Predicate::TextContains {
            field: "name".to_string(),
            value: Value::Text("ada".to_string()),
        },
        Predicate::eq("rank".to_string(), Value::Nat64(7)),
    ]);

    let normalized = normalize(&mixed);
    let Predicate::And(children) = normalized else {
        panic!("normalized mixed predicate should remain AND with two children");
    };
    assert_eq!(
        children.len(),
        2,
        "mixed AND should keep exactly two children"
    );
    assert!(
        matches!(children[0], Predicate::Compare(_)),
        "cheap compare predicate should be evaluated before text-contains predicate",
    );
    assert!(
        matches!(children[1], Predicate::TextContains { .. }),
        "text-contains predicate should be placed after cheap compare predicate",
    );
}

#[test]
fn normalize_and_conflicting_eq_literals_collapses_to_false() {
    let predicate = Predicate::And(vec![
        Predicate::eq("rank".to_string(), Value::Nat64(1)),
        Predicate::eq("rank".to_string(), Value::Nat64(2)),
    ]);

    let normalized = normalize(&predicate);

    assert_eq!(
        normalized,
        Predicate::False,
        "conflicting equalities in conjunction must collapse to false",
    );
}

#[test]
fn normalize_and_tightens_lower_bounds() {
    let predicate = Predicate::And(vec![
        Predicate::Compare(ComparePredicate::gt("rank".to_string(), Value::Nat64(3))),
        Predicate::Compare(ComparePredicate::gte("rank".to_string(), Value::Nat64(5))),
    ]);

    let normalized = normalize(&predicate);

    assert_eq!(
        normalized,
        Predicate::Compare(ComparePredicate::gte("rank".to_string(), Value::Nat64(5))),
        "conjunction should keep the stricter lower bound",
    );
}

#[test]
fn normalize_and_tightens_upper_bounds() {
    let predicate = Predicate::And(vec![
        Predicate::Compare(ComparePredicate::lt("rank".to_string(), Value::Nat64(9))),
        Predicate::Compare(ComparePredicate::lte("rank".to_string(), Value::Nat64(7))),
    ]);

    let normalized = normalize(&predicate);

    assert_eq!(
        normalized,
        Predicate::Compare(ComparePredicate::lte("rank".to_string(), Value::Nat64(7))),
        "conjunction should keep the stricter upper bound",
    );
}

#[test]
fn normalize_and_eq_with_satisfied_bound_collapses_to_eq() {
    let predicate = Predicate::And(vec![
        Predicate::eq("rank".to_string(), Value::Nat64(7)),
        Predicate::Compare(ComparePredicate::gt("rank".to_string(), Value::Nat64(5))),
    ]);

    let normalized = normalize(&predicate);

    assert_eq!(
        normalized,
        Predicate::Compare(ComparePredicate::eq("rank".to_string(), Value::Nat64(7))),
        "equality should subsume compatible lower-bound constraints",
    );
}

#[test]
fn normalize_and_eq_with_conflicting_bound_collapses_to_false() {
    let predicate = Predicate::And(vec![
        Predicate::eq("rank".to_string(), Value::Nat64(3)),
        Predicate::Compare(ComparePredicate::gt("rank".to_string(), Value::Nat64(5))),
    ]);

    let normalized = normalize(&predicate);

    assert_eq!(
        normalized,
        Predicate::False,
        "equality conflicting with a bound must collapse to false",
    );
}

#[test]
fn normalize_and_equal_lower_and_upper_collapse_to_eq() {
    let predicate = Predicate::And(vec![
        Predicate::Compare(ComparePredicate::with_coercion(
            "rank",
            CompareOp::Gte,
            Value::Nat64(11),
            crate::db::predicate::CoercionId::Strict,
        )),
        Predicate::Compare(ComparePredicate::with_coercion(
            "rank",
            CompareOp::Lte,
            Value::Nat64(11),
            crate::db::predicate::CoercionId::Strict,
        )),
    ]);

    let normalized = normalize(&predicate);

    assert_eq!(
        normalized,
        Predicate::Compare(ComparePredicate::eq("rank".to_string(), Value::Nat64(11))),
        "matching inclusive lower/upper bounds should collapse to equality",
    );
}

#[test]
fn normalize_and_crossed_bounds_collapse_to_false() {
    let predicate = Predicate::And(vec![
        Predicate::Compare(ComparePredicate::gt("rank".to_string(), Value::Nat64(9))),
        Predicate::Compare(ComparePredicate::lt("rank".to_string(), Value::Nat64(5))),
    ]);

    let normalized = normalize(&predicate);

    assert_eq!(
        normalized,
        Predicate::False,
        "crossed lower/upper bounds must collapse to false",
    );
}

#[test]
fn normalize_or_same_field_eq_collapses_to_in() {
    let predicate = Predicate::Or(vec![
        Predicate::Compare(ComparePredicate::with_coercion(
            "tag",
            CompareOp::Eq,
            Value::Text("beta".to_string()),
            CoercionId::Strict,
        )),
        Predicate::Compare(ComparePredicate::with_coercion(
            "tag",
            CompareOp::Eq,
            Value::Text("alpha".to_string()),
            CoercionId::Strict,
        )),
        Predicate::Compare(ComparePredicate::with_coercion(
            "tag",
            CompareOp::Eq,
            Value::Text("beta".to_string()),
            CoercionId::Strict,
        )),
    ]);

    let normalized = normalize(&predicate);
    let Predicate::Compare(compare) = normalized else {
        panic!("same-field strict OR-equality should collapse to one IN compare");
    };

    assert_eq!(compare.field, "tag".to_string());
    assert_eq!(compare.op, CompareOp::In);
    assert_eq!(compare.coercion.id, CoercionId::Strict);
    let Value::List(mut values) = compare.value else {
        panic!("collapsed OR-equality compare should carry list literal");
    };
    values.sort_by(Value::canonical_cmp);
    assert_eq!(
        values,
        vec![
            Value::Text("alpha".to_string()),
            Value::Text("beta".to_string()),
        ],
        "same-field strict OR-equality should collapse to deduplicated IN-list members",
    );
}

#[test]
fn normalize_or_mixed_eq_coercions_do_not_collapse_to_in() {
    let predicate = Predicate::Or(vec![
        Predicate::Compare(ComparePredicate::with_coercion(
            "tag",
            CompareOp::Eq,
            Value::Text("alpha".to_string()),
            CoercionId::Strict,
        )),
        Predicate::Compare(ComparePredicate::with_coercion(
            "tag",
            CompareOp::Eq,
            Value::Text("beta".to_string()),
            CoercionId::TextCasefold,
        )),
    ]);

    let normalized = normalize(&predicate);
    let Predicate::Or(children) = normalized else {
        panic!("mixed coercion OR-equality should remain OR in canonical form");
    };

    assert_eq!(children.len(), 2);
}

#[test]
fn normalize_or_list_equality_literals_do_not_collapse_to_in() {
    let predicate = Predicate::Or(vec![
        Predicate::Compare(ComparePredicate::with_coercion(
            "tags",
            CompareOp::Eq,
            Value::List(vec![Value::Text("a".to_string())]),
            CoercionId::Strict,
        )),
        Predicate::Compare(ComparePredicate::with_coercion(
            "tags",
            CompareOp::Eq,
            Value::List(vec![Value::Text("b".to_string())]),
            CoercionId::Strict,
        )),
    ]);

    let normalized = normalize(&predicate);
    let Predicate::Or(children) = normalized else {
        panic!("list-literal OR-equality should remain OR in canonical form");
    };

    assert_eq!(children.len(), 2);
}

#[test]
fn normalize_value_for_set_kind_canonicalizes_members() {
    let normalized = normalize_value_for_kind(
        "tags",
        &Value::List(vec![
            Value::Text("beta".to_string()),
            Value::Text("alpha".to_string()),
            Value::Text("beta".to_string()),
        ]),
        &FieldKind::Set(&FieldKind::Text { max_len: None }),
        &CoercionSpec::new(CoercionId::Strict),
        CompareOp::Eq,
    )
    .expect("set literal normalization should succeed");

    assert_eq!(
        normalized,
        Value::List(vec![
            Value::Text("alpha".to_string()),
            Value::Text("beta".to_string()),
        ]),
        "set literal normalization should sort and deduplicate members",
    );
}

#[test]
fn normalize_compare_value_for_in_kind_canonicalizes_members() {
    let normalized = normalize_compare_value_for_kind(
        "rank",
        CompareOp::In,
        &Value::List(vec![
            Value::Nat64(3),
            Value::Nat64(1),
            Value::Nat64(3),
            Value::Nat64(2),
        ]),
        &FieldKind::Nat64,
        &CoercionSpec::new(CoercionId::Strict),
    )
    .expect("IN literal normalization should succeed");

    assert_eq!(
        normalized,
        Value::List(vec![Value::Nat64(1), Value::Nat64(2), Value::Nat64(3)]),
        "IN literal normalization should sort and deduplicate members",
    );
}

#[test]
fn normalize_compare_value_for_not_in_kind_canonicalizes_members() {
    let normalized = normalize_compare_value_for_kind(
        "rank",
        CompareOp::NotIn,
        &Value::List(vec![
            Value::Nat64(3),
            Value::Nat64(1),
            Value::Nat64(3),
            Value::Nat64(2),
        ]),
        &FieldKind::Nat64,
        &CoercionSpec::new(CoercionId::Strict),
    )
    .expect("NOT IN literal normalization should succeed");

    assert_eq!(
        normalized,
        Value::List(vec![Value::Nat64(1), Value::Nat64(2), Value::Nat64(3)]),
        "NOT IN literal normalization should sort and deduplicate members",
    );
}