Skip to main content

postrust_core/
embed.rs

1//! Relationship embedding: fetching related rows for a set of parent rows.
2//!
3//! Both the REST and GraphQL surfaces embed related resources, and both do it
4//! the same way: take the parent rows already fetched, collect the values of
5//! the join column, and issue **one** query for all of them rather than one
6//! query per parent row.
7//!
8//! ```text
9//! SELECT row_to_json(t) FROM (
10//!     SELECT * FROM "public"."posts" WHERE "user_id" = ANY($1::int4[])
11//! ) t
12//! ```
13//!
14//! The children are then grouped by that column and attached to the parent
15//! rows, so a request embedding two relationships across a page of 25 parents
16//! costs three queries, not fifty-one.
17
18use crate::error::{Error, Result};
19use crate::schema_cache::{Relationship, SchemaCache, Table};
20use std::collections::HashMap;
21
22/// How an embedded row or array is rendered.
23///
24/// **`jsonb` reorders keys.** It sorts them by length and then bytewise,
25/// inside PostgreSQL -- `SELECT row_to_json(t)::jsonb FROM (SELECT 1 AS aaa,
26/// 2 AS zz) t` gives `{"zz": 2, "aaa": 1}` -- so the order the columns were
27/// selected in is gone before the row reaches this process, and nothing on
28/// the Rust side can put it back. It also regularises the spacing that `json`
29/// keeps verbatim.
30///
31/// Both behaviours are wanted, in different places, which is why this is a
32/// choice made per query rather than a constant.
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub enum EmbedRendering {
35    /// `json`. The row is written exactly as `row_to_json` produced it, so the
36    /// columns stay in the order they were selected. This is what an embedded
37    /// resource in a JSON body wants, and what PostgREST itself emits there.
38    Verbatim,
39    /// `jsonb`. PostgreSQL normalises the text.
40    ///
41    /// Only needed where the embedded value is rendered *as text* rather than
42    /// as part of a JSON body: a schema-declared media handler that
43    /// stringifies a whole row renders each column's text, and a `json` column
44    /// renders verbatim where PostgREST's `jsonb` one renders normalised --
45    /// `{"id":1}` against `{"id": 1}`. Measured: it is the only difference
46    /// between the two in PostgREST's whole corpus.
47    Normalised,
48}
49
50impl EmbedRendering {
51    /// The SQL cast this rendering needs.
52    fn cast(self) -> &'static str {
53        match self {
54            Self::Verbatim => "::json",
55            Self::Normalised => "::jsonb",
56        }
57    }
58}
59
60/// Everything needed to fetch one relationship's rows for a set of parents.
61#[derive(Clone, Debug)]
62pub struct EmbedPlan {
63    /// Column on the parent row whose value identifies the parent.
64    pub local_column: String,
65    /// Column on the related table that points back at the parent.
66    pub foreign_column: String,
67    /// PostgreSQL type of the foreign column, used to cast the bound array.
68    pub foreign_column_type: String,
69    /// Schema of the related table.
70    pub foreign_schema: String,
71    /// Name of the related table.
72    pub foreign_table: String,
73    /// Whether the relationship yields many rows per parent.
74    pub is_list: bool,
75    /// Every column pair the join is on, parent side first.
76    ///
77    /// Usually one. A foreign key over several columns joins on all of them,
78    /// and the pairs are ordered so each parent column sits with the child
79    /// column it actually references.
80    pub columns: Vec<(String, String)>,
81    /// The junction of a many-to-many relationship, if that is what this is.
82    ///
83    /// The parent and the child share no key; each points at a table that
84    /// exists to join them, so reaching the child means going through it.
85    pub junction: Option<EmbedJunction>,
86    /// The function behind a computed relationship, if that is what this is.
87    ///
88    /// A computed relationship is a function taking the parent row and
89    /// returning rows of the related table. There is no key to join on -- the
90    /// parent row is the argument -- so the columns above are empty and the
91    /// relationship can only be expressed as a call correlated to the parent.
92    pub function: Option<crate::api_request::QualifiedIdentifier>,
93    /// The name of the parameter that takes the parent's row, where the
94    /// function has more than one and the call has to be written by name.
95    pub row_argument: Option<String>,
96}
97
98/// The table a many-to-many relationship is joined through.
99#[derive(Clone, Debug)]
100pub struct EmbedJunction {
101    /// Schema holding the junction table.
102    pub schema: String,
103    /// The junction table's name.
104    pub table: String,
105    /// `(parent column, junction column)` for each column the parent joins on.
106    ///
107    /// Usually one. A junction may be keyed over several columns on each side
108    /// -- `touched_files` joins `files(project_id, filename)` to
109    /// `users_tasks(user_id, task_id)` -- and joining on the first of them
110    /// alone would relate rows that are not related.
111    pub parent_columns: Vec<(String, String)>,
112    /// `(junction column, child column)` for each column the child joins on.
113    pub child_columns: Vec<(String, String)>,
114}
115
116impl EmbedJunction {
117    /// The junction's join to the child, one equality per column pair.
118    fn child_join(&self, junction_alias: &str, child_alias: &str) -> String {
119        Self::equalities(&self.child_columns, junction_alias, child_alias, true)
120    }
121
122    /// The junction's correlation to one parent row, one equality per pair.
123    fn parent_correlation(&self, junction_alias: &str, parent_alias: &str) -> String {
124        Self::equalities(&self.parent_columns, junction_alias, parent_alias, false)
125    }
126
127    /// `junction.column = other.column` for each pair, joined with `AND`.
128    ///
129    /// `junction_first` says which half of the pair is the junction's, the two
130    /// lists running in opposite directions: the parent's own column comes
131    /// first on the way in, and the child's second on the way out.
132    fn equalities(
133        pairs: &[(String, String)],
134        junction_alias: &str,
135        other_alias: &str,
136        junction_first: bool,
137    ) -> String {
138        pairs
139            .iter()
140            .map(|(left, right)| {
141                let (junction_column, other_column) = match junction_first {
142                    true => (left, right),
143                    false => (right, left),
144                };
145                format!(
146                    "{}.{} = {}.{}",
147                    postrust_sql::escape_ident(junction_alias),
148                    postrust_sql::escape_ident(junction_column),
149                    postrust_sql::escape_ident(other_alias),
150                    postrust_sql::escape_ident(other_column),
151                )
152            })
153            .collect::<Vec<_>>()
154            .join(" AND ")
155    }
156}
157
158impl EmbedPlan {
159    /// Resolve a relationship into an embed plan.
160    ///
161    /// Returns an error for relationships this cannot express yet, rather than
162    /// silently omitting the embedded data.
163    pub fn resolve(relationship: &Relationship, schema_cache: &SchemaCache) -> Result<Self> {
164        let foreign_table_qi = relationship.foreign_table().clone();
165
166        // A computed relationship takes the parent row as its argument, so
167        // there is nothing to look up and nothing to join on.
168        if let Relationship::Computed {
169            function,
170            foreign_table,
171            to_one,
172            row_argument,
173            ..
174        } = relationship
175        {
176            return Ok(Self {
177                local_column: String::new(),
178                foreign_column: String::new(),
179                foreign_column_type: String::new(),
180                foreign_schema: foreign_table.schema.clone(),
181                foreign_table: foreign_table.name.clone(),
182                is_list: !to_one,
183                columns: Vec::new(),
184                junction: None,
185                function: Some(function.clone()),
186                row_argument: row_argument.clone(),
187            });
188        }
189
190        if let Relationship::ForeignKey {
191            cardinality: crate::schema_cache::Cardinality::M2M(junction),
192            ..
193        } = relationship
194        {
195            // The join itself is written from every column pair below. These
196            // two are the parent's and the child's first key column, which is
197            // what the batched path groups by -- and it refuses a junction
198            // keyed on more than one, rather than grouping by half a key.
199            let (local_column, _) =
200                junction.source_columns.first().cloned().ok_or_else(|| {
201                    Error::EmbeddingError("junction has no source columns".into())
202                })?;
203            let (_, foreign_column) =
204                junction.target_columns.first().cloned().ok_or_else(|| {
205                    Error::EmbeddingError("junction has no target columns".into())
206                })?;
207
208            return Ok(Self {
209                local_column,
210                foreign_column,
211                foreign_column_type: String::new(),
212                foreign_schema: foreign_table_qi.schema.clone(),
213                foreign_table: foreign_table_qi.name.clone(),
214                // A junction always yields a set: that is what it is for.
215                is_list: true,
216                columns: Vec::new(),
217                row_argument: None,
218                junction: Some(EmbedJunction {
219                    schema: junction.table.schema.clone(),
220                    table: junction.table.name.clone(),
221                    parent_columns: junction.source_columns.clone(),
222                    child_columns: junction.target_columns.clone(),
223                }),
224                function: None,
225            });
226        }
227
228        let columns = match relationship {
229            Relationship::ForeignKey { cardinality, .. } => cardinality.columns(),
230            Relationship::Computed { .. } => unreachable!("handled above"),
231        };
232
233        let (local_column, foreign_column) = columns
234            .first()
235            .cloned()
236            .ok_or_else(|| Error::EmbeddingError("relationship joins on no columns".into()))?;
237
238        let foreign_table: &Table = schema_cache.get_table(&foreign_table_qi).ok_or_else(|| {
239            Error::EmbeddingError(format!(
240                "cannot embed \"{}\": it is not in an exposed schema",
241                foreign_table_qi
242            ))
243        })?;
244
245        let foreign_column_type = foreign_table
246            .get_column(&foreign_column)
247            .map(|c| c.nominal_type.clone())
248            .ok_or_else(|| {
249                Error::EmbeddingError(format!(
250                    "cannot embed \"{}\": join column \"{}\" not found",
251                    foreign_table_qi, foreign_column
252                ))
253            })?;
254
255        Ok(Self {
256            local_column,
257            foreign_column,
258            foreign_column_type,
259            foreign_schema: foreign_table_qi.schema.clone(),
260            foreign_table: foreign_table_qi.name.clone(),
261            is_list: !relationship.is_to_one(),
262            columns,
263            junction: None,
264            function: None,
265            row_argument: None,
266        })
267    }
268
269    /// The predicate correlating child rows to one parent row.
270    ///
271    /// A foreign key over several columns joins on all of them, so this is one
272    /// equality per pair rather than a single one.
273    fn correlation(&self, parent_alias: &str, child_alias: &str) -> String {
274        let pairs = if self.columns.is_empty() {
275            vec![(self.local_column.clone(), self.foreign_column.clone())]
276        } else {
277            self.columns.clone()
278        };
279
280        pairs
281            .iter()
282            .map(|(local, foreign)| {
283                format!(
284                    "{}.{} = {}.{}",
285                    postrust_sql::escape_ident(child_alias),
286                    postrust_sql::escape_ident(foreign),
287                    postrust_sql::escape_ident(parent_alias),
288                    postrust_sql::escape_ident(local),
289                )
290            })
291            .collect::<Vec<_>>()
292            .join(" AND ")
293    }
294
295    /// SQL that fetches every related row for the given parent key values.
296    ///
297    /// The keys are bound as a single text array and cast to the foreign
298    /// column's type, so the column itself is never wrapped in a cast and an
299    /// index on it remains usable. `limit` bounds the rows per query, not per
300    /// parent.
301    ///
302    /// `columns` is the set of columns the client asked for. An empty set means
303    /// every column. Projecting here rather than discarding columns after the
304    /// fact matters: an unprojected column is read from the heap, serialised to
305    /// JSON by PostgreSQL, sent over the socket and parsed, before being thrown
306    /// away. The join column is always included even when it was not requested,
307    /// because grouping needs it; the caller strips it afterwards.
308    pub fn children_sql(&self, limit: Option<i64>, columns: &[String]) -> Result<String> {
309        let type_name = castable_type_name(&self.foreign_column_type).ok_or_else(|| {
310            Error::EmbeddingError(format!(
311                "cannot embed \"{}\": join column type \"{}\" is not a plain type name",
312                self.foreign_table, self.foreign_column_type
313            ))
314        })?;
315
316        let projection = self.projection(columns);
317
318        let mut inner = format!(
319            "SELECT {} FROM {}.{} WHERE {} = ANY($1::{}[])",
320            projection,
321            postrust_sql::escape_ident(&self.foreign_schema),
322            postrust_sql::escape_ident(&self.foreign_table),
323            postrust_sql::escape_ident(&self.foreign_column),
324            type_name
325        );
326
327        if let Some(limit) = limit {
328            inner.push_str(&format!(" LIMIT {}", limit));
329        }
330
331        Ok(format!("SELECT row_to_json(t) FROM ({}) t", inner))
332    }
333
334    /// SQL that fetches related rows already grouped by their join key.
335    ///
336    /// One row comes back per distinct key: the key itself and a JSON array of
337    /// that key's children. Grouping in PostgreSQL rather than in this process
338    /// removes a per-child-row JSON parse, the per-row hash insert, and the
339    /// clone of each group onto its parent.
340    ///
341    /// The key is returned as JSON rather than cast to text, so it is rendered
342    /// by the same code that renders the parents' keys. Casting to text in SQL
343    /// would agree for integers and uuids and disagree for a NUMERIC join
344    /// column, where PostgreSQL and serde_json format differently.
345    ///
346    /// `limit` still bounds the rows scanned, not the rows per parent, so it is
347    /// applied to the inner select exactly as the ungrouped form does.
348    pub fn children_grouped_sql(&self, limit: Option<i64>, columns: &[String]) -> Result<String> {
349        if self
350            .junction
351            .as_ref()
352            .is_some_and(|j| j.parent_columns.len() > 1)
353        {
354            return Err(Error::EmbeddingError(format!(
355                "embedding \"{}\" this way is not supported: it joins through a junction \
356                 keyed on several columns, and grouping children by key needs a single one",
357                self.foreign_table,
358            )));
359        }
360
361        if self.columns.len() > 1 {
362            return Err(Error::EmbeddingError(format!(
363                "embedding \"{}\" this way is not supported: it joins on {} columns, and \
364                 grouping children by key needs a single one",
365                self.foreign_table,
366                self.columns.len()
367            )));
368        }
369
370        let type_name = castable_type_name(&self.foreign_column_type).ok_or_else(|| {
371            Error::EmbeddingError(format!(
372                "cannot embed \"{}\": join column type \"{}\" is not a plain type name",
373                self.foreign_table, self.foreign_column_type
374            ))
375        })?;
376
377        let key = postrust_sql::escape_ident(&self.foreign_column);
378
379        let mut inner = format!(
380            "SELECT {} FROM {}.{} WHERE {} = ANY($1::{}[])",
381            self.projection(columns),
382            postrust_sql::escape_ident(&self.foreign_schema),
383            postrust_sql::escape_ident(&self.foreign_table),
384            key,
385            type_name
386        );
387
388        if let Some(limit) = limit {
389            inner.push_str(&format!(" LIMIT {}", limit));
390        }
391
392        Ok(format!(
393            "SELECT to_jsonb(c.{key}) AS k, json_agg(row_to_json(c)) AS v \
394             FROM ({inner}) c GROUP BY c.{key}",
395            key = key,
396            inner = inner
397        ))
398    }
399
400    /// The child rows belonging to one parent row, as a SELECT correlated
401    /// against the parent's alias.
402    ///
403    /// This is the single-query form of embedding: instead of fetching parents,
404    /// collecting their keys and issuing a second query, the relationship is
405    /// attached to the parent query as an expression, so PostgreSQL builds the
406    /// array while it already has the parent row.
407    ///
408    /// `inner_select` is the child's SELECT list, which the caller assembles --
409    /// its columns, plus any deeper relationship expressions built by calling
410    /// this again. Only the caller knows the shape of its own selection tree, so
411    /// the recursion lives there and the SQL assembly lives here.
412    ///
413    /// What is done with those rows is the caller's: [`embed_expression`]
414    /// renders them as JSON, [`aggregate_expression`] summarises them.
415    #[allow(clippy::too_many_arguments)] // one parameter per SQL clause
416    pub fn correlated_rows(
417        &self,
418        parent_alias: &str,
419        parent_row: &str,
420        child_alias: &str,
421        inner_select: &str,
422        limit: Option<i64>,
423        // `clients.offset=1` skips rows inside the embed. A to-one embed that
424        // is skipped past yields no row at all, which is null -- the same
425        // answer as no match, and the one PostgREST gives.
426        offset: i64,
427        child_where: Option<&str>,
428        // `clients.order=name.desc` orders the rows inside the embed, which
429        // belongs to the child's own subselect -- and has to be applied before
430        // the child's own limit, or the limit takes an unsorted window.
431        order_by: Option<&str>,
432        // The whole argument list of a computed relationship's function, where
433        // the parent row is not the only thing it takes.
434        function_arguments: Option<&str>,
435    ) -> Result<String> {
436        // The child table is aliased rather than referred to by name. A
437        // self-referential relationship would otherwise make the correlation
438        // ambiguous, since the parent and the child are the same table.
439        //
440        // A computed relationship is correlated by argument rather than by a
441        // key: the function takes the parent row, so the parent alias is
442        // passed to it and there is no predicate to write.
443        let mut inner = match &self.function {
444            Some(function) => format!(
445                "SELECT {} FROM {}.{}({}) AS {} WHERE true",
446                inner_select,
447                postrust_sql::escape_ident(&function.schema),
448                postrust_sql::escape_ident(&function.name),
449                // The parent row alone, unless the caller wrote the whole
450                // argument list: a function with more than the row -- a search
451                // term, the caller's session -- is called by name, and only the
452                // caller knows what to put in those.
453                function_arguments.unwrap_or(parent_row),
454                postrust_sql::escape_ident(child_alias),
455            ),
456            None => match &self.junction {
457                // Two hops: the child is reached through the table that joins
458                // it to the parent, so the correlation lands on the junction.
459                Some(junction) => {
460                    let junction_alias = format!("{}_j", child_alias);
461                    format!(
462                        "SELECT {} FROM {}.{} AS {} JOIN {}.{} AS {} ON {} WHERE {}",
463                        inner_select,
464                        postrust_sql::escape_ident(&self.foreign_schema),
465                        postrust_sql::escape_ident(&self.foreign_table),
466                        postrust_sql::escape_ident(child_alias),
467                        postrust_sql::escape_ident(&junction.schema),
468                        postrust_sql::escape_ident(&junction.table),
469                        postrust_sql::escape_ident(&junction_alias),
470                        junction.child_join(&junction_alias, child_alias),
471                        junction.parent_correlation(&junction_alias, parent_alias),
472                    )
473                }
474                None => format!(
475                    "SELECT {} FROM {}.{} AS {} WHERE {}",
476                    inner_select,
477                    postrust_sql::escape_ident(&self.foreign_schema),
478                    postrust_sql::escape_ident(&self.foreign_table),
479                    postrust_sql::escape_ident(child_alias),
480                    self.correlation(parent_alias, child_alias),
481                ),
482            },
483        };
484
485        // Filters written against the embedded resource (`clients.id=eq.1`)
486        // narrow the children, exactly as they would if the child had been
487        // requested on its own.
488        if let Some(child_where) = child_where {
489            inner.push_str(" AND ");
490            inner.push_str(child_where);
491        }
492
493        if let Some(order_by) = order_by {
494            inner.push_str(" ORDER BY ");
495            inner.push_str(order_by);
496        }
497
498        // A to-one relationship takes the first row; a to-many takes them all.
499        // The limit bounds rows per parent here, which is what a client asking
500        // for a page of children means, and is stricter than the row cap the
501        // two-query form could apply.
502        if let Some(limit) = limit {
503            inner.push_str(&format!(" LIMIT {}", limit));
504        } else if !self.is_list {
505            inner.push_str(" LIMIT 1");
506        }
507
508        if offset > 0 {
509            inner.push_str(&format!(" OFFSET {}", offset));
510        }
511
512        Ok(inner)
513    }
514
515    /// A correlated subselect that yields this relationship as one JSON column.
516    ///
517    /// The rows come from [`correlated_rows`]; this decides how they are
518    /// rendered -- an array for a to-many relationship, one object or null for
519    /// a to-one.
520    #[allow(clippy::too_many_arguments)] // one parameter per SQL clause
521    pub fn embed_expression(
522        &self,
523        parent_alias: &str,
524        parent_row: &str,
525        child_alias: &str,
526        inner_select: &str,
527        limit: Option<i64>,
528        offset: i64,
529        child_where: Option<&str>,
530        order_by: Option<&str>,
531        function_arguments: Option<&str>,
532        rendering: EmbedRendering,
533    ) -> Result<String> {
534        let inner = self.correlated_rows(
535            parent_alias,
536            parent_row,
537            child_alias,
538            inner_select,
539            limit,
540            offset,
541            child_where,
542            order_by,
543            function_arguments,
544        )?;
545
546        let alias = postrust_sql::escape_ident(&format!("{}_j", child_alias));
547
548        Ok(if self.is_list {
549            // An empty array rather than null, so the shape does not depend on
550            // whether the parent happens to have children.
551            format!(
552                "COALESCE((SELECT json_agg(row_to_json({alias})) FROM ({inner}) {alias}), '[]'::json){cast}",
553                alias = alias,
554                inner = inner,
555                cast = rendering.cast()
556            )
557        } else {
558            format!(
559                "(SELECT row_to_json({alias}) FROM ({inner}) {alias}){cast}",
560                alias = alias,
561                inner = inner,
562                cast = rendering.cast()
563            )
564        })
565    }
566
567    /// A correlated subselect that summarises this relationship as one JSON
568    /// column.
569    ///
570    /// `author { articles_aggregate { aggregate { count } } }`. The rows are
571    /// the same correlated set an embed would render, but a `limit` here bounds
572    /// what is *counted* rather than what is returned -- so the aggregate reads
573    /// from the rows as a subquery instead of selecting over them directly.
574    /// The subquery carries the child's own alias, so a predicate or an
575    /// ordering written against it goes on reading the same name.
576    ///
577    /// Parent columns are deliberately left alone: they stay ordinary typed
578    /// columns and are converted to JSON by the same code as an unembedded
579    /// request, so embedding does not change how a NUMERIC or a timestamp is
580    /// rendered.
581    #[allow(clippy::too_many_arguments)] // one parameter per SQL clause
582    pub fn aggregate_expression(
583        &self,
584        parent_alias: &str,
585        parent_row: &str,
586        child_alias: &str,
587        aggregate_select: &str,
588        limit: Option<i64>,
589        offset: i64,
590        child_where: Option<&str>,
591        order_by: Option<&str>,
592        function_arguments: Option<&str>,
593        // A `DISTINCT ON (...) ` prefix, or empty. It belongs to the rows
594        // being summarised rather than to the summary: counting the distinct
595        // titles means counting the rows that survive it.
596        distinct_on: &str,
597        rendering: EmbedRendering,
598    ) -> Result<String> {
599        let child = postrust_sql::escape_ident(child_alias);
600        let rows = self.correlated_rows(
601            parent_alias,
602            parent_row,
603            child_alias,
604            &format!("{}{}.*", distinct_on, child),
605            limit,
606            offset,
607            child_where,
608            order_by,
609            function_arguments,
610        )?;
611        let alias = postrust_sql::escape_ident(&format!("{}_a", child_alias));
612        Ok(format!(
613            "(SELECT row_to_json({alias}) FROM \
614             (SELECT {select} FROM ({rows}) AS {child}) {alias}){cast}",
615            alias = alias,
616            select = aggregate_select,
617            rows = rows,
618            child = child,
619            cast = rendering.cast()
620        ))
621    }
622
623    /// A scalar subselect yielding one column of the related row.
624    ///
625    /// This is what `order=clients(name)` orders by: the parent has no such
626    /// column, so the value is fetched per parent row exactly as the embed
627    /// itself is. A to-many relationship has no single value to order by, so
628    /// the first row wins -- which is what `LIMIT 1` says and what PostgREST
629    /// does.
630    pub fn order_expression(
631        &self,
632        parent_alias: &str,
633        parent_row: &str,
634        child_alias: &str,
635        column_sql: &str,
636    ) -> String {
637        let source = match (&self.function, &self.junction) {
638            (Some(function), _) => format!(
639                "{}.{}({}) AS {} WHERE true",
640                postrust_sql::escape_ident(&function.schema),
641                postrust_sql::escape_ident(&function.name),
642                parent_row,
643                postrust_sql::escape_ident(child_alias),
644            ),
645            (None, Some(junction)) => {
646                let junction_alias = format!("{}_j", child_alias);
647                format!(
648                    "{}.{} AS {} JOIN {}.{} AS {} ON {} WHERE {}",
649                    postrust_sql::escape_ident(&self.foreign_schema),
650                    postrust_sql::escape_ident(&self.foreign_table),
651                    postrust_sql::escape_ident(child_alias),
652                    postrust_sql::escape_ident(&junction.schema),
653                    postrust_sql::escape_ident(&junction.table),
654                    postrust_sql::escape_ident(&junction_alias),
655                    junction.child_join(&junction_alias, child_alias),
656                    junction.parent_correlation(&junction_alias, parent_alias),
657                )
658            }
659            (None, None) => format!(
660                "{}.{} AS {} WHERE {}",
661                postrust_sql::escape_ident(&self.foreign_schema),
662                postrust_sql::escape_ident(&self.foreign_table),
663                postrust_sql::escape_ident(child_alias),
664                self.correlation(parent_alias, child_alias),
665            ),
666        };
667
668        format!("(SELECT {} FROM {} LIMIT 1)", column_sql, source)
669    }
670
671    /// An `EXISTS` predicate restricting parents to those with a matching child.
672    ///
673    /// This is what `!inner` means. The embed expression on its own only
674    /// decides what the relationship column contains; without this the parent
675    /// row survives even when the relationship matched nothing, which is a
676    /// left join. `child_where` should be the same predicate given to
677    /// [`Self::embed_expression`] -- placeholders may be referenced from both
678    /// places, so no parameter needs binding twice.
679    pub fn inner_join_predicate(
680        &self,
681        parent_alias: &str,
682        parent_row: &str,
683        child_alias: &str,
684        child_where: Option<&str>,
685    ) -> String {
686        let mut predicate = match &self.function {
687            Some(function) => format!(
688                "EXISTS (SELECT 1 FROM {}.{}({}) AS {} WHERE true",
689                postrust_sql::escape_ident(&function.schema),
690                postrust_sql::escape_ident(&function.name),
691                parent_row,
692                postrust_sql::escape_ident(child_alias),
693            ),
694            None => match &self.junction {
695                Some(junction) => {
696                    let junction_alias = format!("{}_j", child_alias);
697                    format!(
698                        "EXISTS (SELECT 1 FROM {}.{} AS {} JOIN {}.{} AS {} ON {} WHERE {}",
699                        postrust_sql::escape_ident(&self.foreign_schema),
700                        postrust_sql::escape_ident(&self.foreign_table),
701                        postrust_sql::escape_ident(child_alias),
702                        postrust_sql::escape_ident(&junction.schema),
703                        postrust_sql::escape_ident(&junction.table),
704                        postrust_sql::escape_ident(&junction_alias),
705                        junction.child_join(&junction_alias, child_alias),
706                        junction.parent_correlation(&junction_alias, parent_alias),
707                    )
708                }
709                None => format!(
710                    "EXISTS (SELECT 1 FROM {}.{} AS {} WHERE {}",
711                    postrust_sql::escape_ident(&self.foreign_schema),
712                    postrust_sql::escape_ident(&self.foreign_table),
713                    postrust_sql::escape_ident(child_alias),
714                    self.correlation(parent_alias, child_alias),
715                ),
716            },
717        };
718
719        if let Some(child_where) = child_where {
720            predicate.push_str(" AND ");
721            predicate.push_str(child_where);
722        }
723
724        predicate.push(')');
725        predicate
726    }
727
728    /// The child projection list: the requested columns plus the join column.
729    ///
730    /// Column names come from the client, so each is escaped rather than
731    /// interpolated bare. Anything that is not a plain column reference -- a
732    /// nested relation, say -- is not a column and is skipped.
733    fn projection(&self, columns: &[String]) -> String {
734        if columns.is_empty() {
735            return "*".to_string();
736        }
737
738        let mut wanted: Vec<&str> = Vec::with_capacity(columns.len() + 1);
739        for column in columns {
740            if !wanted.contains(&column.as_str()) {
741                wanted.push(column);
742            }
743        }
744        if !wanted.contains(&self.foreign_column.as_str()) {
745            wanted.push(&self.foreign_column);
746        }
747
748        wanted
749            .into_iter()
750            .map(postrust_sql::escape_ident)
751            .collect::<Vec<_>>()
752            .join(", ")
753    }
754}
755
756/// Reject anything that is not a bare type name, since it is interpolated.
757fn castable_type_name(pg_type: &str) -> Option<&str> {
758    if pg_type.is_empty() {
759        return None;
760    }
761    if pg_type
762        .chars()
763        .all(|c| c.is_ascii_alphanumeric() || c == '_')
764    {
765        Some(pg_type)
766    } else {
767        None
768    }
769}
770
771/// Render a JSON value as the text form used to match join keys.
772///
773/// Keys are compared as text and cast by PostgreSQL, so a numeric key must not
774/// arrive quoted the way `to_string` would render a JSON string.
775pub fn key_to_text(value: &serde_json::Value) -> Option<String> {
776    match value {
777        serde_json::Value::Null => None,
778        serde_json::Value::String(s) => Some(s.clone()),
779        other => Some(other.to_string()),
780    }
781}
782
783/// Group related rows by the value of their join column.
784/// Build the grouping from rows PostgreSQL has already grouped.
785///
786/// Each row is the join key as JSON and a JSON array of that key's children.
787/// The key goes through `key_to_text`, the same rendering the parent side uses,
788/// so the two agree for every column type.
789pub fn group_from_aggregated(
790    rows: Vec<(serde_json::Value, serde_json::Value)>,
791) -> HashMap<String, Vec<serde_json::Value>> {
792    let mut grouped: HashMap<String, Vec<serde_json::Value>> = HashMap::with_capacity(rows.len());
793
794    for (key, children) in rows {
795        let key = key_to_text(&key).unwrap_or_default();
796        let children = match children {
797            serde_json::Value::Array(items) => items,
798            // json_agg only ever yields an array or null.
799            _ => Vec::new(),
800        };
801        grouped.entry(key).or_default().extend(children);
802    }
803
804    grouped
805}
806
807/// Group child rows by the value of one of their columns.
808///
809/// The join is done in SQL and the rows come back flat; this puts each child
810/// under the parent it belongs to. Keyed by the rendered value, so a parent
811/// with no children gets no entry rather than an empty one.
812pub fn group_by_key(
813    children: Vec<serde_json::Value>,
814    foreign_column: &str,
815) -> HashMap<String, Vec<serde_json::Value>> {
816    let mut grouped: HashMap<String, Vec<serde_json::Value>> = HashMap::new();
817
818    for child in children {
819        let key = child
820            .get(foreign_column)
821            .and_then(key_to_text)
822            .unwrap_or_default();
823        grouped.entry(key).or_default().push(child);
824    }
825
826    grouped
827}
828
829/// Attach grouped children onto a parent row under `field_name`.
830///
831/// A to-one relationship yields the first match or `null`; a to-many yields an
832/// array, empty when there are no matches, so the shape of the response does
833/// not depend on whether data happens to exist.
834pub fn attach_to_parent(
835    parent: &mut serde_json::Value,
836    field_name: &str,
837    plan: &EmbedPlan,
838    grouped: &HashMap<String, Vec<serde_json::Value>>,
839) {
840    let key = parent.get(&plan.local_column).and_then(key_to_text);
841
842    let matches = key
843        .as_ref()
844        .and_then(|k| grouped.get(k))
845        .cloned()
846        .unwrap_or_default();
847
848    let value = if plan.is_list {
849        serde_json::Value::Array(matches)
850    } else {
851        matches
852            .into_iter()
853            .next()
854            .unwrap_or(serde_json::Value::Null)
855    };
856
857    if let Some(object) = parent.as_object_mut() {
858        object.insert(field_name.to_string(), value);
859    }
860}
861
862/// Merge a related resource's columns into the parent row itself.
863///
864/// This is what the `...` of `select=title,...directors(name)` means. Where
865/// [`attach_to_parent`] puts the related rows under a key of their own, a
866/// spread lifts their columns into the parent object, so the result stays flat.
867///
868/// Spreading a to-many relationship gives each column an array of that
869/// column's value across the matched rows, rather than an array of objects.
870///
871/// `columns` pairs each requested column with the key it should appear under,
872/// which differ when the client aliased it: `...clients(client_name:name)`
873/// reads `name` and writes `client_name`. Listing them explicitly is also what
874/// lets a parent with no matching child still carry the right keys with null
875/// values, since there is no child row to read the names off.
876///
877/// An empty list spreads nothing, which is what `...clients()` means. Falling
878/// back to every column the child happens to have would be actively wrong: the
879/// related table's own `id` would land on top of the parent's.
880pub fn spread_into_parent(
881    parent: &mut serde_json::Value,
882    plan: &EmbedPlan,
883    grouped: &HashMap<String, Vec<serde_json::Value>>,
884    columns: &[(String, String)],
885) {
886    let key = parent.get(&plan.local_column).and_then(key_to_text);
887    let matches = key
888        .as_ref()
889        .and_then(|k| grouped.get(k))
890        .cloned()
891        .unwrap_or_default();
892
893    let Some(object) = parent.as_object_mut() else {
894        return;
895    };
896
897    for (source, output) in columns {
898        let value = if plan.is_list {
899            serde_json::Value::Array(
900                matches
901                    .iter()
902                    .map(|child| {
903                        child
904                            .get(source)
905                            .cloned()
906                            .unwrap_or(serde_json::Value::Null)
907                    })
908                    .collect(),
909            )
910        } else {
911            matches
912                .first()
913                .and_then(|child| child.get(source).cloned())
914                .unwrap_or(serde_json::Value::Null)
915        };
916        object.insert(output.clone(), value);
917    }
918}
919
920/// Collect the distinct, non-null join keys of a set of parent rows.
921pub fn parent_keys(parents: &[serde_json::Value], local_column: &str) -> Vec<String> {
922    let mut seen = std::collections::HashSet::new();
923    let mut keys = Vec::new();
924
925    for parent in parents {
926        if let Some(key) = parent.get(local_column).and_then(key_to_text) {
927            if seen.insert(key.clone()) {
928                keys.push(key);
929            }
930        }
931    }
932
933    keys
934}
935
936#[cfg(test)]
937mod tests {
938    use super::*;
939
940    fn plan(is_list: bool) -> EmbedPlan {
941        EmbedPlan {
942            row_argument: None,
943            local_column: "id".into(),
944            foreign_column: "user_id".into(),
945            foreign_column_type: "int4".into(),
946            foreign_schema: "public".into(),
947            foreign_table: "posts".into(),
948            is_list,
949            columns: vec![("id".into(), "user_id".into())],
950            junction: None,
951            function: None,
952        }
953    }
954
955    #[test]
956    fn children_sql_binds_keys_as_a_cast_array() {
957        let sql = plan(true).children_sql(None, &[]).unwrap();
958        assert_eq!(
959            sql,
960            "SELECT row_to_json(t) FROM (SELECT * FROM \"public\".\"posts\" \
961             WHERE \"user_id\" = ANY($1::int4[])) t"
962        );
963    }
964
965    #[test]
966    fn embed_expression_aggregates_a_to_many_relation() {
967        let sql = plan(true)
968            .embed_expression(
969                "p",
970                "\"p\"",
971                "posts",
972                r#""id", "title""#,
973                None,
974                0,
975                None,
976                None,
977                None,
978                EmbedRendering::Verbatim,
979            )
980            .unwrap();
981
982        assert!(sql.starts_with("COALESCE((SELECT json_agg("), "{}", sql);
983        // Correlated on the parent, so there is no second query and no bound
984        // array of keys.
985        assert!(sql.contains(r#""posts"."user_id" = "p"."id""#), "{}", sql);
986        assert!(
987            sql.contains(r#"AS "posts""#),
988            "the child table is aliased: {}",
989            sql
990        );
991        assert!(!sql.contains("ANY("), "{}", sql);
992        // An absent relation is an empty array, not null.
993        assert!(sql.contains("'[]'::json"), "{}", sql);
994    }
995
996    #[test]
997    fn embed_expression_takes_one_row_for_a_to_one_relation() {
998        let sql = plan(false)
999            .embed_expression(
1000                "p",
1001                "\"p\"",
1002                "author",
1003                r#""id""#,
1004                None,
1005                0,
1006                None,
1007                None,
1008                None,
1009                EmbedRendering::Verbatim,
1010            )
1011            .unwrap();
1012
1013        assert!(sql.contains("row_to_json"), "{}", sql);
1014        assert!(!sql.contains("json_agg"), "{}", sql);
1015        assert!(
1016            sql.contains("LIMIT 1"),
1017            "a to-one relation yields one row: {}",
1018            sql
1019        );
1020    }
1021
1022    #[test]
1023    fn embed_expression_limits_rows_per_parent() {
1024        let sql = plan(true)
1025            .embed_expression(
1026                "p",
1027                "\"p\"",
1028                "posts",
1029                r#""id""#,
1030                Some(25),
1031                0,
1032                None,
1033                None,
1034                None,
1035                EmbedRendering::Verbatim,
1036            )
1037            .unwrap();
1038        assert!(sql.contains("LIMIT 25"), "{}", sql);
1039    }
1040
1041    #[test]
1042    fn children_sql_projects_only_the_requested_columns() {
1043        let sql = plan(true)
1044            .children_sql(None, &["title".to_string(), "body".to_string()])
1045            .unwrap();
1046
1047        assert!(
1048            sql.contains(r#"SELECT "title", "body", "user_id" FROM"#),
1049            "{}",
1050            sql
1051        );
1052        assert!(
1053            !sql.contains("SELECT *"),
1054            "an unrequested column should not be read at all: {}",
1055            sql
1056        );
1057    }
1058
1059    #[test]
1060    fn children_sql_always_includes_the_join_column() {
1061        // The grouping keys off the join column, so it has to come back even
1062        // when the client did not ask for it.
1063        let sql = plan(true)
1064            .children_sql(None, &["title".to_string()])
1065            .unwrap();
1066        assert!(sql.contains(r#""user_id""#), "{}", sql);
1067    }
1068
1069    #[test]
1070    fn children_sql_does_not_repeat_the_join_column() {
1071        let sql = plan(true)
1072            .children_sql(None, &["user_id".to_string(), "title".to_string()])
1073            .unwrap();
1074        assert_eq!(
1075            sql.matches(r#""user_id""#).count(),
1076            2,
1077            "expected the column once in the projection and once in the WHERE: {}",
1078            sql
1079        );
1080    }
1081
1082    #[test]
1083    fn children_sql_escapes_column_names() {
1084        // Column names reach here from the client.
1085        let sql = plan(true)
1086            .children_sql(None, &[r#"ev"il"#.to_string()])
1087            .unwrap();
1088        assert!(sql.contains(r#""ev""il""#), "{}", sql);
1089    }
1090
1091    #[test]
1092    fn children_sql_falls_back_to_every_column() {
1093        let sql = plan(true).children_sql(None, &[]).unwrap();
1094        assert!(sql.contains("SELECT * FROM"), "{}", sql);
1095    }
1096
1097    #[test]
1098    fn children_sql_applies_a_limit() {
1099        let sql = plan(true).children_sql(Some(25), &[]).unwrap();
1100        assert!(sql.contains("LIMIT 25"), "{}", sql);
1101    }
1102
1103    #[test]
1104    fn children_sql_rejects_a_non_plain_type_name() {
1105        let mut p = plan(true);
1106        p.foreign_column_type = "int4; DROP TABLE users".into();
1107        assert!(p.children_sql(None, &[]).is_err());
1108    }
1109
1110    #[test]
1111    fn keys_are_rendered_without_json_quoting() {
1112        assert_eq!(key_to_text(&serde_json::json!(7)), Some("7".to_string()));
1113        assert_eq!(
1114            key_to_text(&serde_json::json!("abc")),
1115            Some("abc".to_string())
1116        );
1117        assert_eq!(key_to_text(&serde_json::Value::Null), None);
1118    }
1119
1120    #[test]
1121    fn parent_keys_are_distinct_and_skip_nulls() {
1122        let parents = vec![
1123            serde_json::json!({"id": 1}),
1124            serde_json::json!({"id": 2}),
1125            serde_json::json!({"id": 1}),
1126            serde_json::json!({"id": null}),
1127        ];
1128        assert_eq!(parent_keys(&parents, "id"), vec!["1", "2"]);
1129    }
1130
1131    #[test]
1132    fn to_many_attaches_an_array_and_empty_when_absent() {
1133        let grouped = group_by_key(vec![serde_json::json!({"id": 10, "user_id": 1})], "user_id");
1134
1135        let mut matched = serde_json::json!({"id": 1});
1136        attach_to_parent(&mut matched, "posts", &plan(true), &grouped);
1137        assert_eq!(matched["posts"].as_array().map(|a| a.len()), Some(1));
1138
1139        let mut unmatched = serde_json::json!({"id": 2});
1140        attach_to_parent(&mut unmatched, "posts", &plan(true), &grouped);
1141        assert_eq!(
1142            unmatched["posts"],
1143            serde_json::json!([]),
1144            "a to-many with no matches must still be an array"
1145        );
1146    }
1147
1148    #[test]
1149    fn to_one_attaches_an_object_or_null() {
1150        let grouped = group_by_key(vec![serde_json::json!({"id": 10, "user_id": 1})], "user_id");
1151
1152        let mut matched = serde_json::json!({"id": 1});
1153        attach_to_parent(&mut matched, "author", &plan(false), &grouped);
1154        assert_eq!(matched["author"]["id"], serde_json::json!(10));
1155
1156        let mut unmatched = serde_json::json!({"id": 2});
1157        attach_to_parent(&mut unmatched, "author", &plan(false), &grouped);
1158        assert_eq!(unmatched["author"], serde_json::Value::Null);
1159    }
1160
1161    #[test]
1162    fn spread_lifts_a_to_one_relation_into_the_parent() {
1163        let mut grouped = HashMap::new();
1164        grouped.insert(
1165            "1".to_string(),
1166            vec![serde_json::json!({"first_name": "Ada", "last_name": "L"})],
1167        );
1168
1169        let mut parent = serde_json::json!({"id": 1, "title": "Notes"});
1170        spread_into_parent(
1171            &mut parent,
1172            &plan(false),
1173            &grouped,
1174            &[
1175                ("first_name".into(), "first_name".into()),
1176                ("last_name".into(), "last_name".into()),
1177            ],
1178        );
1179
1180        // Flat, not nested under a key of its own.
1181        assert_eq!(parent["first_name"], "Ada");
1182        assert_eq!(parent["last_name"], "L");
1183        assert_eq!(parent["title"], "Notes");
1184        assert!(parent.get("posts").is_none());
1185    }
1186
1187    #[test]
1188    fn spread_without_a_match_still_carries_the_requested_keys() {
1189        let mut parent = serde_json::json!({"id": 9});
1190        spread_into_parent(
1191            &mut parent,
1192            &plan(false),
1193            &HashMap::new(),
1194            &[("first_name".into(), "first_name".into())],
1195        );
1196        assert_eq!(parent["first_name"], serde_json::Value::Null);
1197    }
1198
1199    #[test]
1200    fn spread_of_a_to_many_relation_gives_a_column_per_array() {
1201        let mut grouped = HashMap::new();
1202        grouped.insert(
1203            "1".to_string(),
1204            vec![
1205                serde_json::json!({"title": "a"}),
1206                serde_json::json!({"title": "b"}),
1207            ],
1208        );
1209
1210        let mut parent = serde_json::json!({"id": 1});
1211        spread_into_parent(
1212            &mut parent,
1213            &plan(true),
1214            &grouped,
1215            &[("title".into(), "title".into())],
1216        );
1217
1218        assert_eq!(parent["title"], serde_json::json!(["a", "b"]));
1219    }
1220
1221    #[test]
1222    fn spread_without_a_column_list_adds_nothing() {
1223        // `...clients()` spreads no columns. Taking every column the child has
1224        // would drop the related table's `id` on top of the parent's.
1225        let mut grouped = HashMap::new();
1226        grouped.insert("1".to_string(), vec![serde_json::json!({"id": 7, "b": 2})]);
1227
1228        let mut parent = serde_json::json!({"id": 1});
1229        spread_into_parent(&mut parent, &plan(false), &grouped, &[]);
1230
1231        assert_eq!(parent, serde_json::json!({"id": 1}));
1232    }
1233}