1use 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
42pub trait QailRow: Sized {
68 fn columns() -> &'static [&'static str];
71
72 fn from_row(row: &PgRow) -> Self;
75}
76
77impl PgRow {
78 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 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 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 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 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 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 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 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 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 pub fn is_null(&self, idx: usize) -> bool {
188 self.columns.get(idx).map(|v| v.is_none()).unwrap_or(true)
189 }
190
191 pub fn get_bytes(&self, idx: usize) -> Option<&[u8]> {
193 self.columns.get(idx)?.as_ref().map(|v| v.as_slice())
194 }
195
196 pub fn len(&self) -> usize {
198 self.columns.len()
199 }
200
201 pub fn is_empty(&self) -> bool {
203 self.columns.is_empty()
204 }
205
206 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 use crate::protocol::types::decode_uuid;
214 decode_uuid(bytes).ok()
215 } else {
216 std::str::from_utf8(bytes).ok().map(str::to_owned)
218 }
219 }
220
221 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 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 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 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 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 pub fn text(&self, idx: usize) -> String {
264 self.get_string(idx).unwrap_or_default()
265 }
266
267 pub fn text_or(&self, idx: usize, default: &str) -> String {
270 self.get_string(idx).unwrap_or_else(|| default.to_string())
271 }
272
273 pub fn int(&self, idx: usize) -> i64 {
276 self.get_i64(idx).unwrap_or(0)
277 }
278
279 pub fn float(&self, idx: usize) -> f64 {
281 self.get_f64(idx).unwrap_or(0.0)
282 }
283
284 pub fn boolean(&self, idx: usize) -> bool {
286 self.get_bool(idx).unwrap_or(false)
287 }
288
289 #[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 chrono::DateTime::parse_from_rfc3339(&s.replace(' ', "T"))
300 .ok()
301 .map(|dt| dt.with_timezone(&chrono::Utc))
302 .or_else(|| {
303 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 #[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 pub fn column_index(&self, name: &str) -> Option<usize> {
323 self.column_info.as_ref()?.name_to_index.get(name).copied()
324 }
325
326 pub fn get_string_by_name(&self, name: &str) -> Option<String> {
328 self.get_string(self.column_index(name)?)
329 }
330
331 pub fn get_i32_by_name(&self, name: &str) -> Option<i32> {
333 self.get_i32(self.column_index(name)?)
334 }
335
336 pub fn get_i64_by_name(&self, name: &str) -> Option<i64> {
338 self.get_i64(self.column_index(name)?)
339 }
340
341 pub fn get_f64_by_name(&self, name: &str) -> Option<f64> {
343 self.get_f64(self.column_index(name)?)
344 }
345
346 pub fn get_bool_by_name(&self, name: &str) -> Option<bool> {
348 self.get_bool(self.column_index(name)?)
349 }
350
351 pub fn get_uuid_by_name(&self, name: &str) -> Option<String> {
353 self.get_uuid(self.column_index(name)?)
354 }
355
356 pub fn get_json_by_name(&self, name: &str) -> Option<String> {
358 self.get_json(self.column_index(name)?)
359 }
360
361 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 pub fn get_timestamp_by_name(&self, name: &str) -> Option<String> {
370 self.get_timestamp(self.column_index(name)?)
371 }
372
373 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 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 pub fn text_by_name(&self, name: &str) -> String {
390 self.get_string_by_name(name).unwrap_or_default()
391 }
392
393 pub fn boolean_by_name(&self, name: &str) -> bool {
395 self.get_bool_by_name(name).unwrap_or(false)
396 }
397
398 pub fn int_by_name(&self, name: &str) -> i64 {
400 self.get_i64_by_name(name).unwrap_or(0)
401 }
402
403 pub fn float_by_name(&self, name: &str) -> f64 {
405 self.get_f64_by_name(name).unwrap_or(0.0)
406 }
407
408 #[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 #[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 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 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 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 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 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 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 pub fn len(&self) -> usize {
486 self.spans.len()
487 }
488
489 pub fn is_empty(&self) -> bool {
491 self.spans.is_empty()
492 }
493
494 pub fn is_null(&self, idx: usize) -> bool {
496 self.spans.get(idx).map(|v| v.is_none()).unwrap_or(true)
497 }
498
499 pub fn column_index(&self, name: &str) -> Option<usize> {
501 self.column_info.as_ref()?.name_to_index.get(name).copied()
502 }
503
504 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 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 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)); }
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}