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