use super::Executor;
use super::types::{PostingContext, Value};
use crate::ast::{Expr, FunctionCall, Literal};
use crate::error::QueryError;
use rust_decimal_macros::dec;
use rustledger_core::{Amount, Directive, Posting, Transaction, naive_date};
fn scratch_txn() -> Transaction {
Transaction::new(naive_date(2024, 1, 15).unwrap(), "scratch")
.with_flag('*')
.with_synthesized_posting(Posting::new(
"Assets:Bank:Checking",
Amount::new(dec!(-5.00), "USD"),
))
}
fn value_as_literal(v: &Value) -> Option<Expr> {
Some(match v {
Value::String(s) => Expr::Literal(Literal::String(s.clone())),
Value::Number(n) => Expr::Literal(Literal::Number(*n)),
Value::Integer(i) => Expr::Literal(Literal::Integer(*i)),
Value::Date(d) => Expr::Literal(Literal::Date(*d)),
Value::Boolean(b) => Expr::Literal(Literal::Boolean(*b)),
Value::Null => Expr::Literal(Literal::Null),
_ => return None,
})
}
fn run_both(
name: &str,
args: &[Value],
) -> (Result<Value, QueryError>, Option<Result<Value, QueryError>>) {
let directives: Vec<Directive> = Vec::new();
let executor = Executor::new(&directives);
let eager = executor.evaluate_function_on_values(name, args);
let lazy = args
.iter()
.map(value_as_literal)
.collect::<Option<Vec<Expr>>>()
.map(|lit_args| {
let txn = scratch_txn();
let ctx = PostingContext {
transaction: &txn,
posting_index: 0,
balance: None,
account_balance: None,
directive_index: None,
};
let call = FunctionCall {
name: name.to_string(),
args: lit_args,
};
executor.evaluate_function(&call, &ctx)
});
(eager, lazy)
}
#[track_caller]
fn assert_parity(name: &str, args: &[Value]) {
let (eager, lazy) = run_both(name, args);
let Some(lazy) = lazy else { return };
match (&lazy, &eager) {
(Ok(l), Ok(e)) => assert_eq!(
l, e,
"value mismatch for {name}({args:?}): lazy={l:?} eager={e:?}"
),
(Err(_), Err(_)) => {}
_ => panic!("path divergence for {name}({args:?}): lazy={lazy:?} eager={eager:?}"),
}
}
fn s(x: &str) -> Value {
Value::String(x.to_string())
}
#[test]
fn shared_pure_functions_agree_across_both_paths() {
let acct = "Assets:Bank:Checking";
let cases: &[(&str, Vec<Value>)] = &[
("UPPER", vec![s("hello")]),
("LOWER", vec![s("HELLO")]),
("LENGTH", vec![s("hello")]),
(
"SUBSTR",
vec![s("hello world"), Value::Integer(0), Value::Integer(5)],
),
("TRIM", vec![s(" hi ")]),
("STARTSWITH", vec![s("hello"), s("he")]),
("ENDSWITH", vec![s("hello"), s("lo")]),
("PARENT", vec![s(acct)]),
("LEAF", vec![s(acct)]),
("ROOT", vec![s(acct)]),
("ROOT", vec![s(acct), Value::Integer(2)]),
("ACCOUNT_DEPTH", vec![s(acct)]),
("ABS", vec![Value::Number(dec!(-3.5))]),
("NEG", vec![Value::Number(dec!(3.5))]),
(
"ROUND",
vec![Value::Number(dec!(3.14159)), Value::Integer(2)],
),
(
"SAFEDIV",
vec![Value::Number(dec!(10)), Value::Number(dec!(0))],
),
("INT", vec![Value::Number(dec!(3.9))]),
("DECIMAL", vec![s("3.5")]),
("STR", vec![Value::Integer(42)]),
("BOOL", vec![Value::Integer(1)]),
];
for (name, args) in cases {
assert_parity(name, args);
}
}
#[test]
fn reconciled_root_negative_depth_both_error() {
let (eager, lazy) = run_both("ROOT", &[s("Assets:Bank:Checking"), Value::Integer(-1)]);
assert!(
lazy.unwrap().is_err(),
"lazy ROOT(acct,-1) errors (depth guard)"
);
assert!(
eager.is_err(),
"eager ROOT(acct,-1) now errors (guard ported)"
);
}
#[test]
fn reconciled_getitem_on_null_both_null() {
let (eager, lazy) = run_both("GETITEM", &[Value::Null, s("k")]);
assert!(
matches!(eager, Ok(Value::Null)),
"eager GETITEM(NULL,k) is NULL, got {eager:?}"
);
assert!(
matches!(lazy.unwrap(), Ok(Value::Null)),
"lazy GETITEM(NULL,k) now returns NULL (NULL arm added)"
);
}
#[test]
fn reconciled_extended_date_functions_registered_in_eager() {
let d = |y, m, day| Value::Date(naive_date(y, m, day).unwrap());
let cases: &[(&str, Vec<Value>)] = &[
("DATE", vec![s("2024-01-15")]),
("DATE_ADD", vec![d(2024, 1, 15), Value::Integer(5)]),
("DATE_TRUNC", vec![s("month"), d(2024, 1, 15)]),
("DATE_PART", vec![s("year"), d(2024, 1, 15)]),
("PARSE_DATE", vec![s("2024-01-15")]),
(
"DATE_BIN",
vec![Value::Integer(1), d(2024, 1, 15), d(2024, 1, 1)],
),
("INTERVAL", vec![Value::Integer(1), s("day")]),
];
for (name, args) in cases {
let (eager, lazy) = run_both(name, args);
let lazy = lazy.expect("date-function args are literal-constructible");
match (&eager, &lazy) {
(Ok(e), Ok(l)) => assert_eq!(e, l, "{name}: eager {e:?} != lazy {l:?}"),
_ => panic!("{name} must now succeed on BOTH paths: eager={eager:?} lazy={lazy:?}"),
}
}
}
#[test]
fn reconciled_today_extra_arg_both_error() {
let (eager, lazy) = run_both("TODAY", &[Value::Integer(1)]);
assert!(
lazy.unwrap().is_err(),
"lazy TODAY(x) rejects the extra arg"
);
assert!(eager.is_err(), "eager TODAY(x) now errors (zero-arg guard)");
}