use ddx_core::sqlparser::dialect::{DuckDbDialect, GenericDialect};
use ddx_core::{Ddx, DiffError};
fn rw(sql: &str) -> String {
Ddx::new()
.rewrite_sql(sql, &GenericDialect {})
.unwrap_or_else(|e| panic!("rewrite_sql({sql}) failed: {e}"))
}
#[test]
fn replaces_a_grad_call_in_place() {
assert_eq!(
rw("SELECT grad(sin(x), x) AS d FROM t"),
"SELECT (cos(x)) AS d FROM t"
);
}
#[test]
fn full_gradient_as_tidy_columns() {
assert_eq!(
rw("SELECT grad(x * y, x) AS dfdx, grad(x * y, y) AS dfdy FROM g"),
"SELECT (y) AS dfdx, (x) AS dfdy FROM g"
);
}
#[test]
fn marker_free_query_is_untouched() {
let sql = "SELECT a+b AS s FROM t -- comment";
assert_eq!(rw(sql), sql);
}
#[test]
fn marker_free_query_mentioning_grad_in_a_string_is_returned_verbatim() {
let sql = "SELECT 'grad(' AS label FROM t";
assert_eq!(rw(sql), sql);
}
#[test]
fn nested_higher_order_grad() {
let out = rw("SELECT grad(grad(power(x, 3), x), x) AS d FROM t");
assert!(
!out.to_lowercase().contains("grad("),
"marker left behind: {out}"
);
assert!(out.contains("power(x, 1.0)"), "unexpected rewrite: {out}");
}
#[test]
fn fires_inside_recursive_cte() {
let out = rw("WITH RECURSIVE r AS (SELECT 1.0 AS x UNION ALL \
SELECT x - grad(x * x - 2, x) FROM r WHERE x < 10) SELECT x FROM r");
assert!(out.contains("(x + x)"), "unexpected rewrite: {out}");
assert!(
!out.to_lowercase().contains("grad("),
"marker left behind: {out}"
);
}
#[test]
fn splice_end_ignores_parens_inside_strings_and_comments() {
assert_eq!(
rw("SELECT jvp(x, x, CASE WHEN a = ')' THEN y ELSE z END) FROM t"),
"SELECT (CASE WHEN a = ')' THEN y ELSE z END) FROM t"
);
assert_eq!(
rw("SELECT jvp(x, x, y /* ) ) */) FROM t"),
"SELECT (y) FROM t"
);
}
#[test]
fn dml_update_rule_is_rewritten() {
let out = rw("INSERT INTO p SELECT theta - 0.1 * grad(x * theta, theta) FROM t");
assert!(
!out.to_lowercase().contains("grad("),
"marker left behind: {out}"
);
assert!(out.contains("(x)"), "unexpected rewrite: {out}");
}
#[test]
fn splice_preserves_multibyte_prefix() {
let out = rw("SELECT 'héllo', grad(sin(x), x) AS d FROM t");
assert_eq!(out, "SELECT 'héllo', (cos(x)) AS d FROM t");
}
#[test]
fn splice_multiple_markers_on_one_line() {
let out = rw("SELECT grad(sin(x), x), grad(cos(y), y) FROM t");
assert_eq!(out, "SELECT (cos(x)), (-sin(y)) FROM t");
}
#[test]
fn splice_preserves_exact_surrounding_bytes() {
let out = rw("SELECT GRAD(x*x,x) , y FROM t");
assert_eq!(out, "SELECT (x + x) , y FROM t");
}
#[test]
fn pre_gate_must_not_miss_a_comment_separated_marker() {
assert_eq!(rw("SELECT grad\n(x, x) FROM t"), "SELECT (1.0) FROM t");
assert_eq!(rw("SELECT grad\t(x, x) FROM t"), "SELECT (1.0) FROM t");
assert_eq!(
rw("SELECT grad /* c */ (x, x) FROM t"),
"SELECT (1.0) FROM t",
"block-comment-separated marker was not rewritten"
);
assert_eq!(
rw("SELECT grad-- c\n(x, x) FROM t"),
"SELECT (1.0) FROM t",
"line-comment-separated marker was not rewritten"
);
}
#[test]
fn qualified_grad_is_left_alone() {
let sql = "SELECT myschema.grad(x, x) AS d FROM t";
assert_eq!(rw(sql), sql);
}
#[test]
fn scalar_vjp_is_not_a_marker() {
let sql = "SELECT vjp(sin(x), x, w) AS v FROM t";
assert_eq!(rw(sql), sql);
}
#[test]
fn unquoted_identifiers_fold_case() {
let out = Ddx::new()
.differentiate_sql("Temp * Temp", "temp", &GenericDialect {})
.unwrap();
assert_eq!(out, "Temp + Temp");
}
#[test]
fn duckdb_folds_quoted_identifiers_too() {
let out = Ddx::for_duckdb()
.differentiate_sql(r#""Temp" * "Temp""#, "temp", &DuckDbDialect {})
.unwrap();
assert_eq!(out, r#""Temp" + "Temp""#);
}
#[test]
fn datafusion_keeps_quoted_identifiers_case_sensitive() {
let out = Ddx::for_datafusion()
.differentiate_sql(r#""Temp" * "Temp""#, "temp", &GenericDialect {})
.unwrap();
assert_eq!(out, "0.0");
}
#[test]
fn qualified_wrt_across_a_join_is_accepted() {
let out = rw("SELECT grad(a.v * b.w, a.v) AS d FROM t a JOIN u b ON a.k = b.k");
assert_eq!(out, "SELECT (b.w) AS d FROM t a JOIN u b ON a.k = b.k");
}
#[test]
fn fully_qualified_same_name_across_join_is_accepted() {
let out = rw("SELECT grad(a.x * b.x, a.x) AS d FROM t a JOIN u b ON a.k = b.k");
assert_eq!(out, "SELECT (b.x) AS d FROM t a JOIN u b ON a.k = b.k");
}
#[test]
fn bare_occurrence_with_qualified_wrt_errors() {
let err = Ddx::new()
.rewrite_sql("SELECT grad(x * a.x, a.x) FROM t a", &GenericDialect {})
.unwrap_err();
assert!(matches!(err, DiffError::AmbiguousColumn(_)), "got {err:?}");
}
#[test]
fn qualified_occurrence_with_bare_wrt_errors() {
let err = Ddx::new()
.rewrite_sql(
"SELECT grad(a.x * b.x, x) FROM t a JOIN u b ON a.k = b.k",
&GenericDialect {},
)
.unwrap_err();
assert!(matches!(err, DiffError::AmbiguousColumn(_)), "got {err:?}");
}
#[test]
fn wrt_must_be_a_bare_column() {
let err = Ddx::new()
.rewrite_sql("SELECT grad(x * y, x + y) FROM t", &GenericDialect {})
.unwrap_err();
assert!(matches!(err, DiffError::InvalidMarker(_)), "got {err:?}");
}
#[test]
fn computed_cte_alias_as_non_wrt_term_errors() {
let err = Ddx::new()
.rewrite_sql(
"WITH v AS (SELECT x, sin(x) AS s FROM t) SELECT grad(s * x, x) FROM v",
&GenericDialect {},
)
.unwrap_err();
assert!(
matches!(err, DiffError::ProjectionBoundary(_)),
"got {err:?}"
);
}
#[test]
fn marker_inside_an_aggregate_is_rewritten() {
let out = rw("SELECT AVG(grad(x * theta, theta)) AS step FROM batch");
assert_eq!(out, "SELECT AVG((x)) AS step FROM batch");
}
#[test]
fn qualified_base_column_colliding_with_unrelated_cte_alias_is_accepted() {
let out = rw("WITH v AS (SELECT sin(x) AS s FROM t) \
SELECT grad(w.s * x, x) AS d FROM u w JOIN v ON w.k = v.k");
assert_eq!(
out,
"WITH v AS (SELECT sin(x) AS s FROM t) \
SELECT (w.s) AS d FROM u w JOIN v ON w.k = v.k"
);
}
#[test]
fn qualified_reference_to_the_owning_cte_alias_still_errors() {
let err = Ddx::new()
.rewrite_sql(
"WITH v AS (SELECT sin(x) AS s FROM t) \
SELECT grad(v.s * x, x) AS d FROM v",
&GenericDialect {},
)
.unwrap_err();
assert!(
matches!(err, DiffError::ProjectionBoundary(_)),
"got {err:?}"
);
}
#[test]
fn differentiating_wrt_a_computed_alias_is_allowed() {
let out = rw("SELECT a + b AS s, grad(s * s, s) AS d FROM t");
assert_eq!(out, "SELECT a + b AS s, (s + s) AS d FROM t");
}
#[test]
fn explain_reports_each_marker_and_the_rewrite() {
let ex = Ddx::new()
.explain(
"SELECT grad(x * y, x) AS dfdx, grad(x * y, y) AS dfdy FROM g",
&GenericDialect {},
)
.unwrap();
assert_eq!(
ex.original,
"SELECT grad(x * y, x) AS dfdx, grad(x * y, y) AS dfdy FROM g"
);
assert_eq!(ex.rewritten, "SELECT (y) AS dfdx, (x) AS dfdy FROM g");
assert_eq!(ex.steps.len(), 2);
assert_eq!(ex.steps[0].function, "grad");
assert_eq!(ex.steps[0].marker, "grad(x * y, x)");
assert_eq!(ex.steps[0].derivative, "(y)");
assert_eq!(ex.steps[1].marker, "grad(x * y, y)");
assert_eq!(ex.steps[1].derivative, "(x)");
assert_eq!(ex.rewritten, rw(&ex.original));
}
#[test]
fn explain_display_is_readable() {
let ex = Ddx::new()
.explain("SELECT jvp(sin(x), x, dx) AS t FROM g", &GenericDialect {})
.unwrap();
let shown = format!("{ex}");
assert_eq!(
shown,
"ddx rewrites 1 marker:\n \
• jvp(sin(x), x, dx) → (cos(x) * dx)\n\n \
from: SELECT jvp(sin(x), x, dx) AS t FROM g\n \
into: SELECT (cos(x) * dx) AS t FROM g"
);
}
#[test]
fn explain_on_marker_free_sql_has_no_steps() {
let sql = "SELECT a + b FROM t";
let ex = Ddx::new().explain(sql, &GenericDialect {}).unwrap();
assert!(ex.steps.is_empty());
assert_eq!(ex.rewritten, sql);
assert!(format!("{ex}").contains("unchanged"));
}
#[test]
fn explain_surfaces_errors_like_rewrite() {
let err = Ddx::new()
.explain("SELECT grad(atan2(x, y), x) FROM t", &GenericDialect {})
.unwrap_err();
assert!(matches!(err, DiffError::NotImplemented(_)), "got {err:?}");
}