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