Skip to main content

sqlmodel_core/
row.rs

1//! Database row representation.
2
3use crate::Result;
4use crate::error::{Error, TypeError};
5use crate::value::Value;
6use std::collections::HashMap;
7use std::sync::Arc;
8
9/// Column metadata shared across all rows in a result set.
10///
11/// This struct is wrapped in `Arc` so all rows from the same query share
12/// the same column information, saving memory for large result sets.
13#[derive(Debug, Clone)]
14pub struct ColumnInfo {
15    /// Column names in order
16    names: Vec<String>,
17    /// Name -> index mapping for O(1) lookup
18    name_to_index: HashMap<String, usize>,
19}
20
21impl ColumnInfo {
22    /// Create new column info from a list of column names.
23    pub fn new(names: Vec<String>) -> Self {
24        let name_to_index = names
25            .iter()
26            .enumerate()
27            .map(|(i, name)| (name.clone(), i))
28            .collect();
29        Self {
30            names,
31            name_to_index,
32        }
33    }
34
35    /// Get the number of columns.
36    pub fn len(&self) -> usize {
37        self.names.len()
38    }
39
40    /// Check if there are no columns.
41    pub fn is_empty(&self) -> bool {
42        self.names.is_empty()
43    }
44
45    /// Get the index of a column by name.
46    pub fn index_of(&self, name: &str) -> Option<usize> {
47        self.name_to_index.get(name).copied()
48    }
49
50    /// Get the name of a column by index.
51    pub fn name_at(&self, index: usize) -> Option<&str> {
52        self.names.get(index).map(String::as_str)
53    }
54
55    /// Check if a column exists.
56    pub fn contains(&self, name: &str) -> bool {
57        self.name_to_index.contains_key(name)
58    }
59
60    /// Get all column names.
61    pub fn names(&self) -> &[String] {
62        &self.names
63    }
64}
65
66/// A single row returned from a database query.
67///
68/// Rows provide both index-based and name-based access to column values.
69/// Column metadata is shared via `Arc` for memory efficiency.
70#[derive(Debug, Clone)]
71pub struct Row {
72    /// Column values in order
73    values: Vec<Value>,
74    /// Shared column metadata
75    columns: Arc<ColumnInfo>,
76}
77
78impl Row {
79    /// Create a new row with the given columns and values.
80    ///
81    /// For multiple rows from the same result set, prefer `with_columns`
82    /// to share the column metadata.
83    pub fn new(column_names: Vec<String>, values: Vec<Value>) -> Self {
84        let columns = Arc::new(ColumnInfo::new(column_names));
85        Self { values, columns }
86    }
87
88    /// Extract a subset of columns with a given prefix.
89    ///
90    /// This is useful for eager loading where columns are aliased like
91    /// `table__column`. This method extracts columns matching `prefix__*`
92    /// and returns a new Row with the prefix stripped.
93    ///
94    /// # Example
95    ///
96    /// ```ignore
97    /// let row = Row::new(
98    ///     vec!["heroes__id", "heroes__name", "teams__id", "teams__name"],
99    ///     vec![Value::Int(1), Value::Text("Hero".into()), Value::Int(10), Value::Text("Team".into())],
100    /// );
101    /// let hero_row = row.subset_by_prefix("heroes");
102    /// // hero_row has columns: ["id", "name"] with values [1, "Hero"]
103    /// ```
104    #[must_use]
105    pub fn subset_by_prefix(&self, prefix: &str) -> Self {
106        let prefix_with_sep = format!("{}__", prefix);
107        let mut names = Vec::new();
108        let mut values = Vec::new();
109
110        for (name, value) in self.iter() {
111            if let Some(stripped) = name.strip_prefix(&prefix_with_sep) {
112                names.push(stripped.to_string());
113                values.push(value.clone());
114            }
115        }
116
117        Self::new(names, values)
118    }
119
120    /// Check if this row has any columns with the given prefix.
121    ///
122    /// Useful for checking if a LEFT JOIN returned NULL (no matching rows).
123    #[must_use]
124    pub fn has_prefix(&self, prefix: &str) -> bool {
125        let prefix_with_sep = format!("{}__", prefix);
126        self.column_names()
127            .any(|name| name.starts_with(&prefix_with_sep))
128    }
129
130    /// Check if all values with a given prefix are NULL.
131    ///
132    /// Used to detect LEFT JOIN rows where no related record exists.
133    #[must_use]
134    pub fn prefix_is_all_null(&self, prefix: &str) -> bool {
135        let prefix_with_sep = format!("{}__", prefix);
136        for (name, value) in self.iter() {
137            if name.starts_with(&prefix_with_sep) && !value.is_null() {
138                return false;
139            }
140        }
141
142        // If we found no columns with prefix, consider it "all null".
143        true
144    }
145
146    /// Create a new row with shared column metadata.
147    ///
148    /// This is more efficient for creating multiple rows from the same query.
149    pub fn with_columns(columns: Arc<ColumnInfo>, values: Vec<Value>) -> Self {
150        Self { values, columns }
151    }
152
153    /// Get the shared column metadata.
154    ///
155    /// Use this to create additional rows that share the same column info.
156    pub fn column_info(&self) -> Arc<ColumnInfo> {
157        Arc::clone(&self.columns)
158    }
159
160    /// Get the number of columns in this row.
161    pub fn len(&self) -> usize {
162        self.values.len()
163    }
164
165    /// Check if this row is empty.
166    pub fn is_empty(&self) -> bool {
167        self.values.is_empty()
168    }
169
170    /// Get a value by column index. O(1) operation.
171    pub fn get(&self, index: usize) -> Option<&Value> {
172        self.values.get(index)
173    }
174
175    /// Get a value by column name. O(1) operation via HashMap lookup.
176    pub fn get_by_name(&self, name: &str) -> Option<&Value> {
177        self.columns.index_of(name).and_then(|i| self.values.get(i))
178    }
179
180    /// Check if a column exists by name.
181    pub fn contains_column(&self, name: &str) -> bool {
182        self.columns.contains(name)
183    }
184
185    /// Get a typed value by column index.
186    #[allow(clippy::result_large_err)]
187    pub fn get_as<T: FromValue>(&self, index: usize) -> Result<T> {
188        let value = self.get(index).ok_or_else(|| {
189            Error::Type(TypeError {
190                expected: std::any::type_name::<T>(),
191                actual: format!(
192                    "index {} out of bounds (row has {} columns)",
193                    index,
194                    self.len()
195                ),
196                column: None,
197                rust_type: None,
198            })
199        })?;
200        T::from_value(value)
201    }
202
203    /// Get a typed value by column name.
204    #[allow(clippy::result_large_err)]
205    pub fn get_named<T: FromValue>(&self, name: &str) -> Result<T> {
206        let value = self.get_by_name(name).ok_or_else(|| {
207            Error::Type(TypeError {
208                expected: std::any::type_name::<T>(),
209                actual: format!("column '{}' not found", name),
210                column: Some(name.to_string()),
211                rust_type: None,
212            })
213        })?;
214        T::from_value(value).map_err(|e| match e {
215            Error::Type(mut te) => {
216                te.column = Some(name.to_string());
217                Error::Type(te)
218            }
219            e => e,
220        })
221    }
222
223    /// Get all column names.
224    pub fn column_names(&self) -> impl Iterator<Item = &str> {
225        self.columns.names().iter().map(String::as_str)
226    }
227
228    /// Iterate over all values.
229    pub fn values(&self) -> impl Iterator<Item = &Value> {
230        self.values.iter()
231    }
232
233    /// Iterate over (column_name, value) pairs.
234    pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> {
235        self.columns
236            .names()
237            .iter()
238            .map(String::as_str)
239            .zip(self.values.iter())
240    }
241}
242
243/// Trait for converting from a `Value` to a typed value.
244pub trait FromValue: Sized {
245    /// Convert from a Value, returning an error if the conversion fails.
246    #[allow(clippy::result_large_err)]
247    fn from_value(value: &Value) -> Result<Self>;
248}
249
250impl FromValue for bool {
251    fn from_value(value: &Value) -> Result<Self> {
252        value.as_bool().ok_or_else(|| {
253            Error::Type(TypeError {
254                expected: "bool",
255                actual: value.type_name().to_string(),
256                column: None,
257                rust_type: None,
258            })
259        })
260    }
261}
262
263impl FromValue for i8 {
264    fn from_value(value: &Value) -> Result<Self> {
265        match value {
266            Value::TinyInt(v) => Ok(*v),
267            Value::Bool(v) => Ok(if *v { 1 } else { 0 }),
268            _ => Err(Error::Type(TypeError {
269                expected: "i8",
270                actual: value.type_name().to_string(),
271                column: None,
272                rust_type: None,
273            })),
274        }
275    }
276}
277
278impl FromValue for i16 {
279    fn from_value(value: &Value) -> Result<Self> {
280        match value {
281            Value::TinyInt(v) => Ok(i16::from(*v)),
282            Value::SmallInt(v) => Ok(*v),
283            Value::Bool(v) => Ok(if *v { 1 } else { 0 }),
284            _ => Err(Error::Type(TypeError {
285                expected: "i16",
286                actual: value.type_name().to_string(),
287                column: None,
288                rust_type: None,
289            })),
290        }
291    }
292}
293
294impl FromValue for i32 {
295    fn from_value(value: &Value) -> Result<Self> {
296        match value {
297            Value::TinyInt(v) => Ok(i32::from(*v)),
298            Value::SmallInt(v) => Ok(i32::from(*v)),
299            Value::Int(v) => Ok(*v),
300            Value::Bool(v) => Ok(if *v { 1 } else { 0 }),
301            _ => Err(Error::Type(TypeError {
302                expected: "i32",
303                actual: value.type_name().to_string(),
304                column: None,
305                rust_type: None,
306            })),
307        }
308    }
309}
310
311impl FromValue for i64 {
312    fn from_value(value: &Value) -> Result<Self> {
313        value.as_i64().ok_or_else(|| {
314            Error::Type(TypeError {
315                expected: "i64",
316                actual: value.type_name().to_string(),
317                column: None,
318                rust_type: None,
319            })
320        })
321    }
322}
323
324impl FromValue for u8 {
325    fn from_value(value: &Value) -> Result<Self> {
326        let v = value.as_i64().ok_or_else(|| {
327            Error::Type(TypeError {
328                expected: "u8",
329                actual: value.type_name().to_string(),
330                column: None,
331                rust_type: None,
332            })
333        })?;
334        u8::try_from(v).map_err(|_| {
335            Error::Type(TypeError {
336                expected: "u8",
337                actual: format!("value {} out of range", v),
338                column: None,
339                rust_type: None,
340            })
341        })
342    }
343}
344
345impl FromValue for u16 {
346    fn from_value(value: &Value) -> Result<Self> {
347        let v = value.as_i64().ok_or_else(|| {
348            Error::Type(TypeError {
349                expected: "u16",
350                actual: value.type_name().to_string(),
351                column: None,
352                rust_type: None,
353            })
354        })?;
355        u16::try_from(v).map_err(|_| {
356            Error::Type(TypeError {
357                expected: "u16",
358                actual: format!("value {} out of range", v),
359                column: None,
360                rust_type: None,
361            })
362        })
363    }
364}
365
366impl FromValue for u32 {
367    fn from_value(value: &Value) -> Result<Self> {
368        let v = value.as_i64().ok_or_else(|| {
369            Error::Type(TypeError {
370                expected: "u32",
371                actual: value.type_name().to_string(),
372                column: None,
373                rust_type: None,
374            })
375        })?;
376        u32::try_from(v).map_err(|_| {
377            Error::Type(TypeError {
378                expected: "u32",
379                actual: format!("value {} out of range", v),
380                column: None,
381                rust_type: None,
382            })
383        })
384    }
385}
386
387impl FromValue for u64 {
388    fn from_value(value: &Value) -> Result<Self> {
389        let v = value.as_i64().ok_or_else(|| {
390            Error::Type(TypeError {
391                expected: "u64",
392                actual: value.type_name().to_string(),
393                column: None,
394                rust_type: None,
395            })
396        })?;
397        u64::try_from(v).map_err(|_| {
398            Error::Type(TypeError {
399                expected: "u64",
400                actual: format!("value {} out of range", v),
401                column: None,
402                rust_type: None,
403            })
404        })
405    }
406}
407
408/// Maximum integer value exactly representable in f32: 2^24 = 16,777,216
409const F32_MAX_EXACT_INT: i64 = 1 << 24;
410
411impl FromValue for f32 {
412    /// Convert a Value reference to f32, returning an error if precision would be lost.
413    fn from_value(value: &Value) -> Result<Self> {
414        match value {
415            Value::Float(v) => Ok(*v),
416            #[allow(clippy::cast_possible_truncation)]
417            Value::Double(v) => {
418                let converted = *v as f32;
419                // Check round-trip: if converting back doesn't match, we lost precision
420                if (f64::from(converted) - *v).abs() > f64::EPSILON * v.abs().max(1.0) {
421                    return Err(Error::Type(TypeError {
422                        expected: "f32-representable f64",
423                        actual: format!("f64 value {} loses precision as f32", v),
424                        column: None,
425                        rust_type: Some("f32"),
426                    }));
427                }
428                Ok(converted)
429            }
430            Value::TinyInt(v) => Ok(f32::from(*v)),
431            Value::SmallInt(v) => Ok(f32::from(*v)),
432            #[allow(clippy::cast_possible_truncation)]
433            Value::Int(v) => {
434                if i64::from(*v).abs() > F32_MAX_EXACT_INT {
435                    return Err(Error::Type(TypeError {
436                        expected: "f32-representable i32",
437                        actual: format!(
438                            "i32 value {} exceeds f32 exact integer range (±{})",
439                            v, F32_MAX_EXACT_INT
440                        ),
441                        column: None,
442                        rust_type: Some("f32"),
443                    }));
444                }
445                Ok(*v as f32)
446            }
447            #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
448            Value::BigInt(v) => {
449                // Use unsigned_abs to avoid overflow on i64::MIN
450                if v.unsigned_abs() > F32_MAX_EXACT_INT as u64 {
451                    return Err(Error::Type(TypeError {
452                        expected: "f32-representable i64",
453                        actual: format!(
454                            "i64 value {} exceeds f32 exact integer range (±{})",
455                            v, F32_MAX_EXACT_INT
456                        ),
457                        column: None,
458                        rust_type: Some("f32"),
459                    }));
460                }
461                Ok(*v as f32)
462            }
463            // Bool to f32 is lossless (0.0 or 1.0)
464            Value::Bool(v) => Ok(if *v { 1.0 } else { 0.0 }),
465            _ => Err(Error::Type(TypeError {
466                expected: "f32",
467                actual: value.type_name().to_string(),
468                column: None,
469                rust_type: None,
470            })),
471        }
472    }
473}
474
475/// Maximum integer value exactly representable in f64: 2^53 = 9,007,199,254,740,992
476const F64_MAX_EXACT_INT: i64 = 1 << 53;
477
478impl FromValue for f64 {
479    /// Convert a Value reference to f64, returning an error if precision would be lost.
480    fn from_value(value: &Value) -> Result<Self> {
481        match value {
482            Value::Float(v) => Ok(f64::from(*v)),
483            Value::Double(v) => Ok(*v),
484            Value::TinyInt(v) => Ok(f64::from(*v)),
485            Value::SmallInt(v) => Ok(f64::from(*v)),
486            Value::Int(v) => Ok(f64::from(*v)),
487            #[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
488            Value::BigInt(v) => {
489                // Use unsigned_abs to avoid overflow on i64::MIN
490                if v.unsigned_abs() > F64_MAX_EXACT_INT as u64 {
491                    return Err(Error::Type(TypeError {
492                        expected: "f64-representable i64",
493                        actual: format!(
494                            "i64 value {} exceeds f64 exact integer range (±{})",
495                            v, F64_MAX_EXACT_INT
496                        ),
497                        column: None,
498                        rust_type: Some("f64"),
499                    }));
500                }
501                Ok(*v as f64)
502            }
503            Value::Bool(v) => Ok(if *v { 1.0 } else { 0.0 }),
504            _ => Err(Error::Type(TypeError {
505                expected: "f64",
506                actual: value.type_name().to_string(),
507                column: None,
508                rust_type: None,
509            })),
510        }
511    }
512}
513
514impl FromValue for String {
515    fn from_value(value: &Value) -> Result<Self> {
516        match value {
517            Value::Text(s) => Ok(s.clone()),
518            Value::Decimal(s) => Ok(s.clone()),
519            _ => Err(Error::Type(TypeError {
520                expected: "String",
521                actual: value.type_name().to_string(),
522                column: None,
523                rust_type: None,
524            })),
525        }
526    }
527}
528
529impl FromValue for Vec<u8> {
530    fn from_value(value: &Value) -> Result<Self> {
531        match value {
532            Value::Bytes(b) => Ok(b.clone()),
533            Value::Text(s) => Ok(s.as_bytes().to_vec()),
534            _ => Err(Error::Type(TypeError {
535                expected: "Vec<u8>",
536                actual: value.type_name().to_string(),
537                column: None,
538                rust_type: None,
539            })),
540        }
541    }
542}
543
544impl<T: FromValue> FromValue for Option<T> {
545    fn from_value(value: &Value) -> Result<Self> {
546        if value.is_null() {
547            Ok(None)
548        } else {
549            T::from_value(value).map(Some)
550        }
551    }
552}
553
554impl FromValue for Value {
555    fn from_value(value: &Value) -> Result<Self> {
556        Ok(value.clone())
557    }
558}
559
560impl FromValue for serde_json::Value {
561    fn from_value(value: &Value) -> Result<Self> {
562        match value {
563            Value::Json(v) => Ok(v.clone()),
564            Value::Text(s) => serde_json::from_str(s).map_err(|e| {
565                Error::Type(TypeError {
566                    expected: "valid JSON",
567                    actual: format!("invalid JSON: {}", e),
568                    column: None,
569                    rust_type: None,
570                })
571            }),
572            _ => Err(Error::Type(TypeError {
573                expected: "JSON",
574                actual: value.type_name().to_string(),
575                column: None,
576                rust_type: None,
577            })),
578        }
579    }
580}
581
582impl FromValue for [u8; 16] {
583    fn from_value(value: &Value) -> Result<Self> {
584        match value {
585            Value::Uuid(v) => Ok(*v),
586            Value::Bytes(v) if v.len() == 16 => {
587                let mut arr = [0u8; 16];
588                arr.copy_from_slice(v);
589                Ok(arr)
590            }
591            _ => Err(Error::Type(TypeError {
592                expected: "UUID (16 bytes)",
593                actual: value.type_name().to_string(),
594                column: None,
595                rust_type: None,
596            })),
597        }
598    }
599}
600
601#[cfg(test)]
602mod tests {
603    use super::*;
604
605    #[test]
606    fn test_row_basic_access() {
607        let row = Row::new(
608            vec!["id".to_string(), "name".to_string(), "age".to_string()],
609            vec![
610                Value::Int(1),
611                Value::Text("Alice".to_string()),
612                Value::Int(30),
613            ],
614        );
615
616        assert_eq!(row.len(), 3);
617        assert!(!row.is_empty());
618
619        // Index access
620        assert_eq!(row.get(0), Some(&Value::Int(1)));
621        assert_eq!(row.get(1), Some(&Value::Text("Alice".to_string())));
622        assert_eq!(row.get(3), None);
623
624        // Name access
625        assert_eq!(row.get_by_name("id"), Some(&Value::Int(1)));
626        assert_eq!(
627            row.get_by_name("name"),
628            Some(&Value::Text("Alice".to_string()))
629        );
630        assert_eq!(row.get_by_name("missing"), None);
631    }
632
633    #[test]
634    fn test_row_typed_access() {
635        let row = Row::new(
636            vec!["id".to_string(), "name".to_string()],
637            vec![Value::Int(42), Value::Text("Bob".to_string())],
638        );
639
640        // Typed index access
641        assert_eq!(row.get_as::<i32>(0).unwrap(), 42);
642        assert_eq!(row.get_as::<i64>(0).unwrap(), 42);
643        assert_eq!(row.get_as::<String>(1).unwrap(), "Bob");
644
645        // Typed name access
646        assert_eq!(row.get_named::<i32>("id").unwrap(), 42);
647        assert_eq!(row.get_named::<String>("name").unwrap(), "Bob");
648    }
649
650    #[test]
651    fn test_row_type_errors() {
652        let row = Row::new(
653            vec!["id".to_string()],
654            vec![Value::Text("not a number".to_string())],
655        );
656
657        // Type mismatch
658        assert!(row.get_named::<i32>("id").is_err());
659
660        // Column not found
661        assert!(row.get_named::<i32>("missing").is_err());
662
663        // Index out of bounds
664        assert!(row.get_as::<i32>(99).is_err());
665    }
666
667    #[test]
668    fn test_row_null_handling() {
669        let row = Row::new(vec!["nullable".to_string()], vec![Value::Null]);
670
671        // Option handles NULL gracefully
672        assert_eq!(row.get_named::<Option<i32>>("nullable").unwrap(), None);
673
674        // Non-optional type fails for NULL
675        assert!(row.get_named::<i32>("nullable").is_err());
676    }
677
678    #[test]
679    fn test_row_iterators() {
680        let row = Row::new(
681            vec!["a".to_string(), "b".to_string()],
682            vec![Value::Int(1), Value::Int(2)],
683        );
684
685        // Column names iterator
686        let names: Vec<_> = row.column_names().collect();
687        assert_eq!(names, vec!["a", "b"]);
688
689        // Values iterator
690        let values: Vec<_> = row.values().collect();
691        assert_eq!(values, vec![&Value::Int(1), &Value::Int(2)]);
692
693        // Pairs iterator
694        let pairs: Vec<_> = row.iter().collect();
695        assert_eq!(pairs, vec![("a", &Value::Int(1)), ("b", &Value::Int(2))]);
696    }
697
698    #[test]
699    fn test_row_shared_columns() {
700        let columns = Arc::new(ColumnInfo::new(vec!["id".to_string(), "name".to_string()]));
701
702        let row1 = Row::with_columns(
703            Arc::clone(&columns),
704            vec![Value::Int(1), Value::Text("Alice".to_string())],
705        );
706        let row2 = Row::with_columns(
707            Arc::clone(&columns),
708            vec![Value::Int(2), Value::Text("Bob".to_string())],
709        );
710
711        // Both rows share the same column info
712        assert!(Arc::ptr_eq(&row1.column_info(), &row2.column_info()));
713
714        // Both work correctly
715        assert_eq!(row1.get_named::<i32>("id").unwrap(), 1);
716        assert_eq!(row2.get_named::<i32>("id").unwrap(), 2);
717    }
718
719    #[test]
720    fn test_row_contains_column() {
721        let row = Row::new(vec!["exists".to_string()], vec![Value::Int(1)]);
722
723        assert!(row.contains_column("exists"));
724        assert!(!row.contains_column("missing"));
725    }
726
727    #[test]
728    fn test_column_info() {
729        let info = ColumnInfo::new(vec![
730            "id".to_string(),
731            "name".to_string(),
732            "age".to_string(),
733        ]);
734
735        assert_eq!(info.len(), 3);
736        assert!(!info.is_empty());
737
738        assert_eq!(info.index_of("id"), Some(0));
739        assert_eq!(info.index_of("name"), Some(1));
740        assert_eq!(info.index_of("missing"), None);
741
742        assert_eq!(info.name_at(0), Some("id"));
743        assert_eq!(info.name_at(1), Some("name"));
744        assert_eq!(info.name_at(99), None);
745
746        assert!(info.contains("id"));
747        assert!(!info.contains("missing"));
748    }
749
750    #[test]
751    fn test_from_value_all_types() {
752        // bool
753        assert!(bool::from_value(&Value::Bool(true)).unwrap());
754        assert!(bool::from_value(&Value::Int(1)).unwrap());
755        assert!(!bool::from_value(&Value::Int(0)).unwrap());
756
757        // i8
758        assert_eq!(i8::from_value(&Value::TinyInt(42)).unwrap(), 42);
759
760        // i16
761        assert_eq!(i16::from_value(&Value::SmallInt(100)).unwrap(), 100);
762        assert_eq!(i16::from_value(&Value::TinyInt(10)).unwrap(), 10);
763
764        // i32
765        assert_eq!(i32::from_value(&Value::Int(1000)).unwrap(), 1000);
766
767        // i64
768        assert_eq!(i64::from_value(&Value::BigInt(10000)).unwrap(), 10000);
769
770        // f32
771        let pi_f32 = std::f32::consts::PI;
772        let from_float = f32::from_value(&Value::Float(pi_f32)).unwrap();
773        assert!((from_float - pi_f32).abs() < 1e-6);
774
775        // f64
776        let pi_f64 = std::f64::consts::PI;
777        let from_double = f64::from_value(&Value::Double(pi_f64)).unwrap();
778        assert!((from_double - pi_f64).abs() < 1e-12);
779
780        // String
781        assert_eq!(
782            String::from_value(&Value::Text("hello".to_string())).unwrap(),
783            "hello"
784        );
785
786        // Vec<u8>
787        assert_eq!(
788            Vec::<u8>::from_value(&Value::Bytes(vec![1, 2, 3])).unwrap(),
789            vec![1, 2, 3]
790        );
791    }
792
793    #[test]
794    fn test_empty_row() {
795        let row = Row::new(vec![], vec![]);
796        assert!(row.is_empty());
797        assert_eq!(row.len(), 0);
798        assert_eq!(row.get(0), None);
799        assert!(row.get_as::<i32>(0).is_err());
800    }
801
802    #[test]
803    fn test_large_row() {
804        // Test with many columns
805        let n = 100;
806        let names: Vec<_> = (0..n).map(|i| format!("col_{}", i)).collect();
807        let values: Vec<_> = (0..n).map(Value::Int).collect();
808        let row = Row::new(names, values);
809
810        assert_eq!(row.len(), n as usize);
811        assert_eq!(row.get_named::<i32>("col_0").unwrap(), 0);
812        assert_eq!(row.get_named::<i32>("col_50").unwrap(), 50);
813        assert_eq!(row.get_named::<i32>("col_99").unwrap(), 99);
814    }
815
816    #[test]
817    fn test_subset_by_prefix() {
818        let row = Row::new(
819            vec![
820                "heroes__id".to_string(),
821                "heroes__name".to_string(),
822                "teams__id".to_string(),
823                "teams__name".to_string(),
824            ],
825            vec![
826                Value::Int(1),
827                Value::Text("Batman".to_string()),
828                Value::Int(10),
829                Value::Text("Justice League".to_string()),
830            ],
831        );
832
833        // Extract heroes columns
834        let heroes_row = row.subset_by_prefix("heroes");
835        assert_eq!(heroes_row.len(), 2);
836        assert_eq!(heroes_row.get_named::<i32>("id").unwrap(), 1);
837        assert_eq!(heroes_row.get_named::<String>("name").unwrap(), "Batman");
838
839        // Extract teams columns
840        let teams_row = row.subset_by_prefix("teams");
841        assert_eq!(teams_row.len(), 2);
842        assert_eq!(teams_row.get_named::<i32>("id").unwrap(), 10);
843        assert_eq!(
844            teams_row.get_named::<String>("name").unwrap(),
845            "Justice League"
846        );
847
848        // Non-existent prefix returns empty row
849        let empty_row = row.subset_by_prefix("powers");
850        assert!(empty_row.is_empty());
851    }
852
853    #[test]
854    fn test_has_prefix() {
855        let row = Row::new(
856            vec!["heroes__id".to_string(), "teams__id".to_string()],
857            vec![Value::Int(1), Value::Int(10)],
858        );
859
860        assert!(row.has_prefix("heroes"));
861        assert!(row.has_prefix("teams"));
862        assert!(!row.has_prefix("powers"));
863    }
864
865    #[test]
866    fn test_prefix_is_all_null() {
867        let row = Row::new(
868            vec![
869                "heroes__id".to_string(),
870                "heroes__name".to_string(),
871                "teams__id".to_string(),
872                "teams__name".to_string(),
873            ],
874            vec![
875                Value::Int(1),
876                Value::Text("Batman".to_string()),
877                Value::Null,
878                Value::Null,
879            ],
880        );
881
882        // Heroes have values
883        assert!(!row.prefix_is_all_null("heroes"));
884
885        // Teams are all NULL (LEFT JOIN with no match)
886        assert!(row.prefix_is_all_null("teams"));
887
888        // Non-existent prefix is considered "all null"
889        assert!(row.prefix_is_all_null("powers"));
890    }
891
892    // ==================== FromValue Precision Tests ====================
893
894    #[test]
895    fn test_from_value_f32_precision_checks() {
896        // Small f64 values should convert to f32
897        let v = f32::from_value(&Value::Double(1.5)).unwrap();
898        assert!((v - 1.5).abs() < f32::EPSILON);
899
900        // Large f64 values should error
901        let result = f32::from_value(&Value::Double(1e20_f64));
902        assert!(result.is_err());
903
904        // Small integers should convert
905        let v = f32::from_value(&Value::Int(1000)).unwrap();
906        assert!((v - 1000.0).abs() < f32::EPSILON);
907
908        // Large integers (> 2^24) should error
909        let result = f32::from_value(&Value::BigInt(i64::MAX));
910        assert!(result.is_err());
911    }
912
913    #[test]
914    fn test_from_value_f64_precision_checks() {
915        // Small integers should convert
916        let v = f64::from_value(&Value::BigInt(42)).unwrap();
917        assert!((v - 42.0).abs() < f64::EPSILON);
918
919        // Large integers (> 2^53) should error
920        let result = f64::from_value(&Value::BigInt(i64::MAX));
921        assert!(result.is_err());
922
923        // Exactly 2^53 should succeed
924        let boundary = 1i64 << 53;
925        let v = f64::from_value(&Value::BigInt(boundary)).unwrap();
926        assert!((v - boundary as f64).abs() < 1.0);
927
928        // 2^53 + 1 should fail
929        let result = f64::from_value(&Value::BigInt(boundary + 1));
930        assert!(result.is_err());
931    }
932
933    #[test]
934    fn test_from_value_f32_int_boundary() {
935        const F32_MAX_EXACT: i64 = 1 << 24; // 16,777,216
936
937        // At boundary: success
938        #[allow(clippy::cast_possible_truncation)]
939        let boundary = F32_MAX_EXACT as i32;
940        let v = f32::from_value(&Value::Int(boundary)).unwrap();
941        assert!((v - boundary as f32).abs() < 1.0);
942
943        // Just over boundary: error
944        #[allow(clippy::cast_possible_truncation)]
945        let over = (F32_MAX_EXACT + 1) as i32;
946        let result = f32::from_value(&Value::Int(over));
947        assert!(result.is_err());
948    }
949
950    #[test]
951    fn test_from_value_bool_to_f64() {
952        // Bool should convert to f64
953        assert!((f64::from_value(&Value::Bool(true)).unwrap() - 1.0).abs() < f64::EPSILON);
954        assert!((f64::from_value(&Value::Bool(false)).unwrap() - 0.0).abs() < f64::EPSILON);
955    }
956}