qail_core/ast/cmd/
query.rs

1//! Query builder methods for Qail.
2//!
3//! Common fluent methods: columns, filter, join, order_by, limit, etc.
4
5use crate::ast::{
6    Cage, CageKind, Condition, Expr, Join, JoinKind, LogicalOp, Operator, Qail, SortOrder,
7    Value,
8};
9
10impl Qail {
11    pub fn limit(mut self, n: i64) -> Self {
12        self.cages.push(Cage {
13            kind: CageKind::Limit(n as usize),
14            conditions: vec![],
15            logical_op: LogicalOp::And,
16        });
17        self
18    }
19
20    #[deprecated(since = "0.11.0", note = "Use .order_asc(column) instead")]
21    pub fn sort_asc(mut self, column: &str) -> Self {
22        self.cages.push(Cage {
23            kind: CageKind::Sort(SortOrder::Asc),
24            conditions: vec![Condition {
25                left: Expr::Named(column.to_string()),
26                op: Operator::Eq,
27                value: Value::Null,
28                is_array_unnest: false,
29            }],
30            logical_op: LogicalOp::And,
31        });
32        self
33    }
34
35    pub fn select_all(mut self) -> Self {
36        self.columns.push(Expr::Star);
37        self
38    }
39
40    pub fn columns<I, S>(mut self, cols: I) -> Self
41    where
42        I: IntoIterator<Item = S>,
43        S: AsRef<str>,
44    {
45        self.columns.extend(
46            cols.into_iter()
47                .map(|c| Expr::Named(c.as_ref().to_string())),
48        );
49        self
50    }
51
52    pub fn column(mut self, col: impl AsRef<str>) -> Self {
53        self.columns.push(Expr::Named(col.as_ref().to_string()));
54        self
55    }
56
57    pub fn filter(
58        mut self,
59        column: impl AsRef<str>,
60        op: Operator,
61        value: impl Into<Value>,
62    ) -> Self {
63        let filter_cage = self
64            .cages
65            .iter_mut()
66            .find(|c| matches!(c.kind, CageKind::Filter));
67
68        let condition = Condition {
69            left: Expr::Named(column.as_ref().to_string()),
70            op,
71            value: value.into(),
72            is_array_unnest: false,
73        };
74
75        if let Some(cage) = filter_cage {
76            cage.conditions.push(condition);
77        } else {
78            self.cages.push(Cage {
79                kind: CageKind::Filter,
80                conditions: vec![condition],
81                logical_op: LogicalOp::And,
82            });
83        }
84        self
85    }
86
87    pub fn or_filter(
88        mut self,
89        column: impl AsRef<str>,
90        op: Operator,
91        value: impl Into<Value>,
92    ) -> Self {
93        self.cages.push(Cage {
94            kind: CageKind::Filter,
95            conditions: vec![Condition {
96                left: Expr::Named(column.as_ref().to_string()),
97                op,
98                value: value.into(),
99                is_array_unnest: false,
100            }],
101            logical_op: LogicalOp::Or,
102        });
103        self
104    }
105
106    pub fn where_eq(self, column: impl AsRef<str>, value: impl Into<Value>) -> Self {
107        self.filter(column, Operator::Eq, value)
108    }
109
110
111    pub fn eq(self, column: impl AsRef<str>, value: impl Into<Value>) -> Self {
112        self.filter(column, Operator::Eq, value)
113    }
114
115    pub fn ne(self, column: impl AsRef<str>, value: impl Into<Value>) -> Self {
116        self.filter(column, Operator::Ne, value)
117    }
118
119    pub fn gt(self, column: impl AsRef<str>, value: impl Into<Value>) -> Self {
120        self.filter(column, Operator::Gt, value)
121    }
122    pub fn gte(self, column: impl AsRef<str>, value: impl Into<Value>) -> Self {
123        self.filter(column, Operator::Gte, value)
124    }
125
126    /// Filter: column < value
127    pub fn lt(self, column: impl AsRef<str>, value: impl Into<Value>) -> Self {
128        self.filter(column, Operator::Lt, value)
129    }
130
131    pub fn lte(self, column: impl AsRef<str>, value: impl Into<Value>) -> Self {
132        self.filter(column, Operator::Lte, value)
133    }
134
135    pub fn is_null(self, column: impl AsRef<str>) -> Self {
136        self.filter(column, Operator::IsNull, Value::Null)
137    }
138
139    pub fn is_not_null(self, column: impl AsRef<str>) -> Self {
140        self.filter(column, Operator::IsNotNull, Value::Null)
141    }
142
143    pub fn like(self, column: impl AsRef<str>, pattern: impl Into<Value>) -> Self {
144        self.filter(column, Operator::Like, pattern)
145    }
146
147    pub fn ilike(self, column: impl AsRef<str>, pattern: impl Into<Value>) -> Self {
148        self.filter(column, Operator::ILike, pattern)
149    }
150
151    pub fn in_vals<I, V>(self, column: impl AsRef<str>, values: I) -> Self
152    where
153        I: IntoIterator<Item = V>,
154        V: Into<Value>,
155    {
156        let arr: Vec<Value> = values.into_iter().map(|v| v.into()).collect();
157        self.filter(column, Operator::In, Value::Array(arr))
158    }
159
160    pub fn order_by(mut self, column: impl AsRef<str>, order: SortOrder) -> Self {
161        self.cages.push(Cage {
162            kind: CageKind::Sort(order),
163            conditions: vec![Condition {
164                left: Expr::Named(column.as_ref().to_string()),
165                op: Operator::Eq,
166                value: Value::Null,
167                is_array_unnest: false,
168            }],
169            logical_op: LogicalOp::And,
170        });
171        self
172    }
173
174    pub fn order_desc(self, column: impl AsRef<str>) -> Self {
175        self.order_by(column, SortOrder::Desc)
176    }
177
178    pub fn order_asc(self, column: impl AsRef<str>) -> Self {
179        self.order_by(column, SortOrder::Asc)
180    }
181
182    pub fn offset(mut self, n: i64) -> Self {
183        self.cages.push(Cage {
184            kind: CageKind::Offset(n as usize),
185            conditions: vec![],
186            logical_op: LogicalOp::And,
187        });
188        self
189    }
190
191    pub fn group_by<I, S>(mut self, cols: I) -> Self
192    where
193        I: IntoIterator<Item = S>,
194        S: AsRef<str>,
195    {
196        let conditions: Vec<Condition> = cols
197            .into_iter()
198            .map(|c| Condition {
199                left: Expr::Named(c.as_ref().to_string()),
200                op: Operator::Eq,
201                value: Value::Null,
202                is_array_unnest: false,
203            })
204            .collect();
205
206        self.cages.push(Cage {
207            kind: CageKind::Partition,
208            conditions,
209            logical_op: LogicalOp::And,
210        });
211        self
212    }
213
214    pub fn distinct_on_all(mut self) -> Self {
215        self.distinct = true;
216        self
217    }
218
219    pub fn join(
220        mut self,
221        kind: JoinKind,
222        table: impl AsRef<str>,
223        left_col: impl AsRef<str>,
224        right_col: impl AsRef<str>,
225    ) -> Self {
226        self.joins.push(Join {
227            kind,
228            table: table.as_ref().to_string(),
229            on: Some(vec![Condition {
230                left: Expr::Named(left_col.as_ref().to_string()),
231                op: Operator::Eq,
232                value: Value::Column(right_col.as_ref().to_string()),
233                is_array_unnest: false,
234            }]),
235            on_true: false,
236        });
237        self
238    }
239
240    pub fn left_join(
241        self,
242        table: impl AsRef<str>,
243        left_col: impl AsRef<str>,
244        right_col: impl AsRef<str>,
245    ) -> Self {
246        self.join(JoinKind::Left, table, left_col, right_col)
247    }
248
249    pub fn inner_join(
250        self,
251        table: impl AsRef<str>,
252        left_col: impl AsRef<str>,
253        right_col: impl AsRef<str>,
254    ) -> Self {
255        self.join(JoinKind::Inner, table, left_col, right_col)
256    }
257
258    pub fn returning<I, S>(mut self, cols: I) -> Self
259    where
260        I: IntoIterator<Item = S>,
261        S: AsRef<str>,
262    {
263        self.returning = Some(
264            cols.into_iter()
265                .map(|c| Expr::Named(c.as_ref().to_string()))
266                .collect(),
267        );
268        self
269    }
270
271    pub fn returning_all(mut self) -> Self {
272        self.returning = Some(vec![Expr::Star]);
273        self
274    }
275
276    pub fn values<I, V>(mut self, vals: I) -> Self
277    where
278        I: IntoIterator<Item = V>,
279        V: Into<Value>,
280    {
281        self.cages.push(Cage {
282            kind: CageKind::Payload,
283            conditions: vals
284                .into_iter()
285                .enumerate()
286                .map(|(i, v)| Condition {
287                    left: Expr::Named(format!("${}", i + 1)),
288                    op: Operator::Eq,
289                    value: v.into(),
290                    is_array_unnest: false,
291                })
292                .collect(),
293            logical_op: LogicalOp::And,
294        });
295        self
296    }
297
298    pub fn set_value(mut self, column: impl AsRef<str>, value: impl Into<Value>) -> Self {
299        let payload_cage = self
300            .cages
301            .iter_mut()
302            .find(|c| matches!(c.kind, CageKind::Payload));
303
304        let condition = Condition {
305            left: Expr::Named(column.as_ref().to_string()),
306            op: Operator::Eq,
307            value: value.into(),
308            is_array_unnest: false,
309        };
310
311        if let Some(cage) = payload_cage {
312            cage.conditions.push(condition);
313        } else {
314            self.cages.push(Cage {
315                kind: CageKind::Payload,
316                conditions: vec![condition],
317                logical_op: LogicalOp::And,
318            });
319        }
320        self
321    }
322
323    /// Set value only if Some, skip entirely if None
324    /// This is ergonomic for optional fields - the column is not included in the INSERT at all if None
325    pub fn set_opt<T>(self, column: impl AsRef<str>, value: Option<T>) -> Self
326    where
327        T: Into<Value>,
328    {
329        match value {
330            Some(v) => self.set_value(column, v),
331            None => self, // Skip entirely, don't add column
332        }
333    }
334}