icydb-core 0.94.3

IcyDB — A schema-first typed query engine and persistence runtime for Internet Computer canisters
Documentation
use super::*;

#[test]
fn grouped_projection_arithmetic_over_group_field_evaluates() {
    let group_fields = [FieldSlot::from_parts_for_test(1, "rank")];
    let aggregate_execution_specs: [GroupedAggregateExecutionSpec; 0] = [];
    let grouped_row = GroupedRowView::new(
        &[Value::Int(7)],
        &[],
        group_fields.as_slice(),
        aggregate_execution_specs.as_slice(),
    );
    let expr = Expr::Binary {
        op: BinaryOp::Add,
        left: Box::new(Expr::Field(FieldId::new("rank"))),
        right: Box::new(Expr::Literal(Value::Int(2))),
    };

    let value = eval_expr_grouped(&expr, &grouped_row).expect("grouped arithmetic should evaluate");
    assert_eq!(
        value.cmp_numeric(&Value::Int(9)),
        Some(Ordering::Equal),
        "grouped arithmetic projection should evaluate over grouped keys",
    );
}

#[test]
fn grouped_projection_supports_numeric_equality_widening() {
    let group_fields = [FieldSlot::from_parts_for_test(1, "rank")];
    let aggregate_execution_specs: [GroupedAggregateExecutionSpec; 0] = [];
    let grouped_row = GroupedRowView::new(
        &[Value::Int(7)],
        &[],
        group_fields.as_slice(),
        aggregate_execution_specs.as_slice(),
    );
    let expr = Expr::Binary {
        op: BinaryOp::Eq,
        left: Box::new(Expr::Field(FieldId::new("rank"))),
        right: Box::new(Expr::Literal(Value::Uint(7))),
    };

    let value = eval_expr_grouped(&expr, &grouped_row)
        .expect("grouped numeric equality should widen deterministically");
    assert_eq!(value, Value::Bool(true));
}

#[test]
fn grouped_projection_rejects_numeric_and_non_numeric_equality_mix() {
    let group_fields = [
        FieldSlot::from_parts_for_test(1, "rank"),
        FieldSlot::from_parts_for_test(2, "label"),
    ];
    let aggregate_execution_specs: [GroupedAggregateExecutionSpec; 0] = [];
    let key_values = [Value::Int(7), Value::Text("label-7".to_string())];
    let grouped_row = GroupedRowView::new(
        key_values.as_slice(),
        &[],
        group_fields.as_slice(),
        aggregate_execution_specs.as_slice(),
    );
    let expr = Expr::Binary {
        op: BinaryOp::Eq,
        left: Box::new(Expr::Field(FieldId::new("rank"))),
        right: Box::new(Expr::Field(FieldId::new("label"))),
    };

    let err = eval_expr_grouped(&expr, &grouped_row)
        .expect_err("grouped mixed numeric/non-numeric equality should fail");
    assert!(matches!(
        err,
        crate::db::executor::projection::ProjectionEvalError::InvalidBinaryOperands { op, .. }
            if op == "eq"
    ));
}

#[test]
fn grouped_projection_mixing_aggregate_and_arithmetic_evaluates() {
    let group_fields = [FieldSlot::from_parts_for_test(1, "rank")];
    let aggregate_execution_specs = grouped_execution_specs([sum("rank")]);
    let grouped_row = GroupedRowView::new(
        &[Value::Int(7)],
        &[Value::Int(40)],
        group_fields.as_slice(),
        aggregate_execution_specs.as_slice(),
    );
    let expr = Expr::Binary {
        op: BinaryOp::Add,
        left: Box::new(Expr::Aggregate(sum("rank"))),
        right: Box::new(Expr::Literal(Value::Int(2))),
    };

    let value = eval_expr_grouped(&expr, &grouped_row)
        .expect("grouped aggregate arithmetic projection should evaluate");
    assert_eq!(
        value.cmp_numeric(&Value::Int(42)),
        Some(Ordering::Equal),
        "grouped projections must evaluate aggregate+scalar arithmetic deterministically",
    );
}

#[test]
fn grouped_projection_function_calls_over_grouped_fields_evaluate() {
    let group_fields = [FieldSlot::from_parts_for_test(1, "label")];
    let aggregate_execution_specs: [GroupedAggregateExecutionSpec; 0] = [];
    let key_values = [Value::Text("  Ada  ".to_string())];
    let grouped_row = GroupedRowView::new(
        key_values.as_slice(),
        &[],
        group_fields.as_slice(),
        aggregate_execution_specs.as_slice(),
    );
    let expr = Expr::FunctionCall {
        function: crate::db::query::plan::expr::Function::Trim,
        args: vec![Expr::Field(FieldId::new("label"))],
    };

    let value = eval_expr_grouped(&expr, &grouped_row)
        .expect("grouped projection should evaluate function calls over grouped fields");
    assert_eq!(value, Value::Text("Ada".to_string()));
}

#[test]
fn grouped_projection_alias_wrapping_is_semantic_no_op() {
    let group_fields = [FieldSlot::from_parts_for_test(1, "rank")];
    let aggregate_execution_specs = grouped_execution_specs([sum("rank")]);
    let grouped_row = GroupedRowView::new(
        &[Value::Int(7)],
        &[Value::Int(40)],
        group_fields.as_slice(),
        aggregate_execution_specs.as_slice(),
    );
    let plain = Expr::Binary {
        op: BinaryOp::Add,
        left: Box::new(Expr::Aggregate(sum("rank"))),
        right: Box::new(Expr::Literal(Value::Int(2))),
    };
    let aliased = Expr::Alias {
        expr: Box::new(Expr::Binary {
            op: BinaryOp::Add,
            left: Box::new(Expr::Aggregate(sum("rank"))),
            right: Box::new(Expr::Literal(Value::Int(2))),
        }),
        name: Alias::new("sum_plus_two"),
    };

    let plain_value =
        eval_expr_grouped(&plain, &grouped_row).expect("plain grouped expression should work");
    let alias_value =
        eval_expr_grouped(&aliased, &grouped_row).expect("aliased grouped expression should work");
    assert_eq!(
        plain_value, alias_value,
        "grouped alias wrapping must not change expression values",
    );
}

#[test]
fn grouped_projection_column_order_is_stable() {
    let group_fields = [FieldSlot::from_parts_for_test(1, "rank")];
    let aggregate_execution_specs = grouped_execution_specs([count(), sum("rank")]);
    let grouped_row = GroupedRowView::new(
        &[Value::Int(7)],
        &[Value::Uint(3), Value::Int(40)],
        group_fields.as_slice(),
        aggregate_execution_specs.as_slice(),
    );
    let projection = ProjectionSpec::from_fields_for_test(vec![
        ProjectionField::Scalar {
            expr: Expr::Aggregate(sum("rank")),
            alias: Some(Alias::new("sum_rank")),
        },
        ProjectionField::Scalar {
            expr: Expr::Aggregate(count()),
            alias: Some(Alias::new("count_all")),
        },
        ProjectionField::Scalar {
            expr: Expr::Binary {
                op: BinaryOp::Add,
                left: Box::new(Expr::Aggregate(count())),
                right: Box::new(Expr::Literal(Value::Int(1))),
            },
            alias: Some(Alias::new("count_plus_one")),
        },
    ]);

    let compiled = compile_grouped_projection_plan(
        &projection,
        group_fields.as_slice(),
        aggregate_execution_specs.as_slice(),
    )
    .expect("grouped projection should compile once");
    let values = evaluate_grouped_projection_values(compiled.as_slice(), &grouped_row)
        .expect("grouped projection vector should evaluate");

    assert_eq!(
        values.len(),
        3,
        "grouped projection must preserve declared field count",
    );
    assert_eq!(
        values[0].cmp_numeric(&Value::Int(40)),
        Some(Ordering::Equal),
        "first grouped projection output must follow projection declaration order",
    );
    assert_eq!(
        values[1].cmp_numeric(&Value::Uint(3)),
        Some(Ordering::Equal),
        "second grouped projection output must follow projection declaration order",
    );
    assert_eq!(
        values[2].cmp_numeric(&Value::Int(4)),
        Some(Ordering::Equal),
        "third grouped projection output must evaluate computed aggregate expression in order",
    );
}

#[test]
fn grouped_projection_ordering_preserves_input_group_order() {
    let group_fields = [FieldSlot::from_parts_for_test(1, "rank")];
    let aggregate_execution_specs = grouped_execution_specs([sum("rank")]);
    let projection = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
        expr: Expr::Binary {
            op: BinaryOp::Add,
            left: Box::new(Expr::Aggregate(sum("rank"))),
            right: Box::new(Expr::Literal(Value::Int(1))),
        },
        alias: Some(Alias::new("sum_plus_one")),
    }]);
    let grouped_inputs = vec![
        (vec![Value::Int(1)], vec![Value::Int(10)]),
        (vec![Value::Int(2)], vec![Value::Int(20)]),
        (vec![Value::Int(3)], vec![Value::Int(30)]),
    ];
    let mut observed = Vec::new();
    let compiled = compile_grouped_projection_plan(
        &projection,
        group_fields.as_slice(),
        aggregate_execution_specs.as_slice(),
    )
    .expect("grouped projection should compile once");
    for (key_values, aggregate_values) in grouped_inputs {
        let row_view = GroupedRowView::new(
            key_values.as_slice(),
            aggregate_values.as_slice(),
            group_fields.as_slice(),
            aggregate_execution_specs.as_slice(),
        );
        let evaluated = evaluate_grouped_projection_values(compiled.as_slice(), &row_view)
            .expect("grouped projection should evaluate per-row");
        observed.push(evaluated[0].clone());
    }

    let expected = [Value::Int(11), Value::Int(21), Value::Int(31)];
    for (actual, expected_value) in observed.into_iter().zip(expected) {
        assert_eq!(
            actual.cmp_numeric(&expected_value),
            Some(Ordering::Equal),
            "grouped projection evaluation order must preserve grouped row order",
        );
    }
}