|
54 | 54 | use std::any::Any; |
55 | 55 | use std::collections::HashMap; |
56 | 56 | use std::f64::consts::{LN_10, LN_2}; |
| 57 | +use std::ops::ControlFlow; |
| 58 | +use std::sync::Arc; |
57 | 59 |
|
58 | | -use datafusion::arrow::datatypes::DataType; |
| 60 | +use datafusion::arrow::datatypes::{DataType, Field}; |
59 | 61 | use datafusion::common::tree_node::{Transformed, TreeNode}; |
60 | | -use datafusion::common::{DataFusionError, Result, ScalarValue}; |
| 62 | +use datafusion::common::{DFSchema, DataFusionError, Result, ScalarValue, TableReference}; |
61 | 63 | use datafusion::functions::math::expr_fn; |
62 | 64 | use datafusion::logical_expr::expr::ScalarFunction; |
63 | 65 | use datafusion::logical_expr::{ |
64 | 66 | lit, BinaryExpr, Cast, ColumnarValue, Expr, LogicalPlan, Operator, ScalarFunctionArgs, |
65 | 67 | ScalarUDF, ScalarUDFImpl, Signature, Volatility, |
66 | 68 | }; |
| 69 | +use datafusion::prelude::SessionContext; |
| 70 | +use datafusion::sql::unparser::expr_to_sql; |
| 71 | +use sqlparser::ast::{Expr as SqlExpr, Visit, VisitMut, Visitor, VisitorMut}; |
| 72 | +use sqlparser::dialect::GenericDialect; |
| 73 | +use sqlparser::parser::Parser; |
67 | 74 |
|
68 | 75 | // --------------------------------------------------------------------------- |
69 | 76 | // Constant helpers and the 0/1-folding builders |
@@ -522,6 +529,142 @@ fn rewrite_vjp(args: &[Expr]) -> Result<Expr> { |
522 | 529 | Ok(mul(args[2].clone(), derivative)) |
523 | 530 | } |
524 | 531 |
|
| 532 | +// --------------------------------------------------------------------------- |
| 533 | +// SQL source-to-source rewrite |
| 534 | +// --------------------------------------------------------------------------- |
| 535 | + |
| 536 | +/// Rewrite every `grad`/`jvp`/`vjp` call in a SQL statement into its symbolic |
| 537 | +/// derivative, returning the rewritten SQL text. |
| 538 | +/// |
| 539 | +/// Unlike a logical-plan rewrite, this is a pure source-to-source transform run |
| 540 | +/// *before* the query is planned, so it works for any query shape the SQL parser |
| 541 | +/// accepts — recursive CTEs, DML, and subqueries included. Each marker call is |
| 542 | +/// parsed into a DataFusion [`Expr`], differentiated by the engine in this |
| 543 | +/// module, and rendered back to SQL in place. Columns are taken from the call's |
| 544 | +/// own identifiers (all treated as `Float64`; types don't affect the symbolic |
| 545 | +/// result), so no catalog or table schema is needed. |
| 546 | +pub fn rewrite_grad_in_sql(sql: &str) -> Result<String> { |
| 547 | + let dialect = GenericDialect {}; |
| 548 | + let mut statements = Parser::parse_sql(&dialect, sql) |
| 549 | + .map_err(|e| DataFusionError::Plan(format!("grad: failed to parse SQL: {e}")))?; |
| 550 | + |
| 551 | + // A throwaway context that only needs the marker UDFs registered so the |
| 552 | + // calls parse into `ScalarFunction` nodes the engine can dispatch on. |
| 553 | + let ctx = SessionContext::new(); |
| 554 | + ctx.register_udf(grad_marker()); |
| 555 | + ctx.register_udf(jvp_marker()); |
| 556 | + ctx.register_udf(vjp_marker()); |
| 557 | + |
| 558 | + let mut rewriter = GradSqlRewriter { ctx: &ctx }; |
| 559 | + for stmt in &mut statements { |
| 560 | + if let ControlFlow::Break(msg) = stmt.visit(&mut rewriter) { |
| 561 | + return Err(DataFusionError::Plan(msg)); |
| 562 | + } |
| 563 | + } |
| 564 | + |
| 565 | + Ok(statements |
| 566 | + .iter() |
| 567 | + .map(ToString::to_string) |
| 568 | + .collect::<Vec<_>>() |
| 569 | + .join("; ")) |
| 570 | +} |
| 571 | + |
| 572 | +/// True if `name` is one of the autograd marker functions (case-insensitive). |
| 573 | +fn is_marker_name(name: &str) -> bool { |
| 574 | + matches!(name.to_lowercase().as_str(), "grad" | "jvp" | "vjp") |
| 575 | +} |
| 576 | + |
| 577 | +/// Walks a SQL AST and replaces each `grad`/`jvp`/`vjp` call with its derivative. |
| 578 | +struct GradSqlRewriter<'a> { |
| 579 | + ctx: &'a SessionContext, |
| 580 | +} |
| 581 | + |
| 582 | +impl VisitorMut for GradSqlRewriter<'_> { |
| 583 | + type Break = String; |
| 584 | + |
| 585 | + fn pre_visit_expr(&mut self, expr: &mut SqlExpr) -> ControlFlow<String> { |
| 586 | + let is_marker = matches!( |
| 587 | + expr, |
| 588 | + SqlExpr::Function(f) if is_marker_name(&f.name.to_string()) |
| 589 | + ); |
| 590 | + if !is_marker { |
| 591 | + return ControlFlow::Continue(()); |
| 592 | + } |
| 593 | + match self.rewrite_call(expr) { |
| 594 | + Ok(()) => ControlFlow::Continue(()), |
| 595 | + Err(e) => ControlFlow::Break(e), |
| 596 | + } |
| 597 | + } |
| 598 | +} |
| 599 | + |
| 600 | +impl GradSqlRewriter<'_> { |
| 601 | + /// Differentiate a single marker call in place. The replacement is wrapped |
| 602 | + /// in parentheses so it keeps the call's precedence in the surrounding SQL. |
| 603 | + fn rewrite_call(&self, expr: &mut SqlExpr) -> std::result::Result<(), String> { |
| 604 | + let schema = call_schema(expr)?; |
| 605 | + let text = expr.to_string(); |
| 606 | + let parsed = self |
| 607 | + .ctx |
| 608 | + .parse_sql_expr(&text, &schema) |
| 609 | + .map_err(|e| format!("grad: failed to parse '{text}': {e}"))?; |
| 610 | + let derivative = rewrite_grad_in_expr(parsed) |
| 611 | + .map_err(|e| format!("grad: failed to differentiate '{text}': {e}"))? |
| 612 | + .data; |
| 613 | + let rendered = expr_to_sql(&derivative) |
| 614 | + .map_err(|e| format!("grad: failed to render derivative for '{text}': {e}"))?; |
| 615 | + *expr = SqlExpr::Nested(Box::new(rendered)); |
| 616 | + Ok(()) |
| 617 | + } |
| 618 | +} |
| 619 | + |
| 620 | +/// Build a `Float64` schema covering every column identifier referenced inside a |
| 621 | +/// marker call, so the call's argument expression can be parsed standalone. |
| 622 | +fn call_schema(call: &SqlExpr) -> std::result::Result<DFSchema, String> { |
| 623 | + let mut collector = ColumnCollector::default(); |
| 624 | + let _ = call.visit(&mut collector); |
| 625 | + let fields = collector |
| 626 | + .cols |
| 627 | + .into_iter() |
| 628 | + .map(|(qualifier, name)| { |
| 629 | + let qualifier = qualifier.map(TableReference::bare); |
| 630 | + ( |
| 631 | + qualifier, |
| 632 | + Arc::new(Field::new(name, DataType::Float64, true)), |
| 633 | + ) |
| 634 | + }) |
| 635 | + .collect(); |
| 636 | + DFSchema::new_with_metadata(fields, HashMap::new()) |
| 637 | + .map_err(|e| format!("grad: failed to build schema for differentiation: {e}")) |
| 638 | +} |
| 639 | + |
| 640 | +/// Collects the (optional qualifier, name) of every column identifier in a SQL |
| 641 | +/// expression tree. |
| 642 | +#[derive(Default)] |
| 643 | +struct ColumnCollector { |
| 644 | + cols: Vec<(Option<String>, String)>, |
| 645 | +} |
| 646 | + |
| 647 | +impl Visitor for ColumnCollector { |
| 648 | + type Break = (); |
| 649 | + |
| 650 | + fn pre_visit_expr(&mut self, expr: &SqlExpr) -> ControlFlow<()> { |
| 651 | + let pair = match expr { |
| 652 | + SqlExpr::Identifier(ident) => Some((None, ident.value.clone())), |
| 653 | + SqlExpr::CompoundIdentifier(parts) => parts.last().map(|last| { |
| 654 | + let qualifier = (parts.len() >= 2).then(|| parts[parts.len() - 2].value.clone()); |
| 655 | + (qualifier, last.value.clone()) |
| 656 | + }), |
| 657 | + _ => None, |
| 658 | + }; |
| 659 | + if let Some(pair) = pair { |
| 660 | + if !self.cols.contains(&pair) { |
| 661 | + self.cols.push(pair); |
| 662 | + } |
| 663 | + } |
| 664 | + ControlFlow::Continue(()) |
| 665 | + } |
| 666 | +} |
| 667 | + |
525 | 668 | // --------------------------------------------------------------------------- |
526 | 669 | // Tests |
527 | 670 | // --------------------------------------------------------------------------- |
@@ -650,4 +793,46 @@ mod tests { |
650 | 793 | let rev = rewrite_vjp(&[f, col("x"), one()]).unwrap(); |
651 | 794 | assert_eq!(fwd, rev); |
652 | 795 | } |
| 796 | + |
| 797 | + #[test] |
| 798 | + fn sql_rewrite_replaces_grad_call() { |
| 799 | + // grad(sin(x), x) -> cos(x); the surrounding SELECT is preserved. |
| 800 | + let out = rewrite_grad_in_sql("SELECT grad(sin(x), x) AS d FROM t").unwrap(); |
| 801 | + assert_eq!(out, "SELECT (cos(x)) AS d FROM t"); |
| 802 | + } |
| 803 | + |
| 804 | + #[test] |
| 805 | + fn sql_rewrite_leaves_non_grad_queries_intact() { |
| 806 | + // A query with no marker is still parsed and re-emitted unchanged in |
| 807 | + // meaning (the caller only invokes the rewrite when a marker is present). |
| 808 | + let out = rewrite_grad_in_sql("SELECT a + b FROM t").unwrap(); |
| 809 | + assert_eq!(out, "SELECT a + b FROM t"); |
| 810 | + } |
| 811 | + |
| 812 | + #[test] |
| 813 | + fn sql_rewrite_fires_inside_recursive_cte() { |
| 814 | + // The #197 capability: a marker inside a recursive term is rewritten, |
| 815 | + // a query shape the Substrait bridge could never carry. d/dx(x*x) = x+x. |
| 816 | + let out = rewrite_grad_in_sql( |
| 817 | + "WITH RECURSIVE r AS (SELECT 1.0 AS x UNION ALL \ |
| 818 | + SELECT x - grad(x * x, x) FROM r WHERE x < 10) SELECT x FROM r", |
| 819 | + ) |
| 820 | + .unwrap(); |
| 821 | + assert!(out.contains("(x + x)"), "unexpected rewrite: {out}"); |
| 822 | + assert!( |
| 823 | + !out.to_lowercase().contains("grad("), |
| 824 | + "marker left behind: {out}" |
| 825 | + ); |
| 826 | + } |
| 827 | + |
| 828 | + #[test] |
| 829 | + fn sql_rewrite_handles_nested_higher_order_grad() { |
| 830 | + // grad(grad(power(x, 3), x), x) -> d2/dx2 (x^3) = 6x; bottom-up so the |
| 831 | + // inner call is differentiated before the outer one. |
| 832 | + let out = rewrite_grad_in_sql("SELECT grad(grad(power(x, 3), x), x) AS d FROM t").unwrap(); |
| 833 | + assert!( |
| 834 | + !out.to_lowercase().contains("grad("), |
| 835 | + "marker left behind: {out}" |
| 836 | + ); |
| 837 | + } |
653 | 838 | } |
0 commit comments