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