use serde::{Deserialize, Serialize};
use crate::identifier::{Identifier, QualifiedName};
use crate::ir::catalog::Catalog;
use crate::ir::collation::BUILTIN_COLLATIONS;
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),
EventTrigger(Identifier),
Statistic(QualifiedName),
Collation(QualifiedName),
}
fn should_add_collation_edge(qname: &QualifiedName, catalog: &Catalog) -> bool {
if qname.schema.as_str() == "pg_catalog" {
return false;
}
let name = qname.name.as_str();
if BUILTIN_COLLATIONS.contains(&name) {
return false;
}
catalog.collations.iter().any(|c| &c.qname == qname)
}
#[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 et in &catalog.event_triggers {
g.add_node(NodeId::EventTrigger(et.name.clone()));
if let Some(func) = catalog.functions.iter().find(|f| f.qname == et.function) {
g.add_edge(
NodeId::EventTrigger(et.name.clone()),
NodeId::Function(et.function.clone(), func.arg_types_normalized.clone()),
);
}
}
for s in &catalog.statistics {
let stat_node = NodeId::Statistic(s.qname.clone());
g.add_node(stat_node.clone());
g.add_edge(stat_node, NodeId::Table(s.target.clone()));
}
for c in &catalog.collations {
let coll_node = NodeId::Collation(c.qname.clone());
g.add_node(coll_node.clone());
g.add_edge(coll_node, NodeId::Schema(c.qname.schema.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()),
);
}
if let Some(coll_qname) = &attr.collation
&& should_add_collation_edge(coll_qname, catalog)
{
g.add_edge(
NodeId::Type(ut.qname.clone()),
NodeId::Collation(coll_qname.clone()),
);
}
}
}
UserTypeKind::Domain {
base, collation, ..
} => {
if let ColumnType::UserDefined(base_qname) = base {
g.add_edge(
NodeId::Type(ut.qname.clone()),
NodeId::Type(base_qname.clone()),
);
}
if let Some(coll_qname) = collation
&& should_add_collation_edge(coll_qname, catalog)
{
g.add_edge(
NodeId::Type(ut.qname.clone()),
NodeId::Collation(coll_qname.clone()),
);
}
}
UserTypeKind::Range {
subtype,
canonical,
subtype_diff,
collation,
..
} => {
if subtype.schema.as_str() != "pg_catalog"
&& catalog.types.iter().any(|t| &t.qname == subtype)
{
g.add_edge(
NodeId::Type(ut.qname.clone()),
NodeId::Type(subtype.clone()),
);
}
if let Some(fn_qname) = canonical
&& let Some(f) = catalog.functions.iter().find(|f| &f.qname == fn_qname)
{
g.add_edge(
NodeId::Type(ut.qname.clone()),
NodeId::Function(f.qname.clone(), f.arg_types_normalized.clone()),
);
}
if let Some(fn_qname) = subtype_diff
&& let Some(f) = catalog.functions.iter().find(|f| &f.qname == fn_qname)
{
g.add_edge(
NodeId::Type(ut.qname.clone()),
NodeId::Function(f.qname.clone(), f.arg_types_normalized.clone()),
);
}
if let Some(coll_qname) = collation
&& should_add_collation_edge(coll_qname, catalog)
{
g.add_edge(
NodeId::Type(ut.qname.clone()),
NodeId::Collation(coll_qname.clone()),
);
}
}
UserTypeKind::Enum { .. } => {}
}
}
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()),
);
}
if let Some(coll_qname) = &col.collation
&& should_add_collation_edge(coll_qname, catalog)
{
g.add_edge(
NodeId::Table(t.qname.clone()),
NodeId::Collation(coll_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(),
access_method: None,
});
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(),
access_method: None,
});
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(),
access_method: None,
});
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(),
access_method: None,
});
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(),
access_method: None,
});
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(),
access_method: None,
});
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(),
access_method: None,
});
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(),
access_method: None,
});
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(),
access_method: None,
});
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(),
access_method: None,
});
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(),
access_method: None,
});
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(),
access_method: None,
});
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(),
access_method: None,
};
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(),
access_method: None,
};
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(),
access_method: None,
});
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"
);
}
fn make_range(
schema: &str,
name: &str,
subtype: QualifiedName,
canonical: Option<QualifiedName>,
subtype_diff: Option<QualifiedName>,
) -> UserType {
UserType {
qname: qn(schema, name),
kind: UserTypeKind::Range {
subtype,
subtype_opclass: None,
collation: None,
canonical,
subtype_diff,
multirange_type_name: None,
},
comment: None,
owner: None,
grants: vec![],
}
}
fn make_function(schema: &str, name: &str) -> crate::ir::function::Function {
use crate::ir::function::{
FunctionLanguage, NormalizedArgTypes, ParallelSafety, ReturnType, SecurityMode,
Volatility,
};
use crate::parse::normalize_body::NormalizedBody;
crate::ir::function::Function {
qname: qn(schema, name),
args: vec![],
arg_types_normalized: NormalizedArgTypes::from_args(&[]),
return_type: ReturnType::Scalar {
ty: ColumnType::Integer,
},
language: FunctionLanguage::Sql,
body: NormalizedBody::empty(),
body_dependencies: vec![],
volatility: Volatility::Volatile,
strict: false,
security: SecurityMode::Invoker,
parallel: ParallelSafety::Unsafe,
leakproof: false,
cost: None,
rows: None,
comment: None,
owner: None,
grants: vec![],
}
}
#[test]
fn range_with_builtin_subtype_adds_no_subtype_edge() {
let mut c = Catalog::empty();
c.schemas.push(crate::ir::schema::Schema::new(id("app")));
c.types.push(make_range(
"app",
"ir",
qn("pg_catalog", "int4"),
None,
None,
));
let g = build_create_graph(&c);
let deps: Vec<&NodeId> = g.dependencies_of(&NodeId::Type(qn("app", "ir"))).collect();
assert_eq!(deps, vec![&NodeId::Schema(id("app"))]);
}
#[test]
fn range_with_managed_subtype_adds_type_edge() {
let mut c = Catalog::empty();
c.schemas.push(crate::ir::schema::Schema::new(id("app")));
c.types.push(make_enum("app", "base_t"));
c.types.push(make_range(
"app",
"myrange",
qn("app", "base_t"),
None,
None,
));
let g = build_create_graph(&c);
assert!(
has_edge(
&g,
&NodeId::Type(qn("app", "myrange")),
&NodeId::Type(qn("app", "base_t"))
),
"range type must depend on its managed subtype"
);
}
#[test]
fn range_canonical_fn_adds_function_edge() {
let mut c = Catalog::empty();
c.schemas.push(crate::ir::schema::Schema::new(id("app")));
let f = make_function("app", "canon_fn");
let arg_types = f.arg_types_normalized.clone();
c.functions.push(f);
c.types.push(make_range(
"app",
"myrange",
qn("pg_catalog", "int4"),
Some(qn("app", "canon_fn")),
None,
));
let g = build_create_graph(&c);
assert!(
has_edge(
&g,
&NodeId::Type(qn("app", "myrange")),
&NodeId::Function(qn("app", "canon_fn"), arg_types),
),
"range type must depend on its canonical function"
);
}
#[test]
fn range_subtype_diff_fn_adds_function_edge() {
let mut c = Catalog::empty();
c.schemas.push(crate::ir::schema::Schema::new(id("app")));
let f = make_function("app", "diff_fn");
let arg_types = f.arg_types_normalized.clone();
c.functions.push(f);
c.types.push(make_range(
"app",
"myrange",
qn("pg_catalog", "int4"),
None,
Some(qn("app", "diff_fn")),
));
let g = build_create_graph(&c);
assert!(
has_edge(
&g,
&NodeId::Type(qn("app", "myrange")),
&NodeId::Function(qn("app", "diff_fn"), arg_types),
),
"range type must depend on its subtype_diff function"
);
}
#[test]
fn range_unmanaged_canonical_fn_adds_no_edge() {
let mut c = Catalog::empty();
c.schemas.push(crate::ir::schema::Schema::new(id("app")));
c.types.push(make_range(
"app",
"myrange",
qn("pg_catalog", "int4"),
Some(qn("app", "unmanaged_fn")),
None,
));
let g = build_create_graph(&c);
let deps: Vec<&NodeId> = g
.dependencies_of(&NodeId::Type(qn("app", "myrange")))
.collect();
assert_eq!(deps, vec![&NodeId::Schema(id("app"))]);
}
use crate::ir::collation::{Collation, CollationProvider};
fn make_collation(schema: &str, name: &str) -> Collation {
Collation {
qname: qn(schema, name),
provider: CollationProvider::Libc,
lc_collate: "C".into(),
lc_ctype: "C".into(),
deterministic: true,
version: None,
owner: None,
comment: None,
}
}
fn make_table_empty(schema: &str, name: &str) -> Table {
Table {
qname: qn(schema, name),
columns: vec![],
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(),
access_method: None,
}
}
fn col_text_with_collation(name: &str, collation: QualifiedName) -> Column {
Column {
name: id(name),
ty: ColumnType::Text,
nullable: true,
default: None,
identity: None,
generated: None,
collation: Some(collation),
storage: None,
compression: None,
comment: None,
}
}
fn make_domain_with_collation(schema: &str, name: &str, collation: QualifiedName) -> UserType {
UserType {
qname: qn(schema, name),
kind: UserTypeKind::Domain {
base: ColumnType::Text,
nullable: true,
default: None,
check_constraints: vec![],
collation: Some(collation),
},
comment: None,
owner: None,
grants: vec![],
}
}
fn make_range_with_collation(
schema: &str,
name: &str,
subtype: QualifiedName,
collation: QualifiedName,
) -> UserType {
UserType {
qname: qn(schema, name),
kind: UserTypeKind::Range {
subtype,
subtype_opclass: None,
collation: Some(collation),
canonical: None,
subtype_diff: None,
multirange_type_name: None,
},
comment: None,
owner: None,
grants: vec![],
}
}
fn make_composite_with_collated_attr(
schema: &str,
name: &str,
collation: QualifiedName,
) -> UserType {
UserType {
qname: qn(schema, name),
kind: UserTypeKind::Composite {
attributes: vec![CompositeAttribute {
name: id("val"),
ty: ColumnType::Text,
collation: Some(collation),
}],
},
comment: None,
owner: None,
grants: vec![],
}
}
#[test]
fn collation_node_registered_with_schema_edge() {
let mut c = Catalog::empty();
c.schemas.push(crate::ir::schema::Schema::new(id("app")));
c.collations.push(make_collation("app", "ci"));
let g = build_create_graph(&c);
assert_eq!(g.node_count(), 2);
assert!(
has_edge(
&g,
&NodeId::Collation(qn("app", "ci")),
&NodeId::Schema(id("app")),
),
"collation must depend on its schema (ensures DROP COLLATION before DROP SCHEMA)"
);
}
#[test]
fn column_with_managed_collation_adds_edge() {
let mut cat = Catalog::empty();
cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
cat.collations.push(make_collation("app", "ci"));
let mut t = make_table_empty("app", "users");
t.columns
.push(col_text_with_collation("email", qn("app", "ci")));
cat.tables.push(t);
let g = build_create_graph(&cat);
assert!(
has_edge(
&g,
&NodeId::Table(qn("app", "users")),
&NodeId::Collation(qn("app", "ci")),
),
"table must depend on its column's managed collation"
);
}
#[test]
fn column_with_pg_catalog_collation_no_edge() {
let mut cat = Catalog::empty();
cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
let mut t = make_table_empty("app", "users");
t.columns
.push(col_text_with_collation("email", qn("pg_catalog", "C")));
cat.tables.push(t);
let g = build_create_graph(&cat);
assert!(
!g.edges().any(|(_, to)| matches!(to, NodeId::Collation(_))),
"no collation edge should be added for pg_catalog.C"
);
}
#[test]
fn column_with_builtin_shortname_collation_no_edge() {
let mut cat = Catalog::empty();
cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
let mut t = make_table_empty("app", "users");
t.columns
.push(col_text_with_collation("email", qn("app", "POSIX")));
cat.tables.push(t);
let g = build_create_graph(&cat);
assert!(!g.edges().any(|(_, to)| matches!(to, NodeId::Collation(_))),);
}
#[test]
fn column_with_unmanaged_collation_no_edge() {
let mut cat = Catalog::empty();
cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
let mut t = make_table_empty("app", "users");
t.columns
.push(col_text_with_collation("email", qn("app", "unmanaged")));
cat.tables.push(t);
let g = build_create_graph(&cat);
assert!(!g.edges().any(|(_, to)| matches!(to, NodeId::Collation(_))),);
}
#[test]
fn domain_with_managed_collation_adds_edge() {
let mut cat = Catalog::empty();
cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
cat.collations.push(make_collation("app", "ci"));
cat.types.push(make_domain_with_collation(
"app",
"email_t",
qn("app", "ci"),
));
let g = build_create_graph(&cat);
assert!(
has_edge(
&g,
&NodeId::Type(qn("app", "email_t")),
&NodeId::Collation(qn("app", "ci")),
),
"domain must depend on its managed collation"
);
}
#[test]
fn domain_with_pg_catalog_collation_no_edge() {
let mut cat = Catalog::empty();
cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
cat.types.push(make_domain_with_collation(
"app",
"email_t",
qn("pg_catalog", "C"),
));
let g = build_create_graph(&cat);
assert!(!g.edges().any(|(_, to)| matches!(to, NodeId::Collation(_))),);
}
#[test]
fn range_with_managed_collation_adds_edge() {
let mut cat = Catalog::empty();
cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
cat.collations.push(make_collation("app", "ci"));
cat.types.push(make_range_with_collation(
"app",
"textrange",
qn("pg_catalog", "text"),
qn("app", "ci"),
));
let g = build_create_graph(&cat);
assert!(
has_edge(
&g,
&NodeId::Type(qn("app", "textrange")),
&NodeId::Collation(qn("app", "ci")),
),
"range type must depend on its managed collation"
);
}
#[test]
fn range_with_pg_catalog_collation_no_edge() {
let mut cat = Catalog::empty();
cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
cat.types.push(make_range_with_collation(
"app",
"textrange",
qn("pg_catalog", "text"),
qn("pg_catalog", "C"),
));
let g = build_create_graph(&cat);
assert!(!g.edges().any(|(_, to)| matches!(to, NodeId::Collation(_))),);
}
#[test]
fn composite_attribute_with_managed_collation_adds_edge() {
let mut cat = Catalog::empty();
cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
cat.collations.push(make_collation("app", "ci"));
cat.types.push(make_composite_with_collated_attr(
"app",
"addr_t",
qn("app", "ci"),
));
let g = build_create_graph(&cat);
assert!(
has_edge(
&g,
&NodeId::Type(qn("app", "addr_t")),
&NodeId::Collation(qn("app", "ci")),
),
"composite type must depend on collation of a collated attribute"
);
}
#[test]
fn composite_attribute_with_pg_catalog_collation_no_edge() {
let mut cat = Catalog::empty();
cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
cat.types.push(make_composite_with_collated_attr(
"app",
"addr_t",
qn("pg_catalog", "C"),
));
let g = build_create_graph(&cat);
assert!(!g.edges().any(|(_, to)| matches!(to, NodeId::Collation(_))),);
}
#[test]
fn collation_ordered_before_table_using_it() {
let mut cat = Catalog::empty();
cat.schemas.push(crate::ir::schema::Schema::new(id("app")));
cat.collations.push(make_collation("app", "ci"));
let mut t = make_table_empty("app", "users");
t.columns
.push(col_text_with_collation("email", qn("app", "ci")));
cat.tables.push(t);
let g = build_create_graph(&cat);
let order = g.topological_sort().expect("no cycle");
let coll_pos = order
.iter()
.position(|n| n == &NodeId::Collation(qn("app", "ci")))
.expect("collation in order");
let table_pos = order
.iter()
.position(|n| n == &NodeId::Table(qn("app", "users")))
.expect("table in order");
assert!(
coll_pos < table_pos,
"collation must be created before the table that uses it"
);
}
#[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");
}
use crate::ir::event_trigger::{EventTrigger, EventTriggerEnabled, EventTriggerEvent};
fn make_event_trigger(name: &str, function: QualifiedName) -> EventTrigger {
EventTrigger {
name: id(name),
event: EventTriggerEvent::DdlCommandEnd,
tag_filter: vec![],
function,
enabled: EventTriggerEnabled::Enabled,
owner: None,
comment: None,
}
}
#[test]
fn event_trigger_node_registered() {
let mut c = Catalog::empty();
let et = make_event_trigger("audit_et", qn("app", "audit_fn"));
c.event_triggers.push(et);
let g = build_create_graph(&c);
assert!(
g.nodes()
.any(|n| n == &NodeId::EventTrigger(id("audit_et"))),
"event trigger node must be registered in the graph"
);
}
#[test]
fn event_trigger_depends_on_managed_function() {
let mut c = Catalog::empty();
let f = make_function("app", "audit_fn");
let arg_types = f.arg_types_normalized.clone();
c.functions.push(f);
let et = make_event_trigger("audit_et", qn("app", "audit_fn"));
c.event_triggers.push(et);
let g = build_create_graph(&c);
assert!(
has_edge(
&g,
&NodeId::EventTrigger(id("audit_et")),
&NodeId::Function(qn("app", "audit_fn"), arg_types),
),
"event trigger must depend on the function it executes"
);
}
#[test]
fn event_trigger_no_edge_for_unmanaged_function() {
let mut c = Catalog::empty();
let et = make_event_trigger("audit_et", qn("app", "unmanaged_fn"));
c.event_triggers.push(et);
let g = build_create_graph(&c);
assert!(
g.nodes()
.any(|n| n == &NodeId::EventTrigger(id("audit_et"))),
"event trigger node must still be registered even if function is unmanaged"
);
assert!(
g.dependencies_of(&NodeId::EventTrigger(id("audit_et")))
.next()
.is_none(),
"no edge expected for unmanaged function reference"
);
}
}