Skip to main content

hayson_engine/
errors.rs

1use std::fmt;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub struct HaysonSchemaError(pub String);
5
6impl fmt::Display for HaysonSchemaError {
7    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8        write!(f, "{}", self.0)
9    }
10}
11
12impl std::error::Error for HaysonSchemaError {}
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct ValidationIssue {
16    pub field: String,
17    pub reason: String,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct HaysonValidationError {
22    pub table: String,
23    pub issues: Vec<ValidationIssue>,
24}
25
26impl fmt::Display for HaysonValidationError {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        let detail = self
29            .issues
30            .iter()
31            .map(|issue| format!("{}: {}", issue.field, issue.reason))
32            .collect::<Vec<_>>()
33            .join("; ");
34        write!(f, "record rejected by schema of table \"{}\" — {}", self.table, detail)
35    }
36}
37
38impl std::error::Error for HaysonValidationError {}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct HaysonUniqueViolationError {
42    pub table: String,
43    pub field: String,
44    pub holder: String,
45}
46
47impl fmt::Display for HaysonUniqueViolationError {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        write!(
50            f,
51            "unique field \"{}\" of table \"{}\" is already taken by key \"{}\"",
52            self.field, self.table, self.holder
53        )
54    }
55}
56
57impl std::error::Error for HaysonUniqueViolationError {}
58
59/// A write refused because it broke an `enforce: true` relation — `from.field`'s new value has no
60/// matching record on the `to` side. `value` is the offending value's JSON text.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct HaysonRelationViolationError {
63    pub relation: String,
64    pub table: String,
65    pub field: String,
66    pub value: String,
67}
68
69impl fmt::Display for HaysonRelationViolationError {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        write!(
72            f,
73            "relation \"{}\" refuses \"{}\".\"{}\" = {}: no record on the other side of the relation matches it",
74            self.relation, self.table, self.field, self.value
75        )
76    }
77}
78
79impl std::error::Error for HaysonRelationViolationError {}
80
81/// A `delete` refused because an `on_delete: 'restrict'` relation still has a live record
82/// referencing the key being deleted. `referencing` is the count of records found still pointing
83/// at it — capped at the same sample the diagnostic report uses, never a full scan's whole count.
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct HaysonRelationRestrictError {
86    pub relation: String,
87    pub table: String,
88    pub key: String,
89    pub referencing_table: String,
90    pub referencing_field: String,
91    pub referencing: usize,
92}
93
94impl fmt::Display for HaysonRelationRestrictError {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        write!(
97            f,
98            "relation \"{}\" refuses deleting \"{}\".\"{}\": {} record(s) of \"{}\" still point at it through \"{}\"",
99            self.relation, self.table, self.key, self.referencing, self.referencing_table, self.referencing_field
100        )
101    }
102}
103
104impl std::error::Error for HaysonRelationRestrictError {}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct HaysonPendingConflictError {
108    pub table: String,
109    pub key: String,
110    pub fields: Vec<String>,
111}
112
113impl fmt::Display for HaysonPendingConflictError {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        let list = self
116            .fields
117            .iter()
118            .map(|field| format!("\"{field}\""))
119            .collect::<Vec<_>>()
120            .join(", ");
121        write!(
122            f,
123            "key \"{}\" of table \"{}\" already has a queued change on {} — pass force to overwrite it",
124            self.key, self.table, list
125        )
126    }
127}
128
129impl std::error::Error for HaysonPendingConflictError {}
130
131#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct HaysonCorruptLogError {
133    pub path: String,
134    pub offset: u64,
135    pub reason: String,
136}
137
138impl fmt::Display for HaysonCorruptLogError {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        write!(
141            f,
142            "log \"{}\" is corrupt at offset {}: {}",
143            self.path, self.offset, self.reason
144        )
145    }
146}
147
148impl std::error::Error for HaysonCorruptLogError {}
149
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub struct HaysonIndexMismatchError {
152    pub path: String,
153    /// The log the snapshot claimed to describe — on a partitioned table, the one partition whose
154    /// fingerprint disagreed, not the table as a whole.
155    pub log_path: String,
156    /// Byte the snapshot claims to describe `log_path` up to; equal to that log's size, because a
157    /// snapshot whose watermark differs is stale rather than mismatched.
158    pub watermark: u64,
159    pub expected_hash: String,
160    pub actual_hash: String,
161}
162
163impl fmt::Display for HaysonIndexMismatchError {
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        write!(
166            f,
167            "snapshot \"{}\" claims watermark and schema match the log, but its content fingerprint ({}) does not match the log's ({}) — left on disk for inspection, not silently discarded",
168            self.path, self.expected_hash, self.actual_hash
169        )
170    }
171}
172
173impl std::error::Error for HaysonIndexMismatchError {}
174
175/// The log file behind an open handle is no longer the file at its path: something outside this
176/// engine unlinked or replaced it — a `git checkout` of a data directory kept under version
177/// control, a restore from backup, a stray `mv`. An append would land in the orphaned inode and
178/// vanish with it, so the engine refuses the write; the database has to be reopened to attach to
179/// the file now at that path.
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct HaysonLogReplacedError {
182    pub path: String,
183}
184
185impl fmt::Display for HaysonLogReplacedError {
186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187        write!(
188            f,
189            "log file \"{}\" was replaced on disk while this database held it open — reopen the database before writing again",
190            self.path
191        )
192    }
193}
194
195impl std::error::Error for HaysonLogReplacedError {}
196
197/// A write carried an image whose bytes differ from the file already stored under the same content
198/// hash. The address is a 64-bit FNV-1a, so two different images can collide; the engine refuses the
199/// later one instead of letting it resolve to the earlier one's bytes. `physical_path` is the file
200/// name the two share.
201#[derive(Debug, Clone, PartialEq, Eq)]
202pub struct HaysonImageClashError {
203    pub physical_path: String,
204}
205
206impl fmt::Display for HaysonImageClashError {
207    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208        write!(
209            f,
210            "image bytes differ from the stored file \"{}\" that has the same content hash — refusing to replace it",
211            self.physical_path
212        )
213    }
214}
215
216impl std::error::Error for HaysonImageClashError {}
217
218/// A write carried an image larger than the `max_image_bytes` the database was opened with.
219#[derive(Debug, Clone, PartialEq, Eq)]
220pub struct HaysonImageTooLargeError {
221    pub bytes: u64,
222    pub max_bytes: u64,
223}
224
225impl fmt::Display for HaysonImageTooLargeError {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        write!(
228            f,
229            "image is {} bytes, over the {} byte limit this database was opened with",
230            self.bytes, self.max_bytes
231        )
232    }
233}
234
235impl std::error::Error for HaysonImageTooLargeError {}
236
237#[derive(Debug, Clone, PartialEq, Eq)]
238pub struct HaysonNotFoundError {
239    pub table: String,
240    pub key: String,
241}
242
243impl fmt::Display for HaysonNotFoundError {
244    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245        write!(f, "key \"{}\" is not in table \"{}\"", self.key, self.table)
246    }
247}
248
249impl std::error::Error for HaysonNotFoundError {}
250
251#[derive(Debug, Clone, PartialEq, Eq)]
252pub struct HaysonQueryParseError {
253    pub position: usize,
254    pub reason: String,
255}
256
257impl fmt::Display for HaysonQueryParseError {
258    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259        write!(
260            f,
261            "query text is invalid at position {}: {}",
262            self.position, self.reason
263        )
264    }
265}
266
267impl std::error::Error for HaysonQueryParseError {}
268
269/// A record cannot be placed in a partition file: its `partitionBy` value cannot name one, or the
270/// write would move an existing key out of the partition it already lives in.
271#[derive(Debug, Clone, PartialEq, Eq)]
272pub struct HaysonPartitionError {
273    pub table: String,
274    pub field: String,
275    pub reason: String,
276}
277
278impl HaysonPartitionError {
279    pub fn new(table: impl Into<String>, field: impl Into<String>, reason: impl Into<String>) -> Self {
280        HaysonPartitionError {
281            table: table.into(),
282            field: field.into(),
283            reason: reason.into(),
284        }
285    }
286}
287
288impl fmt::Display for HaysonPartitionError {
289    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290        write!(
291            f,
292            "partition field \"{}\" of table \"{}\": {}",
293            self.field, self.table, self.reason
294        )
295    }
296}
297
298impl std::error::Error for HaysonPartitionError {}
299
300/// A `cypher` field failed to seal/open, or the keyring passed to `open()` is itself unusable.
301/// `table` empty means the second case — the message drops the "of table" clause then.
302#[derive(Debug, Clone, PartialEq, Eq)]
303pub struct HaysonCipherError {
304    pub table: String,
305    pub field: String,
306    pub reason: String,
307    pub key_id: Option<String>,
308}
309
310impl HaysonCipherError {
311    pub fn new(table: impl Into<String>, field: impl Into<String>, reason: impl Into<String>) -> Self {
312        HaysonCipherError {
313            table: table.into(),
314            field: field.into(),
315            reason: reason.into(),
316            key_id: None,
317        }
318    }
319
320    /// Whether the value was written with a key the keyring does not hold (as opposed to one it
321    /// holds that does not open it).
322    pub fn is_missing_key(&self) -> bool {
323        self.reason.ends_with("which was not given")
324    }
325
326    pub fn with_key_id(mut self, key_id: impl Into<String>) -> Self {
327        self.key_id = Some(key_id.into());
328        self
329    }
330}
331
332impl fmt::Display for HaysonCipherError {
333    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334        if self.field == crate::crypto::STORAGE_FIELD {
335            write!(f, "encrypted database {}", self.reason)
336        } else if self.table.is_empty() {
337            write!(f, "cypher configuration {}", self.reason)
338        } else {
339            write!(
340                f,
341                "encrypted field \"{}\" of table \"{}\" {}",
342                self.field, self.table, self.reason
343            )
344        }
345    }
346}
347
348impl std::error::Error for HaysonCipherError {}