1use crate::schema::TableDefinition;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct ForeignKeyDefinition {
15 pub constraint_name: String,
17 pub columns: Vec<String>,
19 pub referenced_table: String,
21 pub referenced_columns: Vec<String>,
23}
24
25#[derive(Debug, Clone)]
31pub enum TableKind {
32 Entity,
34 Relation {
37 in_fk: ForeignKeyDefinition,
38 out_fk: ForeignKeyDefinition,
39 },
40}
41
42pub fn classify_table(table: &TableDefinition, relation_table_overrides: &[String]) -> TableKind {
52 let forced = relation_table_overrides.contains(&table.name);
53
54 let fks = &table.foreign_keys;
55
56 if fks.len() < 2 {
58 return TableKind::Entity;
59 }
60
61 if forced {
62 return TableKind::Relation {
65 in_fk: fks[0].clone(),
66 out_fk: fks[1].clone(),
67 };
68 }
69
70 let (in_fk, out_fk) = match find_distinct_fk_pair(fks) {
72 Some(pair) => pair,
73 None => return TableKind::Entity,
74 };
75
76 if pk_is_composed_of_fk_columns(table, in_fk, out_fk) {
78 TableKind::Relation {
79 in_fk: in_fk.clone(),
80 out_fk: out_fk.clone(),
81 }
82 } else {
83 TableKind::Entity
84 }
85}
86
87fn find_distinct_fk_pair(
89 fks: &[ForeignKeyDefinition],
90) -> Option<(&ForeignKeyDefinition, &ForeignKeyDefinition)> {
91 for (i, a) in fks.iter().enumerate() {
92 for b in &fks[i + 1..] {
93 if a.referenced_table != b.referenced_table {
94 return Some((a, b));
95 }
96 }
97 }
98 None
99}
100
101fn pk_is_composed_of_fk_columns(
104 table: &TableDefinition,
105 in_fk: &ForeignKeyDefinition,
106 out_fk: &ForeignKeyDefinition,
107) -> bool {
108 let mut fk_cols: Vec<&str> = in_fk
109 .columns
110 .iter()
111 .chain(out_fk.columns.iter())
112 .map(|s| s.as_str())
113 .collect();
114 fk_cols.sort();
115 fk_cols.dedup();
116
117 let mut pk_cols = table.primary_key_column_names();
118 pk_cols.sort();
119
120 pk_cols == fk_cols
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::schema::ColumnDefinition;
127 use crate::types::Type;
128
129 fn make_table(
130 name: &str,
131 pk_names: Vec<&str>,
132 column_names: Vec<&str>,
133 fks: Vec<ForeignKeyDefinition>,
134 ) -> TableDefinition {
135 let primary_key = ColumnDefinition::new(pk_names[0], Type::Int64);
136
137 let mut columns: Vec<ColumnDefinition> = column_names
138 .iter()
139 .map(|n| ColumnDefinition::new(*n, Type::Int64))
140 .collect();
141
142 for pk in &pk_names[1..] {
144 if !column_names.contains(pk) {
145 columns.push(ColumnDefinition::new(*pk, Type::Int64));
146 }
147 }
148
149 let mut td = TableDefinition::new(name, primary_key, columns);
150 td.foreign_keys = fks;
151 if pk_names.len() > 1 {
152 td.composite_primary_key = Some(pk_names.iter().map(|s| s.to_string()).collect());
153 }
154 td
155 }
156
157 fn fk(
158 name: &str,
159 cols: Vec<&str>,
160 ref_table: &str,
161 ref_cols: Vec<&str>,
162 ) -> ForeignKeyDefinition {
163 ForeignKeyDefinition {
164 constraint_name: name.to_string(),
165 columns: cols.iter().map(|s| s.to_string()).collect(),
166 referenced_table: ref_table.to_string(),
167 referenced_columns: ref_cols.iter().map(|s| s.to_string()).collect(),
168 }
169 }
170
171 #[test]
172 fn test_entity_no_fks() {
173 let table = make_table("authors", vec!["id"], vec!["name"], vec![]);
174 assert!(matches!(classify_table(&table, &[]), TableKind::Entity));
175 }
176
177 #[test]
178 fn test_entity_one_fk() {
179 let table = make_table(
180 "books",
181 vec!["id"],
182 vec!["title", "author_id"],
183 vec![fk("fk_author", vec!["author_id"], "authors", vec!["id"])],
184 );
185 assert!(matches!(classify_table(&table, &[]), TableKind::Entity));
186 }
187
188 #[test]
189 fn test_entity_two_fks_but_pk_not_composed_of_fks() {
190 let table = make_table(
191 "collaborations",
192 vec!["id"],
193 vec!["author1_id", "author2_id", "project"],
194 vec![
195 fk("fk_a1", vec!["author1_id"], "authors", vec!["id"]),
196 fk("fk_a2", vec!["author2_id"], "editors", vec!["id"]),
197 ],
198 );
199 assert!(matches!(classify_table(&table, &[]), TableKind::Entity));
200 }
201
202 #[test]
203 fn test_relation_classic_join_table() {
204 let table = make_table(
205 "book_tags",
206 vec!["book_id", "tag_id"],
207 vec!["created_at"],
208 vec![
209 fk("fk_book", vec!["book_id"], "books", vec!["id"]),
210 fk("fk_tag", vec!["tag_id"], "tags", vec!["id"]),
211 ],
212 );
213 match classify_table(&table, &[]) {
214 TableKind::Relation { in_fk, out_fk } => {
215 assert_eq!(in_fk.referenced_table, "books");
216 assert_eq!(out_fk.referenced_table, "tags");
217 }
218 TableKind::Entity => panic!("Expected Relation"),
219 }
220 }
221
222 #[test]
223 fn test_entity_pk_has_extra_col_beyond_fks() {
224 let table = make_table(
225 "events",
226 vec!["book_id", "tag_id", "seq"],
227 vec!["data"],
228 vec![
229 fk("fk_book", vec!["book_id"], "books", vec!["id"]),
230 fk("fk_tag", vec!["tag_id"], "tags", vec!["id"]),
231 ],
232 );
233 assert!(matches!(classify_table(&table, &[]), TableKind::Entity));
234 }
235
236 #[test]
237 fn test_override_forces_relation() {
238 let table = make_table(
239 "collaborations",
240 vec!["id"],
241 vec!["author1_id", "author2_id", "project"],
242 vec![
243 fk("fk_a1", vec!["author1_id"], "authors", vec!["id"]),
244 fk("fk_a2", vec!["author2_id"], "editors", vec!["id"]),
245 ],
246 );
247 let overrides = vec!["collaborations".to_string()];
248 match classify_table(&table, &overrides) {
249 TableKind::Relation { in_fk, out_fk } => {
250 assert_eq!(in_fk.referenced_table, "authors");
251 assert_eq!(out_fk.referenced_table, "editors");
252 }
253 TableKind::Entity => panic!("Expected Relation with override"),
254 }
255 }
256
257 #[test]
258 fn test_override_not_matching_uses_heuristic() {
259 let table = make_table(
260 "books",
261 vec!["id"],
262 vec!["title", "author_id"],
263 vec![fk("fk_author", vec!["author_id"], "authors", vec!["id"])],
264 );
265 let overrides = vec!["other_table".to_string()];
266 assert!(matches!(
267 classify_table(&table, &overrides),
268 TableKind::Entity
269 ));
270 }
271
272 #[test]
273 fn test_two_fks_same_table_no_relation() {
274 let table = make_table(
275 "friendships",
276 vec!["user1_id", "user2_id"],
277 vec![],
278 vec![
279 fk("fk_u1", vec!["user1_id"], "users", vec!["id"]),
280 fk("fk_u2", vec!["user2_id"], "users", vec!["id"]),
281 ],
282 );
283 assert!(matches!(classify_table(&table, &[]), TableKind::Entity));
285 }
286
287 #[test]
288 fn test_override_same_table_fks_forces_relation() {
289 let table = make_table(
290 "friendships",
291 vec!["user1_id", "user2_id"],
292 vec![],
293 vec![
294 fk("fk_u1", vec!["user1_id"], "users", vec!["id"]),
295 fk("fk_u2", vec!["user2_id"], "users", vec!["id"]),
296 ],
297 );
298 let overrides = vec!["friendships".to_string()];
299 match classify_table(&table, &overrides) {
300 TableKind::Relation { in_fk, out_fk } => {
301 assert_eq!(in_fk.referenced_table, "users");
302 assert_eq!(out_fk.referenced_table, "users");
303 }
304 TableKind::Entity => panic!("Override should force Relation even with same-table FKs"),
305 }
306 }
307}