1use serde::{Deserialize, Serialize};
16
17use crate::identifier::{Identifier, QualifiedName};
18use crate::ir::catalog::Catalog;
19use crate::ir::column_type::ColumnType;
20use crate::ir::constraint::ConstraintKind;
21use crate::ir::default_expr::DefaultExpr;
22use crate::ir::function::ReturnType;
23use crate::ir::user_type::UserTypeKind;
24use crate::plan::graph::Graph;
25
26pub use crate::ir::function::NormalizedArgTypes;
27
28#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
34pub enum NodeId {
35 Schema(Identifier),
37 Table(QualifiedName),
39 Index(QualifiedName),
41 Sequence(QualifiedName),
43 Constraint {
45 table: QualifiedName,
47 name: Identifier,
49 },
50 View(QualifiedName),
52 Mv(QualifiedName),
54 Type(QualifiedName),
56 Function(QualifiedName, NormalizedArgTypes),
58 Procedure(QualifiedName),
60 Extension(Identifier),
62 Trigger(QualifiedName),
64 Publication(Identifier),
67 Subscription(Identifier),
70}
71
72#[allow(clippy::too_many_lines)]
77pub fn build_create_graph(catalog: &Catalog) -> Graph<NodeId> {
78 let mut g = Graph::new();
79
80 for s in &catalog.schemas {
82 g.add_node(NodeId::Schema(s.name.clone()));
83 }
84 for t in &catalog.tables {
85 g.add_node(NodeId::Table(t.qname.clone()));
86 }
87 for i in &catalog.indexes {
88 g.add_node(NodeId::Index(i.qname.clone()));
89 }
90 for s in &catalog.sequences {
91 g.add_node(NodeId::Sequence(s.qname.clone()));
92 }
93 for t in &catalog.tables {
94 for c in &t.constraints {
95 g.add_node(NodeId::Constraint {
96 table: t.qname.clone(),
97 name: c.qname.name.clone(),
98 });
99 }
100 }
101 for v in &catalog.views {
104 g.add_node(NodeId::View(v.qname.clone()));
105 }
106 for mv in &catalog.materialized_views {
107 g.add_node(NodeId::Mv(mv.qname.clone()));
108 }
109 for t in &catalog.types {
111 g.add_node(NodeId::Type(t.qname.clone()));
112 }
113 for f in &catalog.functions {
115 g.add_node(NodeId::Function(
116 f.qname.clone(),
117 f.arg_types_normalized.clone(),
118 ));
119 }
120 for p in &catalog.procedures {
121 g.add_node(NodeId::Procedure(p.qname.clone()));
122 }
123 for t in &catalog.triggers {
125 g.add_node(NodeId::Trigger(t.qname.clone()));
126 let target_node = if catalog.tables.iter().any(|x| x.qname == t.table) {
127 NodeId::Table(t.table.clone())
128 } else if catalog.views.iter().any(|x| x.qname == t.table) {
129 NodeId::View(t.table.clone())
130 } else if catalog
131 .materialized_views
132 .iter()
133 .any(|x| x.qname == t.table)
134 {
135 NodeId::Mv(t.table.clone())
136 } else {
137 continue;
141 };
142 g.add_edge(NodeId::Trigger(t.qname.clone()), target_node);
143 if let Some(func) = catalog
144 .functions
145 .iter()
146 .find(|f| f.qname == t.function_qname)
147 {
148 g.add_edge(
149 NodeId::Trigger(t.qname.clone()),
150 NodeId::Function(t.function_qname.clone(), func.arg_types_normalized.clone()),
151 );
152 }
153 }
154 for e in &catalog.extensions {
156 g.add_node(NodeId::Extension(e.name.clone()));
157 if let Some(schema) = &e.schema {
158 g.add_edge(
159 NodeId::Extension(e.name.clone()),
160 NodeId::Schema(schema.clone()),
161 );
162 }
163 }
164 for p in &catalog.publications {
169 let pub_node = NodeId::Publication(p.name.clone());
170 g.add_node(pub_node.clone());
171 if let crate::ir::publication::PublicationScope::Selective { schemas, tables } = &p.scope {
172 for t in tables {
173 g.add_edge(pub_node.clone(), NodeId::Table(t.qname.clone()));
174 }
175 for s in schemas {
176 g.add_edge(pub_node.clone(), NodeId::Schema(s.clone()));
177 }
178 }
179 }
180 for s in &catalog.subscriptions {
185 g.add_node(NodeId::Subscription(s.name.clone()));
186 }
187
188 for t in &catalog.types {
191 g.add_edge(
192 NodeId::Type(t.qname.clone()),
193 NodeId::Schema(t.qname.schema.clone()),
194 );
195 }
196 for f in &catalog.functions {
198 let node = NodeId::Function(f.qname.clone(), f.arg_types_normalized.clone());
199 g.add_edge(node, NodeId::Schema(f.qname.schema.clone()));
200 }
201 for p in &catalog.procedures {
202 g.add_edge(
203 NodeId::Procedure(p.qname.clone()),
204 NodeId::Schema(p.qname.schema.clone()),
205 );
206 }
207
208 for ut in &catalog.types {
212 match &ut.kind {
213 UserTypeKind::Composite { attributes } => {
214 for attr in attributes {
215 if let ColumnType::UserDefined(dep_qname) = &attr.ty {
216 g.add_edge(
217 NodeId::Type(ut.qname.clone()),
218 NodeId::Type(dep_qname.clone()),
219 );
220 }
221 }
222 }
223 UserTypeKind::Domain {
224 base: ColumnType::UserDefined(base_qname),
225 ..
226 } => {
227 g.add_edge(
228 NodeId::Type(ut.qname.clone()),
229 NodeId::Type(base_qname.clone()),
230 );
231 }
232 _ => {}
233 }
234 }
235
236 for t in &catalog.tables {
239 for col in &t.columns {
240 if let ColumnType::UserDefined(type_qname) = &col.ty {
241 g.add_edge(
242 NodeId::Table(t.qname.clone()),
243 NodeId::Type(type_qname.clone()),
244 );
245 }
246 }
247 }
248 for f in &catalog.functions {
251 let node = NodeId::Function(f.qname.clone(), f.arg_types_normalized.clone());
252 for arg in &f.args {
253 if let ColumnType::UserDefined(t_qname) = &arg.ty {
254 g.add_edge(node.clone(), NodeId::Type(t_qname.clone()));
255 }
256 }
257 match &f.return_type {
258 ReturnType::Scalar {
259 ty: ColumnType::UserDefined(t),
260 }
261 | ReturnType::SetOf {
262 ty: ColumnType::UserDefined(t),
263 } => {
264 g.add_edge(node.clone(), NodeId::Type(t.clone()));
265 }
266 ReturnType::Table { columns } => {
267 for col in columns {
268 if let ColumnType::UserDefined(t) = &col.ty {
269 g.add_edge(node.clone(), NodeId::Type(t.clone()));
270 }
271 }
272 }
273 _ => {}
274 }
275 }
276 for p in &catalog.procedures {
277 let node = NodeId::Procedure(p.qname.clone());
278 for arg in &p.args {
279 if let ColumnType::UserDefined(t_qname) = &arg.ty {
280 g.add_edge(node.clone(), NodeId::Type(t_qname.clone()));
281 }
282 }
283 }
284
285 for t in &catalog.tables {
291 g.add_edge(
292 NodeId::Table(t.qname.clone()),
293 NodeId::Schema(t.qname.schema.clone()),
294 );
295 for col in &t.columns {
296 if let Some(DefaultExpr::Sequence(seq_qname)) = &col.default {
297 g.add_edge(
298 NodeId::Table(t.qname.clone()),
299 NodeId::Sequence(seq_qname.clone()),
300 );
301 }
302 }
303 if let Some(po) = &t.partition_of {
304 g.add_edge(
306 NodeId::Table(t.qname.clone()),
307 NodeId::Table(po.parent.clone()),
308 );
309 }
310 }
311
312 for v in &catalog.views {
316 for dep in &v.body_dependencies {
317 g.add_edge(dep.from.clone(), dep.to.clone());
318 }
319 }
320 for mv in &catalog.materialized_views {
321 for dep in &mv.body_dependencies {
322 g.add_edge(dep.from.clone(), dep.to.clone());
323 }
324 }
325
326 for f in &catalog.functions {
328 for dep in &f.body_dependencies {
329 g.add_edge(dep.from.clone(), dep.to.clone());
330 }
331 }
332 for p in &catalog.procedures {
333 for dep in &p.body_dependencies {
334 g.add_edge(dep.from.clone(), dep.to.clone());
335 }
336 }
337
338 for i in &catalog.indexes {
342 use crate::ir::index::IndexParent;
343 let parent_node = match &i.on {
344 IndexParent::Table(q) => NodeId::Table(q.clone()),
345 IndexParent::Mv(q) => NodeId::Mv(q.clone()),
346 };
347 g.add_edge(NodeId::Index(i.qname.clone()), parent_node);
348 }
349
350 for t in &catalog.tables {
359 for c in &t.constraints {
360 let constraint_node = NodeId::Constraint {
361 table: t.qname.clone(),
362 name: c.qname.name.clone(),
363 };
364 g.add_edge(constraint_node.clone(), NodeId::Table(t.qname.clone()));
365 if let ConstraintKind::ForeignKey(fk) = &c.kind {
366 g.add_edge(constraint_node, NodeId::Table(fk.referenced_table.clone()));
367 if fk.referenced_table != t.qname {
370 g.add_edge(
371 NodeId::Table(t.qname.clone()),
372 NodeId::Table(fk.referenced_table.clone()),
373 );
374 }
375 }
376 }
377 }
378
379 for s in &catalog.sequences {
381 if let Some(owner) = &s.owned_by {
382 g.add_edge(
383 NodeId::Sequence(s.qname.clone()),
384 NodeId::Table(owner.table.clone()),
385 );
386 }
387 }
388
389 g
390}
391
392pub fn build_drop_graph(catalog: &Catalog) -> Graph<NodeId> {
395 build_create_graph(catalog)
396}
397
398#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
414pub enum DepSource {
415 Structural,
417 AstExtracted,
419 AstDeclared,
421}
422
423#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
428pub struct DepEdge {
429 pub from: NodeId,
431 pub to: NodeId,
433 pub source: DepSource,
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440 use crate::ir::column::Column;
441 use crate::ir::column_type::ColumnType;
442 use crate::ir::constraint::{
443 Constraint, ConstraintKind, Deferrable, FkMatchType, ForeignKey, ReferentialAction,
444 };
445 use crate::ir::index::{
446 Index, IndexColumn, IndexColumnExpr, IndexMethod, IndexParent, NullsOrder, SortOrder,
447 };
448 use crate::ir::schema::Schema;
449 use crate::ir::sequence::{Sequence, SequenceOwner};
450 use crate::ir::table::Table;
451
452 fn id(s: &str) -> Identifier {
453 Identifier::from_unquoted(s).unwrap()
454 }
455
456 fn qn(schema: &str, name: &str) -> QualifiedName {
457 QualifiedName::new(id(schema), id(name))
458 }
459
460 fn col_id_bigint() -> Column {
461 Column {
462 name: id("id"),
463 ty: ColumnType::BigInt,
464 nullable: false,
465 default: None,
466 identity: None,
467 generated: None,
468 collation: None,
469 storage: None,
470 compression: None,
471 comment: None,
472 }
473 }
474
475 fn col_text_notnull(name: &str) -> Column {
476 Column {
477 name: id(name),
478 ty: ColumnType::Text,
479 nullable: false,
480 default: None,
481 identity: None,
482 generated: None,
483 collation: None,
484 storage: None,
485 compression: None,
486 comment: None,
487 }
488 }
489
490 fn pk(name: &str, cols: &[&str]) -> Constraint {
491 Constraint {
492 qname: qn("app", name),
493 kind: ConstraintKind::PrimaryKey {
494 columns: cols.iter().map(|c| id(c)).collect(),
495 include: vec![],
496 },
497 deferrable: Deferrable::NotDeferrable,
498 comment: None,
499 }
500 }
501
502 fn fk(name: &str, ref_table: QualifiedName) -> Constraint {
503 Constraint {
504 qname: qn("app", name),
505 kind: ConstraintKind::ForeignKey(ForeignKey {
506 columns: vec![id("ref_id")],
507 referenced_table: ref_table,
508 referenced_columns: vec![id("id")],
509 on_update: ReferentialAction::NoAction,
510 on_delete: ReferentialAction::NoAction,
511 match_type: FkMatchType::Simple,
512 }),
513 deferrable: Deferrable::NotDeferrable,
514 comment: None,
515 }
516 }
517
518 fn has_edge(g: &Graph<NodeId>, from: &NodeId, to: &NodeId) -> bool {
519 g.dependencies_of(from).any(|n| n == to)
520 }
521
522 #[test]
523 fn empty_catalog_yields_empty_graph() {
524 let g = build_create_graph(&Catalog::empty());
525 assert_eq!(g.node_count(), 0);
526 }
527
528 #[test]
529 fn every_object_appears_as_a_node() {
530 let mut c = Catalog::empty();
531 c.schemas.push(Schema::new(id("app")));
532 c.tables.push(Table {
533 qname: qn("app", "users"),
534 columns: vec![col_id_bigint()],
535 constraints: vec![pk("users_pkey", &["id"])],
536 partition_by: None,
537 partition_of: None,
538 comment: None,
539 owner: None,
540 grants: vec![],
541 rls_enabled: false,
542 rls_forced: false,
543 policies: vec![],
544 storage: crate::ir::reloptions::TableStorageOptions::default(),
545 });
546 c.indexes.push(Index {
547 qname: qn("app", "users_idx"),
548 on: IndexParent::Table(qn("app", "users")),
549 method: IndexMethod::BTree,
550 columns: vec![IndexColumn {
551 expr: IndexColumnExpr::Column(id("id")),
552 collation: None,
553 opclass: None,
554 sort_order: SortOrder::Asc,
555 nulls_order: NullsOrder::NullsLast,
556 }],
557 include: vec![],
558 unique: false,
559 nulls_not_distinct: false,
560 predicate: None,
561 tablespace: None,
562 comment: None,
563 storage: crate::ir::reloptions::IndexStorageOptions::default(),
564 });
565 c.sequences.push(Sequence {
566 qname: qn("app", "seq1"),
567 data_type: ColumnType::BigInt,
568 start: 1,
569 increment: 1,
570 min_value: None,
571 max_value: None,
572 cache: 1,
573 cycle: false,
574 owned_by: None,
575 comment: None,
576 owner: None,
577 grants: vec![],
578 });
579
580 let g = build_create_graph(&c);
581 assert_eq!(g.node_count(), 5);
583 }
584
585 #[test]
586 fn table_depends_on_its_schema() {
587 let mut c = Catalog::empty();
588 c.schemas.push(Schema::new(id("app")));
589 c.tables.push(Table {
590 qname: qn("app", "users"),
591 columns: vec![col_id_bigint()],
592 constraints: vec![],
593 partition_by: None,
594 partition_of: None,
595 comment: None,
596 owner: None,
597 grants: vec![],
598 rls_enabled: false,
599 rls_forced: false,
600 policies: vec![],
601 storage: crate::ir::reloptions::TableStorageOptions::default(),
602 });
603 let g = build_create_graph(&c);
604 assert!(has_edge(
605 &g,
606 &NodeId::Table(qn("app", "users")),
607 &NodeId::Schema(id("app")),
608 ));
609 }
610
611 #[test]
612 fn index_depends_on_its_table() {
613 let mut c = Catalog::empty();
614 c.tables.push(Table {
615 qname: qn("app", "users"),
616 columns: vec![col_id_bigint()],
617 constraints: vec![],
618 partition_by: None,
619 partition_of: None,
620 comment: None,
621 owner: None,
622 grants: vec![],
623 rls_enabled: false,
624 rls_forced: false,
625 policies: vec![],
626 storage: crate::ir::reloptions::TableStorageOptions::default(),
627 });
628 c.indexes.push(Index {
629 qname: qn("app", "users_idx"),
630 on: IndexParent::Table(qn("app", "users")),
631 method: IndexMethod::BTree,
632 columns: vec![IndexColumn {
633 expr: IndexColumnExpr::Column(id("id")),
634 collation: None,
635 opclass: None,
636 sort_order: SortOrder::Asc,
637 nulls_order: NullsOrder::NullsLast,
638 }],
639 include: vec![],
640 unique: false,
641 nulls_not_distinct: false,
642 predicate: None,
643 tablespace: None,
644 comment: None,
645 storage: crate::ir::reloptions::IndexStorageOptions::default(),
646 });
647 let g = build_create_graph(&c);
648 assert!(has_edge(
649 &g,
650 &NodeId::Index(qn("app", "users_idx")),
651 &NodeId::Table(qn("app", "users")),
652 ));
653 }
654
655 #[test]
656 fn fk_constraint_depends_on_both_endpoints() {
657 let mut c = Catalog::empty();
658 c.tables.push(Table {
659 qname: qn("app", "orgs"),
660 columns: vec![col_id_bigint()],
661 constraints: vec![pk("orgs_pkey", &["id"])],
662 partition_by: None,
663 partition_of: None,
664 comment: None,
665 owner: None,
666 grants: vec![],
667 rls_enabled: false,
668 rls_forced: false,
669 policies: vec![],
670 storage: crate::ir::reloptions::TableStorageOptions::default(),
671 });
672 c.tables.push(Table {
673 qname: qn("app", "users"),
674 columns: vec![
675 col_id_bigint(),
676 Column {
677 name: id("ref_id"),
678 ty: ColumnType::BigInt,
679 nullable: false,
680 default: None,
681 identity: None,
682 generated: None,
683 collation: None,
684 storage: None,
685 compression: None,
686 comment: None,
687 },
688 ],
689 constraints: vec![fk("users_orgs_fk", qn("app", "orgs"))],
690 partition_by: None,
691 partition_of: None,
692 comment: None,
693 owner: None,
694 grants: vec![],
695 rls_enabled: false,
696 rls_forced: false,
697 policies: vec![],
698 storage: crate::ir::reloptions::TableStorageOptions::default(),
699 });
700 let g = build_create_graph(&c);
701 let fk_node = NodeId::Constraint {
702 table: qn("app", "users"),
703 name: id("users_orgs_fk"),
704 };
705 assert!(has_edge(&g, &fk_node, &NodeId::Table(qn("app", "users"))));
707 assert!(has_edge(&g, &fk_node, &NodeId::Table(qn("app", "orgs"))));
709 }
710
711 #[test]
712 fn table_depends_on_default_sequence() {
713 let mut c = Catalog::empty();
714 c.sequences.push(Sequence {
715 qname: qn("app", "id_seq"),
716 data_type: ColumnType::BigInt,
717 start: 1,
718 increment: 1,
719 min_value: None,
720 max_value: None,
721 cache: 1,
722 cycle: false,
723 owned_by: None,
724 comment: None,
725 owner: None,
726 grants: vec![],
727 });
728 c.tables.push(Table {
729 qname: qn("app", "users"),
730 columns: vec![Column {
731 name: id("id"),
732 ty: ColumnType::BigInt,
733 nullable: false,
734 default: Some(DefaultExpr::Sequence(qn("app", "id_seq"))),
735 identity: None,
736 generated: None,
737 collation: None,
738 storage: None,
739 compression: None,
740 comment: None,
741 }],
742 constraints: vec![],
743 partition_by: None,
744 partition_of: None,
745 comment: None,
746 owner: None,
747 grants: vec![],
748 rls_enabled: false,
749 rls_forced: false,
750 policies: vec![],
751 storage: crate::ir::reloptions::TableStorageOptions::default(),
752 });
753 let g = build_create_graph(&c);
754 assert!(has_edge(
755 &g,
756 &NodeId::Table(qn("app", "users")),
757 &NodeId::Sequence(qn("app", "id_seq")),
758 ));
759 }
760
761 #[test]
762 fn owned_sequence_depends_on_owner_table() {
763 let mut c = Catalog::empty();
764 c.tables.push(Table {
765 qname: qn("app", "users"),
766 columns: vec![col_id_bigint()],
767 constraints: vec![],
768 partition_by: None,
769 partition_of: None,
770 comment: None,
771 owner: None,
772 grants: vec![],
773 rls_enabled: false,
774 rls_forced: false,
775 policies: vec![],
776 storage: crate::ir::reloptions::TableStorageOptions::default(),
777 });
778 c.sequences.push(Sequence {
779 qname: qn("app", "users_id_seq"),
780 data_type: ColumnType::BigInt,
781 start: 1,
782 increment: 1,
783 min_value: None,
784 max_value: None,
785 cache: 1,
786 cycle: false,
787 owned_by: Some(SequenceOwner {
788 table: qn("app", "users"),
789 column: id("id"),
790 }),
791 comment: None,
792 owner: None,
793 grants: vec![],
794 });
795 let g = build_create_graph(&c);
796 assert!(has_edge(
797 &g,
798 &NodeId::Sequence(qn("app", "users_id_seq")),
799 &NodeId::Table(qn("app", "users")),
800 ));
801 }
802
803 #[test]
804 fn non_fk_constraint_depends_only_on_its_table() {
805 let mut c = Catalog::empty();
806 c.tables.push(Table {
807 qname: qn("app", "users"),
808 columns: vec![col_id_bigint()],
809 constraints: vec![pk("users_pkey", &["id"])],
810 partition_by: None,
811 partition_of: None,
812 comment: None,
813 owner: None,
814 grants: vec![],
815 rls_enabled: false,
816 rls_forced: false,
817 policies: vec![],
818 storage: crate::ir::reloptions::TableStorageOptions::default(),
819 });
820 let g = build_create_graph(&c);
821 let pk_node = NodeId::Constraint {
822 table: qn("app", "users"),
823 name: id("users_pkey"),
824 };
825 let deps: Vec<&NodeId> = g.dependencies_of(&pk_node).collect();
826 assert_eq!(deps, vec![&NodeId::Table(qn("app", "users"))]);
827 }
828
829 #[test]
830 fn drop_graph_matches_create_graph() {
831 let mut c = Catalog::empty();
832 c.tables.push(Table {
833 qname: qn("app", "users"),
834 columns: vec![col_id_bigint()],
835 constraints: vec![pk("users_pkey", &["id"])],
836 partition_by: None,
837 partition_of: None,
838 comment: None,
839 owner: None,
840 grants: vec![],
841 rls_enabled: false,
842 rls_forced: false,
843 policies: vec![],
844 storage: crate::ir::reloptions::TableStorageOptions::default(),
845 });
846 let cg = build_create_graph(&c);
848 let dg = build_drop_graph(&c);
849 assert_eq!(cg.topological_sort(), dg.topological_sort());
850 }
851
852 #[test]
853 fn fk_cycle_produces_table_level_cycle() {
854 let mut c = Catalog::empty();
857 c.tables.push(Table {
858 qname: qn("app", "a"),
859 columns: vec![
860 col_id_bigint(),
861 Column {
862 name: id("ref_id"),
863 ty: ColumnType::BigInt,
864 nullable: false,
865 default: None,
866 identity: None,
867 generated: None,
868 collation: None,
869 storage: None,
870 compression: None,
871 comment: None,
872 },
873 ],
874 constraints: vec![pk("a_pk", &["id"]), fk("a_to_b", qn("app", "b"))],
875 partition_by: None,
876 partition_of: None,
877 comment: None,
878 owner: None,
879 grants: vec![],
880 rls_enabled: false,
881 rls_forced: false,
882 policies: vec![],
883 storage: crate::ir::reloptions::TableStorageOptions::default(),
884 });
885 c.tables.push(Table {
886 qname: qn("app", "b"),
887 columns: vec![
888 col_id_bigint(),
889 Column {
890 name: id("ref_id"),
891 ty: ColumnType::BigInt,
892 nullable: false,
893 default: None,
894 identity: None,
895 generated: None,
896 collation: None,
897 storage: None,
898 compression: None,
899 comment: None,
900 },
901 ],
902 constraints: vec![pk("b_pk", &["id"]), fk("b_to_a", qn("app", "a"))],
903 partition_by: None,
904 partition_of: None,
905 comment: None,
906 owner: None,
907 grants: vec![],
908 rls_enabled: false,
909 rls_forced: false,
910 policies: vec![],
911 storage: crate::ir::reloptions::TableStorageOptions::default(),
912 });
913 let g = build_create_graph(&c);
914 let err = g.topological_sort().unwrap_err();
915 assert!(err.nodes.contains(&NodeId::Table(qn("app", "a"))));
916 assert!(err.nodes.contains(&NodeId::Table(qn("app", "b"))));
917 }
918
919 #[test]
920 fn self_referential_fk_does_not_cycle() {
921 let mut c = Catalog::empty();
924 c.tables.push(Table {
925 qname: qn("app", "tree"),
926 columns: vec![
927 col_id_bigint(),
928 Column {
929 name: id("ref_id"),
930 ty: ColumnType::BigInt,
931 nullable: true,
932 default: None,
933 identity: None,
934 generated: None,
935 collation: None,
936 storage: None,
937 compression: None,
938 comment: None,
939 },
940 ],
941 constraints: vec![
942 pk("tree_pk", &["id"]),
943 fk("tree_parent_fk", qn("app", "tree")),
944 ],
945 partition_by: None,
946 partition_of: None,
947 comment: None,
948 owner: None,
949 grants: vec![],
950 rls_enabled: false,
951 rls_forced: false,
952 policies: vec![],
953 storage: crate::ir::reloptions::TableStorageOptions::default(),
954 });
955 let g = build_create_graph(&c);
956 assert!(g.topological_sort().is_ok());
957 }
958
959 #[test]
960 fn partition_child_depends_on_parent() {
961 use crate::ir::partition::{PartitionBounds, PartitionBy, PartitionOf};
963 let mut c = Catalog::empty();
964
965 let parent = Table {
967 qname: qn("app", "parent"),
968 columns: vec![col_id_bigint(), col_text_notnull("status")],
969 constraints: vec![pk("parent_pkey", &["id"])],
970 partition_by: Some(PartitionBy {
971 strategy: crate::ir::partition::PartitionStrategy::List,
972 columns: vec![crate::ir::partition::PartitionColumn {
973 kind: crate::ir::partition::PartitionColumnKind::Column(id("status")),
974 collation: None,
975 opclass: None,
976 }],
977 }),
978 partition_of: None,
979 comment: None,
980 owner: None,
981 grants: vec![],
982 rls_enabled: false,
983 rls_forced: false,
984 policies: vec![],
985 storage: crate::ir::reloptions::TableStorageOptions::default(),
986 };
987 c.tables.push(parent);
988
989 let child = Table {
991 qname: qn("app", "child"),
992 columns: vec![col_id_bigint(), col_text_notnull("status")],
993 constraints: vec![],
994 partition_by: None,
995 partition_of: Some(PartitionOf {
996 parent: qn("app", "parent"),
997 bounds: PartitionBounds::List { values: vec![] },
998 }),
999 comment: None,
1000 owner: None,
1001 grants: vec![],
1002 rls_enabled: false,
1003 rls_forced: false,
1004 policies: vec![],
1005 storage: crate::ir::reloptions::TableStorageOptions::default(),
1006 };
1007 c.tables.push(child);
1008
1009 let g = build_create_graph(&c);
1010 assert!(
1011 has_edge(
1012 &g,
1013 &NodeId::Table(qn("app", "child")),
1014 &NodeId::Table(qn("app", "parent")),
1015 ),
1016 "expected child partition → parent table edge"
1017 );
1018 }
1019
1020 use crate::ir::user_type::{CompositeAttribute, UserType, UserTypeKind};
1023
1024 fn make_enum(schema: &str, name: &str) -> UserType {
1025 UserType {
1026 qname: qn(schema, name),
1027 kind: UserTypeKind::Enum { values: vec![] },
1028 comment: None,
1029 owner: None,
1030 grants: vec![],
1031 }
1032 }
1033
1034 fn make_composite_with_attr(schema: &str, name: &str, attr_type: ColumnType) -> UserType {
1035 UserType {
1036 qname: qn(schema, name),
1037 kind: UserTypeKind::Composite {
1038 attributes: vec![CompositeAttribute {
1039 name: id("val"),
1040 ty: attr_type,
1041 collation: None,
1042 }],
1043 },
1044 comment: None,
1045 owner: None,
1046 grants: vec![],
1047 }
1048 }
1049
1050 fn make_domain_over(schema: &str, name: &str, base: ColumnType) -> UserType {
1051 UserType {
1052 qname: qn(schema, name),
1053 kind: UserTypeKind::Domain {
1054 base,
1055 nullable: true,
1056 default: None,
1057 check_constraints: vec![],
1058 collation: None,
1059 },
1060 comment: None,
1061 owner: None,
1062 grants: vec![],
1063 }
1064 }
1065
1066 #[test]
1067 fn type_nodes_registered() {
1068 let mut c = Catalog::empty();
1069 c.schemas.push(crate::ir::schema::Schema::new(id("app")));
1070 c.types.push(make_enum("app", "status"));
1071 let g = build_create_graph(&c);
1072 let deps: Vec<_> = g
1074 .dependencies_of(&NodeId::Type(qn("app", "status")))
1075 .collect();
1076 assert_eq!(deps, vec![&NodeId::Schema(id("app"))]);
1077 assert_eq!(g.node_count(), 2);
1079 }
1080
1081 #[test]
1082 fn table_depends_on_user_defined_column_type() {
1083 let mut c = Catalog::empty();
1084 c.types.push(make_enum("app", "status"));
1085 c.tables.push(Table {
1086 qname: qn("app", "orders"),
1087 columns: vec![Column {
1088 name: id("status"),
1089 ty: ColumnType::UserDefined(qn("app", "status")),
1090 nullable: false,
1091 default: None,
1092 identity: None,
1093 generated: None,
1094 collation: None,
1095 storage: None,
1096 compression: None,
1097 comment: None,
1098 }],
1099 constraints: vec![],
1100 partition_by: None,
1101 partition_of: None,
1102 comment: None,
1103 owner: None,
1104 grants: vec![],
1105 rls_enabled: false,
1106 rls_forced: false,
1107 policies: vec![],
1108 storage: crate::ir::reloptions::TableStorageOptions::default(),
1109 });
1110 let g = build_create_graph(&c);
1111 assert!(
1112 has_edge(
1113 &g,
1114 &NodeId::Table(qn("app", "orders")),
1115 &NodeId::Type(qn("app", "status"))
1116 ),
1117 "table must depend on its user-defined column type"
1118 );
1119 }
1120
1121 #[test]
1122 fn composite_depends_on_user_defined_attribute_type() {
1123 let mut c = Catalog::empty();
1124 c.types.push(make_enum("app", "inner_t"));
1125 c.types.push(make_composite_with_attr(
1126 "app",
1127 "outer_t",
1128 ColumnType::UserDefined(qn("app", "inner_t")),
1129 ));
1130 let g = build_create_graph(&c);
1131 assert!(
1132 has_edge(
1133 &g,
1134 &NodeId::Type(qn("app", "outer_t")),
1135 &NodeId::Type(qn("app", "inner_t"))
1136 ),
1137 "composite must depend on the type of its user-defined attribute"
1138 );
1139 }
1140
1141 #[test]
1142 fn domain_depends_on_user_defined_base_type() {
1143 let mut c = Catalog::empty();
1144 c.types.push(make_enum("app", "base_t"));
1145 c.types.push(make_domain_over(
1146 "app",
1147 "derived_t",
1148 ColumnType::UserDefined(qn("app", "base_t")),
1149 ));
1150 let g = build_create_graph(&c);
1151 assert!(
1152 has_edge(
1153 &g,
1154 &NodeId::Type(qn("app", "derived_t")),
1155 &NodeId::Type(qn("app", "base_t"))
1156 ),
1157 "domain must depend on its user-defined base type"
1158 );
1159 }
1160
1161 #[test]
1162 fn type_create_ordering_respects_edges() {
1163 let mut c = Catalog::empty();
1165 c.types.push(make_enum("app", "base_t"));
1166 c.types.push(make_domain_over(
1167 "app",
1168 "derived_t",
1169 ColumnType::UserDefined(qn("app", "base_t")),
1170 ));
1171 let g = build_create_graph(&c);
1172 let order = g.topological_sort().expect("no cycle expected");
1173 let base_pos = order
1174 .iter()
1175 .position(|n| n == &NodeId::Type(qn("app", "base_t")))
1176 .expect("base_t in order");
1177 let derived_pos = order
1178 .iter()
1179 .position(|n| n == &NodeId::Type(qn("app", "derived_t")))
1180 .expect("derived_t in order");
1181 assert!(base_pos < derived_pos, "base_t must come before derived_t");
1182 }
1183}