Skip to main content

qail_core/ast/cmd/
advanced.rs

1//! Advanced query builder methods.
2//!
3//! DISTINCT ON, HAVING, row locks, table sampling, JOIN aliases, etc.
4
5use crate::ast::{
6    CTEDef, Cage, CageKind, Condition, Expr, Join, JoinKind, LockMode, LogicalOp, Operator,
7    OverridingKind, Qail, SampleMethod, SortOrder, Value,
8};
9
10impl Qail {
11    /// Add a column expression.
12    pub fn column_expr(mut self, expr: Expr) -> Self {
13        self.columns.push(expr);
14        self
15    }
16
17    /// Add multiple column expressions.
18    pub fn columns_expr<I>(mut self, exprs: I) -> Self
19    where
20        I: IntoIterator<Item = Expr>,
21    {
22        self.columns.extend(exprs);
23        self
24    }
25
26    /// DISTINCT ON named columns.
27    pub fn distinct_on<I, S>(mut self, cols: I) -> Self
28    where
29        I: IntoIterator<Item = S>,
30        S: AsRef<str>,
31    {
32        self.distinct_on = cols
33            .into_iter()
34            .map(|c| Expr::Named(c.as_ref().to_string()))
35            .collect();
36        self
37    }
38
39    /// DISTINCT ON expressions.
40    pub fn distinct_on_expr<I>(mut self, exprs: I) -> Self
41    where
42        I: IntoIterator<Item = Expr>,
43    {
44        self.distinct_on = exprs.into_iter().collect();
45        self
46    }
47
48    /// Add a raw Condition to the WHERE clause.
49    pub fn filter_cond(mut self, condition: Condition) -> Self {
50        let filter_cage = self
51            .cages
52            .iter_mut()
53            .find(|c| matches!(c.kind, CageKind::Filter) && c.logical_op == LogicalOp::And);
54
55        if let Some(cage) = filter_cage {
56            cage.conditions.push(condition);
57        } else {
58            self.cages.push(Cage {
59                kind: CageKind::Filter,
60                conditions: vec![condition],
61                logical_op: LogicalOp::And,
62            });
63        }
64        self
65    }
66
67    /// Add a HAVING condition.
68    pub fn having_cond(mut self, condition: Condition) -> Self {
69        self.having.push(condition);
70        self
71    }
72
73    /// Add multiple HAVING conditions.
74    pub fn having_conds(mut self, conditions: impl IntoIterator<Item = Condition>) -> Self {
75        self.having.extend(conditions);
76        self
77    }
78
79    /// Set CTEs (WITH clause).
80    pub fn with_ctes(mut self, ctes: Vec<CTEDef>) -> Self {
81        self.ctes = ctes;
82        self
83    }
84
85    /// UPDATE … FROM additional tables.
86    pub fn update_from<I, S>(mut self, tables: I) -> Self
87    where
88        I: IntoIterator<Item = S>,
89        S: AsRef<str>,
90    {
91        self.from_tables
92            .extend(tables.into_iter().map(|s| s.as_ref().to_string()));
93        self
94    }
95
96    /// DELETE … USING additional tables.
97    pub fn delete_using<I, S>(mut self, tables: I) -> Self
98    where
99        I: IntoIterator<Item = S>,
100        S: AsRef<str>,
101    {
102        self.using_tables
103            .extend(tables.into_iter().map(|s| s.as_ref().to_string()));
104        self
105    }
106
107    /// FOR UPDATE row lock.
108    pub fn for_update(mut self) -> Self {
109        self.lock_mode = Some(LockMode::Update);
110        self
111    }
112
113    /// FOR UPDATE SKIP LOCKED row lock.
114    /// Used for concurrent job claiming (e.g. outbox dispatch).
115    pub fn for_update_skip_locked(mut self) -> Self {
116        self.lock_mode = Some(LockMode::Update);
117        self.skip_locked = true;
118        self
119    }
120
121    /// FOR NO KEY UPDATE row lock.
122    pub fn for_no_key_update(mut self) -> Self {
123        self.lock_mode = Some(LockMode::NoKeyUpdate);
124        self
125    }
126
127    /// FOR SHARE row lock.
128    pub fn for_share(mut self) -> Self {
129        self.lock_mode = Some(LockMode::Share);
130        self
131    }
132
133    /// FOR KEY SHARE row lock.
134    pub fn for_key_share(mut self) -> Self {
135        self.lock_mode = Some(LockMode::KeyShare);
136        self
137    }
138
139    /// FETCH FIRST n ROWS ONLY.
140    pub fn fetch_first(mut self, count: u64) -> Self {
141        self.fetch = Some((count, false));
142        self
143    }
144
145    /// FETCH FIRST n ROWS WITH TIES.
146    pub fn fetch_with_ties(mut self, count: u64) -> Self {
147        self.fetch = Some((count, true));
148        self
149    }
150
151    /// INSERT with DEFAULT VALUES.
152    pub fn default_values(mut self) -> Self {
153        self.default_values = true;
154        self
155    }
156
157    /// OVERRIDING SYSTEM VALUE.
158    pub fn overriding_system_value(mut self) -> Self {
159        self.overriding = Some(OverridingKind::SystemValue);
160        self
161    }
162
163    /// OVERRIDING USER VALUE.
164    pub fn overriding_user_value(mut self) -> Self {
165        self.overriding = Some(OverridingKind::UserValue);
166        self
167    }
168
169    /// TABLESAMPLE BERNOULLI(percent).
170    pub fn tablesample_bernoulli(mut self, percent: f64) -> Self {
171        self.sample = Some((SampleMethod::Bernoulli, percent, None));
172        self
173    }
174
175    /// TABLESAMPLE SYSTEM(percent).
176    pub fn tablesample_system(mut self, percent: f64) -> Self {
177        self.sample = Some((SampleMethod::System, percent, None));
178        self
179    }
180
181    /// REPEATABLE(seed) for TABLESAMPLE.
182    pub fn repeatable(mut self, seed: u64) -> Self {
183        if let Some((method, percent, _)) = self.sample {
184            self.sample = Some((method, percent, Some(seed)));
185        }
186        self
187    }
188
189    /// SELECT FROM ONLY (exclude child tables).
190    pub fn only(mut self) -> Self {
191        self.only_table = true;
192        self
193    }
194
195    /// LEFT JOIN with alias.
196    pub fn left_join_as(
197        mut self,
198        table: impl AsRef<str>,
199        alias: impl AsRef<str>,
200        left_col: impl AsRef<str>,
201        right_col: impl AsRef<str>,
202    ) -> Self {
203        self.joins.push(Join {
204            kind: JoinKind::Left,
205            table: format!("{} {}", table.as_ref(), alias.as_ref()),
206            on: Some(vec![Condition {
207                left: Expr::Named(left_col.as_ref().to_string()),
208                op: Operator::Eq,
209                value: Value::Column(right_col.as_ref().to_string()),
210                is_array_unnest: false,
211            }]),
212            on_true: false,
213        });
214        self
215    }
216
217    /// INNER JOIN with alias.
218    pub fn inner_join_as(
219        mut self,
220        table: impl AsRef<str>,
221        alias: impl AsRef<str>,
222        left_col: impl AsRef<str>,
223        right_col: impl AsRef<str>,
224    ) -> Self {
225        self.joins.push(Join {
226            kind: JoinKind::Inner,
227            table: format!("{} {}", table.as_ref(), alias.as_ref()),
228            on: Some(vec![Condition {
229                left: Expr::Named(left_col.as_ref().to_string()),
230                op: Operator::Eq,
231                value: Value::Column(right_col.as_ref().to_string()),
232                is_array_unnest: false,
233            }]),
234            on_true: false,
235        });
236        self
237    }
238
239    /// JOIN with multiple ON conditions.
240    ///
241    /// The table string may include an alias (e.g. `"inventory inv"`).
242    ///
243    /// # Example
244    /// ```ignore
245    /// use qail_core::ast::builders::{eq, col};
246    /// use qail_core::ast::{Condition, Operator, Expr, Value};
247    ///
248    /// // LEFT JOIN odyssey_leg_inventory inv
249    /// //   ON inv.leg_id = ol.id AND inv.service_date = '2024-01-15'
250    /// .left_join_conds("odyssey_leg_inventory inv", vec![
251    ///     Condition { left: Expr::Named("inv.leg_id".into()), op: Operator::Eq,
252    ///                 value: Value::Column("ol.id".into()), is_array_unnest: false },
253    ///     Condition { left: Expr::Named("inv.service_date".into()), op: Operator::Eq,
254    ///                 value: Value::String("2024-01-15".into()), is_array_unnest: false },
255    /// ])
256    /// ```
257    pub fn join_conds(
258        mut self,
259        kind: JoinKind,
260        table: impl AsRef<str>,
261        conditions: Vec<Condition>,
262    ) -> Self {
263        self.joins.push(Join {
264            kind,
265            table: table.as_ref().to_string(),
266            on: Some(conditions),
267            on_true: false,
268        });
269        self
270    }
271
272    /// LEFT JOIN with multiple ON conditions.
273    pub fn left_join_conds(self, table: impl AsRef<str>, conditions: Vec<Condition>) -> Self {
274        self.join_conds(JoinKind::Left, table, conditions)
275    }
276
277    /// INNER JOIN with multiple ON conditions.
278    pub fn inner_join_conds(self, table: impl AsRef<str>, conditions: Vec<Condition>) -> Self {
279        self.join_conds(JoinKind::Inner, table, conditions)
280    }
281
282    /// Set an alias for the FROM table.
283    pub fn table_alias(mut self, alias: impl AsRef<str>) -> Self {
284        self.table = format!("{} {}", self.table, alias.as_ref());
285        self
286    }
287
288    /// ORDER BY an expression.
289    pub fn order_by_expr(mut self, expr: Expr, order: SortOrder) -> Self {
290        self.cages.push(Cage {
291            kind: CageKind::Sort(order),
292            conditions: vec![Condition {
293                left: expr,
294                op: Operator::Eq,
295                value: Value::Null,
296                is_array_unnest: false,
297            }],
298            logical_op: LogicalOp::And,
299        });
300        self
301    }
302
303    /// GROUP BY expressions.
304    pub fn group_by_expr<I>(mut self, exprs: I) -> Self
305    where
306        I: IntoIterator<Item = Expr>,
307    {
308        let conditions: Vec<Condition> = exprs
309            .into_iter()
310            .map(|e| Condition {
311                left: e,
312                op: Operator::Eq,
313                value: Value::Null,
314                is_array_unnest: false,
315            })
316            .collect();
317
318        self.cages.push(Cage {
319            kind: CageKind::Partition,
320            conditions,
321            logical_op: LogicalOp::And,
322        });
323        self
324    }
325}