zerodds_sql_filter/ast.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! AST data types for content-filter expressions.
5
6use alloc::boxed::Box;
7use alloc::string::String;
8use alloc::vec::Vec;
9
10/// An evaluable scalar value. OMG allows more types (char, etc.) — we
11/// deliberately map narrowly to the most common cases here.
12#[derive(Debug, Clone, PartialEq)]
13pub enum Value {
14 /// String literal or string field.
15 String(String),
16 /// Signed 64-bit integer (maps Rust `i8..i64` + `u8..u32`).
17 Int(i64),
18 /// Double precision (for `f32` + `f64` fields).
19 Float(f64),
20 /// Boolean.
21 Bool(bool),
22}
23
24/// Comparison operators.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum CmpOp {
27 /// `=`
28 Eq,
29 /// `!=` / `<>`
30 Neq,
31 /// `<`
32 Lt,
33 /// `<=`
34 Le,
35 /// `>`
36 Gt,
37 /// `>=`
38 Ge,
39 /// `LIKE` — wildcard comparison (only for strings).
40 Like,
41}
42
43/// A filter expression. A recursive tree, produced by [`crate::parse`].
44#[derive(Debug, Clone, PartialEq)]
45pub enum Expr {
46 /// Combined `AND` expression.
47 And(Box<Expr>, Box<Expr>),
48 /// Kombinierter `OR`-Ausdruck.
49 Or(Box<Expr>, Box<Expr>),
50 /// `NOT`-Negation.
51 Not(Box<Expr>),
52 /// Comparison between two operands.
53 Cmp {
54 /// Linker Operand.
55 lhs: Operand,
56 /// Operator.
57 op: CmpOp,
58 /// Rechter Operand.
59 rhs: Operand,
60 },
61 /// `field BETWEEN low AND high` (spec §B.2.1 BetweenPredicate).
62 /// Equivalent to `field >= low AND field <= high`. `negated = true`
63 /// for `NOT BETWEEN`.
64 Between {
65 /// Field operand (left of BETWEEN).
66 field: Operand,
67 /// Lower bound (inclusive).
68 low: Operand,
69 /// Upper bound (inclusive).
70 high: Operand,
71 /// `NOT BETWEEN`?
72 negated: bool,
73 },
74}
75
76/// A comparison operand: literal, field reference or parameter.
77#[derive(Debug, Clone, PartialEq)]
78pub enum Operand {
79 /// Constant literal.
80 Literal(Value),
81 /// Field access, possibly dotted (`a.b.c`).
82 Field(String),
83 /// Positional parameter `%N`.
84 Param(u32),
85}
86
87impl Expr {
88 /// Recursive sub-expression count (useful for tests / metrics).
89 #[must_use]
90 pub fn node_count(&self) -> usize {
91 match self {
92 Self::And(a, b) | Self::Or(a, b) => 1 + a.node_count() + b.node_count(),
93 Self::Not(inner) => 1 + inner.node_count(),
94 Self::Cmp { .. } => 1,
95 Self::Between { .. } => 1,
96 }
97 }
98
99 /// Collects all parameter indices that occur in the expression.
100 #[must_use]
101 pub fn collect_param_indices(&self) -> Vec<u32> {
102 /// zerodds-lint: recursion-depth = parse-tree-depth (≤ 64 via
103 /// parser input caps; no unbounded recursion possible).
104 fn walk(e: &Expr, out: &mut Vec<u32>) {
105 match e {
106 Expr::And(a, b) | Expr::Or(a, b) => {
107 walk(a, out);
108 walk(b, out);
109 }
110 Expr::Not(inner) => walk(inner, out),
111 Expr::Cmp { lhs, rhs, .. } => {
112 if let Operand::Param(i) = lhs {
113 out.push(*i);
114 }
115 if let Operand::Param(i) = rhs {
116 out.push(*i);
117 }
118 }
119 Expr::Between {
120 field, low, high, ..
121 } => {
122 for op in [field, low, high] {
123 if let Operand::Param(i) = op {
124 out.push(*i);
125 }
126 }
127 }
128 }
129 }
130 let mut out = Vec::new();
131 walk(self, &mut out);
132 out
133 }
134}