1use 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
13pub struct QueryBuilder;
15
16impl QueryBuilder {
17 pub fn build_read(tree: &ReadPlanTree) -> Result<SqlFragment> {
19 Self::build_read_plan(&tree.root)
20 }
21
22 fn build_read_plan(plan: &ReadPlan) -> Result<SqlFragment> {
24 let mut builder = SelectBuilder::new();
25
26 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 for field in &plan.select {
41 let col_frag = Self::build_select_field(field)?;
42 builder = builder.column_raw(col_frag);
43 }
44
45 for clause in &plan.where_clauses {
47 let expr = Self::build_logic_tree(clause)?;
48 builder = builder.where_raw(expr);
49 }
50
51 for term in &plan.order {
53 let order = Self::build_order_term(term);
54 builder = builder.order_by(order);
55 }
56
57 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 fn build_select_field(field: &CoercibleSelectField) -> Result<SqlFragment> {
70 let mut frag = SqlFragment::new();
71
72 if let Some(agg) = &field.aggregate {
74 frag.push(agg.to_sql());
75 frag.push("(");
76 }
77
78 frag.push(&escape_ident(&field.field.name));
80
81 if field.aggregate.is_some() {
83 frag.push(")");
84 }
85
86 if let Some(cast) = &field.cast {
88 frag.push("::");
89 frag.push(cast);
90 }
91
92 if let Some(alias) = &field.alias {
94 frag.push(" AS ");
95 frag.push(&escape_ident(alias));
96 }
97
98 Ok(frag)
99 }
100
101 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 fn build_filter(filter: &CoercibleFilter) -> Result<SqlFragment> {
146 let mut frag = SqlFragment::new();
147
148 if filter.op_expr.negated {
153 frag.push("NOT (");
154 }
155
156 frag.push(&escape_ident(&filter.field.name));
158
159 let cast = if filter.field.json_path.is_empty() {
164 castable_type(&filter.field.ir_type)
165 } else {
166 None
167 };
168 let push_value = |frag: &mut SqlFragment, value: String| match cast {
169 Some(pg_type) => {
170 frag.push_typed_param(value, pg_type);
171 }
172 None => {
173 frag.push_param(value);
174 }
175 };
176
177 match &filter.op_expr.operation {
179 crate::api_request::Operation::Simple { op, value } => {
180 frag.push(" ");
181 frag.push(op.to_sql());
182 frag.push(" ");
183 push_value(&mut frag, value.clone());
184 }
185 crate::api_request::Operation::Quant {
186 op,
187 quantifier,
188 value,
189 } => {
190 frag.push(" ");
191 frag.push(op.to_sql());
192 frag.push(" ");
193 if let Some(q) = quantifier {
194 match q {
195 crate::api_request::OpQuantifier::Any => frag.push("ANY("),
196 crate::api_request::OpQuantifier::All => frag.push("ALL("),
197 };
198 match cast.filter(|t| !t.starts_with('_')) {
202 Some(pg_type) => {
203 frag.push_typed_param(value.clone(), &format!("{}[]", pg_type));
204 }
205 None => {
206 frag.push_param(value.clone());
207 }
208 }
209 frag.push(")");
210 } else {
211 push_value(&mut frag, value.clone());
212 }
213 }
214 crate::api_request::Operation::In(values) => {
215 frag.push(" IN (");
216 for (i, v) in values.iter().enumerate() {
217 if i > 0 {
218 frag.push(", ");
219 }
220 push_value(&mut frag, v.clone());
221 }
222 frag.push(")");
223 }
224 crate::api_request::Operation::Is(is_val) => {
225 frag.push(" IS ");
226 frag.push(is_val.to_sql());
227 }
228 crate::api_request::Operation::IsDistinctFrom(value) => {
229 frag.push(" IS DISTINCT FROM ");
230 push_value(&mut frag, value.clone());
231 }
232 crate::api_request::Operation::Fts {
233 op,
234 language,
235 value,
236 } => {
237 frag.push(" @@ ");
238 frag.push(op.to_function());
239 frag.push("(");
240 if let Some(lang) = language {
241 frag.push_param(lang.clone());
242 frag.push(", ");
243 }
244 frag.push_param(value.clone());
245 frag.push(")");
246 }
247 }
248
249 if filter.op_expr.negated {
250 frag.push(")");
251 }
252
253 Ok(frag)
254 }
255
256 fn build_order_term(term: &CoercibleOrderTerm) -> OrderExpr {
258 let mut order = OrderExpr::new(&term.field.name);
259
260 if let Some(dir) = &term.direction {
261 order = match dir {
262 crate::api_request::OrderDirection::Asc => order.asc(),
263 crate::api_request::OrderDirection::Desc => order.desc(),
264 };
265 }
266
267 if let Some(nulls) = &term.nulls {
268 order = match nulls {
269 crate::api_request::OrderNulls::First => order.nulls_first(),
270 crate::api_request::OrderNulls::Last => order.nulls_last(),
271 };
272 }
273
274 order
275 }
276
277 pub fn build_mutate(plan: &MutatePlan) -> Result<SqlFragment> {
279 match plan {
280 MutatePlan::Insert {
281 target,
282 columns,
283 body,
284 on_conflict,
285 returning,
286 ..
287 } => {
288 let qi = postrust_sql::identifier::QualifiedIdentifier::new(
289 &target.schema,
290 &target.name,
291 );
292
293 let mut builder = InsertBuilder::new().into_table(&qi);
294
295 let col_names: Vec<String> = columns.iter().map(|c| c.name.clone()).collect();
297 builder = builder.columns(col_names);
298
299 if let Some(body_bytes) = body {
302 let body_str = String::from_utf8_lossy(body_bytes);
304 let mut frag = SqlFragment::new();
305 frag.push("SELECT * FROM json_populate_recordset(NULL::");
306 frag.push(&from_qi(&qi));
307 frag.push(", ");
308 frag.push_param(body_str.to_string());
309 frag.push("::json)");
310 return Ok(frag);
311 }
312
313 if let Some((resolution, conflict_cols)) = on_conflict {
315 match resolution {
316 crate::api_request::PreferResolution::IgnoreDuplicates => {
317 builder = builder.on_conflict_do_nothing();
318 }
319 crate::api_request::PreferResolution::MergeDuplicates => {
320 let set_cols: Vec<(String, SqlFragment)> = columns
321 .iter()
322 .map(|c| {
323 let mut frag = SqlFragment::new();
324 frag.push("EXCLUDED.");
325 frag.push(&escape_ident(&c.name));
326 (c.name.clone(), frag)
327 })
328 .collect();
329 builder =
330 builder.on_conflict_do_update(conflict_cols.clone(), set_cols);
331 }
332 }
333 }
334
335 for col in returning {
337 builder = builder.returning(col);
338 }
339
340 Ok(builder.build())
341 }
342
343 MutatePlan::Update {
344 target,
345 columns,
346 body,
347 where_clauses,
348 returning,
349 ..
350 } => {
351 let qi = postrust_sql::identifier::QualifiedIdentifier::new(
352 &target.schema,
353 &target.name,
354 );
355
356 let builder = UpdateBuilder::new().table(&qi);
357
358 if let Some(body_bytes) = body {
360 let body_str = String::from_utf8_lossy(body_bytes);
361 let mut frag = SqlFragment::new();
363 frag.push("UPDATE ");
364 frag.push(&from_qi(&qi));
365 frag.push(" SET ");
366
367 for (i, col) in columns.iter().enumerate() {
368 if i > 0 {
369 frag.push(", ");
370 }
371 frag.push(&escape_ident(&col.name));
372 frag.push(" = (");
373 frag.push_param(body_str.to_string());
374 frag.push("::json->>");
375 frag.push_param(col.name.clone());
376 frag.push(")::");
377 frag.push(&col.ir_type);
378 }
379
380 if !where_clauses.is_empty() {
382 frag.push(" WHERE ");
383 for (i, clause) in where_clauses.iter().enumerate() {
384 if i > 0 {
385 frag.push(" AND ");
386 }
387 frag.append(Self::build_logic_tree(clause)?);
388 }
389 }
390
391 if !returning.is_empty() {
393 frag.push(" RETURNING ");
394 for (i, col) in returning.iter().enumerate() {
395 if i > 0 {
396 frag.push(", ");
397 }
398 frag.push(&escape_ident(col));
399 }
400 }
401
402 return Ok(frag);
403 }
404
405 Ok(builder.build())
406 }
407
408 MutatePlan::Delete {
409 target,
410 where_clauses,
411 returning,
412 } => {
413 let qi = postrust_sql::identifier::QualifiedIdentifier::new(
414 &target.schema,
415 &target.name,
416 );
417
418 let mut builder = DeleteBuilder::new().from_table(&qi);
419
420 for clause in where_clauses {
422 let expr = Self::build_logic_tree(clause)?;
423 builder = builder.where_raw(expr);
424 }
425
426 for col in returning {
428 builder = builder.returning(col);
429 }
430
431 Ok(builder.build())
432 }
433 }
434 }
435
436 pub fn build_call(plan: &CallPlan) -> Result<SqlFragment> {
438 let qi = postrust_sql::identifier::QualifiedIdentifier::new(
439 &plan.function.schema,
440 &plan.function.name,
441 );
442
443 let mut frag = SqlFragment::new();
444 frag.push("SELECT * FROM ");
445 frag.push(&from_qi(&qi));
446 frag.push("(");
447
448 match &plan.params {
449 CallParams::Named(params) => {
450 for (i, (name, value)) in params.iter().enumerate() {
451 if i > 0 {
452 frag.push(", ");
453 }
454 frag.push(&escape_ident(name));
455 frag.push(" => ");
456 frag.push_param(SqlParam::Text(value.clone()));
457 }
458 }
459 CallParams::Positional(values) => {
460 for (i, value) in values.iter().enumerate() {
461 if i > 0 {
462 frag.push(", ");
463 }
464 frag.push_param(SqlParam::Text(value.clone()));
465 }
466 }
467 CallParams::SingleObject(body) => {
468 let body_str = String::from_utf8_lossy(body);
469 frag.push_param(SqlParam::Text(body_str.to_string()));
470 }
471 CallParams::None => {}
472 }
473
474 frag.push(")");
475
476 Ok(frag)
477 }
478}
479
480fn castable_type(pg_type: &str) -> Option<&str> {
488 if pg_type.is_empty() {
489 return None;
490 }
491
492 let is_bare_name = pg_type
493 .chars()
494 .all(|c| c.is_ascii_alphanumeric() || c == '_');
495
496 if is_bare_name {
497 Some(pg_type)
498 } else {
499 None
500 }
501}
502
503#[cfg(test)]
504mod tests {
505 use super::*;
506
507 #[test]
508 fn castable_type_accepts_bare_type_names() {
509 assert_eq!(castable_type("int4"), Some("int4"));
510 assert_eq!(castable_type("timestamptz"), Some("timestamptz"));
511 assert_eq!(castable_type("_text"), Some("_text"));
512 }
513
514 #[test]
515 fn castable_type_rejects_unsafe_names() {
516 assert_eq!(castable_type(""), None);
517 assert_eq!(castable_type("character varying"), None);
518 assert_eq!(castable_type("USER-DEFINED"), None);
519 assert_eq!(castable_type("int4; DROP TABLE users"), None);
520 }
521}