ferrule_sql/error.rs
1use thiserror::Error;
2
3/// Errors originating in the `ferrule-sql` driver and write-path layer.
4///
5/// Every backend method, URL parse, connect dispatch, transaction
6/// helper, and copy routine returns this type. Variant names and tuple
7/// shapes are load-bearing across the workspace (the CLI pattern-matches
8/// [`SqlError::QueryFailed`] in several hot paths), so preserve them when
9/// editing.
10///
11/// `RegistryError` is registry/CLI-level rather than driver-level; it
12/// rides along here as a deliberate minimal-diff choice during the
13/// `ferrule-core` -> `ferrule-sql` extraction and is a candidate for a
14/// later relocation to a core-side error type.
15#[derive(Error, Debug)]
16pub enum SqlError {
17 #[error("unsupported URL scheme: {0}")]
18 UnsupportedScheme(String),
19
20 #[error("invalid connection URL: {0}")]
21 InvalidUrl(String),
22
23 #[error("backend '{0}' is not enabled — recompile with the appropriate feature")]
24 BackendDisabled(String),
25
26 #[error("connection failed: {0}")]
27 ConnectionFailed(String),
28
29 #[error("query failed: {0}")]
30 QueryFailed(String),
31
32 /// Backend-native bulk-load path is not usable at runtime (server
33 /// config, missing capability, permission denied, target relation
34 /// is not a base table, etc.). Callers may retry on the generic
35 /// INSERT path. The string is intended for stderr only; do not
36 /// match on it.
37 #[error("bulk path unavailable: {0}")]
38 BulkUnavailable(String),
39
40 #[error("TLS error: {0}")]
41 TlsError(String),
42
43 #[error("I/O error: {0}")]
44 Io(#[from] std::io::Error),
45
46 /// Host key mismatch during SSH tunnel setup (always fatal).
47 #[cfg(feature = "ssh")]
48 #[error("SSH host key mismatch for {host}:{port}")]
49 SshHostKeyMismatch { host: String, port: u16 },
50
51 /// Unknown host during SSH tunnel setup (can be TOFU-prompted
52 /// interactively by the CLI layer).
53 #[cfg(feature = "ssh")]
54 #[error("SSH unknown host {host}:{port}")]
55 SshUnknownHost {
56 host: String,
57 port: u16,
58 algorithm: String,
59 fingerprint: String,
60 /// Boxed so this (otherwise large) variant doesn't inflate the
61 /// whole `SqlError` enum — every synchronous backend method
62 /// returns `Result<_, SqlError>` by value, so an oversized error
63 /// variant would bloat every `Ok` path too.
64 key: Box<russh::keys::ssh_key::PublicKey>,
65 },
66
67 #[error("timeout")]
68 Timeout,
69
70 /// A single cell exceeded the configured `max_cell_bytes` guard.
71 ///
72 /// Raised by the read paths (streaming cursor and eager `query`)
73 /// **before** the offending value is retained, so the guard caps
74 /// peak memory at one over-budget cell rather than letting a
75 /// pathological `bytea` / `TEXT` blow the heap. `row` is the
76 /// 0-based row ordinal within the current result; `column` names the
77 /// offending column; `size` and `cap` are byte counts.
78 #[error(
79 "cell too large: row {row}, column {column:?} is {size} bytes, \
80 exceeds the {cap}-byte cap (raise SizeGuards::max_cell_bytes, or \
81 stream the column out via --format csv/jsonl)"
82 )]
83 CellTooLarge {
84 row: u64,
85 column: String,
86 size: usize,
87 cap: usize,
88 },
89
90 /// A whole row's measured byte size exceeded `max_row_bytes`.
91 ///
92 /// Like [`CellTooLarge`](Self::CellTooLarge) this fires before the
93 /// row is appended to any buffer, so an unexpectedly wide row fails
94 /// fast instead of being materialized. `row` is the 0-based ordinal;
95 /// `size`/`cap` are byte counts.
96 #[error(
97 "row too large: row {row} is {size} bytes, exceeds the \
98 {cap}-byte cap (raise SizeGuards::max_row_bytes, or stream via \
99 --format csv/jsonl)"
100 )]
101 RowTooLarge { row: u64, size: usize, cap: usize },
102
103 /// The running total of buffered row bytes crossed
104 /// `max_total_buffered_bytes` while materializing an eager result.
105 ///
106 /// This is the guard the CLI's eager table path relies on: rather
107 /// than collect an unbounded `Vec<Row>` for a huge table, the eager
108 /// `query` accumulates a byte tally and aborts once it crosses the
109 /// cap. `rows_buffered` is how many rows had been accumulated; `cap`
110 /// is the byte ceiling.
111 #[error(
112 "result too large: buffered {rows_buffered} rows exceeding the \
113 {cap}-byte total cap (raise SizeGuards::max_total_buffered_bytes, \
114 or stream the result via the cursor API / --format csv/jsonl)"
115 )]
116 BufferTooLarge { rows_buffered: u64, cap: usize },
117
118 #[error("registry error: {0}")]
119 RegistryError(String),
120}