Skip to main content

panproto_mig/
error.rs

1//! Error types for migration operations.
2//!
3//! Each error type corresponds to a distinct failure mode in the
4//! migration pipeline: existence checking, compilation, lifting,
5//! composition, and inversion.
6
7use serde::{Deserialize, Serialize};
8
9use crate::solve::build::BuildError;
10use crate::solve::mcsplit::IsoError;
11
12/// Top-level migration error.
13#[derive(Debug, thiserror::Error)]
14#[non_exhaustive]
15pub enum MigError {
16    /// An existence condition was violated.
17    #[error("existence check failed: {0}")]
18    Existence(#[from] ExistenceError),
19
20    /// Lifting a record failed.
21    #[error("lift failed: {0}")]
22    Lift(#[from] LiftError),
23
24    /// Migration composition failed.
25    #[error("compose failed: {0}")]
26    Compose(#[from] ComposeError),
27
28    /// Migration inversion failed.
29    #[error("inversion failed: {0}")]
30    Invert(#[from] InvertError),
31
32    /// A span search could not produce a span.
33    #[error("span search failed: {0}")]
34    Span(#[from] SpanError),
35}
36
37/// Why a span search could not produce a span.
38///
39/// None of these variants means "no morphism exists". The span search is total:
40/// leaving every source vertex out of the apex is always feasible, so the
41/// absence of a match is reported as an empty apex rather than as an error.
42/// What is reported here is a search that could not be posed or a result that
43/// is not a schema, both of which are defects rather than answers.
44#[derive(Debug, thiserror::Error)]
45#[non_exhaustive]
46pub enum SpanError {
47    /// The cost function network could not be built from the schema pair.
48    #[error("the search network could not be built: {source}")]
49    Build {
50        /// What the network builder refused.
51        #[from]
52        source: BuildError,
53    },
54
55    /// The apex is not a well-formed sub-schema of the source.
56    ///
57    /// This carries whatever validating the induced apex against the protocol
58    /// reported. Dangling references are one cause and the one the network
59    /// guards against, by forbidding the assignments whose apex would carry
60    /// one, so a dangling reference here does mean a hard constraint is
61    /// missing. It is not the only cause: validation also checks vertex kinds,
62    /// edge rules and constraint sorts, none of which the network models, and a
63    /// sub-schema of a source the protocol already rejects inherits the
64    /// parent's findings. The common case is therefore an invalid input rather
65    /// than a missing constraint, and the two are told apart by running
66    /// [`validate`](fn@panproto_schema::validate) on the source: if it reports
67    /// the same findings, the apex only surfaced them.
68    #[error("the apex is not a well-formed sub-schema of the source: {source}")]
69    Apex {
70        /// What inducing the apex reported.
71        #[from]
72        source: panproto_schema::SchemaError,
73    },
74
75    /// The total-morphism search stopped before reaching any complete
76    /// assignment, so whether one exists is unknown.
77    ///
78    /// Distinct from `Ok(vec![])`, which is the search finishing and finding
79    /// nothing. Branch and bound reaches complete assignments as it dives, so a
80    /// budget spent before the first leaf leaves it with no incumbent at all,
81    /// and the empty answer that would report is a claim the search never
82    /// established. A stop *after* a leaf is not reported here: that incumbent
83    /// is a genuine total morphism, only not a proven-optimal one.
84    #[error(
85        "the total-morphism search stopped on {limit:?} before reaching any complete \
86         assignment, so whether a total morphism exists is unknown"
87    )]
88    Stopped {
89        /// Which budget ran out.
90        limit: crate::solve::LimitKind,
91    },
92
93    /// The maximum common sub-schema search refused the network.
94    ///
95    /// Its reward frame has preconditions the network must meet, and it refuses
96    /// rather than silently optimising a different objective when one is
97    /// broken.
98    #[error("the maximum common sub-schema search refused the network: {source}")]
99    Iso {
100        /// The precondition of the reward frame the network broke.
101        #[from]
102        source: IsoError,
103    },
104
105    /// Surjectivity was asked of a span.
106    ///
107    /// [`SearchOptions::epic`](crate::SearchOptions::epic) is a property of a
108    /// *total* morphism and the span search cannot promise it: a span's right
109    /// leg is deliberately partial, the empty apex is always feasible, and
110    /// [`find_span`](crate::find_span) is documented never to refuse for want of
111    /// a match. Enforcing surjectivity would make it refuse, and ignoring the
112    /// flag would answer a different question than the one asked, so the
113    /// combination is rejected instead.
114    #[error(
115        "`epic` asks for a surjective vertex map, which is a property of a total \
116         morphism rather than of a span; use `find_morphisms` or \
117         `find_best_morphism` for a surjective total morphism"
118    )]
119    EpicIsNotASpanProperty,
120
121    /// The span's right leg identifies two apex vertices, so it has no pushout.
122    ///
123    /// A merge along the apex has to commute: an apex vertex must reach the
124    /// same merged vertex through either leg. A right leg that sends two apex
125    /// vertices to one target vertex makes that impossible, and the square that
126    /// comes back is not a cocone over the span it was asked about. Set
127    /// [`SearchOptions::iso`](crate::SearchOptions::iso), which is what
128    /// [`discover_overlap`](crate::discover_overlap) does, to search for a span
129    /// whose right leg is an embedding.
130    #[error(
131        "the span's right leg identifies two apex vertices, so merging along it \
132         would not commute; search with `iso` for a span that embeds"
133    )]
134    ContractingRightLeg,
135}
136
137/// A structured existence error detected by `check_existence`.
138///
139/// These conditions are theory-derived: the set of applicable checks
140/// depends on the sorts present in the protocol's schema and instance
141/// theories.
142#[derive(Debug, Clone, Serialize, Deserialize, thiserror::Error)]
143#[non_exhaustive]
144pub enum ExistenceError {
145    /// An edge required by the target schema has no preimage in the migration.
146    #[error("edge missing: {src} -> {tgt} (kind: {kind})")]
147    EdgeMissing {
148        /// Source vertex ID.
149        src: String,
150        /// Target vertex ID.
151        tgt: String,
152        /// Edge kind.
153        kind: String,
154    },
155
156    /// A vertex is mapped to targets with inconsistent kinds.
157    #[error("kind inconsistency for {kind}: targets = {targets:?}")]
158    KindInconsistency {
159        /// The vertex kind that is inconsistent.
160        kind: String,
161        /// The set of target kinds observed.
162        targets: Vec<String>,
163    },
164
165    /// A label is mapped to targets with inconsistent names.
166    #[error("label inconsistency for {label}: targets = {targets:?}")]
167    LabelInconsistency {
168        /// The label that is inconsistent.
169        label: String,
170        /// The set of target labels observed.
171        targets: Vec<String>,
172    },
173
174    /// A required field in the target has no source.
175    #[error("required field missing: vertex {vertex}, field {field}")]
176    RequiredFieldMissing {
177        /// The target vertex ID.
178        vertex: String,
179        /// The missing field (edge name).
180        field: String,
181    },
182
183    /// A constraint was tightened (target is more restrictive than source).
184    #[error("constraint tightened on {vertex}: {sort} changed from {src_val} to {tgt_val}")]
185    ConstraintTightened {
186        /// The vertex ID.
187        vertex: String,
188        /// The constraint sort (e.g., `"maxLength"`).
189        sort: String,
190        /// Source constraint value.
191        src_val: String,
192        /// Target constraint value.
193        tgt_val: String,
194    },
195
196    /// A resolver entry references an invalid vertex pair.
197    #[error("resolver invalid for pair ({}, {})", pair.0, pair.1)]
198    ResolverInvalid {
199        /// The invalid `(src, tgt)` pair.
200        pair: (String, String),
201    },
202
203    /// A general well-formedness violation.
204    #[error("well-formedness: {message}")]
205    WellFormedness {
206        /// Description of the violation.
207        message: String,
208    },
209
210    /// A hyper-edge signature is incoherent after mapping.
211    #[error("signature incoherent for hyper-edge {hyper_edge}: label {label}")]
212    SignatureCoherence {
213        /// The hyper-edge ID.
214        hyper_edge: String,
215        /// The problematic label.
216        label: String,
217    },
218
219    /// A hyper-edge requires simultaneous presence of labels that the
220    /// migration drops.
221    #[error("simultaneity violation for hyper-edge {hyper_edge}: missing label {missing_label}")]
222    Simultaneity {
223        /// The hyper-edge ID.
224        hyper_edge: String,
225        /// The label that would be missing.
226        missing_label: String,
227    },
228
229    /// A vertex risks becoming unreachable after migration.
230    #[error("reachability risk for vertex {vertex}: {reason}")]
231    ReachabilityRisk {
232        /// The vertex at risk.
233        vertex: String,
234        /// Why it risks becoming unreachable.
235        reason: String,
236    },
237
238    /// The migration's mapped fragment is not a structure-preserving
239    /// theory morphism: a mapped edge does not connect the images of its
240    /// own endpoints, or a mapped vertex is absent from the target.
241    #[error("migration is not a theory morphism on its mapped fragment: {detail}")]
242    NotAMorphism {
243        /// The underlying structural violation, as reported by
244        /// `check_morphism`.
245        detail: String,
246    },
247}
248
249/// Errors from the lift (record migration) operation.
250#[derive(Debug, thiserror::Error)]
251#[non_exhaustive]
252pub enum LiftError {
253    /// The underlying restrict operation failed.
254    #[error("restrict failed: {0}")]
255    Restrict(#[from] panproto_inst::RestrictError),
256
257    /// The target schema is missing.
258    #[error("target schema is required for W-type lift")]
259    MissingTargetSchema,
260
261    /// The term-level chase failed while closing a `Sigma` result.
262    ///
263    /// The chase's own error is carried, not flattened to text, because
264    /// the two failures it reports call for different responses: a
265    /// budget that ran out can be retried with a larger one, while an
266    /// equality conflict is a property of the data and the dependencies
267    /// and will recur however much budget it is given.
268    #[error("chase failed: {0}")]
269    Chase(#[from] crate::chase::ChaseError),
270
271    /// The term-level chase ran out of budget before reaching a
272    /// fixpoint. Retrying with a larger budget may succeed.
273    #[error(
274        "term-level chase did not terminate within {max_iterations} iterations / {max_nulls} nulls"
275    )]
276    ChaseBudgetExhausted {
277        /// The iteration ceiling the chase was given.
278        max_iterations: usize,
279        /// The labeled-null ceiling the chase was given.
280        max_nulls: usize,
281    },
282}
283
284impl LiftError {
285    /// Whether retrying the lift with a larger chase budget could
286    /// succeed.
287    ///
288    /// True exactly for the two budget-exhaustion failures. Every other
289    /// variant reports something a larger budget cannot change.
290    #[must_use]
291    pub const fn is_retryable(&self) -> bool {
292        match self {
293            Self::Chase(err) => err.is_retryable(),
294            Self::ChaseBudgetExhausted { .. } => true,
295            _ => false,
296        }
297    }
298}
299
300/// Errors from migration composition.
301#[derive(Debug, thiserror::Error)]
302#[non_exhaustive]
303pub enum ComposeError {
304    /// An edge in the intermediate schema is not in the second migration's domain.
305    #[error("edge not found in second migration's domain: {src} -> {tgt} ({kind})")]
306    EdgeNotInDomain {
307        /// Source vertex.
308        src: String,
309        /// Target vertex.
310        tgt: String,
311        /// Edge kind.
312        kind: String,
313    },
314
315    /// The first migration's codomain does not match the second
316    /// migration's domain, so the two are not composable.
317    #[error(
318        "migrations are not composable: first codomain `{first_codomain}` != second domain `{second_domain}`"
319    )]
320    DomainMismatch {
321        /// The codomain identifier of the first migration.
322        first_codomain: String,
323        /// The domain identifier of the second migration.
324        second_domain: String,
325    },
326}
327
328/// Errors from migration inversion.
329#[derive(Debug, thiserror::Error)]
330#[non_exhaustive]
331pub enum InvertError {
332    /// The vertex map is not bijective (injective + surjective).
333    #[error("vertex map is not bijective: {detail}")]
334    NotBijective {
335        /// Description of the bijectivity failure.
336        detail: String,
337    },
338
339    /// The edge map is not bijective.
340    #[error("edge map is not bijective: {detail}")]
341    EdgeNotBijective {
342        /// Description of the bijectivity failure.
343        detail: String,
344    },
345
346    /// Vertices were dropped (the migration is not surjective on vertices).
347    #[error("migration drops vertices: {dropped:?}")]
348    DroppedVertices {
349        /// The dropped vertex IDs.
350        dropped: Vec<String>,
351    },
352
353    /// Edges were dropped.
354    #[error("migration drops edges")]
355    DroppedEdges,
356
357    /// The hyper-edge map is not bijective.
358    #[error("hyper-edge map is not bijective: {detail}")]
359    HyperEdgeNotBijective {
360        /// Description of the bijectivity failure.
361        detail: String,
362    },
363
364    /// Hyper-edges were dropped (the migration is not surjective on hyper-edges).
365    #[error("migration drops hyper-edges: {dropped:?}")]
366    DroppedHyperEdges {
367        /// The dropped hyper-edge IDs.
368        dropped: Vec<String>,
369    },
370
371    /// A vertex's value-level coercion records no inverse term, so the
372    /// inverted migration has no way to bring its values back.
373    #[error(
374        "the coercion at vertex `{vertex}` records no inverse term, so the values \
375         it rewrites cannot be brought back; the inverse migration is undefined \
376         there"
377    )]
378    CoercionNotInvertible {
379        /// The source vertex whose coercion has no inverse.
380        vertex: String,
381    },
382}