Skip to main content

postrust_core/query/
builder.rs

1//! Query builder implementation.
2
3use crate::error::Result;
4use crate::plan::{
5    CallParams, CallPlan, CoercibleFilter, CoercibleLogicTree, CoercibleOrderTerm,
6    CoercibleSelectField, MutatePlan, ReadPlan, ReadPlanTree,
7};
8use postrust_sql::{
9    escape_ident, from_qi, DeleteBuilder, InsertBuilder, OrderExpr, SelectBuilder, SqlFragment,
10    SqlParam, UpdateBuilder,
11};
12
13/// Query builder for converting plans to SQL.
14pub struct QueryBuilder;
15
16impl QueryBuilder {
17    /// Build a SELECT query from a read plan tree.
18    pub fn build_read(tree: &ReadPlanTree) -> Result<SqlFragment> {
19        Self::build_read_plan(&tree.root)
20    }
21
22    /// Build a SELECT query from a read plan.
23    fn build_read_plan(plan: &ReadPlan) -> Result<SqlFragment> {
24        let mut builder = SelectBuilder::new();
25
26        // FROM clause
27        let qi = &plan.from;
28        if let Some(alias) = &plan.from_alias {
29            builder = builder.from_table_as(
30                &postrust_sql::identifier::QualifiedIdentifier::new(&qi.schema, &qi.name),
31                alias,
32            );
33        } else {
34            builder = builder.from_table(&postrust_sql::identifier::QualifiedIdentifier::new(
35                &qi.schema, &qi.name,
36            ));
37        }
38
39        // SELECT columns
40        for field in &plan.select {
41            let col_frag = Self::build_select_field(field)?;
42            builder = builder.column_raw(col_frag);
43        }
44
45        // WHERE clauses
46        for clause in &plan.where_clauses {
47            let expr = Self::build_logic_tree(clause)?;
48            builder = builder.where_raw(expr);
49        }
50
51        // ORDER BY
52        for term in &plan.order {
53            let order = Self::build_order_term(term);
54            builder = builder.order_by(order);
55        }
56
57        // LIMIT/OFFSET
58        if let Some(limit) = plan.range.limit {
59            builder = builder.limit(limit);
60        }
61        if plan.range.offset > 0 {
62            builder = builder.offset(plan.range.offset);
63        }
64
65        Ok(builder.build())
66    }
67
68    /// Build a SELECT field.
69    fn build_select_field(field: &CoercibleSelectField) -> Result<SqlFragment> {
70        let mut frag = SqlFragment::new();
71
72        // Aggregate function
73        if let Some(agg) = &field.aggregate {
74            frag.push(agg.to_sql());
75            frag.push("(");
76        }
77
78        // Column name with JSON path
79        frag.push(&escape_ident(&field.field.name));
80
81        // Close aggregate
82        if field.aggregate.is_some() {
83            frag.push(")");
84        }
85
86        // Cast
87        if let Some(cast) = &field.cast {
88            frag.push("::");
89            frag.push(cast);
90        }
91
92        // Alias
93        if let Some(alias) = &field.alias {
94            frag.push(" AS ");
95            frag.push(&escape_ident(alias));
96        }
97
98        Ok(frag)
99    }
100
101    /// Build a logic tree.
102    fn build_logic_tree(tree: &CoercibleLogicTree) -> Result<SqlFragment> {
103        match tree {
104            CoercibleLogicTree::Expr {
105                negated,
106                op,
107                children,
108            } => {
109                let sep = match op {
110                    crate::api_request::LogicOperator::And => " AND ",
111                    crate::api_request::LogicOperator::Or => " OR ",
112                };
113
114                let child_frags: Result<Vec<_>> =
115                    children.iter().map(Self::build_logic_tree).collect();
116
117                let mut combined = SqlFragment::join(sep, child_frags?).parens();
118
119                if *negated {
120                    let mut neg = SqlFragment::raw("NOT ");
121                    neg.append(combined);
122                    combined = neg;
123                }
124
125                Ok(combined)
126            }
127            CoercibleLogicTree::Stmt(filter) => Self::build_filter(filter),
128            CoercibleLogicTree::NullEmbed {
129                negated,
130                field_name,
131            } => {
132                let mut frag = SqlFragment::new();
133                frag.push(&escape_ident(field_name));
134                if *negated {
135                    frag.push(" IS NOT NULL");
136                } else {
137                    frag.push(" IS NULL");
138                }
139                Ok(frag)
140            }
141        }
142    }
143
144    /// Build a filter expression.
145    fn build_filter(filter: &CoercibleFilter) -> Result<SqlFragment> {
146        let mut frag = SqlFragment::new();
147
148        // Column name
149        frag.push(&escape_ident(&filter.field.name));
150
151        // Handle negation
152        if filter.op_expr.negated {
153            frag.push(" NOT");
154        }
155
156        // Operation
157        match &filter.op_expr.operation {
158            crate::api_request::Operation::Simple { op, value } => {
159                frag.push(" ");
160                frag.push(op.to_sql());
161                frag.push(" ");
162                frag.push_param(value.clone());
163            }
164            crate::api_request::Operation::Quant {
165                op,
166                quantifier,
167                value,
168            } => {
169                frag.push(" ");
170                frag.push(op.to_sql());
171                frag.push(" ");
172                if let Some(q) = quantifier {
173                    match q {
174                        crate::api_request::OpQuantifier::Any => frag.push("ANY("),
175                        crate::api_request::OpQuantifier::All => frag.push("ALL("),
176                    };
177                    frag.push_param(value.clone());
178                    frag.push(")");
179                } else {
180                    frag.push_param(value.clone());
181                }
182            }
183            crate::api_request::Operation::In(values) => {
184                frag.push(" IN (");
185                for (i, v) in values.iter().enumerate() {
186                    if i > 0 {
187                        frag.push(", ");
188                    }
189                    frag.push_param(v.clone());
190                }
191                frag.push(")");
192            }
193            crate::api_request::Operation::Is(is_val) => {
194                frag.push(" IS ");
195                frag.push(is_val.to_sql());
196            }
197            crate::api_request::Operation::IsDistinctFrom(value) => {
198                frag.push(" IS DISTINCT FROM ");
199                frag.push_param(value.clone());
200            }
201            crate::api_request::Operation::Fts {
202                op,
203                language,
204                value,
205            } => {
206                frag.push(" @@ ");
207                frag.push(op.to_function());
208                frag.push("(");
209                if let Some(lang) = language {
210                    frag.push_param(lang.clone());
211                    frag.push(", ");
212                }
213                frag.push_param(value.clone());
214                frag.push(")");
215            }
216        }
217
218        Ok(frag)
219    }
220
221    /// Build an ORDER BY term.
222    fn build_order_term(term: &CoercibleOrderTerm) -> OrderExpr {
223        let mut order = OrderExpr::new(&term.field.name);
224
225        if let Some(dir) = &term.direction {
226            order = match dir {
227                crate::api_request::OrderDirection::Asc => order.asc(),
228                crate::api_request::OrderDirection::Desc => order.desc(),
229            };
230        }
231
232        if let Some(nulls) = &term.nulls {
233            order = match nulls {
234                crate::api_request::OrderNulls::First => order.nulls_first(),
235                crate::api_request::OrderNulls::Last => order.nulls_last(),
236            };
237        }
238
239        order
240    }
241
242    /// Build a mutation query.
243    pub fn build_mutate(plan: &MutatePlan) -> Result<SqlFragment> {
244        match plan {
245            MutatePlan::Insert {
246                target,
247                columns,
248                body,
249                on_conflict,
250                returning,
251                ..
252            } => {
253                let qi = postrust_sql::identifier::QualifiedIdentifier::new(
254                    &target.schema,
255                    &target.name,
256                );
257
258                let mut builder = InsertBuilder::new().into_table(&qi);
259
260                // Column names
261                let col_names: Vec<String> = columns.iter().map(|c| c.name.clone()).collect();
262                builder = builder.columns(col_names);
263
264                // For bulk insert, we'd use json_populate_recordset
265                // For now, simplified single-row insert
266                if let Some(body_bytes) = body {
267                    // This would be expanded with proper JSON handling
268                    let body_str = String::from_utf8_lossy(body_bytes);
269                    let mut frag = SqlFragment::new();
270                    frag.push("SELECT * FROM json_populate_recordset(NULL::");
271                    frag.push(&from_qi(&qi));
272                    frag.push(", ");
273                    frag.push_param(body_str.to_string());
274                    frag.push("::json)");
275                    return Ok(frag);
276                }
277
278                // ON CONFLICT
279                if let Some((resolution, conflict_cols)) = on_conflict {
280                    match resolution {
281                        crate::api_request::PreferResolution::IgnoreDuplicates => {
282                            builder = builder.on_conflict_do_nothing();
283                        }
284                        crate::api_request::PreferResolution::MergeDuplicates => {
285                            let set_cols: Vec<(String, SqlFragment)> = columns
286                                .iter()
287                                .map(|c| {
288                                    let mut frag = SqlFragment::new();
289                                    frag.push("EXCLUDED.");
290                                    frag.push(&escape_ident(&c.name));
291                                    (c.name.clone(), frag)
292                                })
293                                .collect();
294                            builder =
295                                builder.on_conflict_do_update(conflict_cols.clone(), set_cols);
296                        }
297                    }
298                }
299
300                // RETURNING
301                for col in returning {
302                    builder = builder.returning(col);
303                }
304
305                Ok(builder.build())
306            }
307
308            MutatePlan::Update {
309                target,
310                columns,
311                body,
312                where_clauses,
313                returning,
314                ..
315            } => {
316                let qi = postrust_sql::identifier::QualifiedIdentifier::new(
317                    &target.schema,
318                    &target.name,
319                );
320
321                let builder = UpdateBuilder::new().table(&qi);
322
323                // SET columns from body
324                if let Some(body_bytes) = body {
325                    let body_str = String::from_utf8_lossy(body_bytes);
326                    // Simplified: would properly parse JSON and set columns
327                    let mut frag = SqlFragment::new();
328                    frag.push("UPDATE ");
329                    frag.push(&from_qi(&qi));
330                    frag.push(" SET ");
331
332                    for (i, col) in columns.iter().enumerate() {
333                        if i > 0 {
334                            frag.push(", ");
335                        }
336                        frag.push(&escape_ident(&col.name));
337                        frag.push(" = (");
338                        frag.push_param(body_str.to_string());
339                        frag.push("::json->>");
340                        frag.push_param(col.name.clone());
341                        frag.push(")::");
342                        frag.push(&col.ir_type);
343                    }
344
345                    // WHERE
346                    if !where_clauses.is_empty() {
347                        frag.push(" WHERE ");
348                        for (i, clause) in where_clauses.iter().enumerate() {
349                            if i > 0 {
350                                frag.push(" AND ");
351                            }
352                            frag.append(Self::build_logic_tree(clause)?);
353                        }
354                    }
355
356                    // RETURNING
357                    if !returning.is_empty() {
358                        frag.push(" RETURNING ");
359                        for (i, col) in returning.iter().enumerate() {
360                            if i > 0 {
361                                frag.push(", ");
362                            }
363                            frag.push(&escape_ident(col));
364                        }
365                    }
366
367                    return Ok(frag);
368                }
369
370                Ok(builder.build())
371            }
372
373            MutatePlan::Delete {
374                target,
375                where_clauses,
376                returning,
377            } => {
378                let qi = postrust_sql::identifier::QualifiedIdentifier::new(
379                    &target.schema,
380                    &target.name,
381                );
382
383                let mut builder = DeleteBuilder::new().from_table(&qi);
384
385                // WHERE
386                for clause in where_clauses {
387                    let expr = Self::build_logic_tree(clause)?;
388                    builder = builder.where_raw(expr);
389                }
390
391                // RETURNING
392                for col in returning {
393                    builder = builder.returning(col);
394                }
395
396                Ok(builder.build())
397            }
398        }
399    }
400
401    /// Build an RPC call query.
402    pub fn build_call(plan: &CallPlan) -> Result<SqlFragment> {
403        let qi = postrust_sql::identifier::QualifiedIdentifier::new(
404            &plan.function.schema,
405            &plan.function.name,
406        );
407
408        let mut frag = SqlFragment::new();
409        frag.push("SELECT * FROM ");
410        frag.push(&from_qi(&qi));
411        frag.push("(");
412
413        match &plan.params {
414            CallParams::Named(params) => {
415                for (i, (name, value)) in params.iter().enumerate() {
416                    if i > 0 {
417                        frag.push(", ");
418                    }
419                    frag.push(&escape_ident(name));
420                    frag.push(" => ");
421                    frag.push_param(SqlParam::Text(value.clone()));
422                }
423            }
424            CallParams::Positional(values) => {
425                for (i, value) in values.iter().enumerate() {
426                    if i > 0 {
427                        frag.push(", ");
428                    }
429                    frag.push_param(SqlParam::Text(value.clone()));
430                }
431            }
432            CallParams::SingleObject(body) => {
433                let body_str = String::from_utf8_lossy(body);
434                frag.push_param(SqlParam::Text(body_str.to_string()));
435            }
436            CallParams::None => {}
437        }
438
439        frag.push(")");
440
441        Ok(frag)
442    }
443}