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::{escape_ident, from_qi, OrderExpr, SelectBuilder, SqlFragment, SqlParam};
9
10/// Query builder for converting plans to SQL.
11pub struct QueryBuilder;
12
13impl QueryBuilder {
14 /// Build a SELECT query from a read plan tree.
15 pub fn build_read(tree: &ReadPlanTree) -> Result<SqlFragment> {
16 Self::build_read_plan(&tree.root)
17 }
18
19 /// Build a SELECT query from a read plan.
20 fn build_read_plan(plan: &ReadPlan) -> Result<SqlFragment> {
21 let mut builder = SelectBuilder::new();
22
23 // FROM clause
24 let qi = &plan.from;
25 if let Some(alias) = &plan.from_alias {
26 builder = builder.from_table_as(
27 &postrust_sql::identifier::QualifiedIdentifier::new(&qi.schema, &qi.name),
28 alias,
29 );
30 } else {
31 builder = builder.from_table(&postrust_sql::identifier::QualifiedIdentifier::new(
32 &qi.schema, &qi.name,
33 ));
34 }
35
36 // SELECT columns
37 for field in &plan.select {
38 let col_frag = Self::build_select_field(field)?;
39 builder = builder.column_raw(col_frag);
40 }
41
42 // WHERE clauses
43 for clause in &plan.where_clauses {
44 let expr = Self::build_logic_tree(clause)?;
45 builder = builder.where_raw(expr);
46 }
47
48 // GROUP BY. Selecting an aggregate alongside plain columns means one
49 // row per distinct combination of those columns, so every one of them
50 // has to be grouped -- PostgreSQL rejects the query otherwise.
51 if plan.select.iter().any(|f| f.aggregate.is_some()) {
52 for field in plan.select.iter().filter(|f| f.aggregate.is_none()) {
53 builder = builder.group_by(&field.field.name);
54 }
55 }
56
57 // ORDER BY
58 //
59 // A term naming an embedded resource is left for the caller: it orders
60 // by a column of another table, which this query has no way to reach.
61 for term in plan.order.iter().filter(|t| t.relation.is_none()) {
62 let order = Self::build_order_term(term);
63 builder = builder.order_by(order);
64 }
65
66 // LIMIT/OFFSET
67 if let Some(limit) = plan.range.limit {
68 builder = builder.limit(limit);
69 }
70 if plan.range.offset > 0 {
71 builder = builder.offset(plan.range.offset);
72 }
73
74 Ok(builder.build())
75 }
76
77 /// Build a SELECT field.
78 fn build_select_field(field: &CoercibleSelectField) -> Result<SqlFragment> {
79 let mut frag = SqlFragment::new();
80
81 // `*` reaches here only where the columns are not known ahead of time
82 // -- a function's result, where the plan has no table to expand it
83 // against. Quoting it would ask for a column literally named `*`.
84 if field.field.name == "*" && field.aggregate.is_none() && field.cast.is_none() {
85 frag.push("*");
86 return Ok(frag);
87 }
88
89 // Aggregate function
90 if let Some(agg) = &field.aggregate {
91 frag.push(agg.to_sql());
92 frag.push("(");
93 }
94
95 // `count()` counts rows rather than a column's non-null values.
96 if field.aggregate.is_some() && field.field.name.is_empty() {
97 frag.push("*)");
98 if let Some(cast) = &field.aggregate_cast {
99 frag.push("::");
100 frag.push(cast);
101 }
102 frag.push(" AS ");
103 frag.push(&escape_ident(field.alias.as_deref().unwrap_or("count")));
104 return Ok(frag);
105 }
106
107 // A type this process cannot decode is rendered by PostgreSQL instead.
108 //
109 // The row converter maps a fixed set of built-in types and falls back
110 // to reading a column as text, which fails outright for a type like a
111 // PostGIS geometry -- the column came back as null. `to_jsonb` gives
112 // the database's own rendering, which for a geometry is the GeoJSON
113 // PostgREST returns. PostgREST gets it for free by building its whole
114 // response in SQL; this is the same rendering, asked for by name.
115 //
116 // A column with a declared representation is already rendered by that
117 // -- the schema said how the value is written on the wire, and the
118 // generic fallback would render it some other way instead. A `bytea`
119 // with a base64 representation came out as `\x` and hex, which is
120 // PostgreSQL's rendering of bytes and not the schema's.
121 let render_as_json = field.aggregate.is_none()
122 && field.cast.is_none()
123 && field.field.transform.is_none()
124 && field.field.json_path.is_empty()
125 && !decodable_type(&field.field.ir_type);
126 if render_as_json {
127 let mut inner = SqlFragment::new();
128 push_field_ref(&mut inner, &field.field);
129 frag.push("to_jsonb(");
130 frag.push(inner.sql());
131 frag.push(") AS ");
132 frag.push(&escape_ident(
133 field.alias.as_deref().unwrap_or(&field.field.name),
134 ));
135 return Ok(frag);
136 }
137
138 // Column name with JSON path.
139 //
140 // `::` binds tighter than `->>`, so a cast over a JSON path has to be
141 // parenthesised: `settings ->> 'foo'::json` would cast the *key*
142 // rather than the extracted value.
143 let needs_parens = field.cast.is_some() && !field.field.json_path.is_empty();
144 if needs_parens {
145 frag.push("(");
146 }
147 // A transformer renders the column instead of this process: PostgREST
148 // uses it for a schema's declared data representations, and it is also
149 // how a value whose spelling depends on the session -- a `timestamptz`
150 // under `Prefer: timezone` -- gets rendered where the session is.
151 match &field.field.transform {
152 Some(function) => {
153 frag.push(function);
154 frag.push("(");
155 push_field_ref(&mut frag, &field.field);
156 frag.push(")");
157 }
158 None => push_field_ref(&mut frag, &field.field),
159 }
160 if needs_parens {
161 frag.push(")");
162 }
163
164 // Cast on the column, inside the aggregate.
165 if let Some(cast) = &field.cast {
166 frag.push("::");
167 frag.push(cast);
168 }
169
170 // Close aggregate, then any cast applied to its result.
171 if let Some(agg) = &field.aggregate {
172 frag.push(")");
173 if let Some(cast) = &field.aggregate_cast {
174 frag.push("::");
175 frag.push(cast);
176 }
177 // An aggregate is an expression, so it needs a name. PostgREST
178 // uses the function's own, lowercased.
179 frag.push(" AS ");
180 frag.push(&escape_ident(
181 field
182 .alias
183 .as_deref()
184 .unwrap_or(&agg.to_sql().to_lowercase()),
185 ));
186 return Ok(frag);
187 }
188
189 // Alias
190 //
191 // A JSON path always needs one: `data -> 'a' ->> 'b'` is an expression,
192 // and PostgreSQL would label the column `?column?`. PostgREST names it
193 // after the last key in the path, falling back to the column itself
194 // when the path ends in an array index.
195 let implicit_alias = if field.alias.is_some() {
196 None
197 } else if field.field.transform.is_some() {
198 Some(field.field.name.clone())
199 } else if !field.field.json_path.is_empty() {
200 Some(json_path_alias(&field.field.name, &field.field.json_path))
201 } else if field.field.computed.is_some() {
202 // A call is an expression, and PostgreSQL would label it after the
203 // function -- which is the right name, but only by coincidence of
204 // the two agreeing. Naming it outright keeps a schema-qualified
205 // call from arriving under some other label.
206 Some(field.field.name.clone())
207 } else {
208 None
209 };
210
211 if let Some(alias) = field.alias.as_deref().or(implicit_alias.as_deref()) {
212 frag.push(" AS ");
213 frag.push(&escape_ident(alias));
214 }
215
216 Ok(frag)
217 }
218
219 /// Build a logic tree.
220 fn build_logic_tree(tree: &CoercibleLogicTree) -> Result<SqlFragment> {
221 Self::build_logic_tree_in(None, tree)
222 }
223
224 /// The same, with every column qualified by a relation.
225 ///
226 /// An `UPDATE ... FROM` has the body's columns in scope alongside the
227 /// table's, so a bare column name in the `WHERE` is ambiguous exactly
228 /// when the request filters on a column it is also writing.
229 fn build_logic_tree_in(
230 qualifier: Option<&str>,
231 tree: &CoercibleLogicTree,
232 ) -> Result<SqlFragment> {
233 match tree {
234 CoercibleLogicTree::Expr {
235 negated,
236 op,
237 children,
238 } => {
239 let sep = match op {
240 crate::api_request::LogicOperator::And => " AND ",
241 crate::api_request::LogicOperator::Or => " OR ",
242 };
243
244 let child_frags: Result<Vec<_>> = children
245 .iter()
246 .map(|child| Self::build_logic_tree_in(qualifier, child))
247 .collect();
248
249 let mut combined = SqlFragment::join(sep, child_frags?).parens();
250
251 if *negated {
252 let mut neg = SqlFragment::raw("NOT ");
253 neg.append(combined);
254 combined = neg;
255 }
256
257 Ok(combined)
258 }
259 CoercibleLogicTree::Stmt(filter) => Self::build_filter_in(qualifier, filter),
260 CoercibleLogicTree::NullEmbed {
261 negated,
262 field_name,
263 } => {
264 let mut frag = SqlFragment::new();
265 frag.push(&escape_ident(field_name));
266 if *negated {
267 frag.push(" IS NOT NULL");
268 } else {
269 frag.push(" IS NULL");
270 }
271 Ok(frag)
272 }
273 }
274 }
275
276 /// A column reference with its JSON path, as it appears in SQL.
277 ///
278 /// Exposed for ordering by an embedded resource's column, which is
279 /// assembled outside the read plan.
280 pub fn column_sql(field: &crate::plan::CoercibleField) -> String {
281 Self::qualified_column_sql(None, field)
282 }
283
284 /// The same, with the column qualified by a relation alias.
285 ///
286 /// The alias belongs to the column, not to the expression around it:
287 /// `to_jsonb(e2."settings")->'a'`, never `e2.to_jsonb(...)`, which reads
288 /// as a function in a schema called `e2`.
289 pub fn qualified_column_sql(
290 qualifier: Option<&str>,
291 field: &crate::plan::CoercibleField,
292 ) -> String {
293 let mut frag = SqlFragment::new();
294 push_qualified_field_ref(&mut frag, qualifier, field);
295 frag.sql().to_string()
296 }
297
298 /// Build the SQL for a single filter against a column of `pg_type`.
299 ///
300 /// Exposed for the embedding path, which applies filters inside a child
301 /// subquery it assembles itself rather than through a full plan. Column
302 /// names are left unqualified: inside the child's subselect the innermost
303 /// `FROM` wins, so a bare name binds to the child. The caller is
304 /// responsible for having checked the column exists there -- otherwise the
305 /// name would resolve outward to the correlated parent instead.
306 pub fn filter_sql(filter: &crate::api_request::Filter, pg_type: &str) -> Result<SqlFragment> {
307 Self::build_filter(&CoercibleFilter::from_filter(filter, pg_type))
308 }
309
310 /// Build the SQL for a logic tree against a table whose column types
311 /// `type_resolver` supplies.
312 ///
313 /// Exposed for the same reason as `filter_sql`: an embedded resource's
314 /// `and=`/`or=` is applied inside a subquery the embedding path assembles
315 /// itself, without going through a full plan.
316 pub fn logic_sql<F>(
317 tree: &crate::api_request::LogicTree,
318 type_resolver: F,
319 ) -> Result<SqlFragment>
320 where
321 F: Fn(&str) -> String + Copy,
322 {
323 Self::build_logic_tree(&CoercibleLogicTree::from_logic_tree(tree, type_resolver))
324 }
325
326 fn build_filter(filter: &CoercibleFilter) -> Result<SqlFragment> {
327 Self::build_filter_in(None, filter)
328 }
329
330 fn build_filter_in(qualifier: Option<&str>, filter: &CoercibleFilter) -> Result<SqlFragment> {
331 let mut frag = SqlFragment::new();
332
333 // Negation wraps the whole comparison. Placing `NOT` between the column
334 // and the operator only parses for a few operators -- `col NOT LIKE $1`
335 // is valid but `col NOT = $1` is a syntax error -- so the comparison is
336 // parenthesised instead, which is correct for every operator.
337 if filter.op_expr.negated {
338 frag.push("NOT (");
339 }
340
341 // Column name.
342 //
343 // A text-search operator wants a `tsvector` on the left. A column that
344 // is not already one is wrapped, so `text_search=fts.x` searches the
345 // text instead of failing to find an operator. A domain over tsvector
346 // is already one, hence the prefix test rather than an equality.
347 //
348 // The language belongs on both sides: `to_tsvector('french', col) @@
349 // to_tsquery('french', $1)`. Left off the left-hand side the column is
350 // lexed with the default configuration -- English -- and a French
351 // query then matches nothing, quietly and with a 200.
352 let to_tsvector = match &filter.op_expr.operation {
353 crate::api_request::Operation::Fts { language, .. }
354 if !filter.field.ir_type.starts_with("tsvector") =>
355 {
356 Some(language.clone())
357 }
358 _ => None,
359 };
360 if let Some(language) = &to_tsvector {
361 frag.push("to_tsvector(");
362 if let Some(language) = language {
363 frag.push_typed_param(language.clone(), "regconfig");
364 frag.push(", ");
365 }
366 }
367 push_qualified_field_ref(&mut frag, qualifier, &filter.field);
368 if to_tsvector.is_some() {
369 frag.push(")");
370 }
371
372 // Filter values are always bound as text, so a comparison against a
373 // non-text column needs an explicit cast on the placeholder -- without
374 // it PostgreSQL rejects the query with `operator does not exist:
375 // integer = text`. A JSON path already yields text, so it is left as-is.
376 let cast = if filter.field.json_path.is_empty() {
377 castable_type(&filter.field.ir_type)
378 } else {
379 // A JSON path leaves either `jsonb` (`->`) or `text` (`->>`) on the
380 // left. Text needs no cast, but `jsonb = $1` with the placeholder
381 // bound as text finds no operator, so the value is cast to match.
382 json_path_result_cast(&filter.field.json_path)
383 };
384
385 // `match` is `~`, and on an `ltree` the right-hand side of `~` is an
386 // `lquery` rather than another `ltree`: `path=match.*.Science` is a
387 // pattern, not a path. Casting it to the column's own type -- which is
388 // what every other operator wants -- finds no operator at all.
389 let cast = match (&filter.op_expr.operation, filter.field.ir_type.as_str()) {
390 (
391 crate::api_request::Operation::Quant {
392 op:
393 crate::api_request::QuantOperator::Match
394 | crate::api_request::QuantOperator::IMatch,
395 quantifier: None,
396 ..
397 },
398 "ltree",
399 ) => Some("lquery"),
400 _ => cast,
401 };
402 // A schema that declared how one of its domains is written also
403 // declared how it is read: the cast parses the value the client sent,
404 // in its own spelling, rather than PostgreSQL's input function for the
405 // type underneath.
406 let parser = filter.field.transform.as_deref();
407 let push_value = |frag: &mut SqlFragment, value: String| {
408 if let Some(parser) = parser {
409 frag.push(parser);
410 frag.push("(");
411 frag.push_param(value);
412 frag.push(")");
413 return;
414 }
415 match cast {
416 Some(pg_type) => {
417 frag.push_typed_param(value, pg_type);
418 }
419 None => {
420 frag.push_param(value);
421 }
422 }
423 };
424
425 // Operation
426 match &filter.op_expr.operation {
427 crate::api_request::Operation::Simple { op, value } => {
428 frag.push(" ");
429 frag.push(op.to_sql());
430 frag.push(" ");
431 push_value(&mut frag, value.clone());
432 }
433 crate::api_request::Operation::Quant {
434 op,
435 quantifier,
436 value,
437 } => {
438 // PostgREST spells the LIKE wildcard `*` rather than `%`, and
439 // maps it unconditionally -- `*` is never a literal asterisk in
440 // a like/ilike operand. Other operators take the value as-is,
441 // so `match`/`imatch` regexes keep their own `*`.
442 let value = &match op {
443 crate::api_request::QuantOperator::Like
444 | crate::api_request::QuantOperator::ILike => value.replace('*', "%"),
445 _ => value.clone(),
446 };
447 frag.push(" ");
448 frag.push(op.to_sql());
449 frag.push(" ");
450 if let Some(q) = quantifier {
451 match q {
452 crate::api_request::OpQuantifier::Any => frag.push("ANY("),
453 crate::api_request::OpQuantifier::All => frag.push("ALL("),
454 };
455 // A quantified comparison takes an array of the column's
456 // type. Array-typed columns are already handled by the
457 // element cast, so they are left alone.
458 match cast.filter(|t| !t.starts_with('_')) {
459 Some(pg_type) => {
460 frag.push_typed_param(value.clone(), &format!("{}[]", pg_type));
461 }
462 None => {
463 frag.push_param(value.clone());
464 }
465 }
466 frag.push(")");
467 } else {
468 push_value(&mut frag, value.clone());
469 }
470 }
471 crate::api_request::Operation::In(values) if values.is_empty() => {
472 // `IN ()` is a syntax error. The empty array says the same
473 // thing and is a single expression, so it needs no parentheses
474 // of its own wherever the filter ends up. It is false for
475 // every value, nulls included, which is what makes `not.in.()`
476 // match everything.
477 frag.push(" = ANY('{}')");
478 }
479 crate::api_request::Operation::In(values) => {
480 frag.push(" IN (");
481 for (i, v) in values.iter().enumerate() {
482 if i > 0 {
483 frag.push(", ");
484 }
485 push_value(&mut frag, v.clone());
486 }
487 frag.push(")");
488 }
489 crate::api_request::Operation::Is(is_val) => {
490 frag.push(" IS ");
491 frag.push(is_val.to_sql());
492 }
493 crate::api_request::Operation::IsDistinctFrom(value) => {
494 frag.push(" IS DISTINCT FROM ");
495 push_value(&mut frag, value.clone());
496 }
497 crate::api_request::Operation::Fts {
498 op,
499 language,
500 value,
501 } => {
502 frag.push(" @@ ");
503 frag.push(op.to_function());
504 frag.push("(");
505 if let Some(lang) = language {
506 // The text-search functions take their configuration as a
507 // `regconfig`, and there is no `to_tsquery(text, text)` to
508 // fall back on -- bound as plain text the call resolves to
509 // no function at all.
510 frag.push_typed_param(lang.clone(), "regconfig");
511 frag.push(", ");
512 }
513 // The query is text whatever the column holds, so it never
514 // takes the column's own cast.
515 frag.push_param(value.clone());
516 frag.push(")");
517 }
518 }
519
520 if filter.op_expr.negated {
521 frag.push(")");
522 }
523
524 Ok(frag)
525 }
526
527 /// Build an ORDER BY term.
528 fn build_order_term(term: &CoercibleOrderTerm) -> OrderExpr {
529 // A computed field is a call, not a column, and ordering by its name
530 // asks for a column the table does not have. Everything that reads a
531 // field reference has to go through the same place, or the ones that
532 // do not silently mean something else.
533 let mut order = if term.field.json_path.is_empty() && term.field.computed.is_none() {
534 OrderExpr::new(&term.field.name)
535 } else {
536 let mut frag = SqlFragment::new();
537 push_field_ref(&mut frag, &term.field);
538 // `OrderExpr` carries SQL and no parameters, so anything bound
539 // here would be dropped and every `$n` after it would be reading
540 // the wrong value -- silently, and only for the requests that take
541 // this branch. Nothing on this path binds today; this is what says
542 // so when something starts to.
543 debug_assert!(
544 frag.params().is_empty(),
545 "an ORDER BY term bound a parameter, which this discards: {}",
546 frag.sql()
547 );
548 OrderExpr::raw(frag.sql())
549 };
550
551 if let Some(dir) = &term.direction {
552 order = match dir {
553 crate::api_request::OrderDirection::Asc => order.asc(),
554 crate::api_request::OrderDirection::Desc => order.desc(),
555 };
556 }
557
558 if let Some(nulls) = &term.nulls {
559 order = match nulls {
560 crate::api_request::OrderNulls::First => order.nulls_first(),
561 crate::api_request::OrderNulls::Last => order.nulls_last(),
562 };
563 }
564
565 order
566 }
567
568 /// Build a mutation query.
569 pub fn build_mutate(plan: &MutatePlan) -> Result<SqlFragment> {
570 match plan {
571 MutatePlan::Insert {
572 target,
573 columns,
574 body,
575 on_conflict,
576 where_clauses,
577 returning,
578 reports_inserted,
579 apply_defaults,
580 ..
581 } => {
582 let qi = postrust_sql::identifier::QualifiedIdentifier::new(
583 &target.schema,
584 &target.name,
585 );
586
587 let mut frag = SqlFragment::new();
588 frag.push("INSERT INTO ");
589 frag.push(&from_qi(&qi));
590
591 let object = body
592 .as_ref()
593 .map(|body| String::from_utf8_lossy(body).trim_start().starts_with('{'))
594 .unwrap_or(true);
595
596 match (body, columns.is_empty()) {
597 // A body naming no columns inserts rows of defaults -- one
598 // for `{}`, and one per element for an array, which is
599 // none at all for `[]`. `SELECT` with no columns is what
600 // says "a row, whatever the table's defaults are".
601 (Some(body), true) if !object => {
602 frag.push(" SELECT FROM (SELECT ");
603 frag.push_param(String::from_utf8_lossy(body).into_owned());
604 frag.push(
605 "::json AS json_data) pgrst_payload, LATERAL \
606 (SELECT FROM json_array_elements(pgrst_payload.json_data)) pgrst_body",
607 );
608 }
609 (_, true) => {
610 frag.push(" DEFAULT VALUES");
611 }
612 (Some(body), false) => {
613 frag.push(" (");
614 push_column_list(&mut frag, columns);
615 frag.push(") SELECT ");
616 for (i, column) in columns.iter().enumerate() {
617 if i > 0 {
618 frag.push(", ");
619 }
620 frag.push("pgrst_body.");
621 frag.push(&escape_ident(&column.name));
622 }
623 frag.push(" ");
624 push_json_body(&mut frag, columns, body, false, *apply_defaults);
625
626 // PUT names the row in the URL as well as in the body,
627 // and the two have to agree. The conditions are
628 // written against the body, so a row naming another
629 // key is left unwritten rather than writing the wrong
630 // one -- and the count that comes back then says
631 // whether the request meant exactly one row.
632 if !where_clauses.is_empty() {
633 frag.push(" WHERE ");
634 for (i, clause) in where_clauses.iter().enumerate() {
635 if i > 0 {
636 frag.push(" AND ");
637 }
638 frag.append(Self::build_logic_tree_in(Some("pgrst_body"), clause)?);
639 }
640 }
641 }
642 (None, false) => {
643 frag.push(" DEFAULT VALUES");
644 }
645 }
646
647 if let Some((resolution, conflict_cols)) = on_conflict {
648 frag.push(" ON CONFLICT (");
649 for (i, column) in conflict_cols.iter().enumerate() {
650 if i > 0 {
651 frag.push(", ");
652 }
653 frag.push(&escape_ident(column));
654 }
655 frag.push(") DO ");
656 match resolution {
657 crate::api_request::PreferResolution::IgnoreDuplicates => {
658 frag.push("NOTHING");
659 }
660 crate::api_request::PreferResolution::MergeDuplicates
661 if columns.is_empty() =>
662 {
663 frag.push("NOTHING");
664 }
665 crate::api_request::PreferResolution::MergeDuplicates => {
666 frag.push("UPDATE SET ");
667 for (i, column) in columns.iter().enumerate() {
668 if i > 0 {
669 frag.push(", ");
670 }
671 frag.push(&escape_ident(&column.name));
672 frag.push(" = EXCLUDED.");
673 frag.push(&escape_ident(&column.name));
674 }
675 }
676 }
677 }
678
679 push_returning(&mut frag, &target.name, returning);
680
681 // Whether each row was inserted or merged into an existing
682 // one, which is the difference between 201 and 200 and is
683 // knowable only here: `xmax` is zero on a row this statement
684 // created and non-zero on one it updated.
685 if on_conflict.is_some() && *reports_inserted && !returning.is_empty() {
686 frag.push(", (xmax = 0) AS ");
687 frag.push(&escape_ident(INSERTED_COLUMN));
688 }
689
690 Ok(frag)
691 }
692
693 MutatePlan::Update {
694 target,
695 columns,
696 body,
697 where_clauses,
698 returning,
699 apply_defaults,
700 ..
701 } => {
702 let qi = postrust_sql::identifier::QualifiedIdentifier::new(
703 &target.schema,
704 &target.name,
705 );
706
707 // An update that assigns nothing is not valid SQL, and it is
708 // also not an error: `PATCH` with `{}` matches rows and
709 // changes none of them. Selecting no rows from the table gives
710 // the same answer with the same column names, which is what a
711 // `?select=` over the result needs.
712 if columns.is_empty() || body.is_none() {
713 let mut frag = SqlFragment::new();
714 frag.push("SELECT * FROM ");
715 frag.push(&from_qi(&qi));
716 frag.push(" WHERE false");
717 return Ok(frag);
718 }
719
720 let body = body.as_ref().expect("checked above");
721 let mut frag = SqlFragment::new();
722 frag.push("UPDATE ");
723 frag.push(&from_qi(&qi));
724 frag.push(" SET ");
725 for (i, column) in columns.iter().enumerate() {
726 if i > 0 {
727 frag.push(", ");
728 }
729 frag.push(&escape_ident(&column.name));
730 frag.push(" = pgrst_body.");
731 frag.push(&escape_ident(&column.name));
732 }
733 frag.push(" ");
734 push_json_body(&mut frag, columns, body, true, *apply_defaults);
735
736 if !where_clauses.is_empty() {
737 frag.push(" WHERE ");
738 for (i, clause) in where_clauses.iter().enumerate() {
739 if i > 0 {
740 frag.push(" AND ");
741 }
742 frag.append(Self::build_logic_tree_in(Some(&target.name), clause)?);
743 }
744 }
745
746 push_returning(&mut frag, &target.name, returning);
747 Ok(frag)
748 }
749
750 MutatePlan::Delete {
751 target,
752 where_clauses,
753 returning,
754 } => {
755 let qi = postrust_sql::identifier::QualifiedIdentifier::new(
756 &target.schema,
757 &target.name,
758 );
759
760 let mut frag = SqlFragment::new();
761 frag.push("DELETE FROM ");
762 frag.push(&from_qi(&qi));
763
764 if !where_clauses.is_empty() {
765 frag.push(" WHERE ");
766 for (i, clause) in where_clauses.iter().enumerate() {
767 if i > 0 {
768 frag.push(" AND ");
769 }
770 frag.append(Self::build_logic_tree(clause)?);
771 }
772 }
773
774 push_returning(&mut frag, &target.name, returning);
775 Ok(frag)
776 }
777 }
778 }
779
780 /// Build an RPC call query.
781 pub fn build_call(plan: &CallPlan, read: Option<&ReadPlanTree>) -> Result<SqlFragment> {
782 let qi = postrust_sql::identifier::QualifiedIdentifier::new(
783 &plan.function.schema,
784 &plan.function.name,
785 );
786
787 // The call itself is the source of rows; the read plan, when there is
788 // one, shapes them exactly as it would shape a table's.
789 // A result this process cannot decode is rendered by the database,
790 // exactly as a column of such a type is. Without it a function
791 // returning `xml` or `bytea` answered null.
792 // A composite return -- OUT parameters, `RETURNS TABLE`, a row type --
793 // already expands to its own columns, and wrapping it would bury them
794 // under the function's name.
795 // A bare `record` is the other case: PostgreSQL will not put one in a
796 // `FROM` clause without being told its columns, but it will hand the
797 // value straight to `to_jsonb`, which reads the names off the record
798 // itself. That is where `returns_record()` gets `{"id": 1, ...}` from
799 // without anyone having declared what its columns are.
800 // A function whose return type is a media-type domain has already
801 // produced the response body. Wrapping it in JSON would make the
802 // response a JSON string *about* a PNG rather than the PNG.
803 let render_as_json = read.is_none()
804 && plan.media_type.is_none()
805 && !plan.returns_composite
806 && plan
807 .return_type
808 .as_deref()
809 .map(|t| !decodable_type(t) || t == "record")
810 .unwrap_or(false);
811
812 let mut frag = SqlFragment::new();
813 frag.push("SELECT ");
814 if render_as_json {
815 // Called in the select list rather than the `FROM`: a
816 // set-returning function there still yields a row per result, and
817 // a `record` needs no column list to be called this way.
818 frag.push("to_jsonb(pgrst_call.pgrst_scalar) AS ");
819 frag.push(&escape_ident(&plan.function.name));
820 frag.push(" FROM (SELECT ");
821 frag.push(&from_qi(&qi));
822 frag.push("(");
823 }
824 match read.filter(|tree| !tree.root.select.is_empty()) {
825 Some(tree) => {
826 for (i, field) in tree.root.select.iter().enumerate() {
827 if i > 0 {
828 frag.push(", ");
829 }
830 let column = Self::build_select_field(field)?;
831 frag.append(column);
832 }
833 }
834 None if !render_as_json => push_declared_columns(&mut frag, plan),
835 None => {}
836 }
837 if !render_as_json {
838 frag.push(" FROM ");
839 frag.push(&from_qi(&qi));
840 frag.push("(");
841 }
842
843 // Arguments come off the wire as strings. Casting each one to the type
844 // the function actually declares is what lets PostgreSQL resolve the
845 // signature; binding everything as `text` only works for text-taking
846 // functions. An argument the schema cache doesn't know is left
847 // untyped, so PostgreSQL applies its own inference rather than failing.
848 let declared_type = |name: &str| {
849 plan.param_types
850 .iter()
851 .find(|(n, _)| n == name)
852 .map(|(_, t)| t.clone())
853 };
854
855 match &plan.params {
856 CallParams::Named(params) => {
857 for (i, (name, value)) in params.iter().enumerate() {
858 if i > 0 {
859 frag.push(", ");
860 }
861 // A variadic parameter is passed as the whole tail, which
862 // is what `VARIADIC` says; `v => array[...]` finds no
863 // function, since no signature takes an array there.
864 if plan.variadic_params.iter().any(|v| v == name) {
865 frag.push("VARIADIC ");
866 frag.push(&escape_ident(name));
867 frag.push(" := ");
868 } else {
869 frag.push(&escape_ident(name));
870 frag.push(" => ");
871 }
872 match declared_type(name) {
873 Some(pg_type) => {
874 frag.push_typed_param(SqlParam::Text(value.clone()), &pg_type)
875 }
876 None => frag.push_param(SqlParam::Text(value.clone())),
877 };
878 }
879 }
880 CallParams::Positional(values) => {
881 for (i, value) in values.iter().enumerate() {
882 if i > 0 {
883 frag.push(", ");
884 }
885 match plan.param_types.get(i) {
886 Some((_, pg_type)) => {
887 frag.push_typed_param(SqlParam::Text(value.clone()), pg_type)
888 }
889 None => frag.push_param(SqlParam::Text(value.clone())),
890 };
891 }
892 }
893 CallParams::SingleObject(body) => {
894 let body_str = String::from_utf8_lossy(body);
895 frag.push_param(SqlParam::Text(body_str.to_string()));
896 }
897 CallParams::SingleUnnamed { body, pg_type } => {
898 match body {
899 Some(body) => frag.push_typed_param(
900 SqlParam::Text(String::from_utf8_lossy(body).into_owned()),
901 pg_type,
902 ),
903 // No body is that parameter's null. Casting it is what
904 // tells PostgreSQL which signature is meant, where the
905 // name carries more than one.
906 None => frag.push(&format!("NULL::{}", pg_type)),
907 };
908 }
909 CallParams::None => {}
910 }
911
912 frag.push(")");
913 if render_as_json {
914 frag.push(" AS pgrst_scalar) pgrst_call");
915 }
916
917 // A function returning a table's rows is aliased to that table.
918 //
919 // PostgreSQL names the range-table entry after the *function*, so
920 // `FROM test.search(...)` puts `search` in scope and not `items`. An
921 // unqualified column resolves either way, there being only one entry
922 // to resolve against -- which is why filters and ordering always
923 // worked and hid this. A computed column does not: it is a function of
924 // the whole row, written `test.always_true(items)`, and naming the row
925 // is the whole point of it. Unaliased, that is `column "items" does
926 // not exist` for a field the same request answers over the table
927 // itself.
928 if let Some(alias) = read.map(|tree| tree.root.from.name.as_str()) {
929 if !render_as_json && !alias.is_empty() {
930 frag.push(" AS ");
931 frag.push(&escape_ident(alias));
932 }
933 }
934
935 // Filters, ordering and paging over the returned rows.
936 if let Some(tree) = read {
937 for (i, clause) in tree.root.where_clauses.iter().enumerate() {
938 frag.push(if i == 0 { " WHERE " } else { " AND " });
939 let expr = Self::build_logic_tree(clause)?;
940 frag.append(expr);
941 }
942
943 for (i, term) in tree.root.order.iter().enumerate() {
944 frag.push(if i == 0 { " ORDER BY " } else { ", " });
945 let order = Self::build_order_term(term).into_fragment();
946 frag.append(order);
947 }
948
949 if let Some(limit) = tree.root.range.limit {
950 frag.push(&format!(" LIMIT {}", limit));
951 }
952 if tree.root.range.offset > 0 {
953 frag.push(&format!(" OFFSET {}", tree.root.range.offset));
954 }
955 }
956
957 Ok(frag)
958 }
959}
960
961/// Return the type to cast a bound filter value to, if it is safe to do so.
962///
963/// The type name is interpolated into SQL, so only bare type names are
964/// accepted: anything else (an empty type, a parameterised type such as
965/// `character varying(255)`, or the `ARRAY`/`USER-DEFINED` placeholders that
966/// `information_schema` reports) yields `None` and the value is bound
967/// uncast, preserving the previous behaviour.
968/// Append a JSON path to a column reference: `"data" -> 'a' ->> 'b'`.
969///
970/// Keys are emitted as string literals and indices as bare integers, which is
971/// what distinguishes `data->'1'` from `data->1` in PostgreSQL. Operands reach
972/// us already restricted to alphanumerics and underscores by the parser; the
973/// quote doubling is belt-and-braces so this stays safe if that ever loosens.
974/// The column that says whether a row was created or merged into.
975pub const INSERTED_COLUMN: &str = "pgrst_inserted";
976
977/// The columns a mutation writes, as a comma-separated identifier list.
978fn push_column_list(frag: &mut SqlFragment, columns: &[crate::plan::CoercibleField]) {
979 for (i, column) in columns.iter().enumerate() {
980 if i > 0 {
981 frag.push(", ");
982 }
983 frag.push(&escape_ident(&column.name));
984 }
985}
986
987/// The `FROM` clause that turns a JSON body into rows of the table's columns.
988///
989/// The body is read as a record set of exactly the columns being written, each
990/// typed as the column is -- or as `json`, where a data representation is going
991/// to parse it -- so PostgreSQL does the conversion and this process never has
992/// to guess at a literal's spelling.
993///
994/// `single` reads one object rather than an array, which is what a `PATCH`
995/// body is.
996fn push_json_body(
997 frag: &mut SqlFragment,
998 columns: &[crate::plan::CoercibleField],
999 body: &bytes::Bytes,
1000 single: bool,
1001 apply_defaults: bool,
1002) {
1003 let body = String::from_utf8_lossy(body).into_owned();
1004 let object = body.trim_start().starts_with('{');
1005
1006 // `Prefer: missing=default` asks for a column the body left out to take
1007 // the value the table would have given it, rather than null. The defaults
1008 // are merged underneath the body, so anything the body did name still
1009 // wins -- which is why this is `defaults || body` and not the reverse.
1010 let defaults: Vec<&crate::plan::CoercibleField> = match apply_defaults {
1011 true => columns.iter().filter(|c| c.default.is_some()).collect(),
1012 false => Vec::new(),
1013 };
1014 let merged = !defaults.is_empty();
1015
1016 frag.push("FROM (SELECT ");
1017 frag.push_param(body);
1018 frag.push(match merged {
1019 true => "::jsonb AS json_data) pgrst_payload, ",
1020 false => "::json AS json_data) pgrst_payload, ",
1021 });
1022
1023 if merged {
1024 let mut pairs = String::new();
1025 for (i, column) in defaults.iter().enumerate() {
1026 if i > 0 {
1027 pairs.push_str(", ");
1028 }
1029 pairs.push_str(&format!(
1030 "{}, {}",
1031 postrust_sql::quote_literal(&column.name),
1032 column.default.as_deref().unwrap_or("NULL")
1033 ));
1034 }
1035 frag.push(match object {
1036 true => "LATERAL (SELECT jsonb_build_object(",
1037 false => "LATERAL (SELECT jsonb_agg(jsonb_build_object(",
1038 });
1039 frag.push(&pairs);
1040 frag.push(match object {
1041 true => ") || pgrst_payload.json_data AS val) pgrst_defaults, ",
1042 false => {
1043 ") || pgrst_element) AS val \
1044 FROM jsonb_array_elements(pgrst_payload.json_data) pgrst_element) \
1045 pgrst_defaults, "
1046 }
1047 });
1048 }
1049
1050 frag.push("LATERAL (SELECT ");
1051 for (i, column) in columns.iter().enumerate() {
1052 if i > 0 {
1053 frag.push(", ");
1054 }
1055 match &column.transform {
1056 Some(parser) => {
1057 frag.push(parser);
1058 frag.push("(");
1059 frag.push(&escape_ident(&column.name));
1060 frag.push(") AS ");
1061 frag.push(&escape_ident(&column.name));
1062 }
1063 None => {
1064 frag.push(&escape_ident(&column.name));
1065 }
1066 }
1067 }
1068 // `json_to_record` takes one object and `json_to_recordset` an array, and
1069 // which one the body is is the body's own business -- an update written
1070 // with a one-element array is still an update. What makes it single is the
1071 // `LIMIT 1` below, not a claim about the body's shape: reading an array
1072 // with `json_to_record` is an error about `populate_composite` that says
1073 // nothing to the client that sent it.
1074 frag.push(match (merged, object) {
1075 (false, true) => " FROM json_to_record(pgrst_payload.json_data) AS _(",
1076 (false, false) => " FROM json_to_recordset(pgrst_payload.json_data) AS _(",
1077 (true, true) => " FROM jsonb_to_record(pgrst_defaults.val) AS _(",
1078 (true, false) => " FROM jsonb_to_recordset(pgrst_defaults.val) AS _(",
1079 });
1080 for (i, column) in columns.iter().enumerate() {
1081 if i > 0 {
1082 frag.push(", ");
1083 }
1084 frag.push(&escape_ident(&column.name));
1085 frag.push(" ");
1086 frag.push(castable_type(&column.ir_type).unwrap_or("text"));
1087 }
1088 // One paren closes the column definition list; the `LIMIT 1` then applies
1089 // to the lateral subquery, whose own paren closes after it.
1090 //
1091 // An update writes one set of values however many the body offers, so it
1092 // takes the first. Without this an array body of two objects would update
1093 // every matching row twice over.
1094 frag.push(match single {
1095 true => ") LIMIT 1) pgrst_body",
1096 false => ")) pgrst_body",
1097 });
1098}
1099
1100/// The column a mutation returns when the response reads none of its own.
1101///
1102/// It exists to be counted, never to be read: the body is empty in every case
1103/// that produces it.
1104pub const AFFECTED_COLUMN: &str = "pgrst_affected";
1105
1106/// Column carrying the written row whole, for a computed relationship.
1107///
1108/// A computed relationship is a function of the parent row, and after a
1109/// mutation the parent is a CTE whose row type is anonymous -- `record`, which
1110/// PostgreSQL will not cast to the table's composite type. Inside the
1111/// statement the real table is still in scope, so the row is taken there,
1112/// where a bare reference to it yields the composite, and travels out through
1113/// `RETURNING` as an ordinary column. It is stripped from the response like
1114/// any other column added for embedding.
1115pub const PARENT_ROW_COLUMN: &str = "pgrst_parent_row";
1116
1117/// The `RETURNING` list, qualified by the relation being written.
1118///
1119/// `RETURNING` sees everything in the statement's scope, which for an
1120/// `UPDATE ... FROM` includes the body -- and a bare column name there is
1121/// ambiguous for every column the update writes.
1122fn push_returning(frag: &mut SqlFragment, relation: &str, returning: &[String]) {
1123 // A statement whose result nothing reads still has to say how many rows it
1124 // touched, and the count is the number of rows it returns. A constant is
1125 // what says "one row per row affected" without naming a column the caller
1126 // may not be allowed to read.
1127 if returning.is_empty() {
1128 frag.push(" RETURNING 1 AS ");
1129 frag.push(&escape_ident(AFFECTED_COLUMN));
1130 return;
1131 }
1132 frag.push(" RETURNING ");
1133 for (i, column) in returning.iter().enumerate() {
1134 if i > 0 {
1135 frag.push(", ");
1136 }
1137 // The row itself, not a column of it. A bare reference to the relation
1138 // is the whole row typed as the table's composite, which is the one
1139 // thing a computed relationship can be called with -- and this is the
1140 // only place it can be taken, because one level up the CTE has turned
1141 // it into a `record`.
1142 if column == PARENT_ROW_COLUMN {
1143 frag.push(&escape_ident(relation));
1144 frag.push(" AS ");
1145 frag.push(&escape_ident(PARENT_ROW_COLUMN));
1146 continue;
1147 }
1148 frag.push(&escape_ident(relation));
1149 frag.push(".");
1150 frag.push(&escape_ident(column));
1151 }
1152}
1153
1154/// A column reference, converted to JSON first where the path requires it.
1155fn push_field_ref(frag: &mut SqlFragment, field: &crate::plan::CoercibleField) {
1156 push_qualified_field_ref(frag, None, field)
1157}
1158
1159/// The same, with the column qualified by a relation alias.
1160fn push_qualified_field_ref(
1161 frag: &mut SqlFragment,
1162 qualifier: Option<&str>,
1163 field: &crate::plan::CoercibleField,
1164) {
1165 // A computed field is a function of the whole row. PostgreSQL would also
1166 // accept `items.always_true` and resolve it to the same call, but only
1167 // where the reference is qualified by the relation -- which a bare column
1168 // name here is not -- so the call is written out.
1169 let column = match &field.computed {
1170 Some(computed) => format!(
1171 "{}.{}({}::{}.{})",
1172 escape_ident(&computed.function.schema),
1173 escape_ident(&computed.function.name),
1174 escape_ident(&computed.relation),
1175 escape_ident(&computed.row_type.schema),
1176 escape_ident(&computed.row_type.name),
1177 ),
1178 None => match qualifier {
1179 Some(alias) => format!("{}.{}", escape_ident(alias), escape_ident(&field.name)),
1180 None => escape_ident(&field.name),
1181 },
1182 };
1183
1184 if field.to_json {
1185 frag.push("to_jsonb(");
1186 frag.push(&column);
1187 frag.push(")");
1188 } else {
1189 frag.push(&column);
1190 }
1191 push_json_path(frag, &field.json_path);
1192}
1193
1194/// Project the columns a function declares, rather than asking for `*`.
1195///
1196/// `*` is all a caller can write when the result has no table behind it, and
1197/// it leaves a column of a type this process cannot decode to arrive as null.
1198/// Naming the columns is what lets those be rendered by the database, exactly
1199/// as a table's are. A function that declares none still gets `*`: there is
1200/// nothing better to say.
1201fn push_declared_columns(frag: &mut SqlFragment, plan: &crate::plan::CallPlan) {
1202 if plan.output_columns.is_empty() {
1203 frag.push("*");
1204 return;
1205 }
1206
1207 for (i, (name, pg_type)) in plan.output_columns.iter().enumerate() {
1208 if i > 0 {
1209 frag.push(", ");
1210 }
1211 match decodable_type(pg_type) {
1212 true => {
1213 frag.push(&escape_ident(name));
1214 }
1215 false => {
1216 frag.push("to_jsonb(");
1217 frag.push(&escape_ident(name));
1218 frag.push(") AS ");
1219 frag.push(&escape_ident(name));
1220 }
1221 }
1222 }
1223}
1224
1225/// The type a JSON path leaves on the left of a comparison.
1226///
1227/// `None` for a path ending in `->>`, which is already text -- the type filter
1228/// values are bound as, so no cast is wanted.
1229fn json_path_result_cast(path: &crate::api_request::JsonPath) -> Option<&'static str> {
1230 use crate::api_request::JsonOperation;
1231 match path.last()? {
1232 JsonOperation::Arrow(_) => Some("jsonb"),
1233 JsonOperation::DoubleArrow(_) => None,
1234 }
1235}
1236
1237fn push_json_path(frag: &mut SqlFragment, path: &crate::api_request::JsonPath) {
1238 use crate::api_request::{JsonOperand, JsonOperation};
1239
1240 for operation in path {
1241 let (arrow, operand) = match operation {
1242 JsonOperation::Arrow(operand) => ("->", operand),
1243 JsonOperation::DoubleArrow(operand) => ("->>", operand),
1244 };
1245
1246 frag.push(" ");
1247 frag.push(arrow);
1248 frag.push(" ");
1249
1250 match operand {
1251 JsonOperand::Key(key) => {
1252 frag.push(&format!("'{}'", key.replace('\'', "''")));
1253 }
1254 JsonOperand::Idx(index) => {
1255 frag.push(&index.to_string());
1256 }
1257 }
1258 }
1259}
1260
1261/// The name PostgREST gives an unaliased JSON path expression.
1262///
1263/// The last key in the path wins -- `data->a->>b` is reported as `b`. A path
1264/// that ends in an array index has no name to take, so it keeps the column's.
1265fn json_path_alias(column: &str, path: &crate::api_request::JsonPath) -> String {
1266 use crate::api_request::{JsonOperand, JsonOperation};
1267
1268 path.iter()
1269 .rev()
1270 .find_map(|operation| match operation {
1271 JsonOperation::Arrow(JsonOperand::Key(key))
1272 | JsonOperation::DoubleArrow(JsonOperand::Key(key)) => Some(key.clone()),
1273 _ => None,
1274 })
1275 .unwrap_or_else(|| column.to_string())
1276}
1277
1278/// Whether this process can decode a column of this type.
1279///
1280/// The row converter maps a fixed set of types by name and otherwise reads the
1281/// column as text, which only works for what PostgreSQL will hand over as
1282/// text. Anything else -- `xml`, `bytea`, an array, a PostGIS geometry -- fails
1283/// to decode and the column comes back null, so it is rendered in the database
1284/// instead. An empty type is a field with no column behind it, such as
1285/// `count()`, and never reaches this.
1286fn decodable_type(data_type: &str) -> bool {
1287 matches!(
1288 data_type,
1289 "" | "smallint"
1290 | "integer"
1291 | "bigint"
1292 | "numeric"
1293 | "decimal"
1294 | "real"
1295 | "double precision"
1296 | "boolean"
1297 | "text"
1298 | "character varying"
1299 | "character"
1300 | "name"
1301 | "date"
1302 | "time"
1303 | "time without time zone"
1304 | "time with time zone"
1305 | "timestamp"
1306 | "timestamp without time zone"
1307 | "timestamp with time zone"
1308 | "uuid"
1309 | "json"
1310 | "jsonb"
1311 )
1312}
1313
1314fn castable_type(pg_type: &str) -> Option<&str> {
1315 if pg_type.is_empty() {
1316 return None;
1317 }
1318
1319 // `character` and `bit` name themselves without their length, and casting
1320 // to either truncates to one -- `'10101'::bit` is `bit(1)`. A value on its
1321 // way into a `bit(5)` column has to arrive whole; the column enforces its
1322 // own length when the row is written, which is where that belongs.
1323 let unbounded = match pg_type {
1324 "character" | "bpchar" => Some("varchar"),
1325 "bit" => Some("varbit"),
1326 "character[]" | "bpchar[]" | "_bpchar" => Some("_varchar"),
1327 "bit[]" | "_bit" => Some("_varbit"),
1328 _ => None,
1329 };
1330 if unbounded.is_some() {
1331 return unbounded;
1332 }
1333
1334 let is_bare_name = pg_type
1335 .chars()
1336 .all(|c| c.is_ascii_alphanumeric() || c == '_');
1337
1338 if is_bare_name {
1339 Some(pg_type)
1340 } else {
1341 None
1342 }
1343}
1344
1345#[cfg(test)]
1346mod tests {
1347 use super::*;
1348
1349 #[test]
1350 fn castable_type_accepts_bare_type_names() {
1351 assert_eq!(castable_type("int4"), Some("int4"));
1352 assert_eq!(castable_type("timestamptz"), Some("timestamptz"));
1353 assert_eq!(castable_type("_text"), Some("_text"));
1354 }
1355
1356 /// A length these types do not carry, and would truncate to if cast.
1357 #[test]
1358 fn castable_type_gives_the_unbounded_spelling_of_a_measured_type() {
1359 assert_eq!(castable_type("character"), Some("varchar"));
1360 assert_eq!(castable_type("bit"), Some("varbit"));
1361 assert_eq!(castable_type("_bpchar"), Some("_varchar"));
1362 }
1363
1364 #[test]
1365 fn castable_type_rejects_unsafe_names() {
1366 assert_eq!(castable_type(""), None);
1367 assert_eq!(castable_type("character varying"), None);
1368 assert_eq!(castable_type("USER-DEFINED"), None);
1369 assert_eq!(castable_type("int4; DROP TABLE users"), None);
1370 }
1371
1372 /// Build the SQL for the single root filter in `query`.
1373 fn filter_sql_for(query: &str, pg_type: &str) -> (String, Vec<String>) {
1374 let params = crate::api_request::parse_query_params(query, false).unwrap();
1375 let frag = QueryBuilder::filter_sql(¶ms.filters_root[0], pg_type).unwrap();
1376 let bound = frag
1377 .params()
1378 .iter()
1379 .map(|p| format!("{:?}", p))
1380 .collect::<Vec<_>>();
1381 (frag.sql().to_string(), bound)
1382 }
1383
1384 #[test]
1385 fn quantified_comparison_binds_an_array() {
1386 let (sql, params) = filter_sql_for("id=eq(any).{1,2,3}", "int4");
1387 assert!(sql.contains("= ANY("), "got {sql}");
1388 assert!(sql.contains("int4[]"), "got {sql}");
1389 assert_eq!(params.len(), 1);
1390 assert!(params[0].contains("{1,2,3}"), "got {:?}", params[0]);
1391 }
1392
1393 #[test]
1394 fn quantified_all_renders_all() {
1395 let (sql, _) = filter_sql_for("id=lt(all).{4,5}", "int4");
1396 assert!(sql.contains("< ALL("), "got {sql}");
1397 }
1398
1399 #[test]
1400 fn quantified_like_maps_the_wildcard_inside_the_array() {
1401 let (_, params) = filter_sql_for("name=like(any).{foo*,*bar}", "text");
1402 assert!(params[0].contains("{foo%,%bar}"), "got {:?}", params[0]);
1403 }
1404
1405 #[test]
1406 fn array_column_is_not_double_arrayed() {
1407 // An array-typed column already compares element-wise, so the cast is
1408 // left alone rather than becoming `_text[]`.
1409 let (sql, _) = filter_sql_for("tags=eq(any).{a,b}", "_text");
1410 assert!(sql.contains("= ANY("), "got {sql}");
1411 assert!(!sql.contains("_text[]"), "got {sql}");
1412 }
1413}