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::__private::decode_value;
20use mssql_types::decode::TypeInfo;
21use mssql_types::{FromSql, SqlValue, TypeError};
22
23use crate::blob::BlobReader;
24
25/// Column slice information pointing into the row buffer.
26///
27/// This is the internal representation that enables zero-copy access
28/// to column data within the shared buffer.
29#[derive(Debug, Clone, Copy)]
30#[non_exhaustive]
31pub struct ColumnSlice {
32 /// Offset into the buffer where this column's data begins.
33 pub offset: u32,
34 /// Length of the column data in bytes.
35 pub length: u32,
36 /// Whether this column value is NULL.
37 pub is_null: bool,
38}
39
40impl ColumnSlice {
41 /// Create a new column slice.
42 pub fn new(offset: u32, length: u32, is_null: bool) -> Self {
43 Self {
44 offset,
45 length,
46 is_null,
47 }
48 }
49
50 /// Create a NULL column slice.
51 pub fn null() -> Self {
52 Self {
53 offset: 0,
54 length: 0,
55 is_null: true,
56 }
57 }
58}
59
60/// Column metadata describing a result set column.
61///
62/// This struct is marked `#[non_exhaustive]` to allow adding new fields
63/// in future versions without breaking semver compatibility. Use
64/// [`Column::new()`] or builder methods to construct instances.
65#[derive(Debug, Clone)]
66#[non_exhaustive]
67pub struct Column {
68 /// Column name.
69 pub name: String,
70 /// Column index (0-based).
71 pub index: usize,
72 /// SQL type name (e.g., "INT", "NVARCHAR").
73 pub type_name: String,
74 /// Whether the column allows NULL values.
75 pub nullable: bool,
76 /// Maximum length for variable-length types.
77 pub max_length: Option<u32>,
78 /// Precision for numeric types.
79 pub precision: Option<u8>,
80 /// Scale for numeric types.
81 pub scale: Option<u8>,
82 /// Collation for string types (VARCHAR, CHAR, TEXT).
83 ///
84 /// Used for proper encoding/decoding of non-Unicode string data.
85 /// When present, enables collation-aware decoding that correctly
86 /// handles locale-specific ANSI encodings (e.g., Shift_JIS, GB18030).
87 pub collation: Option<tds_protocol::Collation>,
88}
89
90impl Column {
91 /// Create a new column with basic metadata.
92 pub fn new(name: impl Into<String>, index: usize, type_name: impl Into<String>) -> Self {
93 Self {
94 name: name.into(),
95 index,
96 type_name: type_name.into(),
97 nullable: true,
98 max_length: None,
99 precision: None,
100 scale: None,
101 collation: None,
102 }
103 }
104
105 /// Set whether the column is nullable.
106 #[must_use]
107 pub fn with_nullable(mut self, nullable: bool) -> Self {
108 self.nullable = nullable;
109 self
110 }
111
112 /// Set the maximum length.
113 #[must_use]
114 pub fn with_max_length(mut self, max_length: u32) -> Self {
115 self.max_length = Some(max_length);
116 self
117 }
118
119 /// Set precision and scale for numeric types.
120 #[must_use]
121 pub fn with_precision_scale(mut self, precision: u8, scale: u8) -> Self {
122 self.precision = Some(precision);
123 self.scale = Some(scale);
124 self
125 }
126
127 /// Set the collation for string types.
128 ///
129 /// Used for proper encoding/decoding of non-Unicode string data (VARCHAR, CHAR, TEXT).
130 #[must_use]
131 pub fn with_collation(mut self, collation: tds_protocol::Collation) -> Self {
132 self.collation = Some(collation);
133 self
134 }
135
136 /// Get the encoding name for this column's collation.
137 ///
138 /// Returns the name of the character encoding used for this column's data,
139 /// or "unknown" if the collation is not set or the encoding feature is disabled.
140 ///
141 /// # Examples
142 ///
143 /// - `"Shift_JIS"` - Japanese encoding (LCID 0x0411)
144 /// - `"GB18030"` - Simplified Chinese (LCID 0x0804)
145 /// - `"UTF-8"` - SQL Server 2019+ UTF-8 collation
146 /// - `"windows-1252"` - Latin/Western European (LCID 0x0409)
147 /// - `"unknown"` - No collation or unsupported encoding
148 #[must_use]
149 pub fn encoding_name(&self) -> &'static str {
150 #[cfg(feature = "encoding")]
151 if let Some(ref collation) = self.collation {
152 return collation.encoding_name();
153 }
154 "unknown"
155 }
156
157 /// Check if this column uses UTF-8 encoding.
158 ///
159 /// Returns `true` if the column has a SQL Server 2019+ UTF-8 collation,
160 /// which is indicated by fUTF8 (bit 26, 0x0400_0000) being set in the
161 /// collation info field.
162 #[must_use]
163 pub fn is_utf8_collation(&self) -> bool {
164 #[cfg(feature = "encoding")]
165 if let Some(ref collation) = self.collation {
166 return collation.is_utf8();
167 }
168 false
169 }
170
171 /// Convert column metadata to TDS TypeInfo for decoding.
172 ///
173 /// Maps type names to TDS type IDs and constructs appropriate TypeInfo.
174 pub fn to_type_info(&self) -> TypeInfo {
175 let type_id = type_name_to_id(&self.type_name);
176 TypeInfo {
177 type_id,
178 length: self.max_length,
179 scale: self.scale,
180 precision: self.precision,
181 collation: self.collation.map(|c| mssql_types::decode::Collation {
182 lcid: c.lcid,
183 flags: c.sort_id,
184 }),
185 }
186 }
187}
188
189/// Map SQL type name to TDS type ID.
190fn type_name_to_id(name: &str) -> u8 {
191 match name.to_uppercase().as_str() {
192 // Integer types
193 "INT" | "INTEGER" => 0x38,
194 "BIGINT" => 0x7F,
195 "SMALLINT" => 0x34,
196 "TINYINT" => 0x30,
197 "BIT" => 0x32,
198
199 // Floating point
200 "FLOAT" => 0x3E,
201 "REAL" => 0x3B,
202
203 // Decimal/Numeric
204 "DECIMAL" | "NUMERIC" => 0x6C,
205 "MONEY" | "SMALLMONEY" => 0x6E,
206
207 // String types
208 "NVARCHAR" | "NCHAR" | "NTEXT" => 0xE7,
209 "VARCHAR" | "CHAR" | "TEXT" => 0xA7,
210
211 // Binary types
212 "VARBINARY" | "BINARY" | "IMAGE" => 0xA5,
213
214 // Date/Time types
215 "DATE" => 0x28,
216 "TIME" => 0x29,
217 "DATETIME2" => 0x2A,
218 "DATETIMEOFFSET" => 0x2B,
219 "DATETIME" => 0x3D,
220 "SMALLDATETIME" => 0x3F,
221
222 // GUID
223 "UNIQUEIDENTIFIER" => 0x24,
224
225 // XML
226 "XML" => 0xF1,
227
228 // Nullable variants (INTNTYPE, etc.)
229 _ if name.ends_with("N") => 0x26,
230
231 // Default to binary for unknown types
232 _ => 0xA5,
233 }
234}
235
236/// Shared column metadata for a result set.
237///
238/// This is shared across all rows in the result set to avoid
239/// duplicating metadata per row.
240#[derive(Debug, Clone)]
241pub struct ColMetaData {
242 /// Column definitions.
243 pub columns: Arc<[Column]>,
244}
245
246impl ColMetaData {
247 /// Create new column metadata from a list of columns.
248 pub fn new(columns: Vec<Column>) -> Self {
249 Self {
250 columns: columns.into(),
251 }
252 }
253
254 /// Get the number of columns.
255 #[must_use]
256 pub fn len(&self) -> usize {
257 self.columns.len()
258 }
259
260 /// Check if there are no columns.
261 #[must_use]
262 pub fn is_empty(&self) -> bool {
263 self.columns.is_empty()
264 }
265
266 /// Get a column by index.
267 #[must_use]
268 pub fn get(&self, index: usize) -> Option<&Column> {
269 self.columns.get(index)
270 }
271
272 /// Find a column index by name (case-insensitive).
273 #[must_use]
274 pub fn find_by_name(&self, name: &str) -> Option<usize> {
275 self.columns
276 .iter()
277 .position(|c| c.name.eq_ignore_ascii_case(name))
278 }
279}
280
281/// A row from a query result.
282///
283/// Implements the `Arc<Bytes>` pattern from ADR-004 for reduced memory allocation.
284/// The row holds a shared reference to the raw packet buffer and column slice
285/// information, deferring parsing and allocation until values are accessed.
286///
287/// # Memory Model
288///
289/// ```text
290/// Row {
291/// buffer: Arc<Bytes> ──────────► [raw packet data...]
292/// slices: Arc<[ColumnSlice]> ──► [{offset, length, is_null}, ...]
293/// metadata: Arc<ColMetaData> ──► [Column definitions...]
294/// }
295/// ```
296///
297/// Multiple `Row` instances from the same result set share the `metadata`.
298/// The `buffer` and `slices` are unique per row but use `Arc` for cheap cloning.
299///
300/// # Access Patterns
301///
302/// - **Zero-copy:** `get_bytes()`, `get_str()` (when UTF-8 valid)
303/// - **Allocating:** `get_string()`, `get::<String>()`
304/// - **Type-converting:** `get::<T>()` uses `FromSql` trait
305#[derive(Clone)]
306pub struct Row {
307 /// Shared reference to raw packet body containing row data.
308 buffer: Arc<Bytes>,
309 /// Column offsets into buffer.
310 slices: Arc<[ColumnSlice]>,
311 /// Column metadata (shared across result set).
312 metadata: Arc<ColMetaData>,
313 /// Cached parsed values (lazily populated).
314 /// This maintains backward compatibility with code expecting SqlValue access.
315 values: Option<Arc<[SqlValue]>>,
316}
317
318impl Row {
319 /// Create a new row with the `Arc<Bytes>` pattern.
320 ///
321 /// This is the primary constructor for the reduced-copy pattern.
322 pub fn new(buffer: Arc<Bytes>, slices: Arc<[ColumnSlice]>, metadata: Arc<ColMetaData>) -> Self {
323 Self {
324 buffer,
325 slices,
326 metadata,
327 values: None,
328 }
329 }
330
331 /// Create a row from pre-parsed values (backward compatibility).
332 ///
333 /// This constructor supports existing code that works with `SqlValue` directly.
334 /// It's less efficient than the buffer-based approach but maintains compatibility.
335 pub fn from_values(columns: Vec<Column>, values: Vec<SqlValue>) -> Self {
336 let metadata = Arc::new(ColMetaData::new(columns));
337 let slices: Arc<[ColumnSlice]> = values
338 .iter()
339 .enumerate()
340 .map(|(i, v)| ColumnSlice::new(i as u32, 0, v.is_null()))
341 .collect::<Vec<_>>()
342 .into();
343
344 Self {
345 buffer: Arc::new(Bytes::new()),
346 slices,
347 metadata,
348 values: Some(values.into()),
349 }
350 }
351
352 // ========================================================================
353 // Zero-Copy Access Methods (ADR-004)
354 // ========================================================================
355
356 /// Returns borrowed slice into buffer (zero additional allocation).
357 ///
358 /// This is the most efficient access method when you need raw bytes.
359 #[must_use]
360 pub fn get_bytes(&self, index: usize) -> Option<&[u8]> {
361 let slice = self.slices.get(index)?;
362 if slice.is_null {
363 return None;
364 }
365
366 let start = slice.offset as usize;
367 let end = start + slice.length as usize;
368
369 if end <= self.buffer.len() {
370 Some(&self.buffer[start..end])
371 } else {
372 None
373 }
374 }
375
376 /// Returns Cow - borrowed if valid UTF-8, owned if conversion needed.
377 ///
378 /// For UTF-8 data, this returns a borrowed reference (zero allocation).
379 /// For VARCHAR data with collation, uses collation-aware decoding.
380 /// For UTF-16 data (NVARCHAR), decodes as UTF-16LE.
381 ///
382 /// # Collation-Aware Decoding
383 ///
384 /// When the `encoding` feature is enabled and the column has collation metadata,
385 /// VARCHAR data is decoded using the appropriate character encoding based on the
386 /// collation's LCID. This correctly handles:
387 ///
388 /// - Japanese (Shift_JIS/CP932)
389 /// - Simplified Chinese (GB18030/CP936)
390 /// - Traditional Chinese (Big5/CP950)
391 /// - Korean (EUC-KR/CP949)
392 /// - Windows code pages 874, 1250-1258
393 /// - SQL Server 2019+ UTF-8 collations
394 #[must_use]
395 pub fn get_str(&self, index: usize) -> Option<Cow<'_, str>> {
396 let bytes = self.get_bytes(index)?;
397
398 // Try to interpret as UTF-8 first (zero allocation for ASCII/UTF-8 data)
399 match std::str::from_utf8(bytes) {
400 Ok(s) => Some(Cow::Borrowed(s)),
401 Err(_) => {
402 // Check if we have collation metadata for this column
403 #[cfg(feature = "encoding")]
404 if let Some(column) = self.metadata.get(index) {
405 if let Some(ref collation) = column.collation {
406 // Use collation-aware decoding for VARCHAR/CHAR types
407 if let Some(encoding) = collation.encoding() {
408 let (decoded, _, had_errors) = encoding.decode(bytes);
409 if had_errors {
410 tracing::warn!(
411 column_name = %column.name,
412 column_index = index,
413 encoding = %encoding.name(),
414 lcid = collation.lcid,
415 byte_len = bytes.len(),
416 "collation-aware decoding had errors, falling back to UTF-16LE"
417 );
418 } else {
419 return Some(Cow::Owned(decoded.into_owned()));
420 }
421 } else {
422 tracing::debug!(
423 column_name = %column.name,
424 column_index = index,
425 lcid = collation.lcid,
426 "no encoding found for LCID, falling back to UTF-16LE"
427 );
428 }
429 }
430 }
431
432 // Assume UTF-16LE (SQL Server NVARCHAR encoding)
433 // This requires allocation for the conversion
434 let utf16: Vec<u16> = bytes
435 .chunks_exact(2)
436 .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
437 .collect();
438
439 String::from_utf16(&utf16).ok().map(Cow::Owned)
440 }
441 }
442 }
443
444 /// Allocates new String (explicit allocation).
445 ///
446 /// Use this when you need an owned String.
447 #[must_use]
448 pub fn get_string(&self, index: usize) -> Option<String> {
449 self.get_str(index).map(|cow| cow.into_owned())
450 }
451
452 // ========================================================================
453 // Streaming Access (LOB support)
454 // ========================================================================
455
456 /// Get a streaming reader for a binary/text column.
457 ///
458 /// Returns a [`BlobReader`] that implements [`tokio::io::AsyncRead`] for
459 /// streaming access to large binary or text columns. This is useful for:
460 ///
461 /// - Streaming large data to files without fully loading into memory
462 /// - Processing data in chunks with progress tracking
463 /// - Copying data between I/O destinations efficiently
464 ///
465 /// # Supported Column Types
466 ///
467 /// - `VARBINARY`, `VARBINARY(MAX)`
468 /// - `VARCHAR`, `VARCHAR(MAX)`
469 /// - `NVARCHAR`, `NVARCHAR(MAX)`
470 /// - `TEXT`, `NTEXT`, `IMAGE` (legacy types)
471 /// - `XML`
472 ///
473 /// # Example
474 ///
475 /// ```text
476 /// use tokio::io::AsyncWriteExt;
477 ///
478 /// // Stream a large VARBINARY(MAX) column to a file
479 /// let mut reader = row.get_stream(0)?;
480 /// let mut file = tokio::fs::File::create("output.bin").await?;
481 /// tokio::io::copy(&mut reader, &mut file).await?;
482 /// ```
483 ///
484 /// # Returns
485 ///
486 /// - `Some(BlobReader)` if the column contains binary/text data
487 /// - `None` if the column is NULL or the index is out of bounds
488 #[must_use]
489 pub fn get_stream(&self, index: usize) -> Option<BlobReader> {
490 let slice = self.slices.get(index)?;
491 if slice.is_null {
492 return None;
493 }
494
495 let start = slice.offset as usize;
496 let end = start + slice.length as usize;
497
498 if end <= self.buffer.len() {
499 // Use zero-copy slicing from Arc<Bytes>
500 let data = self.buffer.slice(start..end);
501 Some(BlobReader::from_bytes(data))
502 } else {
503 None
504 }
505 }
506
507 /// Get a streaming reader for a binary/text column by name.
508 ///
509 /// See [`get_stream`](Self::get_stream) for details.
510 ///
511 /// # Example
512 ///
513 /// ```text
514 /// let mut reader = row.get_stream_by_name("document_content")?;
515 /// // Process the blob stream...
516 /// ```
517 #[must_use]
518 pub fn get_stream_by_name(&self, name: &str) -> Option<BlobReader> {
519 let index = self.metadata.find_by_name(name)?;
520 self.get_stream(index)
521 }
522
523 // ========================================================================
524 // Type-Converting Access (FromSql trait)
525 // ========================================================================
526
527 /// Get a value by column index with type conversion.
528 ///
529 /// Uses the `FromSql` trait to convert the raw value to the requested type.
530 pub fn get<T: FromSql>(&self, index: usize) -> Result<T, TypeError> {
531 // If we have cached values, use them
532 if let Some(ref values) = self.values {
533 return values
534 .get(index)
535 .ok_or_else(|| TypeError::TypeMismatch {
536 expected: "valid column index",
537 actual: format!("index {index} out of bounds"),
538 })
539 .and_then(T::from_sql);
540 }
541
542 // Otherwise, parse on demand from the buffer
543 let slice = self
544 .slices
545 .get(index)
546 .ok_or_else(|| TypeError::TypeMismatch {
547 expected: "valid column index",
548 actual: format!("index {index} out of bounds"),
549 })?;
550
551 if slice.is_null {
552 return Err(TypeError::UnexpectedNull);
553 }
554
555 // Parse via SqlValue then convert to target type
556 // Note: parse_value uses zero-copy buffer slicing (Arc<Bytes>::slice)
557 let value = self.parse_value(index, slice)?;
558 T::from_sql(&value)
559 }
560
561 /// Get a value by column name with type conversion.
562 pub fn get_by_name<T: FromSql>(&self, name: &str) -> Result<T, TypeError> {
563 let index = self
564 .metadata
565 .find_by_name(name)
566 .ok_or_else(|| TypeError::TypeMismatch {
567 expected: "valid column name",
568 actual: format!("column '{name}' not found"),
569 })?;
570
571 self.get(index)
572 }
573
574 /// Try to get a value by column index.
575 ///
576 /// Returns `Ok(None)` when the column is NULL or the index is out of
577 /// bounds. Decode and conversion failures are errors — they were
578 /// previously swallowed as `None`, which made a type mismatch on a
579 /// nullable column silently read as NULL (issue #157).
580 ///
581 /// # Errors
582 ///
583 /// Returns [`TypeError`] if the column value cannot be decoded or
584 /// converted to `T`.
585 pub fn try_get<T: FromSql>(&self, index: usize) -> Result<Option<T>, TypeError> {
586 // If we have cached values, use them
587 if let Some(ref values) = self.values {
588 return match values.get(index) {
589 Some(v) => T::from_sql_nullable(v),
590 None => Ok(None),
591 };
592 }
593
594 // Otherwise check the slice
595 let Some(slice) = self.slices.get(index) else {
596 return Ok(None);
597 };
598 if slice.is_null {
599 return Ok(None);
600 }
601
602 self.get(index).map(Some)
603 }
604
605 /// Try to get a value by column name.
606 ///
607 /// Returns `Ok(None)` when the column is NULL or no column with this
608 /// name exists. Decode and conversion failures are errors — see
609 /// [`try_get`](Self::try_get).
610 ///
611 /// # Errors
612 ///
613 /// Returns [`TypeError`] if the column value cannot be decoded or
614 /// converted to `T`.
615 pub fn try_get_by_name<T: FromSql>(&self, name: &str) -> Result<Option<T>, TypeError> {
616 match self.metadata.find_by_name(name) {
617 Some(index) => self.try_get(index),
618 None => Ok(None),
619 }
620 }
621
622 // ========================================================================
623 // Raw Value Access (backward compatibility)
624 // ========================================================================
625
626 /// Get the raw SQL value by index.
627 ///
628 /// Note: This may allocate if values haven't been cached.
629 #[must_use]
630 pub fn get_raw(&self, index: usize) -> Option<SqlValue> {
631 if let Some(ref values) = self.values {
632 return values.get(index).cloned();
633 }
634
635 let slice = self.slices.get(index)?;
636 self.parse_value(index, slice).ok()
637 }
638
639 /// Get the raw SQL value by column name.
640 #[must_use]
641 pub fn get_raw_by_name(&self, name: &str) -> Option<SqlValue> {
642 let index = self.metadata.find_by_name(name)?;
643 self.get_raw(index)
644 }
645
646 // ========================================================================
647 // Metadata Access
648 // ========================================================================
649
650 /// Get the number of columns in the row.
651 #[must_use]
652 pub fn len(&self) -> usize {
653 self.slices.len()
654 }
655
656 /// Check if the row is empty.
657 #[must_use]
658 pub fn is_empty(&self) -> bool {
659 self.slices.is_empty()
660 }
661
662 /// Get the column metadata.
663 #[must_use]
664 pub fn columns(&self) -> &[Column] {
665 &self.metadata.columns
666 }
667
668 /// Get the shared column metadata.
669 #[must_use]
670 pub fn metadata(&self) -> &Arc<ColMetaData> {
671 &self.metadata
672 }
673
674 /// Check if a column value is NULL.
675 #[must_use]
676 pub fn is_null(&self, index: usize) -> bool {
677 self.slices.get(index).map(|s| s.is_null).unwrap_or(true)
678 }
679
680 /// Check if a column value is NULL by name.
681 #[must_use]
682 pub fn is_null_by_name(&self, name: &str) -> bool {
683 self.metadata
684 .find_by_name(name)
685 .map(|i| self.is_null(i))
686 .unwrap_or(true)
687 }
688
689 // ========================================================================
690 // Internal Helpers
691 // ========================================================================
692
693 /// Parse a value from the buffer at the given slice.
694 ///
695 /// Uses the mssql-types decode module for efficient binary parsing.
696 /// Optimized to use zero-copy buffer slicing via Arc<Bytes>.
697 fn parse_value(&self, index: usize, slice: &ColumnSlice) -> Result<SqlValue, TypeError> {
698 if slice.is_null {
699 return Ok(SqlValue::Null);
700 }
701
702 let column = self
703 .metadata
704 .get(index)
705 .ok_or_else(|| TypeError::TypeMismatch {
706 expected: "valid column metadata",
707 actual: format!("no metadata for column {index}"),
708 })?;
709
710 // Calculate byte range for this column
711 let start = slice.offset as usize;
712 let end = start + slice.length as usize;
713
714 // Validate range
715 if end > self.buffer.len() {
716 return Err(TypeError::TypeMismatch {
717 expected: "valid byte range",
718 actual: format!(
719 "range {}..{} exceeds buffer length {}",
720 start,
721 end,
722 self.buffer.len()
723 ),
724 });
725 }
726
727 // Convert column metadata to TypeInfo for the decode module
728 let type_info = column.to_type_info();
729
730 // Use zero-copy slice of the buffer instead of allocating
731 // This avoids the overhead of Bytes::copy_from_slice
732 let mut buf = self.buffer.slice(start..end);
733
734 // Use the unified decode module for efficient parsing
735 decode_value(&mut buf, &type_info)
736 }
737}
738
739impl std::fmt::Debug for Row {
740 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
741 f.debug_struct("Row")
742 .field("columns", &self.metadata.columns.len())
743 .field("buffer_size", &self.buffer.len())
744 .field("has_cached_values", &self.values.is_some())
745 .finish()
746 }
747}
748
749/// Iterator over row values as SqlValue.
750pub struct RowIter<'a> {
751 row: &'a Row,
752 index: usize,
753}
754
755impl Iterator for RowIter<'_> {
756 type Item = SqlValue;
757
758 fn next(&mut self) -> Option<Self::Item> {
759 if self.index >= self.row.len() {
760 return None;
761 }
762 let value = self.row.get_raw(self.index);
763 self.index += 1;
764 value
765 }
766
767 fn size_hint(&self) -> (usize, Option<usize>) {
768 let remaining = self.row.len() - self.index;
769 (remaining, Some(remaining))
770 }
771}
772
773impl<'a> IntoIterator for &'a Row {
774 type Item = SqlValue;
775 type IntoIter = RowIter<'a>;
776
777 fn into_iter(self) -> Self::IntoIter {
778 RowIter {
779 row: self,
780 index: 0,
781 }
782 }
783}
784
785#[cfg(test)]
786#[allow(clippy::unwrap_used, clippy::expect_used)]
787mod tests {
788 use super::*;
789
790 #[test]
791 fn test_column_slice_null() {
792 let slice = ColumnSlice::null();
793 assert!(slice.is_null);
794 assert_eq!(slice.offset, 0);
795 assert_eq!(slice.length, 0);
796 }
797
798 #[test]
799 fn test_column_metadata() {
800 let col = Column::new("id", 0, "INT")
801 .with_nullable(false)
802 .with_precision_scale(10, 0);
803
804 assert_eq!(col.name, "id");
805 assert_eq!(col.index, 0);
806 assert!(!col.nullable);
807 assert_eq!(col.precision, Some(10));
808 }
809
810 #[test]
811 fn test_col_metadata_find_by_name() {
812 let meta = ColMetaData::new(vec![
813 Column::new("id", 0, "INT"),
814 Column::new("Name", 1, "NVARCHAR"),
815 ]);
816
817 assert_eq!(meta.find_by_name("id"), Some(0));
818 assert_eq!(meta.find_by_name("ID"), Some(0)); // case-insensitive
819 assert_eq!(meta.find_by_name("name"), Some(1));
820 assert_eq!(meta.find_by_name("unknown"), None);
821 }
822
823 #[test]
824 fn test_row_from_values_backward_compat() {
825 let columns = vec![
826 Column::new("id", 0, "INT"),
827 Column::new("name", 1, "NVARCHAR"),
828 ];
829 let values = vec![SqlValue::Int(42), SqlValue::String("Alice".to_string())];
830
831 let row = Row::from_values(columns, values);
832
833 assert_eq!(row.len(), 2);
834 assert_eq!(row.get::<i32>(0).unwrap(), 42);
835 assert_eq!(row.get_by_name::<String>("name").unwrap(), "Alice");
836 }
837
838 #[test]
839 fn test_row_is_null() {
840 let columns = vec![
841 Column::new("id", 0, "INT"),
842 Column::new("nullable_col", 1, "NVARCHAR"),
843 ];
844 let values = vec![SqlValue::Int(1), SqlValue::Null];
845
846 let row = Row::from_values(columns, values);
847
848 assert!(!row.is_null(0));
849 assert!(row.is_null(1));
850 assert!(row.is_null(99)); // Out of bounds returns true
851 }
852
853 #[test]
854 fn test_row_get_bytes_with_buffer() {
855 let buffer = Arc::new(Bytes::from_static(b"Hello World"));
856 let slices: Arc<[ColumnSlice]> = vec![
857 ColumnSlice::new(0, 5, false), // "Hello"
858 ColumnSlice::new(6, 5, false), // "World"
859 ]
860 .into();
861 let meta = Arc::new(ColMetaData::new(vec![
862 Column::new("greeting", 0, "VARCHAR"),
863 Column::new("subject", 1, "VARCHAR"),
864 ]));
865
866 let row = Row::new(buffer, slices, meta);
867
868 assert_eq!(row.get_bytes(0), Some(b"Hello".as_slice()));
869 assert_eq!(row.get_bytes(1), Some(b"World".as_slice()));
870 }
871
872 #[test]
873 fn test_row_get_str() {
874 let buffer = Arc::new(Bytes::from_static(b"Test"));
875 let slices: Arc<[ColumnSlice]> = vec![ColumnSlice::new(0, 4, false)].into();
876 let meta = Arc::new(ColMetaData::new(vec![Column::new("val", 0, "VARCHAR")]));
877
878 let row = Row::new(buffer, slices, meta);
879
880 let s = row.get_str(0).unwrap();
881 assert_eq!(s, "Test");
882 // Should be borrowed for valid UTF-8
883 assert!(matches!(s, Cow::Borrowed(_)));
884 }
885
886 #[test]
887 fn test_row_metadata_access() {
888 let columns = vec![Column::new("col1", 0, "INT")];
889 let row = Row::from_values(columns, vec![SqlValue::Int(1)]);
890
891 assert_eq!(row.columns().len(), 1);
892 assert_eq!(row.columns()[0].name, "col1");
893 assert_eq!(row.metadata().len(), 1);
894 }
895
896 /// Issue #157 regression: `try_get` must distinguish SQL NULL (Ok(None))
897 /// from a decode/conversion failure (Err). Previously both collapsed to
898 /// `None`, so a type mismatch on a nullable column silently read as NULL.
899 #[test]
900 fn test_try_get_distinguishes_null_from_conversion_error() {
901 let columns = vec![Column::new("a", 0, "NVARCHAR"), Column::new("b", 1, "INT")];
902 let row = Row::from_values(
903 columns,
904 vec![SqlValue::String("not a number".into()), SqlValue::Null],
905 );
906
907 // NULL → Ok(None)
908 let b: Option<i32> = row.try_get(1).expect("NULL must be Ok(None)");
909 assert!(b.is_none());
910
911 // Missing column/index → Ok(None) (lenient lookup is unchanged)
912 let missing: Option<i32> = row.try_get(9).expect("missing index must be Ok(None)");
913 assert!(missing.is_none());
914 let missing: Option<i32> = row
915 .try_get_by_name("no_such_column")
916 .expect("missing name must be Ok(None)");
917 assert!(missing.is_none());
918
919 // Conversion failure → Err, NOT Ok(None)
920 assert!(row.try_get::<i32>(0).is_err());
921 assert!(row.try_get_by_name::<i32>("a").is_err());
922
923 // The successful typed read still works
924 let a: Option<String> = row.try_get(0).expect("string read must succeed");
925 assert_eq!(a.as_deref(), Some("not a number"));
926 }
927
928 #[test]
929 fn test_row_get_stream() {
930 let buffer = Arc::new(Bytes::from_static(b"Hello, World!"));
931 let slices: Arc<[ColumnSlice]> = vec![
932 ColumnSlice::new(0, 5, false), // "Hello"
933 ColumnSlice::new(7, 5, false), // "World"
934 ColumnSlice::null(), // NULL column
935 ]
936 .into();
937 let meta = Arc::new(ColMetaData::new(vec![
938 Column::new("greeting", 0, "VARBINARY"),
939 Column::new("subject", 1, "VARBINARY"),
940 Column::new("nullable", 2, "VARBINARY"),
941 ]));
942
943 let row = Row::new(buffer, slices, meta);
944
945 // Get stream for first column
946 let reader = row.get_stream(0).unwrap();
947 assert_eq!(reader.len(), Some(5));
948 assert_eq!(reader.as_bytes().as_ref(), b"Hello");
949
950 // Get stream for second column
951 let reader = row.get_stream(1).unwrap();
952 assert_eq!(reader.len(), Some(5));
953 assert_eq!(reader.as_bytes().as_ref(), b"World");
954
955 // NULL column returns None
956 assert!(row.get_stream(2).is_none());
957
958 // Out of bounds returns None
959 assert!(row.get_stream(99).is_none());
960 }
961
962 #[test]
963 fn test_row_get_stream_by_name() {
964 let buffer = Arc::new(Bytes::from_static(b"Binary data here"));
965 let slices: Arc<[ColumnSlice]> = vec![ColumnSlice::new(0, 11, false)].into();
966 let meta = Arc::new(ColMetaData::new(vec![Column::new(
967 "document",
968 0,
969 "VARBINARY",
970 )]));
971
972 let row = Row::new(buffer, slices, meta);
973
974 // Get by name (case-insensitive)
975 let reader = row.get_stream_by_name("document").unwrap();
976 assert_eq!(reader.len(), Some(11));
977
978 let reader = row.get_stream_by_name("DOCUMENT").unwrap();
979 assert_eq!(reader.len(), Some(11));
980
981 // Unknown column returns None
982 assert!(row.get_stream_by_name("unknown").is_none());
983 }
984}