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    },
42}
43
44impl Relationship {
45    /// Get the foreign table for this relationship.
46    pub fn foreign_table(&self) -> &QualifiedIdentifier {
47        match self {
48            Self::ForeignKey { foreign_table, .. } => foreign_table,
49            Self::Computed { foreign_table, .. } => foreign_table,
50        }
51    }
52
53    /// Check if this relationship returns a single row.
54    pub fn is_to_one(&self) -> bool {
55        match self {
56            Self::ForeignKey { cardinality, .. } => matches!(
57                cardinality,
58                Cardinality::M2O { .. } | Cardinality::O2O { .. }
59            ),
60            Self::Computed { to_one, .. } => *to_one,
61        }
62    }
63
64    /// Get the join columns for this relationship.
65    pub fn join_columns(&self) -> Vec<(String, String)> {
66        match self {
67            Self::ForeignKey { cardinality, .. } => cardinality.columns(),
68            Self::Computed { .. } => vec![],
69        }
70    }
71}
72
73/// Relationship cardinality.
74#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
75pub enum Cardinality {
76    /// One-to-Many: parent has many children
77    O2M {
78        constraint: String,
79        columns: Vec<(String, String)>,
80    },
81    /// Many-to-One: child has one parent
82    M2O {
83        constraint: String,
84        columns: Vec<(String, String)>,
85    },
86    /// One-to-One
87    O2O {
88        constraint: String,
89        columns: Vec<(String, String)>,
90        /// Whether this table is the parent in the relationship
91        is_parent: bool,
92    },
93    /// Many-to-Many (via junction table)
94    M2M(Junction),
95}
96
97impl Cardinality {
98    /// Get the join columns.
99    pub fn columns(&self) -> Vec<(String, String)> {
100        match self {
101            Self::O2M { columns, .. } => columns.clone(),
102            Self::M2O { columns, .. } => columns.clone(),
103            Self::O2O { columns, .. } => columns.clone(),
104            Self::M2M(junction) => junction.source_columns(),
105        }
106    }
107
108    /// Get the constraint name.
109    pub fn constraint_name(&self) -> &str {
110        match self {
111            Self::O2M { constraint, .. } => constraint,
112            Self::M2O { constraint, .. } => constraint,
113            Self::O2O { constraint, .. } => constraint,
114            Self::M2M(junction) => &junction.constraint1,
115        }
116    }
117}
118
119/// Junction table for M2M relationships.
120#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
121pub struct Junction {
122    /// The junction table
123    pub table: QualifiedIdentifier,
124    /// FK constraint from junction to source
125    pub constraint1: String,
126    /// FK constraint from junction to target
127    pub constraint2: String,
128    /// Columns linking source to junction
129    pub source_columns: Vec<(String, String)>,
130    /// Columns linking junction to target
131    pub target_columns: Vec<(String, String)>,
132}
133
134impl Junction {
135    /// Get the source-side join columns.
136    pub fn source_columns(&self) -> Vec<(String, String)> {
137        self.source_columns.clone()
138    }
139
140    /// Get the target-side join columns.
141    pub fn target_columns(&self) -> Vec<(String, String)> {
142        self.target_columns.clone()
143    }
144}
145
146/// Map of (table, schema) to relationships.
147pub type RelationshipsMap = HashMap<(QualifiedIdentifier, String), Vec<Relationship>>;
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn test_relationship_foreign_table() {
155        let rel = Relationship::ForeignKey {
156            table: QualifiedIdentifier::new("public", "orders"),
157            foreign_table: QualifiedIdentifier::new("public", "users"),
158            is_self: false,
159            cardinality: Cardinality::M2O {
160                constraint: "orders_user_id_fkey".into(),
161                columns: vec![("user_id".into(), "id".into())],
162            },
163            table_is_view: false,
164            foreign_table_is_view: false,
165            constraint_name: "orders_user_id_fkey".into(),
166        };
167
168        assert_eq!(rel.foreign_table().name, "users");
169        assert!(rel.is_to_one());
170    }
171
172    #[test]
173    fn test_cardinality_columns() {
174        let card = Cardinality::O2M {
175            constraint: "users_id_fkey".into(),
176            columns: vec![("id".into(), "user_id".into())],
177        };
178
179        let cols = card.columns();
180        assert_eq!(cols.len(), 1);
181        assert_eq!(cols[0], ("id".into(), "user_id".into()));
182    }
183}