Skip to main content

qail_pg/driver/
row.rs

1//! PostgreSQL Row Helpers
2//!
3//! Provides convenient methods to extract typed values from row data.
4//! Supports both text and binary result formats when column metadata is available.
5
6use super::{PgBytesRow, PgRow};
7use crate::types::{FromPg, TypeError};
8use bytes::Bytes;
9
10#[inline]
11fn column_type_meta(
12    column_info: &Option<std::sync::Arc<super::ColumnInfo>>,
13    idx: usize,
14) -> Result<(u32, i16), TypeError> {
15    let info = column_info.as_ref().ok_or_else(|| {
16        TypeError::InvalidData(
17            "Column metadata unavailable; use query APIs that preserve RowDescription".to_string(),
18        )
19    })?;
20
21    let oid = info
22        .oids
23        .get(idx)
24        .copied()
25        .ok_or_else(|| TypeError::InvalidData(format!("Missing OID for column {}", idx)))?;
26    let format =
27        info.formats.get(idx).copied().ok_or_else(|| {
28            TypeError::InvalidData(format!("Missing format code for column {}", idx))
29        })?;
30    Ok((oid, format))
31}
32
33#[inline]
34fn parse_bool_text(bytes: &[u8]) -> Option<bool> {
35    match std::str::from_utf8(bytes).ok()?.trim() {
36        "t" | "T" | "true" | "TRUE" | "1" => Some(true),
37        "f" | "F" | "false" | "FALSE" | "0" => Some(false),
38        _ => None,
39    }
40}
41
42/// Trait for types that can be constructed from a database row.
43///
44/// Implement this trait on your structs to enable typed fetching:
45/// ```ignore
46/// impl QailRow for User {
47///     fn columns() -> &'static [&'static str] {
48///         &["id", "name", "email"]
49///     }
50///     
51///     fn from_row(row: &PgRow) -> Self {
52///         User {
53///             // Metadata-aware decoders (OID + text/binary format).
54///             id: row
55///                 .try_get_by_name::<qail_pg::Uuid>("id")
56///                 .map(|v| v.0)
57///                 .unwrap_or_default(),
58///             name: row.try_get_by_name::<String>("name").unwrap_or_default(),
59///             email: row.try_get_by_name::<String>("email").unwrap_or_default(),
60///         }
61///     }
62/// }
63///
64/// // Then use:
65/// let users: Vec<User> = driver.fetch_typed::<User>(&query).await?;
66/// ```
67pub trait QailRow: Sized {
68    /// Return the column names this struct expects.
69    /// These are used to automatically build SELECT queries.
70    fn columns() -> &'static [&'static str];
71
72    /// Construct an instance from a PgRow.
73    /// Column indices match the order returned by `columns()`.
74    fn from_row(row: &PgRow) -> Self;
75}
76
77impl PgRow {
78    /// Decode a non-null column into any `FromPg` type using backend OID/format metadata.
79    ///
80    /// Returns:
81    /// - `TypeError::UnexpectedNull` if the column value is NULL
82    /// - `TypeError::InvalidData` if the column index is out of bounds or metadata is missing
83    /// - other `TypeError` variants from the target decoder
84    pub fn try_get<T: FromPg>(&self, idx: usize) -> Result<T, TypeError> {
85        let cell = self
86            .columns
87            .get(idx)
88            .ok_or_else(|| TypeError::InvalidData(format!("Column index {} out of bounds", idx)))?;
89
90        let bytes = cell.as_deref().ok_or(TypeError::UnexpectedNull)?;
91        let (oid, format) = self.column_type_meta(idx)?;
92        T::from_pg(bytes, oid, format)
93    }
94
95    /// Decode a possibly-null column into `Option<T>` using backend OID/format metadata.
96    ///
97    /// Returns `Ok(None)` when the column is SQL NULL.
98    pub fn try_get_opt<T: FromPg>(&self, idx: usize) -> Result<Option<T>, TypeError> {
99        let cell = self
100            .columns
101            .get(idx)
102            .ok_or_else(|| TypeError::InvalidData(format!("Column index {} out of bounds", idx)))?;
103
104        match cell {
105            None => Ok(None),
106            Some(bytes) => {
107                let (oid, format) = self.column_type_meta(idx)?;
108                Ok(Some(T::from_pg(bytes, oid, format)?))
109            }
110        }
111    }
112
113    /// Decode a non-null column by name into any `FromPg` type.
114    pub fn try_get_by_name<T: FromPg>(&self, name: &str) -> Result<T, TypeError> {
115        let idx = self
116            .column_index(name)
117            .ok_or_else(|| TypeError::InvalidData(format!("Unknown column name '{}'", name)))?;
118        self.try_get(idx)
119    }
120
121    /// Decode a possibly-null column by name into `Option<T>`.
122    pub fn try_get_opt_by_name<T: FromPg>(&self, name: &str) -> Result<Option<T>, TypeError> {
123        let idx = self
124            .column_index(name)
125            .ok_or_else(|| TypeError::InvalidData(format!("Unknown column name '{}'", name)))?;
126        self.try_get_opt(idx)
127    }
128
129    fn column_type_meta(&self, idx: usize) -> Result<(u32, i16), TypeError> {
130        column_type_meta(&self.column_info, idx)
131    }
132
133    /// Get a column value as String.
134    /// Returns None if the value is NULL or invalid UTF-8.
135    pub fn get_string(&self, idx: usize) -> Option<String> {
136        self.columns
137            .get(idx)?
138            .as_ref()
139            .and_then(|bytes| std::str::from_utf8(bytes).ok().map(str::to_owned))
140    }
141
142    /// Get a column value as i32.
143    pub fn get_i32(&self, idx: usize) -> Option<i32> {
144        if self.column_info.is_some()
145            && let Ok(v) = self.try_get::<i32>(idx)
146        {
147            return Some(v);
148        }
149        let bytes = self.columns.get(idx)?.as_ref()?;
150        std::str::from_utf8(bytes).ok()?.parse().ok()
151    }
152
153    /// Get a column value as i64.
154    pub fn get_i64(&self, idx: usize) -> Option<i64> {
155        if self.column_info.is_some()
156            && let Ok(v) = self.try_get::<i64>(idx)
157        {
158            return Some(v);
159        }
160        let bytes = self.columns.get(idx)?.as_ref()?;
161        std::str::from_utf8(bytes).ok()?.parse().ok()
162    }
163
164    /// Get a column value as f64.
165    pub fn get_f64(&self, idx: usize) -> Option<f64> {
166        if self.column_info.is_some()
167            && let Ok(v) = self.try_get::<f64>(idx)
168        {
169            return Some(v);
170        }
171        let bytes = self.columns.get(idx)?.as_ref()?;
172        std::str::from_utf8(bytes).ok()?.parse().ok()
173    }
174
175    /// Get a column value as bool.
176    pub fn get_bool(&self, idx: usize) -> Option<bool> {
177        if self.column_info.is_some()
178            && let Ok(v) = self.try_get::<bool>(idx)
179        {
180            return Some(v);
181        }
182        let bytes = self.columns.get(idx)?.as_ref()?;
183        parse_bool_text(bytes)
184    }
185
186    /// Check if a column is NULL.
187    pub fn is_null(&self, idx: usize) -> bool {
188        self.columns.get(idx).map(|v| v.is_none()).unwrap_or(true)
189    }
190
191    /// Get raw bytes of a column.
192    pub fn get_bytes(&self, idx: usize) -> Option<&[u8]> {
193        self.columns.get(idx)?.as_ref().map(|v| v.as_slice())
194    }
195
196    /// Get number of columns in the row.
197    pub fn len(&self) -> usize {
198        self.columns.len()
199    }
200
201    /// Check if the row has no columns.
202    pub fn is_empty(&self) -> bool {
203        self.columns.is_empty()
204    }
205
206    /// Get a column value as UUID string.
207    /// Handles both text format (36-char string) and binary format (16 bytes).
208    pub fn get_uuid(&self, idx: usize) -> Option<String> {
209        let bytes = self.columns.get(idx)?.as_ref()?;
210
211        if bytes.len() == 16 {
212            // Binary format - decode 16 bytes
213            use crate::protocol::types::decode_uuid;
214            decode_uuid(bytes).ok()
215        } else {
216            // Text format - return as-is
217            std::str::from_utf8(bytes).ok().map(str::to_owned)
218        }
219    }
220
221    /// Get a column value as JSON string.
222    /// Handles both JSON (text) and JSONB (version byte prefix) formats.
223    pub fn get_json(&self, idx: usize) -> Option<String> {
224        let bytes = self.columns.get(idx)?.as_ref()?;
225
226        if bytes.is_empty() {
227            return Some(String::new());
228        }
229
230        // JSONB has version byte (1) as first byte
231        if bytes[0] == 1 && bytes.len() > 1 {
232            std::str::from_utf8(&bytes[1..]).ok().map(str::to_owned)
233        } else {
234            std::str::from_utf8(bytes).ok().map(str::to_owned)
235        }
236    }
237
238    /// Get a column value as timestamp string (ISO 8601 format).
239    pub fn get_timestamp(&self, idx: usize) -> Option<String> {
240        let bytes = self.columns.get(idx)?.as_ref()?;
241        std::str::from_utf8(bytes).ok().map(str::to_owned)
242    }
243
244    /// Get a column value as text array.
245    pub fn get_text_array(&self, idx: usize) -> Option<Vec<String>> {
246        let bytes = self.columns.get(idx)?.as_ref()?;
247        let s = std::str::from_utf8(bytes).ok()?;
248        crate::protocol::types::try_decode_text_array(s).ok()
249    }
250
251    /// Get a column value as integer array.
252    pub fn get_int_array(&self, idx: usize) -> Option<Vec<i64>> {
253        let bytes = self.columns.get(idx)?.as_ref()?;
254        let s = std::str::from_utf8(bytes).ok()?;
255        crate::protocol::types::decode_int_array(s).ok()
256    }
257
258    // ==================== ERGONOMIC SHORTCUTS ====================
259    // These methods reduce boilerplate by providing sensible defaults
260
261    /// Get string, defaulting to empty string if NULL.
262    /// Ergonomic shortcut: `row.text(0)` instead of `row.get_string(0).unwrap_or_default()`
263    pub fn text(&self, idx: usize) -> String {
264        self.get_string(idx).unwrap_or_default()
265    }
266
267    /// Get string with custom default if NULL.
268    /// Example: `row.text_or(1, "Unknown")`
269    pub fn text_or(&self, idx: usize, default: &str) -> String {
270        self.get_string(idx).unwrap_or_else(|| default.to_string())
271    }
272
273    /// Get i64, defaulting to 0 if NULL.
274    /// Ergonomic shortcut: `row.int(4)` instead of `row.get_i64(4).unwrap_or(0)`
275    pub fn int(&self, idx: usize) -> i64 {
276        self.get_i64(idx).unwrap_or(0)
277    }
278
279    /// Get f64, defaulting to 0.0 if NULL.
280    pub fn float(&self, idx: usize) -> f64 {
281        self.get_f64(idx).unwrap_or(0.0)
282    }
283
284    /// Get bool, defaulting to false if NULL.
285    pub fn boolean(&self, idx: usize) -> bool {
286        self.get_bool(idx).unwrap_or(false)
287    }
288
289    /// Parse timestamp as DateTime<Utc>.
290    /// Handles PostgreSQL timestamp formats automatically.
291    #[cfg(feature = "chrono")]
292    pub fn datetime(&self, idx: usize) -> Option<chrono::DateTime<chrono::Utc>> {
293        if let Ok(dt) = self.try_get::<chrono::DateTime<chrono::Utc>>(idx) {
294            return Some(dt);
295        }
296
297        let s = self.get_timestamp(idx)?;
298        // Try parsing various PostgreSQL timestamp formats
299        chrono::DateTime::parse_from_rfc3339(&s.replace(' ', "T"))
300            .ok()
301            .map(|dt| dt.with_timezone(&chrono::Utc))
302            .or_else(|| {
303                // Try PostgreSQL format: "2024-01-01 12:00:00.123456+00"
304                chrono::DateTime::parse_from_str(&s, "%Y-%m-%d %H:%M:%S%.f%#z")
305                    .ok()
306                    .map(|dt| dt.with_timezone(&chrono::Utc))
307            })
308    }
309
310    /// Parse UUID column as uuid::Uuid type.
311    #[cfg(feature = "uuid")]
312    pub fn uuid_typed(&self, idx: usize) -> Option<uuid::Uuid> {
313        self.try_get::<uuid::Uuid>(idx).ok().or_else(|| {
314            self.get_uuid(idx)
315                .and_then(|s| uuid::Uuid::parse_str(&s).ok())
316        })
317    }
318
319    // ==================== GET BY COLUMN NAME ====================
320
321    /// Get column index by name.
322    pub fn column_index(&self, name: &str) -> Option<usize> {
323        self.column_info.as_ref()?.name_to_index.get(name).copied()
324    }
325
326    /// Get a String column by name.
327    pub fn get_string_by_name(&self, name: &str) -> Option<String> {
328        self.get_string(self.column_index(name)?)
329    }
330
331    /// Get an i32 column by name.
332    pub fn get_i32_by_name(&self, name: &str) -> Option<i32> {
333        self.get_i32(self.column_index(name)?)
334    }
335
336    /// Get an i64 column by name.
337    pub fn get_i64_by_name(&self, name: &str) -> Option<i64> {
338        self.get_i64(self.column_index(name)?)
339    }
340
341    /// Get a f64 column by name.
342    pub fn get_f64_by_name(&self, name: &str) -> Option<f64> {
343        self.get_f64(self.column_index(name)?)
344    }
345
346    /// Get a bool column by name.
347    pub fn get_bool_by_name(&self, name: &str) -> Option<bool> {
348        self.get_bool(self.column_index(name)?)
349    }
350
351    /// Get a UUID column by name.
352    pub fn get_uuid_by_name(&self, name: &str) -> Option<String> {
353        self.get_uuid(self.column_index(name)?)
354    }
355
356    /// Get a JSON column by name.
357    pub fn get_json_by_name(&self, name: &str) -> Option<String> {
358        self.get_json(self.column_index(name)?)
359    }
360
361    /// Check if a column is NULL by name.
362    pub fn is_null_by_name(&self, name: &str) -> bool {
363        self.column_index(name)
364            .map(|idx| self.is_null(idx))
365            .unwrap_or(true)
366    }
367
368    /// Get a timestamp column by name.
369    pub fn get_timestamp_by_name(&self, name: &str) -> Option<String> {
370        self.get_timestamp(self.column_index(name)?)
371    }
372
373    /// Get a text array column by name.
374    pub fn get_text_array_by_name(&self, name: &str) -> Option<Vec<String>> {
375        self.get_text_array(self.column_index(name)?)
376    }
377
378    /// Get an integer array column by name.
379    pub fn get_int_array_by_name(&self, name: &str) -> Option<Vec<i64>> {
380        self.get_int_array(self.column_index(name)?)
381    }
382
383    // ==================== ERGONOMIC BY-NAME SHORTCUTS ====================
384    // These mirror the positional shortcuts (text, boolean, int, etc.)
385    // but use column names — safe with RETURNING * regardless of column order.
386
387    /// Get string by column name, defaulting to empty string.
388    /// Example: `row.text_by_name("name")` instead of `row.get_string_by_name("name").unwrap_or_default()`
389    pub fn text_by_name(&self, name: &str) -> String {
390        self.get_string_by_name(name).unwrap_or_default()
391    }
392
393    /// Get bool by column name, defaulting to false.
394    pub fn boolean_by_name(&self, name: &str) -> bool {
395        self.get_bool_by_name(name).unwrap_or(false)
396    }
397
398    /// Get i64 by column name, defaulting to 0.
399    pub fn int_by_name(&self, name: &str) -> i64 {
400        self.get_i64_by_name(name).unwrap_or(0)
401    }
402
403    /// Get f64 by column name, defaulting to 0.0.
404    pub fn float_by_name(&self, name: &str) -> f64 {
405        self.get_f64_by_name(name).unwrap_or(0.0)
406    }
407
408    /// Parse timestamp by column name as DateTime<Utc>.
409    #[cfg(feature = "chrono")]
410    pub fn datetime_by_name(&self, name: &str) -> Option<chrono::DateTime<chrono::Utc>> {
411        self.datetime(self.column_index(name)?)
412    }
413
414    /// Parse UUID by column name as uuid::Uuid type.
415    #[cfg(feature = "uuid")]
416    pub fn uuid_typed_by_name(&self, name: &str) -> Option<uuid::Uuid> {
417        self.uuid_typed(self.column_index(name)?)
418    }
419}
420
421impl PgBytesRow {
422    #[inline]
423    pub(crate) fn release_payload(&mut self) {
424        self.payload = Bytes::new();
425    }
426
427    /// Decode a non-null column into any `FromPg` type using backend OID/format metadata.
428    pub fn try_get<T: FromPg>(&self, idx: usize) -> Result<T, TypeError> {
429        let bytes = self.get_bytes(idx).ok_or(TypeError::UnexpectedNull)?;
430        let (oid, format) = column_type_meta(&self.column_info, idx)?;
431        T::from_pg(bytes, oid, format)
432    }
433
434    /// Decode a possibly-null column into `Option<T>` using backend OID/format metadata.
435    pub fn try_get_opt<T: FromPg>(&self, idx: usize) -> Result<Option<T>, TypeError> {
436        let Some(cell) = self.spans.get(idx) else {
437            return Err(TypeError::InvalidData(format!(
438                "Column index {} out of bounds",
439                idx
440            )));
441        };
442
443        match cell {
444            None => Ok(None),
445            Some(_) => Ok(Some(self.try_get(idx)?)),
446        }
447    }
448
449    /// Decode a non-null column by name into any `FromPg` type.
450    pub fn try_get_by_name<T: FromPg>(&self, name: &str) -> Result<T, TypeError> {
451        let idx = self
452            .column_index(name)
453            .ok_or_else(|| TypeError::InvalidData(format!("Unknown column name '{}'", name)))?;
454        self.try_get(idx)
455    }
456
457    /// Decode a possibly-null column by name into `Option<T>`.
458    pub fn try_get_opt_by_name<T: FromPg>(&self, name: &str) -> Result<Option<T>, TypeError> {
459        let idx = self
460            .column_index(name)
461            .ok_or_else(|| TypeError::InvalidData(format!("Unknown column name '{}'", name)))?;
462        self.try_get_opt(idx)
463    }
464
465    /// Get raw bytes of a column.
466    pub fn get_bytes(&self, idx: usize) -> Option<&[u8]> {
467        let (start, len) = self.spans.get(idx)?.as_ref().copied()?;
468        self.payload.get(start..start + len)
469    }
470
471    /// Visit each column as raw bytes without allocating.
472    pub fn for_each_column<F>(&self, mut f: F)
473    where
474        F: FnMut(usize, Option<&[u8]>),
475    {
476        for (idx, span) in self.spans.iter().enumerate() {
477            let value = span
478                .as_ref()
479                .and_then(|(start, len)| self.payload.get(*start..(*start + *len)));
480            f(idx, value);
481        }
482    }
483
484    /// Get number of columns in the row.
485    pub fn len(&self) -> usize {
486        self.spans.len()
487    }
488
489    /// Check if the row has no columns.
490    pub fn is_empty(&self) -> bool {
491        self.spans.is_empty()
492    }
493
494    /// Check if a column is NULL.
495    pub fn is_null(&self, idx: usize) -> bool {
496        self.spans.get(idx).map(|v| v.is_none()).unwrap_or(true)
497    }
498
499    /// Get column index by name.
500    pub fn column_index(&self, name: &str) -> Option<usize> {
501        self.column_info.as_ref()?.name_to_index.get(name).copied()
502    }
503
504    /// Get a column value as i64.
505    pub fn get_i64(&self, idx: usize) -> Option<i64> {
506        if self.column_info.is_some()
507            && let Ok(v) = self.try_get::<i64>(idx)
508        {
509            return Some(v);
510        }
511        let bytes = self.get_bytes(idx)?;
512        std::str::from_utf8(bytes).ok()?.parse().ok()
513    }
514
515    /// Get a column value as f64.
516    pub fn get_f64(&self, idx: usize) -> Option<f64> {
517        if self.column_info.is_some()
518            && let Ok(v) = self.try_get::<f64>(idx)
519        {
520            return Some(v);
521        }
522        let bytes = self.get_bytes(idx)?;
523        std::str::from_utf8(bytes).ok()?.parse().ok()
524    }
525
526    /// Get a column value as bool.
527    pub fn get_bool(&self, idx: usize) -> Option<bool> {
528        if self.column_info.is_some()
529            && let Ok(v) = self.try_get::<bool>(idx)
530        {
531            return Some(v);
532        }
533        let bytes = self.get_bytes(idx)?;
534        parse_bool_text(bytes)
535    }
536}
537
538#[cfg(test)]
539mod tests {
540    use super::*;
541    use crate::protocol::types::oid;
542    use crate::types::{Json, Uuid};
543    use std::collections::HashMap;
544    use std::sync::Arc;
545
546    fn single_col_info(name: &str, oid: u32, format: i16) -> Arc<super::super::ColumnInfo> {
547        let mut name_to_index = HashMap::new();
548        name_to_index.insert(name.to_string(), 0);
549        Arc::new(super::super::ColumnInfo {
550            name_to_index,
551            oids: vec![oid],
552            formats: vec![format],
553        })
554    }
555
556    #[test]
557    fn test_get_string() {
558        let row = PgRow {
559            columns: vec![Some(b"hello".to_vec()), None, Some(b"world".to_vec())],
560            column_info: None,
561        };
562
563        assert_eq!(row.get_string(0), Some("hello".to_string()));
564        assert_eq!(row.get_string(1), None);
565        assert_eq!(row.get_string(2), Some("world".to_string()));
566    }
567
568    #[test]
569    fn test_get_i32() {
570        let row = PgRow {
571            columns: vec![
572                Some(b"42".to_vec()),
573                Some(b"-123".to_vec()),
574                Some(b"not_a_number".to_vec()),
575            ],
576            column_info: None,
577        };
578
579        assert_eq!(row.get_i32(0), Some(42));
580        assert_eq!(row.get_i32(1), Some(-123));
581        assert_eq!(row.get_i32(2), None);
582    }
583
584    #[test]
585    fn test_get_bool() {
586        let row = PgRow {
587            columns: vec![
588                Some(b"t".to_vec()),
589                Some(b"f".to_vec()),
590                Some(b"true".to_vec()),
591                Some(b"false".to_vec()),
592                Some(b"T".to_vec()),
593                Some(b"FALSE".to_vec()),
594                Some(b" 1 ".to_vec()),
595            ],
596            column_info: None,
597        };
598
599        assert_eq!(row.get_bool(0), Some(true));
600        assert_eq!(row.get_bool(1), Some(false));
601        assert_eq!(row.get_bool(2), Some(true));
602        assert_eq!(row.get_bool(3), Some(false));
603        assert_eq!(row.get_bool(4), Some(true));
604        assert_eq!(row.get_bool(5), Some(false));
605        assert_eq!(row.get_bool(6), Some(true));
606    }
607
608    #[test]
609    fn test_is_null() {
610        let row = PgRow {
611            columns: vec![Some(b"value".to_vec()), None],
612            column_info: None,
613        };
614
615        assert!(!row.is_null(0));
616        assert!(row.is_null(1));
617        assert!(row.is_null(99)); // Out of bounds
618    }
619
620    #[test]
621    fn test_try_get_i64_binary() {
622        let row = PgRow {
623            columns: vec![Some(42i64.to_be_bytes().to_vec())],
624            column_info: Some(single_col_info("count", oid::INT8, 1)),
625        };
626
627        let value: i64 = row.try_get(0).unwrap();
628        assert_eq!(value, 42);
629    }
630
631    #[test]
632    fn test_try_get_i64_text_by_name() {
633        let row = PgRow {
634            columns: vec![Some(b"123".to_vec())],
635            column_info: Some(single_col_info("total", oid::INT8, 0)),
636        };
637
638        let value: i64 = row.try_get_by_name("total").unwrap();
639        assert_eq!(value, 123);
640    }
641
642    #[test]
643    fn test_try_get_opt_null() {
644        let row = PgRow {
645            columns: vec![None],
646            column_info: Some(single_col_info("maybe_count", oid::INT8, 1)),
647        };
648
649        let value: Option<i64> = row.try_get_opt(0).unwrap();
650        assert_eq!(value, None);
651    }
652
653    #[test]
654    fn test_try_get_unexpected_null() {
655        let row = PgRow {
656            columns: vec![None],
657            column_info: Some(single_col_info("required_count", oid::INT8, 1)),
658        };
659
660        assert!(matches!(
661            row.try_get::<i64>(0),
662            Err(TypeError::UnexpectedNull)
663        ));
664    }
665
666    #[test]
667    fn test_try_get_uuid_binary() {
668        let uuid_bytes: [u8; 16] = [
669            0x55, 0x0e, 0x84, 0x00, 0xe2, 0x9b, 0x41, 0xd4, 0xa7, 0x16, 0x44, 0x66, 0x55, 0x44,
670            0x00, 0x00,
671        ];
672        let row = PgRow {
673            columns: vec![Some(uuid_bytes.to_vec())],
674            column_info: Some(single_col_info("id", oid::UUID, 1)),
675        };
676
677        let value: Uuid = row.try_get(0).unwrap();
678        assert_eq!(value.0, "550e8400-e29b-41d4-a716-446655440000");
679    }
680
681    #[test]
682    fn test_try_get_jsonb_binary() {
683        let mut bytes = vec![1u8];
684        bytes.extend_from_slice(br#"{"ok":true}"#);
685        let row = PgRow {
686            columns: vec![Some(bytes)],
687            column_info: Some(single_col_info("meta", oid::JSONB, 1)),
688        };
689
690        let value: Json = row.try_get(0).unwrap();
691        assert_eq!(value.0, r#"{"ok":true}"#);
692    }
693
694    #[test]
695    fn test_try_get_requires_column_metadata() {
696        let row = PgRow {
697            columns: vec![Some(b"42".to_vec())],
698            column_info: None,
699        };
700
701        assert!(matches!(
702            row.try_get::<i64>(0),
703            Err(TypeError::InvalidData(msg)) if msg.contains("metadata")
704        ));
705    }
706
707    #[test]
708    fn test_get_i64_uses_metadata_binary() {
709        let row = PgRow {
710            columns: vec![Some(777i64.to_be_bytes().to_vec())],
711            column_info: Some(single_col_info("v", oid::INT8, 1)),
712        };
713        assert_eq!(row.get_i64(0), Some(777));
714    }
715
716    #[test]
717    fn test_get_bool_uses_metadata_binary() {
718        let row = PgRow {
719            columns: vec![Some(vec![1u8])],
720            column_info: Some(single_col_info("flag", oid::BOOL, 1)),
721        };
722        assert_eq!(row.get_bool(0), Some(true));
723    }
724
725    #[test]
726    fn test_pg_bytes_row_get_bytes() {
727        let row = PgBytesRow {
728            payload: bytes::Bytes::from_static(b"abcdef"),
729            spans: vec![Some((1, 3)), None],
730            column_info: None,
731        };
732
733        assert_eq!(row.get_bytes(0), Some(&b"bcd"[..]));
734        assert_eq!(row.get_bytes(1), None);
735        assert!(row.is_null(1));
736    }
737
738    #[test]
739    fn test_pg_bytes_row_try_get_i64_binary() {
740        let row = PgBytesRow {
741            payload: bytes::Bytes::from(42i64.to_be_bytes().to_vec()),
742            spans: vec![Some((0, 8))],
743            column_info: Some(single_col_info("count", oid::INT8, 1)),
744        };
745
746        let value: i64 = row.try_get(0).unwrap();
747        assert_eq!(value, 42);
748    }
749
750    #[test]
751    fn test_pg_bytes_row_get_bool_fallback_matches_text_decoder() {
752        let row = PgBytesRow {
753            payload: bytes::Bytes::from_static(b"TRUEFALSE"),
754            spans: vec![Some((0, 4)), Some((4, 5))],
755            column_info: None,
756        };
757
758        assert_eq!(row.get_bool(0), Some(true));
759        assert_eq!(row.get_bool(1), Some(false));
760    }
761}