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
33pub trait QailRow: Sized {
59 fn columns() -> &'static [&'static str];
62
63 fn from_row(row: &PgRow) -> Self;
66}
67
68impl PgRow {
69 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 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 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 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 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 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 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 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 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 pub fn is_null(&self, idx: usize) -> bool {
184 self.columns.get(idx).map(|v| v.is_none()).unwrap_or(true)
185 }
186
187 pub fn get_bytes(&self, idx: usize) -> Option<&[u8]> {
189 self.columns.get(idx)?.as_ref().map(|v| v.as_slice())
190 }
191
192 pub fn len(&self) -> usize {
194 self.columns.len()
195 }
196
197 pub fn is_empty(&self) -> bool {
199 self.columns.is_empty()
200 }
201
202 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 use crate::protocol::types::decode_uuid;
210 decode_uuid(bytes).ok()
211 } else {
212 String::from_utf8(bytes.clone()).ok()
214 }
215 }
216
217 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 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 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 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 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 pub fn text(&self, idx: usize) -> String {
260 self.get_string(idx).unwrap_or_default()
261 }
262
263 pub fn text_or(&self, idx: usize, default: &str) -> String {
266 self.get_string(idx).unwrap_or_else(|| default.to_string())
267 }
268
269 pub fn int(&self, idx: usize) -> i64 {
272 self.get_i64(idx).unwrap_or(0)
273 }
274
275 pub fn float(&self, idx: usize) -> f64 {
277 self.get_f64(idx).unwrap_or(0.0)
278 }
279
280 pub fn boolean(&self, idx: usize) -> bool {
282 self.get_bool(idx).unwrap_or(false)
283 }
284
285 #[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 chrono::DateTime::parse_from_rfc3339(&s.replace(' ', "T"))
296 .ok()
297 .map(|dt| dt.with_timezone(&chrono::Utc))
298 .or_else(|| {
299 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 #[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 pub fn column_index(&self, name: &str) -> Option<usize> {
319 self.column_info.as_ref()?.name_to_index.get(name).copied()
320 }
321
322 pub fn get_string_by_name(&self, name: &str) -> Option<String> {
324 self.get_string(self.column_index(name)?)
325 }
326
327 pub fn get_i32_by_name(&self, name: &str) -> Option<i32> {
329 self.get_i32(self.column_index(name)?)
330 }
331
332 pub fn get_i64_by_name(&self, name: &str) -> Option<i64> {
334 self.get_i64(self.column_index(name)?)
335 }
336
337 pub fn get_f64_by_name(&self, name: &str) -> Option<f64> {
339 self.get_f64(self.column_index(name)?)
340 }
341
342 pub fn get_bool_by_name(&self, name: &str) -> Option<bool> {
344 self.get_bool(self.column_index(name)?)
345 }
346
347 pub fn get_uuid_by_name(&self, name: &str) -> Option<String> {
349 self.get_uuid(self.column_index(name)?)
350 }
351
352 pub fn get_json_by_name(&self, name: &str) -> Option<String> {
354 self.get_json(self.column_index(name)?)
355 }
356
357 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 pub fn get_timestamp_by_name(&self, name: &str) -> Option<String> {
366 self.get_timestamp(self.column_index(name)?)
367 }
368
369 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 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 pub fn text_by_name(&self, name: &str) -> String {
386 self.get_string_by_name(name).unwrap_or_default()
387 }
388
389 pub fn boolean_by_name(&self, name: &str) -> bool {
391 self.get_bool_by_name(name).unwrap_or(false)
392 }
393
394 pub fn int_by_name(&self, name: &str) -> i64 {
396 self.get_i64_by_name(name).unwrap_or(0)
397 }
398
399 pub fn float_by_name(&self, name: &str) -> f64 {
401 self.get_f64_by_name(name).unwrap_or(0.0)
402 }
403
404 #[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 #[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 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 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 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 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 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 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 pub fn len(&self) -> usize {
482 self.spans.len()
483 }
484
485 pub fn is_empty(&self) -> bool {
487 self.spans.is_empty()
488 }
489
490 pub fn is_null(&self, idx: usize) -> bool {
492 self.spans.get(idx).map(|v| v.is_none()).unwrap_or(true)
493 }
494
495 pub fn column_index(&self, name: &str) -> Option<usize> {
497 self.column_info.as_ref()?.name_to_index.get(name).copied()
498 }
499
500 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 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 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)); }
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}