postrust_core/query/
builder.rs

1//! Query builder implementation.
2
3use crate::error::Result;
4use crate::plan::{
5    CallPlan, CallParams, CoercibleFilter, CoercibleLogicTree, CoercibleOrderTerm,
6    CoercibleSelectField, MutatePlan, ReadPlan, ReadPlanTree,
7};
8use postrust_sql::{
9    escape_ident, from_qi, DeleteBuilder, InsertBuilder, OrderExpr, SelectBuilder,
10    SqlFragment, 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(
35                &postrust_sql::identifier::QualifiedIdentifier::new(&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 { negated, op, children } => {
105                let sep = match op {
106                    crate::api_request::LogicOperator::And => " AND ",
107                    crate::api_request::LogicOperator::Or => " OR ",
108                };
109
110                let child_frags: Result<Vec<_>> = children
111                    .iter()
112                    .map(|c| Self::build_logic_tree(c))
113                    .collect();
114
115                let mut combined = SqlFragment::join(sep, child_frags?).parens();
116
117                if *negated {
118                    let mut neg = SqlFragment::raw("NOT ");
119                    neg.append(combined);
120                    combined = neg;
121                }
122
123                Ok(combined)
124            }
125            CoercibleLogicTree::Stmt(filter) => Self::build_filter(filter),
126            CoercibleLogicTree::NullEmbed { negated, field_name } => {
127                let mut frag = SqlFragment::new();
128                frag.push(&escape_ident(field_name));
129                if *negated {
130                    frag.push(" IS NOT NULL");
131                } else {
132                    frag.push(" IS NULL");
133                }
134                Ok(frag)
135            }
136        }
137    }
138
139    /// Build a filter expression.
140    fn build_filter(filter: &CoercibleFilter) -> Result<SqlFragment> {
141        let mut frag = SqlFragment::new();
142
143        // Column name
144        frag.push(&escape_ident(&filter.field.name));
145
146        // Handle negation
147        if filter.op_expr.negated {
148            frag.push(" NOT");
149        }
150
151        // Operation
152        match &filter.op_expr.operation {
153            crate::api_request::Operation::Simple { op, value } => {
154                frag.push(" ");
155                frag.push(op.to_sql());
156                frag.push(" ");
157                frag.push_param(value.clone());
158            }
159            crate::api_request::Operation::Quant { op, quantifier, value } => {
160                frag.push(" ");
161                frag.push(op.to_sql());
162                frag.push(" ");
163                if let Some(q) = quantifier {
164                    match q {
165                        crate::api_request::OpQuantifier::Any => frag.push("ANY("),
166                        crate::api_request::OpQuantifier::All => frag.push("ALL("),
167                    };
168                    frag.push_param(value.clone());
169                    frag.push(")");
170                } else {
171                    frag.push_param(value.clone());
172                }
173            }
174            crate::api_request::Operation::In(values) => {
175                frag.push(" IN (");
176                for (i, v) in values.iter().enumerate() {
177                    if i > 0 {
178                        frag.push(", ");
179                    }
180                    frag.push_param(v.clone());
181                }
182                frag.push(")");
183            }
184            crate::api_request::Operation::Is(is_val) => {
185                frag.push(" IS ");
186                frag.push(is_val.to_sql());
187            }
188            crate::api_request::Operation::IsDistinctFrom(value) => {
189                frag.push(" IS DISTINCT FROM ");
190                frag.push_param(value.clone());
191            }
192            crate::api_request::Operation::Fts { op, language, value } => {
193                frag.push(" @@ ");
194                frag.push(op.to_function());
195                frag.push("(");
196                if let Some(lang) = language {
197                    frag.push_param(lang.clone());
198                    frag.push(", ");
199                }
200                frag.push_param(value.clone());
201                frag.push(")");
202            }
203        }
204
205        Ok(frag)
206    }
207
208    /// Build an ORDER BY term.
209    fn build_order_term(term: &CoercibleOrderTerm) -> OrderExpr {
210        let mut order = OrderExpr::new(&term.field.name);
211
212        if let Some(dir) = &term.direction {
213            order = match dir {
214                crate::api_request::OrderDirection::Asc => order.asc(),
215                crate::api_request::OrderDirection::Desc => order.desc(),
216            };
217        }
218
219        if let Some(nulls) = &term.nulls {
220            order = match nulls {
221                crate::api_request::OrderNulls::First => order.nulls_first(),
222                crate::api_request::OrderNulls::Last => order.nulls_last(),
223            };
224        }
225
226        order
227    }
228
229    /// Build a mutation query.
230    pub fn build_mutate(plan: &MutatePlan) -> Result<SqlFragment> {
231        match plan {
232            MutatePlan::Insert {
233                target,
234                columns,
235                body,
236                on_conflict,
237                returning,
238                ..
239            } => {
240                let qi = postrust_sql::identifier::QualifiedIdentifier::new(
241                    &target.schema,
242                    &target.name,
243                );
244
245                let mut builder = InsertBuilder::new().into_table(&qi);
246
247                // Column names
248                let col_names: Vec<String> = columns.iter().map(|c| c.name.clone()).collect();
249                builder = builder.columns(col_names);
250
251                // For bulk insert, we'd use json_populate_recordset
252                // For now, simplified single-row insert
253                if let Some(body_bytes) = body {
254                    // This would be expanded with proper JSON handling
255                    let body_str = String::from_utf8_lossy(body_bytes);
256                    let mut frag = SqlFragment::new();
257                    frag.push("SELECT * FROM json_populate_recordset(NULL::");
258                    frag.push(&from_qi(&qi));
259                    frag.push(", ");
260                    frag.push_param(body_str.to_string());
261                    frag.push("::json)");
262                    return Ok(frag);
263                }
264
265                // ON CONFLICT
266                if let Some((resolution, conflict_cols)) = on_conflict {
267                    match resolution {
268                        crate::api_request::PreferResolution::IgnoreDuplicates => {
269                            builder = builder.on_conflict_do_nothing();
270                        }
271                        crate::api_request::PreferResolution::MergeDuplicates => {
272                            let set_cols: Vec<(String, SqlFragment)> = columns
273                                .iter()
274                                .map(|c| {
275                                    let mut frag = SqlFragment::new();
276                                    frag.push("EXCLUDED.");
277                                    frag.push(&escape_ident(&c.name));
278                                    (c.name.clone(), frag)
279                                })
280                                .collect();
281                            builder = builder.on_conflict_do_update(conflict_cols.clone(), set_cols);
282                        }
283                    }
284                }
285
286                // RETURNING
287                for col in returning {
288                    builder = builder.returning(col);
289                }
290
291                Ok(builder.build())
292            }
293
294            MutatePlan::Update {
295                target,
296                columns,
297                body,
298                where_clauses,
299                returning,
300                ..
301            } => {
302                let qi = postrust_sql::identifier::QualifiedIdentifier::new(
303                    &target.schema,
304                    &target.name,
305                );
306
307                let builder = UpdateBuilder::new().table(&qi);
308
309                // SET columns from body
310                if let Some(body_bytes) = body {
311                    let body_str = String::from_utf8_lossy(body_bytes);
312                    // Simplified: would properly parse JSON and set columns
313                    let mut frag = SqlFragment::new();
314                    frag.push("UPDATE ");
315                    frag.push(&from_qi(&qi));
316                    frag.push(" SET ");
317
318                    for (i, col) in columns.iter().enumerate() {
319                        if i > 0 {
320                            frag.push(", ");
321                        }
322                        frag.push(&escape_ident(&col.name));
323                        frag.push(" = (");
324                        frag.push_param(body_str.to_string());
325                        frag.push("::json->>");
326                        frag.push_param(col.name.clone());
327                        frag.push(")::");
328                        frag.push(&col.ir_type);
329                    }
330
331                    // WHERE
332                    if !where_clauses.is_empty() {
333                        frag.push(" WHERE ");
334                        for (i, clause) in where_clauses.iter().enumerate() {
335                            if i > 0 {
336                                frag.push(" AND ");
337                            }
338                            frag.append(Self::build_logic_tree(clause)?);
339                        }
340                    }
341
342                    // RETURNING
343                    if !returning.is_empty() {
344                        frag.push(" RETURNING ");
345                        for (i, col) in returning.iter().enumerate() {
346                            if i > 0 {
347                                frag.push(", ");
348                            }
349                            frag.push(&escape_ident(col));
350                        }
351                    }
352
353                    return Ok(frag);
354                }
355
356                Ok(builder.build())
357            }
358
359            MutatePlan::Delete {
360                target,
361                where_clauses,
362                returning,
363            } => {
364                let qi = postrust_sql::identifier::QualifiedIdentifier::new(
365                    &target.schema,
366                    &target.name,
367                );
368
369                let mut builder = DeleteBuilder::new().from_table(&qi);
370
371                // WHERE
372                for clause in where_clauses {
373                    let expr = Self::build_logic_tree(clause)?;
374                    builder = builder.where_raw(expr);
375                }
376
377                // RETURNING
378                for col in returning {
379                    builder = builder.returning(col);
380                }
381
382                Ok(builder.build())
383            }
384        }
385    }
386
387    /// Build an RPC call query.
388    pub fn build_call(plan: &CallPlan) -> Result<SqlFragment> {
389        let qi = postrust_sql::identifier::QualifiedIdentifier::new(
390            &plan.function.schema,
391            &plan.function.name,
392        );
393
394        let mut frag = SqlFragment::new();
395        frag.push("SELECT * FROM ");
396        frag.push(&from_qi(&qi));
397        frag.push("(");
398
399        match &plan.params {
400            CallParams::Named(params) => {
401                for (i, (name, value)) in params.iter().enumerate() {
402                    if i > 0 {
403                        frag.push(", ");
404                    }
405                    frag.push(&escape_ident(name));
406                    frag.push(" => ");
407                    frag.push_param(SqlParam::Text(value.clone()));
408                }
409            }
410            CallParams::Positional(values) => {
411                for (i, value) in values.iter().enumerate() {
412                    if i > 0 {
413                        frag.push(", ");
414                    }
415                    frag.push_param(SqlParam::Text(value.clone()));
416                }
417            }
418            CallParams::SingleObject(body) => {
419                let body_str = String::from_utf8_lossy(body);
420                frag.push_param(SqlParam::Text(body_str.to_string()));
421            }
422            CallParams::None => {}
423        }
424
425        frag.push(")");
426
427        Ok(frag)
428    }
429}