use serde::{Deserialize, Serialize};
use crate::identifier::{Identifier, QualifiedName};
use crate::ir::catalog::Catalog;
use crate::ir::column_type::ColumnType;
use crate::ir::constraint::ConstraintKind;
use crate::ir::default_expr::DefaultExpr;
use crate::ir::function::ReturnType;
use crate::ir::user_type::UserTypeKind;
use crate::plan::graph::Graph;
pub use crate::ir::function::NormalizedArgTypes;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum NodeId {
Schema(Identifier),
Table(QualifiedName),
Index(QualifiedName),
Sequence(QualifiedName),
Constraint {
table: QualifiedName,
name: Identifier,
},
View(QualifiedName),
Mv(QualifiedName),
Type(QualifiedName),
Function(QualifiedName, NormalizedArgTypes),
Procedure(QualifiedName),
Extension(Identifier),
Trigger(QualifiedName),
Publication(Identifier),
Subscription(Identifier),
}
#[allow(clippy::too_many_lines)]
pub fn build_create_graph(catalog: &Catalog) -> Graph<NodeId> {
let mut g = Graph::new();
for s in &catalog.schemas {
g.add_node(NodeId::Schema(s.name.clone()));
}
for t in &catalog.tables {
g.add_node(NodeId::Table(t.qname.clone()));
}
for i in &catalog.indexes {
g.add_node(NodeId::Index(i.qname.clone()));
}
for s in &catalog.sequences {
g.add_node(NodeId::Sequence(s.qname.clone()));
}
for t in &catalog.tables {
for c in &t.constraints {
g.add_node(NodeId::Constraint {
table: t.qname.clone(),
name: c.qname.name.clone(),
});
}
}
for v in &catalog.views {
g.add_node(NodeId::View(v.qname.clone()));
}
for mv in &catalog.materialized_views {
g.add_node(NodeId::Mv(mv.qname.clone()));
}
for t in &catalog.types {
g.add_node(NodeId::Type(t.qname.clone()));
}
for f in &catalog.functions {
g.add_node(NodeId::Function(
f.qname.clone(),
f.arg_types_normalized.clone(),
));
}
for p in &catalog.procedures {
g.add_node(NodeId::Procedure(p.qname.clone()));
}
for t in &catalog.triggers {
g.add_node(NodeId::Trigger(t.qname.clone()));
let target_node = if catalog.tables.iter().any(|x| x.qname == t.table) {
NodeId::Table(t.table.clone())
} else if catalog.views.iter().any(|x| x.qname == t.table) {
NodeId::View(t.table.clone())
} else if catalog
.materialized_views
.iter()
.any(|x| x.qname == t.table)
{
NodeId::Mv(t.table.clone())
} else {
continue;
};
g.add_edge(NodeId::Trigger(t.qname.clone()), target_node);
if let Some(func) = catalog
.functions
.iter()
.find(|f| f.qname == t.function_qname)
{
g.add_edge(
NodeId::Trigger(t.qname.clone()),
NodeId::Function(t.function_qname.clone(), func.arg_types_normalized.clone()),
);
}
}
for e in &catalog.extensions {
g.add_node(NodeId::Extension(e.name.clone()));
if let Some(schema) = &e.schema {
g.add_edge(
NodeId::Extension(e.name.clone()),
NodeId::Schema(schema.clone()),
);
}
}
for p in &catalog.publications {
let pub_node = NodeId::Publication(p.name.clone());
g.add_node(pub_node.clone());
if let crate::ir::publication::PublicationScope::Selective { schemas, tables } = &p.scope {
for t in tables {
g.add_edge(pub_node.clone(), NodeId::Table(t.qname.clone()));
}
for s in schemas {
g.add_edge(pub_node.clone(), NodeId::Schema(s.clone()));
}
}
}
for s in &catalog.subscriptions {
g.add_node(NodeId::Subscription(s.name.clone()));
}
for t in &catalog.types {
g.add_edge(
NodeId::Type(t.qname.clone()),
NodeId::Schema(t.qname.schema.clone()),
);
}
for f in &catalog.functions {
let node = NodeId::Function(f.qname.clone(), f.arg_types_normalized.clone());
g.add_edge(node, NodeId::Schema(f.qname.schema.clone()));
}
for p in &catalog.procedures {
g.add_edge(
NodeId::Procedure(p.qname.clone()),
NodeId::Schema(p.qname.schema.clone()),
);
}
for ut in &catalog.types {
match &ut.kind {
UserTypeKind::Composite { attributes } => {
for attr in attributes {
if let ColumnType::UserDefined(dep_qname) = &attr.ty {
g.add_edge(
NodeId::Type(ut.qname.clone()),
NodeId::Type(dep_qname.clone()),
);
}
}
}
UserTypeKind::Domain {
base: ColumnType::UserDefined(base_qname),
..
} => {
g.add_edge(
NodeId::Type(ut.qname.clone()),
NodeId::Type(base_qname.clone()),
);
}
_ => {}
}
}
for t in &catalog.tables {
for col in &t.columns {
if let ColumnType::UserDefined(type_qname) = &col.ty {
g.add_edge(
NodeId::Table(t.qname.clone()),
NodeId::Type(type_qname.clone()),
);
}
}
}
for f in &catalog.functions {
let node = NodeId::Function(f.qname.clone(), f.arg_types_normalized.clone());
for arg in &f.args {
if let ColumnType::UserDefined(t_qname) = &arg.ty {
g.add_edge(node.clone(), NodeId::Type(t_qname.clone()));
}
}
match &f.return_type {
ReturnType::Scalar {
ty: ColumnType::UserDefined(t),
}
| ReturnType::SetOf {
ty: ColumnType::UserDefined(t),
} => {
g.add_edge(node.clone(), NodeId::Type(t.clone()));
}
ReturnType::Table { columns } => {
for col in columns {
if let ColumnType::UserDefined(t) = &col.ty {
g.add_edge(node.clone(), NodeId::Type(t.clone()));
}
}
}
_ => {}
}
}
for p in &catalog.procedures {
let node = NodeId::Procedure(p.qname.clone());
for arg in &p.args {
if let ColumnType::UserDefined(t_qname) = &arg.ty {
g.add_edge(node.clone(), NodeId::Type(t_qname.clone()));
}
}
}
for t in &catalog.tables {
g.add_edge(
NodeId::Table(t.qname.clone()),
NodeId::Schema(t.qname.schema.clone()),
);
for col in &t.columns {
if let Some(DefaultExpr::Sequence(seq_qname)) = &col.default {
g.add_edge(
NodeId::Table(t.qname.clone()),
NodeId::Sequence(seq_qname.clone()),
);
}
}
if let Some(po) = &t.partition_of {
g.add_edge(
NodeId::Table(t.qname.clone()),
NodeId::Table(po.parent.clone()),
);
}
}
for v in &catalog.views {
for dep in &v.body_dependencies {
g.add_edge(dep.from.clone(), dep.to.clone());
}
}
for mv in &catalog.materialized_views {
for dep in &mv.body_dependencies {
g.add_edge(dep.from.clone(), dep.to.clone());
}
}
for f in &catalog.functions {
for dep in &f.body_dependencies {
g.add_edge(dep.from.clone(), dep.to.clone());
}
}
for p in &catalog.procedures {
for dep in &p.body_dependencies {
g.add_edge(dep.from.clone(), dep.to.clone());
}
}
for i in &catalog.indexes {
use crate::ir::index::IndexParent;
let parent_node = match &i.on {
IndexParent::Table(q) => NodeId::Table(q.clone()),
IndexParent::Mv(q) => NodeId::Mv(q.clone()),
};
g.add_edge(NodeId::Index(i.qname.clone()), parent_node);
}
for t in &catalog.tables {
for c in &t.constraints {
let constraint_node = NodeId::Constraint {
table: t.qname.clone(),
name: c.qname.name.clone(),
};
g.add_edge(constraint_node.clone(), NodeId::Table(t.qname.clone()));
if let ConstraintKind::ForeignKey(fk) = &c.kind {
g.add_edge(constraint_node, NodeId::Table(fk.referenced_table.clone()));
if fk.referenced_table != t.qname {
g.add_edge(
NodeId::Table(t.qname.clone()),
NodeId::Table(fk.referenced_table.clone()),
);
}
}
}
}
for s in &catalog.sequences {
if let Some(owner) = &s.owned_by {
g.add_edge(
NodeId::Sequence(s.qname.clone()),
NodeId::Table(owner.table.clone()),
);
}
}
g
}
pub fn build_drop_graph(catalog: &Catalog) -> Graph<NodeId> {
build_create_graph(catalog)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum DepSource {
Structural,
AstExtracted,
AstDeclared,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct DepEdge {
pub from: NodeId,
pub to: NodeId,
pub source: DepSource,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::column::Column;
use crate::ir::column_type::ColumnType;
use crate::ir::constraint::{
Constraint, ConstraintKind, Deferrable, FkMatchType, ForeignKey, ReferentialAction,
};
use crate::ir::index::{
Index, IndexColumn, IndexColumnExpr, IndexMethod, IndexParent, NullsOrder, SortOrder,
};
use crate::ir::schema::Schema;
use crate::ir::sequence::{Sequence, SequenceOwner};
use crate::ir::table::Table;
fn id(s: &str) -> Identifier {
Identifier::from_unquoted(s).unwrap()
}
fn qn(schema: &str, name: &str) -> QualifiedName {
QualifiedName::new(id(schema), id(name))
}
fn col_id_bigint() -> Column {
Column {
name: id("id"),
ty: ColumnType::BigInt,
nullable: false,
default: None,
identity: None,
generated: None,
collation: None,
storage: None,
compression: None,
comment: None,
}
}
fn col_text_notnull(name: &str) -> Column {
Column {
name: id(name),
ty: ColumnType::Text,
nullable: false,
default: None,
identity: None,
generated: None,
collation: None,
storage: None,
compression: None,
comment: None,
}
}
fn pk(name: &str, cols: &[&str]) -> Constraint {
Constraint {
qname: qn("app", name),
kind: ConstraintKind::PrimaryKey {
columns: cols.iter().map(|c| id(c)).collect(),
include: vec![],
},
deferrable: Deferrable::NotDeferrable,
comment: None,
}
}
fn fk(name: &str, ref_table: QualifiedName) -> Constraint {
Constraint {
qname: qn("app", name),
kind: ConstraintKind::ForeignKey(ForeignKey {
columns: vec![id("ref_id")],
referenced_table: ref_table,
referenced_columns: vec![id("id")],
on_update: ReferentialAction::NoAction,
on_delete: ReferentialAction::NoAction,
match_type: FkMatchType::Simple,
}),
deferrable: Deferrable::NotDeferrable,
comment: None,
}
}
fn has_edge(g: &Graph<NodeId>, from: &NodeId, to: &NodeId) -> bool {
g.dependencies_of(from).any(|n| n == to)
}
#[test]
fn empty_catalog_yields_empty_graph() {
let g = build_create_graph(&Catalog::empty());
assert_eq!(g.node_count(), 0);
}
#[test]
fn every_object_appears_as_a_node() {
let mut c = Catalog::empty();
c.schemas.push(Schema::new(id("app")));
c.tables.push(Table {
qname: qn("app", "users"),
columns: vec![col_id_bigint()],
constraints: vec![pk("users_pkey", &["id"])],
partition_by: None,
partition_of: None,
comment: None,
owner: None,
grants: vec![],
rls_enabled: false,
rls_forced: false,
policies: vec![],
storage: crate::ir::reloptions::TableStorageOptions::default(),
});
c.indexes.push(Index {
qname: qn("app", "users_idx"),
on: IndexParent::Table(qn("app", "users")),
method: IndexMethod::BTree,
columns: vec![IndexColumn {
expr: IndexColumnExpr::Column(id("id")),
collation: None,
opclass: None,
sort_order: SortOrder::Asc,
nulls_order: NullsOrder::NullsLast,
}],
include: vec![],
unique: false,
nulls_not_distinct: false,
predicate: None,
tablespace: None,
comment: None,
storage: crate::ir::reloptions::IndexStorageOptions::default(),
});
c.sequences.push(Sequence {
qname: qn("app", "seq1"),
data_type: ColumnType::BigInt,
start: 1,
increment: 1,
min_value: None,
max_value: None,
cache: 1,
cycle: false,
owned_by: None,
comment: None,
owner: None,
grants: vec![],
});
let g = build_create_graph(&c);
assert_eq!(g.node_count(), 5);
}
#[test]
fn table_depends_on_its_schema() {
let mut c = Catalog::empty();
c.schemas.push(Schema::new(id("app")));
c.tables.push(Table {
qname: qn("app", "users"),
columns: vec![col_id_bigint()],
constraints: vec![],
partition_by: None,
partition_of: None,
comment: None,
owner: None,
grants: vec![],
rls_enabled: false,
rls_forced: false,
policies: vec![],
storage: crate::ir::reloptions::TableStorageOptions::default(),
});
let g = build_create_graph(&c);
assert!(has_edge(
&g,
&NodeId::Table(qn("app", "users")),
&NodeId::Schema(id("app")),
));
}
#[test]
fn index_depends_on_its_table() {
let mut c = Catalog::empty();
c.tables.push(Table {
qname: qn("app", "users"),
columns: vec![col_id_bigint()],
constraints: vec![],
partition_by: None,
partition_of: None,
comment: None,
owner: None,
grants: vec![],
rls_enabled: false,
rls_forced: false,
policies: vec![],
storage: crate::ir::reloptions::TableStorageOptions::default(),
});
c.indexes.push(Index {
qname: qn("app", "users_idx"),
on: IndexParent::Table(qn("app", "users")),
method: IndexMethod::BTree,
columns: vec![IndexColumn {
expr: IndexColumnExpr::Column(id("id")),
collation: None,
opclass: None,
sort_order: SortOrder::Asc,
nulls_order: NullsOrder::NullsLast,
}],
include: vec![],
unique: false,
nulls_not_distinct: false,
predicate: None,
tablespace: None,
comment: None,
storage: crate::ir::reloptions::IndexStorageOptions::default(),
});
let g = build_create_graph(&c);
assert!(has_edge(
&g,
&NodeId::Index(qn("app", "users_idx")),
&NodeId::Table(qn("app", "users")),
));
}
#[test]
fn fk_constraint_depends_on_both_endpoints() {
let mut c = Catalog::empty();
c.tables.push(Table {
qname: qn("app", "orgs"),
columns: vec![col_id_bigint()],
constraints: vec![pk("orgs_pkey", &["id"])],
partition_by: None,
partition_of: None,
comment: None,
owner: None,
grants: vec![],
rls_enabled: false,
rls_forced: false,
policies: vec![],
storage: crate::ir::reloptions::TableStorageOptions::default(),
});
c.tables.push(Table {
qname: qn("app", "users"),
columns: vec![
col_id_bigint(),
Column {
name: id("ref_id"),
ty: ColumnType::BigInt,
nullable: false,
default: None,
identity: None,
generated: None,
collation: None,
storage: None,
compression: None,
comment: None,
},
],
constraints: vec![fk("users_orgs_fk", qn("app", "orgs"))],
partition_by: None,
partition_of: None,
comment: None,
owner: None,
grants: vec![],
rls_enabled: false,
rls_forced: false,
policies: vec![],
storage: crate::ir::reloptions::TableStorageOptions::default(),
});
let g = build_create_graph(&c);
let fk_node = NodeId::Constraint {
table: qn("app", "users"),
name: id("users_orgs_fk"),
};
assert!(has_edge(&g, &fk_node, &NodeId::Table(qn("app", "users"))));
assert!(has_edge(&g, &fk_node, &NodeId::Table(qn("app", "orgs"))));
}
#[test]
fn table_depends_on_default_sequence() {
let mut c = Catalog::empty();
c.sequences.push(Sequence {
qname: qn("app", "id_seq"),
data_type: ColumnType::BigInt,
start: 1,
increment: 1,
min_value: None,
max_value: None,
cache: 1,
cycle: false,
owned_by: None,
comment: None,
owner: None,
grants: vec![],
});
c.tables.push(Table {
qname: qn("app", "users"),
columns: vec![Column {
name: id("id"),
ty: ColumnType::BigInt,
nullable: false,
default: Some(DefaultExpr::Sequence(qn("app", "id_seq"))),
identity: None,
generated: None,
collation: None,
storage: None,
compression: None,
comment: None,
}],
constraints: vec![],
partition_by: None,
partition_of: None,
comment: None,
owner: None,
grants: vec![],
rls_enabled: false,
rls_forced: false,
policies: vec![],
storage: crate::ir::reloptions::TableStorageOptions::default(),
});
let g = build_create_graph(&c);
assert!(has_edge(
&g,
&NodeId::Table(qn("app", "users")),
&NodeId::Sequence(qn("app", "id_seq")),
));
}
#[test]
fn owned_sequence_depends_on_owner_table() {
let mut c = Catalog::empty();
c.tables.push(Table {
qname: qn("app", "users"),
columns: vec![col_id_bigint()],
constraints: vec![],
partition_by: None,
partition_of: None,
comment: None,
owner: None,
grants: vec![],
rls_enabled: false,
rls_forced: false,
policies: vec![],
storage: crate::ir::reloptions::TableStorageOptions::default(),
});
c.sequences.push(Sequence {
qname: qn("app", "users_id_seq"),
data_type: ColumnType::BigInt,
start: 1,
increment: 1,
min_value: None,
max_value: None,
cache: 1,
cycle: false,
owned_by: Some(SequenceOwner {
table: qn("app", "users"),
column: id("id"),
}),
comment: None,
owner: None,
grants: vec![],
});
let g = build_create_graph(&c);
assert!(has_edge(
&g,
&NodeId::Sequence(qn("app", "users_id_seq")),
&NodeId::Table(qn("app", "users")),
));
}
#[test]
fn non_fk_constraint_depends_only_on_its_table() {
let mut c = Catalog::empty();
c.tables.push(Table {
qname: qn("app", "users"),
columns: vec![col_id_bigint()],
constraints: vec![pk("users_pkey", &["id"])],
partition_by: None,
partition_of: None,
comment: None,
owner: None,
grants: vec![],
rls_enabled: false,
rls_forced: false,
policies: vec![],
storage: crate::ir::reloptions::TableStorageOptions::default(),
});
let g = build_create_graph(&c);
let pk_node = NodeId::Constraint {
table: qn("app", "users"),
name: id("users_pkey"),
};
let deps: Vec<&NodeId> = g.dependencies_of(&pk_node).collect();
assert_eq!(deps, vec![&NodeId::Table(qn("app", "users"))]);
}
#[test]
fn drop_graph_matches_create_graph() {
let mut c = Catalog::empty();
c.tables.push(Table {
qname: qn("app", "users"),
columns: vec![col_id_bigint()],
constraints: vec![pk("users_pkey", &["id"])],
partition_by: None,
partition_of: None,
comment: None,
owner: None,
grants: vec![],
rls_enabled: false,
rls_forced: false,
policies: vec![],
storage: crate::ir::reloptions::TableStorageOptions::default(),
});
let cg = build_create_graph(&c);
let dg = build_drop_graph(&c);
assert_eq!(cg.topological_sort(), dg.topological_sort());
}
#[test]
fn fk_cycle_produces_table_level_cycle() {
let mut c = Catalog::empty();
c.tables.push(Table {
qname: qn("app", "a"),
columns: vec![
col_id_bigint(),
Column {
name: id("ref_id"),
ty: ColumnType::BigInt,
nullable: false,
default: None,
identity: None,
generated: None,
collation: None,
storage: None,
compression: None,
comment: None,
},
],
constraints: vec![pk("a_pk", &["id"]), fk("a_to_b", qn("app", "b"))],
partition_by: None,
partition_of: None,
comment: None,
owner: None,
grants: vec![],
rls_enabled: false,
rls_forced: false,
policies: vec![],
storage: crate::ir::reloptions::TableStorageOptions::default(),
});
c.tables.push(Table {
qname: qn("app", "b"),
columns: vec![
col_id_bigint(),
Column {
name: id("ref_id"),
ty: ColumnType::BigInt,
nullable: false,
default: None,
identity: None,
generated: None,
collation: None,
storage: None,
compression: None,
comment: None,
},
],
constraints: vec![pk("b_pk", &["id"]), fk("b_to_a", qn("app", "a"))],
partition_by: None,
partition_of: None,
comment: None,
owner: None,
grants: vec![],
rls_enabled: false,
rls_forced: false,
policies: vec![],
storage: crate::ir::reloptions::TableStorageOptions::default(),
});
let g = build_create_graph(&c);
let err = g.topological_sort().unwrap_err();
assert!(err.nodes.contains(&NodeId::Table(qn("app", "a"))));
assert!(err.nodes.contains(&NodeId::Table(qn("app", "b"))));
}
#[test]
fn self_referential_fk_does_not_cycle() {
let mut c = Catalog::empty();
c.tables.push(Table {
qname: qn("app", "tree"),
columns: vec![
col_id_bigint(),
Column {
name: id("ref_id"),
ty: ColumnType::BigInt,
nullable: true,
default: None,
identity: None,
generated: None,
collation: None,
storage: None,
compression: None,
comment: None,
},
],
constraints: vec![
pk("tree_pk", &["id"]),
fk("tree_parent_fk", qn("app", "tree")),
],
partition_by: None,
partition_of: None,
comment: None,
owner: None,
grants: vec![],
rls_enabled: false,
rls_forced: false,
policies: vec![],
storage: crate::ir::reloptions::TableStorageOptions::default(),
});
let g = build_create_graph(&c);
assert!(g.topological_sort().is_ok());
}
#[test]
fn partition_child_depends_on_parent() {
use crate::ir::partition::{PartitionBounds, PartitionBy, PartitionOf};
let mut c = Catalog::empty();
let parent = Table {
qname: qn("app", "parent"),
columns: vec![col_id_bigint(), col_text_notnull("status")],
constraints: vec![pk("parent_pkey", &["id"])],
partition_by: Some(PartitionBy {
strategy: crate::ir::partition::PartitionStrategy::List,
columns: vec![crate::ir::partition::PartitionColumn {
kind: crate::ir::partition::PartitionColumnKind::Column(id("status")),
collation: None,
opclass: None,
}],
}),
partition_of: None,
comment: None,
owner: None,
grants: vec![],
rls_enabled: false,
rls_forced: false,
policies: vec![],
storage: crate::ir::reloptions::TableStorageOptions::default(),
};
c.tables.push(parent);
let child = Table {
qname: qn("app", "child"),
columns: vec![col_id_bigint(), col_text_notnull("status")],
constraints: vec![],
partition_by: None,
partition_of: Some(PartitionOf {
parent: qn("app", "parent"),
bounds: PartitionBounds::List { values: vec![] },
}),
comment: None,
owner: None,
grants: vec![],
rls_enabled: false,
rls_forced: false,
policies: vec![],
storage: crate::ir::reloptions::TableStorageOptions::default(),
};
c.tables.push(child);
let g = build_create_graph(&c);
assert!(
has_edge(
&g,
&NodeId::Table(qn("app", "child")),
&NodeId::Table(qn("app", "parent")),
),
"expected child partition → parent table edge"
);
}
use crate::ir::user_type::{CompositeAttribute, UserType, UserTypeKind};
fn make_enum(schema: &str, name: &str) -> UserType {
UserType {
qname: qn(schema, name),
kind: UserTypeKind::Enum { values: vec![] },
comment: None,
owner: None,
grants: vec![],
}
}
fn make_composite_with_attr(schema: &str, name: &str, attr_type: ColumnType) -> UserType {
UserType {
qname: qn(schema, name),
kind: UserTypeKind::Composite {
attributes: vec![CompositeAttribute {
name: id("val"),
ty: attr_type,
collation: None,
}],
},
comment: None,
owner: None,
grants: vec![],
}
}
fn make_domain_over(schema: &str, name: &str, base: ColumnType) -> UserType {
UserType {
qname: qn(schema, name),
kind: UserTypeKind::Domain {
base,
nullable: true,
default: None,
check_constraints: vec![],
collation: None,
},
comment: None,
owner: None,
grants: vec![],
}
}
#[test]
fn type_nodes_registered() {
let mut c = Catalog::empty();
c.schemas.push(crate::ir::schema::Schema::new(id("app")));
c.types.push(make_enum("app", "status"));
let g = build_create_graph(&c);
let deps: Vec<_> = g
.dependencies_of(&NodeId::Type(qn("app", "status")))
.collect();
assert_eq!(deps, vec![&NodeId::Schema(id("app"))]);
assert_eq!(g.node_count(), 2);
}
#[test]
fn table_depends_on_user_defined_column_type() {
let mut c = Catalog::empty();
c.types.push(make_enum("app", "status"));
c.tables.push(Table {
qname: qn("app", "orders"),
columns: vec![Column {
name: id("status"),
ty: ColumnType::UserDefined(qn("app", "status")),
nullable: false,
default: None,
identity: None,
generated: None,
collation: None,
storage: None,
compression: None,
comment: None,
}],
constraints: vec![],
partition_by: None,
partition_of: None,
comment: None,
owner: None,
grants: vec![],
rls_enabled: false,
rls_forced: false,
policies: vec![],
storage: crate::ir::reloptions::TableStorageOptions::default(),
});
let g = build_create_graph(&c);
assert!(
has_edge(
&g,
&NodeId::Table(qn("app", "orders")),
&NodeId::Type(qn("app", "status"))
),
"table must depend on its user-defined column type"
);
}
#[test]
fn composite_depends_on_user_defined_attribute_type() {
let mut c = Catalog::empty();
c.types.push(make_enum("app", "inner_t"));
c.types.push(make_composite_with_attr(
"app",
"outer_t",
ColumnType::UserDefined(qn("app", "inner_t")),
));
let g = build_create_graph(&c);
assert!(
has_edge(
&g,
&NodeId::Type(qn("app", "outer_t")),
&NodeId::Type(qn("app", "inner_t"))
),
"composite must depend on the type of its user-defined attribute"
);
}
#[test]
fn domain_depends_on_user_defined_base_type() {
let mut c = Catalog::empty();
c.types.push(make_enum("app", "base_t"));
c.types.push(make_domain_over(
"app",
"derived_t",
ColumnType::UserDefined(qn("app", "base_t")),
));
let g = build_create_graph(&c);
assert!(
has_edge(
&g,
&NodeId::Type(qn("app", "derived_t")),
&NodeId::Type(qn("app", "base_t"))
),
"domain must depend on its user-defined base type"
);
}
#[test]
fn type_create_ordering_respects_edges() {
let mut c = Catalog::empty();
c.types.push(make_enum("app", "base_t"));
c.types.push(make_domain_over(
"app",
"derived_t",
ColumnType::UserDefined(qn("app", "base_t")),
));
let g = build_create_graph(&c);
let order = g.topological_sort().expect("no cycle expected");
let base_pos = order
.iter()
.position(|n| n == &NodeId::Type(qn("app", "base_t")))
.expect("base_t in order");
let derived_pos = order
.iter()
.position(|n| n == &NodeId::Type(qn("app", "derived_t")))
.expect("derived_t in order");
assert!(base_pos < derived_pos, "base_t must come before derived_t");
}
}