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