mssql_client/
row.rs

1//! Row representation for query results.
2//!
3//! This module implements the `Arc<Bytes>` pattern from ADR-004 for reduced-copy
4//! row data access. The `Row` struct holds a shared reference to the raw packet
5//! buffer, deferring allocation until explicitly requested.
6//!
7//! ## Access Patterns (per ADR-004)
8//!
9//! - `get_bytes()` - Returns borrowed slice into buffer (zero additional allocation)
10//! - `get_str()` - Returns Cow - borrowed if valid UTF-8, owned if conversion needed
11//! - `get_string()` - Allocates new String (explicit allocation)
12//! - `get<T>()` - Type-converting accessor with allocation only if needed
13
14use std::borrow::Cow;
15use std::sync::Arc;
16
17use bytes::Bytes;
18
19use mssql_types::decode::{TypeInfo, decode_value};
20use mssql_types::{FromSql, SqlValue, TypeError};
21
22/// Column slice information pointing into the row buffer.
23///
24/// This is the internal representation that enables zero-copy access
25/// to column data within the shared buffer.
26#[derive(Debug, Clone, Copy)]
27pub struct ColumnSlice {
28    /// Offset into the buffer where this column's data begins.
29    pub offset: u32,
30    /// Length of the column data in bytes.
31    pub length: u32,
32    /// Whether this column value is NULL.
33    pub is_null: bool,
34}
35
36impl ColumnSlice {
37    /// Create a new column slice.
38    pub fn new(offset: u32, length: u32, is_null: bool) -> Self {
39        Self {
40            offset,
41            length,
42            is_null,
43        }
44    }
45
46    /// Create a NULL column slice.
47    pub fn null() -> Self {
48        Self {
49            offset: 0,
50            length: 0,
51            is_null: true,
52        }
53    }
54}
55
56/// Column metadata describing a result set column.
57#[derive(Debug, Clone)]
58pub struct Column {
59    /// Column name.
60    pub name: String,
61    /// Column index (0-based).
62    pub index: usize,
63    /// SQL type name (e.g., "INT", "NVARCHAR").
64    pub type_name: String,
65    /// Whether the column allows NULL values.
66    pub nullable: bool,
67    /// Maximum length for variable-length types.
68    pub max_length: Option<u32>,
69    /// Precision for numeric types.
70    pub precision: Option<u8>,
71    /// Scale for numeric types.
72    pub scale: Option<u8>,
73}
74
75impl Column {
76    /// Create a new column with basic metadata.
77    pub fn new(name: impl Into<String>, index: usize, type_name: impl Into<String>) -> Self {
78        Self {
79            name: name.into(),
80            index,
81            type_name: type_name.into(),
82            nullable: true,
83            max_length: None,
84            precision: None,
85            scale: None,
86        }
87    }
88
89    /// Set whether the column is nullable.
90    #[must_use]
91    pub fn with_nullable(mut self, nullable: bool) -> Self {
92        self.nullable = nullable;
93        self
94    }
95
96    /// Set the maximum length.
97    #[must_use]
98    pub fn with_max_length(mut self, max_length: u32) -> Self {
99        self.max_length = Some(max_length);
100        self
101    }
102
103    /// Set precision and scale for numeric types.
104    #[must_use]
105    pub fn with_precision_scale(mut self, precision: u8, scale: u8) -> Self {
106        self.precision = Some(precision);
107        self.scale = Some(scale);
108        self
109    }
110
111    /// Convert column metadata to TDS TypeInfo for decoding.
112    ///
113    /// Maps type names to TDS type IDs and constructs appropriate TypeInfo.
114    pub fn to_type_info(&self) -> TypeInfo {
115        let type_id = type_name_to_id(&self.type_name);
116        TypeInfo {
117            type_id,
118            length: self.max_length,
119            scale: self.scale,
120            precision: self.precision,
121            collation: None,
122        }
123    }
124}
125
126/// Map SQL type name to TDS type ID.
127fn type_name_to_id(name: &str) -> u8 {
128    match name.to_uppercase().as_str() {
129        // Integer types
130        "INT" | "INTEGER" => 0x38,
131        "BIGINT" => 0x7F,
132        "SMALLINT" => 0x34,
133        "TINYINT" => 0x30,
134        "BIT" => 0x32,
135
136        // Floating point
137        "FLOAT" => 0x3E,
138        "REAL" => 0x3B,
139
140        // Decimal/Numeric
141        "DECIMAL" | "NUMERIC" => 0x6C,
142        "MONEY" | "SMALLMONEY" => 0x6E,
143
144        // String types
145        "NVARCHAR" | "NCHAR" | "NTEXT" => 0xE7,
146        "VARCHAR" | "CHAR" | "TEXT" => 0xA7,
147
148        // Binary types
149        "VARBINARY" | "BINARY" | "IMAGE" => 0xA5,
150
151        // Date/Time types
152        "DATE" => 0x28,
153        "TIME" => 0x29,
154        "DATETIME2" => 0x2A,
155        "DATETIMEOFFSET" => 0x2B,
156        "DATETIME" => 0x3D,
157        "SMALLDATETIME" => 0x3F,
158
159        // GUID
160        "UNIQUEIDENTIFIER" => 0x24,
161
162        // XML
163        "XML" => 0xF1,
164
165        // Nullable variants (INTNTYPE, etc.)
166        _ if name.ends_with("N") => 0x26,
167
168        // Default to binary for unknown types
169        _ => 0xA5,
170    }
171}
172
173/// Shared column metadata for a result set.
174///
175/// This is shared across all rows in the result set to avoid
176/// duplicating metadata per row.
177#[derive(Debug, Clone)]
178pub struct ColMetaData {
179    /// Column definitions.
180    pub columns: Arc<[Column]>,
181}
182
183impl ColMetaData {
184    /// Create new column metadata from a list of columns.
185    pub fn new(columns: Vec<Column>) -> Self {
186        Self {
187            columns: columns.into(),
188        }
189    }
190
191    /// Get the number of columns.
192    #[must_use]
193    pub fn len(&self) -> usize {
194        self.columns.len()
195    }
196
197    /// Check if there are no columns.
198    #[must_use]
199    pub fn is_empty(&self) -> bool {
200        self.columns.is_empty()
201    }
202
203    /// Get a column by index.
204    #[must_use]
205    pub fn get(&self, index: usize) -> Option<&Column> {
206        self.columns.get(index)
207    }
208
209    /// Find a column index by name (case-insensitive).
210    #[must_use]
211    pub fn find_by_name(&self, name: &str) -> Option<usize> {
212        self.columns
213            .iter()
214            .position(|c| c.name.eq_ignore_ascii_case(name))
215    }
216}
217
218/// A row from a query result.
219///
220/// Implements the `Arc<Bytes>` pattern from ADR-004 for reduced memory allocation.
221/// The row holds a shared reference to the raw packet buffer and column slice
222/// information, deferring parsing and allocation until values are accessed.
223///
224/// # Memory Model
225///
226/// ```text
227/// Row {
228///     buffer: Arc<Bytes> ──────────► [raw packet data...]
229///     slices: Arc<[ColumnSlice]> ──► [{offset, length, is_null}, ...]
230///     metadata: Arc<ColMetaData> ──► [Column definitions...]
231/// }
232/// ```
233///
234/// Multiple `Row` instances from the same result set share the `metadata`.
235/// The `buffer` and `slices` are unique per row but use `Arc` for cheap cloning.
236///
237/// # Access Patterns
238///
239/// - **Zero-copy:** `get_bytes()`, `get_str()` (when UTF-8 valid)
240/// - **Allocating:** `get_string()`, `get::<String>()`
241/// - **Type-converting:** `get::<T>()` uses `FromSql` trait
242#[derive(Clone)]
243pub struct Row {
244    /// Shared reference to raw packet body containing row data.
245    buffer: Arc<Bytes>,
246    /// Column offsets into buffer.
247    slices: Arc<[ColumnSlice]>,
248    /// Column metadata (shared across result set).
249    metadata: Arc<ColMetaData>,
250    /// Cached parsed values (lazily populated).
251    /// This maintains backward compatibility with code expecting SqlValue access.
252    values: Option<Arc<[SqlValue]>>,
253}
254
255impl Row {
256    /// Create a new row with the `Arc<Bytes>` pattern.
257    ///
258    /// This is the primary constructor for the reduced-copy pattern.
259    pub fn new(buffer: Arc<Bytes>, slices: Arc<[ColumnSlice]>, metadata: Arc<ColMetaData>) -> Self {
260        Self {
261            buffer,
262            slices,
263            metadata,
264            values: None,
265        }
266    }
267
268    /// Create a row from pre-parsed values (backward compatibility).
269    ///
270    /// This constructor supports existing code that works with `SqlValue` directly.
271    /// It's less efficient than the buffer-based approach but maintains compatibility.
272    #[allow(dead_code)]
273    pub(crate) fn from_values(columns: Vec<Column>, values: Vec<SqlValue>) -> Self {
274        let metadata = Arc::new(ColMetaData::new(columns));
275        let slices: Arc<[ColumnSlice]> = values
276            .iter()
277            .enumerate()
278            .map(|(i, v)| ColumnSlice::new(i as u32, 0, v.is_null()))
279            .collect::<Vec<_>>()
280            .into();
281
282        Self {
283            buffer: Arc::new(Bytes::new()),
284            slices,
285            metadata,
286            values: Some(values.into()),
287        }
288    }
289
290    // ========================================================================
291    // Zero-Copy Access Methods (ADR-004)
292    // ========================================================================
293
294    /// Returns borrowed slice into buffer (zero additional allocation).
295    ///
296    /// This is the most efficient access method when you need raw bytes.
297    #[must_use]
298    pub fn get_bytes(&self, index: usize) -> Option<&[u8]> {
299        let slice = self.slices.get(index)?;
300        if slice.is_null {
301            return None;
302        }
303
304        let start = slice.offset as usize;
305        let end = start + slice.length as usize;
306
307        if end <= self.buffer.len() {
308            Some(&self.buffer[start..end])
309        } else {
310            None
311        }
312    }
313
314    /// Returns Cow - borrowed if valid UTF-8, owned if conversion needed.
315    ///
316    /// For UTF-8 data, this returns a borrowed reference (zero allocation).
317    /// For UTF-16 data (NVARCHAR), this allocates a new String.
318    #[must_use]
319    pub fn get_str(&self, index: usize) -> Option<Cow<'_, str>> {
320        let bytes = self.get_bytes(index)?;
321
322        // Try to interpret as UTF-8 first (zero allocation for ASCII/UTF-8 data)
323        match std::str::from_utf8(bytes) {
324            Ok(s) => Some(Cow::Borrowed(s)),
325            Err(_) => {
326                // Assume UTF-16LE (SQL Server NVARCHAR encoding)
327                // This requires allocation for the conversion
328                let utf16: Vec<u16> = bytes
329                    .chunks_exact(2)
330                    .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
331                    .collect();
332
333                String::from_utf16(&utf16).ok().map(Cow::Owned)
334            }
335        }
336    }
337
338    /// Allocates new String (explicit allocation).
339    ///
340    /// Use this when you need an owned String.
341    #[must_use]
342    pub fn get_string(&self, index: usize) -> Option<String> {
343        self.get_str(index).map(|cow| cow.into_owned())
344    }
345
346    // ========================================================================
347    // Type-Converting Access (FromSql trait)
348    // ========================================================================
349
350    /// Get a value by column index with type conversion.
351    ///
352    /// Uses the `FromSql` trait to convert the raw value to the requested type.
353    pub fn get<T: FromSql>(&self, index: usize) -> Result<T, TypeError> {
354        // If we have cached values, use them
355        if let Some(ref values) = self.values {
356            return values
357                .get(index)
358                .ok_or_else(|| TypeError::TypeMismatch {
359                    expected: "valid column index",
360                    actual: format!("index {index} out of bounds"),
361                })
362                .and_then(T::from_sql);
363        }
364
365        // Otherwise, parse on demand from the buffer
366        let slice = self
367            .slices
368            .get(index)
369            .ok_or_else(|| TypeError::TypeMismatch {
370                expected: "valid column index",
371                actual: format!("index {index} out of bounds"),
372            })?;
373
374        if slice.is_null {
375            return Err(TypeError::UnexpectedNull);
376        }
377
378        // Parse via SqlValue then convert to target type
379        // Note: parse_value uses zero-copy buffer slicing (Arc<Bytes>::slice)
380        let value = self.parse_value(index, slice)?;
381        T::from_sql(&value)
382    }
383
384    /// Get a value by column name with type conversion.
385    pub fn get_by_name<T: FromSql>(&self, name: &str) -> Result<T, TypeError> {
386        let index = self
387            .metadata
388            .find_by_name(name)
389            .ok_or_else(|| TypeError::TypeMismatch {
390                expected: "valid column name",
391                actual: format!("column '{name}' not found"),
392            })?;
393
394        self.get(index)
395    }
396
397    /// Try to get a value by column index, returning None if NULL or not found.
398    pub fn try_get<T: FromSql>(&self, index: usize) -> Option<T> {
399        // If we have cached values, use them
400        if let Some(ref values) = self.values {
401            return values
402                .get(index)
403                .and_then(|v| T::from_sql_nullable(v).ok().flatten());
404        }
405
406        // Otherwise check the slice
407        let slice = self.slices.get(index)?;
408        if slice.is_null {
409            return None;
410        }
411
412        self.get(index).ok()
413    }
414
415    /// Try to get a value by column name, returning None if NULL or not found.
416    pub fn try_get_by_name<T: FromSql>(&self, name: &str) -> Option<T> {
417        let index = self.metadata.find_by_name(name)?;
418        self.try_get(index)
419    }
420
421    // ========================================================================
422    // Raw Value Access (backward compatibility)
423    // ========================================================================
424
425    /// Get the raw SQL value by index.
426    ///
427    /// Note: This may allocate if values haven't been cached.
428    #[must_use]
429    pub fn get_raw(&self, index: usize) -> Option<SqlValue> {
430        if let Some(ref values) = self.values {
431            return values.get(index).cloned();
432        }
433
434        let slice = self.slices.get(index)?;
435        self.parse_value(index, slice).ok()
436    }
437
438    /// Get the raw SQL value by column name.
439    #[must_use]
440    pub fn get_raw_by_name(&self, name: &str) -> Option<SqlValue> {
441        let index = self.metadata.find_by_name(name)?;
442        self.get_raw(index)
443    }
444
445    // ========================================================================
446    // Metadata Access
447    // ========================================================================
448
449    /// Get the number of columns in the row.
450    #[must_use]
451    pub fn len(&self) -> usize {
452        self.slices.len()
453    }
454
455    /// Check if the row is empty.
456    #[must_use]
457    pub fn is_empty(&self) -> bool {
458        self.slices.is_empty()
459    }
460
461    /// Get the column metadata.
462    #[must_use]
463    pub fn columns(&self) -> &[Column] {
464        &self.metadata.columns
465    }
466
467    /// Get the shared column metadata.
468    #[must_use]
469    pub fn metadata(&self) -> &Arc<ColMetaData> {
470        &self.metadata
471    }
472
473    /// Check if a column value is NULL.
474    #[must_use]
475    pub fn is_null(&self, index: usize) -> bool {
476        self.slices.get(index).map(|s| s.is_null).unwrap_or(true)
477    }
478
479    /// Check if a column value is NULL by name.
480    #[must_use]
481    pub fn is_null_by_name(&self, name: &str) -> bool {
482        self.metadata
483            .find_by_name(name)
484            .map(|i| self.is_null(i))
485            .unwrap_or(true)
486    }
487
488    // ========================================================================
489    // Internal Helpers
490    // ========================================================================
491
492    /// Parse a value from the buffer at the given slice.
493    ///
494    /// Uses the mssql-types decode module for efficient binary parsing.
495    /// Optimized to use zero-copy buffer slicing via Arc<Bytes>.
496    fn parse_value(&self, index: usize, slice: &ColumnSlice) -> Result<SqlValue, TypeError> {
497        if slice.is_null {
498            return Ok(SqlValue::Null);
499        }
500
501        let column = self
502            .metadata
503            .get(index)
504            .ok_or_else(|| TypeError::TypeMismatch {
505                expected: "valid column metadata",
506                actual: format!("no metadata for column {index}"),
507            })?;
508
509        // Calculate byte range for this column
510        let start = slice.offset as usize;
511        let end = start + slice.length as usize;
512
513        // Validate range
514        if end > self.buffer.len() {
515            return Err(TypeError::TypeMismatch {
516                expected: "valid byte range",
517                actual: format!(
518                    "range {}..{} exceeds buffer length {}",
519                    start,
520                    end,
521                    self.buffer.len()
522                ),
523            });
524        }
525
526        // Convert column metadata to TypeInfo for the decode module
527        let type_info = column.to_type_info();
528
529        // Use zero-copy slice of the buffer instead of allocating
530        // This avoids the overhead of Bytes::copy_from_slice
531        let mut buf = self.buffer.slice(start..end);
532
533        // Use the unified decode module for efficient parsing
534        decode_value(&mut buf, &type_info)
535    }
536}
537
538impl std::fmt::Debug for Row {
539    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
540        f.debug_struct("Row")
541            .field("columns", &self.metadata.columns.len())
542            .field("buffer_size", &self.buffer.len())
543            .field("has_cached_values", &self.values.is_some())
544            .finish()
545    }
546}
547
548/// Iterator over row values as SqlValue.
549pub struct RowIter<'a> {
550    row: &'a Row,
551    index: usize,
552}
553
554impl Iterator for RowIter<'_> {
555    type Item = SqlValue;
556
557    fn next(&mut self) -> Option<Self::Item> {
558        if self.index >= self.row.len() {
559            return None;
560        }
561        let value = self.row.get_raw(self.index);
562        self.index += 1;
563        value
564    }
565
566    fn size_hint(&self) -> (usize, Option<usize>) {
567        let remaining = self.row.len() - self.index;
568        (remaining, Some(remaining))
569    }
570}
571
572impl<'a> IntoIterator for &'a Row {
573    type Item = SqlValue;
574    type IntoIter = RowIter<'a>;
575
576    fn into_iter(self) -> Self::IntoIter {
577        RowIter {
578            row: self,
579            index: 0,
580        }
581    }
582}
583
584#[cfg(test)]
585#[allow(clippy::unwrap_used)]
586mod tests {
587    use super::*;
588
589    #[test]
590    fn test_column_slice_null() {
591        let slice = ColumnSlice::null();
592        assert!(slice.is_null);
593        assert_eq!(slice.offset, 0);
594        assert_eq!(slice.length, 0);
595    }
596
597    #[test]
598    fn test_column_metadata() {
599        let col = Column::new("id", 0, "INT")
600            .with_nullable(false)
601            .with_precision_scale(10, 0);
602
603        assert_eq!(col.name, "id");
604        assert_eq!(col.index, 0);
605        assert!(!col.nullable);
606        assert_eq!(col.precision, Some(10));
607    }
608
609    #[test]
610    fn test_col_metadata_find_by_name() {
611        let meta = ColMetaData::new(vec![
612            Column::new("id", 0, "INT"),
613            Column::new("Name", 1, "NVARCHAR"),
614        ]);
615
616        assert_eq!(meta.find_by_name("id"), Some(0));
617        assert_eq!(meta.find_by_name("ID"), Some(0)); // case-insensitive
618        assert_eq!(meta.find_by_name("name"), Some(1));
619        assert_eq!(meta.find_by_name("unknown"), None);
620    }
621
622    #[test]
623    fn test_row_from_values_backward_compat() {
624        let columns = vec![
625            Column::new("id", 0, "INT"),
626            Column::new("name", 1, "NVARCHAR"),
627        ];
628        let values = vec![SqlValue::Int(42), SqlValue::String("Alice".to_string())];
629
630        let row = Row::from_values(columns, values);
631
632        assert_eq!(row.len(), 2);
633        assert_eq!(row.get::<i32>(0).unwrap(), 42);
634        assert_eq!(row.get_by_name::<String>("name").unwrap(), "Alice");
635    }
636
637    #[test]
638    fn test_row_is_null() {
639        let columns = vec![
640            Column::new("id", 0, "INT"),
641            Column::new("nullable_col", 1, "NVARCHAR"),
642        ];
643        let values = vec![SqlValue::Int(1), SqlValue::Null];
644
645        let row = Row::from_values(columns, values);
646
647        assert!(!row.is_null(0));
648        assert!(row.is_null(1));
649        assert!(row.is_null(99)); // Out of bounds returns true
650    }
651
652    #[test]
653    fn test_row_get_bytes_with_buffer() {
654        let buffer = Arc::new(Bytes::from_static(b"Hello World"));
655        let slices: Arc<[ColumnSlice]> = vec![
656            ColumnSlice::new(0, 5, false), // "Hello"
657            ColumnSlice::new(6, 5, false), // "World"
658        ]
659        .into();
660        let meta = Arc::new(ColMetaData::new(vec![
661            Column::new("greeting", 0, "VARCHAR"),
662            Column::new("subject", 1, "VARCHAR"),
663        ]));
664
665        let row = Row::new(buffer, slices, meta);
666
667        assert_eq!(row.get_bytes(0), Some(b"Hello".as_slice()));
668        assert_eq!(row.get_bytes(1), Some(b"World".as_slice()));
669    }
670
671    #[test]
672    fn test_row_get_str() {
673        let buffer = Arc::new(Bytes::from_static(b"Test"));
674        let slices: Arc<[ColumnSlice]> = vec![ColumnSlice::new(0, 4, false)].into();
675        let meta = Arc::new(ColMetaData::new(vec![Column::new("val", 0, "VARCHAR")]));
676
677        let row = Row::new(buffer, slices, meta);
678
679        let s = row.get_str(0).unwrap();
680        assert_eq!(s, "Test");
681        // Should be borrowed for valid UTF-8
682        assert!(matches!(s, Cow::Borrowed(_)));
683    }
684
685    #[test]
686    fn test_row_metadata_access() {
687        let columns = vec![Column::new("col1", 0, "INT")];
688        let row = Row::from_values(columns, vec![SqlValue::Int(1)]);
689
690        assert_eq!(row.columns().len(), 1);
691        assert_eq!(row.columns()[0].name, "col1");
692        assert_eq!(row.metadata().len(), 1);
693    }
694}