icydb-core 0.179.5

IcyDB — A schema-first typed query engine and persistence runtime for Internet Computer canisters
Documentation
use crate::db::{
    codec::new_hash_sha256,
    query::{
        builder::{count, count_by, min_by, sum},
        fingerprint::projection_hash::hash_projection_structural_fingerprint,
        plan::expr::{Alias, BinaryOp, Expr, FieldId, ProjectionField, ProjectionSpec},
    },
};
use crate::{types::Decimal, value::Value};

fn hash_projection(spec: &ProjectionSpec) -> [u8; 32] {
    let mut hasher = new_hash_sha256();
    hash_projection_structural_fingerprint(&mut hasher, spec);
    super::super::finalize_sha256_digest(hasher)
}

#[test]
fn alias_is_excluded_from_projection_semantic_hash_identity() {
    let base = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
        expr: Expr::Field(FieldId::new("rank")),
        alias: None,
    }]);
    let aliased = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
        expr: Expr::Alias {
            expr: Box::new(Expr::Field(FieldId::new("rank"))),
            name: Alias::new("rank_expr"),
        },
        alias: Some(Alias::new("rank_column")),
    }]);

    assert_eq!(hash_projection(&base), hash_projection(&aliased));
}

#[test]
fn projection_field_order_remains_hash_significant() {
    let in_order = ProjectionSpec::from_fields_for_test(vec![
        ProjectionField::Scalar {
            expr: Expr::Field(FieldId::new("id")),
            alias: None,
        },
        ProjectionField::Scalar {
            expr: Expr::Field(FieldId::new("rank")),
            alias: None,
        },
    ]);
    let swapped_order = ProjectionSpec::from_fields_for_test(vec![
        ProjectionField::Scalar {
            expr: Expr::Field(FieldId::new("rank")),
            alias: None,
        },
        ProjectionField::Scalar {
            expr: Expr::Field(FieldId::new("id")),
            alias: None,
        },
    ]);

    assert_ne!(hash_projection(&in_order), hash_projection(&swapped_order));
}

#[test]
fn aggregate_identity_is_hash_significant() {
    let sum_rank = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
        expr: Expr::Aggregate(sum("rank")),
        alias: None,
    }]);
    let count_all = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
        expr: Expr::Aggregate(count()),
        alias: None,
    }]);

    assert_ne!(hash_projection(&sum_rank), hash_projection(&count_all));
}

#[test]
fn extrema_distinct_modifier_is_not_projection_hash_significant() {
    let min_rank = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
        expr: Expr::Aggregate(min_by("rank")),
        alias: None,
    }]);
    let min_distinct_rank = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
        expr: Expr::Aggregate(min_by("rank").distinct()),
        alias: None,
    }]);

    assert_eq!(
        hash_projection(&min_rank),
        hash_projection(&min_distinct_rank)
    );
}

#[test]
fn count_distinct_modifier_remains_projection_hash_significant() {
    let count_rank = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
        expr: Expr::Aggregate(count_by("rank")),
        alias: None,
    }]);
    let count_distinct_rank = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
        expr: Expr::Aggregate(count_by("rank").distinct()),
        alias: None,
    }]);

    assert_ne!(
        hash_projection(&count_rank),
        hash_projection(&count_distinct_rank),
    );
}

#[test]
fn numeric_literal_decimal_scale_is_canonicalized_for_hash_identity() {
    let decimal_one_scale_1 = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
        expr: Expr::Literal(Value::Decimal(Decimal::new(10, 1))),
        alias: None,
    }]);
    let decimal_one_scale_2 = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
        expr: Expr::Literal(Value::Decimal(Decimal::new(100, 2))),
        alias: None,
    }]);

    assert_eq!(
        hash_projection(&decimal_one_scale_1),
        hash_projection(&decimal_one_scale_2),
        "decimal literal scale-only differences must not fragment identity",
    );
}

#[test]
fn literal_numeric_subtype_remains_hash_significant_when_observable() {
    let int_literal = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
        expr: Expr::Literal(Value::Int64(1)),
        alias: None,
    }]);
    let decimal_literal = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
        expr: Expr::Literal(Value::Decimal(Decimal::new(10, 1))),
        alias: None,
    }]);

    assert_ne!(
        hash_projection(&int_literal),
        hash_projection(&decimal_literal),
        "top-level literal subtype is observable and remains identity-significant",
    );
}

#[test]
fn numeric_promotion_paths_do_not_fragment_hash_identity() {
    let int_plus_int = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
        expr: Expr::Binary {
            op: BinaryOp::Add,
            left: Box::new(Expr::Literal(Value::Int64(1))),
            right: Box::new(Expr::Literal(Value::Int64(2))),
        },
        alias: None,
    }]);
    let int_plus_decimal = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
        expr: Expr::Binary {
            op: BinaryOp::Add,
            left: Box::new(Expr::Literal(Value::Int64(1))),
            right: Box::new(Expr::Literal(Value::Decimal(Decimal::new(20, 1)))),
        },
        alias: None,
    }]);
    let decimal_plus_int = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
        expr: Expr::Binary {
            op: BinaryOp::Add,
            left: Box::new(Expr::Literal(Value::Decimal(Decimal::new(10, 1)))),
            right: Box::new(Expr::Literal(Value::Int64(2))),
        },
        alias: None,
    }]);

    let hash_int_plus_int = hash_projection(&int_plus_int);
    let hash_int_plus_decimal = hash_projection(&int_plus_decimal);
    let hash_decimal_plus_int = hash_projection(&decimal_plus_int);

    assert_eq!(hash_int_plus_int, hash_int_plus_decimal);
    assert_eq!(hash_int_plus_int, hash_decimal_plus_int);
}

#[test]
fn commutative_operand_order_remains_hash_significant_without_ast_normalization() {
    let rank_plus_score = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
        expr: Expr::Binary {
            op: BinaryOp::Add,
            left: Box::new(Expr::Field(FieldId::new("rank"))),
            right: Box::new(Expr::Field(FieldId::new("score"))),
        },
        alias: None,
    }]);
    let score_plus_rank = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
        expr: Expr::Binary {
            op: BinaryOp::Add,
            left: Box::new(Expr::Field(FieldId::new("score"))),
            right: Box::new(Expr::Field(FieldId::new("rank"))),
        },
        alias: None,
    }]);

    assert_ne!(
        hash_projection(&rank_plus_score),
        hash_projection(&score_plus_rank),
        "projection hash preserves AST operand order for commutative operators in the current profile",
    );
}

#[test]
fn aggregate_numeric_target_field_remains_hash_significant() {
    let sum_rank = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
        expr: Expr::Aggregate(sum("rank")),
        alias: None,
    }]);
    let sum_score = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
        expr: Expr::Aggregate(sum("score")),
        alias: None,
    }]);

    assert_ne!(
        hash_projection(&sum_rank),
        hash_projection(&sum_score),
        "aggregate target-field semantic changes must invalidate identity",
    );
}

#[test]
fn aggregate_input_expression_shape_remains_hash_significant() {
    let avg_rank_plus_score = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
        expr: Expr::Aggregate(
            crate::db::query::builder::aggregate::AggregateExpr::from_expression_input(
                crate::db::query::plan::AggregateKind::Avg,
                Expr::Binary {
                    op: BinaryOp::Add,
                    left: Box::new(Expr::Field(FieldId::new("rank"))),
                    right: Box::new(Expr::Field(FieldId::new("score"))),
                },
            ),
        ),
        alias: None,
    }]);
    let avg_score_plus_rank = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
        expr: Expr::Aggregate(
            crate::db::query::builder::aggregate::AggregateExpr::from_expression_input(
                crate::db::query::plan::AggregateKind::Avg,
                Expr::Binary {
                    op: BinaryOp::Add,
                    left: Box::new(Expr::Field(FieldId::new("score"))),
                    right: Box::new(Expr::Field(FieldId::new("rank"))),
                },
            ),
        ),
        alias: None,
    }]);

    assert_ne!(
        hash_projection(&avg_rank_plus_score),
        hash_projection(&avg_score_plus_rank),
        "aggregate input expression structure must remain part of projection identity",
    );
}

#[test]
fn aggregate_numeric_promotion_noop_paths_stay_hash_stable() {
    let sum_plus_int_zero = 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::Int64(0))),
        },
        alias: None,
    }]);
    let sum_plus_decimal_zero =
        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::Decimal(Decimal::new(0, 1)))),
            },
            alias: None,
        }]);

    assert_eq!(
        hash_projection(&sum_plus_int_zero),
        hash_projection(&sum_plus_decimal_zero),
        "numeric no-op literal subtype differences must not fragment aggregate identity",
    );
}

#[test]
fn distinct_numeric_promotion_noop_paths_stay_hash_stable() {
    let sum_distinct_plus_int_zero =
        ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
            expr: Expr::Binary {
                op: BinaryOp::Add,
                left: Box::new(Expr::Aggregate(sum("rank").distinct())),
                right: Box::new(Expr::Literal(Value::Int64(0))),
            },
            alias: None,
        }]);
    let sum_distinct_plus_decimal_zero =
        ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
            expr: Expr::Binary {
                op: BinaryOp::Add,
                left: Box::new(Expr::Aggregate(sum("rank").distinct())),
                right: Box::new(Expr::Literal(Value::Decimal(Decimal::new(0, 1)))),
            },
            alias: None,
        }]);

    assert_eq!(
        hash_projection(&sum_distinct_plus_int_zero),
        hash_projection(&sum_distinct_plus_decimal_zero),
        "distinct numeric no-op literal subtype differences must not fragment identity",
    );
}