icydb-core 0.180.17

IcyDB — A schema-first typed query engine and persistence runtime for Internet Computer canisters
Documentation
use super::*;
use crate::{
    db::query::{
        intent::{IntentError, KeyAccessKind},
        plan::{FieldSlot, OrderDirection},
    },
    value::Value,
};

#[test]
fn query_intent_new_starts_in_load_scalar_mode() {
    let intent = QueryIntent::<u64>::new();

    std::assert_matches!(intent.mode(), QueryMode::Load(_));
    std::assert_matches!(
        intent.mode(),
        QueryMode::Load(LoadSpec {
            limit: None,
            offset: 0
        })
    );
    assert!(
        !intent.is_grouped(),
        "new intent must start in scalar shape without grouped policy flags"
    );
    std::assert_matches!(intent.mode(), QueryMode::Load(_));
}

#[test]
fn delete_mode_tracks_offset_in_mode_spec() {
    let intent = QueryIntent::<u64>::new().set_delete_mode().apply_offset(5);

    assert!(
        matches!(
            intent.mode(),
            QueryMode::Delete(DeleteSpec { offset: 5, .. })
        ),
        "offset requested in delete mode must remain visible on the delete spec"
    );
    assert!(
        matches!(intent.mode(), QueryMode::Delete(_)),
        "delete mode must expose delete-mode query state"
    );
}

#[test]
fn grouped_load_to_delete_preserves_grouping_policy_without_group_shape() {
    let mut intent = QueryIntent::<u64>::new();
    let _ = intent.ensure_grouped_mut();
    assert!(
        intent.grouped().is_some(),
        "load mode grouped intent should expose grouped shape"
    );

    let intent = intent.set_delete_mode();

    std::assert_matches!(intent.mode(), QueryMode::Delete(_));
    assert!(
        intent.is_grouped(),
        "delete mode should preserve grouped-delete policy signal"
    );
    assert!(
        intent.grouped().is_none(),
        "delete mode must not carry grouped shape state"
    );
}

#[test]
fn grouped_scalar_flags_survive_mode_transition() {
    let mut intent = QueryIntent::<u64>::new();
    intent.scalar_mut().key_access_conflict = true;
    let _ = intent.ensure_grouped_mut();

    let intent = intent.set_delete_mode();

    assert!(
        intent.scalar().key_access_conflict,
        "mode transitions must preserve scalar conflict flags"
    );
}

#[test]
fn group_field_slot_deduplicates_by_slot_index() {
    let mut intent = QueryIntent::<u64>::new();
    intent.push_group_field_slot(FieldSlot::from_test_slot(4, "rank"));
    intent.push_group_field_slot(FieldSlot::from_test_slot(4, "duplicate-rank"));

    let grouped = intent
        .grouped()
        .expect("grouped shape should be materialized after grouped slot push");

    assert_eq!(
        grouped.group.group_fields.len(),
        1,
        "group field slots should be deduplicated by stable model slot index"
    );
}

#[test]
fn having_clause_requires_grouped_shape() {
    let mut intent = QueryIntent::<u64>::new();

    let result = intent.push_having_expr(Expr::Literal(Value::Bool(true)));

    assert!(
        matches!(result, Err(IntentError::HavingRequiresGroupBy)),
        "having clauses should reject scalar shape"
    );
}

#[test]
fn delete_grouping_policy_accepts_having_clause_when_group_requested() {
    let mut intent = QueryIntent::<u64>::new();
    intent.push_group_field_slot(FieldSlot::from_test_slot(0, "id"));

    let mut intent = intent.set_delete_mode();
    let result = intent.push_having_expr(Expr::Literal(Value::Bool(true)));

    assert!(
        result.is_ok(),
        "delete mode should preserve grouped-delete policy signal for having checks"
    );
    assert!(
        intent.grouped().is_none(),
        "delete mode should not materialize grouped shape state"
    );
    assert!(
        intent.is_grouped(),
        "delete mode should keep grouped policy flag after having clause"
    );
}

#[test]
fn append_predicate_ands_multiple_filters() {
    let mut intent = QueryIntent::<u64>::new();
    intent.append_predicate(Predicate::True);
    intent.append_predicate(Predicate::False);

    assert!(
        matches!(
            intent
                .scalar()
                .filter
                .as_ref()
                .and_then(NormalizedFilter::predicate_subset),
            Some(Predicate::And(clauses)) if clauses.len() == 2
        ),
        "multiple filters should be preserved as a stable AND chain"
    );
}

#[test]
fn push_order_terms_preserve_declared_order_sequence() {
    let mut intent = QueryIntent::<u64>::new();
    intent.push_order_term(crate::db::asc("rank").lower());
    intent.push_order_term(crate::db::desc("created_at").lower());

    let fields = intent
        .scalar()
        .order
        .as_ref()
        .expect("order should exist after order helper calls")
        .fields
        .clone();

    assert_eq!(
        fields,
        vec![
            crate::db::query::plan::OrderTerm::field("rank", OrderDirection::Asc),
            crate::db::query::plan::OrderTerm::field("created_at", OrderDirection::Desc),
        ],
        "typed order-term sequence should match user declaration order"
    );
}

#[test]
fn key_access_conflict_flag_only_flips_for_mixed_access_kinds() {
    let mut intent = QueryIntent::<u64>::new();
    intent.set_by_id(10);
    intent.set_by_id(20);

    assert!(
        !intent.scalar().key_access_conflict,
        "reusing the same key access kind should not mark conflicts"
    );
    assert!(
        matches!(
            intent.scalar().key_access.as_ref().map(|state| state.kind),
            Some(KeyAccessKind::Single)
        ),
        "latest same-kind key access should remain single-key access"
    );

    intent.set_only(20);

    assert!(
        intent.scalar().key_access_conflict,
        "mixing key access kinds should mark intent key-access conflict"
    );
    assert!(
        matches!(
            intent.scalar().key_access.as_ref().map(|state| state.kind),
            Some(KeyAccessKind::Only)
        ),
        "latest mixed-kind key access should keep most recent origin kind"
    );
}