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::writer_task_terminated(WriterTaskRequestState::SideEffectsUnknown)
180 }
181 Self::WriterPoisoned => {
182 StorageError::writer_task_terminated(WriterTaskRequestState::NotStarted)
183 }
184 other => StorageError::driver(capability, operation, other),
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use khive_storage::CapacityUnavailablePhase;
193
194 #[test]
195 fn capacity_and_settlement_errors_keep_their_meaning_from_any_store() {
196 for capability in [StorageCapability::Entities, StorageCapability::Sql] {
197 let refused = SqliteError::CapacityFloor {
198 volume: "/volume".to_string(),
199 available_bytes: 99,
200 floor_bytes: 100,
201 required_headroom_bytes: 12,
202 }
203 .into_storage_error(capability, "write");
204 assert!(
205 matches!(
206 refused,
207 StorageError::CapacityFloor {
208 capability: StorageCapability::Sql,
209 available_bytes: 99,
210 floor_bytes: 100,
211 required_headroom_bytes: 12,
212 ..
213 }
214 ),
215 "{capability:?}: {refused:?}"
216 );
217
218 let unavailable = SqliteError::CapacityUnavailable {
219 phase: CapacityUnavailablePhase::Lock,
220 message: "lease timed out".to_string(),
221 }
222 .into_storage_error(capability, "write");
223 assert!(
224 matches!(
225 unavailable,
226 StorageError::CapacityUnavailable {
227 capability: StorageCapability::Sql,
228 phase: CapacityUnavailablePhase::Lock,
229 ..
230 }
231 ),
232 "{capability:?}: {unavailable:?}"
233 );
234
235 for unsettled in [
236 SqliteError::InheritedWriterTransaction,
237 SqliteError::WriterSettlementUnknown,
238 ] {
239 let mapped = unsettled.into_storage_error(capability, "write");
240 assert!(
241 matches!(
242 mapped,
243 StorageError::WriterTaskTerminated {
244 request_state: WriterTaskRequestState::SideEffectsUnknown,
245 ..
246 }
247 ),
248 "{capability:?}: {mapped:?}"
249 );
250 }
251
252 let refused = SqliteError::WriterPoisoned.into_storage_error(capability, "write");
253 assert!(
254 matches!(
255 refused,
256 StorageError::WriterTaskTerminated {
257 request_state: WriterTaskRequestState::NotStarted,
258 ..
259 }
260 ),
261 "{capability:?}: {refused:?}"
262 );
263 }
264 }
265}