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