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