1use serde::{Deserialize, Serialize};
16
17use crate::identifier::{Identifier, QualifiedName};
18use crate::ir::catalog::Catalog;
19use crate::ir::collation::BUILTIN_COLLATIONS;
20use crate::ir::column_type::ColumnType;
21use crate::ir::constraint::ConstraintKind;
22use crate::ir::default_expr::DefaultExpr;
23use crate::ir::function::ReturnType;
24use crate::ir::user_type::UserTypeKind;
25use crate::plan::graph::Graph;
26
27pub use crate::ir::function::NormalizedArgTypes;
28
29#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
35pub enum NodeId {
36 Schema(Identifier),
38 Table(QualifiedName),
40 Index(QualifiedName),
42 Sequence(QualifiedName),
44 Constraint {
46 table: QualifiedName,
48 name: Identifier,
50 },
51 View(QualifiedName),
53 Mv(QualifiedName),
55 Type(QualifiedName),
57 Function(QualifiedName, NormalizedArgTypes),
59 Procedure(QualifiedName),
61 Extension(Identifier),
63 Trigger(QualifiedName),
65 Publication(Identifier),
68 Subscription(Identifier),
71 Statistic(QualifiedName),
73 Collation(QualifiedName),
80}
81
82fn should_add_collation_edge(qname: &QualifiedName, catalog: &Catalog) -> bool {
86 if qname.schema.as_str() == "pg_catalog" {
87 return false;
88 }
89 let name = qname.name.as_str();
90 if BUILTIN_COLLATIONS.contains(&name) {
91 return false;
92 }
93 catalog.collations.iter().any(|c| &c.qname == qname)
94}
95
96#[allow(clippy::too_many_lines)] pub fn build_create_graph(catalog: &Catalog) -> Graph<NodeId> {
102 let mut g = Graph::new();
103
104 for s in &catalog.schemas {
106 g.add_node(NodeId::Schema(s.name.clone()));
107 }
108 for t in &catalog.tables {
109 g.add_node(NodeId::Table(t.qname.clone()));
110 }
111 for i in &catalog.indexes {
112 g.add_node(NodeId::Index(i.qname.clone()));
113 }
114 for s in &catalog.sequences {
115 g.add_node(NodeId::Sequence(s.qname.clone()));
116 }
117 for t in &catalog.tables {
118 for c in &t.constraints {
119 g.add_node(NodeId::Constraint {
120 table: t.qname.clone(),
121 name: c.qname.name.clone(),
122 });
123 }
124 }
125 for v in &catalog.views {
128 g.add_node(NodeId::View(v.qname.clone()));
129 }
130 for mv in &catalog.materialized_views {
131 g.add_node(NodeId::Mv(mv.qname.clone()));
132 }
133 for t in &catalog.types {
135 g.add_node(NodeId::Type(t.qname.clone()));
136 }
137 for f in &catalog.functions {
139 g.add_node(NodeId::Function(
140 f.qname.clone(),
141 f.arg_types_normalized.clone(),
142 ));
143 }
144 for p in &catalog.procedures {
145 g.add_node(NodeId::Procedure(p.qname.clone()));
146 }
147 for t in &catalog.triggers {
149 g.add_node(NodeId::Trigger(t.qname.clone()));
150 let target_node = if catalog.tables.iter().any(|x| x.qname == t.table) {
151 NodeId::Table(t.table.clone())
152 } else if catalog.views.iter().any(|x| x.qname == t.table) {
153 NodeId::View(t.table.clone())
154 } else if catalog
155 .materialized_views
156 .iter()
157 .any(|x| x.qname == t.table)
158 {
159 NodeId::Mv(t.table.clone())
160 } else {
161 continue;
165 };
166 g.add_edge(NodeId::Trigger(t.qname.clone()), target_node);
167 if let Some(func) = catalog
168 .functions
169 .iter()
170 .find(|f| f.qname == t.function_qname)
171 {
172 g.add_edge(
173 NodeId::Trigger(t.qname.clone()),
174 NodeId::Function(t.function_qname.clone(), func.arg_types_normalized.clone()),
175 );
176 }
177 }
178 for e in &catalog.extensions {
180 g.add_node(NodeId::Extension(e.name.clone()));
181 if let Some(schema) = &e.schema {
182 g.add_edge(
183 NodeId::Extension(e.name.clone()),
184 NodeId::Schema(schema.clone()),
185 );
186 }
187 }
188 for p in &catalog.publications {
193 let pub_node = NodeId::Publication(p.name.clone());
194 g.add_node(pub_node.clone());
195 if let crate::ir::publication::PublicationScope::Selective { schemas, tables } = &p.scope {
196 for t in tables {
197 g.add_edge(pub_node.clone(), NodeId::Table(t.qname.clone()));
198 }
199 for s in schemas {
200 g.add_edge(pub_node.clone(), NodeId::Schema(s.clone()));
201 }
202 }
203 }
204 for s in &catalog.subscriptions {
209 g.add_node(NodeId::Subscription(s.name.clone()));
210 }
211 for s in &catalog.statistics {
214 let stat_node = NodeId::Statistic(s.qname.clone());
215 g.add_node(stat_node.clone());
216 g.add_edge(stat_node, NodeId::Table(s.target.clone()));
217 }
218 for c in &catalog.collations {
221 g.add_node(NodeId::Collation(c.qname.clone()));
222 }
223
224 for t in &catalog.types {
227 g.add_edge(
228 NodeId::Type(t.qname.clone()),
229 NodeId::Schema(t.qname.schema.clone()),
230 );
231 }
232 for f in &catalog.functions {
234 let node = NodeId::Function(f.qname.clone(), f.arg_types_normalized.clone());
235 g.add_edge(node, NodeId::Schema(f.qname.schema.clone()));
236 }
237 for p in &catalog.procedures {
238 g.add_edge(
239 NodeId::Procedure(p.qname.clone()),
240 NodeId::Schema(p.qname.schema.clone()),
241 );
242 }
243
244 for ut in &catalog.types {
250 match &ut.kind {
251 UserTypeKind::Composite { attributes } => {
252 for attr in attributes {
253 if let ColumnType::UserDefined(dep_qname) = &attr.ty {
254 g.add_edge(
255 NodeId::Type(ut.qname.clone()),
256 NodeId::Type(dep_qname.clone()),
257 );
258 }
259 if let Some(coll_qname) = &attr.collation
260 && should_add_collation_edge(coll_qname, catalog)
261 {
262 g.add_edge(
263 NodeId::Type(ut.qname.clone()),
264 NodeId::Collation(coll_qname.clone()),
265 );
266 }
267 }
268 }
269 UserTypeKind::Domain {
270 base, collation, ..
271 } => {
272 if let ColumnType::UserDefined(base_qname) = base {
273 g.add_edge(
274 NodeId::Type(ut.qname.clone()),
275 NodeId::Type(base_qname.clone()),
276 );
277 }
278 if let Some(coll_qname) = collation
279 && should_add_collation_edge(coll_qname, catalog)
280 {
281 g.add_edge(
282 NodeId::Type(ut.qname.clone()),
283 NodeId::Collation(coll_qname.clone()),
284 );
285 }
286 }
287 UserTypeKind::Range {
288 subtype,
289 canonical,
290 subtype_diff,
291 collation,
292 ..
293 } => {
294 if subtype.schema.as_str() != "pg_catalog"
296 && catalog.types.iter().any(|t| &t.qname == subtype)
297 {
298 g.add_edge(
299 NodeId::Type(ut.qname.clone()),
300 NodeId::Type(subtype.clone()),
301 );
302 }
303 if let Some(fn_qname) = canonical
304 && let Some(f) = catalog.functions.iter().find(|f| &f.qname == fn_qname)
305 {
306 g.add_edge(
307 NodeId::Type(ut.qname.clone()),
308 NodeId::Function(f.qname.clone(), f.arg_types_normalized.clone()),
309 );
310 }
311 if let Some(fn_qname) = subtype_diff
312 && let Some(f) = catalog.functions.iter().find(|f| &f.qname == fn_qname)
313 {
314 g.add_edge(
315 NodeId::Type(ut.qname.clone()),
316 NodeId::Function(f.qname.clone(), f.arg_types_normalized.clone()),
317 );
318 }
319 if let Some(coll_qname) = collation
320 && should_add_collation_edge(coll_qname, catalog)
321 {
322 g.add_edge(
323 NodeId::Type(ut.qname.clone()),
324 NodeId::Collation(coll_qname.clone()),
325 );
326 }
327 }
328 UserTypeKind::Enum { .. } => {}
329 }
330 }
331
332 for t in &catalog.tables {
337 for col in &t.columns {
338 if let ColumnType::UserDefined(type_qname) = &col.ty {
339 g.add_edge(
340 NodeId::Table(t.qname.clone()),
341 NodeId::Type(type_qname.clone()),
342 );
343 }
344 if let Some(coll_qname) = &col.collation
345 && should_add_collation_edge(coll_qname, catalog)
346 {
347 g.add_edge(
348 NodeId::Table(t.qname.clone()),
349 NodeId::Collation(coll_qname.clone()),
350 );
351 }
352 }
353 }
354 for f in &catalog.functions {
357 let node = NodeId::Function(f.qname.clone(), f.arg_types_normalized.clone());
358 for arg in &f.args {
359 if let ColumnType::UserDefined(t_qname) = &arg.ty {
360 g.add_edge(node.clone(), NodeId::Type(t_qname.clone()));
361 }
362 }
363 match &f.return_type {
364 ReturnType::Scalar {
365 ty: ColumnType::UserDefined(t),
366 }
367 | ReturnType::SetOf {
368 ty: ColumnType::UserDefined(t),
369 } => {
370 g.add_edge(node.clone(), NodeId::Type(t.clone()));
371 }
372 ReturnType::Table { columns } => {
373 for col in columns {
374 if let ColumnType::UserDefined(t) = &col.ty {
375 g.add_edge(node.clone(), NodeId::Type(t.clone()));
376 }
377 }
378 }
379 _ => {}
380 }
381 }
382 for p in &catalog.procedures {
383 let node = NodeId::Procedure(p.qname.clone());
384 for arg in &p.args {
385 if let ColumnType::UserDefined(t_qname) = &arg.ty {
386 g.add_edge(node.clone(), NodeId::Type(t_qname.clone()));
387 }
388 }
389 }
390
391 for t in &catalog.tables {
397 g.add_edge(
398 NodeId::Table(t.qname.clone()),
399 NodeId::Schema(t.qname.schema.clone()),
400 );
401 for col in &t.columns {
402 if let Some(DefaultExpr::Sequence(seq_qname)) = &col.default {
403 g.add_edge(
404 NodeId::Table(t.qname.clone()),
405 NodeId::Sequence(seq_qname.clone()),
406 );
407 }
408 }
409 if let Some(po) = &t.partition_of {
410 g.add_edge(
412 NodeId::Table(t.qname.clone()),
413 NodeId::Table(po.parent.clone()),
414 );
415 }
416 }
417
418 for v in &catalog.views {
422 for dep in &v.body_dependencies {
423 g.add_edge(dep.from.clone(), dep.to.clone());
424 }
425 }
426 for mv in &catalog.materialized_views {
427 for dep in &mv.body_dependencies {
428 g.add_edge(dep.from.clone(), dep.to.clone());
429 }
430 }
431
432 for f in &catalog.functions {
434 for dep in &f.body_dependencies {
435 g.add_edge(dep.from.clone(), dep.to.clone());
436 }
437 }
438 for p in &catalog.procedures {
439 for dep in &p.body_dependencies {
440 g.add_edge(dep.from.clone(), dep.to.clone());
441 }
442 }
443
444 for i in &catalog.indexes {
448 use crate::ir::index::IndexParent;
449 let parent_node = match &i.on {
450 IndexParent::Table(q) => NodeId::Table(q.clone()),
451 IndexParent::Mv(q) => NodeId::Mv(q.clone()),
452 };
453 g.add_edge(NodeId::Index(i.qname.clone()), parent_node);
454 }
455
456 for t in &catalog.tables {
465 for c in &t.constraints {
466 let constraint_node = NodeId::Constraint {
467 table: t.qname.clone(),
468 name: c.qname.name.clone(),
469 };
470 g.add_edge(constraint_node.clone(), NodeId::Table(t.qname.clone()));
471 if let ConstraintKind::ForeignKey(fk) = &c.kind {
472 g.add_edge(constraint_node, NodeId::Table(fk.referenced_table.clone()));
473 if fk.referenced_table != t.qname {
476 g.add_edge(
477 NodeId::Table(t.qname.clone()),
478 NodeId::Table(fk.referenced_table.clone()),
479 );
480 }
481 }
482 }
483 }
484
485 for s in &catalog.sequences {
487 if let Some(owner) = &s.owned_by {
488 g.add_edge(
489 NodeId::Sequence(s.qname.clone()),
490 NodeId::Table(owner.table.clone()),
491 );
492 }
493 }
494
495 g
496}
497
498pub fn build_drop_graph(catalog: &Catalog) -> Graph<NodeId> {
501 build_create_graph(catalog)
502}
503
504#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
520pub enum DepSource {
521 Structural,
523 AstExtracted,
525 AstDeclared,
527}
528
529#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
534pub struct DepEdge {
535 pub from: NodeId,
537 pub to: NodeId,
539 pub source: DepSource,
541}
542
543#[cfg(test)]
544mod tests {
545 use super::*;
546 use crate::ir::column::Column;
547 use crate::ir::column_type::ColumnType;
548 use crate::ir::constraint::{
549 Constraint, ConstraintKind, Deferrable, FkMatchType, ForeignKey, ReferentialAction,
550 };
551 use crate::ir::index::{
552 Index, IndexColumn, IndexColumnExpr, IndexMethod, IndexParent, NullsOrder, SortOrder,
553 };
554 use crate::ir::schema::Schema;
555 use crate::ir::sequence::{Sequence, SequenceOwner};
556 use crate::ir::table::Table;
557
558 fn id(s: &str) -> Identifier {
559 Identifier::from_unquoted(s).unwrap()
560 }
561
562 fn qn(schema: &str, name: &str) -> QualifiedName {
563 QualifiedName::new(id(schema), id(name))
564 }
565
566 fn col_id_bigint() -> Column {
567 Column {
568 name: id("id"),
569 ty: ColumnType::BigInt,
570 nullable: false,
571 default: None,
572 identity: None,
573 generated: None,
574 collation: None,
575 storage: None,
576 compression: None,
577 comment: None,
578 }
579 }
580
581 fn col_text_notnull(name: &str) -> Column {
582 Column {
583 name: id(name),
584 ty: ColumnType::Text,
585 nullable: false,
586 default: None,
587 identity: None,
588 generated: None,
589 collation: None,
590 storage: None,
591 compression: None,
592 comment: None,
593 }
594 }
595
596 fn pk(name: &str, cols: &[&str]) -> Constraint {
597 Constraint {
598 qname: qn("app", name),
599 kind: ConstraintKind::PrimaryKey {
600 columns: cols.iter().map(|c| id(c)).collect(),
601 include: vec![],
602 },
603 deferrable: Deferrable::NotDeferrable,
604 comment: None,
605 }
606 }
607
608 fn fk(name: &str, ref_table: QualifiedName) -> Constraint {
609 Constraint {
610 qname: qn("app", name),
611 kind: ConstraintKind::ForeignKey(ForeignKey {
612 columns: vec![id("ref_id")],
613 referenced_table: ref_table,
614 referenced_columns: vec![id("id")],
615 on_update: ReferentialAction::NoAction,
616 on_delete: ReferentialAction::NoAction,
617 match_type: FkMatchType::Simple,
618 }),
619 deferrable: Deferrable::NotDeferrable,
620 comment: None,
621 }
622 }
623
624 fn has_edge(g: &Graph<NodeId>, from: &NodeId, to: &NodeId) -> bool {
625 g.dependencies_of(from).any(|n| n == to)
626 }
627
628 #[test]
629 fn empty_catalog_yields_empty_graph() {
630 let g = build_create_graph(&Catalog::empty());
631 assert_eq!(g.node_count(), 0);
632 }
633
634 #[test]
635 fn every_object_appears_as_a_node() {
636 let mut c = Catalog::empty();
637 c.schemas.push(Schema::new(id("app")));
638 c.tables.push(Table {
639 qname: qn("app", "users"),
640 columns: vec![col_id_bigint()],
641 constraints: vec![pk("users_pkey", &["id"])],
642 partition_by: None,
643 partition_of: None,
644 comment: None,
645 owner: None,
646 grants: vec![],
647 rls_enabled: false,
648 rls_forced: false,
649 policies: vec![],
650 storage: crate::ir::reloptions::TableStorageOptions::default(),
651 });
652 c.indexes.push(Index {
653 qname: qn("app", "users_idx"),
654 on: IndexParent::Table(qn("app", "users")),
655 method: IndexMethod::BTree,
656 columns: vec![IndexColumn {
657 expr: IndexColumnExpr::Column(id("id")),
658 collation: None,
659 opclass: None,
660 sort_order: SortOrder::Asc,
661 nulls_order: NullsOrder::NullsLast,
662 }],
663 include: vec![],
664 unique: false,
665 nulls_not_distinct: false,
666 predicate: None,
667 tablespace: None,
668 comment: None,
669 storage: crate::ir::reloptions::IndexStorageOptions::default(),
670 });
671 c.sequences.push(Sequence {
672 qname: qn("app", "seq1"),
673 data_type: ColumnType::BigInt,
674 start: 1,
675 increment: 1,
676 min_value: None,
677 max_value: None,
678 cache: 1,
679 cycle: false,
680 owned_by: None,
681 comment: None,
682 owner: None,
683 grants: vec![],
684 });
685
686 let g = build_create_graph(&c);
687 assert_eq!(g.node_count(), 5);
689 }
690
691 #[test]
692 fn table_depends_on_its_schema() {
693 let mut c = Catalog::empty();
694 c.schemas.push(Schema::new(id("app")));
695 c.tables.push(Table {
696 qname: qn("app", "users"),
697 columns: vec![col_id_bigint()],
698 constraints: vec![],
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 assert!(has_edge(
711 &g,
712 &NodeId::Table(qn("app", "users")),
713 &NodeId::Schema(id("app")),
714 ));
715 }
716
717 #[test]
718 fn index_depends_on_its_table() {
719 let mut c = Catalog::empty();
720 c.tables.push(Table {
721 qname: qn("app", "users"),
722 columns: vec![col_id_bigint()],
723 constraints: vec![],
724 partition_by: None,
725 partition_of: None,
726 comment: None,
727 owner: None,
728 grants: vec![],
729 rls_enabled: false,
730 rls_forced: false,
731 policies: vec![],
732 storage: crate::ir::reloptions::TableStorageOptions::default(),
733 });
734 c.indexes.push(Index {
735 qname: qn("app", "users_idx"),
736 on: IndexParent::Table(qn("app", "users")),
737 method: IndexMethod::BTree,
738 columns: vec![IndexColumn {
739 expr: IndexColumnExpr::Column(id("id")),
740 collation: None,
741 opclass: None,
742 sort_order: SortOrder::Asc,
743 nulls_order: NullsOrder::NullsLast,
744 }],
745 include: vec![],
746 unique: false,
747 nulls_not_distinct: false,
748 predicate: None,
749 tablespace: None,
750 comment: None,
751 storage: crate::ir::reloptions::IndexStorageOptions::default(),
752 });
753 let g = build_create_graph(&c);
754 assert!(has_edge(
755 &g,
756 &NodeId::Index(qn("app", "users_idx")),
757 &NodeId::Table(qn("app", "users")),
758 ));
759 }
760
761 #[test]
762 fn fk_constraint_depends_on_both_endpoints() {
763 let mut c = Catalog::empty();
764 c.tables.push(Table {
765 qname: qn("app", "orgs"),
766 columns: vec![col_id_bigint()],
767 constraints: vec![pk("orgs_pkey", &["id"])],
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.tables.push(Table {
779 qname: qn("app", "users"),
780 columns: vec![
781 col_id_bigint(),
782 Column {
783 name: id("ref_id"),
784 ty: ColumnType::BigInt,
785 nullable: false,
786 default: None,
787 identity: None,
788 generated: None,
789 collation: None,
790 storage: None,
791 compression: None,
792 comment: None,
793 },
794 ],
795 constraints: vec![fk("users_orgs_fk", qn("app", "orgs"))],
796 partition_by: None,
797 partition_of: None,
798 comment: None,
799 owner: None,
800 grants: vec![],
801 rls_enabled: false,
802 rls_forced: false,
803 policies: vec![],
804 storage: crate::ir::reloptions::TableStorageOptions::default(),
805 });
806 let g = build_create_graph(&c);
807 let fk_node = NodeId::Constraint {
808 table: qn("app", "users"),
809 name: id("users_orgs_fk"),
810 };
811 assert!(has_edge(&g, &fk_node, &NodeId::Table(qn("app", "users"))));
813 assert!(has_edge(&g, &fk_node, &NodeId::Table(qn("app", "orgs"))));
815 }
816
817 #[test]
818 fn table_depends_on_default_sequence() {
819 let mut c = Catalog::empty();
820 c.sequences.push(Sequence {
821 qname: qn("app", "id_seq"),
822 data_type: ColumnType::BigInt,
823 start: 1,
824 increment: 1,
825 min_value: None,
826 max_value: None,
827 cache: 1,
828 cycle: false,
829 owned_by: None,
830 comment: None,
831 owner: None,
832 grants: vec![],
833 });
834 c.tables.push(Table {
835 qname: qn("app", "users"),
836 columns: vec![Column {
837 name: id("id"),
838 ty: ColumnType::BigInt,
839 nullable: false,
840 default: Some(DefaultExpr::Sequence(qn("app", "id_seq"))),
841 identity: None,
842 generated: None,
843 collation: None,
844 storage: None,
845 compression: None,
846 comment: None,
847 }],
848 constraints: vec![],
849 partition_by: None,
850 partition_of: None,
851 comment: None,
852 owner: None,
853 grants: vec![],
854 rls_enabled: false,
855 rls_forced: false,
856 policies: vec![],
857 storage: crate::ir::reloptions::TableStorageOptions::default(),
858 });
859 let g = build_create_graph(&c);
860 assert!(has_edge(
861 &g,
862 &NodeId::Table(qn("app", "users")),
863 &NodeId::Sequence(qn("app", "id_seq")),
864 ));
865 }
866
867 #[test]
868 fn owned_sequence_depends_on_owner_table() {
869 let mut c = Catalog::empty();
870 c.tables.push(Table {
871 qname: qn("app", "users"),
872 columns: vec![col_id_bigint()],
873 constraints: vec![],
874 partition_by: None,
875 partition_of: None,
876 comment: None,
877 owner: None,
878 grants: vec![],
879 rls_enabled: false,
880 rls_forced: false,
881 policies: vec![],
882 storage: crate::ir::reloptions::TableStorageOptions::default(),
883 });
884 c.sequences.push(Sequence {
885 qname: qn("app", "users_id_seq"),
886 data_type: ColumnType::BigInt,
887 start: 1,
888 increment: 1,
889 min_value: None,
890 max_value: None,
891 cache: 1,
892 cycle: false,
893 owned_by: Some(SequenceOwner {
894 table: qn("app", "users"),
895 column: id("id"),
896 }),
897 comment: None,
898 owner: None,
899 grants: vec![],
900 });
901 let g = build_create_graph(&c);
902 assert!(has_edge(
903 &g,
904 &NodeId::Sequence(qn("app", "users_id_seq")),
905 &NodeId::Table(qn("app", "users")),
906 ));
907 }
908
909 #[test]
910 fn non_fk_constraint_depends_only_on_its_table() {
911 let mut c = Catalog::empty();
912 c.tables.push(Table {
913 qname: qn("app", "users"),
914 columns: vec![col_id_bigint()],
915 constraints: vec![pk("users_pkey", &["id"])],
916 partition_by: None,
917 partition_of: None,
918 comment: None,
919 owner: None,
920 grants: vec![],
921 rls_enabled: false,
922 rls_forced: false,
923 policies: vec![],
924 storage: crate::ir::reloptions::TableStorageOptions::default(),
925 });
926 let g = build_create_graph(&c);
927 let pk_node = NodeId::Constraint {
928 table: qn("app", "users"),
929 name: id("users_pkey"),
930 };
931 let deps: Vec<&NodeId> = g.dependencies_of(&pk_node).collect();
932 assert_eq!(deps, vec![&NodeId::Table(qn("app", "users"))]);
933 }
934
935 #[test]
936 fn drop_graph_matches_create_graph() {
937 let mut c = Catalog::empty();
938 c.tables.push(Table {
939 qname: qn("app", "users"),
940 columns: vec![col_id_bigint()],
941 constraints: vec![pk("users_pkey", &["id"])],
942 partition_by: None,
943 partition_of: None,
944 comment: None,
945 owner: None,
946 grants: vec![],
947 rls_enabled: false,
948 rls_forced: false,
949 policies: vec![],
950 storage: crate::ir::reloptions::TableStorageOptions::default(),
951 });
952 let cg = build_create_graph(&c);
954 let dg = build_drop_graph(&c);
955 assert_eq!(cg.topological_sort(), dg.topological_sort());
956 }
957
958 #[test]
959 fn fk_cycle_produces_table_level_cycle() {
960 let mut c = Catalog::empty();
963 c.tables.push(Table {
964 qname: qn("app", "a"),
965 columns: vec![
966 col_id_bigint(),
967 Column {
968 name: id("ref_id"),
969 ty: ColumnType::BigInt,
970 nullable: false,
971 default: None,
972 identity: None,
973 generated: None,
974 collation: None,
975 storage: None,
976 compression: None,
977 comment: None,
978 },
979 ],
980 constraints: vec![pk("a_pk", &["id"]), fk("a_to_b", qn("app", "b"))],
981 partition_by: None,
982 partition_of: None,
983 comment: None,
984 owner: None,
985 grants: vec![],
986 rls_enabled: false,
987 rls_forced: false,
988 policies: vec![],
989 storage: crate::ir::reloptions::TableStorageOptions::default(),
990 });
991 c.tables.push(Table {
992 qname: qn("app", "b"),
993 columns: vec![
994 col_id_bigint(),
995 Column {
996 name: id("ref_id"),
997 ty: ColumnType::BigInt,
998 nullable: false,
999 default: None,
1000 identity: None,
1001 generated: None,
1002 collation: None,
1003 storage: None,
1004 compression: None,
1005 comment: None,
1006 },
1007 ],
1008 constraints: vec![pk("b_pk", &["id"]), fk("b_to_a", qn("app", "a"))],
1009 partition_by: None,
1010 partition_of: None,
1011 comment: None,
1012 owner: None,
1013 grants: vec![],
1014 rls_enabled: false,
1015 rls_forced: false,
1016 policies: vec![],
1017 storage: crate::ir::reloptions::TableStorageOptions::default(),
1018 });
1019 let g = build_create_graph(&c);
1020 let err = g.topological_sort().unwrap_err();
1021 assert!(err.nodes.contains(&NodeId::Table(qn("app", "a"))));
1022 assert!(err.nodes.contains(&NodeId::Table(qn("app", "b"))));
1023 }
1024
1025 #[test]
1026 fn self_referential_fk_does_not_cycle() {
1027 let mut c = Catalog::empty();
1030 c.tables.push(Table {
1031 qname: qn("app", "tree"),
1032 columns: vec![
1033 col_id_bigint(),
1034 Column {
1035 name: id("ref_id"),
1036 ty: ColumnType::BigInt,
1037 nullable: true,
1038 default: None,
1039 identity: None,
1040 generated: None,
1041 collation: None,
1042 storage: None,
1043 compression: None,
1044 comment: None,
1045 },
1046 ],
1047 constraints: vec![
1048 pk("tree_pk", &["id"]),
1049 fk("tree_parent_fk", qn("app", "tree")),
1050 ],
1051 partition_by: None,
1052 partition_of: None,
1053 comment: None,
1054 owner: None,
1055 grants: vec![],
1056 rls_enabled: false,
1057 rls_forced: false,
1058 policies: vec![],
1059 storage: crate::ir::reloptions::TableStorageOptions::default(),
1060 });
1061 let g = build_create_graph(&c);
1062 assert!(g.topological_sort().is_ok());
1063 }
1064
1065 #[test]
1066 fn partition_child_depends_on_parent() {
1067 use crate::ir::partition::{PartitionBounds, PartitionBy, PartitionOf};
1069 let mut c = Catalog::empty();
1070
1071 let parent = Table {
1073 qname: qn("app", "parent"),
1074 columns: vec![col_id_bigint(), col_text_notnull("status")],
1075 constraints: vec![pk("parent_pkey", &["id"])],
1076 partition_by: Some(PartitionBy {
1077 strategy: crate::ir::partition::PartitionStrategy::List,
1078 columns: vec![crate::ir::partition::PartitionColumn {
1079 kind: crate::ir::partition::PartitionColumnKind::Column(id("status")),
1080 collation: None,
1081 opclass: None,
1082 }],
1083 }),
1084 partition_of: None,
1085 comment: None,
1086 owner: None,
1087 grants: vec![],
1088 rls_enabled: false,
1089 rls_forced: false,
1090 policies: vec![],
1091 storage: crate::ir::reloptions::TableStorageOptions::default(),
1092 };
1093 c.tables.push(parent);
1094
1095 let child = Table {
1097 qname: qn("app", "child"),
1098 columns: vec![col_id_bigint(), col_text_notnull("status")],
1099 constraints: vec![],
1100 partition_by: None,
1101 partition_of: Some(PartitionOf {
1102 parent: qn("app", "parent"),
1103 bounds: PartitionBounds::List { values: vec![] },
1104 }),
1105 comment: None,
1106 owner: None,
1107 grants: vec![],
1108 rls_enabled: false,
1109 rls_forced: false,
1110 policies: vec![],
1111 storage: crate::ir::reloptions::TableStorageOptions::default(),
1112 };
1113 c.tables.push(child);
1114
1115 let g = build_create_graph(&c);
1116 assert!(
1117 has_edge(
1118 &g,
1119 &NodeId::Table(qn("app", "child")),
1120 &NodeId::Table(qn("app", "parent")),
1121 ),
1122 "expected child partition → parent table edge"
1123 );
1124 }
1125
1126 use crate::ir::user_type::{CompositeAttribute, UserType, UserTypeKind};
1129
1130 fn make_enum(schema: &str, name: &str) -> UserType {
1131 UserType {
1132 qname: qn(schema, name),
1133 kind: UserTypeKind::Enum { values: vec![] },
1134 comment: None,
1135 owner: None,
1136 grants: vec![],
1137 }
1138 }
1139
1140 fn make_composite_with_attr(schema: &str, name: &str, attr_type: ColumnType) -> UserType {
1141 UserType {
1142 qname: qn(schema, name),
1143 kind: UserTypeKind::Composite {
1144 attributes: vec![CompositeAttribute {
1145 name: id("val"),
1146 ty: attr_type,
1147 collation: None,
1148 }],
1149 },
1150 comment: None,
1151 owner: None,
1152 grants: vec![],
1153 }
1154 }
1155
1156 fn make_domain_over(schema: &str, name: &str, base: ColumnType) -> UserType {
1157 UserType {
1158 qname: qn(schema, name),
1159 kind: UserTypeKind::Domain {
1160 base,
1161 nullable: true,
1162 default: None,
1163 check_constraints: vec![],
1164 collation: None,
1165 },
1166 comment: None,
1167 owner: None,
1168 grants: vec![],
1169 }
1170 }
1171
1172 #[test]
1173 fn type_nodes_registered() {
1174 let mut c = Catalog::empty();
1175 c.schemas.push(crate::ir::schema::Schema::new(id("app")));
1176 c.types.push(make_enum("app", "status"));
1177 let g = build_create_graph(&c);
1178 let deps: Vec<_> = g
1180 .dependencies_of(&NodeId::Type(qn("app", "status")))
1181 .collect();
1182 assert_eq!(deps, vec![&NodeId::Schema(id("app"))]);
1183 assert_eq!(g.node_count(), 2);
1185 }
1186
1187 #[test]
1188 fn table_depends_on_user_defined_column_type() {
1189 let mut c = Catalog::empty();
1190 c.types.push(make_enum("app", "status"));
1191 c.tables.push(Table {
1192 qname: qn("app", "orders"),
1193 columns: vec![Column {
1194 name: id("status"),
1195 ty: ColumnType::UserDefined(qn("app", "status")),
1196 nullable: false,
1197 default: None,
1198 identity: None,
1199 generated: None,
1200 collation: None,
1201 storage: None,
1202 compression: None,
1203 comment: None,
1204 }],
1205 constraints: vec![],
1206 partition_by: None,
1207 partition_of: None,
1208 comment: None,
1209 owner: None,
1210 grants: vec![],
1211 rls_enabled: false,
1212 rls_forced: false,
1213 policies: vec![],
1214 storage: crate::ir::reloptions::TableStorageOptions::default(),
1215 });
1216 let g = build_create_graph(&c);
1217 assert!(
1218 has_edge(
1219 &g,
1220 &NodeId::Table(qn("app", "orders")),
1221 &NodeId::Type(qn("app", "status"))
1222 ),
1223 "table must depend on its user-defined column type"
1224 );
1225 }
1226
1227 #[test]
1228 fn composite_depends_on_user_defined_attribute_type() {
1229 let mut c = Catalog::empty();
1230 c.types.push(make_enum("app", "inner_t"));
1231 c.types.push(make_composite_with_attr(
1232 "app",
1233 "outer_t",
1234 ColumnType::UserDefined(qn("app", "inner_t")),
1235 ));
1236 let g = build_create_graph(&c);
1237 assert!(
1238 has_edge(
1239 &g,
1240 &NodeId::Type(qn("app", "outer_t")),
1241 &NodeId::Type(qn("app", "inner_t"))
1242 ),
1243 "composite must depend on the type of its user-defined attribute"
1244 );
1245 }
1246
1247 #[test]
1248 fn domain_depends_on_user_defined_base_type() {
1249 let mut c = Catalog::empty();
1250 c.types.push(make_enum("app", "base_t"));
1251 c.types.push(make_domain_over(
1252 "app",
1253 "derived_t",
1254 ColumnType::UserDefined(qn("app", "base_t")),
1255 ));
1256 let g = build_create_graph(&c);
1257 assert!(
1258 has_edge(
1259 &g,
1260 &NodeId::Type(qn("app", "derived_t")),
1261 &NodeId::Type(qn("app", "base_t"))
1262 ),
1263 "domain must depend on its user-defined base type"
1264 );
1265 }
1266
1267 fn make_range(
1270 schema: &str,
1271 name: &str,
1272 subtype: QualifiedName,
1273 canonical: Option<QualifiedName>,
1274 subtype_diff: Option<QualifiedName>,
1275 ) -> UserType {
1276 UserType {
1277 qname: qn(schema, name),
1278 kind: UserTypeKind::Range {
1279 subtype,
1280 subtype_opclass: None,
1281 collation: None,
1282 canonical,
1283 subtype_diff,
1284 multirange_type_name: None,
1285 },
1286 comment: None,
1287 owner: None,
1288 grants: vec![],
1289 }
1290 }
1291
1292 fn make_function(schema: &str, name: &str) -> crate::ir::function::Function {
1293 use crate::ir::function::{
1294 FunctionLanguage, NormalizedArgTypes, ParallelSafety, ReturnType, SecurityMode,
1295 Volatility,
1296 };
1297 use crate::parse::normalize_body::NormalizedBody;
1298 crate::ir::function::Function {
1299 qname: qn(schema, name),
1300 args: vec![],
1301 arg_types_normalized: NormalizedArgTypes::from_args(&[]),
1302 return_type: ReturnType::Scalar {
1303 ty: ColumnType::Integer,
1304 },
1305 language: FunctionLanguage::Sql,
1306 body: NormalizedBody::empty(),
1307 body_dependencies: vec![],
1308 volatility: Volatility::Volatile,
1309 strict: false,
1310 security: SecurityMode::Invoker,
1311 parallel: ParallelSafety::Unsafe,
1312 leakproof: false,
1313 cost: None,
1314 rows: None,
1315 comment: None,
1316 owner: None,
1317 grants: vec![],
1318 }
1319 }
1320
1321 #[test]
1322 fn range_with_builtin_subtype_adds_no_subtype_edge() {
1323 let mut c = Catalog::empty();
1324 c.schemas.push(crate::ir::schema::Schema::new(id("app")));
1325 c.types.push(make_range(
1326 "app",
1327 "ir",
1328 qn("pg_catalog", "int4"),
1329 None,
1330 None,
1331 ));
1332 let g = build_create_graph(&c);
1333 let deps: Vec<&NodeId> = g.dependencies_of(&NodeId::Type(qn("app", "ir"))).collect();
1335 assert_eq!(deps, vec![&NodeId::Schema(id("app"))]);
1336 }
1337
1338 #[test]
1339 fn range_with_managed_subtype_adds_type_edge() {
1340 let mut c = Catalog::empty();
1341 c.schemas.push(crate::ir::schema::Schema::new(id("app")));
1342 c.types.push(make_enum("app", "base_t"));
1343 c.types.push(make_range(
1344 "app",
1345 "myrange",
1346 qn("app", "base_t"),
1347 None,
1348 None,
1349 ));
1350 let g = build_create_graph(&c);
1351 assert!(
1352 has_edge(
1353 &g,
1354 &NodeId::Type(qn("app", "myrange")),
1355 &NodeId::Type(qn("app", "base_t"))
1356 ),
1357 "range type must depend on its managed subtype"
1358 );
1359 }
1360
1361 #[test]
1362 fn range_canonical_fn_adds_function_edge() {
1363 let mut c = Catalog::empty();
1364 c.schemas.push(crate::ir::schema::Schema::new(id("app")));
1365 let f = make_function("app", "canon_fn");
1366 let arg_types = f.arg_types_normalized.clone();
1367 c.functions.push(f);
1368 c.types.push(make_range(
1369 "app",
1370 "myrange",
1371 qn("pg_catalog", "int4"),
1372 Some(qn("app", "canon_fn")),
1373 None,
1374 ));
1375 let g = build_create_graph(&c);
1376 assert!(
1377 has_edge(
1378 &g,
1379 &NodeId::Type(qn("app", "myrange")),
1380 &NodeId::Function(qn("app", "canon_fn"), arg_types),
1381 ),
1382 "range type must depend on its canonical function"
1383 );
1384 }
1385
1386 #[test]
1387 fn range_subtype_diff_fn_adds_function_edge() {
1388 let mut c = Catalog::empty();
1389 c.schemas.push(crate::ir::schema::Schema::new(id("app")));
1390 let f = make_function("app", "diff_fn");
1391 let arg_types = f.arg_types_normalized.clone();
1392 c.functions.push(f);
1393 c.types.push(make_range(
1394 "app",
1395 "myrange",
1396 qn("pg_catalog", "int4"),
1397 None,
1398 Some(qn("app", "diff_fn")),
1399 ));
1400 let g = build_create_graph(&c);
1401 assert!(
1402 has_edge(
1403 &g,
1404 &NodeId::Type(qn("app", "myrange")),
1405 &NodeId::Function(qn("app", "diff_fn"), arg_types),
1406 ),
1407 "range type must depend on its subtype_diff function"
1408 );
1409 }
1410
1411 #[test]
1412 fn range_unmanaged_canonical_fn_adds_no_edge() {
1413 let mut c = Catalog::empty();
1416 c.schemas.push(crate::ir::schema::Schema::new(id("app")));
1417 c.types.push(make_range(
1418 "app",
1419 "myrange",
1420 qn("pg_catalog", "int4"),
1421 Some(qn("app", "unmanaged_fn")),
1422 None,
1423 ));
1424 let g = build_create_graph(&c);
1425 let deps: Vec<&NodeId> = g
1427 .dependencies_of(&NodeId::Type(qn("app", "myrange")))
1428 .collect();
1429 assert_eq!(deps, vec![&NodeId::Schema(id("app"))]);
1430 }
1431
1432 use crate::ir::collation::{Collation, CollationProvider};
1435
1436 fn make_collation(schema: &str, name: &str) -> Collation {
1437 Collation {
1438 qname: qn(schema, name),
1439 provider: CollationProvider::Libc,
1440 lc_collate: "C".into(),
1441 lc_ctype: "C".into(),
1442 deterministic: true,
1443 version: None,
1444 owner: None,
1445 comment: None,
1446 }
1447 }
1448
1449 fn make_table_empty(schema: &str, name: &str) -> Table {
1450 Table {
1451 qname: qn(schema, name),
1452 columns: vec![],
1453 constraints: vec![],
1454 partition_by: None,
1455 partition_of: None,
1456 comment: None,
1457 owner: None,
1458 grants: vec![],
1459 rls_enabled: false,
1460 rls_forced: false,
1461 policies: vec![],
1462 storage: crate::ir::reloptions::TableStorageOptions::default(),
1463 }
1464 }
1465
1466 fn col_text_with_collation(name: &str, collation: QualifiedName) -> Column {
1467 Column {
1468 name: id(name),
1469 ty: ColumnType::Text,
1470 nullable: true,
1471 default: None,
1472 identity: None,
1473 generated: None,
1474 collation: Some(collation),
1475 storage: None,
1476 compression: None,
1477 comment: None,
1478 }
1479 }
1480
1481 fn make_domain_with_collation(schema: &str, name: &str, collation: QualifiedName) -> UserType {
1482 UserType {
1483 qname: qn(schema, name),
1484 kind: UserTypeKind::Domain {
1485 base: ColumnType::Text,
1486 nullable: true,
1487 default: None,
1488 check_constraints: vec![],
1489 collation: Some(collation),
1490 },
1491 comment: None,
1492 owner: None,
1493 grants: vec![],
1494 }
1495 }
1496
1497 fn make_range_with_collation(
1498 schema: &str,
1499 name: &str,
1500 subtype: QualifiedName,
1501 collation: QualifiedName,
1502 ) -> UserType {
1503 UserType {
1504 qname: qn(schema, name),
1505 kind: UserTypeKind::Range {
1506 subtype,
1507 subtype_opclass: None,
1508 collation: Some(collation),
1509 canonical: None,
1510 subtype_diff: None,
1511 multirange_type_name: None,
1512 },
1513 comment: None,
1514 owner: None,
1515 grants: vec![],
1516 }
1517 }
1518
1519 fn make_composite_with_collated_attr(
1520 schema: &str,
1521 name: &str,
1522 collation: QualifiedName,
1523 ) -> UserType {
1524 UserType {
1525 qname: qn(schema, name),
1526 kind: UserTypeKind::Composite {
1527 attributes: vec![CompositeAttribute {
1528 name: id("val"),
1529 ty: ColumnType::Text,
1530 collation: Some(collation),
1531 }],
1532 },
1533 comment: None,
1534 owner: None,
1535 grants: vec![],
1536 }
1537 }
1538
1539 #[test]
1540 fn collation_node_registered() {
1541 let mut c = Catalog::empty();
1542 c.schemas.push(crate::ir::schema::Schema::new(id("app")));
1543 c.collations.push(make_collation("app", "ci"));
1544 let g = build_create_graph(&c);
1545 assert_eq!(g.node_count(), 2);
1547 }
1548
1549 #[test]
1550 fn column_with_managed_collation_adds_edge() {
1551 let mut cat = Catalog::empty();
1552 cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
1553 cat.collations.push(make_collation("app", "ci"));
1554 let mut t = make_table_empty("app", "users");
1555 t.columns
1556 .push(col_text_with_collation("email", qn("app", "ci")));
1557 cat.tables.push(t);
1558 let g = build_create_graph(&cat);
1559 assert!(
1560 has_edge(
1561 &g,
1562 &NodeId::Table(qn("app", "users")),
1563 &NodeId::Collation(qn("app", "ci")),
1564 ),
1565 "table must depend on its column's managed collation"
1566 );
1567 }
1568
1569 #[test]
1570 fn column_with_pg_catalog_collation_no_edge() {
1571 let mut cat = Catalog::empty();
1572 cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
1573 let mut t = make_table_empty("app", "users");
1574 t.columns
1575 .push(col_text_with_collation("email", qn("pg_catalog", "C")));
1576 cat.tables.push(t);
1577 let g = build_create_graph(&cat);
1578 assert!(
1579 !g.edges().any(|(_, to)| matches!(to, NodeId::Collation(_))),
1580 "no collation edge should be added for pg_catalog.C"
1581 );
1582 }
1583
1584 #[test]
1585 fn column_with_builtin_shortname_collation_no_edge() {
1586 let mut cat = Catalog::empty();
1589 cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
1590 let mut t = make_table_empty("app", "users");
1591 t.columns
1592 .push(col_text_with_collation("email", qn("app", "POSIX")));
1593 cat.tables.push(t);
1594 let g = build_create_graph(&cat);
1595 assert!(!g.edges().any(|(_, to)| matches!(to, NodeId::Collation(_))),);
1596 }
1597
1598 #[test]
1599 fn column_with_unmanaged_collation_no_edge() {
1600 let mut cat = Catalog::empty();
1604 cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
1605 let mut t = make_table_empty("app", "users");
1606 t.columns
1607 .push(col_text_with_collation("email", qn("app", "unmanaged")));
1608 cat.tables.push(t);
1609 let g = build_create_graph(&cat);
1610 assert!(!g.edges().any(|(_, to)| matches!(to, NodeId::Collation(_))),);
1611 }
1612
1613 #[test]
1614 fn domain_with_managed_collation_adds_edge() {
1615 let mut cat = Catalog::empty();
1616 cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
1617 cat.collations.push(make_collation("app", "ci"));
1618 cat.types.push(make_domain_with_collation(
1619 "app",
1620 "email_t",
1621 qn("app", "ci"),
1622 ));
1623 let g = build_create_graph(&cat);
1624 assert!(
1625 has_edge(
1626 &g,
1627 &NodeId::Type(qn("app", "email_t")),
1628 &NodeId::Collation(qn("app", "ci")),
1629 ),
1630 "domain must depend on its managed collation"
1631 );
1632 }
1633
1634 #[test]
1635 fn domain_with_pg_catalog_collation_no_edge() {
1636 let mut cat = Catalog::empty();
1637 cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
1638 cat.types.push(make_domain_with_collation(
1639 "app",
1640 "email_t",
1641 qn("pg_catalog", "C"),
1642 ));
1643 let g = build_create_graph(&cat);
1644 assert!(!g.edges().any(|(_, to)| matches!(to, NodeId::Collation(_))),);
1645 }
1646
1647 #[test]
1648 fn range_with_managed_collation_adds_edge() {
1649 let mut cat = Catalog::empty();
1650 cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
1651 cat.collations.push(make_collation("app", "ci"));
1652 cat.types.push(make_range_with_collation(
1653 "app",
1654 "textrange",
1655 qn("pg_catalog", "text"),
1656 qn("app", "ci"),
1657 ));
1658 let g = build_create_graph(&cat);
1659 assert!(
1660 has_edge(
1661 &g,
1662 &NodeId::Type(qn("app", "textrange")),
1663 &NodeId::Collation(qn("app", "ci")),
1664 ),
1665 "range type must depend on its managed collation"
1666 );
1667 }
1668
1669 #[test]
1670 fn range_with_pg_catalog_collation_no_edge() {
1671 let mut cat = Catalog::empty();
1672 cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
1673 cat.types.push(make_range_with_collation(
1674 "app",
1675 "textrange",
1676 qn("pg_catalog", "text"),
1677 qn("pg_catalog", "C"),
1678 ));
1679 let g = build_create_graph(&cat);
1680 assert!(!g.edges().any(|(_, to)| matches!(to, NodeId::Collation(_))),);
1681 }
1682
1683 #[test]
1684 fn composite_attribute_with_managed_collation_adds_edge() {
1685 let mut cat = Catalog::empty();
1686 cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
1687 cat.collations.push(make_collation("app", "ci"));
1688 cat.types.push(make_composite_with_collated_attr(
1689 "app",
1690 "addr_t",
1691 qn("app", "ci"),
1692 ));
1693 let g = build_create_graph(&cat);
1694 assert!(
1695 has_edge(
1696 &g,
1697 &NodeId::Type(qn("app", "addr_t")),
1698 &NodeId::Collation(qn("app", "ci")),
1699 ),
1700 "composite type must depend on collation of a collated attribute"
1701 );
1702 }
1703
1704 #[test]
1705 fn composite_attribute_with_pg_catalog_collation_no_edge() {
1706 let mut cat = Catalog::empty();
1707 cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
1708 cat.types.push(make_composite_with_collated_attr(
1709 "app",
1710 "addr_t",
1711 qn("pg_catalog", "C"),
1712 ));
1713 let g = build_create_graph(&cat);
1714 assert!(!g.edges().any(|(_, to)| matches!(to, NodeId::Collation(_))),);
1715 }
1716
1717 #[test]
1718 fn collation_ordered_before_table_using_it() {
1719 let mut cat = Catalog::empty();
1722 cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
1723 cat.collations.push(make_collation("app", "ci"));
1724 let mut t = make_table_empty("app", "users");
1725 t.columns
1726 .push(col_text_with_collation("email", qn("app", "ci")));
1727 cat.tables.push(t);
1728 let g = build_create_graph(&cat);
1729 let order = g.topological_sort().expect("no cycle");
1730 let coll_pos = order
1731 .iter()
1732 .position(|n| n == &NodeId::Collation(qn("app", "ci")))
1733 .expect("collation in order");
1734 let table_pos = order
1735 .iter()
1736 .position(|n| n == &NodeId::Table(qn("app", "users")))
1737 .expect("table in order");
1738 assert!(
1739 coll_pos < table_pos,
1740 "collation must be created before the table that uses it"
1741 );
1742 }
1743
1744 #[test]
1745 fn type_create_ordering_respects_edges() {
1746 let mut c = Catalog::empty();
1748 c.types.push(make_enum("app", "base_t"));
1749 c.types.push(make_domain_over(
1750 "app",
1751 "derived_t",
1752 ColumnType::UserDefined(qn("app", "base_t")),
1753 ));
1754 let g = build_create_graph(&c);
1755 let order = g.topological_sort().expect("no cycle expected");
1756 let base_pos = order
1757 .iter()
1758 .position(|n| n == &NodeId::Type(qn("app", "base_t")))
1759 .expect("base_t in order");
1760 let derived_pos = order
1761 .iter()
1762 .position(|n| n == &NodeId::Type(qn("app", "derived_t")))
1763 .expect("derived_t in order");
1764 assert!(base_pos < derived_pos, "base_t must come before derived_t");
1765 }
1766}