ddx-core 0.1.3

Engine-neutral symbolic differentiation of SQL scalar expressions: `grad` & `jvp`.
Documentation

ddx-core

Engine-neutral symbolic differentiation of SQL scalar expressions — the v1 core of ddx, "autograd for composable databases." Write calculus directly in SQL and let the engine evaluate the derivative per row (the relational equivalent of jax.vmap(jax.grad(f))):

SELECT i, grad(x * y, x) AS dfdx, grad(x * y, y) AS dfdy FROM g

grad/jvp are markers, not row functions: they carry a differentiation request through parsing and are always rewritten away before execution.

use ddx_core::Ddx;
use ddx_core::sqlparser::dialect::GenericDialect;

let ddx = Ddx::new();
let out = ddx.rewrite_sql("SELECT grad(sin(x), x) AS d FROM t", &GenericDialect {})?;
assert_eq!(out, "SELECT (cos(x)) AS d FROM t");

The engine differentiates sqlparser::ast::Expr directly — the AST is the IR, there is no bespoke representation. The primary dependency is sqlparser, re-exported as ddx_core::sqlparser so downstream adapters cannot link a mismatched version.

What it supports

+ - * /; the unary chain rule for the trig / inverse-trig / exp / log / hyperbolic set plus abs; power with a constant base or exponent; higher-order via nesting; through-aggregate via linearity (AVG(grad(loss, theta))). Custom unary rules are registrable (ddx.register("myfn", rule) — the rule supplies f'(u), the engine applies the chain rule). Anything else is a typed DiffError, never a silently-wrong number.

Scalar vjp is deliberately not here: the name is reserved for the query-level reverse-mode operation in ddx-ad (design.md §3.6, §4).

Correctness properties worth knowing

  • Identifier folding is per-dialect (Ddx::for_datafusion() vs Ddx::for_duckdb()): unquoted identifiers always fold case; DuckDB folds quoted ones too. grad(Temp*Temp, temp) matches.
  • Ambiguity is a hard error, not a guess: a wrt that can't be pinned syntactically (grad(a.x*b.x, x)) errors rather than differentiating the wrong column.
  • div forces floating-point division (CAST(<numerator> AS DOUBLE)), so integer columns don't silently truncate the derivative.
  • 0/1-folding follows JAX's Zero-tangent convention and differs from unfolded SQL only on NULL-bearing rows (documented, tested).

See ../../docs/design.md §3 for the full rationale and the decision log (F#/G#) behind each of these.

Status

M0 (the scalar core) — implemented. The per-engine adapters (ddx-datafusion, ddx-duckdb), the Python wheel (ddxdb), and the query-level v2 engine (ddx-ad) are later milestones; see the design doc's §8.

Licensed under Apache-2.0.