1use compact_str::CompactString;
7use smallvec::SmallVec;
8
9use crate::types::QualifiedIdentifier;
10
11#[derive(Debug, Clone)]
15pub struct Relationship {
16 pub table: QualifiedIdentifier,
18 pub foreign_table: QualifiedIdentifier,
20 pub is_self: bool,
22 pub cardinality: Cardinality,
24 pub table_is_view: bool,
26 pub foreign_table_is_view: bool,
28}
29
30impl Relationship {
31 pub fn is_to_one(&self) -> bool {
35 matches!(
36 self.cardinality,
37 Cardinality::M2O { .. } | Cardinality::O2O { .. }
38 )
39 }
40
41 pub fn is_to_many(&self) -> bool {
43 matches!(
44 self.cardinality,
45 Cardinality::O2M { .. } | Cardinality::M2M(_)
46 )
47 }
48
49 pub fn constraint_name(&self) -> &str {
51 match &self.cardinality {
52 Cardinality::M2O { constraint, .. } => constraint,
53 Cardinality::O2M { constraint, .. } => constraint,
54 Cardinality::O2O { constraint, .. } => constraint,
55 Cardinality::M2M(j) => &j.constraint1,
56 }
57 }
58
59 pub fn columns(&self) -> &[(CompactString, CompactString)] {
63 match &self.cardinality {
64 Cardinality::M2O { columns, .. } => columns,
65 Cardinality::O2M { columns, .. } => columns,
66 Cardinality::O2O { columns, .. } => columns,
67 Cardinality::M2M(j) => &j.cols_source,
68 }
69 }
70
71 pub fn source_columns(&self) -> impl Iterator<Item = &str> {
73 self.columns().iter().map(|(src, _)| src.as_str())
74 }
75
76 pub fn target_columns(&self) -> impl Iterator<Item = &str> {
78 self.columns().iter().map(|(_, tgt)| tgt.as_str())
79 }
80
81 pub fn uses_column(&self, col_name: &str) -> bool {
83 self.columns()
84 .iter()
85 .any(|(src, tgt)| src.as_str() == col_name || tgt.as_str() == col_name)
86 }
87
88 pub fn is_m2m(&self) -> bool {
90 matches!(self.cardinality, Cardinality::M2M(_))
91 }
92
93 pub fn junction(&self) -> Option<&Junction> {
95 match &self.cardinality {
96 Cardinality::M2M(j) => Some(j),
97 _ => None,
98 }
99 }
100
101 pub fn reverse(&self) -> Self {
109 let rev_cardinality = match &self.cardinality {
110 Cardinality::M2O {
111 constraint,
112 columns,
113 } => Cardinality::O2M {
114 constraint: constraint.clone(),
115 columns: columns
116 .iter()
117 .map(|(a, b)| (b.clone(), a.clone()))
118 .collect(),
119 },
120 Cardinality::O2M {
121 constraint,
122 columns,
123 } => Cardinality::M2O {
124 constraint: constraint.clone(),
125 columns: columns
126 .iter()
127 .map(|(a, b)| (b.clone(), a.clone()))
128 .collect(),
129 },
130 Cardinality::O2O {
131 constraint,
132 columns,
133 is_parent,
134 } => Cardinality::O2O {
135 constraint: constraint.clone(),
136 columns: columns
137 .iter()
138 .map(|(a, b)| (b.clone(), a.clone()))
139 .collect(),
140 is_parent: !is_parent,
141 },
142 Cardinality::M2M(j) => Cardinality::M2M(j.clone()), };
144
145 Relationship {
146 table: self.foreign_table.clone(),
147 foreign_table: self.table.clone(),
148 is_self: self.is_self,
149 cardinality: rev_cardinality,
150 table_is_view: self.foreign_table_is_view,
151 foreign_table_is_view: self.table_is_view,
152 }
153 }
154
155 pub fn is_o2o_parent(&self) -> bool {
157 matches!(
158 &self.cardinality,
159 Cardinality::O2O {
160 is_parent: true,
161 ..
162 }
163 )
164 }
165
166 pub fn is_o2o_child(&self) -> bool {
168 matches!(
169 &self.cardinality,
170 Cardinality::O2O {
171 is_parent: false,
172 ..
173 }
174 )
175 }
176}
177
178#[derive(Debug, Clone)]
182pub enum Cardinality {
183 M2O {
187 constraint: CompactString,
189 columns: SmallVec<[(CompactString, CompactString); 2]>,
191 },
192
193 O2M {
197 constraint: CompactString,
199 columns: SmallVec<[(CompactString, CompactString); 2]>,
201 },
202
203 O2O {
207 constraint: CompactString,
209 columns: SmallVec<[(CompactString, CompactString); 2]>,
211 is_parent: bool,
213 },
214
215 M2M(Junction),
219}
220
221impl Cardinality {
222 pub fn as_str(&self) -> &'static str {
224 match self {
225 Cardinality::M2O { .. } => "M2O",
226 Cardinality::O2M { .. } => "O2M",
227 Cardinality::O2O { .. } => "O2O",
228 Cardinality::M2M(_) => "M2M",
229 }
230 }
231}
232
233#[derive(Debug, Clone)]
237pub struct Junction {
238 pub table: QualifiedIdentifier,
240 pub constraint1: CompactString,
242 pub constraint2: CompactString,
244 pub cols_source: SmallVec<[(CompactString, CompactString); 2]>,
246 pub cols_target: SmallVec<[(CompactString, CompactString); 2]>,
248}
249
250impl Junction {
251 pub fn junction_columns(&self) -> impl Iterator<Item = &str> {
253 self.cols_source
254 .iter()
255 .chain(self.cols_target.iter())
256 .map(|(junc_col, _)| junc_col.as_str())
257 }
258
259 pub fn source_columns(&self) -> impl Iterator<Item = &str> {
261 self.cols_source.iter().map(|(_, src_col)| src_col.as_str())
262 }
263
264 pub fn target_columns(&self) -> impl Iterator<Item = &str> {
266 self.cols_target.iter().map(|(_, tgt_col)| tgt_col.as_str())
267 }
268}
269
270#[derive(Debug, Clone)]
275pub struct ComputedRelationship {
276 pub table: QualifiedIdentifier,
278 pub function: QualifiedIdentifier,
280 pub foreign_table: QualifiedIdentifier,
282 pub table_alias: QualifiedIdentifier,
284 pub is_self: bool,
286 pub single_row: bool,
288}
289
290impl ComputedRelationship {
291 pub fn returns_set(&self) -> bool {
293 !self.single_row
294 }
295}
296
297#[derive(Debug, Clone)]
301#[allow(clippy::large_enum_variant)] pub enum AnyRelationship {
303 ForeignKey(Relationship),
305 Computed(ComputedRelationship),
307}
308
309impl AnyRelationship {
310 pub fn table(&self) -> &QualifiedIdentifier {
312 match self {
313 AnyRelationship::ForeignKey(r) => &r.table,
314 AnyRelationship::Computed(r) => &r.table,
315 }
316 }
317
318 pub fn foreign_table(&self) -> &QualifiedIdentifier {
320 match self {
321 AnyRelationship::ForeignKey(r) => &r.foreign_table,
322 AnyRelationship::Computed(r) => &r.foreign_table,
323 }
324 }
325
326 pub fn is_self(&self) -> bool {
328 match self {
329 AnyRelationship::ForeignKey(r) => r.is_self,
330 AnyRelationship::Computed(r) => r.is_self,
331 }
332 }
333
334 pub fn is_to_one(&self) -> bool {
336 match self {
337 AnyRelationship::ForeignKey(r) => r.is_to_one(),
338 AnyRelationship::Computed(r) => r.single_row,
339 }
340 }
341
342 pub fn is_fk(&self) -> bool {
344 matches!(self, AnyRelationship::ForeignKey(_))
345 }
346
347 pub fn is_computed(&self) -> bool {
349 matches!(self, AnyRelationship::Computed(_))
350 }
351
352 pub fn as_fk(&self) -> Option<&Relationship> {
354 match self {
355 AnyRelationship::ForeignKey(r) => Some(r),
356 AnyRelationship::Computed(_) => None,
357 }
358 }
359
360 pub fn as_computed(&self) -> Option<&ComputedRelationship> {
362 match self {
363 AnyRelationship::ForeignKey(_) => None,
364 AnyRelationship::Computed(r) => Some(r),
365 }
366 }
367}
368
369impl From<Relationship> for AnyRelationship {
370 fn from(r: Relationship) -> Self {
371 AnyRelationship::ForeignKey(r)
372 }
373}
374
375impl From<ComputedRelationship> for AnyRelationship {
376 fn from(r: ComputedRelationship) -> Self {
377 AnyRelationship::Computed(r)
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384 use crate::test_helpers::*;
385
386 #[test]
391 fn test_relationship_is_to_one_m2o() {
392 let rel = test_relationship()
393 .m2o("fk_user", &[("user_id", "id")])
394 .build();
395 assert!(rel.is_to_one());
396 assert!(!rel.is_to_many());
397 }
398
399 #[test]
400 fn test_relationship_is_to_one_o2o() {
401 let rel = test_relationship()
402 .o2o("fk_profile", &[("user_id", "id")], false)
403 .build();
404 assert!(rel.is_to_one());
405 assert!(!rel.is_to_many());
406 }
407
408 #[test]
409 fn test_relationship_is_to_many_o2m() {
410 let rel = test_relationship()
411 .o2m("fk_posts", &[("id", "user_id")])
412 .build();
413 assert!(!rel.is_to_one());
414 assert!(rel.is_to_many());
415 }
416
417 #[test]
418 fn test_relationship_is_to_many_m2m() {
419 let junction = test_junction()
420 .table("public", "user_roles")
421 .cols_source(&[("user_id", "id")])
422 .cols_target(&[("role_id", "id")])
423 .build();
424
425 let rel = test_relationship().m2m(junction).build();
426 assert!(!rel.is_to_one());
427 assert!(rel.is_to_many());
428 }
429
430 #[test]
431 fn test_relationship_constraint_name() {
432 let rel = test_relationship()
433 .m2o("my_constraint", &[("fk_col", "pk_col")])
434 .build();
435 assert_eq!(rel.constraint_name(), "my_constraint");
436 }
437
438 #[test]
439 fn test_relationship_columns() {
440 let rel = test_relationship()
441 .m2o("fk_test", &[("col_a", "col_b"), ("col_c", "col_d")])
442 .build();
443
444 let cols = rel.columns();
445 assert_eq!(cols.len(), 2);
446 assert_eq!(cols[0].0.as_str(), "col_a");
447 assert_eq!(cols[0].1.as_str(), "col_b");
448 }
449
450 #[test]
451 fn test_relationship_source_columns() {
452 let rel = test_relationship()
453 .m2o("fk", &[("src1", "tgt1"), ("src2", "tgt2")])
454 .build();
455
456 let sources: Vec<_> = rel.source_columns().collect();
457 assert_eq!(sources, vec!["src1", "src2"]);
458 }
459
460 #[test]
461 fn test_relationship_target_columns() {
462 let rel = test_relationship()
463 .m2o("fk", &[("src1", "tgt1"), ("src2", "tgt2")])
464 .build();
465
466 let targets: Vec<_> = rel.target_columns().collect();
467 assert_eq!(targets, vec!["tgt1", "tgt2"]);
468 }
469
470 #[test]
471 fn test_relationship_uses_column() {
472 let rel = test_relationship().m2o("fk", &[("user_id", "id")]).build();
473
474 assert!(rel.uses_column("user_id"));
475 assert!(rel.uses_column("id"));
476 assert!(!rel.uses_column("name"));
477 }
478
479 #[test]
480 fn test_relationship_is_m2m() {
481 let junction = test_junction().build();
482 let m2m_rel = test_relationship().m2m(junction).build();
483 assert!(m2m_rel.is_m2m());
484
485 let m2o_rel = test_relationship().m2o("fk", &[("a", "b")]).build();
486 assert!(!m2o_rel.is_m2m());
487 }
488
489 #[test]
490 fn test_relationship_junction() {
491 let junction = test_junction().table("public", "user_roles").build();
492 let rel = test_relationship().m2m(junction).build();
493
494 let j = rel.junction().unwrap();
495 assert_eq!(j.table.name.as_str(), "user_roles");
496 }
497
498 #[test]
499 fn test_relationship_o2o_parent_child() {
500 let parent_rel = test_relationship()
501 .o2o("fk", &[("id", "user_id")], true)
502 .build();
503 assert!(parent_rel.is_o2o_parent());
504 assert!(!parent_rel.is_o2o_child());
505
506 let child_rel = test_relationship()
507 .o2o("fk", &[("user_id", "id")], false)
508 .build();
509 assert!(!child_rel.is_o2o_parent());
510 assert!(child_rel.is_o2o_child());
511 }
512
513 #[test]
514 fn test_relationship_is_self() {
515 let self_rel = test_relationship()
516 .table("public", "employees")
517 .foreign_table("public", "employees")
518 .is_self(true)
519 .build();
520 assert!(self_rel.is_self);
521
522 let normal_rel = test_relationship()
523 .table("public", "posts")
524 .foreign_table("public", "users")
525 .is_self(false)
526 .build();
527 assert!(!normal_rel.is_self);
528 }
529
530 #[test]
535 fn test_cardinality_as_str() {
536 assert_eq!(
537 Cardinality::M2O {
538 constraint: "fk".into(),
539 columns: smallvec::smallvec![]
540 }
541 .as_str(),
542 "M2O"
543 );
544 assert_eq!(
545 Cardinality::O2M {
546 constraint: "fk".into(),
547 columns: smallvec::smallvec![]
548 }
549 .as_str(),
550 "O2M"
551 );
552 assert_eq!(
553 Cardinality::O2O {
554 constraint: "fk".into(),
555 columns: smallvec::smallvec![],
556 is_parent: false
557 }
558 .as_str(),
559 "O2O"
560 );
561 assert_eq!(Cardinality::M2M(test_junction().build()).as_str(), "M2M");
562 }
563
564 #[test]
569 fn test_junction_columns() {
570 let junction = test_junction()
571 .cols_source(&[("user_id", "id")])
572 .cols_target(&[("role_id", "id")])
573 .build();
574
575 let junc_cols: Vec<_> = junction.junction_columns().collect();
576 assert_eq!(junc_cols, vec!["user_id", "role_id"]);
577 }
578
579 #[test]
580 fn test_junction_source_columns() {
581 let junction = test_junction().cols_source(&[("user_id", "id")]).build();
582
583 let cols: Vec<_> = junction.source_columns().collect();
584 assert_eq!(cols, vec!["id"]);
585 }
586
587 #[test]
588 fn test_junction_target_columns() {
589 let junction = test_junction().cols_target(&[("role_id", "id")]).build();
590
591 let cols: Vec<_> = junction.target_columns().collect();
592 assert_eq!(cols, vec!["id"]);
593 }
594
595 #[test]
600 fn test_computed_rel_returns_set() {
601 let single_row = test_computed_rel().single_row(true).build();
602 assert!(!single_row.returns_set());
603
604 let multi_row = test_computed_rel().single_row(false).build();
605 assert!(multi_row.returns_set());
606 }
607
608 #[test]
613 fn test_any_relationship_table() {
614 let fk_rel: AnyRelationship = test_relationship().table("api", "posts").build().into();
615
616 assert_eq!(fk_rel.table().schema.as_str(), "api");
617 assert_eq!(fk_rel.table().name.as_str(), "posts");
618
619 let computed_rel: AnyRelationship =
620 test_computed_rel().table("api", "users").build().into();
621
622 assert_eq!(computed_rel.table().schema.as_str(), "api");
623 assert_eq!(computed_rel.table().name.as_str(), "users");
624 }
625
626 #[test]
627 fn test_any_relationship_foreign_table() {
628 let fk_rel: AnyRelationship = test_relationship()
629 .foreign_table("api", "users")
630 .build()
631 .into();
632
633 assert_eq!(fk_rel.foreign_table().name.as_str(), "users");
634 }
635
636 #[test]
637 fn test_any_relationship_is_self() {
638 let self_rel: AnyRelationship = test_relationship().is_self(true).build().into();
639 assert!(self_rel.is_self());
640
641 let computed_self: AnyRelationship = test_computed_rel().is_self(true).build().into();
642 assert!(computed_self.is_self());
643 }
644
645 #[test]
646 fn test_any_relationship_is_to_one() {
647 let m2o: AnyRelationship = test_relationship().m2o("fk", &[("a", "b")]).build().into();
648 assert!(m2o.is_to_one());
649
650 let o2m: AnyRelationship = test_relationship().o2m("fk", &[("a", "b")]).build().into();
651 assert!(!o2m.is_to_one());
652
653 let computed_single: AnyRelationship = test_computed_rel().single_row(true).build().into();
654 assert!(computed_single.is_to_one());
655
656 let computed_multi: AnyRelationship = test_computed_rel().single_row(false).build().into();
657 assert!(!computed_multi.is_to_one());
658 }
659
660 #[test]
661 fn test_any_relationship_is_fk_computed() {
662 let fk_rel: AnyRelationship = test_relationship().build().into();
663 assert!(fk_rel.is_fk());
664 assert!(!fk_rel.is_computed());
665
666 let computed_rel: AnyRelationship = test_computed_rel().build().into();
667 assert!(!computed_rel.is_fk());
668 assert!(computed_rel.is_computed());
669 }
670
671 #[test]
672 fn test_any_relationship_as_fk_computed() {
673 let fk_rel: AnyRelationship = test_relationship().build().into();
674 assert!(fk_rel.as_fk().is_some());
675 assert!(fk_rel.as_computed().is_none());
676
677 let computed_rel: AnyRelationship = test_computed_rel().build().into();
678 assert!(computed_rel.as_fk().is_none());
679 assert!(computed_rel.as_computed().is_some());
680 }
681}