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/// 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(&params.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}