use std::collections::{BTreeMap, BTreeSet};
use crate::diff::change::{
Change, FunctionChange, MvChange, ProcedureChange, UserTypeChange, ViewChange,
};
use crate::identifier::QualifiedName;
use crate::ir::catalog::Catalog;
use crate::plan::edges::NodeId;
use crate::plan::policy::PlannerPolicy;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct DepTrigger {
qname: QualifiedName,
column: Option<String>,
}
pub fn extend_with_dependent_recreations(
changes: &mut Vec<Change>,
target_catalog: &Catalog,
policy: &PlannerPolicy,
) -> Result<(), Vec<QualifiedName>> {
let triggers = collect_upstream_triggers(changes);
if triggers.is_empty() {
return Ok(());
}
let affected = walk_transitive(&triggers, target_catalog);
if affected.is_empty() {
return Ok(());
}
#[allow(clippy::match_same_arms)] let already_in_changes: BTreeSet<QualifiedName> = changes
.iter()
.filter_map(|c| match c {
Change::View(ViewChange::ReplaceBody { source, .. }) => Some(source.qname.clone()),
Change::View(ViewChange::Drop(q)) => Some(q.clone()),
Change::Mv(MvChange::ReplaceBody { source, .. }) => Some(source.qname.clone()),
Change::Mv(MvChange::Drop(q)) => Some(q.clone()),
_ => None,
})
.collect();
let new_affected: Vec<QualifiedName> = affected
.into_iter()
.filter(|q| !already_in_changes.contains(q))
.collect();
if new_affected.is_empty() {
return Ok(());
}
if !policy.view_drop_create_dependents() {
return Err(new_affected);
}
for obj_qname in new_affected {
if let Some(cat) = target_catalog.views.iter().find(|v| v.qname == obj_qname) {
changes.push(Change::View(ViewChange::ReplaceBody {
source: cat.clone(),
catalog: cat.clone(),
compatible: false,
}));
} else if let Some(cat_mv) = target_catalog
.materialized_views
.iter()
.find(|mv| mv.qname == obj_qname)
{
changes.push(Change::Mv(MvChange::ReplaceBody {
source: cat_mv.clone(),
catalog: cat_mv.clone(),
}));
}
}
Ok(())
}
fn collect_upstream_triggers(changes: &[Change]) -> Vec<DepTrigger> {
let mut out = Vec::new();
for change in changes {
if let Some(trigger_qname) = object_drop_qname(change) {
out.push(DepTrigger {
qname: trigger_qname,
column: None,
});
} else {
match change {
Change::AlterTable { qname, ops } => {
for op_entry in ops {
match &op_entry.op {
crate::diff::table_op::TableOp::DropColumn { name, .. }
| crate::diff::table_op::TableOp::AlterColumnType { name, .. } => {
out.push(DepTrigger {
qname: qname.clone(),
column: Some(name.as_str().to_string()),
});
}
_ => {}
}
}
}
Change::View(ViewChange::ReplaceBody {
catalog,
compatible: false,
..
}) => out.push(DepTrigger {
qname: catalog.qname.clone(),
column: None,
}),
Change::Mv(MvChange::ReplaceBody { catalog, .. }) => out.push(DepTrigger {
qname: catalog.qname.clone(),
column: None,
}),
_ => {}
}
}
}
out
}
fn object_drop_qname(change: &Change) -> Option<QualifiedName> {
match change {
Change::DropTable { qname, .. }
| Change::View(ViewChange::Drop(qname))
| Change::Mv(MvChange::Drop(qname))
| Change::UserType(UserTypeChange::Drop(qname))
| Change::Function(FunctionChange::Drop { qname, .. })
| Change::Procedure(ProcedureChange::Drop(qname)) => Some(qname.clone()),
Change::UserType(UserTypeChange::ReplaceWithCascade { source, .. }) => {
Some(source.qname.clone())
}
Change::Function(FunctionChange::ReplaceWithCascade { source, .. }) => {
Some(source.qname.clone())
}
_ => None,
}
}
fn build_dep_index(
catalog: &Catalog,
) -> BTreeMap<QualifiedName, Vec<(QualifiedName, Option<String>)>> {
let mut index: BTreeMap<QualifiedName, Vec<(QualifiedName, Option<String>)>> = BTreeMap::new();
for v in &catalog.views {
let deps: Vec<(QualifiedName, Option<String>)> = v
.body_dependencies
.iter()
.filter_map(|dep| match &dep.to {
NodeId::Table(q)
| NodeId::View(q)
| NodeId::Mv(q)
| NodeId::Type(q)
| NodeId::Procedure(q)
| NodeId::Function(q, _) => Some((q.clone(), None)),
_ => None,
})
.collect();
if !deps.is_empty() {
index.insert(v.qname.clone(), deps);
}
}
for mv in &catalog.materialized_views {
let deps: Vec<(QualifiedName, Option<String>)> = mv
.body_dependencies
.iter()
.filter_map(|dep| match &dep.to {
NodeId::Table(q)
| NodeId::View(q)
| NodeId::Mv(q)
| NodeId::Type(q)
| NodeId::Procedure(q)
| NodeId::Function(q, _) => Some((q.clone(), None)),
_ => None,
})
.collect();
if !deps.is_empty() {
index.insert(mv.qname.clone(), deps);
}
}
index
}
fn walk_transitive(triggers: &[DepTrigger], catalog: &Catalog) -> Vec<QualifiedName> {
let dep_index = build_dep_index(catalog);
let mut reverse: BTreeMap<QualifiedName, BTreeSet<QualifiedName>> = BTreeMap::new();
for (view_qname, deps) in &dep_index {
for (dep_qname, _col) in deps {
reverse
.entry(dep_qname.clone())
.or_default()
.insert(view_qname.clone());
}
}
let mut affected: BTreeSet<QualifiedName> = BTreeSet::new();
let mut work_queue: Vec<QualifiedName> = Vec::new();
for trigger in triggers {
if let Some(dependent_views) = reverse.get(&trigger.qname) {
for view_qname in dependent_views {
if affected.insert(view_qname.clone()) {
work_queue.push(view_qname.clone());
}
}
}
}
while let Some(trigger_qname) = work_queue.pop() {
if let Some(dependent_views) = reverse.get(&trigger_qname) {
for view_qname in dependent_views {
if affected.insert(view_qname.clone()) {
work_queue.push(view_qname.clone());
}
}
}
}
if affected.is_empty() {
return Vec::new();
}
topological_sort_affected(&affected, &dep_index)
}
fn topological_sort_affected(
affected: &BTreeSet<QualifiedName>,
dep_index: &BTreeMap<QualifiedName, Vec<(QualifiedName, Option<String>)>>,
) -> Vec<QualifiedName> {
let mut in_degree: BTreeMap<&QualifiedName, usize> = affected.iter().map(|q| (q, 0)).collect();
let mut adj: BTreeMap<&QualifiedName, Vec<&QualifiedName>> = BTreeMap::new();
for view_qname in affected {
if let Some(deps) = dep_index.get(view_qname) {
for (dep_qname, _) in deps {
if affected.contains(dep_qname) {
adj.entry(dep_qname).or_default().push(view_qname);
*in_degree.entry(view_qname).or_insert(0) += 1;
}
}
}
}
let mut queue: std::collections::BinaryHeap<std::cmp::Reverse<&QualifiedName>> = in_degree
.iter()
.filter(|(_, deg)| **deg == 0)
.map(|(q, _)| std::cmp::Reverse(*q))
.collect();
let mut result = Vec::with_capacity(affected.len());
while let Some(std::cmp::Reverse(node)) = queue.pop() {
result.push(node.clone());
if let Some(dependents) = adj.get(node) {
for dep in dependents {
let entry = in_degree.entry(dep).or_insert(0);
if *entry > 0 {
*entry -= 1;
if *entry == 0 {
queue.push(std::cmp::Reverse(dep));
}
}
}
}
}
if result.len() < affected.len() {
let emitted: BTreeSet<QualifiedName> = result.iter().cloned().collect();
for q in affected {
if !emitted.contains(q) {
result.push(q.clone());
}
}
}
result
}
#[cfg(test)]
#[allow(clippy::redundant_clone)] mod tests {
use super::*;
use crate::diff::destructiveness::Destructiveness;
use crate::diff::table_op::{TableOp, TableOpEntry};
use crate::identifier::Identifier;
use crate::ir::catalog::Catalog;
use crate::ir::column_type::ColumnType;
use crate::ir::view::{View, ViewColumn};
use crate::parse::normalize_body::NormalizedBody;
use crate::plan::edges::{DepEdge, DepSource, NodeId};
use crate::plan::policy::PlannerPolicy;
fn id(s: &str) -> Identifier {
Identifier::from_unquoted(s).unwrap()
}
fn qn(schema: &str, name: &str) -> QualifiedName {
QualifiedName::new(id(schema), id(name))
}
fn body(sql: &str) -> NormalizedBody {
NormalizedBody::from_sql(sql).unwrap()
}
fn simple_view(schema: &str, name: &str, deps: Vec<DepEdge>) -> View {
View {
qname: qn(schema, name),
columns: vec![ViewColumn {
name: id("id"),
column_type: ColumnType::BigInt,
comment: None,
}],
body_canonical: body("SELECT 1"),
body_dependencies: deps,
security_barrier: None,
security_invoker: None,
comment: None,
raw_body: String::new(),
owner: None,
grants: vec![],
}
}
fn dep_edge(from_view: &QualifiedName, to_table: &QualifiedName) -> DepEdge {
DepEdge {
from: NodeId::View(from_view.clone()),
to: NodeId::Table(to_table.clone()),
source: DepSource::AstExtracted,
}
}
fn view_to_view_edge(from_view: &QualifiedName, to_view: &QualifiedName) -> DepEdge {
DepEdge {
from: NodeId::View(from_view.clone()),
to: NodeId::View(to_view.clone()),
source: DepSource::AstExtracted,
}
}
fn drop_table_change(qname: QualifiedName) -> Change {
Change::DropTable {
qname,
row_count_estimate: None,
}
}
fn alter_drop_col(table: QualifiedName, col: &str) -> Change {
Change::AlterTable {
qname: table,
ops: vec![TableOpEntry {
op: TableOp::DropColumn {
name: id(col),
is_populated: false,
},
destructiveness: Destructiveness::Safe,
}],
}
}
fn alter_col_type(table: QualifiedName, col: &str) -> Change {
Change::AlterTable {
qname: table,
ops: vec![TableOpEntry {
op: TableOp::AlterColumnType {
name: id(col),
from: ColumnType::BigInt,
to: ColumnType::Text,
using: None,
},
destructiveness: Destructiveness::Safe,
}],
}
}
fn policy_enabled() -> PlannerPolicy {
PlannerPolicy::default()
}
fn policy_disabled() -> PlannerPolicy {
let mut p = PlannerPolicy::default();
p.online.view_drop_create_dependents = false;
p
}
#[test]
fn drop_table_produces_trigger() {
let changes = vec![drop_table_change(qn("app", "users"))];
let triggers = collect_upstream_triggers(&changes);
assert_eq!(triggers.len(), 1);
assert_eq!(triggers[0].qname, qn("app", "users"));
assert!(triggers[0].column.is_none());
}
#[test]
fn drop_column_produces_column_trigger() {
let changes = vec![alter_drop_col(qn("app", "users"), "email")];
let triggers = collect_upstream_triggers(&changes);
assert_eq!(triggers.len(), 1);
assert_eq!(triggers[0].qname, qn("app", "users"));
assert_eq!(triggers[0].column.as_deref(), Some("email"));
}
#[test]
fn alter_column_type_produces_trigger() {
let changes = vec![alter_col_type(qn("app", "users"), "name")];
let triggers = collect_upstream_triggers(&changes);
assert_eq!(triggers.len(), 1);
assert_eq!(triggers[0].column.as_deref(), Some("name"));
}
#[test]
fn compatible_body_replace_does_not_trigger() {
let view = simple_view("app", "v", vec![]);
let changes = vec![Change::View(ViewChange::ReplaceBody {
source: view.clone(),
catalog: view,
compatible: true,
})];
let triggers = collect_upstream_triggers(&changes);
assert!(triggers.is_empty(), "compatible replace must not trigger");
}
#[test]
fn incompatible_body_replace_triggers() {
let view = simple_view("app", "v", vec![]);
let changes = vec![Change::View(ViewChange::ReplaceBody {
source: view.clone(),
catalog: view,
compatible: false,
})];
let triggers = collect_upstream_triggers(&changes);
assert_eq!(triggers.len(), 1);
assert_eq!(triggers[0].qname, qn("app", "v"));
}
#[test]
fn mv_drop_triggers() {
let changes = vec![Change::Mv(MvChange::Drop(qn("app", "mv")))];
let triggers = collect_upstream_triggers(&changes);
assert_eq!(triggers.len(), 1);
assert_eq!(triggers[0].qname, qn("app", "mv"));
}
#[test]
fn single_dependent_view_is_found() {
let v_qname = qn("app", "v");
let users = qn("app", "users");
let v = simple_view("app", "v", vec![dep_edge(&v_qname, &users)]);
let mut catalog = Catalog::empty();
catalog.views.push(v);
let triggers = vec![DepTrigger {
qname: users.clone(),
column: None,
}];
let affected = walk_transitive(&triggers, &catalog);
assert_eq!(affected, vec![v_qname]);
}
#[test]
fn chain_a_b_c_all_affected() {
let t = qn("app", "t");
let a = qn("app", "a");
let b = qn("app", "b");
let c = qn("app", "c");
let view_a = simple_view("app", "a", vec![dep_edge(&a, &t)]);
let view_b = simple_view("app", "b", vec![view_to_view_edge(&b, &a)]);
let view_c = simple_view("app", "c", vec![view_to_view_edge(&c, &b)]);
let mut catalog = Catalog::empty();
catalog.views.push(view_a);
catalog.views.push(view_b);
catalog.views.push(view_c);
let triggers = vec![DepTrigger {
qname: t.clone(),
column: None,
}];
let affected = walk_transitive(&triggers, &catalog);
assert!(
affected.contains(&a),
"view a must be in affected: {affected:?}"
);
assert!(
affected.contains(&b),
"view b must be in affected: {affected:?}"
);
assert!(
affected.contains(&c),
"view c must be in affected: {affected:?}"
);
let pos_a = affected.iter().position(|q| q == &a).unwrap();
let pos_b = affected.iter().position(|q| q == &b).unwrap();
let pos_c = affected.iter().position(|q| q == &c).unwrap();
assert!(pos_a < pos_b, "a must come before b: {affected:?}");
assert!(pos_b < pos_c, "b must come before c: {affected:?}");
}
#[test]
fn unrelated_view_not_included() {
let users = qn("app", "users");
let orders = qn("app", "orders");
let v1 = qn("app", "v1");
let v2 = qn("app", "v2");
let view_v1 = simple_view("app", "v1", vec![dep_edge(&v1, &users)]);
let view_v2 = simple_view("app", "v2", vec![dep_edge(&v2, &orders)]);
let mut catalog = Catalog::empty();
catalog.views.push(view_v1);
catalog.views.push(view_v2);
let triggers = vec![DepTrigger {
qname: users.clone(),
column: None,
}];
let affected = walk_transitive(&triggers, &catalog);
assert_eq!(affected, vec![v1]);
}
#[test]
fn no_triggers_no_affected() {
let v = simple_view("app", "v", vec![]);
let mut catalog = Catalog::empty();
catalog.views.push(v);
let affected = walk_transitive(&[], &catalog);
assert!(affected.is_empty());
}
#[test]
fn extend_adds_replace_body_for_affected_view() {
let users = qn("app", "users");
let v_qname = qn("app", "v");
let view = simple_view("app", "v", vec![dep_edge(&v_qname, &users)]);
let mut catalog = Catalog::empty();
catalog.views.push(view);
let mut changes = vec![drop_table_change(users.clone())];
let result = extend_with_dependent_recreations(&mut changes, &catalog, &policy_enabled());
assert!(result.is_ok());
assert_eq!(changes.len(), 2);
assert!(
matches!(
&changes[1],
Change::View(ViewChange::ReplaceBody { source, compatible: false, .. })
if source.qname == v_qname
),
"expected ReplaceBody for v, got: {:?}",
changes[1]
);
}
#[test]
fn extend_does_not_duplicate_already_replaced_view() {
let users = qn("app", "users");
let v_qname = qn("app", "v");
let view = simple_view("app", "v", vec![dep_edge(&v_qname, &users)]);
let mut catalog = Catalog::empty();
catalog.views.push(view.clone());
let mut changes = vec![
drop_table_change(users.clone()),
Change::View(ViewChange::ReplaceBody {
source: view.clone(),
catalog: view,
compatible: false,
}),
];
let result = extend_with_dependent_recreations(&mut changes, &catalog, &policy_enabled());
assert!(result.is_ok());
assert_eq!(changes.len(), 2);
}
#[test]
fn policy_disabled_returns_error_with_names() {
let users = qn("app", "users");
let v_qname = qn("app", "v");
let view = simple_view("app", "v", vec![dep_edge(&v_qname, &users)]);
let mut catalog = Catalog::empty();
catalog.views.push(view);
let mut changes = vec![drop_table_change(users.clone())];
let result = extend_with_dependent_recreations(&mut changes, &catalog, &policy_disabled());
assert!(result.is_err());
let affected = result.unwrap_err();
assert!(
affected.contains(&v_qname),
"expected v in error list: {affected:?}"
);
assert_eq!(changes.len(), 1);
}
#[test]
fn no_changes_no_recreation() {
let view = simple_view("app", "v", vec![]);
let mut catalog = Catalog::empty();
catalog.views.push(view);
let mut changes = vec![];
let result = extend_with_dependent_recreations(&mut changes, &catalog, &policy_enabled());
assert!(result.is_ok());
assert!(changes.is_empty());
}
fn simple_mv(
schema: &str,
name: &str,
deps: Vec<DepEdge>,
) -> crate::ir::view::MaterializedView {
crate::ir::view::MaterializedView {
qname: qn(schema, name),
columns: vec![],
body_canonical: body("SELECT 1"),
body_dependencies: deps,
comment: None,
raw_body: String::new(),
owner: None,
grants: vec![],
storage: crate::ir::reloptions::MaterializedViewStorageOptions::default(),
}
}
fn mv_dep_edge(from_mv: &QualifiedName, to_table: &QualifiedName) -> DepEdge {
DepEdge {
from: NodeId::Mv(from_mv.clone()),
to: NodeId::Table(to_table.clone()),
source: DepSource::AstExtracted,
}
}
#[test]
fn mv_with_body_dep_on_table_is_recreated_on_table_drop() {
let orders = qn("app", "orders");
let mv_qname = qn("app", "totals");
let mv = simple_mv("app", "totals", vec![mv_dep_edge(&mv_qname, &orders)]);
let mut catalog = Catalog::empty();
catalog.materialized_views.push(mv);
let mut changes = vec![drop_table_change(orders.clone())];
let result = extend_with_dependent_recreations(&mut changes, &catalog, &policy_enabled());
assert!(result.is_ok());
assert_eq!(
changes.len(),
2,
"expected DropTable + MvChange::ReplaceBody"
);
assert!(
matches!(
&changes[1],
Change::Mv(MvChange::ReplaceBody { source, .. })
if source.qname == mv_qname
),
"expected MvChange::ReplaceBody for totals, got: {:?}",
changes[1]
);
}
#[test]
fn mv_replace_body_not_duplicated() {
let orders = qn("app", "orders");
let mv_qname = qn("app", "totals");
let mv = simple_mv("app", "totals", vec![mv_dep_edge(&mv_qname, &orders)]);
let mut catalog = Catalog::empty();
catalog.materialized_views.push(mv.clone());
let mut changes = vec![
drop_table_change(orders.clone()),
Change::Mv(MvChange::ReplaceBody {
source: mv.clone(),
catalog: mv,
}),
];
let result = extend_with_dependent_recreations(&mut changes, &catalog, &policy_enabled());
assert!(result.is_ok());
assert_eq!(
changes.len(),
2,
"should not duplicate MvChange::ReplaceBody"
);
}
#[test]
fn mv_recreation_blocked_by_policy_returns_error() {
let orders = qn("app", "orders");
let mv_qname = qn("app", "totals");
let mv = simple_mv("app", "totals", vec![mv_dep_edge(&mv_qname, &orders)]);
let mut catalog = Catalog::empty();
catalog.materialized_views.push(mv);
let mut changes = vec![drop_table_change(orders.clone())];
let result = extend_with_dependent_recreations(&mut changes, &catalog, &policy_disabled());
assert!(result.is_err(), "policy disabled must return Err");
let affected = result.unwrap_err();
assert!(
affected.contains(&mv_qname),
"expected totals in error list: {affected:?}"
);
assert_eq!(
changes.len(),
1,
"changes must not be modified when policy blocks"
);
}
use crate::diff::change::UserTypeChange;
use crate::ir::user_type::{UserType, UserTypeKind};
fn make_enum_type(schema: &str, name: &str) -> UserType {
UserType {
qname: qn(schema, name),
kind: UserTypeKind::Enum { values: vec![] },
comment: None,
owner: None,
grants: vec![],
}
}
fn type_dep_edge(from_view: &QualifiedName, to_type: &QualifiedName) -> DepEdge {
DepEdge {
from: NodeId::View(from_view.clone()),
to: NodeId::Type(to_type.clone()),
source: DepSource::AstExtracted,
}
}
fn mv_type_dep_edge(from_mv: &QualifiedName, to_type: &QualifiedName) -> DepEdge {
DepEdge {
from: NodeId::Mv(from_mv.clone()),
to: NodeId::Type(to_type.clone()),
source: DepSource::AstExtracted,
}
}
#[test]
fn type_drop_cascades_to_dependent_view() {
let status_type = qn("app", "status");
let v_qname = qn("app", "order_view");
let view = simple_view(
"app",
"order_view",
vec![type_dep_edge(&v_qname, &status_type)],
);
let mut catalog = Catalog::empty();
catalog.views.push(view);
let mut changes = vec![Change::UserType(UserTypeChange::Drop(status_type.clone()))];
let result = extend_with_dependent_recreations(&mut changes, &catalog, &policy_enabled());
assert!(result.is_ok());
assert_eq!(
changes.len(),
2,
"expected UserType::Drop + ReplaceBody for dependent view"
);
assert!(
matches!(
&changes[1],
Change::View(ViewChange::ReplaceBody { source, compatible: false, .. })
if source.qname == v_qname
),
"expected ReplaceBody for order_view, got: {:?}",
changes[1]
);
}
#[test]
fn type_replace_with_cascade_cascades_to_dependent_view() {
let status_type = qn("app", "status");
let v_qname = qn("app", "order_view");
let view = simple_view(
"app",
"order_view",
vec![type_dep_edge(&v_qname, &status_type)],
);
let ut = make_enum_type("app", "status");
let mut catalog = Catalog::empty();
catalog.views.push(view);
let mut changes = vec![Change::UserType(UserTypeChange::ReplaceWithCascade {
source: ut.clone(),
catalog: ut.clone(),
})];
let result = extend_with_dependent_recreations(&mut changes, &catalog, &policy_enabled());
assert!(result.is_ok());
assert_eq!(
changes.len(),
2,
"expected ReplaceWithCascade + ReplaceBody for dependent view"
);
assert!(
matches!(
&changes[1],
Change::View(ViewChange::ReplaceBody { source, compatible: false, .. })
if source.qname == v_qname
),
"expected ReplaceBody for order_view on cascade replace, got: {:?}",
changes[1]
);
}
#[test]
fn type_drop_cascades_to_dependent_mv() {
let status_type = qn("app", "status");
let mv_qname = qn("app", "type_summary");
let mv = simple_mv(
"app",
"type_summary",
vec![mv_type_dep_edge(&mv_qname, &status_type)],
);
let mut catalog = Catalog::empty();
catalog.materialized_views.push(mv);
let mut changes = vec![Change::UserType(UserTypeChange::Drop(status_type.clone()))];
let result = extend_with_dependent_recreations(&mut changes, &catalog, &policy_enabled());
assert!(result.is_ok());
assert_eq!(
changes.len(),
2,
"expected UserType::Drop + MvChange::ReplaceBody"
);
assert!(
matches!(
&changes[1],
Change::Mv(MvChange::ReplaceBody { source, .. })
if source.qname == mv_qname
),
"expected MvChange::ReplaceBody for type_summary, got: {:?}",
changes[1]
);
}
#[test]
fn type_drop_recreation_blocked_by_policy() {
let status_type = qn("app", "status");
let v_qname = qn("app", "order_view");
let view = simple_view(
"app",
"order_view",
vec![type_dep_edge(&v_qname, &status_type)],
);
let mut catalog = Catalog::empty();
catalog.views.push(view);
let mut changes = vec![Change::UserType(UserTypeChange::Drop(status_type.clone()))];
let result = extend_with_dependent_recreations(&mut changes, &catalog, &policy_disabled());
assert!(result.is_err(), "policy must block recreation");
let affected = result.unwrap_err();
assert!(
affected.contains(&v_qname),
"expected order_view in error list: {affected:?}"
);
assert_eq!(changes.len(), 1);
}
use crate::diff::change::FunctionChange;
use crate::ir::function::{
Function, FunctionLanguage, NormalizedArgTypes, ParallelSafety, ReturnType, SecurityMode,
Volatility,
};
use crate::ir::procedure::Procedure;
fn make_function(schema: &str, name: &str) -> Function {
let args = vec![];
let arg_types_normalized = NormalizedArgTypes::from_args(&args);
Function {
qname: qn(schema, name),
args,
arg_types_normalized,
return_type: ReturnType::Scalar {
ty: ColumnType::BigInt,
},
language: FunctionLanguage::Sql,
body: NormalizedBody::from_sql("SELECT 1").unwrap(),
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![],
}
}
fn make_procedure(schema: &str, name: &str) -> Procedure {
Procedure {
qname: qn(schema, name),
args: vec![],
language: FunctionLanguage::PlPgSql,
body: NormalizedBody::empty(),
body_dependencies: vec![],
security: SecurityMode::Invoker,
commits_in_body: false,
comment: None,
owner: None,
grants: vec![],
}
}
fn function_dep_edge(from_view: &QualifiedName, to_fn: &QualifiedName) -> DepEdge {
let nat = NormalizedArgTypes::from_args(&[]);
DepEdge {
from: NodeId::View(from_view.clone()),
to: NodeId::Function(to_fn.clone(), nat),
source: DepSource::AstExtracted,
}
}
fn procedure_dep_edge(from_view: &QualifiedName, to_proc: &QualifiedName) -> DepEdge {
DepEdge {
from: NodeId::View(from_view.clone()),
to: NodeId::Procedure(to_proc.clone()),
source: DepSource::AstExtracted,
}
}
#[test]
fn function_drop_cascades_to_dependent_view() {
let fn_qname = qn("app", "f");
let v_qname = qn("app", "v_using_f");
let view = simple_view(
"app",
"v_using_f",
vec![function_dep_edge(&v_qname, &fn_qname)],
);
let f = make_function("app", "f");
let mut catalog = Catalog::empty();
catalog.views.push(view);
catalog.functions.push(f.clone());
let mut changes = vec![Change::Function(FunctionChange::Drop {
qname: fn_qname.clone(),
args: f.arg_types_normalized.clone(),
})];
let result = extend_with_dependent_recreations(&mut changes, &catalog, &policy_enabled());
assert!(result.is_ok());
assert_eq!(
changes.len(),
2,
"expected Function::Drop + ReplaceBody for dependent view, got: {changes:?}"
);
assert!(
matches!(
&changes[1],
Change::View(ViewChange::ReplaceBody { source, compatible: false, .. })
if source.qname == v_qname
),
"expected ReplaceBody for v_using_f, got: {:?}",
changes[1]
);
}
#[test]
fn function_replace_with_cascade_propagates_to_dependent_view() {
let fn_qname = qn("app", "f");
let v_qname = qn("app", "v_using_f");
let view = simple_view(
"app",
"v_using_f",
vec![function_dep_edge(&v_qname, &fn_qname)],
);
let f = make_function("app", "f");
let mut catalog = Catalog::empty();
catalog.views.push(view);
catalog.functions.push(f.clone());
let mut changes = vec![Change::Function(FunctionChange::ReplaceWithCascade {
source: f.clone(),
catalog: f,
})];
let result = extend_with_dependent_recreations(&mut changes, &catalog, &policy_enabled());
assert!(result.is_ok());
assert_eq!(
changes.len(),
2,
"expected ReplaceWithCascade + ReplaceBody for dependent view, got: {changes:?}"
);
assert!(
matches!(
&changes[1],
Change::View(ViewChange::ReplaceBody { source, compatible: false, .. })
if source.qname == v_qname
),
"expected ReplaceBody for v_using_f on cascade replace, got: {:?}",
changes[1]
);
}
#[test]
fn procedure_drop_does_not_cascade_unnecessarily() {
let proc_qname = qn("app", "do_thing");
let mut catalog = Catalog::empty();
catalog.procedures.push(make_procedure("app", "do_thing"));
let mut changes = vec![Change::Procedure(ProcedureChange::Drop(proc_qname.clone()))];
let result = extend_with_dependent_recreations(&mut changes, &catalog, &policy_enabled());
assert!(result.is_ok());
assert_eq!(
changes.len(),
1,
"a procedure drop with no dependents must not append extra changes; got: {changes:?}"
);
}
#[test]
fn procedure_drop_cascades_to_dependent_view() {
let proc_qname = qn("app", "do_thing");
let v_qname = qn("app", "v_using_proc");
let view = simple_view(
"app",
"v_using_proc",
vec![procedure_dep_edge(&v_qname, &proc_qname)],
);
let mut catalog = Catalog::empty();
catalog.views.push(view);
catalog.procedures.push(make_procedure("app", "do_thing"));
let mut changes = vec![Change::Procedure(ProcedureChange::Drop(proc_qname.clone()))];
let result = extend_with_dependent_recreations(&mut changes, &catalog, &policy_enabled());
assert!(result.is_ok());
assert_eq!(
changes.len(),
2,
"expected Procedure::Drop + ReplaceBody for dependent view; got: {changes:?}"
);
assert!(
matches!(
&changes[1],
Change::View(ViewChange::ReplaceBody { source, compatible: false, .. })
if source.qname == v_qname
),
"expected ReplaceBody for v_using_proc, got: {:?}",
changes[1]
);
}
}