Skip to main content

oxisql_parse/optimizer/
expr_util.rs

1//! Shared expression utilities for optimizer passes.
2//!
3//! Provides:
4//! - SQL fragment parsing helpers (`parse_predicate`, `parse_expr`)
5//! - Expression rendering (`render`)
6//! - Conjunct splitting and joining (`split_conjuncts`, `join_conjuncts`)
7//! - Equi-join key extraction (`equi_key`)
8//! - Column-reference collection (`collect_colrefs`)
9//! - Canonical structural hashing (`canonical_hash`)
10//! - Common-subexpression detection (`find_common_subexprs`)
11
12use std::collections::hash_map::DefaultHasher;
13use std::hash::{Hash, Hasher};
14
15use sqlparser::ast::{BinaryOperator, Expr, SelectItem, SetExpr, Statement};
16use sqlparser::dialect::GenericDialect;
17use sqlparser::parser::Parser;
18
19// ── Types ─────────────────────────────────────────────────────────────────────
20
21/// A qualified column reference extracted from an expression.
22#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23pub struct ColRef {
24    /// The table qualifier, if the column was written as `table.column`.
25    pub qualifier: Option<String>,
26    /// The bare column name.
27    pub name: String,
28}
29
30// ── Parsing helpers ───────────────────────────────────────────────────────────
31
32/// Parse a SQL predicate string into an [`Expr`], returning `None` on failure.
33///
34/// The predicate is wrapped in `SELECT 1 WHERE <predicate>` so that the full
35/// `sqlparser` WHERE-expression grammar is available.
36pub fn parse_predicate(s: &str) -> Option<Expr> {
37    let sql = format!("SELECT 1 WHERE {s}");
38    let dialect = GenericDialect {};
39    let stmts = Parser::parse_sql(&dialect, &sql).ok()?;
40    if let Some(Statement::Query(q)) = stmts.into_iter().next() {
41        if let SetExpr::Select(sel) = *q.body {
42            return sel.selection;
43        }
44    }
45    None
46}
47
48/// Parse a standalone SQL expression (not a WHERE predicate), returning `None`
49/// on failure.
50///
51/// The expression is wrapped in `SELECT <expr>` so that the full projection
52/// grammar is available, including function calls, casts, etc.
53pub fn parse_expr(s: &str) -> Option<Expr> {
54    let sql = format!("SELECT {s}");
55    let dialect = GenericDialect {};
56    let stmts = Parser::parse_sql(&dialect, &sql).ok()?;
57    if let Some(Statement::Query(q)) = stmts.into_iter().next() {
58        if let SetExpr::Select(sel) = *q.body {
59            if let Some(SelectItem::UnnamedExpr(e)) = sel.projection.into_iter().next() {
60                return Some(e);
61            }
62        }
63    }
64    None
65}
66
67// ── Rendering ────────────────────────────────────────────────────────────────
68
69/// Render an [`Expr`] back to a SQL string via its `Display` implementation.
70pub fn render(e: &Expr) -> String {
71    e.to_string()
72}
73
74// ── Conjunct utilities ────────────────────────────────────────────────────────
75
76/// Split a top-level AND expression into its conjuncts (leaves of the AND tree).
77///
78/// `a AND b AND c` → `[a, b, c]`.  Non-AND expressions are returned as a
79/// single-element `Vec`.
80pub fn split_conjuncts(e: Expr) -> Vec<Expr> {
81    match e {
82        Expr::BinaryOp {
83            left,
84            op: BinaryOperator::And,
85            right,
86        } => {
87            let mut v = split_conjuncts(*left);
88            v.extend(split_conjuncts(*right));
89            v
90        }
91        other => vec![other],
92    }
93}
94
95/// Join a list of expressions with AND.
96///
97/// Returns `None` when the list is empty, and `Some(expr)` when it has one
98/// element, or `Some(a AND b AND … AND z)` for multiple elements.
99pub fn join_conjuncts(exprs: Vec<Expr>) -> Option<Expr> {
100    exprs.into_iter().reduce(|acc, e| Expr::BinaryOp {
101        left: Box::new(acc),
102        op: BinaryOperator::And,
103        right: Box::new(e),
104    })
105}
106
107// ── Equi-join key extraction ──────────────────────────────────────────────────
108
109/// If `e` is an equi-join predicate `a.x = b.y` (or `x = y`), return the two
110/// [`ColRef`]s.  Returns `None` for any other expression shape.
111pub fn equi_key(e: &Expr) -> Option<(ColRef, ColRef)> {
112    if let Expr::BinaryOp {
113        left,
114        op: BinaryOperator::Eq,
115        right,
116    } = e
117    {
118        let l = expr_to_colref(left)?;
119        let r = expr_to_colref(right)?;
120        Some((l, r))
121    } else {
122        None
123    }
124}
125
126fn expr_to_colref(e: &Expr) -> Option<ColRef> {
127    match e {
128        Expr::Identifier(id) => Some(ColRef {
129            qualifier: None,
130            name: id.value.clone(),
131        }),
132        Expr::CompoundIdentifier(parts) if parts.len() == 2 => Some(ColRef {
133            qualifier: Some(parts[0].value.clone()),
134            name: parts[1].value.clone(),
135        }),
136        _ => None,
137    }
138}
139
140// ── Column reference collection ───────────────────────────────────────────────
141
142/// Collect all column references reachable from `e`.
143///
144/// Both bare identifiers (`col`) and qualified identifiers (`tbl.col`) are
145/// included.  Aggregate arguments, subquery expressions, and other non-column
146/// leaves are skipped.
147pub fn collect_colrefs(e: &Expr) -> Vec<ColRef> {
148    let mut refs = Vec::new();
149    collect_colrefs_inner(e, &mut refs);
150    refs
151}
152
153fn collect_colrefs_inner(e: &Expr, out: &mut Vec<ColRef>) {
154    match e {
155        Expr::Identifier(id) => out.push(ColRef {
156            qualifier: None,
157            name: id.value.clone(),
158        }),
159        Expr::CompoundIdentifier(parts) if parts.len() == 2 => out.push(ColRef {
160            qualifier: Some(parts[0].value.clone()),
161            name: parts[1].value.clone(),
162        }),
163        Expr::BinaryOp { left, right, .. } => {
164            collect_colrefs_inner(left, out);
165            collect_colrefs_inner(right, out);
166        }
167        Expr::UnaryOp { expr, .. } => collect_colrefs_inner(expr, out),
168        Expr::IsNull(e) | Expr::IsNotNull(e) => collect_colrefs_inner(e, out),
169        Expr::Between {
170            expr, low, high, ..
171        } => {
172            collect_colrefs_inner(expr, out);
173            collect_colrefs_inner(low, out);
174            collect_colrefs_inner(high, out);
175        }
176        Expr::InList { expr, list, .. } => {
177            collect_colrefs_inner(expr, out);
178            for item in list {
179                collect_colrefs_inner(item, out);
180            }
181        }
182        Expr::Like { expr, pattern, .. } | Expr::ILike { expr, pattern, .. } => {
183            collect_colrefs_inner(expr, out);
184            collect_colrefs_inner(pattern, out);
185        }
186        _ => {}
187    }
188}
189
190// ── Canonical hashing ─────────────────────────────────────────────────────────
191
192/// Compute a canonical (order-normalised) structural hash of an [`Expr`].
193///
194/// Commutative binary operators (`AND`, `OR`, `+`, `*`) sort their children
195/// by hash before combining, so `a AND b` and `b AND a` produce the same hash.
196/// This is used to identify identical subexpressions regardless of operand order.
197pub fn canonical_hash(e: &Expr) -> u64 {
198    let mut h = DefaultHasher::new();
199    hash_expr(e, &mut h);
200    h.finish()
201}
202
203fn hash_expr(e: &Expr, h: &mut DefaultHasher) {
204    use std::mem::discriminant;
205    discriminant(e).hash(h);
206    match e {
207        Expr::Identifier(id) => id.value.hash(h),
208        Expr::CompoundIdentifier(parts) => {
209            for p in parts {
210                p.value.hash(h);
211            }
212        }
213        Expr::Value(v) => v.to_string().hash(h),
214        Expr::BinaryOp { left, op, right } => {
215            let is_commutative = matches!(
216                op,
217                BinaryOperator::And
218                    | BinaryOperator::Or
219                    | BinaryOperator::Plus
220                    | BinaryOperator::Multiply
221            );
222            op.to_string().hash(h);
223            if is_commutative {
224                let lh = {
225                    let mut tmp = DefaultHasher::new();
226                    hash_expr(left, &mut tmp);
227                    tmp.finish()
228                };
229                let rh = {
230                    let mut tmp = DefaultHasher::new();
231                    hash_expr(right, &mut tmp);
232                    tmp.finish()
233                };
234                let (a, b) = if lh <= rh { (lh, rh) } else { (rh, lh) };
235                a.hash(h);
236                b.hash(h);
237            } else {
238                hash_expr(left, h);
239                hash_expr(right, h);
240            }
241        }
242        Expr::UnaryOp { op, expr } => {
243            op.to_string().hash(h);
244            hash_expr(expr, h);
245        }
246        other => other.to_string().hash(h),
247    }
248}
249
250// ── Common subexpression detection ───────────────────────────────────────────
251
252/// Find common (repeated) subexpressions in `e` by canonical hash.
253///
254/// Returns one entry per unique subexpression appearing more than once:
255/// `(canonical_hash, rendered_sql_string, occurrence_count)`.
256///
257/// Trivial leaves (identifiers, column references, literal values) are not
258/// reported — only structurally non-trivial subexpressions are candidates.
259pub fn find_common_subexprs(e: &Expr) -> Vec<(u64, String, usize)> {
260    use std::collections::HashMap;
261    let mut counts: HashMap<u64, (String, usize)> = HashMap::new();
262    count_subexprs(e, &mut counts);
263    counts
264        .into_iter()
265        .filter(|(_, (_, cnt))| *cnt > 1)
266        .map(|(hash, (s, cnt))| (hash, s, cnt))
267        .collect()
268}
269
270fn count_subexprs(e: &Expr, counts: &mut std::collections::HashMap<u64, (String, usize)>) {
271    // Only non-trivial subexpressions are candidates for CSE.
272    let is_nontrivial = !matches!(
273        e,
274        Expr::Identifier(_) | Expr::CompoundIdentifier(_) | Expr::Value(_)
275    );
276    if is_nontrivial {
277        let h = canonical_hash(e);
278        let entry = counts.entry(h).or_insert_with(|| (render(e), 0));
279        entry.1 += 1;
280    }
281    // Recurse into children.
282    match e {
283        Expr::BinaryOp { left, right, .. } => {
284            count_subexprs(left, counts);
285            count_subexprs(right, counts);
286        }
287        Expr::UnaryOp { expr, .. } => count_subexprs(expr, counts),
288        Expr::Between {
289            expr, low, high, ..
290        } => {
291            count_subexprs(expr, counts);
292            count_subexprs(low, counts);
293            count_subexprs(high, counts);
294        }
295        Expr::InList { expr, list, .. } => {
296            count_subexprs(expr, counts);
297            for item in list {
298                count_subexprs(item, counts);
299            }
300        }
301        _ => {}
302    }
303}
304
305// ── Tests ─────────────────────────────────────────────────────────────────────
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310
311    #[test]
312    fn test_parse_predicate_simple() {
313        let e = parse_predicate("x > 1");
314        assert!(e.is_some(), "should parse simple predicate");
315    }
316
317    #[test]
318    fn test_parse_predicate_and() {
319        let e = parse_predicate("x > 1 AND y < 10");
320        assert!(e.is_some(), "should parse AND predicate");
321    }
322
323    #[test]
324    fn test_parse_expr_column() {
325        let e = parse_expr("a.b");
326        assert!(e.is_some(), "should parse qualified identifier");
327    }
328
329    #[test]
330    fn test_split_conjuncts_single() {
331        let e = parse_predicate("x > 1").expect("parse");
332        let parts = split_conjuncts(e);
333        assert_eq!(parts.len(), 1);
334    }
335
336    #[test]
337    fn test_split_conjuncts_and() {
338        let e = parse_predicate("x > 1 AND y < 10 AND z = 5").expect("parse");
339        let parts = split_conjuncts(e);
340        assert_eq!(parts.len(), 3, "AND should yield 3 conjuncts");
341    }
342
343    #[test]
344    fn test_join_conjuncts_empty_is_none() {
345        assert!(join_conjuncts(vec![]).is_none());
346    }
347
348    #[test]
349    fn test_join_conjuncts_single() {
350        let e = parse_predicate("x > 1").expect("parse");
351        let joined = join_conjuncts(vec![e.clone()]);
352        assert!(joined.is_some());
353    }
354
355    #[test]
356    fn test_equi_key_qualified() {
357        let e = parse_predicate("a.id = b.id").expect("parse");
358        let key = equi_key(&e);
359        assert!(key.is_some(), "equi_key should find a.id = b.id");
360        let (l, r) = key.expect("key");
361        assert_eq!(l.qualifier.as_deref(), Some("a"));
362        assert_eq!(l.name, "id");
363        assert_eq!(r.qualifier.as_deref(), Some("b"));
364    }
365
366    #[test]
367    fn test_equi_key_non_eq_returns_none() {
368        let e = parse_predicate("x > 1").expect("parse");
369        assert!(equi_key(&e).is_none(), "non-equality should return None");
370    }
371
372    #[test]
373    fn test_collect_colrefs() {
374        let e = parse_predicate("a.x = b.y AND z > 1").expect("parse");
375        let refs = collect_colrefs(&e);
376        assert!(refs.len() >= 2, "should collect at least a.x and b.y");
377    }
378
379    #[test]
380    fn test_canonical_hash_commutative() {
381        let e1 = parse_predicate("x = 1 AND y = 2").expect("parse");
382        let e2 = parse_predicate("y = 2 AND x = 1").expect("parse");
383        assert_eq!(
384            canonical_hash(&e1),
385            canonical_hash(&e2),
386            "commutative AND should produce same hash"
387        );
388    }
389
390    #[test]
391    fn test_canonical_hash_non_commutative() {
392        let e1 = parse_predicate("x > 1").expect("parse e1");
393        let e2 = parse_predicate("1 > x").expect("parse e2");
394        assert_ne!(
395            canonical_hash(&e1),
396            canonical_hash(&e2),
397            "non-commutative > should produce different hashes"
398        );
399    }
400
401    #[test]
402    fn test_find_common_subexprs_no_repeat() {
403        let e = parse_predicate("x > 1 AND y < 2").expect("parse");
404        let common = find_common_subexprs(&e);
405        assert!(
406            common.is_empty(),
407            "no repeated subexpressions in x>1 AND y<2"
408        );
409    }
410
411    #[test]
412    fn test_find_common_subexprs_with_repeat() {
413        // (x + 1) > 0 AND (x + 1) < 10 — the subexpr `x + 1` appears twice.
414        let e = parse_predicate("(x + 1) > 0 AND (x + 1) < 10").expect("parse");
415        let common = find_common_subexprs(&e);
416        assert!(!common.is_empty(), "should detect repeated subexpr `x + 1`");
417    }
418
419    #[test]
420    fn test_render_roundtrip() {
421        let e = parse_predicate("a.id = b.id").expect("parse");
422        let s = render(&e);
423        assert!(!s.is_empty(), "render should produce non-empty string");
424        // Should round-trip (re-parseable)
425        assert!(
426            parse_predicate(&s).is_some(),
427            "rendered string should be re-parseable"
428        );
429    }
430}