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::DriverError;
11use crate::arena::Arena;
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    pub fn get_raw(&self, idx: usize) -> Option<&'a [u8]> {
454        let (offset, len) = self.col_offsets[idx];
455        if len < 0 {
456            None
457        } else if let Some(buf) = self.data {
458            Some(&buf[offset..offset + len as usize])
459        } else {
460            Some(self.arena.get(offset, len as usize))
461        }
462    }
463
464    /// Whether a column is NULL.
465    pub fn is_null(&self, idx: usize) -> bool {
466        self.col_offsets[idx].1 < 0
467    }
468
469    /// Number of columns.
470    pub fn column_count(&self) -> usize {
471        self.col_offsets.len()
472    }
473
474    /// Get a boolean column value. Returns `None` on NULL or decode error.
475    pub fn get_bool(&self, idx: usize) -> Option<bool> {
476        self.get_raw(idx)
477            .and_then(|data| crate::codec::decode_bool(data).ok())
478    }
479
480    /// Get an i16 column value. Returns `None` on NULL or decode error.
481    pub fn get_i16(&self, idx: usize) -> Option<i16> {
482        self.get_raw(idx)
483            .and_then(|data| crate::codec::decode_i16(data).ok())
484    }
485
486    /// Get an i32 column value. Returns `None` on NULL or decode error.
487    pub fn get_i32(&self, idx: usize) -> Option<i32> {
488        self.get_raw(idx)
489            .and_then(|data| crate::codec::decode_i32(data).ok())
490    }
491
492    /// Get an i64 column value. Returns `None` on NULL or decode error.
493    pub fn get_i64(&self, idx: usize) -> Option<i64> {
494        self.get_raw(idx)
495            .and_then(|data| crate::codec::decode_i64(data).ok())
496    }
497
498    /// Get an f32 column value. Returns `None` on NULL or decode error.
499    pub fn get_f32(&self, idx: usize) -> Option<f32> {
500        self.get_raw(idx)
501            .and_then(|data| crate::codec::decode_f32(data).ok())
502    }
503
504    /// Get an f64 column value. Returns `None` on NULL or decode error.
505    pub fn get_f64(&self, idx: usize) -> Option<f64> {
506        self.get_raw(idx)
507            .and_then(|data| crate::codec::decode_f64(data).ok())
508    }
509
510    /// Get a string column value. Returns `None` on NULL or decode error.
511    pub fn get_str(&self, idx: usize) -> Option<&'a str> {
512        self.get_raw(idx)
513            .and_then(|data| crate::codec::decode_str(data).ok())
514    }
515
516    /// Get a byte slice column value.
517    pub fn get_bytes(&self, idx: usize) -> Option<&'a [u8]> {
518        self.get_raw(idx)
519    }
520
521    /// Get the column name by index.
522    pub fn column_name(&self, idx: usize) -> &str {
523        &self.columns[idx].name
524    }
525
526    /// Get the column type OID by index.
527    pub fn column_type_oid(&self, idx: usize) -> u32 {
528        self.columns[idx].type_oid
529    }
530}
531
532// ---------------------------------------------------------------------------
533// PgDataRow (zero-copy row view for for_each)
534// ---------------------------------------------------------------------------
535
536/// A temporary view of a single PostgreSQL DataRow message.
537///
538/// Reads columns directly from the wire buffer -- no arena copy.
539/// Column offsets are pre-computed on construction using a `SmallVec`
540/// that is stack-allocated for up to 16 columns (zero heap allocation
541/// for the common case).
542///
543/// Lifetime `'a` borrows from `Connection::read_buf`.
544pub struct PgDataRow<'a> {
545    data: &'a [u8],
546    /// Pre-scanned `(byte_offset, wire_len)` pairs for each column.
547    /// `wire_len = -1` means NULL.
548    offsets: smallvec::SmallVec<[(usize, i32); 16]>,
549}
550
551impl<'a> PgDataRow<'a> {
552    /// Parse column boundaries from a raw DataRow payload.
553    ///
554    /// `data` is the DataRow message payload (after the 'D' type byte and
555    /// 4-byte length prefix have been stripped by the framing layer).
556    pub fn new(data: &'a [u8]) -> Result<Self, DriverError> {
557        if data.len() < 2 {
558            return Err(DriverError::Protocol("DataRow too short".into()));
559        }
560        let num_cols = i16::from_be_bytes([data[0], data[1]]);
561        if num_cols < 0 {
562            return Err(DriverError::Protocol(
563                "DataRow: negative column count".into(),
564            ));
565        }
566        let num_cols = num_cols as usize;
567        let mut offsets = smallvec::SmallVec::<[(usize, i32); 16]>::with_capacity(num_cols);
568        let mut pos = 2usize;
569        for _ in 0..num_cols {
570            if pos + 4 > data.len() {
571                return Err(DriverError::Protocol("DataRow truncated".into()));
572            }
573            let col_len =
574                i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
575            pos += 4;
576            offsets.push((pos, col_len));
577            if col_len > 0 {
578                pos += col_len as usize;
579            }
580        }
581        Ok(Self { data, offsets })
582    }
583
584    /// Get the raw bytes for a column, or `None` if NULL.
585    #[inline]
586    pub fn get_raw(&self, idx: usize) -> Option<&'a [u8]> {
587        let (offset, len) = self.offsets[idx];
588        if len < 0 {
589            None
590        } else {
591            Some(&self.data[offset..offset + len as usize])
592        }
593    }
594
595    /// Whether a column is NULL.
596    #[inline]
597    pub fn is_null(&self, idx: usize) -> bool {
598        self.offsets[idx].1 < 0
599    }
600
601    /// Number of columns.
602    #[inline]
603    pub fn column_count(&self) -> usize {
604        self.offsets.len()
605    }
606
607    /// Get a boolean column value. Returns `None` on NULL or decode error.
608    #[inline]
609    pub fn get_bool(&self, idx: usize) -> Option<bool> {
610        self.get_raw(idx)
611            .and_then(|data| crate::codec::decode_bool(data).ok())
612    }
613
614    /// Get an i16 column value.
615    #[inline]
616    pub fn get_i16(&self, idx: usize) -> Option<i16> {
617        self.get_raw(idx)
618            .and_then(|data| crate::codec::decode_i16(data).ok())
619    }
620
621    /// Get an i32 column value.
622    #[inline]
623    pub fn get_i32(&self, idx: usize) -> Option<i32> {
624        self.get_raw(idx)
625            .and_then(|data| crate::codec::decode_i32(data).ok())
626    }
627
628    /// Get an i64 column value.
629    #[inline]
630    pub fn get_i64(&self, idx: usize) -> Option<i64> {
631        self.get_raw(idx)
632            .and_then(|data| crate::codec::decode_i64(data).ok())
633    }
634
635    /// Get an f32 column value.
636    #[inline]
637    pub fn get_f32(&self, idx: usize) -> Option<f32> {
638        self.get_raw(idx)
639            .and_then(|data| crate::codec::decode_f32(data).ok())
640    }
641
642    /// Get an f64 column value.
643    #[inline]
644    pub fn get_f64(&self, idx: usize) -> Option<f64> {
645        self.get_raw(idx)
646            .and_then(|data| crate::codec::decode_f64(data).ok())
647    }
648
649    /// Get a string column value (zero-copy borrow from the wire buffer).
650    #[inline]
651    pub fn get_str(&self, idx: usize) -> Option<&'a str> {
652        self.get_raw(idx)
653            .and_then(|data| crate::codec::decode_str(data).ok())
654    }
655
656    /// Get a byte slice column value (zero-copy borrow from the wire buffer).
657    #[inline]
658    pub fn get_bytes(&self, idx: usize) -> Option<&'a [u8]> {
659        self.get_raw(idx)
660    }
661}
662
663// ---------------------------------------------------------------------------
664// hash_sql
665// ---------------------------------------------------------------------------
666
667/// Compute a rapidhash of a SQL string.
668///
669/// Uses `str::hash()` via the `Hash` trait, matching `bsql_core::rapid_hash_str`.
670pub fn hash_sql(sql: &str) -> u64 {
671    use std::hash::{Hash, Hasher};
672    let mut hasher = RapidHasher::default();
673    sql.hash(&mut hasher);
674    hasher.finish()
675}
676
677// ---------------------------------------------------------------------------
678// Tests
679// ---------------------------------------------------------------------------
680
681#[cfg(test)]
682#[allow(clippy::approx_constant)]
683mod tests {
684    use super::*;
685
686    // ===================================================================
687    // Config tests
688    // ===================================================================
689
690    #[test]
691    fn config_parse_full_url() {
692        let cfg = Config::from_url("postgres://user:pass@localhost:5432/mydb").unwrap();
693        assert_eq!(cfg.user, "user");
694        assert_eq!(cfg.password, "pass");
695        assert_eq!(cfg.host, "localhost");
696        assert_eq!(cfg.port, 5432);
697        assert_eq!(cfg.database, "mydb");
698    }
699
700    #[test]
701    fn config_parse_default_port() {
702        let cfg = Config::from_url("postgres://user:pass@localhost/mydb").unwrap();
703        assert_eq!(cfg.port, 5432);
704    }
705
706    #[test]
707    fn config_parse_no_password() {
708        let cfg = Config::from_url("postgres://user@localhost/mydb").unwrap();
709        assert_eq!(cfg.user, "user");
710        assert_eq!(cfg.password, "");
711    }
712
713    #[test]
714    fn config_parse_empty_database() {
715        let cfg = Config::from_url("postgres://user:pass@localhost").unwrap();
716        // database defaults to user
717        assert_eq!(cfg.database, "user");
718    }
719
720    #[test]
721    fn config_parse_sslmode() {
722        let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=require").unwrap();
723        assert_eq!(cfg.ssl, SslMode::Require);
724    }
725
726    #[test]
727    fn config_parse_percent_encoding() {
728        let cfg = Config::from_url("postgres://user%40domain:p%40ss@localhost/db").unwrap();
729        assert_eq!(cfg.user, "user@domain");
730        assert_eq!(cfg.password, "p@ss");
731    }
732
733    #[test]
734    fn config_rejects_bad_scheme() {
735        let result = Config::from_url("mysql://user:pass@localhost/db");
736        assert!(result.is_err());
737    }
738
739    /// Unknown sslmode should error, not silently default to Prefer.
740    #[test]
741    fn config_rejects_unknown_sslmode() {
742        let result = Config::from_url("postgres://user:pass@localhost/db?sslmode=requre");
743        assert!(result.is_err(), "typo 'requre' should be rejected");
744        let result = Config::from_url("postgres://user:pass@localhost/db?sslmode=REQUIRE");
745        assert!(result.is_err(), "uppercase should be rejected");
746        let result = Config::from_url("postgres://user:pass@localhost/db?sslmode=bogus");
747        assert!(result.is_err(), "bogus value should be rejected");
748    }
749
750    /// Valid sslmodes should still work.
751    #[test]
752    fn config_accepts_valid_sslmodes() {
753        let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=disable").unwrap();
754        assert_eq!(cfg.ssl, SslMode::Disable);
755        let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=prefer").unwrap();
756        assert_eq!(cfg.ssl, SslMode::Prefer);
757        let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=require").unwrap();
758        assert_eq!(cfg.ssl, SslMode::Require);
759    }
760
761    // #68: Config with postgresql:// scheme
762    #[test]
763    fn config_parse_postgresql_scheme() {
764        let cfg = Config::from_url("postgresql://user:pass@localhost:5432/mydb").unwrap();
765        assert_eq!(cfg.user, "user");
766        assert_eq!(cfg.password, "pass");
767        assert_eq!(cfg.host, "localhost");
768        assert_eq!(cfg.port, 5432);
769        assert_eq!(cfg.database, "mydb");
770    }
771
772    // #69: Config URL without password
773    #[test]
774    fn config_parse_no_password_standalone() {
775        let cfg = Config::from_url("postgres://admin@db.example.com/myapp").unwrap();
776        assert_eq!(cfg.user, "admin");
777        assert_eq!(cfg.password, "");
778        assert_eq!(cfg.host, "db.example.com");
779        assert_eq!(cfg.database, "myapp");
780    }
781
782    // #70: Config URL with empty database (falls back to user)
783    #[test]
784    fn config_empty_database_falls_back_to_user() {
785        let cfg = Config::from_url("postgres://testuser:pass@localhost").unwrap();
786        assert_eq!(cfg.database, "testuser");
787    }
788
789    // #71: Config URL with unknown sslmode error
790    #[test]
791    fn config_unknown_sslmode_error() {
792        let result = Config::from_url("postgres://u:p@h/d?sslmode=verify-full");
793        assert!(result.is_err());
794        let err = result.unwrap_err().to_string();
795        assert!(
796            err.contains("unknown sslmode"),
797            "should describe unknown sslmode: {err}"
798        );
799    }
800
801    // #72: Config URL with multiple query params
802    #[test]
803    fn config_multiple_query_params() {
804        let cfg = Config::from_url(
805            "postgres://user:pass@localhost/db?sslmode=disable&statement_timeout=60",
806        )
807        .unwrap();
808        assert_eq!(cfg.ssl, SslMode::Disable);
809        assert_eq!(cfg.statement_timeout_secs, 60);
810    }
811
812    // Config validation: empty host
813    #[test]
814    fn config_validate_empty_host() {
815        let cfg = Config {
816            host: String::new(),
817            port: 5432,
818            user: "user".into(),
819            password: "pass".into(),
820            database: "db".into(),
821            ssl: SslMode::Disable,
822            statement_timeout_secs: 30,
823        };
824        assert!(cfg.validate().is_err());
825    }
826
827    // Config validation: empty user
828    #[test]
829    fn config_validate_empty_user() {
830        let cfg = Config {
831            host: "localhost".into(),
832            port: 5432,
833            user: String::new(),
834            password: "pass".into(),
835            database: "db".into(),
836            ssl: SslMode::Disable,
837            statement_timeout_secs: 30,
838        };
839        assert!(cfg.validate().is_err());
840    }
841
842    // Config validation: empty database
843    #[test]
844    fn config_validate_empty_database() {
845        let cfg = Config {
846            host: "localhost".into(),
847            port: 5432,
848            user: "user".into(),
849            password: "pass".into(),
850            database: String::new(),
851            ssl: SslMode::Disable,
852            statement_timeout_secs: 30,
853        };
854        assert!(cfg.validate().is_err());
855    }
856
857    // Config missing @ in URL
858    #[test]
859    fn config_missing_at_sign() {
860        let result = Config::from_url("postgres://userpasslocalhost/db");
861        assert!(result.is_err());
862    }
863
864    // Config with custom port
865    #[test]
866    fn config_custom_port() {
867        let cfg = Config::from_url("postgres://user:pass@localhost:5433/db").unwrap();
868        assert_eq!(cfg.port, 5433);
869    }
870
871    // Config with invalid port
872    #[test]
873    fn config_invalid_port() {
874        let result = Config::from_url("postgres://user:pass@localhost:notaport/db");
875        assert!(result.is_err());
876    }
877
878    // #76: Config SslMode::Require without tls feature
879    #[cfg(not(feature = "tls"))]
880    #[test]
881    fn config_sslmode_require_without_tls_feature() {
882        // The config parses fine, but validate doesn't check this.
883        // The error occurs at connection time. Just verify parsing works.
884        let cfg = Config::from_url("postgres://user:pass@localhost/db?sslmode=require").unwrap();
885        assert_eq!(cfg.ssl, SslMode::Require);
886    }
887
888    #[test]
889    fn config_statement_timeout_default() {
890        let cfg = Config::from_url("postgres://user:pass@localhost/db").unwrap();
891        assert_eq!(cfg.statement_timeout_secs, 30);
892    }
893
894    #[test]
895    fn config_statement_timeout_custom() {
896        let cfg =
897            Config::from_url("postgres://user:pass@localhost/db?statement_timeout=120").unwrap();
898        assert_eq!(cfg.statement_timeout_secs, 120);
899    }
900
901    #[test]
902    fn config_statement_timeout_zero() {
903        let cfg =
904            Config::from_url("postgres://user:pass@localhost/db?statement_timeout=0").unwrap();
905        assert_eq!(cfg.statement_timeout_secs, 0);
906    }
907
908    #[test]
909    fn config_statement_timeout_invalid_falls_back() {
910        let cfg =
911            Config::from_url("postgres://user:pass@localhost/db?statement_timeout=notanumber")
912                .unwrap();
913        assert_eq!(cfg.statement_timeout_secs, 30); // fallback
914    }
915
916    #[test]
917    fn config_uds_path_format() {
918        let cfg = Config::from_url("postgres://user@localhost/db?host=/tmp").unwrap();
919        assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5432");
920    }
921
922    #[test]
923    fn config_uds_path_custom_port() {
924        let cfg = Config::from_url("postgres://user@localhost:5433/db?host=/tmp").unwrap();
925        assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5433");
926    }
927
928    // ===================================================================
929    // UDS (Unix domain socket) tests
930    // ===================================================================
931
932    #[test]
933    fn config_host_is_uds_absolute_path() {
934        let cfg = Config {
935            host: "/tmp".into(),
936            port: 5432,
937            user: "user".into(),
938            password: "".into(),
939            database: "db".into(),
940            ssl: SslMode::Disable,
941            statement_timeout_secs: 30,
942        };
943        assert!(cfg.host_is_uds());
944        assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5432");
945    }
946
947    #[test]
948    fn config_host_is_uds_var_run() {
949        let cfg = Config {
950            host: "/var/run/postgresql".into(),
951            port: 5433,
952            user: "user".into(),
953            password: "".into(),
954            database: "db".into(),
955            ssl: SslMode::Disable,
956            statement_timeout_secs: 30,
957        };
958        assert!(cfg.host_is_uds());
959        assert_eq!(cfg.uds_path(), "/var/run/postgresql/.s.PGSQL.5433");
960    }
961
962    #[test]
963    fn config_host_is_not_uds_for_hostname() {
964        let cfg = Config {
965            host: "localhost".into(),
966            port: 5432,
967            user: "user".into(),
968            password: "".into(),
969            database: "db".into(),
970            ssl: SslMode::Disable,
971            statement_timeout_secs: 30,
972        };
973        assert!(!cfg.host_is_uds());
974    }
975
976    #[test]
977    fn config_host_is_not_uds_for_ip() {
978        let cfg = Config {
979            host: "127.0.0.1".into(),
980            port: 5432,
981            user: "user".into(),
982            password: "".into(),
983            database: "db".into(),
984            ssl: SslMode::Disable,
985            statement_timeout_secs: 30,
986        };
987        assert!(!cfg.host_is_uds());
988    }
989
990    #[test]
991    fn config_parse_uds_host_query_param() {
992        let cfg = Config::from_url("postgres://user@localhost/mydb?host=/tmp").unwrap();
993        assert_eq!(cfg.host, "/tmp");
994        assert!(cfg.host_is_uds());
995        assert_eq!(cfg.uds_path(), "/tmp/.s.PGSQL.5432");
996        assert_eq!(cfg.database, "mydb");
997        assert_eq!(cfg.user, "user");
998    }
999
1000    #[test]
1001    fn config_parse_uds_host_query_param_custom_port() {
1002        let cfg = Config::from_url("postgres://user@localhost:5433/mydb?host=/var/run/postgresql")
1003            .unwrap();
1004        assert_eq!(cfg.host, "/var/run/postgresql");
1005        assert_eq!(cfg.port, 5433);
1006        assert_eq!(cfg.uds_path(), "/var/run/postgresql/.s.PGSQL.5433");
1007    }
1008
1009    #[test]
1010    fn config_parse_uds_host_with_other_params() {
1011        let cfg = Config::from_url(
1012            "postgres://user@localhost/db?host=/tmp&sslmode=disable&statement_timeout=60",
1013        )
1014        .unwrap();
1015        assert_eq!(cfg.host, "/tmp");
1016        assert!(cfg.host_is_uds());
1017        assert_eq!(cfg.ssl, SslMode::Disable);
1018        assert_eq!(cfg.statement_timeout_secs, 60);
1019    }
1020
1021    #[test]
1022    fn config_parse_uds_host_percent_encoded() {
1023        // %2F = '/'
1024        let cfg = Config::from_url("postgres://user@localhost/db?host=%2Ftmp").unwrap();
1025        assert_eq!(cfg.host, "/tmp");
1026        assert!(cfg.host_is_uds());
1027    }
1028
1029    #[test]
1030    fn config_parse_tcp_host_not_overridden_without_param() {
1031        // No ?host= param: hostname from URL is used (TCP)
1032        let cfg = Config::from_url("postgres://user@myserver/db").unwrap();
1033        assert_eq!(cfg.host, "myserver");
1034        assert!(!cfg.host_is_uds());
1035    }
1036
1037    #[test]
1038    fn config_parse_uds_host_overrides_url_hostname() {
1039        // ?host= overrides even an explicit hostname
1040        let cfg = Config::from_url("postgres://user@db.example.com/mydb?host=/var/run/postgresql")
1041            .unwrap();
1042        assert_eq!(cfg.host, "/var/run/postgresql");
1043        assert!(cfg.host_is_uds());
1044    }
1045
1046    #[test]
1047    fn config_parse_uds_empty_url_host() {
1048        // postgres:///dbname?host=/tmp -- empty hostname before /, host from param
1049        let cfg = Config::from_url("postgres://user@/mydb?host=/tmp").unwrap();
1050        assert_eq!(cfg.host, "/tmp");
1051        assert!(cfg.host_is_uds());
1052        assert_eq!(cfg.database, "mydb");
1053    }
1054
1055    // ===================================================================
1056    // url_decode tests
1057    // ===================================================================
1058
1059    #[test]
1060    fn url_decode_works() {
1061        assert_eq!(url_decode("hello%20world").unwrap(), "hello world");
1062        assert_eq!(url_decode("no%20escape").unwrap(), "no escape");
1063        assert_eq!(url_decode("plain").unwrap(), "plain");
1064        assert_eq!(url_decode("a%40b").unwrap(), "a@b");
1065    }
1066
1067    #[test]
1068    fn url_decode_malformed_percent_trailing() {
1069        // Truncated percent sequence at end of string
1070        let result = url_decode("abc%2");
1071        assert!(result.is_err(), "truncated %2 should error");
1072    }
1073
1074    #[test]
1075    fn url_decode_malformed_percent_no_digits() {
1076        // % followed by no digits at all
1077        let result = url_decode("abc%");
1078        assert!(result.is_err(), "bare % at end should error");
1079    }
1080
1081    #[test]
1082    fn url_decode_invalid_hex_digit() {
1083        // %GG -- 'G' is not a valid hex digit
1084        let result = url_decode("abc%GG");
1085        assert!(result.is_err(), "%GG should error");
1086    }
1087
1088    #[test]
1089    fn url_decode_invalid_hex_second_digit() {
1090        // %2Z -- 'Z' is not a valid hex digit
1091        let result = url_decode("abc%2Z");
1092        assert!(result.is_err(), "%2Z should error");
1093    }
1094
1095    /// url_decode with invalid UTF-8 from percent-decoded bytes
1096    #[test]
1097    fn url_decode_invalid_utf8_percent() {
1098        // %80%81 are not valid UTF-8 start bytes
1099        let result = url_decode("%80%81");
1100        assert!(result.is_err(), "invalid UTF-8 bytes should error");
1101    }
1102
1103    /// url_decode with percent-encoded chars in all positions
1104    #[test]
1105    fn url_decode_percent_everywhere() {
1106        assert_eq!(url_decode("%41%42%43").unwrap(), "ABC");
1107        assert_eq!(url_decode("%61").unwrap(), "a");
1108        assert_eq!(url_decode("x%2Fy%2Fz").unwrap(), "x/y/z");
1109    }
1110
1111    /// url_decode with bare percent at various positions
1112    #[test]
1113    fn url_decode_bare_percent_middle() {
1114        assert!(url_decode("a%b").is_err(), "bare % in middle should error");
1115    }
1116
1117    /// T-02: url_decode with multi-byte UTF-8 (%C3%A9 -> e with acute)
1118    #[test]
1119    fn url_decode_multibyte_utf8() {
1120        let result = url_decode("caf%C3%A9").unwrap();
1121        assert_eq!(result, "caf\u{00e9}"); // cafe with accent
1122    }
1123
1124    // #73: url_decode with invalid percent (%ZZ)
1125    #[test]
1126    fn url_decode_invalid_percent_zz() {
1127        let result = url_decode("abc%ZZ");
1128        assert!(result.is_err(), "%ZZ should error");
1129    }
1130
1131    // #74: url_decode with truncated percent (trailing %)
1132    #[test]
1133    fn url_decode_truncated_percent_trailing() {
1134        let result = url_decode("abc%");
1135        assert!(result.is_err(), "trailing % should error");
1136    }
1137
1138    // #75: url_decode producing invalid UTF-8
1139    #[test]
1140    fn url_decode_invalid_utf8() {
1141        // 0x80 alone is not valid UTF-8
1142        let result = url_decode("%80");
1143        assert!(result.is_err(), "invalid UTF-8 should error");
1144    }
1145
1146    #[test]
1147    fn url_decode_empty_string() {
1148        assert_eq!(url_decode("").unwrap(), "");
1149    }
1150
1151    #[test]
1152    fn url_decode_no_encoding() {
1153        assert_eq!(url_decode("hello").unwrap(), "hello");
1154    }
1155
1156    #[test]
1157    fn url_decode_all_ascii_hex() {
1158        // Uppercase hex
1159        assert_eq!(url_decode("%2F").unwrap(), "/");
1160        assert_eq!(url_decode("%2f").unwrap(), "/");
1161    }
1162
1163    // --- Config URL edge cases ---
1164
1165    // Unicode password: Cyrillic пароль (percent-encoded)
1166    #[test]
1167    fn config_unicode_password() {
1168        // "пароль" in UTF-8 is D0 BF D0 B0 D1 80 D0 BE D0 BB D1 8C
1169        let cfg =
1170            Config::from_url("postgres://user:%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C@localhost/db")
1171                .unwrap();
1172        assert_eq!(cfg.user, "user");
1173        assert_eq!(
1174            cfg.password,
1175            "\u{043F}\u{0430}\u{0440}\u{043E}\u{043B}\u{044C}"
1176        ); // пароль
1177        assert_eq!(cfg.host, "localhost");
1178        assert_eq!(cfg.database, "db");
1179    }
1180
1181    // Port 0 (edge of u16 range)
1182    #[test]
1183    fn config_port_zero() {
1184        let cfg = Config::from_url("postgres://user:pass@localhost:0/db").unwrap();
1185        assert_eq!(cfg.port, 0);
1186    }
1187
1188    // Port 65535 (max u16)
1189    #[test]
1190    fn config_port_max() {
1191        let cfg = Config::from_url("postgres://user:pass@localhost:65535/db").unwrap();
1192        assert_eq!(cfg.port, 65535);
1193    }
1194
1195    // Port 65536 (overflow, should error)
1196    #[test]
1197    fn config_port_overflow() {
1198        let result = Config::from_url("postgres://user:pass@localhost:65536/db");
1199        assert!(result.is_err(), "port 65536 exceeds u16 max");
1200    }
1201
1202    // Unknown query parameter should be silently ignored
1203    #[test]
1204    fn config_unknown_param_ignored() {
1205        let cfg = Config::from_url(
1206            "postgres://user:pass@localhost/db?application_name=myapp&connect_timeout=10",
1207        )
1208        .unwrap();
1209        // Should parse without error, ignoring unknown params
1210        assert_eq!(cfg.user, "user");
1211        assert_eq!(cfg.host, "localhost");
1212        assert_eq!(cfg.database, "db");
1213        // Default values for known params should be unaffected
1214        assert_eq!(cfg.statement_timeout_secs, 30);
1215        assert_eq!(cfg.ssl, SslMode::Prefer);
1216    }
1217
1218    // Double percent encoding: %2525 should decode to %25
1219    #[test]
1220    fn url_decode_double_percent_encoding() {
1221        // %25 decodes to '%', so %2525 decodes to '%25'
1222        assert_eq!(url_decode("%2525").unwrap(), "%25");
1223    }
1224
1225    // URL with empty password field (explicit colon, empty password)
1226    #[test]
1227    fn config_explicit_empty_password() {
1228        let cfg = Config::from_url("postgres://user:@localhost/db").unwrap();
1229        assert_eq!(cfg.user, "user");
1230        assert_eq!(cfg.password, "");
1231    }
1232
1233    // URL with special characters in user and database
1234    #[test]
1235    fn config_special_chars_in_user() {
1236        let cfg = Config::from_url("postgres://my%2Fuser:pass@localhost/my%2Fdb").unwrap();
1237        assert_eq!(cfg.user, "my/user");
1238        assert_eq!(cfg.database, "my/db");
1239    }
1240
1241    // url_decode with plus sign (should be literal, not space -- this is not form encoding)
1242    #[test]
1243    fn url_decode_plus_is_literal() {
1244        assert_eq!(url_decode("a+b").unwrap(), "a+b");
1245    }
1246
1247    // Config with only host, port, and user (minimal valid URL)
1248    #[test]
1249    fn config_minimal_valid_url() {
1250        let cfg = Config::from_url("postgres://user@localhost/db").unwrap();
1251        assert_eq!(cfg.user, "user");
1252        assert_eq!(cfg.password, "");
1253        assert_eq!(cfg.host, "localhost");
1254        assert_eq!(cfg.port, 5432);
1255        assert_eq!(cfg.database, "db");
1256    }
1257
1258    // Multiple ampersands and empty param segments
1259    #[test]
1260    fn config_empty_param_segments() {
1261        let cfg =
1262            Config::from_url("postgres://user:pass@localhost/db?&&statement_timeout=60&&").unwrap();
1263        assert_eq!(cfg.statement_timeout_secs, 60);
1264    }
1265
1266    // ===================================================================
1267    // hash_sql tests
1268    // ===================================================================
1269
1270    #[test]
1271    fn hash_sql_deterministic() {
1272        let h1 = hash_sql("SELECT 1");
1273        let h2 = hash_sql("SELECT 1");
1274        assert_eq!(h1, h2);
1275    }
1276
1277    #[test]
1278    fn hash_sql_different_queries() {
1279        let h1 = hash_sql("SELECT 1");
1280        let h2 = hash_sql("SELECT 2");
1281        assert_ne!(h1, h2);
1282    }
1283
1284    #[test]
1285    fn hash_sql_empty() {
1286        let _h = hash_sql(""); // should not panic
1287    }
1288
1289    #[test]
1290    fn hash_sql_whitespace_only() {
1291        let h = hash_sql("   ");
1292        assert_ne!(h, hash_sql(""));
1293    }
1294
1295    #[test]
1296    fn hash_sql_very_long() {
1297        let long_sql = "SELECT ".to_string() + &"x".repeat(10_000);
1298        let h = hash_sql(&long_sql);
1299        assert_eq!(h, hash_sql(&long_sql));
1300    }
1301
1302    #[test]
1303    fn hash_sql_unicode() {
1304        let h = hash_sql("SELECT '\u{1F600}'");
1305        assert_ne!(h, hash_sql("SELECT 'x'"));
1306    }
1307
1308    // ===================================================================
1309    // Notification tests
1310    // ===================================================================
1311
1312    #[test]
1313    fn notification_struct_fields() {
1314        let n = Notification {
1315            pid: 42,
1316            channel: "test_chan".to_owned(),
1317            payload: "hello".to_owned(),
1318        };
1319        assert_eq!(n.pid, 42);
1320        assert_eq!(n.channel, "test_chan");
1321        assert_eq!(n.payload, "hello");
1322    }
1323
1324    #[test]
1325    fn notification_clone() {
1326        let n = Notification {
1327            pid: 1,
1328            channel: "c".to_owned(),
1329            payload: "p".to_owned(),
1330        };
1331        let n2 = n.clone();
1332        assert_eq!(n2.pid, 1);
1333        assert_eq!(n2.channel, "c");
1334    }
1335
1336    #[test]
1337    fn notification_debug() {
1338        let n = Notification {
1339            pid: 1,
1340            channel: "c".to_owned(),
1341            payload: "p".to_owned(),
1342        };
1343        let dbg = format!("{n:?}");
1344        assert!(dbg.contains("Notification"));
1345    }
1346
1347    // ===================================================================
1348    // QueryResult tests
1349    // ===================================================================
1350
1351    #[test]
1352    fn query_result_empty() {
1353        let result = QueryResult {
1354            all_col_offsets: vec![],
1355            num_cols: 0,
1356            columns: Arc::from(Vec::new()),
1357            affected_rows: 0,
1358            data_buf: None,
1359        };
1360        assert!(result.is_empty());
1361        assert_eq!(result.len(), 0);
1362    }
1363
1364    #[test]
1365    fn query_result_from_parts() {
1366        let result = QueryResult::from_parts(vec![(0, 4), (0, -1)], 2, Arc::from(Vec::new()), 5);
1367        assert_eq!(result.len(), 1);
1368        assert_eq!(result.num_cols, 2);
1369        assert_eq!(result.affected_rows, 5);
1370    }
1371
1372    #[test]
1373    fn query_result_affected_rows() {
1374        let result = QueryResult {
1375            all_col_offsets: vec![],
1376            num_cols: 0,
1377            columns: Arc::from(Vec::new()),
1378            affected_rows: 42,
1379            data_buf: None,
1380        };
1381        assert_eq!(result.affected_rows, 42);
1382        assert!(result.is_empty());
1383    }
1384
1385    // ===================================================================
1386    // PgDataRow tests
1387    // ===================================================================
1388
1389    /// Build a DataRow payload: [i16 num_cols] ([i32 len] [bytes])...
1390    /// len = -1 for NULL
1391    fn make_data_row(columns: &[Option<&[u8]>]) -> Vec<u8> {
1392        let mut buf = Vec::new();
1393        buf.extend_from_slice(&(columns.len() as i16).to_be_bytes());
1394        for col in columns {
1395            match col {
1396                Some(data) => {
1397                    buf.extend_from_slice(&(data.len() as i32).to_be_bytes());
1398                    buf.extend_from_slice(data);
1399                }
1400                None => {
1401                    buf.extend_from_slice(&(-1i32).to_be_bytes());
1402                }
1403            }
1404        }
1405        buf
1406    }
1407
1408    #[test]
1409    fn pg_data_row_get_i32() {
1410        let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
1411        let row = PgDataRow::new(&data).unwrap();
1412        assert_eq!(row.get_i32(0), Some(42));
1413        assert_eq!(row.column_count(), 1);
1414    }
1415
1416    #[test]
1417    fn pg_data_row_get_i64() {
1418        let data = make_data_row(&[Some(&12345i64.to_be_bytes())]);
1419        let row = PgDataRow::new(&data).unwrap();
1420        assert_eq!(row.get_i64(0), Some(12345));
1421    }
1422
1423    #[test]
1424    fn pg_data_row_get_str() {
1425        let data = make_data_row(&[Some(b"hello")]);
1426        let row = PgDataRow::new(&data).unwrap();
1427        assert_eq!(row.get_str(0), Some("hello"));
1428    }
1429
1430    #[test]
1431    fn pg_data_row_get_bytes() {
1432        let data = make_data_row(&[Some(&[0xDE, 0xAD, 0xBE, 0xEF])]);
1433        let row = PgDataRow::new(&data).unwrap();
1434        assert_eq!(row.get_bytes(0), Some(&[0xDE, 0xAD, 0xBE, 0xEF][..]));
1435    }
1436
1437    #[test]
1438    fn pg_data_row_get_bool() {
1439        let data = make_data_row(&[Some(&[1u8])]);
1440        let row = PgDataRow::new(&data).unwrap();
1441        assert_eq!(row.get_bool(0), Some(true));
1442
1443        let data = make_data_row(&[Some(&[0u8])]);
1444        let row = PgDataRow::new(&data).unwrap();
1445        assert_eq!(row.get_bool(0), Some(false));
1446    }
1447
1448    #[test]
1449    fn pg_data_row_get_f64() {
1450        let data = make_data_row(&[Some(&3.14f64.to_be_bytes())]);
1451        let row = PgDataRow::new(&data).unwrap();
1452        assert!((row.get_f64(0).unwrap() - 3.14).abs() < 1e-10);
1453    }
1454
1455    #[test]
1456    fn pg_data_row_null_column() {
1457        let data = make_data_row(&[None]);
1458        let row = PgDataRow::new(&data).unwrap();
1459        assert!(row.is_null(0));
1460        assert_eq!(row.get_i32(0), None);
1461        assert_eq!(row.get_str(0), None);
1462    }
1463
1464    #[test]
1465    fn pg_data_row_multiple_columns() {
1466        let data = make_data_row(&[
1467            Some(&42i32.to_be_bytes()),
1468            Some(b"alice"),
1469            Some(b"alice@example.com"),
1470            Some(&[1u8]),
1471            Some(&3.14f64.to_be_bytes()),
1472        ]);
1473        let row = PgDataRow::new(&data).unwrap();
1474        assert_eq!(row.column_count(), 5);
1475        assert_eq!(row.get_i32(0), Some(42));
1476        assert_eq!(row.get_str(1), Some("alice"));
1477        assert_eq!(row.get_str(2), Some("alice@example.com"));
1478        assert_eq!(row.get_bool(3), Some(true));
1479        assert!((row.get_f64(4).unwrap() - 3.14).abs() < 1e-10);
1480    }
1481
1482    #[test]
1483    fn pg_data_row_mixed_null() {
1484        let data = make_data_row(&[Some(&42i32.to_be_bytes()), None, Some(b"text")]);
1485        let row = PgDataRow::new(&data).unwrap();
1486        assert_eq!(row.get_i32(0), Some(42));
1487        assert!(row.is_null(1));
1488        assert_eq!(row.get_str(1), None);
1489        assert_eq!(row.get_str(2), Some("text"));
1490    }
1491
1492    #[test]
1493    fn pg_data_row_empty() {
1494        let data = make_data_row(&[]);
1495        let row = PgDataRow::new(&data).unwrap();
1496        assert_eq!(row.column_count(), 0);
1497    }
1498
1499    #[test]
1500    fn pg_data_row_too_short() {
1501        let data = vec![0u8]; // only 1 byte, need at least 2
1502        assert!(PgDataRow::new(&data).is_err());
1503    }
1504
1505    #[test]
1506    fn pg_data_row_truncated() {
1507        // Declare 2 columns but only include 1
1508        let mut data = Vec::new();
1509        data.extend_from_slice(&2i16.to_be_bytes());
1510        data.extend_from_slice(&4i32.to_be_bytes());
1511        data.extend_from_slice(&42i32.to_be_bytes());
1512        // Missing second column
1513        assert!(PgDataRow::new(&data).is_err());
1514    }
1515
1516    #[test]
1517    fn pg_data_row_get_i16() {
1518        let data = make_data_row(&[Some(&7i16.to_be_bytes())]);
1519        let row = PgDataRow::new(&data).unwrap();
1520        assert_eq!(row.get_i16(0), Some(7));
1521    }
1522
1523    #[test]
1524    fn pg_data_row_get_f32() {
1525        let data = make_data_row(&[Some(&2.5f32.to_be_bytes())]);
1526        let row = PgDataRow::new(&data).unwrap();
1527        assert!((row.get_f32(0).unwrap() - 2.5).abs() < 1e-6);
1528    }
1529
1530    #[test]
1531    fn pg_data_row_get_raw_null() {
1532        let data = make_data_row(&[None]);
1533        let row = PgDataRow::new(&data).unwrap();
1534        assert_eq!(row.get_raw(0), None);
1535    }
1536
1537    #[test]
1538    fn pg_data_row_get_raw_data() {
1539        let data = make_data_row(&[Some(&[1, 2, 3])]);
1540        let row = PgDataRow::new(&data).unwrap();
1541        assert_eq!(row.get_raw(0), Some(&[1u8, 2, 3][..]));
1542    }
1543
1544    #[test]
1545    fn pg_data_row_stack_alloc_16_columns() {
1546        // SmallVec<16> should not heap-allocate for <= 16 columns
1547        let cols: Vec<Option<&[u8]>> = (0..16).map(|_| Some(&[0u8][..])).collect();
1548        let data = make_data_row(&cols);
1549        let row = PgDataRow::new(&data).unwrap();
1550        assert_eq!(row.column_count(), 16);
1551        // All columns should be accessible
1552        for i in 0..16 {
1553            assert_eq!(row.get_raw(i), Some(&[0u8][..]));
1554        }
1555    }
1556
1557    // --- Inline sequential decode tests (validates the raw-bytes pattern) ---
1558
1559    /// Validate inline sequential decode of a 5-column DataRow
1560    /// (i32, str, str, bool, f64) -- the same pattern the generated code uses.
1561    #[test]
1562    fn inline_sequential_decode_five_columns() {
1563        let data = make_data_row(&[
1564            Some(&42i32.to_be_bytes()),
1565            Some(b"alice"),
1566            Some(b"alice@example.com"),
1567            Some(&[1u8]),
1568            Some(&3.14f64.to_be_bytes()),
1569        ]);
1570
1571        // Simulate generated inline decode
1572        let mut pos: usize = 2; // skip i16 num_cols
1573
1574        // Column 0: i32
1575        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1576        pos += 4;
1577        assert_eq!(len, 4);
1578        let id = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1579        pos += len as usize;
1580        assert_eq!(id, 42);
1581
1582        // Column 1: str
1583        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1584        pos += 4;
1585        assert_eq!(len, 5);
1586        let name = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1587        pos += len as usize;
1588        assert_eq!(name, "alice");
1589
1590        // Column 2: str
1591        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1592        pos += 4;
1593        let email = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1594        pos += len as usize;
1595        assert_eq!(email, "alice@example.com");
1596
1597        // Column 3: bool
1598        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1599        pos += 4;
1600        assert_eq!(len, 1);
1601        let active = data[pos] != 0;
1602        pos += len as usize;
1603        assert!(active);
1604
1605        // Column 4: f64
1606        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1607        pos += 4;
1608        assert_eq!(len, 8);
1609        let score = f64::from_be_bytes([
1610            data[pos],
1611            data[pos + 1],
1612            data[pos + 2],
1613            data[pos + 3],
1614            data[pos + 4],
1615            data[pos + 5],
1616            data[pos + 6],
1617            data[pos + 7],
1618        ]);
1619        pos += len as usize;
1620        assert!((score - 3.14).abs() < 1e-10);
1621        assert_eq!(pos, data.len());
1622    }
1623
1624    /// Validate inline decode with NULL columns.
1625    #[test]
1626    fn inline_sequential_decode_with_nulls() {
1627        let data = make_data_row(&[
1628            Some(&42i32.to_be_bytes()),
1629            None, // NULL name
1630            Some(b"text"),
1631        ]);
1632
1633        let mut pos: usize = 2;
1634
1635        // Column 0: i32 NOT NULL
1636        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1637        pos += 4;
1638        let id = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1639        pos += len as usize;
1640        assert_eq!(id, 42);
1641
1642        // Column 1: str NULLABLE -> None
1643        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1644        pos += 4;
1645        let name: Option<&str> = if len < 0 {
1646            None
1647        } else {
1648            let s = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1649            pos += len as usize;
1650            Some(s)
1651        };
1652        assert!(name.is_none());
1653
1654        // Column 2: str NOT NULL
1655        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1656        pos += 4;
1657        let txt = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1658        pos += len as usize;
1659        assert_eq!(txt, "text");
1660        assert_eq!(pos, data.len());
1661    }
1662
1663    /// Validate inline decode with all supported scalar types.
1664    #[test]
1665    fn inline_sequential_decode_all_scalar_types() {
1666        let data = make_data_row(&[
1667            Some(&[1u8]),                  // bool
1668            Some(&7i16.to_be_bytes()),     // i16
1669            Some(&42i32.to_be_bytes()),    // i32
1670            Some(&12345i64.to_be_bytes()), // i64
1671            Some(&2.5f32.to_be_bytes()),   // f32
1672            Some(&3.14f64.to_be_bytes()),  // f64
1673        ]);
1674
1675        let mut pos: usize = 2;
1676
1677        // bool
1678        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1679        pos += 4;
1680        let v_bool = data[pos] != 0;
1681        pos += len as usize;
1682        assert!(v_bool);
1683
1684        // i16
1685        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1686        pos += 4;
1687        let v_i16 = i16::from_be_bytes([data[pos], data[pos + 1]]);
1688        pos += len as usize;
1689        assert_eq!(v_i16, 7);
1690
1691        // i32
1692        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1693        pos += 4;
1694        let v_i32 = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1695        pos += len as usize;
1696        assert_eq!(v_i32, 42);
1697
1698        // i64
1699        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1700        pos += 4;
1701        let v_i64 = i64::from_be_bytes([
1702            data[pos],
1703            data[pos + 1],
1704            data[pos + 2],
1705            data[pos + 3],
1706            data[pos + 4],
1707            data[pos + 5],
1708            data[pos + 6],
1709            data[pos + 7],
1710        ]);
1711        pos += len as usize;
1712        assert_eq!(v_i64, 12345);
1713
1714        // f32
1715        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1716        pos += 4;
1717        let v_f32 = f32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1718        pos += len as usize;
1719        assert!((v_f32 - 2.5).abs() < 1e-6);
1720
1721        // f64
1722        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1723        pos += 4;
1724        let v_f64 = f64::from_be_bytes([
1725            data[pos],
1726            data[pos + 1],
1727            data[pos + 2],
1728            data[pos + 3],
1729            data[pos + 4],
1730            data[pos + 5],
1731            data[pos + 6],
1732            data[pos + 7],
1733        ]);
1734        pos += len as usize;
1735        assert!((v_f64 - 3.14).abs() < 1e-10);
1736        assert_eq!(pos, data.len());
1737    }
1738
1739    /// Validate PgDataRow::new is public (callable from external code).
1740    #[test]
1741    fn pg_data_row_new_is_public() {
1742        let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
1743        // This compiles because PgDataRow::new is pub.
1744        let row = PgDataRow::new(&data).unwrap();
1745        assert_eq!(row.get_i32(0), Some(42));
1746    }
1747
1748    /// Inline decode produces identical results to PgDataRow for mixed data.
1749    #[test]
1750    fn inline_decode_matches_pgdatarow() {
1751        let data = make_data_row(&[
1752            Some(&99i32.to_be_bytes()),
1753            Some(b"hello world"),
1754            None,
1755            Some(&[0u8]),
1756            Some(&1.23f64.to_be_bytes()),
1757        ]);
1758
1759        // PgDataRow results
1760        let row = PgDataRow::new(&data).unwrap();
1761        let dr_i32 = row.get_i32(0);
1762        let dr_str = row.get_str(1);
1763        let dr_null = row.get_str(2);
1764        let dr_bool = row.get_bool(3);
1765        let dr_f64 = row.get_f64(4);
1766
1767        // Inline results
1768        let mut pos: usize = 2;
1769
1770        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1771        pos += 4;
1772        let in_i32 = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1773        pos += len as usize;
1774
1775        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1776        pos += 4;
1777        let in_str = std::str::from_utf8(&data[pos..pos + len as usize]).unwrap();
1778        pos += len as usize;
1779
1780        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1781        pos += 4;
1782        let in_null: Option<&str> = if len < 0 { None } else { unreachable!() };
1783
1784        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1785        pos += 4;
1786        let in_bool = data[pos] != 0;
1787        pos += len as usize;
1788
1789        let len = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
1790        pos += 4;
1791        let in_f64 = f64::from_be_bytes([
1792            data[pos],
1793            data[pos + 1],
1794            data[pos + 2],
1795            data[pos + 3],
1796            data[pos + 4],
1797            data[pos + 5],
1798            data[pos + 6],
1799            data[pos + 7],
1800        ]);
1801        pos += len as usize;
1802
1803        // Both paths must produce identical results
1804        assert_eq!(dr_i32, Some(in_i32));
1805        assert_eq!(dr_str, Some(in_str));
1806        assert_eq!(dr_null, in_null);
1807        assert_eq!(dr_bool, Some(in_bool));
1808        assert!((dr_f64.unwrap() - in_f64).abs() < 1e-15);
1809        assert_eq!(pos, data.len());
1810    }
1811
1812    // ===================================================================
1813    // PgDataRow -- comprehensive tests
1814    // ===================================================================
1815
1816    #[test]
1817    fn pg_data_row_all_null_columns() {
1818        let data = make_data_row(&[None, None, None, None, None]);
1819        let row = PgDataRow::new(&data).unwrap();
1820        assert_eq!(row.column_count(), 5);
1821        for i in 0..5 {
1822            assert!(row.is_null(i), "column {i} should be null");
1823            assert_eq!(row.get_raw(i), None);
1824            assert_eq!(row.get_i32(i), None);
1825            assert_eq!(row.get_i64(i), None);
1826            assert_eq!(row.get_str(i), None);
1827            assert_eq!(row.get_bool(i), None);
1828            assert_eq!(row.get_f64(i), None);
1829        }
1830    }
1831
1832    #[test]
1833    fn pg_data_row_very_long_text() {
1834        let long_text = "x".repeat(2048);
1835        let data = make_data_row(&[Some(long_text.as_bytes())]);
1836        let row = PgDataRow::new(&data).unwrap();
1837        assert_eq!(row.get_str(0), Some(long_text.as_str()));
1838    }
1839
1840    #[test]
1841    fn pg_data_row_empty_text() {
1842        let data = make_data_row(&[Some(b"")]);
1843        let row = PgDataRow::new(&data).unwrap();
1844        assert!(!row.is_null(0));
1845        assert_eq!(row.get_str(0), Some(""));
1846        assert_eq!(row.get_bytes(0), Some(&[][..]));
1847    }
1848
1849    #[test]
1850    fn pg_data_row_20_columns_exceeds_inline() {
1851        let col_data: Vec<[u8; 4]> = (0..20).map(|i: i32| i.to_be_bytes()).collect();
1852        let cols: Vec<Option<&[u8]>> = col_data.iter().map(|b| Some(b.as_slice())).collect();
1853        let data = make_data_row(&cols);
1854        let row = PgDataRow::new(&data).unwrap();
1855        assert_eq!(row.column_count(), 20);
1856        for i in 0..20 {
1857            assert_eq!(row.get_i32(i), Some(i as i32));
1858        }
1859    }
1860
1861    #[test]
1862    fn pg_data_row_is_null_each_position() {
1863        // 3 columns: data, null, data
1864        let data = make_data_row(&[Some(&1i32.to_be_bytes()), None, Some(&3i32.to_be_bytes())]);
1865        let row = PgDataRow::new(&data).unwrap();
1866        assert!(!row.is_null(0));
1867        assert!(row.is_null(1));
1868        assert!(!row.is_null(2));
1869    }
1870
1871    #[test]
1872    fn pg_data_row_negative_column_count() {
1873        let data = (-1i16).to_be_bytes();
1874        assert!(PgDataRow::new(&data).is_err());
1875    }
1876
1877    #[test]
1878    fn pg_data_row_get_str_invalid_utf8() {
1879        let invalid_utf8 = &[0xFF, 0xFE, 0x80];
1880        let data = make_data_row(&[Some(invalid_utf8)]);
1881        let row = PgDataRow::new(&data).unwrap();
1882        // get_str returns None for invalid UTF-8, but get_bytes returns the raw data
1883        assert_eq!(row.get_str(0), None);
1884        assert_eq!(row.get_bytes(0), Some(&[0xFF, 0xFE, 0x80][..]));
1885    }
1886
1887    #[test]
1888    fn pg_data_row_get_i32_wrong_length() {
1889        // i32 needs exactly 4 bytes; give it 2
1890        let data = make_data_row(&[Some(&7i16.to_be_bytes())]);
1891        let row = PgDataRow::new(&data).unwrap();
1892        assert_eq!(row.get_i32(0), None); // 2 bytes != 4 bytes
1893        assert_eq!(row.get_i16(0), Some(7)); // but i16 works
1894    }
1895
1896    #[test]
1897    fn pg_data_row_get_i64_wrong_length() {
1898        // i64 needs 8 bytes; give it 4
1899        let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
1900        let row = PgDataRow::new(&data).unwrap();
1901        assert_eq!(row.get_i64(0), None);
1902    }
1903
1904    #[test]
1905    fn pg_data_row_get_f64_wrong_length() {
1906        let data = make_data_row(&[Some(&2.5f32.to_be_bytes())]);
1907        let row = PgDataRow::new(&data).unwrap();
1908        assert_eq!(row.get_f64(0), None); // 4 bytes != 8 bytes
1909    }
1910
1911    #[test]
1912    fn pg_data_row_get_f32_wrong_length() {
1913        let data = make_data_row(&[Some(&3.14f64.to_be_bytes())]);
1914        let row = PgDataRow::new(&data).unwrap();
1915        assert_eq!(row.get_f32(0), None); // 8 bytes != 4 bytes
1916    }
1917
1918    #[test]
1919    fn pg_data_row_get_bool_wrong_length() {
1920        // bool needs 1 byte; give it 4
1921        let data = make_data_row(&[Some(&42i32.to_be_bytes())]);
1922        let row = PgDataRow::new(&data).unwrap();
1923        assert_eq!(row.get_bool(0), None);
1924    }
1925
1926    #[test]
1927    fn pg_data_row_unicode_text() {
1928        let texts = [
1929            "\u{1F600}\u{1F4A9}\u{1F680}", // emoji
1930            "\u{4e16}\u{754c}",            // CJK
1931            "\u{0645}\u{0631}\u{062D}",    // Arabic
1932            "\u{1F468}\u{200D}\u{1F469}",  // ZWJ
1933        ];
1934        for text in &texts {
1935            let data = make_data_row(&[Some(text.as_bytes())]);
1936            let row = PgDataRow::new(&data).unwrap();
1937            assert_eq!(row.get_str(0), Some(*text));
1938        }
1939    }
1940
1941    #[test]
1942    fn pg_data_row_i32_boundary_values() {
1943        for &val in &[i32::MIN, -1, 0, 1, i32::MAX] {
1944            let data = make_data_row(&[Some(&val.to_be_bytes())]);
1945            let row = PgDataRow::new(&data).unwrap();
1946            assert_eq!(row.get_i32(0), Some(val), "failed for {val}");
1947        }
1948    }
1949
1950    #[test]
1951    fn pg_data_row_i64_boundary_values() {
1952        for &val in &[i64::MIN, -1, 0, 1, i64::MAX] {
1953            let data = make_data_row(&[Some(&val.to_be_bytes())]);
1954            let row = PgDataRow::new(&data).unwrap();
1955            assert_eq!(row.get_i64(0), Some(val), "failed for {val}");
1956        }
1957    }
1958
1959    #[test]
1960    fn pg_data_row_f64_special_values() {
1961        let data = make_data_row(&[Some(&f64::INFINITY.to_be_bytes())]);
1962        let row = PgDataRow::new(&data).unwrap();
1963        assert_eq!(row.get_f64(0), Some(f64::INFINITY));
1964
1965        let data = make_data_row(&[Some(&f64::NEG_INFINITY.to_be_bytes())]);
1966        let row = PgDataRow::new(&data).unwrap();
1967        assert_eq!(row.get_f64(0), Some(f64::NEG_INFINITY));
1968
1969        let data = make_data_row(&[Some(&f64::NAN.to_be_bytes())]);
1970        let row = PgDataRow::new(&data).unwrap();
1971        assert!(row.get_f64(0).unwrap().is_nan());
1972    }
1973
1974    #[test]
1975    fn pg_data_row_f32_special_values() {
1976        let data = make_data_row(&[Some(&f32::INFINITY.to_be_bytes())]);
1977        let row = PgDataRow::new(&data).unwrap();
1978        assert_eq!(row.get_f32(0), Some(f32::INFINITY));
1979
1980        let data = make_data_row(&[Some(&f32::NAN.to_be_bytes())]);
1981        let row = PgDataRow::new(&data).unwrap();
1982        assert!(row.get_f32(0).unwrap().is_nan());
1983    }
1984
1985    #[test]
1986    fn pg_data_row_i16_boundary_values() {
1987        for &val in &[i16::MIN, -1, 0, 1, i16::MAX] {
1988            let data = make_data_row(&[Some(&val.to_be_bytes())]);
1989            let row = PgDataRow::new(&data).unwrap();
1990            assert_eq!(row.get_i16(0), Some(val));
1991        }
1992    }
1993}