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 EventTrigger(Identifier),
73 Statistic(QualifiedName),
75 Collation(QualifiedName),
82 Aggregate(QualifiedName, NormalizedArgTypes),
87 Cast(QualifiedName, QualifiedName),
92}
93
94fn find_sfunc<'a>(
111 catalog: &'a Catalog,
112 fn_qname: &QualifiedName,
113 implied_arg_types: &[ColumnType],
114) -> Option<&'a crate::ir::function::Function> {
115 let candidates: Vec<&crate::ir::function::Function> = catalog
116 .functions
117 .iter()
118 .filter(|f| &f.qname == fn_qname)
119 .collect();
120 match candidates.as_slice() {
121 [] => None,
122 [only] => Some(only),
123 many => {
124 let by_arity: Vec<&crate::ir::function::Function> = many
125 .iter()
126 .copied()
127 .filter(|f| positional_arg_types(f).len() == implied_arg_types.len())
128 .collect();
129 match by_arity.as_slice() {
130 [] => None,
131 [only] => Some(only),
132 still_many => still_many.iter().copied().find(|f| {
133 positional_arg_types(f)
134 .into_iter()
135 .eq(implied_arg_types.iter())
136 }),
137 }
138 }
139 }
140}
141
142fn positional_arg_types(f: &crate::ir::function::Function) -> Vec<&ColumnType> {
145 use crate::ir::function::ArgMode;
146 f.args
147 .iter()
148 .filter(|a| matches!(a.mode, ArgMode::In | ArgMode::InOut | ArgMode::Variadic))
149 .map(|a| &a.ty)
150 .collect()
151}
152
153pub(crate) fn aggregate_arg_types(arg_types: &[ColumnType]) -> NormalizedArgTypes {
158 use crate::ir::function::{ArgMode, FunctionArg};
159 let args: Vec<FunctionArg> = arg_types
160 .iter()
161 .map(|ty| FunctionArg {
162 name: None,
163 mode: ArgMode::In,
164 ty: ty.clone(),
165 default: None,
166 })
167 .collect();
168 NormalizedArgTypes::from_args(&args)
169}
170
171fn should_add_collation_edge(qname: &QualifiedName, catalog: &Catalog) -> bool {
175 if qname.schema.as_str() == "pg_catalog" {
176 return false;
177 }
178 let name = qname.name.as_str();
179 if BUILTIN_COLLATIONS.contains(&name) {
180 return false;
181 }
182 catalog.collations.iter().any(|c| &c.qname == qname)
183}
184
185#[allow(clippy::too_many_lines)] pub fn build_create_graph(catalog: &Catalog) -> Graph<NodeId> {
191 let mut g = Graph::new();
192
193 for s in &catalog.schemas {
195 g.add_node(NodeId::Schema(s.name.clone()));
196 }
197 for t in &catalog.tables {
198 g.add_node(NodeId::Table(t.qname.clone()));
199 }
200 for i in &catalog.indexes {
201 g.add_node(NodeId::Index(i.qname.clone()));
202 }
203 for s in &catalog.sequences {
204 g.add_node(NodeId::Sequence(s.qname.clone()));
205 }
206 for t in &catalog.tables {
207 for c in &t.constraints {
208 g.add_node(NodeId::Constraint {
209 table: t.qname.clone(),
210 name: c.qname.name.clone(),
211 });
212 }
213 }
214 for v in &catalog.views {
217 g.add_node(NodeId::View(v.qname.clone()));
218 }
219 for mv in &catalog.materialized_views {
220 g.add_node(NodeId::Mv(mv.qname.clone()));
221 }
222 for t in &catalog.types {
224 g.add_node(NodeId::Type(t.qname.clone()));
225 }
226 for f in &catalog.functions {
228 g.add_node(NodeId::Function(
229 f.qname.clone(),
230 f.arg_types_normalized.clone(),
231 ));
232 }
233 for p in &catalog.procedures {
234 g.add_node(NodeId::Procedure(p.qname.clone()));
235 }
236 for t in &catalog.triggers {
238 g.add_node(NodeId::Trigger(t.qname.clone()));
239 let target_node = if catalog.tables.iter().any(|x| x.qname == t.table) {
240 NodeId::Table(t.table.clone())
241 } else if catalog.views.iter().any(|x| x.qname == t.table) {
242 NodeId::View(t.table.clone())
243 } else if catalog
244 .materialized_views
245 .iter()
246 .any(|x| x.qname == t.table)
247 {
248 NodeId::Mv(t.table.clone())
249 } else {
250 continue;
254 };
255 g.add_edge(NodeId::Trigger(t.qname.clone()), target_node);
256 if let Some(func) = catalog
257 .functions
258 .iter()
259 .find(|f| f.qname == t.function_qname)
260 {
261 g.add_edge(
262 NodeId::Trigger(t.qname.clone()),
263 NodeId::Function(t.function_qname.clone(), func.arg_types_normalized.clone()),
264 );
265 }
266 }
267 for e in &catalog.extensions {
269 g.add_node(NodeId::Extension(e.name.clone()));
270 if let Some(schema) = &e.schema {
271 g.add_edge(
272 NodeId::Extension(e.name.clone()),
273 NodeId::Schema(schema.clone()),
274 );
275 }
276 }
277 for p in &catalog.publications {
282 let pub_node = NodeId::Publication(p.name.clone());
283 g.add_node(pub_node.clone());
284 if let crate::ir::publication::PublicationScope::Selective { schemas, tables } = &p.scope {
285 for t in tables {
286 g.add_edge(pub_node.clone(), NodeId::Table(t.qname.clone()));
287 }
288 for s in schemas {
289 g.add_edge(pub_node.clone(), NodeId::Schema(s.clone()));
290 }
291 }
292 }
293 for s in &catalog.subscriptions {
298 g.add_node(NodeId::Subscription(s.name.clone()));
299 }
300 for et in &catalog.event_triggers {
303 g.add_node(NodeId::EventTrigger(et.name.clone()));
304 if let Some(func) = catalog.functions.iter().find(|f| f.qname == et.function) {
305 g.add_edge(
306 NodeId::EventTrigger(et.name.clone()),
307 NodeId::Function(et.function.clone(), func.arg_types_normalized.clone()),
308 );
309 }
310 }
311 for s in &catalog.statistics {
314 let stat_node = NodeId::Statistic(s.qname.clone());
315 g.add_node(stat_node.clone());
316 g.add_edge(stat_node, NodeId::Table(s.target.clone()));
317 }
318 for c in &catalog.collations {
325 let coll_node = NodeId::Collation(c.qname.clone());
326 g.add_node(coll_node.clone());
327 g.add_edge(coll_node, NodeId::Schema(c.qname.schema.clone()));
328 }
329 for agg in &catalog.aggregates {
336 let agg_node = NodeId::Aggregate(agg.qname.clone(), aggregate_arg_types(&agg.arg_types));
337 g.add_node(agg_node.clone());
338 let mut sfunc_sig = Vec::with_capacity(1 + agg.arg_types.len());
340 sfunc_sig.push(agg.state_type.clone());
341 sfunc_sig.extend(agg.arg_types.iter().cloned());
342 if let Some(sfunc) = find_sfunc(catalog, &agg.sfunc, &sfunc_sig) {
343 g.add_edge(
344 agg_node.clone(),
345 NodeId::Function(sfunc.qname.clone(), sfunc.arg_types_normalized.clone()),
346 );
347 }
348 if let Some(finalfunc_qname) = &agg.finalfunc
350 && let Some(finalfunc) = find_sfunc(
351 catalog,
352 finalfunc_qname,
353 std::slice::from_ref(&agg.state_type),
354 )
355 {
356 g.add_edge(
357 agg_node,
358 NodeId::Function(
359 finalfunc.qname.clone(),
360 finalfunc.arg_types_normalized.clone(),
361 ),
362 );
363 }
364 }
365 for cast in &catalog.casts {
375 let cast_node = NodeId::Cast(cast.source.clone(), cast.target.clone());
376 g.add_node(cast_node.clone());
377 if let crate::ir::cast::CastMethod::Function { name, arg_types } = &cast.method
379 && let Some(func) = find_sfunc(catalog, name, arg_types)
380 {
381 g.add_edge(
382 cast_node.clone(),
383 NodeId::Function(func.qname.clone(), func.arg_types_normalized.clone()),
384 );
385 }
386 if catalog.types.iter().any(|t| t.qname == cast.source) {
388 g.add_edge(cast_node.clone(), NodeId::Type(cast.source.clone()));
389 }
390 if catalog.types.iter().any(|t| t.qname == cast.target) {
392 g.add_edge(cast_node.clone(), NodeId::Type(cast.target.clone()));
393 }
394 }
395
396 for t in &catalog.types {
399 g.add_edge(
400 NodeId::Type(t.qname.clone()),
401 NodeId::Schema(t.qname.schema.clone()),
402 );
403 }
404 for f in &catalog.functions {
406 let node = NodeId::Function(f.qname.clone(), f.arg_types_normalized.clone());
407 g.add_edge(node, NodeId::Schema(f.qname.schema.clone()));
408 }
409 for p in &catalog.procedures {
410 g.add_edge(
411 NodeId::Procedure(p.qname.clone()),
412 NodeId::Schema(p.qname.schema.clone()),
413 );
414 }
415
416 for ut in &catalog.types {
422 match &ut.kind {
423 UserTypeKind::Composite { attributes } => {
424 for attr in attributes {
425 if let ColumnType::UserDefined(dep_qname) = &attr.ty {
426 g.add_edge(
427 NodeId::Type(ut.qname.clone()),
428 NodeId::Type(dep_qname.clone()),
429 );
430 }
431 if let Some(coll_qname) = &attr.collation
432 && should_add_collation_edge(coll_qname, catalog)
433 {
434 g.add_edge(
435 NodeId::Type(ut.qname.clone()),
436 NodeId::Collation(coll_qname.clone()),
437 );
438 }
439 }
440 }
441 UserTypeKind::Domain {
442 base, collation, ..
443 } => {
444 if let ColumnType::UserDefined(base_qname) = base {
445 g.add_edge(
446 NodeId::Type(ut.qname.clone()),
447 NodeId::Type(base_qname.clone()),
448 );
449 }
450 if let Some(coll_qname) = collation
451 && should_add_collation_edge(coll_qname, catalog)
452 {
453 g.add_edge(
454 NodeId::Type(ut.qname.clone()),
455 NodeId::Collation(coll_qname.clone()),
456 );
457 }
458 }
459 UserTypeKind::Range {
460 subtype,
461 canonical,
462 subtype_diff,
463 collation,
464 ..
465 } => {
466 if subtype.schema.as_str() != "pg_catalog"
468 && catalog.types.iter().any(|t| &t.qname == subtype)
469 {
470 g.add_edge(
471 NodeId::Type(ut.qname.clone()),
472 NodeId::Type(subtype.clone()),
473 );
474 }
475 if let Some(fn_qname) = canonical
476 && let Some(f) = catalog.functions.iter().find(|f| &f.qname == fn_qname)
477 {
478 g.add_edge(
479 NodeId::Type(ut.qname.clone()),
480 NodeId::Function(f.qname.clone(), f.arg_types_normalized.clone()),
481 );
482 }
483 if let Some(fn_qname) = subtype_diff
484 && let Some(f) = catalog.functions.iter().find(|f| &f.qname == fn_qname)
485 {
486 g.add_edge(
487 NodeId::Type(ut.qname.clone()),
488 NodeId::Function(f.qname.clone(), f.arg_types_normalized.clone()),
489 );
490 }
491 if let Some(coll_qname) = collation
492 && should_add_collation_edge(coll_qname, catalog)
493 {
494 g.add_edge(
495 NodeId::Type(ut.qname.clone()),
496 NodeId::Collation(coll_qname.clone()),
497 );
498 }
499 }
500 UserTypeKind::Enum { .. } => {}
501 }
502 }
503
504 for t in &catalog.tables {
509 for col in &t.columns {
510 if let ColumnType::UserDefined(type_qname) = &col.ty {
511 g.add_edge(
512 NodeId::Table(t.qname.clone()),
513 NodeId::Type(type_qname.clone()),
514 );
515 }
516 if let Some(coll_qname) = &col.collation
517 && should_add_collation_edge(coll_qname, catalog)
518 {
519 g.add_edge(
520 NodeId::Table(t.qname.clone()),
521 NodeId::Collation(coll_qname.clone()),
522 );
523 }
524 }
525 }
526 for f in &catalog.functions {
529 let node = NodeId::Function(f.qname.clone(), f.arg_types_normalized.clone());
530 for arg in &f.args {
531 if let ColumnType::UserDefined(t_qname) = &arg.ty {
532 g.add_edge(node.clone(), NodeId::Type(t_qname.clone()));
533 }
534 }
535 match &f.return_type {
536 ReturnType::Scalar {
537 ty: ColumnType::UserDefined(t),
538 }
539 | ReturnType::SetOf {
540 ty: ColumnType::UserDefined(t),
541 } => {
542 g.add_edge(node.clone(), NodeId::Type(t.clone()));
543 }
544 ReturnType::Table { columns } => {
545 for col in columns {
546 if let ColumnType::UserDefined(t) = &col.ty {
547 g.add_edge(node.clone(), NodeId::Type(t.clone()));
548 }
549 }
550 }
551 _ => {}
552 }
553 }
554 for p in &catalog.procedures {
555 let node = NodeId::Procedure(p.qname.clone());
556 for arg in &p.args {
557 if let ColumnType::UserDefined(t_qname) = &arg.ty {
558 g.add_edge(node.clone(), NodeId::Type(t_qname.clone()));
559 }
560 }
561 }
562
563 for t in &catalog.tables {
569 g.add_edge(
570 NodeId::Table(t.qname.clone()),
571 NodeId::Schema(t.qname.schema.clone()),
572 );
573 for col in &t.columns {
574 if let Some(DefaultExpr::Sequence(seq_qname)) = &col.default {
575 g.add_edge(
576 NodeId::Table(t.qname.clone()),
577 NodeId::Sequence(seq_qname.clone()),
578 );
579 }
580 }
581 if let Some(po) = &t.partition_of {
582 g.add_edge(
584 NodeId::Table(t.qname.clone()),
585 NodeId::Table(po.parent.clone()),
586 );
587 }
588 }
589
590 for v in &catalog.views {
594 for dep in &v.body_dependencies {
595 g.add_edge(dep.from.clone(), dep.to.clone());
596 }
597 }
598 for mv in &catalog.materialized_views {
599 for dep in &mv.body_dependencies {
600 g.add_edge(dep.from.clone(), dep.to.clone());
601 }
602 }
603
604 for f in &catalog.functions {
606 for dep in &f.body_dependencies {
607 g.add_edge(dep.from.clone(), dep.to.clone());
608 }
609 }
610 for p in &catalog.procedures {
611 for dep in &p.body_dependencies {
612 g.add_edge(dep.from.clone(), dep.to.clone());
613 }
614 }
615
616 for i in &catalog.indexes {
620 use crate::ir::index::IndexParent;
621 let parent_node = match &i.on {
622 IndexParent::Table(q) => NodeId::Table(q.clone()),
623 IndexParent::Mv(q) => NodeId::Mv(q.clone()),
624 };
625 g.add_edge(NodeId::Index(i.qname.clone()), parent_node);
626 }
627
628 for t in &catalog.tables {
637 for c in &t.constraints {
638 let constraint_node = NodeId::Constraint {
639 table: t.qname.clone(),
640 name: c.qname.name.clone(),
641 };
642 g.add_edge(constraint_node.clone(), NodeId::Table(t.qname.clone()));
643 if let ConstraintKind::ForeignKey(fk) = &c.kind {
644 g.add_edge(constraint_node, NodeId::Table(fk.referenced_table.clone()));
645 if fk.referenced_table != t.qname {
648 g.add_edge(
649 NodeId::Table(t.qname.clone()),
650 NodeId::Table(fk.referenced_table.clone()),
651 );
652 }
653 }
654 }
655 }
656
657 for s in &catalog.sequences {
659 if let Some(owner) = &s.owned_by {
660 g.add_edge(
661 NodeId::Sequence(s.qname.clone()),
662 NodeId::Table(owner.table.clone()),
663 );
664 }
665 }
666
667 g
668}
669
670pub fn build_drop_graph(catalog: &Catalog) -> Graph<NodeId> {
673 build_create_graph(catalog)
674}
675
676#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
692pub enum DepSource {
693 Structural,
695 AstExtracted,
697 AstDeclared,
699}
700
701#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
706pub struct DepEdge {
707 pub from: NodeId,
709 pub to: NodeId,
711 pub source: DepSource,
713}
714
715#[cfg(test)]
716mod tests {
717 use super::*;
718 use crate::ir::column::Column;
719 use crate::ir::column_type::ColumnType;
720 use crate::ir::constraint::{
721 Constraint, ConstraintKind, Deferrable, FkMatchType, ForeignKey, ReferentialAction,
722 };
723 use crate::ir::index::{
724 Index, IndexColumn, IndexColumnExpr, IndexMethod, IndexParent, NullsOrder, SortOrder,
725 };
726 use crate::ir::schema::Schema;
727 use crate::ir::sequence::{Sequence, SequenceOwner};
728 use crate::ir::table::Table;
729
730 fn id(s: &str) -> Identifier {
731 Identifier::from_unquoted(s).unwrap()
732 }
733
734 fn qn(schema: &str, name: &str) -> QualifiedName {
735 QualifiedName::new(id(schema), id(name))
736 }
737
738 fn col_id_bigint() -> Column {
739 Column {
740 name: id("id"),
741 ty: ColumnType::BigInt,
742 nullable: false,
743 default: None,
744 identity: None,
745 generated: None,
746 collation: None,
747 storage: None,
748 compression: None,
749 comment: None,
750 }
751 }
752
753 fn col_text_notnull(name: &str) -> Column {
754 Column {
755 name: id(name),
756 ty: ColumnType::Text,
757 nullable: false,
758 default: None,
759 identity: None,
760 generated: None,
761 collation: None,
762 storage: None,
763 compression: None,
764 comment: None,
765 }
766 }
767
768 fn pk(name: &str, cols: &[&str]) -> Constraint {
769 Constraint {
770 qname: qn("app", name),
771 kind: ConstraintKind::PrimaryKey {
772 columns: cols.iter().map(|c| id(c)).collect(),
773 include: vec![],
774 },
775 deferrable: Deferrable::NotDeferrable,
776 comment: None,
777 }
778 }
779
780 fn fk(name: &str, ref_table: QualifiedName) -> Constraint {
781 Constraint {
782 qname: qn("app", name),
783 kind: ConstraintKind::ForeignKey(ForeignKey {
784 columns: vec![id("ref_id")],
785 referenced_table: ref_table,
786 referenced_columns: vec![id("id")],
787 on_update: ReferentialAction::NoAction,
788 on_delete: ReferentialAction::NoAction,
789 match_type: FkMatchType::Simple,
790 }),
791 deferrable: Deferrable::NotDeferrable,
792 comment: None,
793 }
794 }
795
796 fn has_edge(g: &Graph<NodeId>, from: &NodeId, to: &NodeId) -> bool {
797 g.dependencies_of(from).any(|n| n == to)
798 }
799
800 #[test]
801 fn empty_catalog_yields_empty_graph() {
802 let g = build_create_graph(&Catalog::empty());
803 assert_eq!(g.node_count(), 0);
804 }
805
806 #[test]
807 fn every_object_appears_as_a_node() {
808 let mut c = Catalog::empty();
809 c.schemas.push(Schema::new(id("app")));
810 c.tables.push(Table {
811 qname: qn("app", "users"),
812 columns: vec![col_id_bigint()],
813 constraints: vec![pk("users_pkey", &["id"])],
814 partition_by: None,
815 partition_of: None,
816 comment: None,
817 owner: None,
818 grants: vec![],
819 rls_enabled: false,
820 rls_forced: false,
821 policies: vec![],
822 storage: crate::ir::reloptions::TableStorageOptions::default(),
823 access_method: None,
824 });
825 c.indexes.push(Index {
826 qname: qn("app", "users_idx"),
827 on: IndexParent::Table(qn("app", "users")),
828 method: IndexMethod::BTree,
829 columns: vec![IndexColumn {
830 expr: IndexColumnExpr::Column(id("id")),
831 collation: None,
832 opclass: None,
833 sort_order: SortOrder::Asc,
834 nulls_order: NullsOrder::NullsLast,
835 }],
836 include: vec![],
837 unique: false,
838 nulls_not_distinct: false,
839 predicate: None,
840 tablespace: None,
841 comment: None,
842 storage: crate::ir::reloptions::IndexStorageOptions::default(),
843 });
844 c.sequences.push(Sequence {
845 qname: qn("app", "seq1"),
846 data_type: ColumnType::BigInt,
847 start: 1,
848 increment: 1,
849 min_value: None,
850 max_value: None,
851 cache: 1,
852 cycle: false,
853 owned_by: None,
854 comment: None,
855 owner: None,
856 grants: vec![],
857 });
858
859 let g = build_create_graph(&c);
860 assert_eq!(g.node_count(), 5);
862 }
863
864 #[test]
865 fn table_depends_on_its_schema() {
866 let mut c = Catalog::empty();
867 c.schemas.push(Schema::new(id("app")));
868 c.tables.push(Table {
869 qname: qn("app", "users"),
870 columns: vec![col_id_bigint()],
871 constraints: vec![],
872 partition_by: None,
873 partition_of: None,
874 comment: None,
875 owner: None,
876 grants: vec![],
877 rls_enabled: false,
878 rls_forced: false,
879 policies: vec![],
880 storage: crate::ir::reloptions::TableStorageOptions::default(),
881 access_method: None,
882 });
883 let g = build_create_graph(&c);
884 assert!(has_edge(
885 &g,
886 &NodeId::Table(qn("app", "users")),
887 &NodeId::Schema(id("app")),
888 ));
889 }
890
891 #[test]
892 fn index_depends_on_its_table() {
893 let mut c = Catalog::empty();
894 c.tables.push(Table {
895 qname: qn("app", "users"),
896 columns: vec![col_id_bigint()],
897 constraints: vec![],
898 partition_by: None,
899 partition_of: None,
900 comment: None,
901 owner: None,
902 grants: vec![],
903 rls_enabled: false,
904 rls_forced: false,
905 policies: vec![],
906 storage: crate::ir::reloptions::TableStorageOptions::default(),
907 access_method: None,
908 });
909 c.indexes.push(Index {
910 qname: qn("app", "users_idx"),
911 on: IndexParent::Table(qn("app", "users")),
912 method: IndexMethod::BTree,
913 columns: vec![IndexColumn {
914 expr: IndexColumnExpr::Column(id("id")),
915 collation: None,
916 opclass: None,
917 sort_order: SortOrder::Asc,
918 nulls_order: NullsOrder::NullsLast,
919 }],
920 include: vec![],
921 unique: false,
922 nulls_not_distinct: false,
923 predicate: None,
924 tablespace: None,
925 comment: None,
926 storage: crate::ir::reloptions::IndexStorageOptions::default(),
927 });
928 let g = build_create_graph(&c);
929 assert!(has_edge(
930 &g,
931 &NodeId::Index(qn("app", "users_idx")),
932 &NodeId::Table(qn("app", "users")),
933 ));
934 }
935
936 #[test]
937 fn fk_constraint_depends_on_both_endpoints() {
938 let mut c = Catalog::empty();
939 c.tables.push(Table {
940 qname: qn("app", "orgs"),
941 columns: vec![col_id_bigint()],
942 constraints: vec![pk("orgs_pkey", &["id"])],
943 partition_by: None,
944 partition_of: None,
945 comment: None,
946 owner: None,
947 grants: vec![],
948 rls_enabled: false,
949 rls_forced: false,
950 policies: vec![],
951 storage: crate::ir::reloptions::TableStorageOptions::default(),
952 access_method: None,
953 });
954 c.tables.push(Table {
955 qname: qn("app", "users"),
956 columns: vec![
957 col_id_bigint(),
958 Column {
959 name: id("ref_id"),
960 ty: ColumnType::BigInt,
961 nullable: false,
962 default: None,
963 identity: None,
964 generated: None,
965 collation: None,
966 storage: None,
967 compression: None,
968 comment: None,
969 },
970 ],
971 constraints: vec![fk("users_orgs_fk", qn("app", "orgs"))],
972 partition_by: None,
973 partition_of: None,
974 comment: None,
975 owner: None,
976 grants: vec![],
977 rls_enabled: false,
978 rls_forced: false,
979 policies: vec![],
980 storage: crate::ir::reloptions::TableStorageOptions::default(),
981 access_method: None,
982 });
983 let g = build_create_graph(&c);
984 let fk_node = NodeId::Constraint {
985 table: qn("app", "users"),
986 name: id("users_orgs_fk"),
987 };
988 assert!(has_edge(&g, &fk_node, &NodeId::Table(qn("app", "users"))));
990 assert!(has_edge(&g, &fk_node, &NodeId::Table(qn("app", "orgs"))));
992 }
993
994 #[test]
995 fn table_depends_on_default_sequence() {
996 let mut c = Catalog::empty();
997 c.sequences.push(Sequence {
998 qname: qn("app", "id_seq"),
999 data_type: ColumnType::BigInt,
1000 start: 1,
1001 increment: 1,
1002 min_value: None,
1003 max_value: None,
1004 cache: 1,
1005 cycle: false,
1006 owned_by: None,
1007 comment: None,
1008 owner: None,
1009 grants: vec![],
1010 });
1011 c.tables.push(Table {
1012 qname: qn("app", "users"),
1013 columns: vec![Column {
1014 name: id("id"),
1015 ty: ColumnType::BigInt,
1016 nullable: false,
1017 default: Some(DefaultExpr::Sequence(qn("app", "id_seq"))),
1018 identity: None,
1019 generated: None,
1020 collation: None,
1021 storage: None,
1022 compression: None,
1023 comment: None,
1024 }],
1025 constraints: vec![],
1026 partition_by: None,
1027 partition_of: None,
1028 comment: None,
1029 owner: None,
1030 grants: vec![],
1031 rls_enabled: false,
1032 rls_forced: false,
1033 policies: vec![],
1034 storage: crate::ir::reloptions::TableStorageOptions::default(),
1035 access_method: None,
1036 });
1037 let g = build_create_graph(&c);
1038 assert!(has_edge(
1039 &g,
1040 &NodeId::Table(qn("app", "users")),
1041 &NodeId::Sequence(qn("app", "id_seq")),
1042 ));
1043 }
1044
1045 #[test]
1046 fn owned_sequence_depends_on_owner_table() {
1047 let mut c = Catalog::empty();
1048 c.tables.push(Table {
1049 qname: qn("app", "users"),
1050 columns: vec![col_id_bigint()],
1051 constraints: vec![],
1052 partition_by: None,
1053 partition_of: None,
1054 comment: None,
1055 owner: None,
1056 grants: vec![],
1057 rls_enabled: false,
1058 rls_forced: false,
1059 policies: vec![],
1060 storage: crate::ir::reloptions::TableStorageOptions::default(),
1061 access_method: None,
1062 });
1063 c.sequences.push(Sequence {
1064 qname: qn("app", "users_id_seq"),
1065 data_type: ColumnType::BigInt,
1066 start: 1,
1067 increment: 1,
1068 min_value: None,
1069 max_value: None,
1070 cache: 1,
1071 cycle: false,
1072 owned_by: Some(SequenceOwner {
1073 table: qn("app", "users"),
1074 column: id("id"),
1075 }),
1076 comment: None,
1077 owner: None,
1078 grants: vec![],
1079 });
1080 let g = build_create_graph(&c);
1081 assert!(has_edge(
1082 &g,
1083 &NodeId::Sequence(qn("app", "users_id_seq")),
1084 &NodeId::Table(qn("app", "users")),
1085 ));
1086 }
1087
1088 #[test]
1089 fn non_fk_constraint_depends_only_on_its_table() {
1090 let mut c = Catalog::empty();
1091 c.tables.push(Table {
1092 qname: qn("app", "users"),
1093 columns: vec![col_id_bigint()],
1094 constraints: vec![pk("users_pkey", &["id"])],
1095 partition_by: None,
1096 partition_of: None,
1097 comment: None,
1098 owner: None,
1099 grants: vec![],
1100 rls_enabled: false,
1101 rls_forced: false,
1102 policies: vec![],
1103 storage: crate::ir::reloptions::TableStorageOptions::default(),
1104 access_method: None,
1105 });
1106 let g = build_create_graph(&c);
1107 let pk_node = NodeId::Constraint {
1108 table: qn("app", "users"),
1109 name: id("users_pkey"),
1110 };
1111 let deps: Vec<&NodeId> = g.dependencies_of(&pk_node).collect();
1112 assert_eq!(deps, vec![&NodeId::Table(qn("app", "users"))]);
1113 }
1114
1115 #[test]
1116 fn drop_graph_matches_create_graph() {
1117 let mut c = Catalog::empty();
1118 c.tables.push(Table {
1119 qname: qn("app", "users"),
1120 columns: vec![col_id_bigint()],
1121 constraints: vec![pk("users_pkey", &["id"])],
1122 partition_by: None,
1123 partition_of: None,
1124 comment: None,
1125 owner: None,
1126 grants: vec![],
1127 rls_enabled: false,
1128 rls_forced: false,
1129 policies: vec![],
1130 storage: crate::ir::reloptions::TableStorageOptions::default(),
1131 access_method: None,
1132 });
1133 let cg = build_create_graph(&c);
1135 let dg = build_drop_graph(&c);
1136 assert_eq!(cg.topological_sort(), dg.topological_sort());
1137 }
1138
1139 #[test]
1140 fn fk_cycle_produces_table_level_cycle() {
1141 let mut c = Catalog::empty();
1144 c.tables.push(Table {
1145 qname: qn("app", "a"),
1146 columns: vec![
1147 col_id_bigint(),
1148 Column {
1149 name: id("ref_id"),
1150 ty: ColumnType::BigInt,
1151 nullable: false,
1152 default: None,
1153 identity: None,
1154 generated: None,
1155 collation: None,
1156 storage: None,
1157 compression: None,
1158 comment: None,
1159 },
1160 ],
1161 constraints: vec![pk("a_pk", &["id"]), fk("a_to_b", qn("app", "b"))],
1162 partition_by: None,
1163 partition_of: None,
1164 comment: None,
1165 owner: None,
1166 grants: vec![],
1167 rls_enabled: false,
1168 rls_forced: false,
1169 policies: vec![],
1170 storage: crate::ir::reloptions::TableStorageOptions::default(),
1171 access_method: None,
1172 });
1173 c.tables.push(Table {
1174 qname: qn("app", "b"),
1175 columns: vec![
1176 col_id_bigint(),
1177 Column {
1178 name: id("ref_id"),
1179 ty: ColumnType::BigInt,
1180 nullable: false,
1181 default: None,
1182 identity: None,
1183 generated: None,
1184 collation: None,
1185 storage: None,
1186 compression: None,
1187 comment: None,
1188 },
1189 ],
1190 constraints: vec![pk("b_pk", &["id"]), fk("b_to_a", qn("app", "a"))],
1191 partition_by: None,
1192 partition_of: None,
1193 comment: None,
1194 owner: None,
1195 grants: vec![],
1196 rls_enabled: false,
1197 rls_forced: false,
1198 policies: vec![],
1199 storage: crate::ir::reloptions::TableStorageOptions::default(),
1200 access_method: None,
1201 });
1202 let g = build_create_graph(&c);
1203 let err = g.topological_sort().unwrap_err();
1204 assert!(err.nodes.contains(&NodeId::Table(qn("app", "a"))));
1205 assert!(err.nodes.contains(&NodeId::Table(qn("app", "b"))));
1206 }
1207
1208 #[test]
1209 fn self_referential_fk_does_not_cycle() {
1210 let mut c = Catalog::empty();
1213 c.tables.push(Table {
1214 qname: qn("app", "tree"),
1215 columns: vec![
1216 col_id_bigint(),
1217 Column {
1218 name: id("ref_id"),
1219 ty: ColumnType::BigInt,
1220 nullable: true,
1221 default: None,
1222 identity: None,
1223 generated: None,
1224 collation: None,
1225 storage: None,
1226 compression: None,
1227 comment: None,
1228 },
1229 ],
1230 constraints: vec![
1231 pk("tree_pk", &["id"]),
1232 fk("tree_parent_fk", qn("app", "tree")),
1233 ],
1234 partition_by: None,
1235 partition_of: None,
1236 comment: None,
1237 owner: None,
1238 grants: vec![],
1239 rls_enabled: false,
1240 rls_forced: false,
1241 policies: vec![],
1242 storage: crate::ir::reloptions::TableStorageOptions::default(),
1243 access_method: None,
1244 });
1245 let g = build_create_graph(&c);
1246 assert!(g.topological_sort().is_ok());
1247 }
1248
1249 #[test]
1250 fn partition_child_depends_on_parent() {
1251 use crate::ir::partition::{PartitionBounds, PartitionBy, PartitionOf};
1253 let mut c = Catalog::empty();
1254
1255 let parent = Table {
1257 qname: qn("app", "parent"),
1258 columns: vec![col_id_bigint(), col_text_notnull("status")],
1259 constraints: vec![pk("parent_pkey", &["id"])],
1260 partition_by: Some(PartitionBy {
1261 strategy: crate::ir::partition::PartitionStrategy::List,
1262 columns: vec![crate::ir::partition::PartitionColumn {
1263 kind: crate::ir::partition::PartitionColumnKind::Column(id("status")),
1264 collation: None,
1265 opclass: None,
1266 }],
1267 }),
1268 partition_of: None,
1269 comment: None,
1270 owner: None,
1271 grants: vec![],
1272 rls_enabled: false,
1273 rls_forced: false,
1274 policies: vec![],
1275 storage: crate::ir::reloptions::TableStorageOptions::default(),
1276 access_method: None,
1277 };
1278 c.tables.push(parent);
1279
1280 let child = Table {
1282 qname: qn("app", "child"),
1283 columns: vec![col_id_bigint(), col_text_notnull("status")],
1284 constraints: vec![],
1285 partition_by: None,
1286 partition_of: Some(PartitionOf {
1287 parent: qn("app", "parent"),
1288 bounds: PartitionBounds::List { values: vec![] },
1289 }),
1290 comment: None,
1291 owner: None,
1292 grants: vec![],
1293 rls_enabled: false,
1294 rls_forced: false,
1295 policies: vec![],
1296 storage: crate::ir::reloptions::TableStorageOptions::default(),
1297 access_method: None,
1298 };
1299 c.tables.push(child);
1300
1301 let g = build_create_graph(&c);
1302 assert!(
1303 has_edge(
1304 &g,
1305 &NodeId::Table(qn("app", "child")),
1306 &NodeId::Table(qn("app", "parent")),
1307 ),
1308 "expected child partition → parent table edge"
1309 );
1310 }
1311
1312 use crate::ir::user_type::{CompositeAttribute, UserType, UserTypeKind};
1315
1316 fn make_enum(schema: &str, name: &str) -> UserType {
1317 UserType {
1318 qname: qn(schema, name),
1319 kind: UserTypeKind::Enum { values: vec![] },
1320 comment: None,
1321 owner: None,
1322 grants: vec![],
1323 }
1324 }
1325
1326 fn make_composite_with_attr(schema: &str, name: &str, attr_type: ColumnType) -> UserType {
1327 UserType {
1328 qname: qn(schema, name),
1329 kind: UserTypeKind::Composite {
1330 attributes: vec![CompositeAttribute {
1331 name: id("val"),
1332 ty: attr_type,
1333 collation: None,
1334 }],
1335 },
1336 comment: None,
1337 owner: None,
1338 grants: vec![],
1339 }
1340 }
1341
1342 fn make_domain_over(schema: &str, name: &str, base: ColumnType) -> UserType {
1343 UserType {
1344 qname: qn(schema, name),
1345 kind: UserTypeKind::Domain {
1346 base,
1347 nullable: true,
1348 default: None,
1349 check_constraints: vec![],
1350 collation: None,
1351 },
1352 comment: None,
1353 owner: None,
1354 grants: vec![],
1355 }
1356 }
1357
1358 #[test]
1359 fn type_nodes_registered() {
1360 let mut c = Catalog::empty();
1361 c.schemas.push(crate::ir::schema::Schema::new(id("app")));
1362 c.types.push(make_enum("app", "status"));
1363 let g = build_create_graph(&c);
1364 let deps: Vec<_> = g
1366 .dependencies_of(&NodeId::Type(qn("app", "status")))
1367 .collect();
1368 assert_eq!(deps, vec![&NodeId::Schema(id("app"))]);
1369 assert_eq!(g.node_count(), 2);
1371 }
1372
1373 #[test]
1374 fn table_depends_on_user_defined_column_type() {
1375 let mut c = Catalog::empty();
1376 c.types.push(make_enum("app", "status"));
1377 c.tables.push(Table {
1378 qname: qn("app", "orders"),
1379 columns: vec![Column {
1380 name: id("status"),
1381 ty: ColumnType::UserDefined(qn("app", "status")),
1382 nullable: false,
1383 default: None,
1384 identity: None,
1385 generated: None,
1386 collation: None,
1387 storage: None,
1388 compression: None,
1389 comment: None,
1390 }],
1391 constraints: vec![],
1392 partition_by: None,
1393 partition_of: None,
1394 comment: None,
1395 owner: None,
1396 grants: vec![],
1397 rls_enabled: false,
1398 rls_forced: false,
1399 policies: vec![],
1400 storage: crate::ir::reloptions::TableStorageOptions::default(),
1401 access_method: None,
1402 });
1403 let g = build_create_graph(&c);
1404 assert!(
1405 has_edge(
1406 &g,
1407 &NodeId::Table(qn("app", "orders")),
1408 &NodeId::Type(qn("app", "status"))
1409 ),
1410 "table must depend on its user-defined column type"
1411 );
1412 }
1413
1414 #[test]
1415 fn composite_depends_on_user_defined_attribute_type() {
1416 let mut c = Catalog::empty();
1417 c.types.push(make_enum("app", "inner_t"));
1418 c.types.push(make_composite_with_attr(
1419 "app",
1420 "outer_t",
1421 ColumnType::UserDefined(qn("app", "inner_t")),
1422 ));
1423 let g = build_create_graph(&c);
1424 assert!(
1425 has_edge(
1426 &g,
1427 &NodeId::Type(qn("app", "outer_t")),
1428 &NodeId::Type(qn("app", "inner_t"))
1429 ),
1430 "composite must depend on the type of its user-defined attribute"
1431 );
1432 }
1433
1434 #[test]
1435 fn domain_depends_on_user_defined_base_type() {
1436 let mut c = Catalog::empty();
1437 c.types.push(make_enum("app", "base_t"));
1438 c.types.push(make_domain_over(
1439 "app",
1440 "derived_t",
1441 ColumnType::UserDefined(qn("app", "base_t")),
1442 ));
1443 let g = build_create_graph(&c);
1444 assert!(
1445 has_edge(
1446 &g,
1447 &NodeId::Type(qn("app", "derived_t")),
1448 &NodeId::Type(qn("app", "base_t"))
1449 ),
1450 "domain must depend on its user-defined base type"
1451 );
1452 }
1453
1454 fn make_range(
1457 schema: &str,
1458 name: &str,
1459 subtype: QualifiedName,
1460 canonical: Option<QualifiedName>,
1461 subtype_diff: Option<QualifiedName>,
1462 ) -> UserType {
1463 UserType {
1464 qname: qn(schema, name),
1465 kind: UserTypeKind::Range {
1466 subtype,
1467 subtype_opclass: None,
1468 collation: None,
1469 canonical,
1470 subtype_diff,
1471 multirange_type_name: None,
1472 },
1473 comment: None,
1474 owner: None,
1475 grants: vec![],
1476 }
1477 }
1478
1479 fn make_function(schema: &str, name: &str) -> crate::ir::function::Function {
1480 use crate::ir::function::{
1481 FunctionLanguage, NormalizedArgTypes, ParallelSafety, ReturnType, SecurityMode,
1482 Volatility,
1483 };
1484 use crate::parse::normalize_body::NormalizedBody;
1485 crate::ir::function::Function {
1486 qname: qn(schema, name),
1487 args: vec![],
1488 arg_types_normalized: NormalizedArgTypes::from_args(&[]),
1489 return_type: ReturnType::Scalar {
1490 ty: ColumnType::Integer,
1491 },
1492 language: FunctionLanguage::Sql,
1493 body: NormalizedBody::empty(),
1494 body_dependencies: vec![],
1495 volatility: Volatility::Volatile,
1496 strict: false,
1497 security: SecurityMode::Invoker,
1498 parallel: ParallelSafety::Unsafe,
1499 leakproof: false,
1500 cost: None,
1501 rows: None,
1502 comment: None,
1503 owner: None,
1504 grants: vec![],
1505 }
1506 }
1507
1508 #[test]
1509 fn range_with_builtin_subtype_adds_no_subtype_edge() {
1510 let mut c = Catalog::empty();
1511 c.schemas.push(crate::ir::schema::Schema::new(id("app")));
1512 c.types.push(make_range(
1513 "app",
1514 "ir",
1515 qn("pg_catalog", "int4"),
1516 None,
1517 None,
1518 ));
1519 let g = build_create_graph(&c);
1520 let deps: Vec<&NodeId> = g.dependencies_of(&NodeId::Type(qn("app", "ir"))).collect();
1522 assert_eq!(deps, vec![&NodeId::Schema(id("app"))]);
1523 }
1524
1525 #[test]
1526 fn range_with_managed_subtype_adds_type_edge() {
1527 let mut c = Catalog::empty();
1528 c.schemas.push(crate::ir::schema::Schema::new(id("app")));
1529 c.types.push(make_enum("app", "base_t"));
1530 c.types.push(make_range(
1531 "app",
1532 "myrange",
1533 qn("app", "base_t"),
1534 None,
1535 None,
1536 ));
1537 let g = build_create_graph(&c);
1538 assert!(
1539 has_edge(
1540 &g,
1541 &NodeId::Type(qn("app", "myrange")),
1542 &NodeId::Type(qn("app", "base_t"))
1543 ),
1544 "range type must depend on its managed subtype"
1545 );
1546 }
1547
1548 #[test]
1549 fn range_canonical_fn_adds_function_edge() {
1550 let mut c = Catalog::empty();
1551 c.schemas.push(crate::ir::schema::Schema::new(id("app")));
1552 let f = make_function("app", "canon_fn");
1553 let arg_types = f.arg_types_normalized.clone();
1554 c.functions.push(f);
1555 c.types.push(make_range(
1556 "app",
1557 "myrange",
1558 qn("pg_catalog", "int4"),
1559 Some(qn("app", "canon_fn")),
1560 None,
1561 ));
1562 let g = build_create_graph(&c);
1563 assert!(
1564 has_edge(
1565 &g,
1566 &NodeId::Type(qn("app", "myrange")),
1567 &NodeId::Function(qn("app", "canon_fn"), arg_types),
1568 ),
1569 "range type must depend on its canonical function"
1570 );
1571 }
1572
1573 #[test]
1574 fn range_subtype_diff_fn_adds_function_edge() {
1575 let mut c = Catalog::empty();
1576 c.schemas.push(crate::ir::schema::Schema::new(id("app")));
1577 let f = make_function("app", "diff_fn");
1578 let arg_types = f.arg_types_normalized.clone();
1579 c.functions.push(f);
1580 c.types.push(make_range(
1581 "app",
1582 "myrange",
1583 qn("pg_catalog", "int4"),
1584 None,
1585 Some(qn("app", "diff_fn")),
1586 ));
1587 let g = build_create_graph(&c);
1588 assert!(
1589 has_edge(
1590 &g,
1591 &NodeId::Type(qn("app", "myrange")),
1592 &NodeId::Function(qn("app", "diff_fn"), arg_types),
1593 ),
1594 "range type must depend on its subtype_diff function"
1595 );
1596 }
1597
1598 #[test]
1599 fn range_unmanaged_canonical_fn_adds_no_edge() {
1600 let mut c = Catalog::empty();
1603 c.schemas.push(crate::ir::schema::Schema::new(id("app")));
1604 c.types.push(make_range(
1605 "app",
1606 "myrange",
1607 qn("pg_catalog", "int4"),
1608 Some(qn("app", "unmanaged_fn")),
1609 None,
1610 ));
1611 let g = build_create_graph(&c);
1612 let deps: Vec<&NodeId> = g
1614 .dependencies_of(&NodeId::Type(qn("app", "myrange")))
1615 .collect();
1616 assert_eq!(deps, vec![&NodeId::Schema(id("app"))]);
1617 }
1618
1619 use crate::ir::collation::{Collation, CollationProvider};
1622
1623 fn make_collation(schema: &str, name: &str) -> Collation {
1624 Collation {
1625 qname: qn(schema, name),
1626 provider: CollationProvider::Libc,
1627 lc_collate: "C".into(),
1628 lc_ctype: "C".into(),
1629 deterministic: true,
1630 version: None,
1631 owner: None,
1632 comment: None,
1633 }
1634 }
1635
1636 fn make_table_empty(schema: &str, name: &str) -> Table {
1637 Table {
1638 qname: qn(schema, name),
1639 columns: vec![],
1640 constraints: vec![],
1641 partition_by: None,
1642 partition_of: None,
1643 comment: None,
1644 owner: None,
1645 grants: vec![],
1646 rls_enabled: false,
1647 rls_forced: false,
1648 policies: vec![],
1649 storage: crate::ir::reloptions::TableStorageOptions::default(),
1650 access_method: None,
1651 }
1652 }
1653
1654 fn col_text_with_collation(name: &str, collation: QualifiedName) -> Column {
1655 Column {
1656 name: id(name),
1657 ty: ColumnType::Text,
1658 nullable: true,
1659 default: None,
1660 identity: None,
1661 generated: None,
1662 collation: Some(collation),
1663 storage: None,
1664 compression: None,
1665 comment: None,
1666 }
1667 }
1668
1669 fn make_domain_with_collation(schema: &str, name: &str, collation: QualifiedName) -> UserType {
1670 UserType {
1671 qname: qn(schema, name),
1672 kind: UserTypeKind::Domain {
1673 base: ColumnType::Text,
1674 nullable: true,
1675 default: None,
1676 check_constraints: vec![],
1677 collation: Some(collation),
1678 },
1679 comment: None,
1680 owner: None,
1681 grants: vec![],
1682 }
1683 }
1684
1685 fn make_range_with_collation(
1686 schema: &str,
1687 name: &str,
1688 subtype: QualifiedName,
1689 collation: QualifiedName,
1690 ) -> UserType {
1691 UserType {
1692 qname: qn(schema, name),
1693 kind: UserTypeKind::Range {
1694 subtype,
1695 subtype_opclass: None,
1696 collation: Some(collation),
1697 canonical: None,
1698 subtype_diff: None,
1699 multirange_type_name: None,
1700 },
1701 comment: None,
1702 owner: None,
1703 grants: vec![],
1704 }
1705 }
1706
1707 fn make_composite_with_collated_attr(
1708 schema: &str,
1709 name: &str,
1710 collation: QualifiedName,
1711 ) -> UserType {
1712 UserType {
1713 qname: qn(schema, name),
1714 kind: UserTypeKind::Composite {
1715 attributes: vec![CompositeAttribute {
1716 name: id("val"),
1717 ty: ColumnType::Text,
1718 collation: Some(collation),
1719 }],
1720 },
1721 comment: None,
1722 owner: None,
1723 grants: vec![],
1724 }
1725 }
1726
1727 #[test]
1728 fn collation_node_registered_with_schema_edge() {
1729 let mut c = Catalog::empty();
1730 c.schemas.push(crate::ir::schema::Schema::new(id("app")));
1731 c.collations.push(make_collation("app", "ci"));
1732 let g = build_create_graph(&c);
1733 assert_eq!(g.node_count(), 2);
1735 assert!(
1736 has_edge(
1737 &g,
1738 &NodeId::Collation(qn("app", "ci")),
1739 &NodeId::Schema(id("app")),
1740 ),
1741 "collation must depend on its schema (ensures DROP COLLATION before DROP SCHEMA)"
1742 );
1743 }
1744
1745 #[test]
1746 fn column_with_managed_collation_adds_edge() {
1747 let mut cat = Catalog::empty();
1748 cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
1749 cat.collations.push(make_collation("app", "ci"));
1750 let mut t = make_table_empty("app", "users");
1751 t.columns
1752 .push(col_text_with_collation("email", qn("app", "ci")));
1753 cat.tables.push(t);
1754 let g = build_create_graph(&cat);
1755 assert!(
1756 has_edge(
1757 &g,
1758 &NodeId::Table(qn("app", "users")),
1759 &NodeId::Collation(qn("app", "ci")),
1760 ),
1761 "table must depend on its column's managed collation"
1762 );
1763 }
1764
1765 #[test]
1766 fn column_with_pg_catalog_collation_no_edge() {
1767 let mut cat = Catalog::empty();
1768 cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
1769 let mut t = make_table_empty("app", "users");
1770 t.columns
1771 .push(col_text_with_collation("email", qn("pg_catalog", "C")));
1772 cat.tables.push(t);
1773 let g = build_create_graph(&cat);
1774 assert!(
1775 !g.edges().any(|(_, to)| matches!(to, NodeId::Collation(_))),
1776 "no collation edge should be added for pg_catalog.C"
1777 );
1778 }
1779
1780 #[test]
1781 fn column_with_builtin_shortname_collation_no_edge() {
1782 let mut cat = Catalog::empty();
1785 cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
1786 let mut t = make_table_empty("app", "users");
1787 t.columns
1788 .push(col_text_with_collation("email", qn("app", "POSIX")));
1789 cat.tables.push(t);
1790 let g = build_create_graph(&cat);
1791 assert!(!g.edges().any(|(_, to)| matches!(to, NodeId::Collation(_))),);
1792 }
1793
1794 #[test]
1795 fn column_with_unmanaged_collation_no_edge() {
1796 let mut cat = Catalog::empty();
1800 cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
1801 let mut t = make_table_empty("app", "users");
1802 t.columns
1803 .push(col_text_with_collation("email", qn("app", "unmanaged")));
1804 cat.tables.push(t);
1805 let g = build_create_graph(&cat);
1806 assert!(!g.edges().any(|(_, to)| matches!(to, NodeId::Collation(_))),);
1807 }
1808
1809 #[test]
1810 fn domain_with_managed_collation_adds_edge() {
1811 let mut cat = Catalog::empty();
1812 cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
1813 cat.collations.push(make_collation("app", "ci"));
1814 cat.types.push(make_domain_with_collation(
1815 "app",
1816 "email_t",
1817 qn("app", "ci"),
1818 ));
1819 let g = build_create_graph(&cat);
1820 assert!(
1821 has_edge(
1822 &g,
1823 &NodeId::Type(qn("app", "email_t")),
1824 &NodeId::Collation(qn("app", "ci")),
1825 ),
1826 "domain must depend on its managed collation"
1827 );
1828 }
1829
1830 #[test]
1831 fn domain_with_pg_catalog_collation_no_edge() {
1832 let mut cat = Catalog::empty();
1833 cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
1834 cat.types.push(make_domain_with_collation(
1835 "app",
1836 "email_t",
1837 qn("pg_catalog", "C"),
1838 ));
1839 let g = build_create_graph(&cat);
1840 assert!(!g.edges().any(|(_, to)| matches!(to, NodeId::Collation(_))),);
1841 }
1842
1843 #[test]
1844 fn range_with_managed_collation_adds_edge() {
1845 let mut cat = Catalog::empty();
1846 cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
1847 cat.collations.push(make_collation("app", "ci"));
1848 cat.types.push(make_range_with_collation(
1849 "app",
1850 "textrange",
1851 qn("pg_catalog", "text"),
1852 qn("app", "ci"),
1853 ));
1854 let g = build_create_graph(&cat);
1855 assert!(
1856 has_edge(
1857 &g,
1858 &NodeId::Type(qn("app", "textrange")),
1859 &NodeId::Collation(qn("app", "ci")),
1860 ),
1861 "range type must depend on its managed collation"
1862 );
1863 }
1864
1865 #[test]
1866 fn range_with_pg_catalog_collation_no_edge() {
1867 let mut cat = Catalog::empty();
1868 cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
1869 cat.types.push(make_range_with_collation(
1870 "app",
1871 "textrange",
1872 qn("pg_catalog", "text"),
1873 qn("pg_catalog", "C"),
1874 ));
1875 let g = build_create_graph(&cat);
1876 assert!(!g.edges().any(|(_, to)| matches!(to, NodeId::Collation(_))),);
1877 }
1878
1879 #[test]
1880 fn composite_attribute_with_managed_collation_adds_edge() {
1881 let mut cat = Catalog::empty();
1882 cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
1883 cat.collations.push(make_collation("app", "ci"));
1884 cat.types.push(make_composite_with_collated_attr(
1885 "app",
1886 "addr_t",
1887 qn("app", "ci"),
1888 ));
1889 let g = build_create_graph(&cat);
1890 assert!(
1891 has_edge(
1892 &g,
1893 &NodeId::Type(qn("app", "addr_t")),
1894 &NodeId::Collation(qn("app", "ci")),
1895 ),
1896 "composite type must depend on collation of a collated attribute"
1897 );
1898 }
1899
1900 #[test]
1901 fn composite_attribute_with_pg_catalog_collation_no_edge() {
1902 let mut cat = Catalog::empty();
1903 cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
1904 cat.types.push(make_composite_with_collated_attr(
1905 "app",
1906 "addr_t",
1907 qn("pg_catalog", "C"),
1908 ));
1909 let g = build_create_graph(&cat);
1910 assert!(!g.edges().any(|(_, to)| matches!(to, NodeId::Collation(_))),);
1911 }
1912
1913 #[test]
1914 fn collation_ordered_before_table_using_it() {
1915 let mut cat = Catalog::empty();
1918 cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
1919 cat.collations.push(make_collation("app", "ci"));
1920 let mut t = make_table_empty("app", "users");
1921 t.columns
1922 .push(col_text_with_collation("email", qn("app", "ci")));
1923 cat.tables.push(t);
1924 let g = build_create_graph(&cat);
1925 let order = g.topological_sort().expect("no cycle");
1926 let coll_pos = order
1927 .iter()
1928 .position(|n| n == &NodeId::Collation(qn("app", "ci")))
1929 .expect("collation in order");
1930 let table_pos = order
1931 .iter()
1932 .position(|n| n == &NodeId::Table(qn("app", "users")))
1933 .expect("table in order");
1934 assert!(
1935 coll_pos < table_pos,
1936 "collation must be created before the table that uses it"
1937 );
1938 }
1939
1940 #[test]
1941 fn type_create_ordering_respects_edges() {
1942 let mut c = Catalog::empty();
1944 c.types.push(make_enum("app", "base_t"));
1945 c.types.push(make_domain_over(
1946 "app",
1947 "derived_t",
1948 ColumnType::UserDefined(qn("app", "base_t")),
1949 ));
1950 let g = build_create_graph(&c);
1951 let order = g.topological_sort().expect("no cycle expected");
1952 let base_pos = order
1953 .iter()
1954 .position(|n| n == &NodeId::Type(qn("app", "base_t")))
1955 .expect("base_t in order");
1956 let derived_pos = order
1957 .iter()
1958 .position(|n| n == &NodeId::Type(qn("app", "derived_t")))
1959 .expect("derived_t in order");
1960 assert!(base_pos < derived_pos, "base_t must come before derived_t");
1961 }
1962
1963 use crate::ir::event_trigger::{EventTrigger, EventTriggerEnabled, EventTriggerEvent};
1966
1967 fn make_event_trigger(name: &str, function: QualifiedName) -> EventTrigger {
1968 EventTrigger {
1969 name: id(name),
1970 event: EventTriggerEvent::DdlCommandEnd,
1971 tag_filter: vec![],
1972 function,
1973 enabled: EventTriggerEnabled::Enabled,
1974 owner: None,
1975 comment: None,
1976 }
1977 }
1978
1979 #[test]
1980 fn event_trigger_node_registered() {
1981 let mut c = Catalog::empty();
1982 let et = make_event_trigger("audit_et", qn("app", "audit_fn"));
1983 c.event_triggers.push(et);
1984 let g = build_create_graph(&c);
1985 assert!(
1986 g.nodes()
1987 .any(|n| n == &NodeId::EventTrigger(id("audit_et"))),
1988 "event trigger node must be registered in the graph"
1989 );
1990 }
1991
1992 #[test]
1993 fn event_trigger_depends_on_managed_function() {
1994 let mut c = Catalog::empty();
1995 let f = make_function("app", "audit_fn");
1996 let arg_types = f.arg_types_normalized.clone();
1997 c.functions.push(f);
1998 let et = make_event_trigger("audit_et", qn("app", "audit_fn"));
1999 c.event_triggers.push(et);
2000 let g = build_create_graph(&c);
2001 assert!(
2002 has_edge(
2003 &g,
2004 &NodeId::EventTrigger(id("audit_et")),
2005 &NodeId::Function(qn("app", "audit_fn"), arg_types),
2006 ),
2007 "event trigger must depend on the function it executes"
2008 );
2009 }
2010
2011 #[test]
2012 fn event_trigger_no_edge_for_unmanaged_function() {
2013 let mut c = Catalog::empty();
2015 let et = make_event_trigger("audit_et", qn("app", "unmanaged_fn"));
2016 c.event_triggers.push(et);
2017 let g = build_create_graph(&c);
2018 assert!(
2020 g.nodes()
2021 .any(|n| n == &NodeId::EventTrigger(id("audit_et"))),
2022 "event trigger node must still be registered even if function is unmanaged"
2023 );
2024 assert!(
2025 g.dependencies_of(&NodeId::EventTrigger(id("audit_et")))
2026 .next()
2027 .is_none(),
2028 "no edge expected for unmanaged function reference"
2029 );
2030 }
2031
2032 use crate::ir::aggregate::Aggregate;
2035 use crate::ir::function::{ArgMode, Function, FunctionArg};
2036
2037 fn make_function_with_args(schema: &str, name: &str, arg_types: &[ColumnType]) -> Function {
2039 let mut f = make_function(schema, name);
2040 f.args = arg_types
2041 .iter()
2042 .map(|ty| FunctionArg {
2043 name: None,
2044 mode: ArgMode::In,
2045 ty: ty.clone(),
2046 default: None,
2047 })
2048 .collect();
2049 f.arg_types_normalized = NormalizedArgTypes::from_args(&f.args);
2050 f
2051 }
2052
2053 fn make_aggregate(
2054 schema: &str,
2055 name: &str,
2056 arg_types: Vec<ColumnType>,
2057 state_type: ColumnType,
2058 sfunc: QualifiedName,
2059 finalfunc: Option<QualifiedName>,
2060 ) -> Aggregate {
2061 Aggregate {
2062 qname: qn(schema, name),
2063 arg_types,
2064 state_type,
2065 sfunc,
2066 finalfunc,
2067 initcond: None,
2068 owner: None,
2069 comment: None,
2070 }
2071 }
2072
2073 #[test]
2074 fn aggregate_node_registered() {
2075 let mut c = Catalog::empty();
2076 c.aggregates.push(make_aggregate(
2077 "app",
2078 "my_sum",
2079 vec![ColumnType::Integer],
2080 ColumnType::BigInt,
2081 qn("app", "my_sfunc"),
2082 None,
2083 ));
2084 let g = build_create_graph(&c);
2085 let agg_node = NodeId::Aggregate(
2086 qn("app", "my_sum"),
2087 aggregate_arg_types(&[ColumnType::Integer]),
2088 );
2089 assert!(
2090 g.nodes().any(|n| n == &agg_node),
2091 "aggregate node must be registered in the graph"
2092 );
2093 }
2094
2095 #[test]
2096 fn aggregate_depends_on_managed_sfunc() {
2097 let mut c = Catalog::empty();
2098 let sfunc = make_function_with_args(
2100 "app",
2101 "my_sfunc",
2102 &[ColumnType::BigInt, ColumnType::Integer],
2103 );
2104 let sfunc_norm = sfunc.arg_types_normalized.clone();
2105 c.functions.push(sfunc);
2106 c.aggregates.push(make_aggregate(
2107 "app",
2108 "my_sum",
2109 vec![ColumnType::Integer],
2110 ColumnType::BigInt,
2111 qn("app", "my_sfunc"),
2112 None,
2113 ));
2114 let g = build_create_graph(&c);
2115 let agg_node = NodeId::Aggregate(
2116 qn("app", "my_sum"),
2117 aggregate_arg_types(&[ColumnType::Integer]),
2118 );
2119 assert!(
2120 has_edge(
2121 &g,
2122 &agg_node,
2123 &NodeId::Function(qn("app", "my_sfunc"), sfunc_norm),
2124 ),
2125 "aggregate must depend on its managed state function"
2126 );
2127 }
2128
2129 #[test]
2130 fn aggregate_depends_on_managed_finalfunc() {
2131 let mut c = Catalog::empty();
2132 let sfunc = make_function_with_args(
2133 "app",
2134 "my_sfunc",
2135 &[ColumnType::BigInt, ColumnType::Integer],
2136 );
2137 c.functions.push(sfunc);
2138 let finalfunc = make_function_with_args("app", "my_final", &[ColumnType::BigInt]);
2140 let final_norm = finalfunc.arg_types_normalized.clone();
2141 c.functions.push(finalfunc);
2142 c.aggregates.push(make_aggregate(
2143 "app",
2144 "my_sum",
2145 vec![ColumnType::Integer],
2146 ColumnType::BigInt,
2147 qn("app", "my_sfunc"),
2148 Some(qn("app", "my_final")),
2149 ));
2150 let g = build_create_graph(&c);
2151 let agg_node = NodeId::Aggregate(
2152 qn("app", "my_sum"),
2153 aggregate_arg_types(&[ColumnType::Integer]),
2154 );
2155 assert!(
2156 has_edge(
2157 &g,
2158 &agg_node,
2159 &NodeId::Function(qn("app", "my_final"), final_norm),
2160 ),
2161 "aggregate must depend on its managed final function"
2162 );
2163 }
2164
2165 #[test]
2166 fn aggregate_sfunc_overload_resolved_by_signature() {
2167 let mut c = Catalog::empty();
2170 let wrong = make_function_with_args("app", "agg_sf", &[ColumnType::Text, ColumnType::Text]);
2171 let right =
2172 make_function_with_args("app", "agg_sf", &[ColumnType::BigInt, ColumnType::Integer]);
2173 let right_norm = right.arg_types_normalized.clone();
2174 let wrong_norm = wrong.arg_types_normalized.clone();
2175 c.functions.push(wrong);
2176 c.functions.push(right);
2177 c.aggregates.push(make_aggregate(
2178 "app",
2179 "my_sum",
2180 vec![ColumnType::Integer],
2181 ColumnType::BigInt,
2182 qn("app", "agg_sf"),
2183 None,
2184 ));
2185 let g = build_create_graph(&c);
2186 let agg_node = NodeId::Aggregate(
2187 qn("app", "my_sum"),
2188 aggregate_arg_types(&[ColumnType::Integer]),
2189 );
2190 assert!(
2191 has_edge(
2192 &g,
2193 &agg_node,
2194 &NodeId::Function(qn("app", "agg_sf"), right_norm),
2195 ),
2196 "edge must point at the (bigint, integer) overload"
2197 );
2198 assert!(
2199 !has_edge(
2200 &g,
2201 &agg_node,
2202 &NodeId::Function(qn("app", "agg_sf"), wrong_norm),
2203 ),
2204 "edge must NOT point at the (text, text) overload"
2205 );
2206 }
2207
2208 #[test]
2209 fn aggregate_no_edge_for_unmanaged_sfunc() {
2210 let mut c = Catalog::empty();
2211 c.aggregates.push(make_aggregate(
2212 "app",
2213 "my_sum",
2214 vec![ColumnType::Integer],
2215 ColumnType::BigInt,
2216 qn("app", "unmanaged_sfunc"),
2217 None,
2218 ));
2219 let g = build_create_graph(&c);
2220 let agg_node = NodeId::Aggregate(
2221 qn("app", "my_sum"),
2222 aggregate_arg_types(&[ColumnType::Integer]),
2223 );
2224 assert!(
2225 g.nodes().any(|n| n == &agg_node),
2226 "aggregate node must still be registered even if sfunc is unmanaged"
2227 );
2228 assert!(
2229 g.dependencies_of(&agg_node).next().is_none(),
2230 "no edge expected for unmanaged sfunc reference"
2231 );
2232 }
2233
2234 use crate::ir::cast::{Cast, CastContext, CastMethod};
2237
2238 fn make_cast_with_function(
2239 src_schema: &str,
2240 src_name: &str,
2241 tgt_schema: &str,
2242 tgt_name: &str,
2243 fn_schema: &str,
2244 fn_name: &str,
2245 fn_arg_types: Vec<ColumnType>,
2246 ) -> Cast {
2247 Cast {
2248 source: qn(src_schema, src_name),
2249 target: qn(tgt_schema, tgt_name),
2250 method: CastMethod::Function {
2251 name: qn(fn_schema, fn_name),
2252 arg_types: fn_arg_types,
2253 },
2254 context: CastContext::Explicit,
2255 comment: None,
2256 }
2257 }
2258
2259 fn make_cast_binary(
2260 src_schema: &str,
2261 src_name: &str,
2262 tgt_schema: &str,
2263 tgt_name: &str,
2264 ) -> Cast {
2265 Cast {
2266 source: qn(src_schema, src_name),
2267 target: qn(tgt_schema, tgt_name),
2268 method: CastMethod::Binary,
2269 context: CastContext::Implicit,
2270 comment: None,
2271 }
2272 }
2273
2274 #[test]
2275 fn cast_node_registered() {
2276 let mut c = Catalog::empty();
2277 c.casts
2278 .push(make_cast_binary("app", "src_t", "app", "tgt_t"));
2279 let g = build_create_graph(&c);
2280 let cast_node = NodeId::Cast(qn("app", "src_t"), qn("app", "tgt_t"));
2281 assert!(
2282 g.nodes().any(|n| n == &cast_node),
2283 "cast node must be registered in the graph"
2284 );
2285 }
2286
2287 #[test]
2288 fn cast_depends_on_managed_function() {
2289 let mut c = Catalog::empty();
2290 let func = make_function_with_args(
2292 "app",
2293 "src_to_tgt",
2294 &[ColumnType::UserDefined(qn("app", "src_t"))],
2295 );
2296 let func_norm = func.arg_types_normalized.clone();
2297 c.functions.push(func);
2298 c.casts.push(make_cast_with_function(
2299 "app",
2300 "src_t",
2301 "app",
2302 "tgt_t",
2303 "app",
2304 "src_to_tgt",
2305 vec![ColumnType::UserDefined(qn("app", "src_t"))],
2306 ));
2307 let g = build_create_graph(&c);
2308 let cast_node = NodeId::Cast(qn("app", "src_t"), qn("app", "tgt_t"));
2309 assert!(
2310 has_edge(
2311 &g,
2312 &cast_node,
2313 &NodeId::Function(qn("app", "src_to_tgt"), func_norm),
2314 ),
2315 "cast must depend on its managed conversion function"
2316 );
2317 }
2318
2319 #[test]
2320 fn cast_depends_on_managed_source_and_target_types() {
2321 let mut c = Catalog::empty();
2322 c.types.push(make_enum("app", "src_t"));
2323 c.types.push(make_enum("app", "tgt_t"));
2324 c.casts
2325 .push(make_cast_binary("app", "src_t", "app", "tgt_t"));
2326 let g = build_create_graph(&c);
2327 let cast_node = NodeId::Cast(qn("app", "src_t"), qn("app", "tgt_t"));
2328 assert!(
2329 has_edge(&g, &cast_node, &NodeId::Type(qn("app", "src_t"))),
2330 "cast must depend on the managed source type"
2331 );
2332 assert!(
2333 has_edge(&g, &cast_node, &NodeId::Type(qn("app", "tgt_t"))),
2334 "cast must depend on the managed target type"
2335 );
2336 }
2337
2338 #[test]
2339 fn cast_no_type_edge_for_builtin_target() {
2340 let mut c = Catalog::empty();
2342 c.types.push(make_enum("app", "src_t"));
2343 let cast = Cast {
2345 source: qn("app", "src_t"),
2346 target: qn("pg_catalog", "text"),
2347 method: CastMethod::Inout,
2348 context: CastContext::Assignment,
2349 comment: None,
2350 };
2351 c.casts.push(cast);
2352 let g = build_create_graph(&c);
2353 let cast_node = NodeId::Cast(qn("app", "src_t"), qn("pg_catalog", "text"));
2354 assert!(
2356 has_edge(&g, &cast_node, &NodeId::Type(qn("app", "src_t"))),
2357 "cast must depend on the managed source type"
2358 );
2359 assert!(
2361 !has_edge(&g, &cast_node, &NodeId::Type(qn("pg_catalog", "text"))),
2362 "no type edge expected for a built-in (pg_catalog) target type"
2363 );
2364 }
2365
2366 #[test]
2367 fn cast_no_edge_for_unmanaged_function() {
2368 let mut c = Catalog::empty();
2371 c.casts.push(make_cast_with_function(
2372 "app",
2373 "src_t",
2374 "pg_catalog",
2375 "text",
2376 "app",
2377 "unmanaged_fn",
2378 vec![ColumnType::UserDefined(qn("app", "src_t"))],
2379 ));
2380 let g = build_create_graph(&c);
2381 let cast_node = NodeId::Cast(qn("app", "src_t"), qn("pg_catalog", "text"));
2382 assert!(
2383 g.nodes().any(|n| n == &cast_node),
2384 "cast node must be registered even when function is unmanaged"
2385 );
2386 assert!(
2388 g.dependencies_of(&cast_node).next().is_none(),
2389 "no edge expected when conversion function is unmanaged"
2390 );
2391 }
2392}