Skip to main content

bsql_driver_postgres/
types.rs

1//! Shared types used by both async `Connection` and sync `SyncConnection`.
2//!
3//! Extracted from `conn.rs` to avoid duplication between the async and sync
4//! code paths. Contains configuration, result types, row views, and helpers.
5
6use std::sync::Arc;
7
8use rapidhash::quality::RapidHasher;
9
10use crate::arena::Arena;
11use crate::DriverError;
12
13// ---------------------------------------------------------------------------
14// Config
15// ---------------------------------------------------------------------------
16
17/// Implements Drop to zeroize the password field, minimizing the
18/// window where plaintext credentials live in memory.
19#[derive(Debug, Clone)]
20pub struct Config {
21    pub host: String,
22    pub port: u16,
23    pub user: String,
24    pub password: String,
25    pub database: String,
26    pub ssl: SslMode,
27    /// PG-side statement timeout in seconds. Default: 30. 0 = no timeout.
28    ///
29    /// After connecting, the driver sends `SET statement_timeout = '{N}s'`.
30    /// If a query exceeds this duration, PostgreSQL kills it and returns an error.
31    pub statement_timeout_secs: u32,
32}
33
34/// Zeroize password on drop to minimize credential lifetime in memory.
35impl Drop for Config {
36    fn drop(&mut self) {
37        use zeroize::Zeroize;
38        self.password.zeroize();
39    }
40}
41
42/// SSL/TLS connection mode.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum SslMode {
45    /// Never use TLS.
46    Disable,
47    /// Try TLS, fall back to plain if server says 'N'.
48    Prefer,
49    /// Require TLS, fail if server says 'N'.
50    Require,
51}
52
53impl Config {
54    /// Parse a PostgreSQL connection URL.
55    ///
56    /// Format: `postgres://user:password@host:port/database?sslmode=prefer`
57    ///
58    /// # Unix domain sockets
59    ///
60    /// Use the `host` query parameter to specify a UDS directory (libpq convention):
61    /// ```text
62    /// postgres://user@localhost/dbname?host=/tmp
63    /// postgres:///dbname?host=/var/run/postgresql
64    /// ```
65    /// When `host` starts with `/`, the driver connects via Unix domain socket at
66    /// `{host}/.s.PGSQL.{port}` instead of TCP. TLS is skipped for UDS connections.
67    pub fn from_url(url: &str) -> Result<Self, DriverError> {
68        let url = url
69            .strip_prefix("postgres://")
70            .or_else(|| url.strip_prefix("postgresql://"))
71            .ok_or_else(|| DriverError::Protocol("URL must start with postgres://".into()))?;
72
73        // Split user:password@host:port/database
74        let (userinfo, rest) = url
75            .split_once('@')
76            .ok_or_else(|| DriverError::Protocol("missing @ in connection URL".into()))?;
77
78        let (user, password) = userinfo.split_once(':').unwrap_or((userinfo, ""));
79
80        // Split host:port/database?params
81        let (hostport, rest) = rest.split_once('/').unwrap_or((rest, ""));
82        let (database, params) = rest.split_once('?').unwrap_or((rest, ""));
83
84        let (host, port) = if let Some((h, p)) = hostport.split_once(':') {
85            let port = p
86                .parse::<u16>()
87                .map_err(|_| DriverError::Protocol(format!("invalid port: {p}")))?;
88            (h.to_owned(), port)
89        } else {
90            (hostport.to_owned(), 5432)
91        };
92
93        let mut ssl = SslMode::Prefer;
94        let mut statement_timeout_secs: u32 = 30;
95        let mut host_override: Option<String> = None;
96        for param in params.split('&') {
97            if param.is_empty() {
98                continue;
99            }
100            if let Some(val) = param.strip_prefix("sslmode=") {
101                // A typo like "sslmode=require" (missing 'e') would go unencrypted.
102                ssl = match val {
103                    "disable" => SslMode::Disable,
104                    "prefer" => SslMode::Prefer,
105                    "require" => SslMode::Require,
106                    _ => {
107                        return Err(DriverError::Protocol(format!(
108                            "unknown sslmode: '{val}' (expected: disable, prefer, require)"
109                        )));
110                    }
111                };
112            } else if let Some(val) = param.strip_prefix("statement_timeout=") {
113                statement_timeout_secs = val.parse::<u32>().unwrap_or(30);
114            } else if let Some(val) = param.strip_prefix("host=") {
115                host_override = Some(url_decode(val)?);
116            }
117        }
118
119        // If ?host=/path was specified, override the URL hostname with it.
120        // This follows the libpq convention: host=/tmp means UDS.
121        let final_host = if let Some(h) = host_override {
122            h
123        } else {
124            url_decode(&host)?
125        };
126
127        let config = Config {
128            host: final_host,
129            port,
130            user: url_decode(user)?,
131            password: url_decode(password)?,
132            database: if database.is_empty() {
133                url_decode(user)?
134            } else {
135                url_decode(database)?
136            },
137            ssl,
138            statement_timeout_secs,
139        };
140        config.validate()?;
141        Ok(config)
142    }
143
144    /// Validate configuration fields before attempting a connection.
145    ///
146    /// Called automatically by `from_url()`. Call manually if constructing
147    /// a `Config` by hand.
148    pub fn validate(&self) -> Result<(), DriverError> {
149        if self.host.is_empty() {
150            return Err(DriverError::Protocol("host cannot be empty".into()));
151        }
152        if self.user.is_empty() {
153            return Err(DriverError::Protocol("user cannot be empty".into()));
154        }
155        if self.database.is_empty() {
156            return Err(DriverError::Protocol("database cannot be empty".into()));
157        }
158        Ok(())
159    }
160
161    /// Returns `true` if the host is a Unix domain socket directory path.
162    ///
163    /// libpq convention: if `host` starts with `/`, the connection uses a
164    /// Unix domain socket at `{host}/.s.PGSQL.{port}`.
165    pub fn host_is_uds(&self) -> bool {
166        self.host.starts_with('/')
167    }
168
169    /// Returns the Unix domain socket path: `{host}/.s.PGSQL.{port}`.
170    ///
171    /// Only meaningful when [`host_is_uds()`](Self::host_is_uds) returns `true`.
172    pub fn uds_path(&self) -> String {
173        format!("{}/.s.PGSQL.{}", self.host, self.port)
174    }
175}
176
177// ---------------------------------------------------------------------------
178// url_decode / hex_val
179// ---------------------------------------------------------------------------
180
181/// Minimal percent-decoding for connection URL components.
182///
183/// Decodes `%XX` hex sequences into raw bytes, then validates as UTF-8.
184/// This correctly handles multi-byte UTF-8 characters that are percent-encoded
185/// byte-by-byte (e.g. `%C3%A9` for 'e').
186fn url_decode(s: &str) -> Result<String, DriverError> {
187    let mut bytes = Vec::with_capacity(s.len());
188    let input = s.as_bytes();
189    let mut i = 0;
190    while i < input.len() {
191        if input[i] == b'%' {
192            if i + 2 >= input.len() {
193                return Err(DriverError::Protocol(format!(
194                    "malformed percent-encoding in URL: '{s}'"
195                )));
196            }
197            let hi = hex_val(input[i + 1]).ok_or_else(|| {
198                DriverError::Protocol(format!(
199                    "invalid hex digit '{}' in URL: '{s}'",
200                    input[i + 1] as char
201                ))
202            })?;
203            let lo = hex_val(input[i + 2]).ok_or_else(|| {
204                DriverError::Protocol(format!(
205                    "invalid hex digit '{}' in URL: '{s}'",
206                    input[i + 2] as char
207                ))
208            })?;
209            bytes.push(hi * 16 + lo);
210            i += 3;
211        } else {
212            bytes.push(input[i]);
213            i += 1;
214        }
215    }
216    String::from_utf8(bytes)
217        .map_err(|_| DriverError::Protocol(format!("invalid UTF-8 in URL: '{s}'")))
218}
219
220fn hex_val(b: u8) -> Option<u8> {
221    match b {
222        b'0'..=b'9' => Some(b - b'0'),
223        b'a'..=b'f' => Some(b - b'a' + 10),
224        b'A'..=b'F' => Some(b - b'A' + 10),
225        _ => None,
226    }
227}
228
229// ---------------------------------------------------------------------------
230// StartupAction
231// ---------------------------------------------------------------------------
232
233/// Owned action from a startup message, avoiding borrow conflicts with `self.read_buf`.
234pub(crate) enum StartupAction {
235    AuthOk,
236    AuthCleartext,
237    AuthMd5([u8; 4]),
238    AuthSasl(Vec<u8>),
239    ParameterStatus(Box<str>, Box<str>),
240    BackendKeyData(i32, i32),
241    ReadyForQuery(u8),
242    Error(String),
243    Notice,
244}
245
246// ---------------------------------------------------------------------------
247// ColumnDesc / PrepareResult / SimpleRow / Notification
248// ---------------------------------------------------------------------------
249
250/// Description of a result column.
251#[derive(Debug, Clone)]
252pub struct ColumnDesc {
253    /// Column name from the query.
254    pub name: Box<str>,
255    /// PostgreSQL type OID.
256    pub type_oid: u32,
257    /// Type size in bytes (-1 for variable-length).
258    pub type_size: i16,
259    /// OID of the source table (0 if not a table column, e.g. computed).
260    pub table_oid: u32,
261    /// Column number within the source table (0 if not a table column).
262    pub column_id: i16,
263}
264
265/// Result of a `prepare_describe` call -- column and parameter metadata
266/// without executing the query.
267#[derive(Debug, Clone)]
268pub struct PrepareResult {
269    /// Output columns (empty for INSERT/UPDATE/DELETE without RETURNING).
270    pub columns: Vec<ColumnDesc>,
271    /// PostgreSQL OIDs of the expected parameter types.
272    pub param_oids: Vec<u32>,
273}
274
275/// A single row of text values returned by `simple_query_rows`.
276///
277/// Each field is `None` for SQL NULL, `Some(text)` otherwise.
278/// Only used for compile-time schema introspection queries.
279pub type SimpleRow = Vec<Option<String>>;
280
281/// A notification received during normal query processing.
282///
283/// When the read loop encounters a NotificationResponse during queries,
284/// it is buffered here instead of being dropped. Call
285/// [`Connection::drain_notifications`] to retrieve and clear the buffer.
286#[derive(Debug, Clone)]
287pub struct Notification {
288    /// Backend process ID that sent the notification.
289    pub pid: i32,
290    /// Channel name.
291    pub channel: String,
292    /// Payload string (may be empty).
293    pub payload: String,
294}
295
296// ---------------------------------------------------------------------------
297// QueryResult
298// ---------------------------------------------------------------------------
299
300/// Collected result of a query: all rows' column offsets plus metadata.
301///
302/// Data lives in an [`Arena`]; this struct holds only the offset/length
303/// bookkeeping. Access rows via [`row()`](Self::row) or [`rows()`](Self::rows).
304///
305/// # Example
306///
307/// ```ignore
308/// for row in result.rows(&arena) {
309///     // Access columns by index
310/// }
311/// ```
312pub struct QueryResult {
313    /// All rows' column (offset, length) pairs, contiguous.
314    /// length = -1 means NULL. Offsets point into `data_buf` if present,
315    /// otherwise into the arena.
316    pub(crate) all_col_offsets: Vec<(usize, i32)>,
317    /// Number of columns per row.
318    pub(crate) num_cols: usize,
319    pub(crate) columns: Arc<[ColumnDesc]>,
320    pub(crate) affected_rows: u64,
321    /// Inline data buffer for non-streaming queries.
322    /// When present, column offsets point here instead of the arena.
323    /// This eliminates the final arena.alloc_copy for entire result sets.
324    pub(crate) data_buf: Option<Vec<u8>>,
325}
326
327impl QueryResult {
328    /// Construct a `QueryResult` from its constituent parts.
329    ///
330    /// Used by `bsql-core`'s streaming layer to assemble per-chunk results.
331    pub fn from_parts(
332        all_col_offsets: Vec<(usize, i32)>,
333        num_cols: usize,
334        columns: Arc<[ColumnDesc]>,
335        affected_rows: u64,
336    ) -> Self {
337        Self {
338            all_col_offsets,
339            num_cols,
340            columns,
341            affected_rows,
342            data_buf: None,
343        }
344    }
345
346    /// Construct with inline data buffer (zero-copy from wire).
347    pub fn from_parts_with_buf(
348        all_col_offsets: Vec<(usize, i32)>,
349        num_cols: usize,
350        columns: Arc<[ColumnDesc]>,
351        affected_rows: u64,
352        data_buf: Vec<u8>,
353    ) -> Self {
354        Self {
355            all_col_offsets,
356            num_cols,
357            columns,
358            affected_rows,
359            data_buf: if data_buf.is_empty() {
360                None
361            } else {
362                Some(data_buf)
363            },
364        }
365    }
366
367    /// Number of rows in the result.
368    pub fn len(&self) -> usize {
369        if self.num_cols == 0 {
370            return 0;
371        }
372        self.all_col_offsets.len() / self.num_cols
373    }
374
375    /// Whether the result set is empty.
376    pub fn is_empty(&self) -> bool {
377        self.all_col_offsets.is_empty()
378    }
379
380    /// Number of affected rows (for INSERT/UPDATE/DELETE).
381    pub fn affected_rows(&self) -> u64 {
382        self.affected_rows
383    }
384
385    /// Column descriptors.
386    pub fn columns(&self) -> &[ColumnDesc] {
387        &self.columns
388    }
389
390    /// Get a row by index. The returned `Row` borrows from the arena or
391    /// the inline data buffer.
392    pub fn row<'a>(&'a self, idx: usize, arena: &'a Arena) -> Row<'a> {
393        let start = idx * self.num_cols;
394        let end = start + self.num_cols;
395        Row {
396            data: self.data_buf.as_deref(),
397            arena,
398            col_offsets: &self.all_col_offsets[start..end],
399            columns: &self.columns,
400        }
401    }
402
403    /// Take the `col_offsets` vec out of this result, leaving it empty.
404    ///
405    /// Used by `QueryStream` to reclaim and reuse the allocation between chunks
406    /// instead of allocating a new `Vec` per chunk.
407    pub fn take_col_offsets(&mut self) -> Vec<(usize, i32)> {
408        std::mem::take(&mut self.all_col_offsets)
409    }
410
411    /// Take the data buffer for recycling. Returns None if no data_buf.
412    pub fn take_data_buf(&mut self) -> Option<Vec<u8>> {
413        self.data_buf.take()
414    }
415
416    /// Iterate over rows.
417    pub fn rows<'a>(&'a self, arena: &'a Arena) -> impl Iterator<Item = Row<'a>> {
418        let num_cols = self.num_cols;
419        let columns = &self.columns;
420        let data = self.data_buf.as_deref();
421        self.all_col_offsets
422            .chunks(num_cols.max(1))
423            .map(move |chunk| Row {
424                data,
425                arena,
426                col_offsets: chunk,
427                columns,
428            })
429    }
430}
431
432// ---------------------------------------------------------------------------
433// Row
434// ---------------------------------------------------------------------------
435
436/// A view into a single result row, borrowing data from the arena.
437///
438/// Column values are accessed by index. NULL values return `None`.
439/// Decode errors (protocol violations from a malicious server) are treated
440/// as `None` rather than panicking -- a compliant PostgreSQL server always
441/// sends correctly-sized data for the declared type.
442pub struct Row<'a> {
443    /// Inline data buffer (when QueryResult has data_buf).
444    /// If present, column offsets point here. Otherwise they point into arena.
445    data: Option<&'a [u8]>,
446    arena: &'a Arena,
447    col_offsets: &'a [(usize, i32)],
448    columns: &'a [ColumnDesc],
449}
450
451impl<'a> Row<'a> {
452    /// Get the raw bytes for a column, or `None` if NULL.
453    #[inline]
454    pub fn get_raw(&self, idx: usize) -> Option<&'a [u8]> {
455        let (offset, len) = self.col_offsets[idx];
456        if len < 0 {
457            None
458        } else if let Some(buf) = self.data {
459            Some(&buf[offset..offset + len as usize])
460        } else {
461            Some(self.arena.get(offset, len as usize))
462        }
463    }
464
465    /// Whether a column is NULL.
466    #[inline]
467    pub fn is_null(&self, idx: usize) -> bool {
468        self.col_offsets[idx].1 < 0
469    }
470
471    /// Number of columns.
472    #[inline]
473    pub fn column_count(&self) -> usize {
474        self.col_offsets.len()
475    }
476
477    /// Get a boolean column value. Returns `None` on NULL or decode error.
478    #[inline]
479    pub fn get_bool(&self, idx: usize) -> Option<bool> {
480        self.get_raw(idx)
481            .and_then(|data| crate::codec::decode_bool(data).ok())
482    }
483
484    /// Get an i16 column value. Returns `None` on NULL or decode error.
485    #[inline]
486    pub fn get_i16(&self, idx: usize) -> Option<i16> {
487        self.get_raw(idx)
488            .and_then(|data| crate::codec::decode_i16(data).ok())
489    }
490
491    /// Get an i32 column value. Returns `None` on NULL or decode error.
492    #[inline]
493    pub fn get_i32(&self, idx: usize) -> Option<i32> {
494        self.get_raw(idx)
495            .and_then(|data| crate::codec::decode_i32(data).ok())
496    }
497
498    /// Get an i64 column value. Returns `None` on NULL or decode error.
499    #[inline]
500    pub fn get_i64(&self, idx: usize) -> Option<i64> {
501        self.get_raw(idx)
502            .and_then(|data| crate::codec::decode_i64(data).ok())
503    }
504
505    /// Get an f32 column value. Returns `None` on NULL or decode error.
506    #[inline]
507    pub fn get_f32(&self, idx: usize) -> Option<f32> {
508        self.get_raw(idx)
509            .and_then(|data| crate::codec::decode_f32(data).ok())
510    }
511
512    /// Get an f64 column value. Returns `None` on NULL or decode error.
513    #[inline]
514    pub fn get_f64(&self, idx: usize) -> Option<f64> {
515        self.get_raw(idx)
516            .and_then(|data| crate::codec::decode_f64(data).ok())
517    }
518
519    /// Get a string column value. Returns `None` on NULL or decode error.
520    #[inline]
521    pub fn get_str(&self, idx: usize) -> Option<&'a str> {
522        self.get_raw(idx)
523            .and_then(|data| crate::codec::decode_str(data).ok())
524    }
525
526    /// Get a byte slice column value.
527    #[inline]
528    pub fn get_bytes(&self, idx: usize) -> Option<&'a [u8]> {
529        self.get_raw(idx)
530    }
531
532    /// Get the column name by index.
533    #[inline]
534    pub fn column_name(&self, idx: usize) -> &str {
535        &self.columns[idx].name
536    }
537
538    /// Get the column type OID by index.
539    #[inline]
540    pub fn column_type_oid(&self, idx: usize) -> u32 {
541        self.columns[idx].type_oid
542    }
543}
544
545// ---------------------------------------------------------------------------
546// PgDataRow (zero-copy row view for for_each)
547// ---------------------------------------------------------------------------
548
549/// A temporary view of a single PostgreSQL DataRow message.
550///
551/// Reads columns directly from the wire buffer -- no arena copy.
552/// Column offsets are pre-computed on construction using a `SmallVec`
553/// that is stack-allocated for up to 16 columns (zero heap allocation
554/// for the common case).
555///
556/// Lifetime `'a` borrows from `Connection::read_buf`.
557pub struct PgDataRow<'a> {
558    data: &'a [u8],
559    /// Pre-scanned `(byte_offset, wire_len)` pairs for each column.
560    /// `wire_len = -1` means NULL.
561    offsets: smallvec::SmallVec<[(usize, i32); 16]>,
562}
563
564impl<'a> PgDataRow<'a> {
565    /// Parse column boundaries from a raw DataRow payload.
566    ///
567    /// `data` is the DataRow message payload (after the 'D' type byte and
568    /// 4-byte length prefix have been stripped by the framing layer).
569    pub fn new(data: &'a [u8]) -> Result<Self, DriverError> {
570        if data.len() < 2 {
571            return Err(DriverError::Protocol("DataRow too short".into()));
572        }
573        let num_cols = i16::from_be_bytes([data[0], data[1]]);
574        if num_cols < 0 {
575            return Err(DriverError::Protocol(
576                "DataRow: negative column count".into(),
577            ));
578        }
579        let num_cols = num_cols as usize;
580        let mut offsets = smallvec::SmallVec::<[(usize, i32); 16]>::with_capacity(num_cols);
581        let mut pos = 2usize;
582        for _ in 0..num_cols {
583            if pos + 4 > data.len() {
584                return Err(DriverError::Protocol("DataRow truncated".into()));
585            }
586            let col_len =
587                i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
588            pos += 4;
589            offsets.push((pos, col_len));
590            if col_len > 0 {
591                pos += col_len as usize;
592            }
593        }
594        Ok(Self { data, offsets })
595    }
596
597    /// Get the raw bytes for a column, or `None` if NULL.
598    #[inline]
599    pub fn get_raw(&self, idx: usize) -> Option<&'a [u8]> {
600        let (offset, len) = self.offsets[idx];
601        if len < 0 {
602            None
603        } else {
604            Some(&self.data[offset..offset + len as usize])
605        }
606    }
607
608    /// Whether a column is NULL.
609    #[inline]
610    pub fn is_null(&self, idx: usize) -> bool {
611        self.offsets[idx].1 < 0
612    }
613
614    /// Number of columns.
615    #[inline]
616    pub fn column_count(&self) -> usize {
617        self.offsets.len()
618    }
619
620    /// Get a boolean column value. Returns `None` on NULL or decode error.
621    #[inline]
622    pub fn get_bool(&self, idx: usize) -> Option<bool> {
623        self.get_raw(idx)
624            .and_then(|data| crate::codec::decode_bool(data).ok())
625    }
626
627    /// Get an i16 column value.
628    #[inline]
629    pub fn get_i16(&self, idx: usize) -> Option<i16> {
630        self.get_raw(idx)
631            .and_then(|data| crate::codec::decode_i16(data).ok())
632    }
633
634    /// Get an i32 column value.
635    #[inline]
636    pub fn get_i32(&self, idx: usize) -> Option<i32> {
637        self.get_raw(idx)
638            .and_then(|data| crate::codec::decode_i32(data).ok())
639    }
640
641    /// Get an i64 column value.
642    #[inline]
643    pub fn get_i64(&self, idx: usize) -> Option<i64> {
644        self.get_raw(idx)
645            .and_then(|data| crate::codec::decode_i64(data).ok())
646    }
647
648    /// Get an f32 column value.
649    #[inline]
650    pub fn get_f32(&self, idx: usize) -> Option<f32> {
651        self.get_raw(idx)
652            .and_then(|data| crate::codec::decode_f32(data).ok())
653    }
654
655    /// Get an f64 column value.
656    #[inline]
657    pub fn get_f64(&self, idx: usize) -> Option<f64> {
658        self.get_raw(idx)
659            .and_then(|data| crate::codec::decode_f64(data).ok())
660    }
661
662    /// Get a string column value (zero-copy borrow from the wire buffer).
663    #[inline]
664    pub fn get_str(&self, idx: usize) -> Option<&'a str> {
665        self.get_raw(idx)
666            .and_then(|data| crate::codec::decode_str(data).ok())
667    }
668
669    /// Get a byte slice column value (zero-copy borrow from the wire buffer).
670    #[inline]
671    pub fn get_bytes(&self, idx: usize) -> Option<&'a [u8]> {
672        self.get_raw(idx)
673    }
674}
675
676// ---------------------------------------------------------------------------
677// hash_sql
678// ---------------------------------------------------------------------------
679
680/// Compute a rapidhash of a SQL string.
681///
682/// Uses `str::hash()` via the `Hash` trait, matching `bsql_core::rapid_hash_str`.
683pub fn hash_sql(sql: &str) -> u64 {
684    use std::hash::{Hash, Hasher};
685    let mut hasher = RapidHasher::default();
686    sql.hash(&mut hasher);
687    hasher.finish()
688}
689
690// ---------------------------------------------------------------------------
691// Tests
692// ---------------------------------------------------------------------------
693
694#[cfg(test)]
695#[allow(clippy::approx_constant)]
696mod tests {
697    use super::*;
698
699    // ===================================================================
700    // Config tests
701    // ===================================================================
702
703    #[test]
704    fn config_parse_full_url() {
705        let cfg = Config::from_url("postgres://user:pass@localhost:5432/mydb").unwrap();
706        assert_eq!(cfg.user, "user");
707        assert_eq!(cfg.password, "pass");
708        assert_eq!(cfg.host, "localhost");
709        assert_eq!(cfg.port, 5432);
710        assert_eq!(cfg.database, "mydb");
711    }
712
713    #[test]
714    fn config_parse_default_port() {
715        let cfg = Config::from_url("postgres://user:pass@localhost/mydb").unwrap();
716        assert_eq!(cfg.port, 5432);
717    }
718
719    #[test]
720    fn config_parse_no_password() {
721        let cfg = Config::from_url("postgres://user@localhost/mydb").unwrap();
722        assert_eq!(cfg.user, "user");
723        assert_eq!(cfg.password, "");
724    }
725
726    #[test]
727    fn config_parse_empty_database() {
728        let cfg = Config::from_url("postgres://user:pass@localhost").unwrap();
729        // database defaults to user
730        assert_eq!(cfg.database, "user");
731    }
732
733    #[test]
734    fn config_parse_sslmode() {
735        let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=require").unwrap();
736        assert_eq!(cfg.ssl, SslMode::Require);
737    }
738
739    #[test]
740    fn config_parse_percent_encoding() {
741        let cfg = Config::from_url("postgres://user%40domain:p%40ss@localhost/db").unwrap();
742        assert_eq!(cfg.user, "user@domain");
743        assert_eq!(cfg.password, "p@ss");
744    }
745
746    #[test]
747    fn config_rejects_bad_scheme() {
748        let result = Config::from_url("mysql://user:pass@localhost/db");
749        assert!(result.is_err());
750    }
751
752    /// Unknown sslmode should error, not silently default to Prefer.
753    #[test]
754    fn config_rejects_unknown_sslmode() {
755        let result = Config::from_url("postgres://user:pass@localhost/db?sslmode=requre");
756        assert!(result.is_err(), "typo 'requre' should be rejected");
757        let result = Config::from_url("postgres://user:pass@localhost/db?sslmode=REQUIRE");
758        assert!(result.is_err(), "uppercase should be rejected");
759        let result = Config::from_url("postgres://user:pass@localhost/db?sslmode=bogus");
760        assert!(result.is_err(), "bogus value should be rejected");
761    }
762
763    /// Valid sslmodes should still work.
764    #[test]
765    fn config_accepts_valid_sslmodes() {
766        let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=disable").unwrap();
767        assert_eq!(cfg.ssl, SslMode::Disable);
768        let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=prefer").unwrap();
769        assert_eq!(cfg.ssl, SslMode::Prefer);
770        let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=require").unwrap();
771        assert_eq!(cfg.ssl, SslMode::Require);
772    }
773
774    // #68: Config with postgresql:// scheme
775    #[test]
776    fn config_parse_postgresql_scheme() {
777        let cfg = Config::from_url("postgresql://user:pass@localhost:5432/mydb").unwrap();
778        assert_eq!(cfg.user, "user");
779        assert_eq!(cfg.password, "pass");
780        assert_eq!(cfg.host, "localhost");
781        assert_eq!(cfg.port, 5432);
782        assert_eq!(cfg.database, "mydb");
783    }
784
785    // #69: Config URL without password
786    #[test]
787    fn config_parse_no_password_standalone() {
788        let cfg = Config::from_url("postgres://admin@db.example.com/myapp").unwrap();
789        assert_eq!(cfg.user, "admin");
790        assert_eq!(cfg.password, "");
791        assert_eq!(cfg.host, "db.example.com");
792        assert_eq!(cfg.database, "myapp");
793    }
794
795    // #70: Config URL with empty database (falls back to user)
796    #[test]
797    fn config_empty_database_falls_back_to_user() {
798        let cfg = Config::from_url("postgres://testuser:pass@localhost").unwrap();
799        assert_eq!(cfg.database, "testuser");
800    }
801
802    // #71: Config URL with unknown sslmode error
803    #[test]
804    fn config_unknown_sslmode_error() {
805        let result = Config::from_url("postgres://u:p@h/d?sslmode=verify-full");
806        assert!(result.is_err());
807        let err = result.unwrap_err().to_string();
808        assert!(
809            err.contains("unknown sslmode"),
810            "should describe unknown sslmode: {err}"
811        );
812    }
813
814    // #72: Config URL with multiple query params
815    #[test]
816    fn config_multiple_query_params() {
817        let cfg = Config::from_url(
818            "postgres://user:pass@localhost/db?sslmode=disable&statement_timeout=60",
819        )
820        .unwrap();
821        assert_eq!(cfg.ssl, SslMode::Disable);
822        assert_eq!(cfg.statement_timeout_secs, 60);
823    }
824
825    // Config validation: empty host
826    #[test]
827    fn config_validate_empty_host() {
828        let cfg = Config {
829            host: String::new(),
830            port: 5432,
831            user: "user".into(),
832            password: "pass".into(),
833            database: "db".into(),
834            ssl: SslMode::Disable,
835            statement_timeout_secs: 30,
836        };
837        assert!(cfg.validate().is_err());
838    }
839
840    // Config validation: empty user
841    #[test]
842    fn config_validate_empty_user() {
843        let cfg = Config {
844            host: "localhost".into(),
845            port: 5432,
846            user: String::new(),
847            password: "pass".into(),
848            database: "db".into(),
849            ssl: SslMode::Disable,
850            statement_timeout_secs: 30,
851        };
852        assert!(cfg.validate().is_err());
853    }
854
855    // Config validation: empty database
856    #[test]
857    fn config_validate_empty_database() {
858        let cfg = Config {
859            host: "localhost".into(),
860            port: 5432,
861            user: "user".into(),
862            password: "pass".into(),
863            database: String::new(),
864            ssl: SslMode::Disable,
865            statement_timeout_secs: 30,
866        };
867        assert!(cfg.validate().is_err());
868    }
869
870    // Config missing @ in URL
871    #[test]
872    fn config_missing_at_sign() {
873        let result = Config::from_url("postgres://userpasslocalhost/db");
874        assert!(result.is_err());
875    }
876
877    // Config with custom port
878    #[test]
879    fn config_custom_port() {
880        let cfg = Config::from_url("postgres://user:pass@localhost:5433/db").unwrap();
881        assert_eq!(cfg.port, 5433);
882    }
883
884    // Config with invalid port
885    #[test]
886    fn config_invalid_port() {
887        let result = Config::from_url("postgres://user:pass@localhost:notaport/db");
888        assert!(result.is_err());
889    }
890
891    // #76: Config SslMode::Require without tls feature
892    #[cfg(not(feature = "tls"))]
893    #[test]
894    fn config_sslmode_require_without_tls_feature() {
895        // The config parses fine, but validate doesn't check this.
896        // The error occurs at connection time. Just verify parsing works.
897        let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=require").unwrap();
898        assert_eq!(cfg.ssl, SslMode::Require);
899    }
900
901    #[test]
902    fn config_statement_timeout_default() {
903        let cfg = Config::from_url("postgres://user:pass@localhost/db").unwrap();
904        assert_eq!(cfg.statement_timeout_secs, 30);
905    }
906
907    #[test]
908    fn config_statement_timeout_custom() {
909        let cfg =
910            Config::from_url("postgres://user:pass@localhost/db?statement_timeout=120").unwrap();
911        assert_eq!(cfg.statement_timeout_secs, 120);
912    }
913
914    #[test]
915    fn config_statement_timeout_zero() {
916        let cfg =
917            Config::from_url("postgres://user:pass@localhost/db?statement_timeout=0").unwrap();
918        assert_eq!(cfg.statement_timeout_secs, 0);
919    }
920
921    #[test]
922    fn config_statement_timeout_invalid_falls_back() {
923        let cfg =
924            Config::from_url("postgres://user:pass@localhost/db?statement_timeout=notanumber")
925                .unwrap();
926        assert_eq!(cfg.statement_timeout_secs, 30); // fallback
927    }
928
929    #[test]
930    fn config_uds_path_format() {
931        let cfg = Config::from_url("postgres://user@localhost/db?host=/tmp").unwrap();
932        assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5432");
933    }
934
935    #[test]
936    fn config_uds_path_custom_port() {
937        let cfg = Config::from_url("postgres://user@localhost:5433/db?host=/tmp").unwrap();
938        assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5433");
939    }
940
941    // ===================================================================
942    // UDS (Unix domain socket) tests
943    // ===================================================================
944
945    #[test]
946    fn config_host_is_uds_absolute_path() {
947        let cfg = Config {
948            host: "/tmp".into(),
949            port: 5432,
950            user: "user".into(),
951            password: "".into(),
952            database: "db".into(),
953            ssl: SslMode::Disable,
954            statement_timeout_secs: 30,
955        };
956        assert!(cfg.host_is_uds());
957        assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5432");
958    }
959
960    #[test]
961    fn config_host_is_uds_var_run() {
962        let cfg = Config {
963            host: "/var/run/postgresql".into(),
964            port: 5433,
965            user: "user".into(),
966            password: "".into(),
967            database: "db".into(),
968            ssl: SslMode::Disable,
969            statement_timeout_secs: 30,
970        };
971        assert!(cfg.host_is_uds());
972        assert_eq!(cfg.uds_path(), "/var/run/postgresql/.s.PGSQL.5433");
973    }
974
975    #[test]
976    fn config_host_is_not_uds_for_hostname() {
977        let cfg = Config {
978            host: "localhost".into(),
979            port: 5432,
980            user: "user".into(),
981            password: "".into(),
982            database: "db".into(),
983            ssl: SslMode::Disable,
984            statement_timeout_secs: 30,
985        };
986        assert!(!cfg.host_is_uds());
987    }
988
989    #[test]
990    fn config_host_is_not_uds_for_ip() {
991        let cfg = Config {
992            host: "127.0.0.1".into(),
993            port: 5432,
994            user: "user".into(),
995            password: "".into(),
996            database: "db".into(),
997            ssl: SslMode::Disable,
998            statement_timeout_secs: 30,
999        };
1000        assert!(!cfg.host_is_uds());
1001    }
1002
1003    #[test]
1004    fn config_parse_uds_host_query_param() {
1005        let cfg = Config::from_url("postgres://user@localhost/mydb?host=/tmp").unwrap();
1006        assert_eq!(cfg.host, "/tmp");
1007        assert!(cfg.host_is_uds());
1008        assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5432");
1009        assert_eq!(cfg.database, "mydb");
1010        assert_eq!(cfg.user, "user");
1011    }
1012
1013    #[test]
1014    fn config_parse_uds_host_query_param_custom_port() {
1015        let cfg = Config::from_url("postgres://user@localhost:5433/mydb?host=/var/run/postgresql")
1016            .unwrap();
1017        assert_eq!(cfg.host, "/var/run/postgresql");
1018        assert_eq!(cfg.port, 5433);
1019        assert_eq!(cfg.uds_path(), "/var/run/postgresql/.s.PGSQL.5433");
1020    }
1021
1022    #[test]
1023    fn config_parse_uds_host_with_other_params() {
1024        let cfg = Config::from_url(
1025            "postgres://user@localhost/db?host=/tmp&sslmode=disable&statement_timeout=60",
1026        )
1027        .unwrap();
1028        assert_eq!(cfg.host, "/tmp");
1029        assert!(cfg.host_is_uds());
1030        assert_eq!(cfg.ssl, SslMode::Disable);
1031        assert_eq!(cfg.statement_timeout_secs, 60);
1032    }
1033
1034    #[test]
1035    fn config_parse_uds_host_percent_encoded() {
1036        // %2F = '/'
1037        let cfg = Config::from_url("postgres://user@localhost/db?host=%2Ftmp").unwrap();
1038        assert_eq!(cfg.host, "/tmp");
1039        assert!(cfg.host_is_uds());
1040    }
1041
1042    #[test]
1043    fn config_parse_tcp_host_not_overridden_without_param() {
1044        // No ?host= param: hostname from URL is used (TCP)
1045        let cfg = Config::from_url("postgres://user@myserver/db").unwrap();
1046        assert_eq!(cfg.host, "myserver");
1047        assert!(!cfg.host_is_uds());
1048    }
1049
1050    #[test]
1051    fn config_parse_uds_host_overrides_url_hostname() {
1052        // ?host= overrides even an explicit hostname
1053        let cfg = Config::from_url("postgres://user@db.example.com/mydb?host=/var/run/postgresql")
1054            .unwrap();
1055        assert_eq!(cfg.host, "/var/run/postgresql");
1056        assert!(cfg.host_is_uds());
1057    }
1058
1059    #[test]
1060    fn config_parse_uds_empty_url_host() {
1061        // postgres:///dbname?host=/tmp -- empty hostname before /, host from param
1062        let cfg = Config::from_url("postgres://user@/mydb?host=/tmp").unwrap();
1063        assert_eq!(cfg.host, "/tmp");
1064        assert!(cfg.host_is_uds());
1065        assert_eq!(cfg.database, "mydb");
1066    }
1067
1068    // ===================================================================
1069    // url_decode tests
1070    // ===================================================================
1071
1072    #[test]
1073    fn url_decode_works() {
1074        assert_eq!(url_decode("hello%20world").unwrap(), "hello world");
1075        assert_eq!(url_decode("no%20escape").unwrap(), "no escape");
1076        assert_eq!(url_decode("plain").unwrap(), "plain");
1077        assert_eq!(url_decode("a%40b").unwrap(), "a@b");
1078    }
1079
1080    #[test]
1081    fn url_decode_malformed_percent_trailing() {
1082        // Truncated percent sequence at end of string
1083        let result = url_decode("abc%2");
1084        assert!(result.is_err(), "truncated %2 should error");
1085    }
1086
1087    #[test]
1088    fn url_decode_malformed_percent_no_digits() {
1089        // % followed by no digits at all
1090        let result = url_decode("abc%");
1091        assert!(result.is_err(), "bare % at end should error");
1092    }
1093
1094    #[test]
1095    fn url_decode_invalid_hex_digit() {
1096        // %GG -- 'G' is not a valid hex digit
1097        let result = url_decode("abc%GG");
1098        assert!(result.is_err(), "%GG should error");
1099    }
1100
1101    #[test]
1102    fn url_decode_invalid_hex_second_digit() {
1103        // %2Z -- 'Z' is not a valid hex digit
1104        let result = url_decode("abc%2Z");
1105        assert!(result.is_err(), "%2Z should error");
1106    }
1107
1108    /// url_decode with invalid UTF-8 from percent-decoded bytes
1109    #[test]
1110    fn url_decode_invalid_utf8_percent() {
1111        // %80%81 are not valid UTF-8 start bytes
1112        let result = url_decode("%80%81");
1113        assert!(result.is_err(), "invalid UTF-8 bytes should error");
1114    }
1115
1116    /// url_decode with percent-encoded chars in all positions
1117    #[test]
1118    fn url_decode_percent_everywhere() {
1119        assert_eq!(url_decode("%41%42%43").unwrap(), "ABC");
1120        assert_eq!(url_decode("%61").unwrap(), "a");
1121        assert_eq!(url_decode("x%2Fy%2Fz").unwrap(), "x/y/z");
1122    }
1123
1124    /// url_decode with bare percent at various positions
1125    #[test]
1126    fn url_decode_bare_percent_middle() {
1127        assert!(url_decode("a%b").is_err(), "bare % in middle should error");
1128    }
1129
1130    /// T-02: url_decode with multi-byte UTF-8 (%C3%A9 -> e with acute)
1131    #[test]
1132    fn url_decode_multibyte_utf8() {
1133        let result = url_decode("caf%C3%A9").unwrap();
1134        assert_eq!(result, "caf\u{00e9}"); // cafe with accent
1135    }
1136
1137    // #73: url_decode with invalid percent (%ZZ)
1138    #[test]
1139    fn url_decode_invalid_percent_zz() {
1140        let result = url_decode("abc%ZZ");
1141        assert!(result.is_err(), "%ZZ should error");
1142    }
1143
1144    // #74: url_decode with truncated percent (trailing %)
1145    #[test]
1146    fn url_decode_truncated_percent_trailing() {
1147        let result = url_decode("abc%");
1148        assert!(result.is_err(), "trailing % should error");
1149    }
1150
1151    // #75: url_decode producing invalid UTF-8
1152    #[test]
1153    fn url_decode_invalid_utf8() {
1154        // 0x80 alone is not valid UTF-8
1155        let result = url_decode("%80");
1156        assert!(result.is_err(), "invalid UTF-8 should error");
1157    }
1158
1159    #[test]
1160    fn url_decode_empty_string() {
1161        assert_eq!(url_decode("").unwrap(), "");
1162    }
1163
1164    #[test]
1165    fn url_decode_no_encoding() {
1166        assert_eq!(url_decode("hello").unwrap(), "hello");
1167    }
1168
1169    #[test]
1170    fn url_decode_all_ascii_hex() {
1171        // Uppercase hex
1172        assert_eq!(url_decode("%2F").unwrap(), "/");
1173        assert_eq!(url_decode("%2f").unwrap(), "/");
1174    }
1175
1176    // --- Config URL edge cases ---
1177
1178    // Unicode password: Cyrillic пароль (percent-encoded)
1179    #[test]
1180    fn config_unicode_password() {
1181        // "пароль" in UTF-8 is D0 BF D0 B0 D1 80 D0 BE D0 BB D1 8C
1182        let cfg =
1183            Config::from_url("postgres://user:%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C@localhost/db")
1184                .unwrap();
1185        assert_eq!(cfg.user, "user");
1186        assert_eq!(
1187            cfg.password,
1188            "\u{043F}\u{0430}\u{0440}\u{043E}\u{043B}\u{044C}"
1189        ); // пароль
1190        assert_eq!(cfg.host, "localhost");
1191        assert_eq!(cfg.database, "db");
1192    }
1193
1194    // Port 0 (edge of u16 range)
1195    #[test]
1196    fn config_port_zero() {
1197        let cfg = Config::from_url("postgres://user:pass@localhost:0/db").unwrap();
1198        assert_eq!(cfg.port, 0);
1199    }
1200
1201    // Port 65535 (max u16)
1202    #[test]
1203    fn config_port_max() {
1204        let cfg = Config::from_url("postgres://user:pass@localhost:65535/db").unwrap();
1205        assert_eq!(cfg.port, 65535);
1206    }
1207
1208    // Port 65536 (overflow, should error)
1209    #[test]
1210    fn config_port_overflow() {
1211        let result = Config::from_url("postgres://user:pass@localhost:65536/db");
1212        assert!(result.is_err(), "port 65536 exceeds u16 max");
1213    }
1214
1215    // Unknown query parameter should be silently ignored
1216    #[test]
1217    fn config_unknown_param_ignored() {
1218        let cfg = Config::from_url(
1219            "postgres://user:pass@localhost/db?application_name=myapp&connect_timeout=10",
1220        )
1221        .unwrap();
1222        // Should parse without error, ignoring unknown params
1223        assert_eq!(cfg.user, "user");
1224        assert_eq!(cfg.host, "localhost");
1225        assert_eq!(cfg.database, "db");
1226        // Default values for known params should be unaffected
1227        assert_eq!(cfg.statement_timeout_secs, 30);
1228        assert_eq!(cfg.ssl, SslMode::Prefer);
1229    }
1230
1231    // Double percent encoding: %2525 should decode to %25
1232    #[test]
1233    fn url_decode_double_percent_encoding() {
1234        // %25 decodes to '%', so %2525 decodes to '%25'
1235        assert_eq!(url_decode("%2525").unwrap(), "%25");
1236    }
1237
1238    // URL with empty password field (explicit colon, empty password)
1239    #[test]
1240    fn config_explicit_empty_password() {
1241        let cfg = Config::from_url("postgres://user:@localhost/db").unwrap();
1242        assert_eq!(cfg.user, "user");
1243        assert_eq!(cfg.password, "");
1244    }
1245
1246    // URL with special characters in user and database
1247    #[test]
1248    fn config_special_chars_in_user() {
1249        let cfg = Config::from_url("postgres://my%2Fuser:pass@localhost/my%2Fdb").unwrap();
1250        assert_eq!(cfg.user, "my/user");
1251        assert_eq!(cfg.database, "my/db");
1252    }
1253
1254    // url_decode with plus sign (should be literal, not space -- this is not form encoding)
1255    #[test]
1256    fn url_decode_plus_is_literal() {
1257        assert_eq!(url_decode("a+b").unwrap(), "a+b");
1258    }
1259
1260    // Config with only host, port, and user (minimal valid URL)
1261    #[test]
1262    fn config_minimal_valid_url() {
1263        let cfg = Config::from_url("postgres://user@localhost/db").unwrap();
1264        assert_eq!(cfg.user, "user");
1265        assert_eq!(cfg.password, "");
1266        assert_eq!(cfg.host, "localhost");
1267        assert_eq!(cfg.port, 5432);
1268        assert_eq!(cfg.database, "db");
1269    }
1270
1271    // Multiple ampersands and empty param segments
1272    #[test]
1273    fn config_empty_param_segments() {
1274        let cfg =
1275            Config::from_url("postgres://user:pass@localhost/db?&&statement_timeout=60&&").unwrap();
1276        assert_eq!(cfg.statement_timeout_secs, 60);
1277    }
1278
1279    // ===================================================================
1280    // hash_sql tests
1281    // ===================================================================
1282
1283    #[test]
1284    fn hash_sql_deterministic() {
1285        let h1 = hash_sql("SELECT 1");
1286        let h2 = hash_sql("SELECT 1");
1287        assert_eq!(h1, h2);
1288    }
1289
1290    #[test]
1291    fn hash_sql_different_queries() {
1292        let h1 = hash_sql("SELECT 1");
1293        let h2 = hash_sql("SELECT 2");
1294        assert_ne!(h1, h2);
1295    }
1296
1297    #[test]
1298    fn hash_sql_empty() {
1299        let _h = hash_sql(""); // should not panic
1300    }
1301
1302    #[test]
1303    fn hash_sql_whitespace_only() {
1304        let h = hash_sql("   ");
1305        assert_ne!(h, hash_sql(""));
1306    }
1307
1308    #[test]
1309    fn hash_sql_very_long() {
1310        let long_sql = "SELECT ".to_string() + &"x".repeat(10_000);
1311        let h = hash_sql(&long_sql);
1312        assert_eq!(h, hash_sql(&long_sql));
1313    }
1314
1315    #[test]
1316    fn hash_sql_unicode() {
1317        let h = hash_sql("SELECT '\u{1F600}'");
1318        assert_ne!(h, hash_sql("SELECT 'x'"));
1319    }
1320
1321    // ===================================================================
1322    // Notification tests
1323    // ===================================================================
1324
1325    #[test]
1326    fn notification_struct_fields() {
1327        let n = Notification {
1328            pid: 42,
1329            channel: "test_chan".to_owned(),
1330            payload: "hello".to_owned(),
1331        };
1332        assert_eq!(n.pid, 42);
1333        assert_eq!(n.channel, "test_chan");
1334        assert_eq!(n.payload, "hello");
1335    }
1336
1337    #[test]
1338    fn notification_clone() {
1339        let n = Notification {
1340            pid: 1,
1341            channel: "c".to_owned(),
1342            payload: "p".to_owned(),
1343        };
1344        let n2 = n.clone();
1345        assert_eq!(n2.pid, 1);
1346        assert_eq!(n2.channel, "c");
1347    }
1348
1349    #[test]
1350    fn notification_debug() {
1351        let n = Notification {
1352            pid: 1,
1353            channel: "c".to_owned(),
1354            payload: "p".to_owned(),
1355        };
1356        let dbg = format!("{n:?}");
1357        assert!(dbg.contains("Notification"));
1358    }
1359
1360    // ===================================================================
1361    // QueryResult tests
1362    // ===================================================================
1363
1364    #[test]
1365    fn query_result_empty() {
1366        let result = QueryResult {
1367            all_col_offsets: vec![],
1368            num_cols: 0,
1369            columns: Arc::from(Vec::new()),
1370            affected_rows: 0,
1371            data_buf: None,
1372        };
1373        assert!(result.is_empty());
1374        assert_eq!(result.len(), 0);
1375    }
1376
1377    #[test]
1378    fn query_result_from_parts() {
1379        let result = QueryResult::from_parts(vec![(0, 4), (0, -1)], 2, Arc::from(Vec::new()), 5);
1380        assert_eq!(result.len(), 1);
1381        assert_eq!(result.num_cols, 2);
1382        assert_eq!(result.affected_rows, 5);
1383    }
1384
1385    #[test]
1386    fn query_result_affected_rows() {
1387        let result = QueryResult {
1388            all_col_offsets: vec![],
1389            num_cols: 0,
1390            columns: Arc::from(Vec::new()),
1391            affected_rows: 42,
1392            data_buf: None,
1393        };
1394        assert_eq!(result.affected_rows, 42);
1395        assert!(result.is_empty());
1396    }
1397
1398    // ===================================================================
1399    // PgDataRow tests
1400    // ===================================================================
1401
1402    /// Build a DataRow payload: [i16 num_cols] ([i32 len] [bytes])...
1403    /// len = -1 for NULL
1404    fn make_data_row(columns: &[Option<&[u8]>]) -> Vec<u8> {
1405        let mut buf = Vec::new();
1406        buf.extend_from_slice(&(columns.len() as i16).to_be_bytes());
1407        for col in columns {
1408            match col {
1409                Some(data) => {
1410                    buf.extend_from_slice(&(data.len() as i32).to_be_bytes());
1411                    buf.extend_from_slice(data);
1412                }
1413                None => {
1414                    buf.extend_from_slice(&(-1i32).to_be_bytes());
1415                }
1416            }
1417        }
1418        buf
1419    }
1420
1421    #[test]
1422    fn pg_data_row_get_i32() {
1423        let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
1424        let row = PgDataRow::new(&data).unwrap();
1425        assert_eq!(row.get_i32(0), Some(42));
1426        assert_eq!(row.column_count(), 1);
1427    }
1428
1429    #[test]
1430    fn pg_data_row_get_i64() {
1431        let data = make_data_row(&[Some(&12345i64.to_be_bytes())]);
1432        let row = PgDataRow::new(&data).unwrap();
1433        assert_eq!(row.get_i64(0), Some(12345));
1434    }
1435
1436    #[test]
1437    fn pg_data_row_get_str() {
1438        let data = make_data_row(&[Some(b"hello")]);
1439        let row = PgDataRow::new(&data).unwrap();
1440        assert_eq!(row.get_str(0), Some("hello"));
1441    }
1442
1443    #[test]
1444    fn pg_data_row_get_bytes() {
1445        let data = make_data_row(&[Some(&[0xDE, 0xAD, 0xBE, 0xEF])]);
1446        let row = PgDataRow::new(&data).unwrap();
1447        assert_eq!(row.get_bytes(0), Some(&[0xDE, 0xAD, 0xBE, 0xEF][..]));
1448    }
1449
1450    #[test]
1451    fn pg_data_row_get_bool() {
1452        let data = make_data_row(&[Some(&[1u8])]);
1453        let row = PgDataRow::new(&data).unwrap();
1454        assert_eq!(row.get_bool(0), Some(true));
1455
1456        let data = make_data_row(&[Some(&[0u8])]);
1457        let row = PgDataRow::new(&data).unwrap();
1458        assert_eq!(row.get_bool(0), Some(false));
1459    }
1460
1461    #[test]
1462    fn pg_data_row_get_f64() {
1463        let data = make_data_row(&[Some(&3.14f64.to_be_bytes())]);
1464        let row = PgDataRow::new(&data).unwrap();
1465        assert!((row.get_f64(0).unwrap() - 3.14).abs() < 1e-10);
1466    }
1467
1468    #[test]
1469    fn pg_data_row_null_column() {
1470        let data = make_data_row(&[None]);
1471        let row = PgDataRow::new(&data).unwrap();
1472        assert!(row.is_null(0));
1473        assert_eq!(row.get_i32(0), None);
1474        assert_eq!(row.get_str(0), None);
1475    }
1476
1477    #[test]
1478    fn pg_data_row_multiple_columns() {
1479        let data = make_data_row(&[
1480            Some(&42i32.to_be_bytes()),
1481            Some(b"alice"),
1482            Some(b"alice@example.com"),
1483            Some(&[1u8]),
1484            Some(&3.14f64.to_be_bytes()),
1485        ]);
1486        let row = PgDataRow::new(&data).unwrap();
1487        assert_eq!(row.column_count(), 5);
1488        assert_eq!(row.get_i32(0), Some(42));
1489        assert_eq!(row.get_str(1), Some("alice"));
1490        assert_eq!(row.get_str(2), Some("alice@example.com"));
1491        assert_eq!(row.get_bool(3), Some(true));
1492        assert!((row.get_f64(4).unwrap() - 3.14).abs() < 1e-10);
1493    }
1494
1495    #[test]
1496    fn pg_data_row_mixed_null() {
1497        let data = make_data_row(&[Some(&42i32.to_be_bytes()), None, Some(b"text")]);
1498        let row = PgDataRow::new(&data).unwrap();
1499        assert_eq!(row.get_i32(0), Some(42));
1500        assert!(row.is_null(1));
1501        assert_eq!(row.get_str(1), None);
1502        assert_eq!(row.get_str(2), Some("text"));
1503    }
1504
1505    #[test]
1506    fn pg_data_row_empty() {
1507        let data = make_data_row(&[]);
1508        let row = PgDataRow::new(&data).unwrap();
1509        assert_eq!(row.column_count(), 0);
1510    }
1511
1512    #[test]
1513    fn pg_data_row_too_short() {
1514        let data = vec![0u8]; // only 1 byte, need at least 2
1515        assert!(PgDataRow::new(&data).is_err());
1516    }
1517
1518    #[test]
1519    fn pg_data_row_truncated() {
1520        // Declare 2 columns but only include 1
1521        let mut data = Vec::new();
1522        data.extend_from_slice(&2i16.to_be_bytes());
1523        data.extend_from_slice(&4i32.to_be_bytes());
1524        data.extend_from_slice(&42i32.to_be_bytes());
1525        // Missing second column
1526        assert!(PgDataRow::new(&data).is_err());
1527    }
1528
1529    #[test]
1530    fn pg_data_row_get_i16() {
1531        let data = make_data_row(&[Some(&7i16.to_be_bytes())]);
1532        let row = PgDataRow::new(&data).unwrap();
1533        assert_eq!(row.get_i16(0), Some(7));
1534    }
1535
1536    #[test]
1537    fn pg_data_row_get_f32() {
1538        let data = make_data_row(&[Some(&2.5f32.to_be_bytes())]);
1539        let row = PgDataRow::new(&data).unwrap();
1540        assert!((row.get_f32(0).unwrap() - 2.5).abs() < 1e-6);
1541    }
1542
1543    #[test]
1544    fn pg_data_row_get_raw_null() {
1545        let data = make_data_row(&[None]);
1546        let row = PgDataRow::new(&data).unwrap();
1547        assert_eq!(row.get_raw(0), None);
1548    }
1549
1550    #[test]
1551    fn pg_data_row_get_raw_data() {
1552        let data = make_data_row(&[Some(&[1, 2, 3])]);
1553        let row = PgDataRow::new(&data).unwrap();
1554        assert_eq!(row.get_raw(0), Some(&[1u8, 2, 3][..]));
1555    }
1556
1557    #[test]
1558    fn pg_data_row_stack_alloc_16_columns() {
1559        // SmallVec<16> should not heap-allocate for <= 16 columns
1560        let cols: Vec<Option<&[u8]>> = (0..16).map(|_| Some(&[0u8][..])).collect();
1561        let data = make_data_row(&cols);
1562        let row = PgDataRow::new(&data).unwrap();
1563        assert_eq!(row.column_count(), 16);
1564        // All columns should be accessible
1565        for i in 0..16 {
1566            assert_eq!(row.get_raw(i), Some(&[0u8][..]));
1567        }
1568    }
1569
1570    // --- Inline sequential decode tests (validates the raw-bytes pattern) ---
1571
1572    /// Validate inline sequential decode of a 5-column DataRow
1573    /// (i32, str, str, bool, f64) -- the same pattern the generated code uses.
1574    #[test]
1575    fn inline_sequential_decode_five_columns() {
1576        let data = make_data_row(&[
1577            Some(&42i32.to_be_bytes()),
1578            Some(b"alice"),
1579            Some(b"alice@example.com"),
1580            Some(&[1u8]),
1581            Some(&3.14f64.to_be_bytes()),
1582        ]);
1583
1584        // Simulate generated inline decode
1585        let mut pos: usize = 2; // skip i16 num_cols
1586
1587        // Column 0: i32
1588        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1589        pos += 4;
1590        assert_eq!(len, 4);
1591        let id = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1592        pos += len as usize;
1593        assert_eq!(id, 42);
1594
1595        // Column 1: str
1596        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1597        pos += 4;
1598        assert_eq!(len, 5);
1599        let name = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1600        pos += len as usize;
1601        assert_eq!(name, "alice");
1602
1603        // Column 2: str
1604        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1605        pos += 4;
1606        let email = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1607        pos += len as usize;
1608        assert_eq!(email, "alice@example.com");
1609
1610        // Column 3: bool
1611        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1612        pos += 4;
1613        assert_eq!(len, 1);
1614        let active = data[pos] != 0;
1615        pos += len as usize;
1616        assert!(active);
1617
1618        // Column 4: f64
1619        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1620        pos += 4;
1621        assert_eq!(len, 8);
1622        let score = f64::from_be_bytes([
1623            data[pos],
1624            data[pos + 1],
1625            data[pos + 2],
1626            data[pos + 3],
1627            data[pos + 4],
1628            data[pos + 5],
1629            data[pos + 6],
1630            data[pos + 7],
1631        ]);
1632        pos += len as usize;
1633        assert!((score - 3.14).abs() < 1e-10);
1634        assert_eq!(pos, data.len());
1635    }
1636
1637    /// Validate inline decode with NULL columns.
1638    #[test]
1639    fn inline_sequential_decode_with_nulls() {
1640        let data = make_data_row(&[
1641            Some(&42i32.to_be_bytes()),
1642            None, // NULL name
1643            Some(b"text"),
1644        ]);
1645
1646        let mut pos: usize = 2;
1647
1648        // Column 0: i32 NOT NULL
1649        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1650        pos += 4;
1651        let id = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1652        pos += len as usize;
1653        assert_eq!(id, 42);
1654
1655        // Column 1: str NULLABLE -> None
1656        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1657        pos += 4;
1658        let name: Option<&str> = if len < 0 {
1659            None
1660        } else {
1661            let s = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1662            pos += len as usize;
1663            Some(s)
1664        };
1665        assert!(name.is_none());
1666
1667        // Column 2: str NOT NULL
1668        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1669        pos += 4;
1670        let txt = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1671        pos += len as usize;
1672        assert_eq!(txt, "text");
1673        assert_eq!(pos, data.len());
1674    }
1675
1676    /// Validate inline decode with all supported scalar types.
1677    #[test]
1678    fn inline_sequential_decode_all_scalar_types() {
1679        let data = make_data_row(&[
1680            Some(&[1u8]),                  // bool
1681            Some(&7i16.to_be_bytes()),     // i16
1682            Some(&42i32.to_be_bytes()),    // i32
1683            Some(&12345i64.to_be_bytes()), // i64
1684            Some(&2.5f32.to_be_bytes()),   // f32
1685            Some(&3.14f64.to_be_bytes()),  // f64
1686        ]);
1687
1688        let mut pos: usize = 2;
1689
1690        // bool
1691        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1692        pos += 4;
1693        let v_bool = data[pos] != 0;
1694        pos += len as usize;
1695        assert!(v_bool);
1696
1697        // i16
1698        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1699        pos += 4;
1700        let v_i16 = i16::from_be_bytes([data[pos], data[pos + 1]]);
1701        pos += len as usize;
1702        assert_eq!(v_i16, 7);
1703
1704        // i32
1705        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1706        pos += 4;
1707        let v_i32 = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1708        pos += len as usize;
1709        assert_eq!(v_i32, 42);
1710
1711        // i64
1712        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1713        pos += 4;
1714        let v_i64 = i64::from_be_bytes([
1715            data[pos],
1716            data[pos + 1],
1717            data[pos + 2],
1718            data[pos + 3],
1719            data[pos + 4],
1720            data[pos + 5],
1721            data[pos + 6],
1722            data[pos + 7],
1723        ]);
1724        pos += len as usize;
1725        assert_eq!(v_i64, 12345);
1726
1727        // f32
1728        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1729        pos += 4;
1730        let v_f32 = f32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1731        pos += len as usize;
1732        assert!((v_f32 - 2.5).abs() < 1e-6);
1733
1734        // f64
1735        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1736        pos += 4;
1737        let v_f64 = f64::from_be_bytes([
1738            data[pos],
1739            data[pos + 1],
1740            data[pos + 2],
1741            data[pos + 3],
1742            data[pos + 4],
1743            data[pos + 5],
1744            data[pos + 6],
1745            data[pos + 7],
1746        ]);
1747        pos += len as usize;
1748        assert!((v_f64 - 3.14).abs() < 1e-10);
1749        assert_eq!(pos, data.len());
1750    }
1751
1752    /// Validate PgDataRow::new is public (callable from external code).
1753    #[test]
1754    fn pg_data_row_new_is_public() {
1755        let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
1756        // This compiles because PgDataRow::new is pub.
1757        let row = PgDataRow::new(&data).unwrap();
1758        assert_eq!(row.get_i32(0), Some(42));
1759    }
1760
1761    /// Inline decode produces identical results to PgDataRow for mixed data.
1762    #[test]
1763    fn inline_decode_matches_pgdatarow() {
1764        let data = make_data_row(&[
1765            Some(&99i32.to_be_bytes()),
1766            Some(b"hello world"),
1767            None,
1768            Some(&[0u8]),
1769            Some(&1.23f64.to_be_bytes()),
1770        ]);
1771
1772        // PgDataRow results
1773        let row = PgDataRow::new(&data).unwrap();
1774        let dr_i32 = row.get_i32(0);
1775        let dr_str = row.get_str(1);
1776        let dr_null = row.get_str(2);
1777        let dr_bool = row.get_bool(3);
1778        let dr_f64 = row.get_f64(4);
1779
1780        // Inline results
1781        let mut pos: usize = 2;
1782
1783        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1784        pos += 4;
1785        let in_i32 = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1786        pos += len as usize;
1787
1788        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1789        pos += 4;
1790        let in_str = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1791        pos += len as usize;
1792
1793        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1794        pos += 4;
1795        let in_null: Option<&str> = if len < 0 { None } else { unreachable!() };
1796
1797        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1798        pos += 4;
1799        let in_bool = data[pos] != 0;
1800        pos += len as usize;
1801
1802        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1803        pos += 4;
1804        let in_f64 = f64::from_be_bytes([
1805            data[pos],
1806            data[pos + 1],
1807            data[pos + 2],
1808            data[pos + 3],
1809            data[pos + 4],
1810            data[pos + 5],
1811            data[pos + 6],
1812            data[pos + 7],
1813        ]);
1814        pos += len as usize;
1815
1816        // Both paths must produce identical results
1817        assert_eq!(dr_i32, Some(in_i32));
1818        assert_eq!(dr_str, Some(in_str));
1819        assert_eq!(dr_null, in_null);
1820        assert_eq!(dr_bool, Some(in_bool));
1821        assert!((dr_f64.unwrap() - in_f64).abs() < 1e-15);
1822        assert_eq!(pos, data.len());
1823    }
1824
1825    // ===================================================================
1826    // PgDataRow -- comprehensive tests
1827    // ===================================================================
1828
1829    #[test]
1830    fn pg_data_row_all_null_columns() {
1831        let data = make_data_row(&[None, None, None, None, None]);
1832        let row = PgDataRow::new(&data).unwrap();
1833        assert_eq!(row.column_count(), 5);
1834        for i in 0..5 {
1835            assert!(row.is_null(i), "column {i} should be null");
1836            assert_eq!(row.get_raw(i), None);
1837            assert_eq!(row.get_i32(i), None);
1838            assert_eq!(row.get_i64(i), None);
1839            assert_eq!(row.get_str(i), None);
1840            assert_eq!(row.get_bool(i), None);
1841            assert_eq!(row.get_f64(i), None);
1842        }
1843    }
1844
1845    #[test]
1846    fn pg_data_row_very_long_text() {
1847        let long_text = "x".repeat(2048);
1848        let data = make_data_row(&[Some(long_text.as_bytes())]);
1849        let row = PgDataRow::new(&data).unwrap();
1850        assert_eq!(row.get_str(0), Some(long_text.as_str()));
1851    }
1852
1853    #[test]
1854    fn pg_data_row_empty_text() {
1855        let data = make_data_row(&[Some(b"")]);
1856        let row = PgDataRow::new(&data).unwrap();
1857        assert!(!row.is_null(0));
1858        assert_eq!(row.get_str(0), Some(""));
1859        assert_eq!(row.get_bytes(0), Some(&[][..]));
1860    }
1861
1862    #[test]
1863    fn pg_data_row_20_columns_exceeds_inline() {
1864        let col_data: Vec<[u8; 4]> = (0..20).map(|i: i32| i.to_be_bytes()).collect();
1865        let cols: Vec<Option<&[u8]>> = col_data.iter().map(|b| Some(b.as_slice())).collect();
1866        let data = make_data_row(&cols);
1867        let row = PgDataRow::new(&data).unwrap();
1868        assert_eq!(row.column_count(), 20);
1869        for i in 0..20 {
1870            assert_eq!(row.get_i32(i), Some(i as i32));
1871        }
1872    }
1873
1874    #[test]
1875    fn pg_data_row_is_null_each_position() {
1876        // 3 columns: data, null, data
1877        let data = make_data_row(&[Some(&1i32.to_be_bytes()), None, Some(&3i32.to_be_bytes())]);
1878        let row = PgDataRow::new(&data).unwrap();
1879        assert!(!row.is_null(0));
1880        assert!(row.is_null(1));
1881        assert!(!row.is_null(2));
1882    }
1883
1884    #[test]
1885    fn pg_data_row_negative_column_count() {
1886        let data = (-1i16).to_be_bytes();
1887        assert!(PgDataRow::new(&data).is_err());
1888    }
1889
1890    #[test]
1891    fn pg_data_row_get_str_invalid_utf8() {
1892        let invalid_utf8 = &[0xFF, 0xFE, 0x80];
1893        let data = make_data_row(&[Some(invalid_utf8)]);
1894        let row = PgDataRow::new(&data).unwrap();
1895        // get_str returns None for invalid UTF-8, but get_bytes returns the raw data
1896        assert_eq!(row.get_str(0), None);
1897        assert_eq!(row.get_bytes(0), Some(&[0xFF, 0xFE, 0x80][..]));
1898    }
1899
1900    #[test]
1901    fn pg_data_row_get_i32_wrong_length() {
1902        // i32 needs exactly 4 bytes; give it 2
1903        let data = make_data_row(&[Some(&7i16.to_be_bytes())]);
1904        let row = PgDataRow::new(&data).unwrap();
1905        assert_eq!(row.get_i32(0), None); // 2 bytes != 4 bytes
1906        assert_eq!(row.get_i16(0), Some(7)); // but i16 works
1907    }
1908
1909    #[test]
1910    fn pg_data_row_get_i64_wrong_length() {
1911        // i64 needs 8 bytes; give it 4
1912        let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
1913        let row = PgDataRow::new(&data).unwrap();
1914        assert_eq!(row.get_i64(0), None);
1915    }
1916
1917    #[test]
1918    fn pg_data_row_get_f64_wrong_length() {
1919        let data = make_data_row(&[Some(&2.5f32.to_be_bytes())]);
1920        let row = PgDataRow::new(&data).unwrap();
1921        assert_eq!(row.get_f64(0), None); // 4 bytes != 8 bytes
1922    }
1923
1924    #[test]
1925    fn pg_data_row_get_f32_wrong_length() {
1926        let data = make_data_row(&[Some(&3.14f64.to_be_bytes())]);
1927        let row = PgDataRow::new(&data).unwrap();
1928        assert_eq!(row.get_f32(0), None); // 8 bytes != 4 bytes
1929    }
1930
1931    #[test]
1932    fn pg_data_row_get_bool_wrong_length() {
1933        // bool needs 1 byte; give it 4
1934        let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
1935        let row = PgDataRow::new(&data).unwrap();
1936        assert_eq!(row.get_bool(0), None);
1937    }
1938
1939    #[test]
1940    fn pg_data_row_unicode_text() {
1941        let texts = [
1942            "\u{1F600}\u{1F4A9}\u{1F680}", // emoji
1943            "\u{4e16}\u{754c}",            // CJK
1944            "\u{0645}\u{0631}\u{062D}",    // Arabic
1945            "\u{1F468}\u{200D}\u{1F469}",  // ZWJ
1946        ];
1947        for text in &texts {
1948            let data = make_data_row(&[Some(text.as_bytes())]);
1949            let row = PgDataRow::new(&data).unwrap();
1950            assert_eq!(row.get_str(0), Some(*text));
1951        }
1952    }
1953
1954    #[test]
1955    fn pg_data_row_i32_boundary_values() {
1956        for &val in &[i32::MIN, -1, 0, 1, i32::MAX] {
1957            let data = make_data_row(&[Some(&val.to_be_bytes())]);
1958            let row = PgDataRow::new(&data).unwrap();
1959            assert_eq!(row.get_i32(0), Some(val), "failed for {val}");
1960        }
1961    }
1962
1963    #[test]
1964    fn pg_data_row_i64_boundary_values() {
1965        for &val in &[i64::MIN, -1, 0, 1, i64::MAX] {
1966            let data = make_data_row(&[Some(&val.to_be_bytes())]);
1967            let row = PgDataRow::new(&data).unwrap();
1968            assert_eq!(row.get_i64(0), Some(val), "failed for {val}");
1969        }
1970    }
1971
1972    #[test]
1973    fn pg_data_row_f64_special_values() {
1974        let data = make_data_row(&[Some(&f64::INFINITY.to_be_bytes())]);
1975        let row = PgDataRow::new(&data).unwrap();
1976        assert_eq!(row.get_f64(0), Some(f64::INFINITY));
1977
1978        let data = make_data_row(&[Some(&f64::NEG_INFINITY.to_be_bytes())]);
1979        let row = PgDataRow::new(&data).unwrap();
1980        assert_eq!(row.get_f64(0), Some(f64::NEG_INFINITY));
1981
1982        let data = make_data_row(&[Some(&f64::NAN.to_be_bytes())]);
1983        let row = PgDataRow::new(&data).unwrap();
1984        assert!(row.get_f64(0).unwrap().is_nan());
1985    }
1986
1987    #[test]
1988    fn pg_data_row_f32_special_values() {
1989        let data = make_data_row(&[Some(&f32::INFINITY.to_be_bytes())]);
1990        let row = PgDataRow::new(&data).unwrap();
1991        assert_eq!(row.get_f32(0), Some(f32::INFINITY));
1992
1993        let data = make_data_row(&[Some(&f32::NAN.to_be_bytes())]);
1994        let row = PgDataRow::new(&data).unwrap();
1995        assert!(row.get_f32(0).unwrap().is_nan());
1996    }
1997
1998    #[test]
1999    fn pg_data_row_i16_boundary_values() {
2000        for &val in &[i16::MIN, -1, 0, 1, i16::MAX] {
2001            let data = make_data_row(&[Some(&val.to_be_bytes())]);
2002            let row = PgDataRow::new(&data).unwrap();
2003            assert_eq!(row.get_i16(0), Some(val));
2004        }
2005    }
2006
2007    mod proptest_fuzz {
2008        use super::*;
2009        use proptest::prelude::*;
2010
2011        proptest! {
2012            #[test]
2013            fn config_from_url_never_panics(url in ".*") {
2014                let _ = Config::from_url(&url);
2015            }
2016
2017            #[test]
2018            fn url_decode_never_panics(s in ".*") {
2019                let _ = url_decode(&s);
2020            }
2021
2022            #[test]
2023            fn pg_data_row_new_never_panics(data in proptest::collection::vec(any::<u8>(), 0..8192)) {
2024                let _ = PgDataRow::new(&data);
2025            }
2026
2027            #[test]
2028            fn hash_sql_never_panics(sql in ".*") {
2029                let _ = hash_sql(&sql);
2030            }
2031        }
2032    }
2033}