toasty 0.2.0

An async ORM for Rust supporting SQL and NoSQL databases
Documentation
use super::test_schema;
use crate::engine::simplify::Simplify;
use toasty_core::stmt::{self, Expr, ExprLet, MatchArm, Value, VisitMut};

// --- simplify_expr_let unit tests ---

#[test]
fn single_binding_inlined() {
    let schema = test_schema();
    let simplify = Simplify::new(&schema);

    // Let { bindings: [I64(42)], body: Arg(0) } → I64(42)
    let mut expr_let = ExprLet {
        bindings: vec![Expr::from(42i64)],
        body: Box::new(Expr::arg(0)),
    };

    let result = simplify.simplify_expr_let(&mut expr_let);
    assert_eq!(result, Some(Expr::from(42i64)));
}

#[test]
fn multiple_bindings_inlined() {
    let schema = test_schema();
    let simplify = Simplify::new(&schema);

    // Let { bindings: [I64(1), I64(2)], body: Record([Arg(1), Arg(0)]) }
    // → Record([I64(2), I64(1)])
    let mut expr_let = ExprLet {
        bindings: vec![Expr::from(1i64), Expr::from(2i64)],
        body: Box::new(Expr::record([Expr::arg(1), Expr::arg(0)])),
    };

    let result = simplify.simplify_expr_let(&mut expr_let);
    assert_eq!(
        result,
        Some(Expr::record([Expr::from(2i64), Expr::from(1i64)]))
    );
}

#[test]
fn unstable_binding_not_inlined() {
    let schema = test_schema();
    let simplify = Simplify::new(&schema);

    // Binding contains Default (unstable) → no inlining
    let mut expr_let = ExprLet {
        bindings: vec![Expr::Default],
        body: Box::new(Expr::arg(0)),
    };

    let result = simplify.simplify_expr_let(&mut expr_let);
    assert!(result.is_none());
}

#[test]
fn body_with_match_inlined() {
    let schema = test_schema();
    let simplify = Simplify::new(&schema);

    // The real-world pattern: nullable relation wrapping.
    // Let { bindings: [Arg(0)], body: Match(Arg(0), [Null → I64(0)], Arg(0)) }
    // With stable binding (say I64(5)):
    // → Match(I64(5), [Null → I64(0)], I64(5))
    let mut expr_let = ExprLet {
        bindings: vec![Expr::from(5i64)],
        body: Box::new(Expr::match_expr(
            Expr::arg(0),
            vec![MatchArm {
                pattern: Value::Null,
                expr: Expr::from(0i64),
            }],
            Expr::arg(0),
        )),
    };

    let result = simplify.simplify_expr_let(&mut expr_let);
    let expected = Expr::match_expr(
        Expr::from(5i64),
        vec![MatchArm {
            pattern: Value::Null,
            expr: Expr::from(0i64),
        }],
        Expr::from(5i64),
    );
    assert_eq!(result, Some(expected));
}

#[test]
fn outer_arg_nesting_decremented() {
    let schema = test_schema();
    let simplify = Simplify::new(&schema);

    // Body references both the Let binding (nesting=0) and an outer scope
    // (nesting=1). After inlining, the outer ref should become nesting=0.
    // Let { bindings: [I64(1)], body: Record([Arg(pos=0,nest=0), Arg(pos=0,nest=1)]) }
    // → Record([I64(1), Arg(pos=0,nest=0)])
    let mut expr_let = ExprLet {
        bindings: vec![Expr::from(1i64)],
        body: Box::new(Expr::record([
            Expr::arg(0),
            Expr::Arg(stmt::ExprArg {
                position: 0,
                nesting: 1,
            }),
        ])),
    };

    let result = simplify.simplify_expr_let(&mut expr_let);
    assert_eq!(result, Some(Expr::record([Expr::from(1i64), Expr::arg(0)])));
}

// --- visit_expr_mut end-to-end tests ---

#[test]
fn let_inlined_through_visit() {
    let schema = test_schema();
    let mut simplify = Simplify::new(&schema);

    // End-to-end: visit_expr_mut should inline the Let.
    let mut expr = Expr::Let(ExprLet {
        bindings: vec![Expr::from(42i64)],
        body: Box::new(Expr::arg(0)),
    });

    simplify.visit_expr_mut(&mut expr);
    assert_eq!(expr, Expr::from(42i64));
}

#[test]
fn nested_let_inlined_bottom_up() {
    let schema = test_schema();
    let mut simplify = Simplify::new(&schema);

    // Inner Let is inlined first (bottom-up), then the outer Let.
    // Outer: Let { bindings: [I64(10)], body: Let { bindings: [Arg(0)], body: Arg(0) } }
    // After inner inlining: Let { bindings: [I64(10)], body: Arg(0) }
    // After outer inlining: I64(10)
    let inner = Expr::Let(ExprLet {
        bindings: vec![Expr::arg(0)],
        body: Box::new(Expr::arg(0)),
    });
    let mut expr = Expr::Let(ExprLet {
        bindings: vec![Expr::from(10i64)],
        body: Box::new(inner),
    });

    simplify.visit_expr_mut(&mut expr);
    assert_eq!(expr, Expr::from(10i64));
}

#[test]
fn let_with_match_fully_simplified() {
    let schema = test_schema();
    let mut simplify = Simplify::new(&schema);

    // Full pipeline: Let { bindings: [Null], body: Match(Arg(0), [Null→I64(0)], Arg(0)) }
    // Step 1 (Let inlining): Match(Null, [Null→I64(0)], Null)
    // Step 2 (Match folding): I64(0)
    //
    // Because children are visited first, the Let body's Match is visited
    // before the Let itself. But the Let is what makes the Match's subject
    // non-constant (it's Arg(0)). So the Let is inlined first, yielding
    // Match(Null, ...), and then a *second* simplify pass would fold the
    // Match. In a single pass the result is the inlined Match.
    let mut expr = Expr::Let(ExprLet {
        bindings: vec![Expr::null()],
        body: Box::new(Expr::match_expr(
            Expr::arg(0),
            vec![MatchArm {
                pattern: Value::Null,
                expr: Expr::from(0i64),
            }],
            Expr::arg(0),
        )),
    });

    simplify.visit_expr_mut(&mut expr);

    // After a single pass the Let is inlined. The Match subject becomes
    // Value(Null) which is constant, so the match also folds in the same
    // pass because visit_expr_mut re-applies rules after replacing the Let.
    // However, the simplifier replaces the node *after* children have already
    // been visited, so the newly-produced Match won't get another child-visit
    // in the same pass. The Match folding happens when `simplify_expr_match`
    // runs on the *replacement* expression.
    //
    // Actually — the simplifier replaces `*i = expr` at line 85, so the
    // substituted Match(Null, ...) is set as the new `*i`. But `visit_expr_mut`
    // already returned; the replacement is not re-visited in the same call.
    // So the result is the un-folded Match with a constant subject.
    assert!(
        matches!(&expr, Expr::Match(m) if matches!(&*m.subject, Expr::Value(Value::Null)))
            || matches!(&expr, Expr::Value(Value::I64(0))),
        "expected either inlined Match(Null, ...) or fully folded I64(0), got: {expr:?}"
    );
}