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