Skip to main content

fraiseql_cli/schema/
native_joins.rs

1//! The native columns a relationship joins on, proven by the database (#1587).
2//!
3//! A composed read correlates an embedded level to its parent by stored document keys unless
4//! the compiled schema says which SQL columns join them. `fraiseql compile --database` reads
5//! that from `pg_tviews`' registry: a relationship whose child and parent types read TVIEWs,
6//! and whose declared columns are a reference the registry publishes (a child column holding
7//! the parent's identity, proven by a `FOREIGN KEY` and indexed). Nothing is guessed: a type
8//! reading a plain view, or columns the registry does not link, keeps its stored keys.
9
10use fraiseql_core::schema::{CompiledSchema, NativeJoin};
11
12/// One reference the registry publishes, between two relations as the schema names them.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct TviewReference {
15    /// The `sql_source` of the child, whose column references the parent.
16    pub child:          String,
17    /// The `sql_source` of the parent.
18    pub parent:         String,
19    /// The child's reference column (`fk_post`).
20    pub foreign_key:    String,
21    /// The parent's identity column (`pk_post`).
22    pub referenced_key: String,
23}
24
25/// Record on each relationship of `schema` the native columns `references` prove it joins on.
26///
27/// A relationship names SQL columns (`foreign_key` on the child, `referenced_key` on the
28/// parent); it gets a proof only when a reference between the two types' views holds exactly
29/// those two columns. Nothing else is taken, not even the only reference between the views:
30/// it may be another relationship's. Returns how many relationships were given one.
31pub fn apply_native_joins(schema: &mut CompiledSchema, references: &[TviewReference]) -> usize {
32    let sources: std::collections::HashMap<String, String> = schema
33        .types
34        .iter()
35        .map(|t| (t.name.to_string(), t.sql_source.to_string()))
36        .collect();
37    let mut given = 0;
38    for declaring in &mut schema.types {
39        for rel in &mut declaring.relationships {
40            // The side holding the foreign key is the child: the target of a list, the
41            // declaring type of a to-one embed.
42            let (child, parent) = if rel.cardinality.is_to_one() {
43                (declaring.name.as_str(), rel.target_type.as_str())
44            } else {
45                (rel.target_type.as_str(), declaring.name.as_str())
46            };
47            let (Some(child), Some(parent)) = (sources.get(child), sources.get(parent)) else {
48                continue;
49            };
50            rel.native_join = references
51                .iter()
52                .find(|r| {
53                    &r.child == child
54                        && &r.parent == parent
55                        && r.foreign_key == rel.foreign_key
56                        && r.referenced_key == rel.referenced_key
57                })
58                .map(|r| NativeJoin {
59                    foreign_key:    r.foreign_key.clone(),
60                    referenced_key: r.referenced_key.clone(),
61                    child_view:     child.clone(),
62                    parent_view:    parent.clone(),
63                });
64            given += usize::from(rel.native_join.is_some());
65        }
66    }
67    given
68}
69
70#[cfg(test)]
71#[path = "native_joins_tests.rs"]
72mod tests;