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::{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/// The column that says whether a row was created or merged into.
969pub const INSERTED_COLUMN: &str = "pgrst_inserted";
970
971/// The columns a mutation writes, as a comma-separated identifier list.
972fn push_column_list(frag: &mut SqlFragment, columns: &[crate::plan::CoercibleField]) {
973    for (i, column) in columns.iter().enumerate() {
974        if i > 0 {
975            frag.push(", ");
976        }
977        frag.push(&escape_ident(&column.name));
978    }
979}
980
981/// The `FROM` clause that turns a JSON body into rows of the table's columns.
982///
983/// The body is read as a record set of exactly the columns being written, each
984/// typed as the column is -- or as `json`, where a data representation is going
985/// to parse it -- so PostgreSQL does the conversion and this process never has
986/// to guess at a literal's spelling.
987///
988/// `single` reads one object rather than an array, which is what a `PATCH`
989/// body is.
990fn push_json_body(
991    frag: &mut SqlFragment,
992    columns: &[crate::plan::CoercibleField],
993    body: &bytes::Bytes,
994    single: bool,
995    apply_defaults: bool,
996) {
997    let body = String::from_utf8_lossy(body).into_owned();
998    let object = body.trim_start().starts_with('{');
999
1000    // `Prefer: missing=default` asks for a column the body left out to take
1001    // the value the table would have given it, rather than null. The defaults
1002    // are merged underneath the body, so anything the body did name still
1003    // wins -- which is why this is `defaults || body` and not the reverse.
1004    let defaults: Vec<&crate::plan::CoercibleField> = match apply_defaults {
1005        true => columns.iter().filter(|c| c.default.is_some()).collect(),
1006        false => Vec::new(),
1007    };
1008    let merged = !defaults.is_empty();
1009
1010    frag.push("FROM (SELECT ");
1011    frag.push_param(body);
1012    frag.push(match merged {
1013        true => "::jsonb AS json_data) pgrst_payload, ",
1014        false => "::json AS json_data) pgrst_payload, ",
1015    });
1016
1017    if merged {
1018        let mut pairs = String::new();
1019        for (i, column) in defaults.iter().enumerate() {
1020            if i > 0 {
1021                pairs.push_str(", ");
1022            }
1023            pairs.push_str(&format!(
1024                "{}, {}",
1025                postrust_sql::quote_literal(&column.name),
1026                column.default.as_deref().unwrap_or("NULL")
1027            ));
1028        }
1029        frag.push(match object {
1030            true => "LATERAL (SELECT jsonb_build_object(",
1031            false => "LATERAL (SELECT jsonb_agg(jsonb_build_object(",
1032        });
1033        frag.push(&pairs);
1034        frag.push(match object {
1035            true => ") || pgrst_payload.json_data AS val) pgrst_defaults, ",
1036            false => {
1037                ") || pgrst_element) AS val \
1038                 FROM jsonb_array_elements(pgrst_payload.json_data) pgrst_element) \
1039                 pgrst_defaults, "
1040            }
1041        });
1042    }
1043
1044    frag.push("LATERAL (SELECT ");
1045    for (i, column) in columns.iter().enumerate() {
1046        if i > 0 {
1047            frag.push(", ");
1048        }
1049        match &column.transform {
1050            Some(parser) => {
1051                frag.push(parser);
1052                frag.push("(");
1053                frag.push(&escape_ident(&column.name));
1054                frag.push(") AS ");
1055                frag.push(&escape_ident(&column.name));
1056            }
1057            None => {
1058                frag.push(&escape_ident(&column.name));
1059            }
1060        }
1061    }
1062    // `json_to_record` takes one object and `json_to_recordset` an array, and
1063    // which one the body is is the body's own business -- an update written
1064    // with a one-element array is still an update. What makes it single is the
1065    // `LIMIT 1` below, not a claim about the body's shape: reading an array
1066    // with `json_to_record` is an error about `populate_composite` that says
1067    // nothing to the client that sent it.
1068    frag.push(match (merged, object) {
1069        (false, true) => " FROM json_to_record(pgrst_payload.json_data) AS _(",
1070        (false, false) => " FROM json_to_recordset(pgrst_payload.json_data) AS _(",
1071        (true, true) => " FROM jsonb_to_record(pgrst_defaults.val) AS _(",
1072        (true, false) => " FROM jsonb_to_recordset(pgrst_defaults.val) AS _(",
1073    });
1074    for (i, column) in columns.iter().enumerate() {
1075        if i > 0 {
1076            frag.push(", ");
1077        }
1078        frag.push(&escape_ident(&column.name));
1079        frag.push(" ");
1080        frag.push(castable_type(&column.ir_type).unwrap_or("text"));
1081    }
1082    // One paren closes the column definition list; the `LIMIT 1` then applies
1083    // to the lateral subquery, whose own paren closes after it.
1084    //
1085    // An update writes one set of values however many the body offers, so it
1086    // takes the first. Without this an array body of two objects would update
1087    // every matching row twice over.
1088    frag.push(match single {
1089        true => ") LIMIT 1) pgrst_body",
1090        false => ")) pgrst_body",
1091    });
1092}
1093
1094/// The column a mutation returns when the response reads none of its own.
1095///
1096/// It exists to be counted, never to be read: the body is empty in every case
1097/// that produces it.
1098pub const AFFECTED_COLUMN: &str = "pgrst_affected";
1099
1100/// Column carrying the written row whole, for a computed relationship.
1101///
1102/// A computed relationship is a function of the parent row, and after a
1103/// mutation the parent is a CTE whose row type is anonymous -- `record`, which
1104/// PostgreSQL will not cast to the table's composite type. Inside the
1105/// statement the real table is still in scope, so the row is taken there,
1106/// where a bare reference to it yields the composite, and travels out through
1107/// `RETURNING` as an ordinary column. It is stripped from the response like
1108/// any other column added for embedding.
1109pub const PARENT_ROW_COLUMN: &str = "pgrst_parent_row";
1110
1111/// The `RETURNING` list, qualified by the relation being written.
1112///
1113/// `RETURNING` sees everything in the statement's scope, which for an
1114/// `UPDATE ... FROM` includes the body -- and a bare column name there is
1115/// ambiguous for every column the update writes.
1116fn push_returning(frag: &mut SqlFragment, relation: &str, returning: &[String]) {
1117    // A statement whose result nothing reads still has to say how many rows it
1118    // touched, and the count is the number of rows it returns. A constant is
1119    // what says "one row per row affected" without naming a column the caller
1120    // may not be allowed to read.
1121    if returning.is_empty() {
1122        frag.push(" RETURNING 1 AS ");
1123        frag.push(&escape_ident(AFFECTED_COLUMN));
1124        return;
1125    }
1126    frag.push(" RETURNING ");
1127    for (i, column) in returning.iter().enumerate() {
1128        if i > 0 {
1129            frag.push(", ");
1130        }
1131        // The row itself, not a column of it. A bare reference to the relation
1132        // is the whole row typed as the table's composite, which is the one
1133        // thing a computed relationship can be called with -- and this is the
1134        // only place it can be taken, because one level up the CTE has turned
1135        // it into a `record`.
1136        if column == PARENT_ROW_COLUMN {
1137            frag.push(&escape_ident(relation));
1138            frag.push(" AS ");
1139            frag.push(&escape_ident(PARENT_ROW_COLUMN));
1140            continue;
1141        }
1142        frag.push(&escape_ident(relation));
1143        frag.push(".");
1144        frag.push(&escape_ident(column));
1145    }
1146}
1147
1148/// A column reference, converted to JSON first where the path requires it.
1149fn push_field_ref(frag: &mut SqlFragment, field: &crate::plan::CoercibleField) {
1150    push_qualified_field_ref(frag, None, field)
1151}
1152
1153/// The same, with the column qualified by a relation alias.
1154fn push_qualified_field_ref(
1155    frag: &mut SqlFragment,
1156    qualifier: Option<&str>,
1157    field: &crate::plan::CoercibleField,
1158) {
1159    // A computed field is a function of the whole row. PostgreSQL would also
1160    // accept `items.always_true` and resolve it to the same call, but only
1161    // where the reference is qualified by the relation -- which a bare column
1162    // name here is not -- so the call is written out.
1163    let column = match &field.computed {
1164        Some(computed) => format!(
1165            "{}.{}({}::{}.{})",
1166            escape_ident(&computed.function.schema),
1167            escape_ident(&computed.function.name),
1168            escape_ident(&computed.relation),
1169            escape_ident(&computed.row_type.schema),
1170            escape_ident(&computed.row_type.name),
1171        ),
1172        None => match qualifier {
1173            Some(alias) => format!("{}.{}", escape_ident(alias), escape_ident(&field.name)),
1174            None => escape_ident(&field.name),
1175        },
1176    };
1177
1178    if field.to_json {
1179        frag.push("to_jsonb(");
1180        frag.push(&column);
1181        frag.push(")");
1182    } else {
1183        frag.push(&column);
1184    }
1185    push_json_path(frag, &field.json_path);
1186}
1187
1188/// Project the columns a function declares, rather than asking for `*`.
1189///
1190/// `*` is all a caller can write when the result has no table behind it, and
1191/// it leaves a column of a type this process cannot decode to arrive as null.
1192/// Naming the columns is what lets those be rendered by the database, exactly
1193/// as a table's are. A function that declares none still gets `*`: there is
1194/// nothing better to say.
1195fn push_declared_columns(frag: &mut SqlFragment, plan: &crate::plan::CallPlan) {
1196    if plan.output_columns.is_empty() {
1197        frag.push("*");
1198        return;
1199    }
1200
1201    for (i, (name, pg_type)) in plan.output_columns.iter().enumerate() {
1202        if i > 0 {
1203            frag.push(", ");
1204        }
1205        match decodable_type(pg_type) {
1206            true => {
1207                frag.push(&escape_ident(name));
1208            }
1209            false => {
1210                frag.push("to_jsonb(");
1211                frag.push(&escape_ident(name));
1212                frag.push(") AS ");
1213                frag.push(&escape_ident(name));
1214            }
1215        }
1216    }
1217}
1218
1219/// The type a JSON path leaves on the left of a comparison.
1220///
1221/// `None` for a path ending in `->>`, which is already text -- the type filter
1222/// values are bound as, so no cast is wanted.
1223fn json_path_result_cast(path: &crate::api_request::JsonPath) -> Option<&'static str> {
1224    use crate::api_request::JsonOperation;
1225    match path.last()? {
1226        JsonOperation::Arrow(_) => Some("jsonb"),
1227        JsonOperation::DoubleArrow(_) => None,
1228    }
1229}
1230
1231/// Append a JSON path to a column reference: `"data" -> 'a' ->> 'b'`.
1232///
1233/// Keys are emitted as string literals and indices as bare integers, which is
1234/// what distinguishes `data->'1'` from `data->1` in PostgreSQL. A key is
1235/// whatever the client wrote up to the next `,`, `(`, `)`, `:` or arrow --
1236/// quotes and backslashes included -- so it is written by `quote_literal`,
1237/// which keeps it one literal whatever `standard_conforming_strings` is.
1238fn push_json_path(frag: &mut SqlFragment, path: &crate::api_request::JsonPath) {
1239    use crate::api_request::{JsonOperand, JsonOperation};
1240
1241    for operation in path {
1242        let (arrow, operand) = match operation {
1243            JsonOperation::Arrow(operand) => ("->", operand),
1244            JsonOperation::DoubleArrow(operand) => ("->>", operand),
1245        };
1246
1247        frag.push(" ");
1248        frag.push(arrow);
1249        frag.push(" ");
1250
1251        match operand {
1252            JsonOperand::Key(key) => {
1253                frag.push(&postrust_sql::quote_literal(key));
1254            }
1255            JsonOperand::Idx(index) => {
1256                frag.push(&index.to_string());
1257            }
1258        }
1259    }
1260}
1261
1262/// The name PostgREST gives an unaliased JSON path expression.
1263///
1264/// The last key in the path wins -- `data->a->>b` is reported as `b`. A path
1265/// that ends in an array index has no name to take, so it keeps the column's.
1266fn json_path_alias(column: &str, path: &crate::api_request::JsonPath) -> String {
1267    use crate::api_request::{JsonOperand, JsonOperation};
1268
1269    path.iter()
1270        .rev()
1271        .find_map(|operation| match operation {
1272            JsonOperation::Arrow(JsonOperand::Key(key))
1273            | JsonOperation::DoubleArrow(JsonOperand::Key(key)) => Some(key.clone()),
1274            _ => None,
1275        })
1276        .unwrap_or_else(|| column.to_string())
1277}
1278
1279/// Whether this process can decode a column of this type.
1280///
1281/// The row converter maps a fixed set of types by name and otherwise reads the
1282/// column as text, which only works for what PostgreSQL will hand over as
1283/// text. Anything else -- `xml`, `bytea`, an array, a PostGIS geometry -- fails
1284/// to decode and the column comes back null, so it is rendered in the database
1285/// instead. An empty type is a field with no column behind it, such as
1286/// `count()`, and never reaches this.
1287fn decodable_type(data_type: &str) -> bool {
1288    matches!(
1289        data_type,
1290        "" | "smallint"
1291            | "integer"
1292            | "bigint"
1293            | "numeric"
1294            | "decimal"
1295            | "real"
1296            | "double precision"
1297            | "boolean"
1298            | "text"
1299            | "character varying"
1300            | "character"
1301            | "name"
1302            | "date"
1303            | "time"
1304            | "time without time zone"
1305            | "time with time zone"
1306            | "timestamp"
1307            | "timestamp without time zone"
1308            | "timestamp with time zone"
1309            | "uuid"
1310            | "json"
1311            | "jsonb"
1312    )
1313}
1314
1315fn castable_type(pg_type: &str) -> Option<&str> {
1316    if pg_type.is_empty() {
1317        return None;
1318    }
1319
1320    // `character` and `bit` name themselves without their length, and casting
1321    // to either truncates to one -- `'10101'::bit` is `bit(1)`. A value on its
1322    // way into a `bit(5)` column has to arrive whole; the column enforces its
1323    // own length when the row is written, which is where that belongs.
1324    let unbounded = match pg_type {
1325        "character" | "bpchar" => Some("varchar"),
1326        "bit" => Some("varbit"),
1327        "character[]" | "bpchar[]" | "_bpchar" => Some("_varchar"),
1328        "bit[]" | "_bit" => Some("_varbit"),
1329        _ => None,
1330    };
1331    if unbounded.is_some() {
1332        return unbounded;
1333    }
1334
1335    let is_bare_name = pg_type
1336        .chars()
1337        .all(|c| c.is_ascii_alphanumeric() || c == '_');
1338
1339    if is_bare_name {
1340        Some(pg_type)
1341    } else {
1342        None
1343    }
1344}
1345
1346#[cfg(test)]
1347mod tests {
1348    use super::*;
1349
1350    #[test]
1351    fn castable_type_accepts_bare_type_names() {
1352        assert_eq!(castable_type("int4"), Some("int4"));
1353        assert_eq!(castable_type("timestamptz"), Some("timestamptz"));
1354        assert_eq!(castable_type("_text"), Some("_text"));
1355    }
1356
1357    /// A length these types do not carry, and would truncate to if cast.
1358    #[test]
1359    fn castable_type_gives_the_unbounded_spelling_of_a_measured_type() {
1360        assert_eq!(castable_type("character"), Some("varchar"));
1361        assert_eq!(castable_type("bit"), Some("varbit"));
1362        assert_eq!(castable_type("_bpchar"), Some("_varchar"));
1363    }
1364
1365    #[test]
1366    fn castable_type_rejects_unsafe_names() {
1367        assert_eq!(castable_type(""), None);
1368        assert_eq!(castable_type("character varying"), None);
1369        assert_eq!(castable_type("USER-DEFINED"), None);
1370        assert_eq!(castable_type("int4; DROP TABLE users"), None);
1371    }
1372
1373    /// Build the SQL for the single root filter in `query`.
1374    fn filter_sql_for(query: &str, pg_type: &str) -> (String, Vec<String>) {
1375        let params = crate::api_request::parse_query_params(query, false).unwrap();
1376        let frag = QueryBuilder::filter_sql(&params.filters_root[0], pg_type).unwrap();
1377        let bound = frag
1378            .params()
1379            .iter()
1380            .map(|p| format!("{:?}", p))
1381            .collect::<Vec<_>>();
1382        (frag.sql().to_string(), bound)
1383    }
1384
1385    #[test]
1386    fn quantified_comparison_binds_an_array() {
1387        let (sql, params) = filter_sql_for("id=eq(any).{1,2,3}", "int4");
1388        assert!(sql.contains("= ANY("), "got {sql}");
1389        assert!(sql.contains("int4[]"), "got {sql}");
1390        assert_eq!(params.len(), 1);
1391        assert!(params[0].contains("{1,2,3}"), "got {:?}", params[0]);
1392    }
1393
1394    #[test]
1395    fn quantified_all_renders_all() {
1396        let (sql, _) = filter_sql_for("id=lt(all).{4,5}", "int4");
1397        assert!(sql.contains("< ALL("), "got {sql}");
1398    }
1399
1400    #[test]
1401    fn quantified_like_maps_the_wildcard_inside_the_array() {
1402        let (_, params) = filter_sql_for("name=like(any).{foo*,*bar}", "text");
1403        assert!(params[0].contains("{foo%,%bar}"), "got {:?}", params[0]);
1404    }
1405
1406    #[test]
1407    fn array_column_is_not_double_arrayed() {
1408        // An array-typed column already compares element-wise, so the cast is
1409        // left alone rather than becoming `_text[]`.
1410        let (sql, _) = filter_sql_for("tags=eq(any).{a,b}", "_text");
1411        assert!(sql.contains("= ANY("), "got {sql}");
1412        assert!(!sql.contains("_text[]"), "got {sql}");
1413    }
1414}