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;