use std::collections::HashSet;
use std::fmt;
use std::ops::ControlFlow;
use sqlparser::ast::Spanned;
use sqlparser::ast::{
Expr, Function, ObjectNamePart, Query, Select, SelectItem, SetExpr, Statement, TableFactor,
Visit, VisitMut, Visitor, VisitorMut,
};
use sqlparser::dialect::Dialect;
use sqlparser::parser::Parser;
use sqlparser::tokenizer::{Location, Span, Token, TokenWithSpan, Tokenizer};
use crate::colref::{ColRef, IdentCasing};
use crate::engine::{differentiate, jvp, positional_args, RuleRegistry};
use crate::error::{DiffError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum MarkerKind {
Grad,
Jvp,
}
pub(crate) fn marker_kind(f: &Function) -> Option<MarkerKind> {
if f.name.0.len() != 1 {
return None;
}
let ObjectNamePart::Identifier(id) = &f.name.0[0] else {
return None;
};
match id.value.to_ascii_lowercase().as_str() {
"grad" => Some(MarkerKind::Grad),
"jvp" => Some(MarkerKind::Jvp),
_ => None,
}
}
impl MarkerKind {
fn name(self) -> &'static str {
match self {
MarkerKind::Grad => "grad",
MarkerKind::Jvp => "jvp",
}
}
}
fn is_marker_expr(e: &Expr) -> bool {
matches!(e, Expr::Function(f) if marker_kind(f).is_some())
}
fn marker_expr_kind(e: &Expr) -> Option<MarkerKind> {
match e {
Expr::Function(f) => marker_kind(f),
_ => None,
}
}
#[derive(Debug, Clone)]
pub struct Explanation {
pub original: String,
pub rewritten: String,
pub steps: Vec<ExplainStep>,
}
#[derive(Debug, Clone)]
pub struct ExplainStep {
pub function: &'static str,
pub marker: String,
pub derivative: String,
}
impl fmt::Display for Explanation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.steps.is_empty() {
return write!(
f,
"No grad/jvp markers to rewrite; the statement is unchanged:\n {}",
self.original
);
}
let n = self.steps.len();
writeln!(
f,
"ddx rewrites {n} marker{}:",
if n == 1 { "" } else { "s" }
)?;
for step in &self.steps {
writeln!(f, " • {} → {}", step.marker, step.derivative)?;
}
writeln!(f)?;
writeln!(f, " from: {}", self.original)?;
write!(f, " into: {}", self.rewritten)
}
}
fn pre_gate_hit(sql: &str) -> bool {
let lower = sql.to_ascii_lowercase();
for kw in ["grad", "jvp"] {
let mut from = 0;
while let Some(rel) = lower[from..].find(kw) {
let idx = from + rel;
from = idx + 1;
let ok_prev = idx == 0
|| sql[..idx].chars().next_back().is_some_and(|prev| {
!(prev.is_ascii_alphanumeric() || prev == '_' || prev == '.')
});
if !ok_prev {
continue;
}
let after = &sql[idx + kw.len()..];
if after[skip_trivia(after)..].starts_with('(') {
return true;
}
}
}
false
}
fn skip_trivia(s: &str) -> usize {
let b = s.as_bytes();
let n = b.len();
let mut i = 0;
loop {
while i < n && b[i].is_ascii_whitespace() {
i += 1;
}
if i + 1 < n && b[i] == b'-' && b[i + 1] == b'-' {
i += 2;
while i < n && b[i] != b'\n' {
i += 1;
}
continue;
}
if i + 1 < n && b[i] == b'/' && b[i + 1] == b'*' {
i += 2;
let mut depth = 1usize;
while i < n && depth > 0 {
if i + 1 < n && b[i] == b'/' && b[i + 1] == b'*' {
depth += 1;
i += 2;
} else if i + 1 < n && b[i] == b'*' && b[i + 1] == b'/' {
depth -= 1;
i += 2;
} else {
i += 1;
}
}
continue;
}
break;
}
i
}
pub(crate) fn rewrite_sql(
sql: &str,
dialect: &dyn Dialect,
casing: IdentCasing,
reg: &RuleRegistry,
) -> Result<String> {
match resolve_markers(sql, dialect, casing, reg)? {
Resolution::Verbatim => Ok(sql.to_string()),
Resolution::Reprinted(out) => Ok(out),
Resolution::Spliced(repls) => Ok(apply_splice(sql, repls)),
}
}
pub(crate) fn explain_sql(
sql: &str,
dialect: &dyn Dialect,
casing: IdentCasing,
reg: &RuleRegistry,
) -> Result<Explanation> {
let (rewritten, steps) = match resolve_markers(sql, dialect, casing, reg)? {
Resolution::Verbatim => (sql.to_string(), Vec::new()),
Resolution::Reprinted(out) => (out, Vec::new()),
Resolution::Spliced(repls) => {
let steps = repls
.iter()
.map(|r| ExplainStep {
function: r.function.name(),
marker: r.marker.clone(),
derivative: r.derivative.clone(),
})
.collect();
(apply_splice(sql, repls), steps)
}
};
Ok(Explanation {
original: sql.to_string(),
rewritten,
steps,
})
}
fn apply_splice(sql: &str, mut repls: Vec<Replacement>) -> String {
repls.sort_by_key(|r| std::cmp::Reverse(r.start));
let mut out = sql.to_string();
for r in repls {
out.replace_range(r.start..r.end, &r.derivative);
}
out
}
struct Replacement {
start: usize,
end: usize,
function: MarkerKind,
marker: String,
derivative: String,
}
enum Resolution {
Verbatim,
Reprinted(String),
Spliced(Vec<Replacement>),
}
fn resolve_markers(
sql: &str,
dialect: &dyn Dialect,
casing: IdentCasing,
reg: &RuleRegistry,
) -> Result<Resolution> {
if !pre_gate_hit(sql) {
return Ok(Resolution::Verbatim);
}
let statements = Parser::parse_sql(dialect, sql)
.map_err(|e| DiffError::Parse(format!("failed to parse SQL: {e}")))?;
let mut aliases = ComputedAliases::default();
for stmt in &statements {
collect_computed_aliases(stmt, &mut aliases);
}
let mut collector = MarkerCollector::default();
for stmt in &statements {
let _ = Visit::visit(stmt, &mut collector);
}
if collector.found.is_empty() {
return Ok(Resolution::Verbatim);
}
if collector.found.iter().any(|(span, _)| is_empty_span(span)) {
return Ok(Resolution::Reprinted(reprint_fallback(
statements, casing, reg, &aliases,
)?));
}
let tokens = Tokenizer::new(dialect, sql)
.tokenize_with_location()
.map_err(|e| DiffError::Parse(format!("failed to tokenize SQL: {e}")))?;
let mut repls = Vec::with_capacity(collector.found.len());
for (span, marker_expr) in &collector.found {
let derivative = differentiate_marker_tree(marker_expr, casing, reg, &aliases)?;
let function = marker_expr_kind(marker_expr)
.ok_or_else(|| DiffError::Internal("outermost marker lost its kind".into()))?;
let start = locate(sql, span.start, false)
.ok_or_else(|| DiffError::Internal("marker span start out of range".into()))?;
let close = marker_call_close(&tokens, span.start).ok_or_else(|| {
DiffError::Internal("could not locate the marker call's closing parenthesis".into())
})?;
let end = locate(sql, close, true)
.ok_or_else(|| DiffError::Internal("marker span end out of range".into()))?;
let marker = sql[start..end].to_string();
repls.push(Replacement {
start,
end,
function,
marker,
derivative,
});
}
Ok(Resolution::Spliced(repls))
}
fn differentiate_marker_tree(
marker_expr: &Expr,
casing: IdentCasing,
reg: &RuleRegistry,
aliases: &ComputedAliases,
) -> Result<String> {
let mut clone = marker_expr.clone();
let mut rw = MarkerRewriter {
casing,
reg,
aliases,
};
if let ControlFlow::Break(err) = VisitMut::visit(&mut clone, &mut rw) {
return Err(err);
}
Ok(clone.to_string())
}
fn reprint_fallback(
mut statements: Vec<Statement>,
casing: IdentCasing,
reg: &RuleRegistry,
aliases: &ComputedAliases,
) -> Result<String> {
for stmt in &mut statements {
let mut rw = MarkerRewriter {
casing,
reg,
aliases,
};
if let ControlFlow::Break(err) = VisitMut::visit(stmt, &mut rw) {
return Err(err);
}
}
Ok(statements
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("; "))
}
fn differentiate_marker(f: &Function, casing: IdentCasing, reg: &RuleRegistry) -> Result<Expr> {
let kind = marker_kind(f).ok_or_else(|| DiffError::Internal("not a marker".into()))?;
let args = positional_args(f).ok_or_else(|| {
DiffError::InvalidMarker("marker call has non-positional arguments".into())
})?;
match kind {
MarkerKind::Grad => {
if args.len() != 2 {
return Err(DiffError::InvalidMarker(format!(
"grad(expr, column) expects 2 arguments, got {}",
args.len()
)));
}
let wrt = ColRef::from_wrt_arg("grad", args[1])?;
differentiate(args[0], &wrt, casing, reg)
}
MarkerKind::Jvp => {
if args.len() != 3 {
return Err(DiffError::InvalidMarker(format!(
"jvp(expr, column, tangent) expects 3 arguments, got {}",
args.len()
)));
}
let wrt = ColRef::from_wrt_arg("jvp", args[1])?;
let seeds = vec![(wrt, args[2].clone())];
jvp(args[0], &seeds, casing, reg)
}
}
}
fn projection_guard(f: &Function, aliases: &ComputedAliases) -> Result<()> {
if aliases.is_empty() {
return Ok(());
}
let Some(args) = positional_args(f) else {
return Ok(());
};
let Some(expr_arg) = args.first() else {
return Ok(());
};
let wrt_name = args
.get(1)
.and_then(|a| ColRef::from_expr(a))
.map(|c| c.name.value.to_ascii_lowercase());
let mut cols = ColumnCollector::default();
let _ = Visit::visit(*expr_arg, &mut cols);
for c in cols.cols {
let lname = c.name.value.to_ascii_lowercase();
if Some(&lname) == wrt_name.as_ref() {
continue;
}
let is_boundary = match &c.qualifier {
None => aliases.bare.contains(&lname),
Some(q) => aliases
.qualified
.contains(&(q.value.to_ascii_lowercase(), lname.clone())),
};
if is_boundary {
return Err(DiffError::ProjectionBoundary(format!(
"`{}` is a computed select-list alias of a CTE/derived table used \
as a non-differentiation term; grad does not see through the \
projection boundary — differentiate inside that CTE instead",
c.display()
)));
}
}
Ok(())
}
#[derive(Default)]
struct MarkerCollector {
depth: usize,
found: Vec<(Span, Expr)>,
}
impl Visitor for MarkerCollector {
type Break = ();
fn pre_visit_expr(&mut self, expr: &Expr) -> ControlFlow<()> {
if is_marker_expr(expr) {
if self.depth == 0 {
self.found.push((expr.span(), expr.clone()));
}
self.depth += 1;
}
ControlFlow::Continue(())
}
fn post_visit_expr(&mut self, expr: &Expr) -> ControlFlow<()> {
if is_marker_expr(expr) {
self.depth -= 1;
}
ControlFlow::Continue(())
}
}
struct MarkerRewriter<'a> {
casing: IdentCasing,
reg: &'a RuleRegistry,
aliases: &'a ComputedAliases,
}
impl VisitorMut for MarkerRewriter<'_> {
type Break = DiffError;
fn post_visit_expr(&mut self, expr: &mut Expr) -> ControlFlow<DiffError> {
let replacement = match expr {
Expr::Function(f) if marker_kind(f).is_some() => {
if let Err(err) = projection_guard(f, self.aliases) {
return ControlFlow::Break(err);
}
match differentiate_marker(f, self.casing, self.reg) {
Ok(d) => Some(d),
Err(err) => return ControlFlow::Break(err),
}
}
_ => None,
};
if let Some(d) = replacement {
*expr = Expr::Nested(Box::new(d));
}
ControlFlow::Continue(())
}
}
#[derive(Default)]
struct ColumnCollector {
cols: Vec<ColRef>,
}
impl Visitor for ColumnCollector {
type Break = ();
fn pre_visit_expr(&mut self, expr: &Expr) -> ControlFlow<()> {
match expr {
Expr::Identifier(_) | Expr::CompoundIdentifier(_) => {
if let Some(cr) = ColRef::from_expr(expr) {
self.cols.push(cr);
}
}
_ => {}
}
ControlFlow::Continue(())
}
}
#[derive(Default)]
struct ComputedAliases {
bare: HashSet<String>,
qualified: HashSet<(String, String)>,
}
impl ComputedAliases {
fn is_empty(&self) -> bool {
self.bare.is_empty() && self.qualified.is_empty()
}
}
fn collect_computed_aliases(stmt: &Statement, out: &mut ComputedAliases) {
match stmt {
Statement::Query(q) => walk_query(q, None, out),
Statement::Insert(insert) => {
if let Some(source) = &insert.source {
walk_query(source, None, out);
}
}
_ => {}
}
}
fn walk_query(q: &Query, owner: Option<&str>, out: &mut ComputedAliases) {
if let Some(with) = &q.with {
for cte in &with.cte_tables {
let name = cte.alias.name.value.to_ascii_lowercase();
walk_query(&cte.query, Some(&name), out);
}
}
walk_set_expr(&q.body, owner, out);
}
fn walk_set_expr(body: &SetExpr, owner: Option<&str>, out: &mut ComputedAliases) {
match body {
SetExpr::Select(select) => walk_select(select, owner, out),
SetExpr::Query(q) => walk_query(q, owner, out),
SetExpr::SetOperation { left, right, .. } => {
walk_set_expr(left, owner, out);
walk_set_expr(right, owner, out);
}
_ => {}
}
}
fn walk_select(select: &Select, owner: Option<&str>, out: &mut ComputedAliases) {
for item in &select.projection {
if let SelectItem::ExprWithAlias { expr, alias } = item {
if ColRef::from_expr(expr).is_none() {
let name = alias.value.to_ascii_lowercase();
if let Some(o) = owner {
out.qualified.insert((o.to_string(), name.clone()));
}
out.bare.insert(name);
}
}
}
for twj in &select.from {
walk_table_factor(&twj.relation, out);
for join in &twj.joins {
walk_table_factor(&join.relation, out);
}
}
}
fn walk_table_factor(tf: &TableFactor, out: &mut ComputedAliases) {
if let TableFactor::Derived {
subquery, alias, ..
} = tf
{
let owner = alias.as_ref().map(|a| a.name.value.to_ascii_lowercase());
walk_query(subquery, owner.as_deref(), out);
}
}
fn marker_call_close(tokens: &[TokenWithSpan], name_start: Location) -> Option<Location> {
let mut depth = 0usize;
let mut opened = false;
for t in tokens.iter().filter(|t| t.span.start >= name_start) {
match t.token {
Token::LParen => {
depth += 1;
opened = true;
}
Token::RParen => {
depth = depth.checked_sub(1)?;
if opened && depth == 0 {
return Some(t.span.start);
}
}
_ => {}
}
}
None
}
fn is_empty_span(span: &Span) -> bool {
span.start.line == 0 || span.start.column == 0 || span.end.line == 0 || span.end.column == 0
}
fn locate(sql: &str, loc: Location, past: bool) -> Option<usize> {
let mut line: u64 = 1;
let mut col: u64 = 1;
for (byte_idx, ch) in sql.char_indices() {
if line == loc.line && col == loc.column {
return Some(if past {
byte_idx + ch.len_utf8()
} else {
byte_idx
});
}
if ch == '\n' {
line += 1;
col = 1;
} else {
col += 1;
}
}
if line == loc.line && col == loc.column {
return Some(sql.len());
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pre_gate_matches_unqualified_markers() {
assert!(pre_gate_hit("SELECT grad(x, x) FROM t"));
assert!(pre_gate_hit("SELECT jvp(x, x, dx) FROM t"));
assert!(pre_gate_hit("grad(x,x)")); assert!(pre_gate_hit("SELECT GRAD (x, x) FROM t")); assert!(pre_gate_hit("SELECT AVG(grad(x, x)) FROM t")); }
#[test]
fn pre_gate_rejects_non_markers() {
assert!(!pre_gate_hit("SELECT a + b FROM t")); assert!(!pre_gate_hit("SELECT mygrad(x) FROM t")); assert!(!pre_gate_hit("SELECT schema.grad(x, x) FROM t")); assert!(!pre_gate_hit("SELECT grad AS g FROM t")); assert!(!pre_gate_hit("SELECT upgrade(x) FROM t")); }
}