Skip to main content

khive_db/
error.rs

1//! Error types for the SQLite storage layer.
2
3use std::time::Duration;
4
5use khive_storage::error::{SqliteWriteFailure, SqliteWriteStage};
6use khive_storage::{StorageCapability, StorageError, WriterTaskRequestState};
7use thiserror::Error;
8
9// Inspect only owned error shapes. User callbacks may supply cyclic or effectful
10// source() implementations; attaching evidence must not invoke those again.
11pub(crate) fn native_write_failure(
12    error: &(dyn std::error::Error + 'static),
13    stage: SqliteWriteStage,
14) -> Option<SqliteWriteFailure> {
15    if let Some(error) = error.downcast_ref::<StorageError>() {
16        return match error {
17            StorageError::SqliteWrite { failure, .. } => Some(*failure),
18            StorageError::Driver { source, .. } => native_write_failure(source.as_ref(), stage),
19            StorageError::WriterTaskRequestFailed { source, .. } => {
20                native_write_failure(source.as_ref(), stage)
21            }
22            _ => None,
23        };
24    }
25    if let Some(error) = error.downcast_ref::<SqliteError>() {
26        return match error {
27            SqliteError::Write { stage, source } => native_write_failure(source, *stage),
28            SqliteError::WriteSettlementUnknown { failure } => Some(*failure),
29            SqliteError::Rusqlite(source) => native_write_failure(source, stage),
30            _ => None,
31        };
32    }
33    if let Some(
34        rusqlite::Error::SqliteFailure(code, _)
35        | rusqlite::Error::SqlInputError { error: code, .. },
36    ) = error.downcast_ref::<rusqlite::Error>()
37    {
38        return Some(SqliteWriteFailure {
39            stage,
40            primary_code: code.extended_code & 0xff,
41            extended_code: code.extended_code,
42            settlement_unknown: false,
43        });
44    }
45    None
46}
47
48pub(crate) fn with_write_stage(error: StorageError, stage: SqliteWriteStage) -> StorageError {
49    match native_write_failure(&error, stage) {
50        Some(failure) => error.with_sqlite_write_failure(failure),
51        None => error,
52    }
53}
54
55pub(crate) fn statement_failure(error: StorageError) -> StorageError {
56    with_write_stage(error, SqliteWriteStage::Statement)
57}
58
59pub(crate) fn unknown_write_settlement(mut error: StorageError) -> StorageError {
60    if let StorageError::SqliteWrite { failure, .. } = &mut error {
61        failure.settlement_unknown = true;
62    }
63    error
64}
65
66/// Stable ADR-194 capacity stages. The refusal stage is reserved for the WAL
67/// I/O limiter; this configuration-only slice emits only unavailable.
68pub const SQLITE_WAL_CAPACITY_REFUSED_STAGE: &str = "sqlite_wal_capacity_refused";
69pub const SQLITE_WAL_CAPACITY_UNAVAILABLE_STAGE: &str = "sqlite_wal_capacity_unavailable";
70
71/// Errors produced by the SQLite storage backend.
72#[derive(Debug, Error)]
73pub enum SqliteError {
74    #[error("sqlite error: {source}")]
75    Write {
76        stage: SqliteWriteStage,
77        #[source]
78        source: rusqlite::Error,
79    },
80    /// A request-scoped read or store acquisition stopped, or read cleanup failed.
81    #[error(transparent)]
82    RequestReadStopped(khive_storage::StorageError),
83
84    /// Underlying rusqlite driver error.
85    #[error("sqlite error: {0}")]
86    Rusqlite(#[from] rusqlite::Error),
87
88    /// Data invariant violation (corrupt row, unexpected schema state).
89    #[error("invalid data: {0}")]
90    InvalidData(String),
91
92    /// A pooled connection contained a transaction from an earlier owner.
93    /// Its prior side effects cannot be attributed to the new request.
94    #[error("pooled writer contains an inherited transaction; prior side effects are unknown")]
95    InheritedWriterTransaction,
96
97    /// The writer could not prove transaction settlement before retirement.
98    #[error("writer transaction settlement is unknown; connection retired")]
99    WriterSettlementUnknown,
100
101    /// Source-free native evidence retained when cleanup cannot prove settlement.
102    #[error("writer transaction settlement is unknown; connection retired")]
103    WriteSettlementUnknown { failure: SqliteWriteFailure },
104
105    /// An earlier write on this database could not prove its settlement, so
106    /// every later write is refused before it starts. Only the write whose
107    /// settlement failed reports an unknown outcome; this one never ran.
108    #[error("writer refused: an earlier write's settlement is unknown; this write did not start")]
109    WriterPoisoned,
110
111    /// The process-local writer mutex was not acquired within the pool's
112    /// configured finite checkout deadline. This stage happens before SQLite
113    /// executes, so callers must not conflate it with SQLite busy/locked or
114    /// checkpoint starvation.
115    ///
116    /// The display text intentionally retains the historical `InvalidData`
117    /// prefix for compatibility while the variant supplies stable structural
118    /// classification (ADR-135 F6).
119    #[error("invalid data: timed out after {timeout:?} waiting for sqlite writer connection")]
120    WriterPoolCheckoutTimeout {
121        /// Pool checkout deadline that elapsed.
122        timeout: Duration,
123    },
124
125    /// A file-backed writer was refused before SQLite began the operation
126    /// because the volume's free space had reached its configured reserve.
127    #[error(
128        "refusing sqlite write on {volume}: {available_bytes} bytes available, \
129         at or below the {floor_bytes}-byte free-space floor plus \
130         {required_headroom_bytes} bytes of operation headroom"
131    )]
132    CapacityFloor {
133        volume: String,
134        available_bytes: u64,
135        floor_bytes: u64,
136        required_headroom_bytes: u64,
137    },
138
139    /// A new logical write could not resolve its volume, acquire its lease,
140    /// or sample available space.
141    #[error("sqlite capacity admission unavailable in {phase} phase: {message}")]
142    CapacityUnavailable {
143        phase: khive_storage::CapacityUnavailablePhase,
144        message: String,
145    },
146
147    /// The thread asking for a volume's write lease already holds it, so
148    /// waiting could never succeed. This is a nested write, not lock
149    /// contention, and it is refused at once instead of at the deadline.
150    #[error(
151        "volume lease re-entry: this thread already holds the lease for this volume \
152         (held at {holder_site}, requested again at {requester_site})"
153    )]
154    VolumeLeaseReentry {
155        holder_site: String,
156        requester_site: String,
157    },
158
159    /// A configured WAL ceiling cannot be represented by SQLite's signed
160    /// file-offset arithmetic.
161    #[error("invalid WAL ceiling {bytes} bytes: exceeds supported SQLite file offsets")]
162    WalCeilingOffsetOverflow { bytes: u64 },
163
164    /// A WAL ceiling was enabled for a backend that cannot produce a WAL.
165    #[error("invalid WAL ceiling {bytes} bytes: {backend_kind} does not support WAL enforcement")]
166    WalCeilingUnsupported {
167        bytes: u64,
168        backend_kind: &'static str,
169    },
170
171    /// One committed WAL frame cannot fit, even immediately after reset.
172    #[error(
173        "invalid WAL ceiling {bytes} bytes: page size {page_size} requires at least {minimum_bytes} bytes for one WAL frame"
174    )]
175    WalCeilingBelowMinimum {
176        bytes: u64,
177        page_size: u64,
178        minimum_bytes: u64,
179    },
180
181    /// A valid enabled policy cannot run until its WAL I/O limiter exists.
182    #[error(
183        "{stage}: WAL ceiling {bytes} bytes cannot be enforced: missing {capability}",
184        stage = SQLITE_WAL_CAPACITY_UNAVAILABLE_STAGE
185    )]
186    WalCapacityUnavailable {
187        bytes: u64,
188        capability: &'static str,
189    },
190
191    /// A `PoolConfig` value violated a validated invariant at configuration
192    /// load time (e.g. ADR-131 Decision 2's `write_admission_deadline_ms`
193    /// range). Fires before any connection is opened, and is never silently
194    /// clamped into range.
195    #[error("invalid config: {0}")]
196    InvalidConfig(String),
197
198    /// Filesystem I/O error.
199    #[error("io error: {0}")]
200    Io(#[from] std::io::Error),
201
202    /// A versioned migration failed to apply.
203    #[error("migration v{version} failed: {error}")]
204    Migration {
205        /// The migration version number that failed.
206        version: u32,
207        /// Human-readable description of the failure.
208        error: String,
209    },
210}
211
212impl SqliteError {
213    /// Attach a known write boundary while retaining the original native driver error.
214    pub fn write(error: rusqlite::Error, stage: SqliteWriteStage) -> Self {
215        if matches!(
216            error,
217            rusqlite::Error::SqliteFailure(..) | rusqlite::Error::SqlInputError { .. }
218        ) {
219            Self::Write {
220                stage,
221                source: error,
222            }
223        } else {
224            Self::Rusqlite(error)
225        }
226    }
227
228    pub(crate) fn settlement_with_cause(
229        error: &(dyn std::error::Error + 'static),
230        stage: SqliteWriteStage,
231    ) -> Self {
232        match native_write_failure(error, stage) {
233            Some(mut failure) => {
234                failure.settlement_unknown = true;
235                Self::WriteSettlementUnknown { failure }
236            }
237            None => Self::WriterSettlementUnknown,
238        }
239    }
240
241    pub fn write_failure(&self) -> Option<SqliteWriteFailure> {
242        match self {
243            Self::Write { stage, source } => native_write_failure(source, *stage),
244            Self::WriteSettlementUnknown { failure } => Some(*failure),
245            _ => None,
246        }
247    }
248    /// Stable structured stage for an ADR-194 WAL-capacity failure.
249    pub fn wal_capacity_stage(&self) -> Option<&'static str> {
250        match self {
251            Self::WalCapacityUnavailable { .. } => Some(SQLITE_WAL_CAPACITY_UNAVAILABLE_STAGE),
252            _ => None,
253        }
254    }
255
256    /// Capacity admission is a property of the SQLite file, so its refusals
257    /// carry `StorageCapability::Sql` whichever store requested the write.
258    /// Concrete backend callers use the same mapping as trait implementations:
259    /// capacity and writer-settlement failures retain their storage taxonomy,
260    /// and other failures remain typed sources of `StorageError::Driver`.
261    pub fn into_storage_error(
262        self,
263        capability: StorageCapability,
264        operation: &'static str,
265    ) -> StorageError {
266        match self {
267            error @ Self::Write { stage, .. } => {
268                with_write_stage(StorageError::driver(capability, operation, error), stage)
269            }
270            Self::CapacityFloor {
271                volume,
272                available_bytes,
273                floor_bytes,
274                required_headroom_bytes,
275            } => StorageError::CapacityFloor {
276                capability: StorageCapability::Sql,
277                volume,
278                available_bytes,
279                floor_bytes,
280                required_headroom_bytes,
281            },
282            Self::CapacityUnavailable { phase, message } => StorageError::CapacityUnavailable {
283                capability: StorageCapability::Sql,
284                phase,
285                message,
286            },
287            Self::WriteSettlementUnknown { failure } => {
288                StorageError::writer_task_terminated(WriterTaskRequestState::SideEffectsUnknown)
289                    .with_sqlite_write_failure(failure)
290            }
291            Self::InheritedWriterTransaction | Self::WriterSettlementUnknown => {
292                StorageError::writer_task_terminated(WriterTaskRequestState::SideEffectsUnknown)
293            }
294            Self::WriterPoisoned => {
295                StorageError::writer_task_terminated(WriterTaskRequestState::NotStarted)
296            }
297            other => StorageError::driver(capability, operation, other),
298        }
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305    use khive_storage::CapacityUnavailablePhase;
306
307    #[test]
308    fn capacity_and_settlement_errors_keep_their_meaning_from_any_store() {
309        for capability in [StorageCapability::Entities, StorageCapability::Sql] {
310            let refused = SqliteError::CapacityFloor {
311                volume: "/volume".to_string(),
312                available_bytes: 99,
313                floor_bytes: 100,
314                required_headroom_bytes: 12,
315            }
316            .into_storage_error(capability, "write");
317            assert!(
318                matches!(
319                    refused,
320                    StorageError::CapacityFloor {
321                        capability: StorageCapability::Sql,
322                        available_bytes: 99,
323                        floor_bytes: 100,
324                        required_headroom_bytes: 12,
325                        ..
326                    }
327                ),
328                "{capability:?}: {refused:?}"
329            );
330
331            let unavailable = SqliteError::CapacityUnavailable {
332                phase: CapacityUnavailablePhase::LockContended,
333                message: "lease timed out".to_string(),
334            }
335            .into_storage_error(capability, "write");
336            assert!(
337                matches!(
338                    unavailable,
339                    StorageError::CapacityUnavailable {
340                        capability: StorageCapability::Sql,
341                        phase: CapacityUnavailablePhase::LockContended,
342                        ..
343                    }
344                ),
345                "{capability:?}: {unavailable:?}"
346            );
347
348            for unsettled in [
349                SqliteError::InheritedWriterTransaction,
350                SqliteError::WriterSettlementUnknown,
351            ] {
352                let mapped = unsettled.into_storage_error(capability, "write");
353                assert!(
354                    matches!(
355                        mapped,
356                        StorageError::WriterTaskTerminated {
357                            request_state: WriterTaskRequestState::SideEffectsUnknown,
358                            ..
359                        }
360                    ),
361                    "{capability:?}: {mapped:?}"
362                );
363            }
364
365            let refused = SqliteError::WriterPoisoned.into_storage_error(capability, "write");
366            assert!(
367                matches!(
368                    refused,
369                    StorageError::WriterTaskTerminated {
370                        request_state: WriterTaskRequestState::NotStarted,
371                        ..
372                    }
373                ),
374                "{capability:?}: {refused:?}"
375            );
376        }
377    }
378}