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