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