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