Skip to main content

postrust_core/schema_cache/
relationship.rs

1//! Relationship types for resource embedding.
2
3use crate::api_request::QualifiedIdentifier;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// A relationship between tables.
8#[derive(Clone, Debug, Serialize, Deserialize)]
9pub enum Relationship {
10    /// Foreign key relationship
11    ForeignKey {
12        /// Source table
13        table: QualifiedIdentifier,
14        /// Target table
15        foreign_table: QualifiedIdentifier,
16        /// Whether this is a self-referential relationship
17        is_self: bool,
18        /// Relationship cardinality
19        cardinality: Cardinality,
20        /// Whether the source is a view
21        table_is_view: bool,
22        /// Whether the target is a view
23        foreign_table_is_view: bool,
24        /// FK constraint name
25        constraint_name: String,
26    },
27    /// Computed relationship (from a function)
28    Computed {
29        /// Function that computes the relationship
30        function: QualifiedIdentifier,
31        /// Source table
32        table: QualifiedIdentifier,
33        /// Target table
34        foreign_table: QualifiedIdentifier,
35        /// Alias for the relationship
36        table_alias: QualifiedIdentifier,
37        /// Whether this returns a single row
38        to_one: bool,
39        /// Whether this is self-referential
40        is_self: bool,
41        /// The name of the parameter that takes the parent's row.
42        ///
43        /// Usually the only one, and then the call is positional. A function
44        /// that also takes a search term or the caller's session has the row
45        /// somewhere among its parameters and is called by name, which is why
46        /// the name is kept rather than the position.
47        #[serde(default)]
48        row_argument: Option<String>,
49    },
50}
51
52impl Relationship {
53    /// Get the foreign table for this relationship.
54    pub fn foreign_table(&self) -> &QualifiedIdentifier {
55        match self {
56            Self::ForeignKey { foreign_table, .. } => foreign_table,
57            Self::Computed { foreign_table, .. } => foreign_table,
58        }
59    }
60
61    /// Check if this relationship returns a single row.
62    pub fn is_to_one(&self) -> bool {
63        match self {
64            Self::ForeignKey { cardinality, .. } => matches!(
65                cardinality,
66                Cardinality::M2O { .. } | Cardinality::O2O { .. }
67            ),
68            Self::Computed { to_one, .. } => *to_one,
69        }
70    }
71
72    /// The name of the foreign key constraint behind this relationship.
73    ///
74    /// Empty for a many-to-many, which is two constraints rather than one, and
75    /// for a computed relationship, which has none.
76    pub fn constraint_name(&self) -> &str {
77        match self {
78            Self::ForeignKey { cardinality, .. } => match cardinality {
79                Cardinality::O2M { constraint, .. }
80                | Cardinality::M2O { constraint, .. }
81                | Cardinality::O2O { constraint, .. } => constraint,
82                Cardinality::M2M(_) => "",
83            },
84            Self::Computed { .. } => "",
85        }
86    }
87
88    /// How PostgREST names this relationship's cardinality.
89    pub fn cardinality_name(&self) -> &'static str {
90        match self {
91            Self::ForeignKey { cardinality, .. } => match cardinality {
92                Cardinality::O2M { .. } => "one-to-many",
93                Cardinality::M2O { .. } => "many-to-one",
94                Cardinality::O2O { .. } => "one-to-one",
95                Cardinality::M2M(_) => "many-to-many",
96            },
97            Self::Computed { to_one, .. } => match to_one {
98                true => "many-to-one",
99                false => "one-to-many",
100            },
101        }
102    }
103
104    /// How this relationship reads when several of them cannot be told apart.
105    ///
106    /// `message_sender_fkey using message(sender) and person_detail(id)` --
107    /// the constraint, then each side with the columns it joins on, which is
108    /// what a client needs to write the hint that disambiguates it.
109    pub fn describe(&self) -> String {
110        match self {
111            // A junction is described by the table it joins through and the
112            // two constraints that make it one -- there is no single
113            // constraint to name, and the empty string that came out instead
114            // told a client nothing about which relationship it had found.
115            Self::ForeignKey {
116                cardinality: Cardinality::M2M(junction),
117                ..
118            } => {
119                // Both constraints belong to the junction table, so both are
120                // named with the junction's own columns. The two lists are
121                // oriented in opposite directions -- `source_columns` runs
122                // (source, through) and `target_columns` runs (through,
123                // target) -- so the junction's side is the second element of
124                // one and the first of the other. Taking the second of both
125                // described the far constraint with the column it points at
126                // rather than the column it is on:
127                // `whatev_jobs_project_id_1_fkey(id)` for a constraint that is
128                // on `project_id_1`.
129                let joined = |columns: Vec<String>| columns.join(", ");
130                format!(
131                    "{} using {}({}) and {}({})",
132                    junction.table.name,
133                    junction.constraint1,
134                    joined(
135                        junction
136                            .source_columns
137                            .iter()
138                            .map(|(_, through)| through.clone())
139                            .collect()
140                    ),
141                    junction.constraint2,
142                    joined(
143                        junction
144                            .target_columns
145                            .iter()
146                            .map(|(through, _)| through.clone())
147                            .collect()
148                    ),
149                )
150            }
151            Self::ForeignKey {
152                table,
153                foreign_table,
154                cardinality,
155                ..
156            } => {
157                let columns = cardinality.columns();
158                let local = columns
159                    .iter()
160                    .map(|(local, _)| local.clone())
161                    .collect::<Vec<_>>()
162                    .join(", ");
163                let foreign = columns
164                    .iter()
165                    .map(|(_, foreign)| foreign.clone())
166                    .collect::<Vec<_>>()
167                    .join(", ");
168                format!(
169                    "{} using {}({}) and {}({})",
170                    self.constraint_name(),
171                    table.name,
172                    local,
173                    foreign_table.name,
174                    foreign
175                )
176            }
177            Self::Computed { function, .. } => function.name.clone(),
178        }
179    }
180
181    /// The `!hint` that picks this relationship out from its rivals.
182    ///
183    /// The constraint for a foreign key, the junction table for a
184    /// many-to-many: whichever name is the one a client would have to write to
185    /// mean this relationship and no other.
186    pub fn disambiguator(&self) -> String {
187        match self {
188            Self::ForeignKey {
189                cardinality: Cardinality::M2M(junction),
190                ..
191            } => junction.table.name.clone(),
192            Self::ForeignKey {
193                constraint_name, ..
194            } => constraint_name.clone(),
195            Self::Computed { function, .. } => function.name.clone(),
196        }
197    }
198
199    /// Whether a hint names this relationship.
200    ///
201    /// A hint may be the constraint, the column that joins them, or the table
202    /// on the far side -- whichever the client found unambiguous.
203    pub fn matches_hint(&self, hint: &str) -> bool {
204        // Both columns of the join count, because which side carries the
205        // telling name depends on the direction: `whatev_jobs!site_id_1` names
206        // a column on the jobs, which is the far side when the request starts
207        // from the sites.
208        //
209        // A self-referential relationship appears twice, once each way, and
210        // both directions join on the same pair of columns -- so matching
211        // either side would match both and make every hint ambiguous. The far
212        // side is the telling one: `family_tree!parent` asks for the rows
213        // whose `parent` points here.
214        let self_referential = matches!(self, Self::ForeignKey { is_self, .. } if *is_self);
215        let columns_match = self
216            .join_columns()
217            .iter()
218            .any(|(local, foreign)| foreign == hint || (!self_referential && local == hint));
219
220        self.constraint_name() == hint
221            || self.foreign_table().name == hint
222            || columns_match
223            || match self {
224                Self::Computed { function, .. } => function.name == hint,
225                // A junction is named by the table it joins through, or by
226                // either of the constraints that make it one.
227                Self::ForeignKey {
228                    cardinality: Cardinality::M2M(junction),
229                    ..
230                } => {
231                    junction.table.name == hint
232                        || junction.constraint1 == hint
233                        || junction.constraint2 == hint
234                        || junction
235                            .source_columns
236                            .iter()
237                            .chain(junction.target_columns.iter())
238                            .any(|(a, b)| a == hint || b == hint)
239                }
240                Self::ForeignKey { .. } => false,
241            }
242    }
243
244    /// Whether this is a self-referential foreign key.
245    pub fn is_self_referential(&self) -> bool {
246        matches!(self, Self::ForeignKey { is_self, .. } if *is_self)
247    }
248
249    /// Whether this relationship yields many rows per row of its own table.
250    pub fn is_one_to_many(&self) -> bool {
251        matches!(
252            self,
253            Self::ForeignKey {
254                cardinality: Cardinality::O2M { .. },
255                ..
256            }
257        )
258    }
259
260    /// The single column this side joins on, where it joins on exactly one.
261    pub fn single_local_column(&self) -> Option<&str> {
262        match self.join_columns().len() {
263            1 => match self {
264                Self::ForeignKey { cardinality, .. } => match cardinality {
265                    Cardinality::O2M { columns, .. }
266                    | Cardinality::M2O { columns, .. }
267                    | Cardinality::O2O { columns, .. } => Some(columns[0].0.as_str()),
268                    Cardinality::M2M(_) => None,
269                },
270                Self::Computed { .. } => None,
271            },
272            _ => None,
273        }
274    }
275
276    /// The single column the other side joins on, where there is exactly one.
277    pub fn single_foreign_column(&self) -> Option<&str> {
278        match self {
279            Self::ForeignKey { cardinality, .. } => match cardinality {
280                Cardinality::O2M { columns, .. }
281                | Cardinality::M2O { columns, .. }
282                | Cardinality::O2O { columns, .. }
283                    if columns.len() == 1 =>
284                {
285                    Some(columns[0].1.as_str())
286                }
287                _ => None,
288            },
289            Self::Computed { .. } => None,
290        }
291    }
292
293    /// Get the join columns for this relationship.
294    pub fn join_columns(&self) -> Vec<(String, String)> {
295        match self {
296            Self::ForeignKey { cardinality, .. } => cardinality.columns(),
297            Self::Computed { .. } => vec![],
298        }
299    }
300}
301
302/// Relationship cardinality.
303#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
304pub enum Cardinality {
305    /// One-to-Many: parent has many children
306    O2M {
307        constraint: String,
308        columns: Vec<(String, String)>,
309    },
310    /// Many-to-One: child has one parent
311    M2O {
312        constraint: String,
313        columns: Vec<(String, String)>,
314    },
315    /// One-to-One
316    O2O {
317        constraint: String,
318        columns: Vec<(String, String)>,
319        /// Whether this table is the parent in the relationship
320        is_parent: bool,
321    },
322    /// Many-to-Many (via junction table)
323    M2M(Junction),
324}
325
326impl Cardinality {
327    /// Get the join columns as `(local column, foreign column)` pairs.
328    ///
329    /// "Local" is the table the relationship is stored under, so a pair can be
330    /// used directly to join from the local table to the foreign one without
331    /// having to know the cardinality.
332    pub fn columns(&self) -> Vec<(String, String)> {
333        match self {
334            Self::O2M { columns, .. } => columns.clone(),
335            Self::M2O { columns, .. } => columns.clone(),
336            Self::O2O { columns, .. } => columns.clone(),
337            Self::M2M(junction) => junction.source_columns(),
338        }
339    }
340
341    /// Get the constraint name.
342    pub fn constraint_name(&self) -> &str {
343        match self {
344            Self::O2M { constraint, .. } => constraint,
345            Self::M2O { constraint, .. } => constraint,
346            Self::O2O { constraint, .. } => constraint,
347            Self::M2M(junction) => &junction.constraint1,
348        }
349    }
350}
351
352/// Junction table for M2M relationships.
353#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
354pub struct Junction {
355    /// The junction table
356    pub table: QualifiedIdentifier,
357    /// FK constraint from junction to source
358    pub constraint1: String,
359    /// FK constraint from junction to target
360    pub constraint2: String,
361    /// Columns linking source to junction
362    pub source_columns: Vec<(String, String)>,
363    /// Columns linking junction to target
364    pub target_columns: Vec<(String, String)>,
365}
366
367impl Junction {
368    /// Get the source-side join columns.
369    pub fn source_columns(&self) -> Vec<(String, String)> {
370        self.source_columns.clone()
371    }
372
373    /// Get the target-side join columns.
374    pub fn target_columns(&self) -> Vec<(String, String)> {
375        self.target_columns.clone()
376    }
377}
378
379/// Map of (table, schema) to relationships.
380pub type RelationshipsMap = HashMap<(QualifiedIdentifier, String), Vec<Relationship>>;
381
382#[cfg(test)]
383mod tests {
384    use super::*;
385
386    #[test]
387    fn test_relationship_foreign_table() {
388        let rel = Relationship::ForeignKey {
389            table: QualifiedIdentifier::new("public", "orders"),
390            foreign_table: QualifiedIdentifier::new("public", "users"),
391            is_self: false,
392            cardinality: Cardinality::M2O {
393                constraint: "orders_user_id_fkey".into(),
394                columns: vec![("user_id".into(), "id".into())],
395            },
396            table_is_view: false,
397            foreign_table_is_view: false,
398            constraint_name: "orders_user_id_fkey".into(),
399        };
400
401        assert_eq!(rel.foreign_table().name, "users");
402        assert!(rel.is_to_one());
403    }
404
405    #[test]
406    fn test_cardinality_columns() {
407        let card = Cardinality::O2M {
408            constraint: "users_id_fkey".into(),
409            columns: vec![("id".into(), "user_id".into())],
410        };
411
412        let cols = card.columns();
413        assert_eq!(cols.len(), 1);
414        assert_eq!(cols[0], ("id".into(), "user_id".into()));
415    }
416}
417
418/// A user-defined renderer for a media type.
419///
420/// PostgREST lets a schema override how a media type is produced by declaring
421/// an aggregate whose state type is a domain named after that media type --
422/// `create domain "application/geo+json" as jsonb` and an aggregate over the
423/// table's rows returning it. The aggregate is then applied to the rows the
424/// request selected and its result is the whole response body.
425#[derive(Clone, Debug)]
426pub struct MediaHandler {
427    /// The aggregate to apply.
428    pub aggregate: QualifiedIdentifier,
429    /// The table it renders, or `None` when it takes `anyelement` and so
430    /// renders any of them.
431    pub table: Option<QualifiedIdentifier>,
432    /// The type the handler's media-type domain is a domain over, which is
433    /// what says whether its output is bytes or text.
434    pub base_type: String,
435}
436
437/// Media handlers by (schema, media type).
438pub type MediaHandlerMap = std::collections::HashMap<(String, String), Vec<MediaHandler>>;