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 Collation, Constraint, Extension, Function, Index, Mv, Procedure, Publication, Schema,
75 Sequence, Statistic, Subscription, Table, Trigger, Type, View,
76 };
77 match n {
78 Schema(s) | Extension(s) | Publication(s) | Subscription(s) => s.as_str().to_string(),
79 Table(q) | Index(q) | Sequence(q) | View(q) | Mv(q) | Type(q) | Procedure(q)
80 | Statistic(q) | Collation(q) => q.to_string(),
81 Trigger(qname) => format!("trigger {qname}"),
82 Constraint { table, name } => format!("{}.{}", table, name.as_str()),
83 Function(q, args) => format!(
84 "{}({})",
85 q,
86 args.types
87 .iter()
88 .map(crate::ir::column_type::ColumnType::render_sql)
89 .collect::<Vec<_>>()
90 .join(",")
91 ),
92 }
93}