Skip to main content

bsql_driver_postgres/
lib.rs

1//! PostgreSQL wire protocol driver for bsql.
2//!
3//! `bsql-driver-postgres` is a purpose-built PostgreSQL driver optimized for bsql's
4//! architecture: binary protocol only, arena allocation for row data, pipelined
5//! extended query protocol, LIFO connection pool with fail-fast semantics.
6//!
7//! # Design
8//!
9//! - **Binary protocol only** — numeric types are memcpy, not parsed from ASCII.
10//! - **Arena allocation** — all row data from one query shares a single bump allocator.
11//! - **Pipelined messages** — Parse+Bind+Execute+Sync in one TCP write.
12//! - **Statement cache** — keyed by rapidhash of SQL text. Second query skips Parse.
13//! - **LIFO pool** — returns the warmest connection (best PG backend cache locality).
14//! - **Fail-fast** — pool exhaustion returns an error immediately, never blocks.
15//! - **No unsafe code** — `#![forbid(unsafe_code)]`.
16//!
17//! # Example
18//!
19//! ```no_run
20//! use bsql_driver_postgres::{Pool, Arena};
21//!
22//! # fn example() -> Result<(), bsql_driver_postgres::DriverError> {
23//! let pool = Pool::connect("postgres://user:pass@localhost/db")?;
24//! let mut conn = pool.acquire()?;
25//! let arena = Arena::new();
26//!
27//! let hash = bsql_driver_postgres::hash_sql("SELECT $1::int4 + $2::int4 AS sum");
28//! let result = conn.query(
29//!     "SELECT $1::int4 + $2::int4 AS sum",
30//!     hash,
31//!     &[&1i32, &2i32],
32//! )?;
33//!
34//! let row = result.row(0, &arena);
35//! assert_eq!(row.get_i32(0), Some(3));
36//! # Ok(())
37//! # }
38//! ```
39#![forbid(unsafe_code)]
40#![deny(clippy::all)]
41
42pub mod arena;
43pub mod codec;
44pub mod pool;
45pub(crate) mod types;
46
47mod auth;
48mod conn;
49mod proto;
50mod stmt_cache;
51mod sync_io;
52#[cfg(feature = "tls")]
53mod tls_sync;
54
55pub use arena::Arena;
56pub use codec::Encode;
57pub use conn::Connection;
58pub use conn::release_resp_buf;
59pub use pool::{Pool, PoolBuilder, PoolGuard, PoolStatus, Transaction};
60pub use types::{
61    ColumnDesc, Config, Notification, PgDataRow, PrepareResult, QueryResult, Row, SimpleRow,
62    SslMode, hash_sql,
63};
64
65// --- DriverError ---
66
67/// Error type for all bsql-driver-postgres operations.
68///
69/// Variants cover the four failure modes: I/O, authentication, wire protocol
70/// violations, server-reported errors, and pool management.
71///
72/// # Example
73///
74/// ```
75/// use bsql_driver_postgres::DriverError;
76///
77/// fn handle_error(err: DriverError) {
78///     match err {
79///         DriverError::Io(e) => eprintln!("network error: {e}"),
80///         DriverError::Auth(msg) => eprintln!("auth failed: {msg}"),
81///         DriverError::Protocol(msg) => eprintln!("protocol error: {msg}"),
82///         DriverError::Server { code, message, position, .. } => {
83///             eprintln!("PG error [{code}]: {message} (pos: {position:?})");
84///         }
85///         DriverError::Pool(msg) => eprintln!("pool error: {msg}"),
86///     }
87/// }
88/// ```
89#[derive(Debug)]
90pub enum DriverError {
91    /// TCP/TLS I/O failure.
92    Io(std::io::Error),
93    /// Authentication failure (wrong password, unsupported mechanism, etc.).
94    Auth(String),
95    /// Wire protocol violation (malformed message, unexpected message type, etc.).
96    Protocol(String),
97    /// Server-reported error (invalid SQL, constraint violation, etc.).
98    Server {
99        /// Five-character SQLSTATE code (e.g. "42P01" for undefined table).
100        code: Box<str>,
101        /// Human-readable error message.
102        message: Box<str>,
103        /// Optional detail text.
104        detail: Option<Box<str>>,
105        /// Optional hint text.
106        hint: Option<Box<str>>,
107        /// Character position in the original query where the error occurred (1-indexed).
108        position: Option<u32>,
109    },
110    /// Connection pool error (exhaustion, misconfiguration).
111    Pool(String),
112}
113
114impl std::fmt::Display for DriverError {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        match self {
117            Self::Io(e) => write!(f, "I/O error: {e}"),
118            Self::Auth(msg) => write!(f, "auth error: {msg}"),
119            Self::Protocol(msg) => write!(f, "protocol error: {msg}"),
120            Self::Server {
121                code,
122                message,
123                detail,
124                hint,
125                position,
126            } => {
127                write!(f, "server error [{code}]: {message}")?;
128                if let Some(pos) = position {
129                    write!(f, " (at position {pos})")?;
130                }
131                if let Some(d) = detail {
132                    write!(f, " DETAIL: {d}")?;
133                }
134                if let Some(h) = hint {
135                    write!(f, " HINT: {h}")?;
136                }
137                Ok(())
138            }
139            Self::Pool(msg) => write!(f, "pool error: {msg}"),
140        }
141    }
142}
143
144impl std::error::Error for DriverError {
145    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
146        match self {
147            Self::Io(e) => Some(e),
148            _ => None,
149        }
150    }
151}
152
153impl From<std::io::Error> for DriverError {
154    fn from(e: std::io::Error) -> Self {
155        Self::Io(e)
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn driver_error_display_io() {
165        let e = DriverError::Io(std::io::Error::new(
166            std::io::ErrorKind::ConnectionRefused,
167            "refused",
168        ));
169        assert!(e.to_string().contains("I/O error"));
170        assert!(e.to_string().contains("refused"));
171    }
172
173    #[test]
174    fn driver_error_display_auth() {
175        let e = DriverError::Auth("wrong password".into());
176        assert_eq!(e.to_string(), "auth error: wrong password");
177    }
178
179    #[test]
180    fn driver_error_display_protocol() {
181        let e = DriverError::Protocol("unexpected message".into());
182        assert_eq!(e.to_string(), "protocol error: unexpected message");
183    }
184
185    #[test]
186    fn driver_error_display_server() {
187        let e = DriverError::Server {
188            code: "42P01".into(),
189            message: "relation does not exist".into(),
190            detail: Some("table was dropped".into()),
191            hint: None,
192            position: None,
193        };
194        let s = e.to_string();
195        assert!(s.contains("42P01"));
196        assert!(s.contains("relation does not exist"));
197        assert!(s.contains("table was dropped"));
198    }
199
200    #[test]
201    fn driver_error_display_server_no_detail() {
202        let e = DriverError::Server {
203            code: Box::from("23505"),
204            message: Box::from("duplicate key"),
205            detail: None,
206            hint: None,
207            position: None,
208        };
209        assert_eq!(e.to_string(), "server error [23505]: duplicate key");
210    }
211
212    #[test]
213    fn driver_error_display_server_with_position() {
214        let e = DriverError::Server {
215            code: Box::from("42601"),
216            message: Box::from("syntax error"),
217            detail: None,
218            hint: None,
219            position: Some(8),
220        };
221        let s = e.to_string();
222        assert!(s.contains("(at position 8)"));
223    }
224
225    #[test]
226    fn driver_error_display_pool() {
227        let e = DriverError::Pool("exhausted".into());
228        assert_eq!(e.to_string(), "pool error: exhausted");
229    }
230
231    #[test]
232    fn driver_error_source_io() {
233        let inner = std::io::Error::other("test");
234        let e = DriverError::Io(inner);
235        assert!(std::error::Error::source(&e).is_some());
236    }
237
238    #[test]
239    fn driver_error_source_non_io() {
240        let e = DriverError::Auth("test".into());
241        assert!(std::error::Error::source(&e).is_none());
242    }
243
244    #[test]
245    fn driver_error_from_io() {
246        let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
247        let e: DriverError = io_err.into();
248        assert!(matches!(e, DriverError::Io(_)));
249    }
250
251    #[test]
252    fn forbid_unsafe_code() {
253        // This test exists to document the safety guarantee.
254        // The `#![forbid(unsafe_code)]` at the crate root ensures this at compile time.
255    }
256
257    // ===============================================================
258    // DriverError — extended coverage
259    // ===============================================================
260
261    #[test]
262    fn driver_error_display_server_all_none() {
263        let e = DriverError::Server {
264            code: "00000".into(),
265            message: "successful completion".into(),
266            detail: None,
267            hint: None,
268            position: None,
269        };
270        let s = e.to_string();
271        assert_eq!(s, "server error [00000]: successful completion");
272        // Should NOT contain DETAIL, HINT, or position
273        assert!(!s.contains("DETAIL"));
274        assert!(!s.contains("HINT"));
275        assert!(!s.contains("position"));
276    }
277
278    #[test]
279    fn driver_error_display_server_detail_only() {
280        let e = DriverError::Server {
281            code: "23505".into(),
282            message: "duplicate key".into(),
283            detail: Some("Key (id)=(1) exists.".into()),
284            hint: None,
285            position: None,
286        };
287        let s = e.to_string();
288        assert!(s.contains("DETAIL: Key (id)=(1) exists."));
289        assert!(!s.contains("HINT"));
290    }
291
292    #[test]
293    fn driver_error_display_server_hint_only() {
294        let e = DriverError::Server {
295            code: "42601".into(),
296            message: "syntax error".into(),
297            detail: None,
298            hint: Some("check SQL".into()),
299            position: None,
300        };
301        let s = e.to_string();
302        assert!(s.contains("HINT: check SQL"));
303        assert!(!s.contains("DETAIL"));
304    }
305
306    #[test]
307    fn driver_error_display_server_position_only() {
308        let e = DriverError::Server {
309            code: "42601".into(),
310            message: "syntax error".into(),
311            detail: None,
312            hint: None,
313            position: Some(15),
314        };
315        let s = e.to_string();
316        assert!(s.contains("(at position 15)"));
317    }
318
319    #[test]
320    fn driver_error_display_server_all_fields() {
321        let e = DriverError::Server {
322            code: "42P01".into(),
323            message: "relation does not exist".into(),
324            detail: Some("table was dropped".into()),
325            hint: Some("recreate the table".into()),
326            position: Some(42),
327        };
328        let s = e.to_string();
329        assert!(s.contains("[42P01]"));
330        assert!(s.contains("relation does not exist"));
331        assert!(s.contains("(at position 42)"));
332        assert!(s.contains("DETAIL: table was dropped"));
333        assert!(s.contains("HINT: recreate the table"));
334    }
335
336    #[test]
337    fn driver_error_io_preserves_kind() {
338        let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
339        let e = DriverError::Io(io_err);
340        match &e {
341            DriverError::Io(inner) => {
342                assert_eq!(inner.kind(), std::io::ErrorKind::ConnectionRefused);
343            }
344            _ => panic!("expected Io variant"),
345        }
346    }
347
348    #[test]
349    fn driver_error_io_timeout() {
350        let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "connection timed out");
351        let e = DriverError::Io(io_err);
352        let s = e.to_string();
353        assert!(s.contains("timed out"));
354    }
355
356    #[test]
357    fn driver_error_io_unexpected_eof() {
358        let io_err = std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "connection closed");
359        let e: DriverError = io_err.into();
360        let s = e.to_string();
361        assert!(s.contains("connection closed"));
362    }
363
364    #[test]
365    fn driver_error_auth_empty() {
366        let e = DriverError::Auth(String::new());
367        assert_eq!(e.to_string(), "auth error: ");
368    }
369
370    #[test]
371    fn driver_error_protocol_empty() {
372        let e = DriverError::Protocol(String::new());
373        assert_eq!(e.to_string(), "protocol error: ");
374    }
375
376    #[test]
377    fn driver_error_pool_empty() {
378        let e = DriverError::Pool(String::new());
379        assert_eq!(e.to_string(), "pool error: ");
380    }
381
382    #[test]
383    fn driver_error_source_protocol_is_none() {
384        let e = DriverError::Protocol("test".into());
385        assert!(std::error::Error::source(&e).is_none());
386    }
387
388    #[test]
389    fn driver_error_source_server_is_none() {
390        let e = DriverError::Server {
391            code: "42601".into(),
392            message: "err".into(),
393            detail: None,
394            hint: None,
395            position: None,
396        };
397        assert!(std::error::Error::source(&e).is_none());
398    }
399
400    #[test]
401    fn driver_error_source_pool_is_none() {
402        let e = DriverError::Pool("test".into());
403        assert!(std::error::Error::source(&e).is_none());
404    }
405
406    #[test]
407    fn driver_error_debug_all_variants() {
408        let variants: Vec<DriverError> = vec![
409            DriverError::Io(std::io::Error::other("io")),
410            DriverError::Auth("auth".into()),
411            DriverError::Protocol("proto".into()),
412            DriverError::Server {
413                code: "00000".into(),
414                message: "ok".into(),
415                detail: None,
416                hint: None,
417                position: None,
418            },
419            DriverError::Pool("pool".into()),
420        ];
421        for v in &variants {
422            let dbg = format!("{v:?}");
423            assert!(!dbg.is_empty());
424        }
425    }
426}