use datafusion::error::Result;
use datafusion::prelude::SessionContext;
mod common;
async fn ctx() -> Result<SessionContext> {
let ctx = SessionContext::new();
ddx_datafusion::install(&ctx);
ctx.sql(
"CREATE TABLE t AS
SELECT * FROM (VALUES (1.0, 4.0), (2.0, 5.0), (3.0, 6.0)) AS v(x, y)",
)
.await?
.collect()
.await?;
Ok(ctx)
}
async fn col(ctx: &SessionContext, sql: &str) -> Result<Vec<f64>> {
let batches = ctx.sql(sql).await?.collect().await?;
Ok(common::f64_column(&batches))
}
#[tokio::test]
async fn bare_grad_runs_end_to_end() -> Result<()> {
let ctx = ctx().await?;
assert_eq!(
col(&ctx, "SELECT grad(x * x, x) AS d FROM t ORDER BY x").await?,
vec![2.0, 4.0, 6.0], );
Ok(())
}
#[tokio::test]
async fn product_rule_picks_the_right_variable() -> Result<()> {
let ctx = ctx().await?;
assert_eq!(
col(&ctx, "SELECT grad(x * y, x) AS d FROM t ORDER BY x").await?,
vec![4.0, 5.0, 6.0],
);
assert_eq!(
col(&ctx, "SELECT grad(x * y, y) AS d FROM t ORDER BY x").await?,
vec![1.0, 2.0, 3.0],
);
Ok(())
}
#[tokio::test]
async fn chain_rule_through_a_function() -> Result<()> {
let ctx = ctx().await?;
let got = col(&ctx, "SELECT grad(sin(x * y), x) AS d FROM t ORDER BY x").await?;
let want: Vec<f64> = [(1.0, 4.0), (2.0, 5.0), (3.0, 6.0)]
.iter()
.map(|(x, y): &(f64, f64)| y * (x * y).cos())
.collect();
for (g, w) in got.iter().zip(&want) {
assert!((g - w).abs() < 1e-12, "got {g}, want {w}");
}
Ok(())
}
#[tokio::test]
async fn higher_order_falls_out_of_nesting() -> Result<()> {
let ctx = ctx().await?;
assert_eq!(
col(
&ctx,
"SELECT grad(grad(x * x * x, x), x) AS d FROM t ORDER BY x"
)
.await?,
vec![6.0, 12.0, 18.0], );
Ok(())
}
#[tokio::test]
async fn jvp_is_the_directional_derivative() -> Result<()> {
let ctx = ctx().await?;
assert_eq!(
col(&ctx, "SELECT jvp(x * x, x, y) AS d FROM t ORDER BY x").await?,
vec![8.0, 20.0, 36.0],
);
Ok(())
}
#[tokio::test]
async fn grad_inside_an_aggregate_is_one_descent_step() -> Result<()> {
let ctx = ctx().await?;
let got = col(&ctx, "SELECT AVG(grad(x * x, x)) AS g FROM t").await?;
assert_eq!(got, vec![4.0]); Ok(())
}
#[tokio::test]
async fn works_through_the_dataframe_api_too() -> Result<()> {
use datafusion::logical_expr::col as c;
let ctx = ctx().await?;
let grad = ddx_datafusion::grad_udf();
let df = ctx
.table("t")
.await?
.select(vec![grad.call(vec![c("x") * c("x"), c("x")]).alias("d")])?
.sort(vec![c("d").sort(true, false)])?;
let batches = df.collect().await?;
assert_eq!(common::f64_column(&batches), vec![2.0, 4.0, 6.0]);
Ok(())
}
#[tokio::test]
async fn a_query_without_markers_is_untouched() -> Result<()> {
let ctx = ctx().await?;
assert_eq!(
col(&ctx, "SELECT x * 2 AS d FROM t ORDER BY x").await?,
vec![2.0, 4.0, 6.0],
);
Ok(())
}
#[tokio::test]
async fn unsupported_construct_is_a_loud_error() -> Result<()> {
let ctx = ctx().await?;
let err = ctx
.sql("SELECT grad(atan2(x, y), x) FROM t")
.await?
.collect()
.await
.expect_err("atan2 has no rule yet — this must fail, not guess");
let msg = err.to_string();
assert!(msg.contains("not implemented"), "unexpected error: {msg}");
assert!(
msg.contains("atan2"),
"the message must name the culprit: {msg}"
);
let mut source = std::error::Error::source(&err);
let mut found_typed = false;
while let Some(s) = source {
if let Some(d) = s.downcast_ref::<ddx_datafusion::ddx_core::DiffError>() {
assert!(matches!(
d,
ddx_datafusion::ddx_core::DiffError::NotImplemented(_)
));
found_typed = true;
}
source = s.source();
}
assert!(
found_typed,
"the DiffError must be downcastable from the error chain"
);
Ok(())
}
#[tokio::test]
async fn a_marker_that_reaches_execution_errors() -> Result<()> {
let ctx = SessionContext::new();
ctx.register_udf(ddx_datafusion::grad_udf());
ctx.sql("CREATE TABLE t AS SELECT * FROM (VALUES (1.0)) AS v(x)")
.await?
.collect()
.await?;
let err = ctx
.sql("SELECT grad(x * x, x) FROM t")
.await?
.collect()
.await
.expect_err("an unrewritten marker must never produce a value");
let msg = err.to_string();
assert!(msg.contains("reached execution"), "unexpected: {msg}");
Ok(())
}