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}