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    // FLUENT FILTER SHORTCUTS (ergonomic API)
112    // ============================================================
113
114    /// Filter: column = value (shorter alias for where_eq)
115    pub fn eq(self, column: impl AsRef<str>, value: impl Into<Value>) -> Self {
116        self.filter(column, Operator::Eq, value)
117    }
118
119    /// Filter: column != value
120    pub fn ne(self, column: impl AsRef<str>, value: impl Into<Value>) -> Self {
121        self.filter(column, Operator::Ne, value)
122    }
123
124    /// Filter: column > value
125    pub fn gt(self, column: impl AsRef<str>, value: impl Into<Value>) -> Self {
126        self.filter(column, Operator::Gt, value)
127    }
128
129    /// Filter: column >= value
130    pub fn gte(self, column: impl AsRef<str>, value: impl Into<Value>) -> Self {
131        self.filter(column, Operator::Gte, value)
132    }
133
134    /// Filter: column < value
135    pub fn lt(self, column: impl AsRef<str>, value: impl Into<Value>) -> Self {
136        self.filter(column, Operator::Lt, value)
137    }
138
139    /// Filter: column <= value
140    pub fn lte(self, column: impl AsRef<str>, value: impl Into<Value>) -> Self {
141        self.filter(column, Operator::Lte, value)
142    }
143
144    /// Filter: column IS NULL
145    pub fn is_null(self, column: impl AsRef<str>) -> Self {
146        self.filter(column, Operator::IsNull, Value::Null)
147    }
148
149    /// Filter: column IS NOT NULL
150    pub fn is_not_null(self, column: impl AsRef<str>) -> Self {
151        self.filter(column, Operator::IsNotNull, Value::Null)
152    }
153
154    /// Filter: column LIKE pattern
155    pub fn like(self, column: impl AsRef<str>, pattern: impl Into<Value>) -> Self {
156        self.filter(column, Operator::Like, pattern)
157    }
158
159    /// Filter: column ILIKE pattern (case-insensitive)
160    pub fn ilike(self, column: impl AsRef<str>, pattern: impl Into<Value>) -> Self {
161        self.filter(column, Operator::ILike, pattern)
162    }
163
164    /// Filter: column IN (values)
165    pub fn in_vals<I, V>(self, column: impl AsRef<str>, values: I) -> Self
166    where
167        I: IntoIterator<Item = V>,
168        V: Into<Value>,
169    {
170        let arr: Vec<Value> = values.into_iter().map(|v| v.into()).collect();
171        self.filter(column, Operator::In, Value::Array(arr))
172    }
173
174    pub fn order_by(mut self, column: impl AsRef<str>, order: SortOrder) -> Self {
175        self.cages.push(Cage {
176            kind: CageKind::Sort(order),
177            conditions: vec![Condition {
178                left: Expr::Named(column.as_ref().to_string()),
179                op: Operator::Eq,
180                value: Value::Null,
181                is_array_unnest: false,
182            }],
183            logical_op: LogicalOp::And,
184        });
185        self
186    }
187
188    pub fn order_desc(self, column: impl AsRef<str>) -> Self {
189        self.order_by(column, SortOrder::Desc)
190    }
191
192    pub fn order_asc(self, column: impl AsRef<str>) -> Self {
193        self.order_by(column, SortOrder::Asc)
194    }
195
196    pub fn offset(mut self, n: i64) -> Self {
197        self.cages.push(Cage {
198            kind: CageKind::Offset(n as usize),
199            conditions: vec![],
200            logical_op: LogicalOp::And,
201        });
202        self
203    }
204
205    pub fn group_by<I, S>(mut self, cols: I) -> Self
206    where
207        I: IntoIterator<Item = S>,
208        S: AsRef<str>,
209    {
210        let conditions: Vec<Condition> = cols
211            .into_iter()
212            .map(|c| Condition {
213                left: Expr::Named(c.as_ref().to_string()),
214                op: Operator::Eq,
215                value: Value::Null,
216                is_array_unnest: false,
217            })
218            .collect();
219
220        self.cages.push(Cage {
221            kind: CageKind::Partition,
222            conditions,
223            logical_op: LogicalOp::And,
224        });
225        self
226    }
227
228    pub fn distinct_on_all(mut self) -> Self {
229        self.distinct = true;
230        self
231    }
232
233    pub fn join(
234        mut self,
235        kind: JoinKind,
236        table: impl AsRef<str>,
237        left_col: impl AsRef<str>,
238        right_col: impl AsRef<str>,
239    ) -> Self {
240        self.joins.push(Join {
241            kind,
242            table: table.as_ref().to_string(),
243            on: Some(vec![Condition {
244                left: Expr::Named(left_col.as_ref().to_string()),
245                op: Operator::Eq,
246                value: Value::Column(right_col.as_ref().to_string()),
247                is_array_unnest: false,
248            }]),
249            on_true: false,
250        });
251        self
252    }
253
254    pub fn left_join(
255        self,
256        table: impl AsRef<str>,
257        left_col: impl AsRef<str>,
258        right_col: impl AsRef<str>,
259    ) -> Self {
260        self.join(JoinKind::Left, table, left_col, right_col)
261    }
262
263    pub fn inner_join(
264        self,
265        table: impl AsRef<str>,
266        left_col: impl AsRef<str>,
267        right_col: impl AsRef<str>,
268    ) -> Self {
269        self.join(JoinKind::Inner, table, left_col, right_col)
270    }
271
272    pub fn returning<I, S>(mut self, cols: I) -> Self
273    where
274        I: IntoIterator<Item = S>,
275        S: AsRef<str>,
276    {
277        self.returning = Some(
278            cols.into_iter()
279                .map(|c| Expr::Named(c.as_ref().to_string()))
280                .collect(),
281        );
282        self
283    }
284
285    pub fn returning_all(mut self) -> Self {
286        self.returning = Some(vec![Expr::Star]);
287        self
288    }
289
290    pub fn values<I, V>(mut self, vals: I) -> Self
291    where
292        I: IntoIterator<Item = V>,
293        V: Into<Value>,
294    {
295        self.cages.push(Cage {
296            kind: CageKind::Payload,
297            conditions: vals
298                .into_iter()
299                .enumerate()
300                .map(|(i, v)| Condition {
301                    left: Expr::Named(format!("${}", i + 1)),
302                    op: Operator::Eq,
303                    value: v.into(),
304                    is_array_unnest: false,
305                })
306                .collect(),
307            logical_op: LogicalOp::And,
308        });
309        self
310    }
311
312    pub fn set_value(mut self, column: impl AsRef<str>, value: impl Into<Value>) -> Self {
313        let payload_cage = self
314            .cages
315            .iter_mut()
316            .find(|c| matches!(c.kind, CageKind::Payload));
317
318        let condition = Condition {
319            left: Expr::Named(column.as_ref().to_string()),
320            op: Operator::Eq,
321            value: value.into(),
322            is_array_unnest: false,
323        };
324
325        if let Some(cage) = payload_cage {
326            cage.conditions.push(condition);
327        } else {
328            self.cages.push(Cage {
329                kind: CageKind::Payload,
330                conditions: vec![condition],
331                logical_op: LogicalOp::And,
332            });
333        }
334        self
335    }
336
337    /// Set value only if Some, skip entirely if None
338    /// This is ergonomic for optional fields - the column is not included in the INSERT at all if None
339    pub fn set_opt<T>(self, column: impl AsRef<str>, value: Option<T>) -> Self
340    where
341        T: Into<Value>,
342    {
343        match value {
344            Some(v) => self.set_value(column, v),
345            None => self, // Skip entirely, don't add column
346        }
347    }
348}