iqdb_types/error.rs
1//! The iqdb domain error.
2//!
3//! [`IqdbError`] is the single error type for the iqdb vector-database spine.
4//! Each variant names one specific failure so a caller can react to the exact
5//! cause rather than parsing a message. It implements
6//! [`error_forge::ForgeError`], so it slots into the portfolio error stack
7//! (kinds, captions, the central error hook) the same way every other domain
8//! error does.
9
10use core::fmt;
11
12use error_forge::ForgeError;
13
14/// An error from an iqdb vector-database operation.
15///
16/// Each variant identifies one specific failure. The enum is
17/// `#[non_exhaustive]`: future releases may add variants without it being a
18/// breaking change, so a `match` on it must include a wildcard arm.
19///
20/// # Examples
21///
22/// ```
23/// use iqdb_types::IqdbError;
24///
25/// let err = IqdbError::DimensionMismatch { expected: 3, found: 2 };
26/// assert_eq!(
27/// err.to_string(),
28/// "vector dimension mismatch: expected 3, found 2",
29/// );
30///
31/// let cfg = IqdbError::InvalidConfig { reason: "dim must be greater than zero" };
32/// assert_eq!(
33/// cfg.to_string(),
34/// "invalid configuration: dim must be greater than zero",
35/// );
36/// ```
37#[non_exhaustive]
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum IqdbError {
40 /// A vector did not have the dimensionality the operation required —
41 /// `expected` is the index's dimension, `found` is what was supplied.
42 DimensionMismatch {
43 /// The dimensionality the operation required.
44 expected: usize,
45 /// The dimensionality that was actually supplied.
46 found: usize,
47 },
48 /// A vector was not valid for the operation (for example, empty when a
49 /// non-empty vector was required).
50 InvalidVector,
51 /// A configuration value could not describe a working index or query.
52 /// `reason` is a short static description of which configuration check
53 /// failed, suitable for inclusion in operator-facing logs.
54 InvalidConfig {
55 /// Short static description of which configuration check failed.
56 reason: &'static str,
57 },
58 /// The requested id or record does not exist.
59 NotFound,
60 /// An insert collided with an id that is already present.
61 Duplicate,
62 /// The distance metric was not valid for the operation or the vectors.
63 InvalidMetric,
64 /// A filter expression was malformed or could not be evaluated.
65 InvalidFilter,
66 /// An incoming write exceeded a configured resource limit. `kind` names
67 /// which cap was hit (one of `"id_bytes"`, `"metadata_keys"`,
68 /// `"metadata_key_bytes"`, `"metadata_value_string_bytes"`,
69 /// `"total_vectors"`); `max` is the cap; `found` is what the caller
70 /// supplied. Surfaces from the `Database` write boundary; the
71 /// type-level constructors in this crate never produce it.
72 ResourceLimitExceeded {
73 /// Short, stable identifier for the cap that was exceeded.
74 kind: &'static str,
75 /// The configured maximum the cap allowed.
76 max: usize,
77 /// The value the caller actually supplied.
78 found: usize,
79 },
80}
81
82impl fmt::Display for IqdbError {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 match self {
85 Self::DimensionMismatch { expected, found } => {
86 write!(
87 f,
88 "vector dimension mismatch: expected {expected}, found {found}"
89 )
90 }
91 Self::InvalidVector => f.write_str("invalid vector"),
92 Self::InvalidConfig { reason } => write!(f, "invalid configuration: {reason}"),
93 Self::NotFound => f.write_str("not found"),
94 Self::Duplicate => f.write_str("duplicate id"),
95 Self::InvalidMetric => f.write_str("invalid distance metric"),
96 Self::InvalidFilter => f.write_str("invalid filter"),
97 Self::ResourceLimitExceeded { kind, max, found } => {
98 write!(f, "resource limit exceeded: {kind} max={max} found={found}")
99 }
100 }
101 }
102}
103
104impl std::error::Error for IqdbError {}
105
106impl ForgeError for IqdbError {
107 #[inline]
108 fn kind(&self) -> &'static str {
109 match self {
110 Self::DimensionMismatch { .. } => "DimensionMismatch",
111 Self::InvalidVector => "InvalidVector",
112 Self::InvalidConfig { .. } => "InvalidConfig",
113 Self::NotFound => "NotFound",
114 Self::Duplicate => "Duplicate",
115 Self::InvalidMetric => "InvalidMetric",
116 Self::InvalidFilter => "InvalidFilter",
117 Self::ResourceLimitExceeded { .. } => "ResourceLimitExceeded",
118 }
119 }
120
121 #[inline]
122 fn caption(&self) -> &'static str {
123 match self {
124 Self::DimensionMismatch { .. } => "vector dimension does not match the index",
125 Self::InvalidVector => "vector is empty or contains non-finite components",
126 Self::InvalidConfig { .. } => "configuration is not valid for the requested operation",
127 Self::NotFound => "requested id is not present in the index",
128 Self::Duplicate => "id already exists in the index",
129 Self::InvalidMetric => "distance metric does not match the index or vectors",
130 Self::InvalidFilter => "filter expression is malformed or cannot be evaluated",
131 Self::ResourceLimitExceeded { .. } => "an input exceeded a configured resource limit",
132 }
133 }
134}
135
136/// A specialized [`Result`](core::result::Result) whose error is [`IqdbError`].
137///
138/// # Examples
139///
140/// ```
141/// use iqdb_types::{IqdbError, Result};
142///
143/// fn require_non_empty(dim: usize) -> Result<()> {
144/// if dim == 0 {
145/// return Err(IqdbError::InvalidConfig { reason: "dim must be greater than zero" });
146/// }
147/// Ok(())
148/// }
149///
150/// assert!(require_non_empty(0).is_err());
151/// assert!(require_non_empty(3).is_ok());
152/// ```
153pub type Result<T> = core::result::Result<T, IqdbError>;