use ddx_core::sqlparser::dialect::GenericDialect;
use ddx_core::{Ddx, DiffError};
fn d(expr: &str, wrt: &str) -> String {
Ddx::new()
.differentiate_sql(expr, wrt, &GenericDialect {})
.unwrap_or_else(|e| panic!("differentiate_sql({expr}, {wrt}) failed: {e}"))
}
#[test]
fn constant_has_zero_derivative() {
assert_eq!(d("3.0", "x"), "0.0");
}
#[test]
fn variable_has_unit_derivative() {
assert_eq!(d("x", "x"), "1.0");
}
#[test]
fn other_variable_has_zero_derivative() {
assert_eq!(d("y", "x"), "0.0");
}
#[test]
fn sum_rule_folds_constants() {
assert_eq!(d("x + y", "x"), "1.0");
}
#[test]
fn product_rule() {
assert_eq!(d("x * x", "x"), "x + x");
}
#[test]
fn quotient_rule() {
assert_eq!(d("x / y", "x"), "CAST(y AS DOUBLE) / (y * y)");
}
#[test]
fn chain_rule_sin() {
assert_eq!(d("sin(x)", "x"), "cos(x)");
}
#[test]
fn composite_sin_times_x() {
assert_eq!(d("sin(x) * x", "x"), "cos(x) * x + sin(x)");
}
#[test]
fn power_constant_exponent() {
assert_eq!(d("power(x, 2)", "x"), "2.0 * power(x, 1.0)");
}
#[test]
fn higher_order_derivative() {
let d1 = d("sin(x)", "x");
assert_eq!(d(&d1, "x"), "-sin(x)");
}
#[test]
fn unsupported_operator_errors() {
assert!(Ddx::new()
.differentiate_sql("x % y", "x", &GenericDialect {})
.is_err());
}
#[test]
fn unsupported_function_errors() {
assert!(Ddx::new()
.differentiate_sql("atan2(x, y)", "x", &GenericDialect {})
.is_err());
}
#[test]
fn power_negative_constant_exponent() {
assert_eq!(d("power(x, -2)", "x"), "-2.0 * power(x, -3.0)");
}
#[test]
fn power_negative_fractional_exponent() {
assert_eq!(d("power(x, -0.5)", "x"), "-0.5 * power(x, -1.5)");
}
#[test]
fn power_fractional_exponent_output_is_reconsumable() {
let once = d("power(x, 0.5)", "x");
assert!(once.contains("power(x, -0.5)"), "unexpected: {once}");
let twice = Ddx::new().differentiate_sql(&once, "x", &GenericDialect {});
assert!(twice.is_ok(), "engine rejected its own output: {twice:?}");
}
#[test]
fn power_non_positive_constant_base_errors() {
let err = Ddx::new()
.differentiate_sql("power(0, x)", "x", &GenericDialect {})
.unwrap_err();
assert!(matches!(err, DiffError::NotImplemented(_)), "got {err:?}");
let out = Ddx::new().differentiate_sql("power(2, x)", "x", &GenericDialect {});
assert!(out.is_ok(), "positive base should differentiate: {out:?}");
assert!(!out.unwrap().to_lowercase().contains("inf"));
}
#[test]
fn power_non_finite_constant_exponent_errors() {
let err = Ddx::new()
.differentiate_sql("power(x, 1e400)", "x", &GenericDialect {})
.unwrap_err();
assert!(matches!(err, DiffError::NotImplemented(_)), "got {err:?}");
}
#[test]
fn non_finite_exponent_still_differentiates_to_zero_when_base_is_constant_in_wrt() {
assert_eq!(d("power(x, 1e400)", "y"), "0.0");
}
#[test]
fn schema_qualified_call_does_not_match_builtin_rule() {
assert!(Ddx::new()
.differentiate_sql("myschema.sin(x)", "x", &GenericDialect {})
.is_err());
assert_eq!(d("sin(x)", "x"), "cos(x)");
}
#[test]
fn general_power_uv_errors() {
assert!(Ddx::new()
.differentiate_sql("power(x, x)", "x", &GenericDialect {})
.is_err());
}
#[test]
fn double_negation_does_not_render_a_comment() {
let out = d("-cos(x)", "x");
assert!(!out.contains("--"), "rendered a `--` comment: {out}");
assert_eq!(out, "sin(x)");
let nested = d("sin(-cos(x))", "x");
assert!(!nested.contains("--"), "rendered a `--` comment: {nested}");
assert_eq!(nested, "cos(-cos(x)) * sin(x)");
}
#[test]
fn cast_to_numeric_type_is_differentiable() {
assert_eq!(d("CAST(x * x AS DOUBLE)", "x"), "CAST(x + x AS DOUBLE)");
}
#[test]
fn cast_to_non_numeric_type_errors() {
assert!(Ddx::new()
.differentiate_sql("CAST(x AS VARCHAR)", "x", &GenericDialect {})
.is_err());
}
#[test]
fn abs_derivative_is_portable_and_pins_the_kink_at_zero() {
let out = d("abs(x)", "x");
assert!(
!out.to_lowercase().contains("signum"),
"must not emit the non-portable signum builtin: {out}"
);
assert_eq!(
out,
"CASE WHEN x > 0.0 THEN 1.0 WHEN x < 0.0 THEN -1.0 ELSE 0.0 END"
);
let chained = d("abs(x * y)", "x");
assert!(chained.contains("* y"), "chain rule missing: {chained}");
assert!(!chained.to_lowercase().contains("signum"));
}
#[test]
fn jvp_seeds_a_tangent_on_one_input() {
let out = Ddx::new()
.rewrite_sql("SELECT jvp(x * y, x, dx) AS t FROM g", &GenericDialect {})
.unwrap();
assert_eq!(out, "SELECT (dx * y) AS t FROM g");
}
#[test]
fn jvp_with_unit_seed_matches_grad() {
let jvp = Ddx::new()
.rewrite_sql("SELECT jvp(sin(x), x, 1.0) AS t FROM g", &GenericDialect {})
.unwrap();
let grad = Ddx::new()
.rewrite_sql("SELECT grad(sin(x), x) AS t FROM g", &GenericDialect {})
.unwrap();
assert_eq!(jvp, grad);
}
fn err_msg(expr: &str, wrt: &str) -> String {
Ddx::new()
.differentiate_sql(expr, wrt, &GenericDialect {})
.unwrap_err()
.to_string()
}
#[test]
fn unsupported_function_error_names_the_supported_set_and_the_escape_hatch() {
let m = err_msg("cbrt(x)", "x");
assert!(m.contains("cbrt"), "should name the function: {m}");
assert!(
m.contains("sin/cos"),
"should list supported functions: {m}"
);
assert!(
m.contains("Ddx::register"),
"should point at the custom-rule hatch: {m}"
);
}
#[test]
fn unsupported_operator_error_lists_supported_operators() {
let m = err_msg("x % y", "x");
assert!(
m.contains("+ - * /"),
"should list differentiable operators: {m}"
);
}
#[test]
fn wrt_not_a_column_error_shows_the_right_form() {
let m = err_msg("x * y", "x + y");
assert!(
m.contains("bare column"),
"should say a bare column is required: {m}"
);
assert!(m.contains("x + y"), "should echo the offending wrt: {m}");
}
#[test]
fn general_power_error_suggests_the_constant_side_and_the_exp_ln_rewrite() {
let m = err_msg("power(x, x)", "x");
assert!(
m.contains("power(x, 2)") || m.contains("constant"),
"should suggest a constant side: {m}"
);
assert!(
m.contains("exp(exponent * ln(base))"),
"should suggest the u^v rewrite: {m}"
);
}