1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
//! [`PlanError`] — errors raised by the dependency analyzer / planner.
use thiserror::Error;
/// Errors raised by the plan-ordering phase.
#[derive(Debug, Error, PartialEq, Eq)]
pub enum PlanError {
/// A dependency cycle remained after the planner attempted to break it
/// by extracting FK constraints. Carries the rendered node identifiers
/// participating in the cycle.
#[error("unbreakable dependency cycle: {0:?}")]
UnbreakableCycle(Vec<String>),
/// After FK extraction the modify-graph topo sort still cycled. This
/// indicates a non-FK cycle that the planner cannot resolve and is
/// almost certainly a bug in upstream phases.
#[error("unexpected cycle in modify graph after FK extraction: {0:?}")]
UnexpectedCycleAfterFkExtraction(Vec<String>),
/// The drop-graph topo sort cycled. Drops never have legitimate cycles;
/// this indicates a corrupt target catalog.
#[error("unexpected cycle in drop graph: {0:?}")]
UnexpectedDropCycle(Vec<String>),
/// An internal invariant was violated.
#[error("internal error: {0}")]
Internal(String),
/// Body-derived cycle in the dependency graph.
///
/// FK cycles between tables are auto-extracted into deferred FK adds
/// (`UnbreakableCycle` is a different error). Body-derived cycles —
/// view A queries view B that queries view A — have no general
/// mechanical fix and surface here. User must edit source to break
/// the cycle.
#[error("body-derived dependency cycle: {}", format_node_chain(.nodes))]
BodyCycle {
/// Nodes participating in the cycle, in graph-walk order.
nodes: Vec<crate::plan::edges::NodeId>,
},
/// An AST resolution error escalated to plan time (e.g., a sub-spec
/// resolver runs after the initial parse pass).
#[error("AST resolution failed during planning: {0}")]
AstResolution(String),
/// The `view_drop_create_dependents` policy is `false` and at least one
/// change would force dependent views to be dropped and recreated. Carries
/// the names of the blocked views.
///
/// Resolution: either enable `view_drop_create_dependents` in the planner
/// policy, or modify the migration to explicitly DROP + CREATE the listed
/// views.
#[error(
"view_drop_create_dependents is disabled but the following views would be \
force-recreated: {}", .views.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
)]
DependentViewsBlocked {
/// Views that would need to be recreated.
views: Vec<crate::identifier::QualifiedName>,
},
}
fn format_node_chain(nodes: &[crate::plan::edges::NodeId]) -> String {
nodes
.iter()
.map(render_node)
.collect::<Vec<_>>()
.join(" \u{2192} ")
}
fn render_node(n: &crate::plan::edges::NodeId) -> String {
use crate::plan::edges::NodeId::{
Constraint, Extension, Function, Index, Mv, Procedure, Schema, Sequence, Table, Trigger,
Type, View,
};
match n {
Schema(s) | Extension(s) => s.as_str().to_string(),
Table(q) | Index(q) | Sequence(q) | View(q) | Mv(q) | Type(q) | Procedure(q) => {
q.to_string()
}
Trigger(qname) => format!("trigger {qname}"),
Constraint { table, name } => format!("{}.{}", table, name.as_str()),
Function(q, args) => format!(
"{}({})",
q,
args.types
.iter()
.map(crate::ir::column_type::ColumnType::render_sql)
.collect::<Vec<_>>()
.join(",")
),
}
}