1use std::time::Duration;
4
5use khive_storage::{StorageCapability, StorageError, WriterTaskRequestState};
6use thiserror::Error;
7
8pub const SQLITE_WAL_CAPACITY_REFUSED_STAGE: &str = "sqlite_wal_capacity_refused";
11pub const SQLITE_WAL_CAPACITY_UNAVAILABLE_STAGE: &str = "sqlite_wal_capacity_unavailable";
12
13#[derive(Debug, Error)]
15pub enum SqliteError {
16 #[error(transparent)]
18 RequestReadStopped(khive_storage::StorageError),
19
20 #[error("sqlite error: {0}")]
22 Rusqlite(#[from] rusqlite::Error),
23
24 #[error("invalid data: {0}")]
26 InvalidData(String),
27
28 #[error("pooled writer contains an inherited transaction; prior side effects are unknown")]
31 InheritedWriterTransaction,
32
33 #[error("writer transaction settlement is unknown; connection retired")]
35 WriterSettlementUnknown,
36
37 #[error("writer refused: an earlier write's settlement is unknown; this write did not start")]
41 WriterPoisoned,
42
43 #[error("invalid data: timed out after {timeout:?} waiting for sqlite writer connection")]
52 WriterPoolCheckoutTimeout {
53 timeout: Duration,
55 },
56
57 #[error(
60 "refusing sqlite write on {volume}: {available_bytes} bytes available, \
61 at or below the {floor_bytes}-byte free-space floor plus \
62 {required_headroom_bytes} bytes of operation headroom"
63 )]
64 CapacityFloor {
65 volume: String,
66 available_bytes: u64,
67 floor_bytes: u64,
68 required_headroom_bytes: u64,
69 },
70
71 #[error("sqlite capacity admission unavailable in {phase} phase: {message}")]
74 CapacityUnavailable {
75 phase: khive_storage::CapacityUnavailablePhase,
76 message: String,
77 },
78
79 #[error(
83 "volume lease re-entry: this thread already holds the lease for this volume \
84 (held at {holder_site}, requested again at {requester_site})"
85 )]
86 VolumeLeaseReentry {
87 holder_site: String,
88 requester_site: String,
89 },
90
91 #[error("invalid WAL ceiling {bytes} bytes: exceeds supported SQLite file offsets")]
94 WalCeilingOffsetOverflow { bytes: u64 },
95
96 #[error("invalid WAL ceiling {bytes} bytes: {backend_kind} does not support WAL enforcement")]
98 WalCeilingUnsupported {
99 bytes: u64,
100 backend_kind: &'static str,
101 },
102
103 #[error(
105 "invalid WAL ceiling {bytes} bytes: page size {page_size} requires at least {minimum_bytes} bytes for one WAL frame"
106 )]
107 WalCeilingBelowMinimum {
108 bytes: u64,
109 page_size: u64,
110 minimum_bytes: u64,
111 },
112
113 #[error(
115 "{stage}: WAL ceiling {bytes} bytes cannot be enforced: missing {capability}",
116 stage = SQLITE_WAL_CAPACITY_UNAVAILABLE_STAGE
117 )]
118 WalCapacityUnavailable {
119 bytes: u64,
120 capability: &'static str,
121 },
122
123 #[error("invalid config: {0}")]
128 InvalidConfig(String),
129
130 #[error("io error: {0}")]
132 Io(#[from] std::io::Error),
133
134 #[error("migration v{version} failed: {error}")]
136 Migration {
137 version: u32,
139 error: String,
141 },
142}
143
144impl SqliteError {
145 pub fn wal_capacity_stage(&self) -> Option<&'static str> {
147 match self {
148 Self::WalCapacityUnavailable { .. } => Some(SQLITE_WAL_CAPACITY_UNAVAILABLE_STAGE),
149 _ => None,
150 }
151 }
152
153 pub(crate) fn into_storage_error(
156 self,
157 capability: StorageCapability,
158 operation: &'static str,
159 ) -> StorageError {
160 match self {
161 Self::CapacityFloor {
162 volume,
163 available_bytes,
164 floor_bytes,
165 required_headroom_bytes,
166 } => StorageError::CapacityFloor {
167 capability: StorageCapability::Sql,
168 volume,
169 available_bytes,
170 floor_bytes,
171 required_headroom_bytes,
172 },
173 Self::CapacityUnavailable { phase, message } => StorageError::CapacityUnavailable {
174 capability: StorageCapability::Sql,
175 phase,
176 message,
177 },
178 Self::InheritedWriterTransaction | Self::WriterSettlementUnknown => {
179 StorageError::WriterTaskTerminated {
180 request_state: WriterTaskRequestState::SideEffectsUnknown,
181 }
182 }
183 Self::WriterPoisoned => StorageError::WriterTaskTerminated {
184 request_state: WriterTaskRequestState::NotStarted,
185 },
186 other => StorageError::driver(capability, operation, other),
187 }
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use khive_storage::CapacityUnavailablePhase;
195
196 #[test]
197 fn capacity_and_settlement_errors_keep_their_meaning_from_any_store() {
198 for capability in [StorageCapability::Entities, StorageCapability::Sql] {
199 let refused = SqliteError::CapacityFloor {
200 volume: "/volume".to_string(),
201 available_bytes: 99,
202 floor_bytes: 100,
203 required_headroom_bytes: 12,
204 }
205 .into_storage_error(capability, "write");
206 assert!(
207 matches!(
208 refused,
209 StorageError::CapacityFloor {
210 capability: StorageCapability::Sql,
211 available_bytes: 99,
212 floor_bytes: 100,
213 required_headroom_bytes: 12,
214 ..
215 }
216 ),
217 "{capability:?}: {refused:?}"
218 );
219
220 let unavailable = SqliteError::CapacityUnavailable {
221 phase: CapacityUnavailablePhase::Lock,
222 message: "lease timed out".to_string(),
223 }
224 .into_storage_error(capability, "write");
225 assert!(
226 matches!(
227 unavailable,
228 StorageError::CapacityUnavailable {
229 capability: StorageCapability::Sql,
230 phase: CapacityUnavailablePhase::Lock,
231 ..
232 }
233 ),
234 "{capability:?}: {unavailable:?}"
235 );
236
237 for unsettled in [
238 SqliteError::InheritedWriterTransaction,
239 SqliteError::WriterSettlementUnknown,
240 ] {
241 let mapped = unsettled.into_storage_error(capability, "write");
242 assert!(
243 matches!(
244 mapped,
245 StorageError::WriterTaskTerminated {
246 request_state: WriterTaskRequestState::SideEffectsUnknown,
247 }
248 ),
249 "{capability:?}: {mapped:?}"
250 );
251 }
252
253 let refused = SqliteError::WriterPoisoned.into_storage_error(capability, "write");
254 assert!(
255 matches!(
256 refused,
257 StorageError::WriterTaskTerminated {
258 request_state: WriterTaskRequestState::NotStarted,
259 }
260 ),
261 "{capability:?}: {refused:?}"
262 );
263 }
264 }
265}