icydb-core 0.257.20

IcyDB — A schema-first typed query engine and persistence runtime for Internet Computer canisters
Documentation
use crate::{
    db::{
        query::{
            plan::expr::{BinaryOp, Expr, FieldId},
            preparation::with_preparation_work,
        },
        sql::{
            lowering::{
                SqlLoweringError,
                expr::{SqlExprPhase, lower_sql_expr, validate_numeric_scale_function_scale},
            },
            parser::{SqlExpr, SqlExprBinaryOp},
        },
    },
    types::{
        Date, Decimal, Duration, Float32 as F32, Float64 as F64, IntBig, NatBig, Timestamp, U256,
    },
    value::Value,
};
use icydb_diagnostic_code::{DiagnosticDetail, QueryProjectionCode};

fn arithmetic(op: SqlExprBinaryOp, value: Value) -> SqlExpr {
    SqlExpr::Binary {
        op,
        left: Box::new(SqlExpr::Field("amount".to_string())),
        right: Box::new(SqlExpr::Literal(value)),
    }
}

#[test]
fn arithmetic_preserves_all_dynamic_numeric_families_and_operators() {
    let values = [
        Value::Int64(-3),
        Value::Int128(-3),
        Value::IntBig(IntBig::from(-3)),
        Value::Nat64(3),
        Value::Nat128(3),
        Value::NatBig(NatBig::from(3_u64)),
        Value::U256(U256::from(3_u64)),
        Value::Decimal(Decimal::new(123, 2)),
        Value::Float32(F32::try_new(1.25).unwrap()),
        Value::Float64(F64::try_new(2.5).unwrap()),
        Value::Duration(Duration::from_secs(1)),
        Value::Timestamp(Timestamp::from_secs(1)),
        Value::Date(Date::try_new(2024, 1, 2).unwrap()),
    ];
    with_preparation_work(|work| {
        for (sql_op, op) in [
            (SqlExprBinaryOp::Add, BinaryOp::Add),
            (SqlExprBinaryOp::Sub, BinaryOp::Sub),
            (SqlExprBinaryOp::Mul, BinaryOp::Mul),
            (SqlExprBinaryOp::Div, BinaryOp::Div),
        ] {
            for value in &values {
                let syntax = arithmetic(sql_op, value.clone());
                for phase in [
                    SqlExprPhase::Scalar,
                    SqlExprPhase::Where,
                    SqlExprPhase::PreAggregate,
                    SqlExprPhase::PostAggregate,
                ] {
                    assert_eq!(
                        lower_sql_expr(&syntax, phase, work).unwrap(),
                        Expr::Binary {
                            op,
                            left: Box::new(Expr::Field(FieldId::new("amount"))),
                            right: Box::new(Expr::Literal(value.clone())),
                        }
                    );
                }
            }
        }
    });
}

#[test]
fn arithmetic_rejects_non_numeric_literals_with_current_projection_detail() {
    with_preparation_work(|work| {
        for op in [
            SqlExprBinaryOp::Add,
            SqlExprBinaryOp::Sub,
            SqlExprBinaryOp::Mul,
            SqlExprBinaryOp::Div,
        ] {
            for value in [
                Value::Null,
                Value::Bool(true),
                Value::Text("3".to_string()),
                Value::Blob(vec![3]),
                Value::List(vec![Value::Nat64(3)]),
                Value::Map(vec![]),
                Value::Unit,
            ] {
                let err =
                    lower_sql_expr(&arithmetic(op, value), SqlExprPhase::Scalar, work).unwrap_err();
                let SqlLoweringError::Query(err) = err else {
                    panic!("expected query error");
                };
                assert_eq!(
                    err.diagnostic().detail(),
                    Some(&DiagnosticDetail::QueryProjection {
                        reason: QueryProjectionCode::NumericLiteralRequired,
                    })
                );
            }
        }
    });
}

#[test]
fn comparisons_do_not_acquire_arithmetic_literal_restrictions() {
    with_preparation_work(|work| {
        for op in [
            SqlExprBinaryOp::Eq,
            SqlExprBinaryOp::Ne,
            SqlExprBinaryOp::Lt,
            SqlExprBinaryOp::Lte,
            SqlExprBinaryOp::Gt,
            SqlExprBinaryOp::Gte,
        ] {
            assert!(
                lower_sql_expr(
                    &arithmetic(op, Value::Text("x".to_string())),
                    SqlExprPhase::Where,
                    work
                )
                .is_ok()
            );
        }
    });
}

#[test]
fn numeric_scale_validation_borrows_and_preserves_integer_bounds() {
    for (value, expected) in [
        (Value::Int64(0), 0),
        (Value::Nat64(u64::from(u32::MAX)), u32::MAX),
    ] {
        assert_eq!(
            validate_numeric_scale_function_scale(&value).unwrap(),
            expected
        );
    }
    for value in [
        Value::Int64(-1),
        Value::Nat64(u64::from(u32::MAX) + 1),
        Value::Text("0".repeat(4096)),
        Value::List(vec![Value::Nat64(1)]),
    ] {
        assert!(validate_numeric_scale_function_scale(&value).is_err());
    }
}