Skip to main content

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