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>>;