Skip to main content

dibs_sql/
expr.rs

1//! SQL expressions.
2
3use crate::{ColumnName, ParamName, PgType, TableName};
4
5/// A SQL expression.
6#[derive(Debug, Clone, PartialEq)]
7pub enum Expr {
8    /// A parameter placeholder (e.g., $handle -> $1)
9    Param(ParamName),
10    /// A column reference
11    Column(ColumnRef),
12    /// A string literal
13    String(String),
14    /// An integer literal
15    Int(i64),
16    /// A boolean literal
17    Bool(bool),
18    /// NULL
19    Null,
20    /// NOW() function
21    Now,
22    /// DEFAULT keyword
23    Default,
24    /// Binary operation (e.g., a = b, a AND b)
25    BinOp {
26        left: Box<Expr>,
27        op: BinOp,
28        right: Box<Expr>,
29    },
30    /// IS NULL / IS NOT NULL
31    IsNull { expr: Box<Expr>, negated: bool },
32    /// LIKE pattern match (case-sensitive)
33    Like { expr: Box<Expr>, pattern: Box<Expr> },
34    /// ILIKE pattern match (case-insensitive)
35    ILike { expr: Box<Expr>, pattern: Box<Expr> },
36    /// = ANY(array) for IN checks with array parameter
37    Any { expr: Box<Expr>, array: Box<Expr> },
38    /// JSONB -> operator (get object field, returns JSONB)
39    JsonGet { expr: Box<Expr>, key: Box<Expr> },
40    /// JSONB ->> operator (get object field as text)
41    JsonGetText { expr: Box<Expr>, key: Box<Expr> },
42    /// @> operator (contains, typically for JSONB)
43    Contains { expr: Box<Expr>, value: Box<Expr> },
44    /// ? operator (key exists, typically for JSONB)
45    KeyExists { expr: Box<Expr>, key: Box<Expr> },
46    /// Type cast (e.g., $1::text[], value::integer)
47    Cast { expr: Box<Expr>, pg_type: PgType },
48    /// EXCLUDED.column reference for ON CONFLICT DO UPDATE
49    Excluded(ColumnName),
50    /// Function call
51    FnCall { name: String, args: Vec<Expr> },
52    /// COUNT(table.*) for counting related rows
53    Count { table: TableName },
54    /// Raw SQL (escape hatch)
55    Raw(String),
56}
57
58/// A column reference, optionally qualified with table/alias.
59///
60/// Examples:
61/// - `"id"` (unqualified)
62/// - `"users"."id"` (qualified with table name)
63/// - `"t0"."id"` (qualified with table alias)
64#[derive(Debug, Clone, PartialEq)]
65pub struct ColumnRef {
66    /// Table name or alias qualifier. Renders as `"table".` prefix.
67    ///
68    /// Example: `"users"` in `"users"."id"`, or `"t0"` in `"t0"."id"`
69    pub table: Option<TableName>,
70
71    /// The column name. Renders as `"column"`.
72    ///
73    /// Example: `"id"` in `"users"."id"`
74    pub column: ColumnName,
75}
76
77impl ColumnRef {
78    pub fn new(column: ColumnName) -> Self {
79        Self {
80            table: None,
81            column,
82        }
83    }
84
85    pub fn qualified(table: TableName, column: ColumnName) -> Self {
86        Self {
87            table: Some(table),
88            column,
89        }
90    }
91}
92
93/// Binary operators for SQL expressions.
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum BinOp {
96    /// Equality: `=`
97    Eq,
98    /// Inequality: `<>`
99    Ne,
100    /// Less than: `<`
101    Lt,
102    /// Less than or equal: `<=`
103    Le,
104    /// Greater than: `>`
105    Gt,
106    /// Greater than or equal: `>=`
107    Ge,
108    /// Logical AND
109    And,
110    /// Logical OR
111    Or,
112}
113
114impl BinOp {
115    pub fn as_str(self) -> &'static str {
116        match self {
117            BinOp::Eq => "=",
118            BinOp::Ne => "<>",
119            BinOp::Lt => "<",
120            BinOp::Le => "<=",
121            BinOp::Gt => ">",
122            BinOp::Ge => ">=",
123            BinOp::And => "AND",
124            BinOp::Or => "OR",
125        }
126    }
127}
128
129// Convenience constructors
130impl Expr {
131    pub fn param(name: ParamName) -> Self {
132        Expr::Param(name)
133    }
134
135    pub fn column(name: ColumnName) -> Self {
136        Expr::Column(ColumnRef::new(name))
137    }
138
139    pub fn qualified_column(table: TableName, column: ColumnName) -> Self {
140        Expr::Column(ColumnRef::qualified(table, column))
141    }
142
143    pub fn string(s: impl Into<String>) -> Self {
144        Expr::String(s.into())
145    }
146
147    pub fn int(n: i64) -> Self {
148        Expr::Int(n)
149    }
150
151    pub fn bool(b: bool) -> Self {
152        Expr::Bool(b)
153    }
154
155    /// Create an equality expression: self = other
156    pub fn eq(self, other: Expr) -> Self {
157        Expr::BinOp {
158            left: Box::new(self),
159            op: BinOp::Eq,
160            right: Box::new(other),
161        }
162    }
163
164    /// Create an AND expression: self AND other
165    pub fn and(self, other: Expr) -> Self {
166        Expr::BinOp {
167            left: Box::new(self),
168            op: BinOp::And,
169            right: Box::new(other),
170        }
171    }
172
173    /// Create an OR expression: self OR other
174    pub fn or(self, other: Expr) -> Self {
175        Expr::BinOp {
176            left: Box::new(self),
177            op: BinOp::Or,
178            right: Box::new(other),
179        }
180    }
181
182    /// Create IS NULL expression
183    pub fn is_null(self) -> Self {
184        Expr::IsNull {
185            expr: Box::new(self),
186            negated: false,
187        }
188    }
189
190    /// Create IS NOT NULL expression
191    pub fn is_not_null(self) -> Self {
192        Expr::IsNull {
193            expr: Box::new(self),
194            negated: true,
195        }
196    }
197
198    /// Create LIKE expression (case-sensitive pattern match)
199    pub fn like(self, pattern: Expr) -> Self {
200        Expr::Like {
201            expr: Box::new(self),
202            pattern: Box::new(pattern),
203        }
204    }
205
206    /// Create ILIKE expression (case-insensitive pattern match)
207    pub fn ilike(self, pattern: Expr) -> Self {
208        Expr::ILike {
209            expr: Box::new(self),
210            pattern: Box::new(pattern),
211        }
212    }
213
214    /// Create = ANY(array) expression for IN checks
215    pub fn any(self, array: Expr) -> Self {
216        Expr::Any {
217            expr: Box::new(self),
218            array: Box::new(array),
219        }
220    }
221
222    /// Create JSONB -> expression (get object field, returns JSONB)
223    pub fn json_get(self, key: Expr) -> Self {
224        Expr::JsonGet {
225            expr: Box::new(self),
226            key: Box::new(key),
227        }
228    }
229
230    /// Create JSONB ->> expression (get object field as text)
231    pub fn json_get_text(self, key: Expr) -> Self {
232        Expr::JsonGetText {
233            expr: Box::new(self),
234            key: Box::new(key),
235        }
236    }
237
238    /// Create @> expression (contains, typically for JSONB)
239    pub fn contains(self, value: Expr) -> Self {
240        Expr::Contains {
241            expr: Box::new(self),
242            value: Box::new(value),
243        }
244    }
245
246    /// Create ? expression (key exists, typically for JSONB)
247    pub fn key_exists(self, key: Expr) -> Self {
248        Expr::KeyExists {
249            expr: Box::new(self),
250            key: Box::new(key),
251        }
252    }
253
254    /// Create a type cast expression (e.g., `$1::text[]`)
255    pub fn cast(self, pg_type: PgType) -> Self {
256        Expr::Cast {
257            expr: Box::new(self),
258            pg_type,
259        }
260    }
261
262    /// Create an EXCLUDED.column reference for ON CONFLICT DO UPDATE
263    pub fn excluded(column: ColumnName) -> Self {
264        Expr::Excluded(column)
265    }
266}