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 oid_map;
45pub mod pool;
46pub(crate) mod types;
47
48mod auth;
49mod conn;
50mod proto;
51mod stmt_cache;
52mod sync_io;
53#[cfg(feature = "tls")]
54mod tls_common;
55#[cfg(feature = "tls")]
56mod tls_sync;
57
58#[cfg(feature = "async")]
59pub mod async_conn;
60#[cfg(feature = "async")]
61mod async_io;
62
63pub use arena::Arena;
64#[cfg(feature = "async")]
65pub use async_conn::AsyncConnection;
66pub use codec::Encode;
67pub use conn::release_col_offsets;
68pub use conn::release_resp_buf;
69pub use conn::Connection;
70pub use pool::{Pool, PoolBuilder, PoolGuard, PoolStatus, Transaction};
71pub use types::{
72    hash_sql, ColumnDesc, Config, Notification, PgDataRow, PrepareResult, QueryResult, Row,
73    SimpleRow, SslMode, StatementCacheMode,
74};
75
76// --- DriverError ---
77
78/// Error type for all bsql-driver-postgres operations.
79///
80/// Variants cover the four failure modes: I/O, authentication, wire protocol
81/// violations, server-reported errors, and pool management.
82///
83/// # Example
84///
85/// ```
86/// use bsql_driver_postgres::DriverError;
87///
88/// fn handle_error(err: DriverError) {
89///     match err {
90///         DriverError::Io(e) => eprintln!("network error: {e}"),
91///         DriverError::Auth(msg) => eprintln!("auth failed: {msg}"),
92///         DriverError::Protocol(msg) => eprintln!("protocol error: {msg}"),
93///         DriverError::Server { code, message, position, .. } => {
94///             let code_str = std::str::from_utf8(&code).unwrap_or("?????");
95///             eprintln!("PG error [{code_str}]: {message} (pos: {position:?})");
96///         }
97///         DriverError::Pool(msg) => eprintln!("pool error: {msg}"),
98///     }
99/// }
100/// ```
101#[derive(Debug)]
102pub enum DriverError {
103    /// TCP/TLS I/O failure.
104    Io(std::io::Error),
105    /// Authentication failure (wrong password, unsupported mechanism, etc.).
106    Auth(String),
107    /// Wire protocol violation (malformed message, unexpected message type, etc.).
108    Protocol(String),
109    /// Server-reported error (invalid SQL, constraint violation, etc.).
110    Server {
111        /// SQLSTATE error code — always exactly 5 ASCII bytes.
112        ///
113        /// The SQL standard (ISO/IEC 9075) defines SQLSTATE as a 5-character
114        /// code: 2-character class + 3-character subclass. PostgreSQL follows
115        /// this strictly — every error response contains a 5-byte `'C'` field.
116        ///
117        /// Stored as `[u8; 5]` instead of `String` or `Box<str>` because:
118        /// - The length is fixed by the SQL standard (always 5, never more, never less)
119        /// - Eliminates a heap allocation per server error
120        /// - Shrinks `DriverError` by 11 bytes (16-byte Box → 5-byte array)
121        /// - Shrinks every `Result<T, DriverError>` on the stack
122        ///
123        /// Compare with string literals using byte strings: `&err.code == b"23505"`
124        code: [u8; 5],
125        /// Human-readable error message.
126        message: Box<str>,
127        /// Optional detail text.
128        detail: Option<Box<str>>,
129        /// Optional hint text.
130        hint: Option<Box<str>>,
131        /// Character position in the original query where the error occurred (1-indexed).
132        position: Option<u32>,
133    },
134    /// Connection pool error (exhaustion, misconfiguration).
135    Pool(String),
136}
137
138impl std::fmt::Display for DriverError {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        match self {
141            Self::Io(e) => write!(f, "I/O error: {e}"),
142            Self::Auth(msg) => write!(f, "auth error: {msg}"),
143            Self::Protocol(msg) => write!(f, "protocol error: {msg}"),
144            Self::Server {
145                code,
146                message,
147                detail,
148                hint,
149                position,
150            } => {
151                write!(
152                    f,
153                    "server error [{}]: {message}",
154                    std::str::from_utf8(code).unwrap_or("?????")
155                )?;
156                if let Some(pos) = position {
157                    write!(f, " (at position {pos})")?;
158                }
159                if let Some(d) = detail {
160                    write!(f, " DETAIL: {d}")?;
161                }
162                if let Some(h) = hint {
163                    write!(f, " HINT: {h}")?;
164                }
165                Ok(())
166            }
167            Self::Pool(msg) => write!(f, "pool error: {msg}"),
168        }
169    }
170}
171
172impl std::error::Error for DriverError {
173    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
174        match self {
175            Self::Io(e) => Some(e),
176            _ => None,
177        }
178    }
179}
180
181impl From<std::io::Error> for DriverError {
182    fn from(e: std::io::Error) -> Self {
183        Self::Io(e)
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn driver_error_display_io() {
193        let e = DriverError::Io(std::io::Error::new(
194            std::io::ErrorKind::ConnectionRefused,
195            "refused",
196        ));
197        assert!(e.to_string().contains("I/O error"));
198        assert!(e.to_string().contains("refused"));
199    }
200
201    #[test]
202    fn driver_error_display_auth() {
203        let e = DriverError::Auth("wrong password".into());
204        assert_eq!(e.to_string(), "auth error: wrong password");
205    }
206
207    #[test]
208    fn driver_error_display_protocol() {
209        let e = DriverError::Protocol("unexpected message".into());
210        assert_eq!(e.to_string(), "protocol error: unexpected message");
211    }
212
213    #[test]
214    fn driver_error_display_server() {
215        let e = DriverError::Server {
216            code: *b"42P01",
217            message: "relation does not exist".into(),
218            detail: Some("table was dropped".into()),
219            hint: None,
220            position: None,
221        };
222        let s = e.to_string();
223        assert!(s.contains("42P01"));
224        assert!(s.contains("relation does not exist"));
225        assert!(s.contains("table was dropped"));
226    }
227
228    #[test]
229    fn driver_error_display_server_no_detail() {
230        let e = DriverError::Server {
231            code: *b"23505",
232            message: Box::from("duplicate key"),
233            detail: None,
234            hint: None,
235            position: None,
236        };
237        assert_eq!(e.to_string(), "server error [23505]: duplicate key");
238    }
239
240    #[test]
241    fn driver_error_display_server_with_position() {
242        let e = DriverError::Server {
243            code: *b"42601",
244            message: Box::from("syntax error"),
245            detail: None,
246            hint: None,
247            position: Some(8),
248        };
249        let s = e.to_string();
250        assert!(s.contains("(at position 8)"));
251    }
252
253    #[test]
254    fn driver_error_display_pool() {
255        let e = DriverError::Pool("exhausted".into());
256        assert_eq!(e.to_string(), "pool error: exhausted");
257    }
258
259    #[test]
260    fn driver_error_source_io() {
261        let inner = std::io::Error::other("test");
262        let e = DriverError::Io(inner);
263        assert!(std::error::Error::source(&e).is_some());
264    }
265
266    #[test]
267    fn driver_error_source_non_io() {
268        let e = DriverError::Auth("test".into());
269        assert!(std::error::Error::source(&e).is_none());
270    }
271
272    #[test]
273    fn driver_error_from_io() {
274        let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
275        let e: DriverError = io_err.into();
276        assert!(matches!(e, DriverError::Io(_)));
277    }
278
279    #[test]
280    fn forbid_unsafe_code() {
281        // This test exists to document the safety guarantee.
282        // The `#![forbid(unsafe_code)]` at the crate root ensures this at compile time.
283    }
284
285    // ===============================================================
286    // DriverError — extended coverage
287    // ===============================================================
288
289    #[test]
290    fn driver_error_display_server_all_none() {
291        let e = DriverError::Server {
292            code: *b"00000",
293            message: "successful completion".into(),
294            detail: None,
295            hint: None,
296            position: None,
297        };
298        let s = e.to_string();
299        assert_eq!(s, "server error [00000]: successful completion");
300        // Should NOT contain DETAIL, HINT, or position
301        assert!(!s.contains("DETAIL"));
302        assert!(!s.contains("HINT"));
303        assert!(!s.contains("position"));
304    }
305
306    #[test]
307    fn driver_error_display_server_detail_only() {
308        let e = DriverError::Server {
309            code: *b"23505",
310            message: "duplicate key".into(),
311            detail: Some("Key (id)=(1) exists.".into()),
312            hint: None,
313            position: None,
314        };
315        let s = e.to_string();
316        assert!(s.contains("DETAIL: Key (id)=(1) exists."));
317        assert!(!s.contains("HINT"));
318    }
319
320    #[test]
321    fn driver_error_display_server_hint_only() {
322        let e = DriverError::Server {
323            code: *b"42601",
324            message: "syntax error".into(),
325            detail: None,
326            hint: Some("check SQL".into()),
327            position: None,
328        };
329        let s = e.to_string();
330        assert!(s.contains("HINT: check SQL"));
331        assert!(!s.contains("DETAIL"));
332    }
333
334    #[test]
335    fn driver_error_display_server_position_only() {
336        let e = DriverError::Server {
337            code: *b"42601",
338            message: "syntax error".into(),
339            detail: None,
340            hint: None,
341            position: Some(15),
342        };
343        let s = e.to_string();
344        assert!(s.contains("(at position 15)"));
345    }
346
347    #[test]
348    fn driver_error_display_server_all_fields() {
349        let e = DriverError::Server {
350            code: *b"42P01",
351            message: "relation does not exist".into(),
352            detail: Some("table was dropped".into()),
353            hint: Some("recreate the table".into()),
354            position: Some(42),
355        };
356        let s = e.to_string();
357        assert!(s.contains("[42P01]"));
358        assert!(s.contains("relation does not exist"));
359        assert!(s.contains("(at position 42)"));
360        assert!(s.contains("DETAIL: table was dropped"));
361        assert!(s.contains("HINT: recreate the table"));
362    }
363
364    #[test]
365    fn driver_error_io_preserves_kind() {
366        let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
367        let e = DriverError::Io(io_err);
368        match &e {
369            DriverError::Io(inner) => {
370                assert_eq!(inner.kind(), std::io::ErrorKind::ConnectionRefused);
371            }
372            _ => panic!("expected Io variant"),
373        }
374    }
375
376    #[test]
377    fn driver_error_io_timeout() {
378        let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "connection timed out");
379        let e = DriverError::Io(io_err);
380        let s = e.to_string();
381        assert!(s.contains("timed out"));
382    }
383
384    #[test]
385    fn driver_error_io_unexpected_eof() {
386        let io_err = std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "connection closed");
387        let e: DriverError = io_err.into();
388        let s = e.to_string();
389        assert!(s.contains("connection closed"));
390    }
391
392    #[test]
393    fn driver_error_auth_empty() {
394        let e = DriverError::Auth(String::new());
395        assert_eq!(e.to_string(), "auth error: ");
396    }
397
398    #[test]
399    fn driver_error_protocol_empty() {
400        let e = DriverError::Protocol(String::new());
401        assert_eq!(e.to_string(), "protocol error: ");
402    }
403
404    #[test]
405    fn driver_error_pool_empty() {
406        let e = DriverError::Pool(String::new());
407        assert_eq!(e.to_string(), "pool error: ");
408    }
409
410    #[test]
411    fn driver_error_source_protocol_is_none() {
412        let e = DriverError::Protocol("test".into());
413        assert!(std::error::Error::source(&e).is_none());
414    }
415
416    #[test]
417    fn driver_error_source_server_is_none() {
418        let e = DriverError::Server {
419            code: *b"42601",
420            message: "err".into(),
421            detail: None,
422            hint: None,
423            position: None,
424        };
425        assert!(std::error::Error::source(&e).is_none());
426    }
427
428    #[test]
429    fn driver_error_source_pool_is_none() {
430        let e = DriverError::Pool("test".into());
431        assert!(std::error::Error::source(&e).is_none());
432    }
433
434    #[test]
435    fn driver_error_debug_all_variants() {
436        let variants: Vec<DriverError> = vec![
437            DriverError::Io(std::io::Error::other("io")),
438            DriverError::Auth("auth".into()),
439            DriverError::Protocol("proto".into()),
440            DriverError::Server {
441                code: *b"00000",
442                message: "ok".into(),
443                detail: None,
444                hint: None,
445                position: None,
446            },
447            DriverError::Pool("pool".into()),
448        ];
449        for v in &variants {
450            let dbg = format!("{v:?}");
451            assert!(!dbg.is_empty());
452        }
453    }
454}