1use std::rc::Rc;
9
10use crate::codec::ColumnDesc;
11use crate::error::{PgError, PgResult};
12use crate::protocol::FormatCode;
13use crate::types::{FromSql, PgValue};
14
15const INLINE_CAP: usize = 24;
20
21#[derive(Clone)]
23enum CompactBytes {
24 Inline { data: [u8; INLINE_CAP], len: u8 },
26 Heap(Vec<u8>),
28}
29
30impl std::fmt::Debug for CompactBytes {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 write!(f, "CompactBytes({}b)", self.as_slice().len())
33 }
34}
35
36impl CompactBytes {
37 #[inline]
39 fn from_slice(data: &[u8]) -> Self {
40 if data.len() <= INLINE_CAP {
41 let mut buf = [0u8; INLINE_CAP];
42 buf[..data.len()].copy_from_slice(data);
43 CompactBytes::Inline {
44 data: buf,
45 len: data.len() as u8,
46 }
47 } else {
48 CompactBytes::Heap(data.to_vec())
49 }
50 }
51
52 #[inline]
54 fn as_slice(&self) -> &[u8] {
55 match self {
56 CompactBytes::Inline { data, len } => &data[..*len as usize],
57 CompactBytes::Heap(v) => v,
58 }
59 }
60}
61
62#[derive(Debug)]
70pub struct Row {
71 columns: Rc<Vec<ColumnDesc>>,
72 values: Vec<Option<CompactBytes>>,
73}
74
75impl Row {
76 pub fn new(columns: Rc<Vec<ColumnDesc>>, raw_values: Vec<Option<&[u8]>>) -> Self {
80 let values = raw_values
81 .into_iter()
82 .map(|v| v.map(CompactBytes::from_slice))
83 .collect();
84 Self { columns, values }
85 }
86
87 pub fn mock(names: &[&str], values: &[PgValue]) -> Self {
89 let mut cols = Vec::new();
90 let mut raw_values: Vec<Option<CompactBytes>> = Vec::new();
91 for (i, name) in names.iter().enumerate() {
92 let (type_oid, data) = match &values[i] {
93 PgValue::Null => (25, None), PgValue::Int4(v) => (23, Some(v.to_string().into_bytes())),
95 PgValue::Int8(v) => (20, Some(v.to_string().into_bytes())),
96 PgValue::Text(s) => (25, Some(s.clone().into_bytes())),
97 PgValue::Bool(b) => (16, Some(if *b { b"t".to_vec() } else { b"f".to_vec() })),
98 _ => (25, values[i].to_text_bytes()),
100 };
101 cols.push(ColumnDesc {
102 name: name.to_string(),
103 table_oid: 0,
104 col_attr: 0,
105 type_oid,
106 type_size: -1,
107 type_modifier: -1,
108 format_code: FormatCode::Text,
109 });
110 raw_values.push(data.map(|d| CompactBytes::from_slice(&d)));
111 }
112 Self {
113 columns: Rc::new(cols),
114 values: raw_values,
115 }
116 }
117
118 pub fn len(&self) -> usize {
120 self.columns.len()
121 }
122
123 pub fn is_empty(&self) -> bool {
125 self.columns.is_empty()
126 }
127
128 pub fn get(&self, index: usize) -> PgResult<PgValue> {
130 if index >= self.columns.len() {
131 return Err(PgError::TypeConversion(format!(
132 "Column index {} out of range",
133 index
134 )));
135 }
136
137 let col = &self.columns[index];
138 match &self.values[index] {
139 None => Ok(PgValue::Null),
140 Some(data) => match col.format_code {
141 FormatCode::Text => PgValue::from_text(col.type_oid, data.as_slice()),
142 FormatCode::Binary => PgValue::from_binary(col.type_oid, data.as_slice()),
143 },
144 }
145 }
146
147 pub fn get_by_name(&self, name: &str) -> PgResult<PgValue> {
149 let index = self
150 .columns
151 .iter()
152 .position(|c| c.name == name)
153 .ok_or_else(|| PgError::TypeConversion(format!("Column '{}' not found", name)))?;
154 self.get(index)
155 }
156
157 pub fn get_typed<T: FromSql>(&self, index: usize) -> PgResult<T> {
165 let value = self.get(index)?;
166 T::from_sql(&value)
167 }
168
169 pub fn get_typed_by_name<T: FromSql>(&self, name: &str) -> PgResult<T> {
171 let value = self.get_by_name(name)?;
172 T::from_sql(&value)
173 }
174
175 pub fn get_str(&self, index: usize) -> PgResult<Option<&str>> {
177 if index >= self.columns.len() {
178 return Err(PgError::TypeConversion(format!(
179 "Column index {} out of range",
180 index
181 )));
182 }
183 match &self.values[index] {
184 None => Ok(None),
185 Some(data) => std::str::from_utf8(data.as_slice())
186 .map(Some)
187 .map_err(|_| PgError::TypeConversion("Invalid UTF-8".to_string())),
188 }
189 }
190
191 pub fn get_i32(&self, index: usize) -> PgResult<Option<i32>> {
193 match self.get(index)? {
194 PgValue::Null => Ok(None),
195 PgValue::Int4(v) => Ok(Some(v)),
196 PgValue::Int2(v) => Ok(Some(v as i32)),
197 PgValue::Text(s) => s
198 .parse()
199 .map(Some)
200 .map_err(|_| PgError::TypeConversion("Not an i32".to_string())),
201 _ => Err(PgError::TypeConversion("Cannot convert to i32".to_string())),
202 }
203 }
204
205 pub fn get_i64(&self, index: usize) -> PgResult<Option<i64>> {
207 match self.get(index)? {
208 PgValue::Null => Ok(None),
209 PgValue::Int8(v) => Ok(Some(v)),
210 PgValue::Int4(v) => Ok(Some(v as i64)),
211 PgValue::Int2(v) => Ok(Some(v as i64)),
212 PgValue::Text(s) => s
213 .parse()
214 .map(Some)
215 .map_err(|_| PgError::TypeConversion("Not an i64".to_string())),
216 _ => Err(PgError::TypeConversion("Cannot convert to i64".to_string())),
217 }
218 }
219
220 pub fn get_bool(&self, index: usize) -> PgResult<Option<bool>> {
222 match self.get(index)? {
223 PgValue::Null => Ok(None),
224 PgValue::Bool(v) => Ok(Some(v)),
225 _ => Err(PgError::TypeConversion(
226 "Cannot convert to bool".to_string(),
227 )),
228 }
229 }
230
231 pub fn get_f64(&self, index: usize) -> PgResult<Option<f64>> {
233 match self.get(index)? {
234 PgValue::Null => Ok(None),
235 PgValue::Float8(v) => Ok(Some(v)),
236 PgValue::Float4(v) => Ok(Some(v as f64)),
237 PgValue::Int4(v) => Ok(Some(v as f64)),
238 PgValue::Text(s) => s
239 .parse()
240 .map(Some)
241 .map_err(|_| PgError::TypeConversion("Not an f64".to_string())),
242 _ => Err(PgError::TypeConversion("Cannot convert to f64".to_string())),
243 }
244 }
245
246 pub fn get_uuid(&self, index: usize) -> PgResult<Option<[u8; 16]>> {
248 match self.get(index)? {
249 PgValue::Null => Ok(None),
250 PgValue::Uuid(bytes) => Ok(Some(bytes)),
251 _ => Err(PgError::TypeConversion(
252 "Cannot convert to UUID".to_string(),
253 )),
254 }
255 }
256
257 pub fn columns(&self) -> &[ColumnDesc] {
259 &self.columns
260 }
261
262 pub fn column_name(&self, index: usize) -> Option<&str> {
264 self.columns.get(index).map(|c| c.name.as_str())
265 }
266
267 pub fn column_index(&self, name: &str) -> Option<usize> {
269 self.columns.iter().position(|c| c.name == name)
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use crate::codec::ColumnDesc;
277 use crate::error::{PgError, PgResult};
278 use crate::protocol::FormatCode;
279 use crate::types::PgValue;
280 use std::rc::Rc;
281
282 const OID_TEXT: u32 = 25;
284 const OID_INT4: u32 = 23;
285 const OID_INT8: u32 = 20;
286 const OID_BOOL: u32 = 16;
287 const OID_FLOAT8: u32 = 701;
288
289 fn col(name: &str, type_oid: u32) -> ColumnDesc {
290 ColumnDesc {
291 name: name.to_string(),
292 table_oid: 0,
293 col_attr: 0,
294 type_oid,
295 type_size: -1,
296 type_modifier: -1,
297 format_code: FormatCode::Text,
298 }
299 }
300
301 fn make_row(cols: &[(&str, u32)], vals: &[Option<&[u8]>]) -> Row {
302 let descs = Rc::new(cols.iter().map(|(n, o)| col(n, *o)).collect::<Vec<_>>());
303 Row::new(descs, vals.to_vec())
304 }
305
306 #[test]
309 fn test_row_len_two_columns() {
310 let row = make_row(
311 &[("name", OID_TEXT), ("age", OID_INT4)],
312 &[Some(b"alice"), Some(b"30")],
313 );
314 assert_eq!(row.len(), 2);
315 assert!(!row.is_empty());
316 }
317
318 #[test]
319 fn test_row_empty_columns() {
320 let row = Row::new(Rc::new(vec![]), vec![]);
321 assert_eq!(row.len(), 0);
322 assert!(row.is_empty());
323 }
324
325 #[test]
326 fn test_row_len_matches_column_count() {
327 let row = make_row(
328 &[("a", OID_TEXT), ("b", OID_INT4), ("c", OID_BOOL)],
329 &[Some(b"x"), Some(b"1"), Some(b"t")],
330 );
331 assert_eq!(row.len(), row.columns().len());
332 }
333
334 #[test]
337 fn test_get_text_value() {
338 let row = make_row(&[("name", OID_TEXT)], &[Some(b"hello")]);
339 match row.get(0).unwrap() {
340 PgValue::Text(s) => assert_eq!(s, "hello"),
341 other => panic!("Expected Text, got {:?}", other),
342 }
343 }
344
345 #[test]
346 fn test_get_int4_value() {
347 let row = make_row(&[("n", OID_INT4)], &[Some(b"42")]);
348 match row.get(0).unwrap() {
349 PgValue::Int4(v) => assert_eq!(v, 42),
350 other => panic!("Expected Int4, got {:?}", other),
351 }
352 }
353
354 #[test]
355 fn test_get_bool_true() {
356 let row = make_row(&[("flag", OID_BOOL)], &[Some(b"t")]);
357 match row.get(0).unwrap() {
358 PgValue::Bool(v) => assert!(v),
359 other => panic!("Expected Bool, got {:?}", other),
360 }
361 }
362
363 #[test]
364 fn test_get_bool_false() {
365 let row = make_row(&[("flag", OID_BOOL)], &[Some(b"f")]);
366 match row.get(0).unwrap() {
367 PgValue::Bool(v) => assert!(!v),
368 other => panic!("Expected Bool, got {:?}", other),
369 }
370 }
371
372 #[test]
373 fn test_get_null_returns_pgvalue_null() {
374 let row = make_row(&[("name", OID_TEXT)], &[None]);
375 assert!(matches!(row.get(0).unwrap(), PgValue::Null));
376 }
377
378 #[test]
379 fn test_get_index_out_of_range_returns_error() {
380 let row = make_row(&[("name", OID_TEXT)], &[Some(b"alice")]);
381 let err = row.get(99);
382 assert!(err.is_err());
383 if let Err(PgError::TypeConversion(msg)) = err {
384 assert!(
385 msg.contains("out of range") || msg.contains("index"),
386 "Error should mention out-of-range: {}",
387 msg
388 );
389 } else {
390 panic!("Expected TypeConversion error");
391 }
392 }
393
394 #[test]
397 fn test_get_by_name_found() {
398 let row = make_row(
399 &[("id", OID_INT4), ("name", OID_TEXT)],
400 &[Some(b"1"), Some(b"charlie")],
401 );
402 match row.get_by_name("name").unwrap() {
403 PgValue::Text(s) => assert_eq!(s, "charlie"),
404 other => panic!("Expected Text, got {:?}", other),
405 }
406 }
407
408 #[test]
409 fn test_get_by_name_not_found_returns_error() {
410 let row = make_row(&[("id", OID_INT4)], &[Some(b"5")]);
411 let err = row.get_by_name("nonexistent");
412 assert!(err.is_err());
413 if let Err(PgError::TypeConversion(msg)) = err {
414 assert!(
415 msg.contains("not found") || msg.contains("nonexistent"),
416 "Error should mention missing column: {}",
417 msg
418 );
419 } else {
420 panic!("Expected TypeConversion error");
421 }
422 }
423
424 #[test]
425 fn test_get_by_name_null_column() {
426 let row = make_row(&[("data", OID_TEXT)], &[None]);
427 assert!(matches!(row.get_by_name("data").unwrap(), PgValue::Null));
428 }
429
430 #[test]
433 fn test_get_str_valid_utf8() {
434 let row = make_row(&[("msg", OID_TEXT)], &[Some(b"hello world")]);
435 assert_eq!(row.get_str(0).unwrap(), Some("hello world"));
436 }
437
438 #[test]
439 fn test_get_str_null() {
440 let row = make_row(&[("msg", OID_TEXT)], &[None]);
441 assert_eq!(row.get_str(0).unwrap(), None);
442 }
443
444 #[test]
445 fn test_get_i32_value() {
446 let row = make_row(&[("n", OID_INT4)], &[Some(b"777")]);
447 assert_eq!(row.get_i32(0).unwrap(), Some(777));
448 }
449
450 #[test]
451 fn test_get_i32_null() {
452 let row = make_row(&[("n", OID_INT4)], &[None]);
453 assert_eq!(row.get_i32(0).unwrap(), None);
454 }
455
456 #[test]
457 fn test_get_i64_large_value() {
458 let row = make_row(&[("n", OID_INT8)], &[Some(b"9876543210")]);
459 assert_eq!(row.get_i64(0).unwrap(), Some(9_876_543_210_i64));
460 }
461
462 #[test]
463 fn test_get_i64_null() {
464 let row = make_row(&[("n", OID_INT8)], &[None]);
465 assert_eq!(row.get_i64(0).unwrap(), None);
466 }
467
468 #[test]
469 fn test_get_bool_null() {
470 let row = make_row(&[("flag", OID_BOOL)], &[None]);
471 assert_eq!(row.get_bool(0).unwrap(), None);
472 }
473
474 #[test]
475 fn test_get_f64_value() {
476 let pi = std::f64::consts::PI;
477 let text = pi.to_string();
478 let row = make_row(&[("score", OID_FLOAT8)], &[Some(text.as_bytes())]);
479 let v = row.get_f64(0).unwrap().unwrap();
480 assert!((v - pi).abs() < f64::EPSILON);
481 }
482
483 #[test]
484 fn test_get_f64_null() {
485 let row = make_row(&[("score", OID_FLOAT8)], &[None]);
486 assert_eq!(row.get_f64(0).unwrap(), None);
487 }
488
489 #[test]
492 fn test_column_name_by_index() {
493 let row = make_row(
494 &[("user_id", OID_INT4), ("email", OID_TEXT)],
495 &[Some(b"1"), Some(b"a@b.com")],
496 );
497 assert_eq!(row.column_name(0), Some("user_id"));
498 assert_eq!(row.column_name(1), Some("email"));
499 assert_eq!(row.column_name(99), None);
500 }
501
502 #[test]
503 fn test_column_index_by_name() {
504 let row = make_row(
505 &[("id", OID_INT4), ("name", OID_TEXT)],
506 &[Some(b"1"), Some(b"bob")],
507 );
508 assert_eq!(row.column_index("id"), Some(0));
509 assert_eq!(row.column_index("name"), Some(1));
510 assert_eq!(row.column_index("missing"), None);
511 }
512
513 #[test]
514 fn test_columns_slice() {
515 let row = make_row(
516 &[("a", OID_TEXT), ("b", OID_INT4)],
517 &[Some(b"x"), Some(b"1")],
518 );
519 let cols = row.columns();
520 assert_eq!(cols.len(), 2);
521 assert_eq!(cols[0].name, "a");
522 assert_eq!(cols[1].name, "b");
523 }
524
525 #[test]
528 fn test_get_typed_i32() {
529 let row = make_row(&[("n", OID_INT4)], &[Some(b"77")]);
530 let v: i32 = row.get_typed(0).unwrap();
531 assert_eq!(v, 77);
532 }
533
534 #[test]
535 fn test_get_typed_option_null_is_none() {
536 let row = make_row(&[("n", OID_INT4)], &[None]);
537 let v: Option<i32> = row.get_typed(0).unwrap();
538 assert_eq!(v, None);
539 }
540
541 #[test]
542 fn test_get_typed_option_value_is_some() {
543 let row = make_row(&[("n", OID_INT4)], &[Some(b"55")]);
544 let v: Option<i32> = row.get_typed(0).unwrap();
545 assert_eq!(v, Some(55));
546 }
547
548 #[test]
549 fn test_get_typed_by_name_string() {
550 let row = make_row(&[("label", OID_TEXT)], &[Some(b"production")]);
551 let v: String = row.get_typed_by_name("label").unwrap();
552 assert_eq!(v, "production");
553 }
554
555 #[test]
556 fn test_get_typed_by_name_not_found() {
557 let row = make_row(&[("n", OID_INT4)], &[Some(b"1")]);
558 let r: PgResult<i32> = row.get_typed_by_name("missing");
559 assert!(r.is_err());
560 }
561
562 #[test]
566 fn test_rc_columns_shared_across_rows() {
567 let cols = Rc::new(vec![col("v", OID_INT4)]);
568 let initial_count = Rc::strong_count(&cols);
569
570 let row1 = Row::new(Rc::clone(&cols), vec![Some(b"1")]);
571 let row2 = Row::new(Rc::clone(&cols), vec![Some(b"2")]);
572 let row3 = Row::new(Rc::clone(&cols), vec![Some(b"3")]);
573
574 assert_eq!(Rc::strong_count(&cols), initial_count + 3);
576
577 drop(row1);
578 assert_eq!(Rc::strong_count(&cols), initial_count + 2);
579
580 drop(row2);
581 drop(row3);
582 assert_eq!(Rc::strong_count(&cols), initial_count);
583 }
584
585 #[test]
586 fn test_rc_1000_rows_share_same_column_descriptor() {
587 let cols = Rc::new(vec![col("id", OID_INT4), col("name", OID_TEXT)]);
589 let rows: Vec<Row> = (0..1000)
590 .map(|i| {
591 let val = i.to_string();
592 Row::new(Rc::clone(&cols), vec![Some(val.as_bytes()), Some(b"x")])
594 })
595 .collect();
596
597 assert_eq!(Rc::strong_count(&cols), 1001);
599
600 drop(rows);
601 assert_eq!(Rc::strong_count(&cols), 1);
602 }
603
604 #[test]
607 fn test_get_mixed_null_and_non_null() {
608 let row = make_row(
609 &[("a", OID_TEXT), ("b", OID_INT4), ("c", OID_TEXT)],
610 &[Some(b"hello"), None, Some(b"world")],
611 );
612 assert!(matches!(row.get(0).unwrap(), PgValue::Text(_)));
613 assert!(matches!(row.get(1).unwrap(), PgValue::Null));
614 assert!(matches!(row.get(2).unwrap(), PgValue::Text(_)));
615 }
616
617 #[test]
618 fn test_get_first_and_last_column() {
619 let row = make_row(
620 &[
621 ("first", OID_INT4),
622 ("middle", OID_TEXT),
623 ("last", OID_BOOL),
624 ],
625 &[Some(b"1"), Some(b"mid"), Some(b"t")],
626 );
627 assert!(matches!(row.get(0).unwrap(), PgValue::Int4(1)));
628 assert!(matches!(row.get(2).unwrap(), PgValue::Bool(true)));
629 }
630}