1mod from_crdt;
4mod from_sql;
5
6#[derive(Debug, thiserror::Error)]
8pub enum LiteError {
9 #[error("storage error: {detail}")]
10 Storage { detail: String },
11
12 #[error("storage backend returned poison lock")]
13 LockPoisoned,
14
15 #[error("async task join failed: {detail}")]
16 JoinError { detail: String },
17
18 #[error("serialization error: {detail}")]
19 Serialization { detail: String },
20
21 #[error("namespace {ns} not recognized")]
22 InvalidNamespace { ns: u8 },
23
24 #[error("bad request: {detail}")]
25 BadRequest { detail: String },
26
27 #[error("{engine} collection '{collection}' does not exist")]
30 CollectionNotFound {
31 engine: &'static str,
32 collection: String,
33 },
34
35 #[error("duplicate key value violates unique constraint on '{collection}': {detail}")]
38 UniqueViolation { collection: String, detail: String },
39
40 #[error("null value in column '{column}' of '{collection}' violates not-null constraint")]
43 NotNullViolation { collection: String, column: String },
44
45 #[error("sync error: {detail}")]
46 Sync { detail: String },
47
48 #[error("query error: {0}")]
49 Query(String),
50
51 #[error("Arrow type conversion: expected {expected}, got {got}")]
52 ArrowTypeConversion { expected: String, got: String },
53
54 #[error("backpressure: {detail}")]
55 Backpressure { detail: String },
56
57 #[error("unsupported: {detail}")]
59 Unsupported { detail: String },
60
61 #[error("OPFS worker bridge failed: {detail}")]
67 WorkerFailed { detail: String },
68
69 #[error("encryption error: {detail}")]
71 Encryption { detail: String },
72
73 #[error("storage corrupted: {detail}")]
80 Corrupted { detail: String },
81
82 #[error("full-text index update failed for {collection}: {detail}")]
89 FtsIndex { collection: String, detail: String },
90
91 #[error(
95 "collection '{collection}' has no full-text index for field '{field}': \
96 write a document with a string value in '{field}', or search with an \
97 empty field to cover every string field"
98 )]
99 TextIndexMissing { collection: String, field: String },
100
101 #[error("invalid full-text query on {collection}: {detail}")]
104 FtsQueryInvalid { collection: String, detail: String },
105
106 #[error("full-text search failed for {collection}: {detail}")]
109 FtsSearch { collection: String, detail: String },
110
111 #[error("{fault} on {collection}")]
115 CounterFault {
116 collection: String,
117 fault: nodedb_physical::kv_atomic::CounterFault,
118 },
119
120 #[error("{}", type_mismatch_message(.collection, .detail))]
125 TypeMismatch { collection: String, detail: String },
126
127 #[error("{detail}")]
131 DataException { detail: String },
132
133 #[error("{detail}")]
136 NumericValueOutOfRange { detail: String },
137
138 #[error("{object} does not exist")]
141 UndefinedObject { object: String },
142
143 #[error("{detail}")]
146 ObjectNotReady { object: String, detail: String },
147
148 #[error("constraint violation: {constraint} on '{collection}': {detail}")]
151 ConstraintViolation {
152 collection: String,
153 constraint: String,
154 detail: String,
155 },
156
157 #[error("refused, retry: {detail}")]
161 RetryableRefusal { detail: String },
162
163 #[error("authorization denied on {resource}")]
166 AuthorizationDenied { resource: String },
167
168 #[error("pre-validation rejected: {reason}")]
171 PrevalidationRejected { reason: String },
172}
173
174fn type_mismatch_message(collection: &str, detail: &str) -> String {
176 if collection.is_empty() {
177 detail.to_string()
178 } else {
179 format!("type mismatch on {collection}: {detail}")
180 }
181}
182
183impl LiteError {
184 pub fn collection_not_found(engine: &'static str, collection: impl Into<String>) -> Self {
187 Self::CollectionNotFound {
188 engine,
189 collection: collection.into(),
190 }
191 }
192
193 pub(crate) fn crdt(op: &str, e: nodedb_crdt::CrdtError) -> Self {
196 Self::from(e).within(op)
197 }
198
199 pub(crate) fn within(self, op: &str) -> Self {
202 match self {
203 Self::Storage { detail } => Self::Storage {
204 detail: format!("{op}: {detail}"),
205 },
206 other => other,
207 }
208 }
209}
210
211impl From<nodedb_vector::error::VectorError> for LiteError {
216 fn from(e: nodedb_vector::error::VectorError) -> Self {
217 use nodedb_vector::error::VectorError as Ve;
218 let detail = e.to_string();
219 match e {
220 Ve::DimensionMismatch { .. } | Ve::InvalidInput { .. } => {
221 Self::DataException { detail }
222 }
223 Ve::BudgetExhausted(_) => Self::Backpressure { detail },
224 Ve::SegmentIo(_) | Ve::InvalidFilterBitmap { .. } => Self::Storage { detail },
225 _ => Self::Corrupted { detail },
228 }
229 }
230}
231
232pub(crate) fn is_corruption(e: &LiteError) -> bool {
235 matches!(e, LiteError::Corrupted { .. })
236}
237
238impl From<nodedb_query::EvalError> for LiteError {
247 fn from(e: nodedb_query::EvalError) -> Self {
248 use nodedb_query::EvalError as Ee;
249 match e {
250 e @ Ee::NumericOverflow { .. } => Self::NumericValueOutOfRange {
251 detail: e.to_string(),
252 },
253 e @ (Ee::DivisionByZero
254 | Ee::UnknownFunction { .. }
255 | Ee::VectorDimensionMismatch { .. }
256 | Ee::ArgumentType { .. }
257 | Ee::InvalidJsonPath { .. }) => Self::Query(e.to_string()),
258 }
259 }
260}
261
262impl From<nodedb_types::columnar::SchemaError> for LiteError {
263 fn from(e: nodedb_types::columnar::SchemaError) -> Self {
264 Self::BadRequest {
265 detail: e.to_string(),
266 }
267 }
268}
269
270impl From<LiteError> for nodedb_types::error::NodeDbError {
271 fn from(e: LiteError) -> Self {
272 use nodedb_types::error::{ErrorCode, NodeDbError};
273 match e {
274 LiteError::CounterFault { collection, fault } => {
276 NodeDbError::kv_counter_fault(collection, fault, fault.is_out_of_range())
277 }
278 LiteError::TypeMismatch { collection, detail } if collection.is_empty() => {
279 NodeDbError::from_wire(ErrorCode::TYPE_MISMATCH, detail)
280 }
281 LiteError::TypeMismatch { collection, detail } => {
282 NodeDbError::type_mismatch(collection, detail)
283 }
284 LiteError::DataException { detail } => NodeDbError::data_exception(detail),
285 LiteError::NumericValueOutOfRange { detail } => {
286 NodeDbError::numeric_value_out_of_range(detail)
287 }
288 LiteError::UndefinedObject { object } => NodeDbError::undefined_object(object),
289 LiteError::ObjectNotReady { object, detail } => {
290 NodeDbError::object_not_ready(object, detail)
291 }
292 LiteError::CollectionNotFound { collection, .. } => {
293 NodeDbError::collection_not_found(collection)
294 }
295 LiteError::UniqueViolation { collection, detail } => {
296 NodeDbError::constraint_violation(collection, "unique", detail)
297 }
298 LiteError::NotNullViolation { collection, column } => {
299 NodeDbError::constraint_violation(
300 collection,
301 "not_null",
302 format!("null value in column '{column}'"),
303 )
304 }
305 LiteError::ConstraintViolation {
306 collection,
307 constraint,
308 detail,
309 } => NodeDbError::constraint_violation(collection, constraint, detail),
310 e @ LiteError::RetryableRefusal { .. } => {
313 NodeDbError::from_wire(ErrorCode::WRITE_CONFLICT, e.to_string())
314 }
315 LiteError::AuthorizationDenied { resource } => {
316 NodeDbError::authorization_denied(resource)
317 }
318 LiteError::PrevalidationRejected { reason } => {
319 NodeDbError::prevalidation_rejected("crdt", reason)
320 }
321 LiteError::Query(detail) => NodeDbError::plan_error(detail),
322 LiteError::BadRequest { detail } => NodeDbError::bad_request(detail),
323 LiteError::Unsupported { detail } => {
326 NodeDbError::from_wire(ErrorCode::SQL_NOT_ENABLED, detail)
327 }
328 e @ (LiteError::TextIndexMissing { .. } | LiteError::FtsQueryInvalid { .. }) => {
329 NodeDbError::bad_request(e)
330 }
331 e if is_corruption(&e) => NodeDbError::segment_corrupted(e.to_string()),
332 e => NodeDbError::storage(e),
333 }
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340
341 #[test]
342 fn lite_error_display() {
343 let e = LiteError::Storage {
344 detail: "disk full".into(),
345 };
346 assert!(e.to_string().contains("disk full"));
347 }
348
349 #[test]
350 fn lite_error_converts_to_nodedb_error() {
351 let e = LiteError::Storage {
352 detail: "test".into(),
353 };
354 let ndb: nodedb_types::error::NodeDbError = e.into();
355 assert!(ndb.to_string().contains("test"));
356 }
357
358 #[test]
359 fn a_unique_violation_is_a_constraint_violation() {
360 let e = LiteError::UniqueViolation {
361 collection: "users".into(),
362 detail: "key ($.email)=(a@x) already exists".into(),
363 };
364 let ndb: nodedb_types::error::NodeDbError = e.into();
365 assert!(ndb.is_constraint_violation(), "{ndb}");
366 assert!(ndb.to_string().contains("unique"), "{ndb}");
367 }
368
369 #[test]
370 fn lite_error_encryption_display_and_convert() {
371 let e = LiteError::Encryption {
372 detail: "argon2 key derivation failed".into(),
373 };
374 let rendered = e.to_string();
375 assert!(rendered.contains("encryption error"));
376 assert!(rendered.contains("argon2 key derivation failed"));
377
378 let ndb: nodedb_types::error::NodeDbError = e.into();
379 assert!(ndb.to_string().contains("argon2 key derivation failed"));
380 }
381
382 #[test]
383 fn a_missing_collection_is_collection_not_found() {
384 let e = LiteError::collection_not_found("strict", "users");
385 assert_eq!(e.to_string(), "strict collection 'users' does not exist");
386
387 let ndb: nodedb_types::error::NodeDbError = e.into();
388 assert_eq!(
389 ndb.code(),
390 nodedb_types::error::ErrorCode::COLLECTION_NOT_FOUND,
391 "{ndb}"
392 );
393 }
394
395 #[test]
396 fn a_not_null_violation_is_a_constraint_violation() {
397 let e = LiteError::NotNullViolation {
398 collection: "users".into(),
399 column: "email".into(),
400 };
401 let ndb: nodedb_types::error::NodeDbError = e.into();
402 assert!(ndb.is_constraint_violation(), "{ndb}");
403 }
404
405 #[test]
406 fn a_bad_request_keeps_its_class() {
407 let e = LiteError::BadRequest {
408 detail: "invalid version vector JSON".into(),
409 };
410 let ndb: nodedb_types::error::NodeDbError = e.into();
411 assert_eq!(ndb.code(), nodedb_types::error::ErrorCode::BAD_REQUEST);
412 }
413
414 #[test]
415 fn a_type_mismatch_without_a_collection_renders_its_detail() {
416 let e = LiteError::TypeMismatch {
417 collection: String::new(),
418 detail: "column 'v': expected a number".into(),
419 };
420 assert_eq!(e.to_string(), "column 'v': expected a number");
421 let ndb: nodedb_types::error::NodeDbError = e.into();
422 assert_eq!(ndb.code(), nodedb_types::error::ErrorCode::TYPE_MISMATCH);
423 }
424
425 #[test]
426 fn lite_error_backpressure_display() {
427 let e = LiteError::Backpressure {
428 detail: "outbound queue full".into(),
429 };
430 assert!(e.to_string().contains("backpressure"));
431 assert!(e.to_string().contains("outbound queue full"));
432 }
433}