Skip to main content

tiberius/
row.rs

1use crate::{
2    error::Error,
3    tds::codec::{ColumnData, FixedLenType, TokenRow, TypeInfo, VarLenType},
4    FromSql,
5};
6use std::{fmt::Display, sync::Arc};
7
8/// A column of data from a query.
9#[derive(Debug, Clone)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11pub struct Column {
12    pub(crate) name: String,
13    pub(crate) column_type: ColumnType,
14}
15
16impl Column {
17    /// Construct a new Column.
18    pub fn new(name: String, column_type: ColumnType) -> Self {
19        Self { name, column_type }
20    }
21
22    /// The name of the column.
23    pub fn name(&self) -> &str {
24        &self.name
25    }
26
27    /// The type of the column.
28    pub fn column_type(&self) -> ColumnType {
29        self.column_type
30    }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
35/// The type of the column.
36pub enum ColumnType {
37    /// The column doesn't have a specified type.
38    Null,
39    /// A bit or boolean value.
40    Bit,
41    /// An 8-bit integer value.
42    Int1,
43    /// A 16-bit integer value.
44    Int2,
45    /// A 32-bit integer value.
46    Int4,
47    /// A 64-bit integer value.
48    Int8,
49    /// A 32-bit datetime value.
50    Datetime4,
51    /// A 32-bit floating point value.
52    Float4,
53    /// A 64-bit floating point value.
54    Float8,
55    /// Money value.
56    Money,
57    /// A TDS 7.2 datetime value.
58    Datetime,
59    /// A 32-bit money value.
60    Money4,
61    /// A unique identifier, UUID.
62    Guid,
63    /// N-bit integer value (variable).
64    Intn,
65    /// A bit value in a variable-length type.
66    Bitn,
67    /// A decimal value (same as `Numericn`).
68    Decimaln,
69    /// A numeric value (same as `Decimaln`).
70    Numericn,
71    /// A n-bit floating point value.
72    Floatn,
73    /// A n-bit datetime value (TDS 7.2).
74    Datetimen,
75    /// A n-bit date value (TDS 7.3).
76    Daten,
77    /// A n-bit time value (TDS 7.3).
78    Timen,
79    /// A n-bit datetime2 value (TDS 7.3).
80    Datetime2,
81    /// A n-bit datetime value with an offset (TDS 7.3).
82    DatetimeOffsetn,
83    /// A variable binary value.
84    BigVarBin,
85    /// A large variable string value.
86    BigVarChar,
87    /// A binary value.
88    BigBinary,
89    /// A string value.
90    BigChar,
91    /// A variable string value with UTF-16 encoding.
92    NVarchar,
93    /// A string value with UTF-16 encoding.
94    NChar,
95    /// A XML value.
96    Xml,
97    /// User-defined type.
98    Udt,
99    /// A text value (deprecated).
100    Text,
101    /// A image value (deprecated).
102    Image,
103    /// A text value with UTF-16 encoding (deprecated).
104    NText,
105    /// An SQL variant type.
106    SSVariant,
107}
108
109impl From<&TypeInfo> for ColumnType {
110    fn from(ti: &TypeInfo) -> Self {
111        match ti {
112            TypeInfo::FixedLen(flt) => match flt {
113                FixedLenType::Int1 => Self::Int1,
114                FixedLenType::Bit => Self::Bit,
115                FixedLenType::Int2 => Self::Int2,
116                FixedLenType::Int4 => Self::Int4,
117                FixedLenType::Datetime4 => Self::Datetime4,
118                FixedLenType::Float4 => Self::Float4,
119                FixedLenType::Money => Self::Money,
120                FixedLenType::Datetime => Self::Datetime,
121                FixedLenType::Float8 => Self::Float8,
122                FixedLenType::Money4 => Self::Money4,
123                FixedLenType::Int8 => Self::Int8,
124                FixedLenType::Null => Self::Null,
125            },
126            TypeInfo::VarLenSized(cx) => match cx.r#type() {
127                VarLenType::Guid => Self::Guid,
128                VarLenType::Intn => match cx.len() {
129                    1 => Self::Int1,
130                    2 => Self::Int2,
131                    4 => Self::Int4,
132                    8 => Self::Int8,
133                    _ => Self::Intn,
134                },
135                VarLenType::Bitn => Self::Bitn,
136                VarLenType::Decimaln => Self::Decimaln,
137                VarLenType::Numericn => Self::Numericn,
138                VarLenType::Floatn => match cx.len() {
139                    4 => Self::Float4,
140                    8 => Self::Float8,
141                    _ => Self::Floatn,
142                },
143                VarLenType::Money => Self::Money,
144                VarLenType::Datetimen => Self::Datetimen,
145                #[cfg(feature = "tds73")]
146                VarLenType::Daten => Self::Daten,
147                #[cfg(feature = "tds73")]
148                VarLenType::Timen => Self::Timen,
149                #[cfg(feature = "tds73")]
150                VarLenType::Datetime2 => Self::Datetime2,
151                #[cfg(feature = "tds73")]
152                VarLenType::DatetimeOffsetn => Self::DatetimeOffsetn,
153                VarLenType::BigVarBin => Self::BigVarBin,
154                VarLenType::BigVarChar => Self::BigVarChar,
155                VarLenType::BigBinary => Self::BigBinary,
156                VarLenType::BigChar => Self::BigChar,
157                VarLenType::NVarchar => Self::NVarchar,
158                VarLenType::NChar => Self::NChar,
159                VarLenType::Xml => Self::Xml,
160                VarLenType::Udt => Self::Udt,
161                VarLenType::Text => Self::Text,
162                VarLenType::Image => Self::Image,
163                VarLenType::NText => Self::NText,
164                VarLenType::SSVariant => Self::SSVariant,
165            },
166            TypeInfo::VarLenSizedPrecision { ty, .. } => match ty {
167                VarLenType::Guid => Self::Guid,
168                VarLenType::Intn => Self::Intn,
169                VarLenType::Bitn => Self::Bitn,
170                VarLenType::Decimaln => Self::Decimaln,
171                VarLenType::Numericn => Self::Numericn,
172                VarLenType::Floatn => Self::Floatn,
173                VarLenType::Money => Self::Money,
174                VarLenType::Datetimen => Self::Datetimen,
175                #[cfg(feature = "tds73")]
176                VarLenType::Daten => Self::Daten,
177                #[cfg(feature = "tds73")]
178                VarLenType::Timen => Self::Timen,
179                #[cfg(feature = "tds73")]
180                VarLenType::Datetime2 => Self::Datetime2,
181                #[cfg(feature = "tds73")]
182                VarLenType::DatetimeOffsetn => Self::DatetimeOffsetn,
183                VarLenType::BigVarBin => Self::BigVarBin,
184                VarLenType::BigVarChar => Self::BigVarChar,
185                VarLenType::BigBinary => Self::BigBinary,
186                VarLenType::BigChar => Self::BigChar,
187                VarLenType::NVarchar => Self::NVarchar,
188                VarLenType::NChar => Self::NChar,
189                VarLenType::Xml => Self::Xml,
190                VarLenType::Udt => Self::Udt,
191                VarLenType::Text => Self::Text,
192                VarLenType::Image => Self::Image,
193                VarLenType::NText => Self::NText,
194                VarLenType::SSVariant => Self::SSVariant,
195            },
196            TypeInfo::Xml { .. } => Self::Xml,
197            TypeInfo::Udt(_) => Self::Udt,
198        }
199    }
200}
201
202/// A row of data from a query.
203///
204/// Data can be accessed either by copying through [`get`] or [`try_get`]
205/// methods, or moving by value using the [`IntoIterator`] implementation.
206///
207/// ```
208/// # use tiberius::{Config, FromSqlOwned};
209/// # use tokio_util::compat::TokioAsyncWriteCompatExt;
210/// # use std::env;
211/// # #[tokio::main]
212/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
213/// # let c_str = env::var("TIBERIUS_TEST_CONNECTION_STRING").unwrap_or(
214/// #     "server=tcp:localhost,1433;integratedSecurity=true;TrustServerCertificate=true".to_owned(),
215/// # );
216/// # let config = Config::from_ado_string(&c_str)?;
217/// # let tcp = tokio::net::TcpStream::connect(config.get_addr()).await?;
218/// # tcp.set_nodelay(true)?;
219/// # let mut client = tiberius::Client::connect(config, tcp.compat_write()).await?;
220/// // by-reference
221/// let row = client
222///     .query("SELECT @P1 AS col1", &[&"test"])
223///     .await?
224///     .into_row()
225///     .await?
226///     .unwrap();
227///
228/// assert_eq!(Some("test"), row.get("col1"));
229///
230/// // ...or by-value
231/// let row = client
232///     .query("SELECT @P1 AS col1", &[&"test"])
233///     .await?
234///     .into_row()
235///     .await?
236///     .unwrap();
237///
238/// for val in row.into_iter() {
239///     assert_eq!(
240///         Some(String::from("test")),
241///         String::from_sql_owned(val)?
242///     )
243/// }
244/// # Ok(())
245/// # }
246/// ```
247///
248/// [`get`]: #method.get
249/// [`try_get`]: #method.try_get
250/// [`IntoIterator`]: #impl-IntoIterator
251#[derive(Debug)]
252#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
253pub struct Row {
254    pub(crate) columns: Arc<Vec<Column>>,
255    pub(crate) data: TokenRow<'static>,
256    pub(crate) result_index: usize,
257}
258
259/// A type that can address a column within a [`Row`], either by its zero-based
260/// position (`usize`) or by name (`&str`).
261///
262/// Implement this for a custom column identifier (for example a generated
263/// column-name enum) to index rows with it via [`Row::get`]/[`Row::try_get`].
264pub trait QueryIdx
265where
266    Self: Display,
267{
268    /// Resolves this index to the column's zero-based position in `row`, or
269    /// `None` if it does not name/point to a column in the row.
270    fn idx(&self, row: &Row) -> Option<usize>;
271}
272
273impl QueryIdx for usize {
274    fn idx(&self, row: &Row) -> Option<usize> {
275        if *self < row.columns.len() {
276            Some(*self)
277        } else {
278            None
279        }
280    }
281}
282
283impl QueryIdx for &str {
284    fn idx(&self, row: &Row) -> Option<usize> {
285        // Allow matching a column selected with a Rust raw identifier (e.g.
286        // `r#type`) against the plain SQL column name (`type`).
287        let name = self.strip_prefix("r#").unwrap_or(self);
288        row.columns.iter().position(|c| c.name() == name)
289    }
290}
291
292impl Row {
293    /// Columns defining the row data. Columns listed here are in the same order
294    /// as the resulting data.
295    ///
296    /// # Example
297    ///
298    /// ```
299    /// # use tiberius::Config;
300    /// # use tokio_util::compat::TokioAsyncWriteCompatExt;
301    /// # use std::env;
302    /// # #[tokio::main]
303    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
304    /// # let c_str = env::var("TIBERIUS_TEST_CONNECTION_STRING").unwrap_or(
305    /// #     "server=tcp:localhost,1433;integratedSecurity=true;TrustServerCertificate=true".to_owned(),
306    /// # );
307    /// # let config = Config::from_ado_string(&c_str)?;
308    /// # let tcp = tokio::net::TcpStream::connect(config.get_addr()).await?;
309    /// # tcp.set_nodelay(true)?;
310    /// # let mut client = tiberius::Client::connect(config, tcp.compat_write()).await?;
311    /// let row = client
312    ///     .query("SELECT 1 AS foo, 2 AS bar", &[])
313    ///     .await?
314    ///     .into_row()
315    ///     .await?
316    ///     .unwrap();
317    ///
318    /// assert_eq!("foo", row.columns()[0].name());
319    /// assert_eq!("bar", row.columns()[1].name());
320    /// # Ok(())
321    /// # }
322    /// ```
323    pub fn columns(&self) -> &[Column] {
324        &self.columns
325    }
326
327    /// Return an iterator over row column-value pairs.
328    pub fn cells(&self) -> impl Iterator<Item = (&Column, &ColumnData<'static>)> {
329        self.columns().iter().zip(self.data.iter())
330    }
331
332    /// The result set number, starting from zero and increasing if the stream
333    /// has results from more than one query.
334    pub fn result_index(&self) -> usize {
335        self.result_index
336    }
337
338    /// Returns the number of columns in the row.
339    ///
340    /// # Example
341    ///
342    /// ```
343    /// # use tiberius::Config;
344    /// # use tokio_util::compat::TokioAsyncWriteCompatExt;
345    /// # use std::env;
346    /// # #[tokio::main]
347    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
348    /// # let c_str = env::var("TIBERIUS_TEST_CONNECTION_STRING").unwrap_or(
349    /// #     "server=tcp:localhost,1433;integratedSecurity=true;TrustServerCertificate=true".to_owned(),
350    /// # );
351    /// # let config = Config::from_ado_string(&c_str)?;
352    /// # let tcp = tokio::net::TcpStream::connect(config.get_addr()).await?;
353    /// # tcp.set_nodelay(true)?;
354    /// # let mut client = tiberius::Client::connect(config, tcp.compat_write()).await?;
355    /// let row = client
356    ///     .query("SELECT 1, 2", &[])
357    ///     .await?
358    ///     .into_row()
359    ///     .await?
360    ///     .unwrap();
361    ///
362    /// assert_eq!(2, row.len());
363    /// # Ok(())
364    /// # }
365    /// ```
366    #[allow(clippy::len_without_is_empty)]
367    pub fn len(&self) -> usize {
368        self.data.len()
369    }
370
371    /// Retrieve a column value for a given column index, which can either be
372    /// the zero-indexed position or the name of the column.
373    ///
374    /// # Example
375    ///
376    /// ```
377    /// # use tiberius::Config;
378    /// # use tokio_util::compat::TokioAsyncWriteCompatExt;
379    /// # use std::env;
380    /// # #[tokio::main]
381    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
382    /// # let c_str = env::var("TIBERIUS_TEST_CONNECTION_STRING").unwrap_or(
383    /// #     "server=tcp:localhost,1433;integratedSecurity=true;TrustServerCertificate=true".to_owned(),
384    /// # );
385    /// # let config = Config::from_ado_string(&c_str)?;
386    /// # let tcp = tokio::net::TcpStream::connect(config.get_addr()).await?;
387    /// # tcp.set_nodelay(true)?;
388    /// # let mut client = tiberius::Client::connect(config, tcp.compat_write()).await?;
389    /// let row = client
390    ///     .query("SELECT @P1 AS col1", &[&1i32])
391    ///     .await?
392    ///     .into_row()
393    ///     .await?
394    ///     .unwrap();
395    ///
396    /// assert_eq!(Some(1i32), row.get(0));
397    /// assert_eq!(Some(1i32), row.get("col1"));
398    /// # Ok(())
399    /// # }
400    /// ```
401    ///
402    /// # Panics
403    ///
404    /// - The requested type conversion (SQL->Rust) is not possible.
405    /// - The given index is out of bounds (column does not exist).
406    ///
407    /// Use [`try_get`] for a non-panicking version of the function.
408    ///
409    /// [`try_get`]: #method.try_get
410    #[track_caller]
411    pub fn get<'a, R, I>(&'a self, idx: I) -> Option<R>
412    where
413        R: FromSql<'a>,
414        I: QueryIdx,
415    {
416        self.try_get(idx).unwrap()
417    }
418
419    /// Retrieve a column's value for a given column index.
420    #[track_caller]
421    pub fn try_get<'a, R, I>(&'a self, idx: I) -> crate::Result<Option<R>>
422    where
423        R: FromSql<'a>,
424        I: QueryIdx,
425    {
426        let data = self.get_column_data(idx)?;
427
428        R::from_sql(data)
429    }
430
431    /// Retrieve a column's data for a given column index.
432    #[track_caller]
433    pub fn get_column_data<I>(&self, idx: I) -> crate::Result<&ColumnData<'static>>
434    where
435        I: QueryIdx,
436    {
437        let idx = idx.idx(self).ok_or_else(|| {
438            Error::Conversion(format!("Could not find column with index {}", idx).into())
439        })?;
440
441        // `idx` was validated against the column metadata; the cell should exist,
442        // but a malformed ROW/NBCROW with fewer cells than columns must not
443        // panic here — return an error instead of unwrapping.
444        self.data.get(idx).ok_or_else(|| {
445            Error::Protocol(format!("row has no data for column index {idx}").into())
446        })
447    }
448
449    /// Consumes the row, returning the underlying [`TokenRow`] holding the raw
450    /// column data as received from the server.
451    ///
452    /// This is useful when direct access to the raw [`ColumnData`] values is
453    /// needed instead of converting them through [`get`] or [`try_get`].
454    ///
455    /// [`get`]: #method.get
456    /// [`try_get`]: #method.try_get
457    pub fn into_token_row(self) -> TokenRow<'static> {
458        self.data
459    }
460}
461
462impl IntoIterator for Row {
463    type Item = ColumnData<'static>;
464    type IntoIter = std::vec::IntoIter<Self::Item>;
465
466    fn into_iter(self) -> Self::IntoIter {
467        self.data.into_iter()
468    }
469}
470
471#[cfg(test)]
472mod tests {
473    use super::*;
474
475    fn make_row() -> Row {
476        let columns = Arc::new(vec![
477            Column::new("foo".to_string(), ColumnType::Int4),
478            Column::new("type".to_string(), ColumnType::Int4),
479        ]);
480
481        let mut data = TokenRow::new();
482        data.push(ColumnData::I32(Some(1)));
483        data.push(ColumnData::I32(Some(2)));
484
485        Row {
486            columns,
487            data,
488            result_index: 0,
489        }
490    }
491
492    #[test]
493    fn result_index_reflects_the_field() {
494        // A non-zero index pins `result_index()` (a `-> 0` mutant would pass if
495        // every fixture used index 0).
496        let columns = Arc::new(vec![Column::new("c".to_string(), ColumnType::Int4)]);
497        let mut data = TokenRow::new();
498        data.push(ColumnData::I32(Some(1)));
499        let row = Row {
500            columns,
501            data,
502            result_index: 3,
503        };
504        assert_eq!(row.result_index(), 3);
505    }
506
507    // Regression test for #211: an out-of-range usize index must not panic.
508    #[test]
509    fn try_get_out_of_range_index_returns_none() {
510        let row = make_row();
511
512        assert_eq!(None, 2usize.idx(&row));
513        assert_eq!(Some(0), 0usize.idx(&row));
514
515        let value: crate::Result<Option<i32>> = row.try_get(5usize);
516        assert!(value.is_err());
517    }
518
519    // Regression test for #382: a raw-identifier column name (`r#type`) must
520    // match the plain SQL column name (`type`).
521    #[test]
522    fn raw_identifier_column_name_matches() {
523        let row = make_row();
524
525        assert_eq!(Some(1), "type".idx(&row));
526        assert_eq!(Some(1), "r#type".idx(&row));
527        assert_eq!(Some(0), "r#foo".idx(&row));
528        assert_eq!(None, "r#missing".idx(&row));
529
530        assert_eq!(Some(2i32), row.get::<i32, _>("r#type"));
531    }
532
533    #[test]
534    fn row_accessors() {
535        let row = make_row();
536
537        assert_eq!(2, row.columns().len());
538        assert_eq!(2, row.len());
539        assert_eq!(0, row.result_index());
540
541        let cells: Vec<_> = row.cells().collect();
542        assert_eq!(2, cells.len());
543        assert_eq!("foo", cells[0].0.name());
544
545        let token_row = row.into_token_row();
546        assert_eq!(2, token_row.len());
547    }
548
549    #[test]
550    fn row_into_iterator_yields_column_data() {
551        let row = make_row();
552        let values: Vec<_> = row.into_iter().collect();
553        assert_eq!(
554            vec![ColumnData::I32(Some(1)), ColumnData::I32(Some(2))],
555            values
556        );
557    }
558
559    #[test]
560    fn get_column_data_missing_cell_errors() {
561        let columns = Arc::new(vec![
562            Column::new("a".to_string(), ColumnType::Int4),
563            Column::new("b".to_string(), ColumnType::Int4),
564        ]);
565
566        // Malformed row: metadata says 2 columns, but only 1 cell present.
567        let mut data = TokenRow::new();
568        data.push(ColumnData::I32(Some(1)));
569
570        let row = Row {
571            columns,
572            data,
573            result_index: 0,
574        };
575
576        let err = row.get_column_data(1usize).unwrap_err();
577        assert!(format!("{}", err).contains("row has no data for column index"));
578    }
579
580    #[test]
581    fn column_new_and_accessors() {
582        let column = Column::new("id".to_string(), ColumnType::Int8);
583        assert_eq!("id", column.name());
584        assert_eq!(ColumnType::Int8, column.column_type());
585    }
586
587    #[test]
588    fn column_type_from_fixed_len_type_info() {
589        use crate::tds::codec::FixedLenType;
590
591        let cases = [
592            (FixedLenType::Int1, ColumnType::Int1),
593            (FixedLenType::Bit, ColumnType::Bit),
594            (FixedLenType::Int2, ColumnType::Int2),
595            (FixedLenType::Int4, ColumnType::Int4),
596            (FixedLenType::Datetime4, ColumnType::Datetime4),
597            (FixedLenType::Float4, ColumnType::Float4),
598            (FixedLenType::Money, ColumnType::Money),
599            (FixedLenType::Datetime, ColumnType::Datetime),
600            (FixedLenType::Float8, ColumnType::Float8),
601            (FixedLenType::Money4, ColumnType::Money4),
602            (FixedLenType::Int8, ColumnType::Int8),
603            (FixedLenType::Null, ColumnType::Null),
604        ];
605
606        for (flt, expected) in cases {
607            let ti = TypeInfo::FixedLen(flt);
608            assert_eq!(ColumnType::from(&ti), expected);
609        }
610    }
611
612    #[test]
613    fn column_type_from_var_len_sized_type_info() {
614        use crate::tds::codec::VarLenType;
615        use crate::VarLenContext;
616
617        let cases = [
618            (VarLenType::Guid, 16, ColumnType::Guid),
619            (VarLenType::Intn, 1, ColumnType::Int1),
620            (VarLenType::Intn, 2, ColumnType::Int2),
621            (VarLenType::Intn, 4, ColumnType::Int4),
622            (VarLenType::Intn, 8, ColumnType::Int8),
623            (VarLenType::Intn, 3, ColumnType::Intn),
624            (VarLenType::Bitn, 1, ColumnType::Bitn),
625            (VarLenType::Decimaln, 17, ColumnType::Decimaln),
626            (VarLenType::Numericn, 17, ColumnType::Numericn),
627            (VarLenType::Floatn, 4, ColumnType::Float4),
628            (VarLenType::Floatn, 8, ColumnType::Float8),
629            (VarLenType::Floatn, 2, ColumnType::Floatn),
630            (VarLenType::Money, 8, ColumnType::Money),
631            (VarLenType::Datetimen, 8, ColumnType::Datetimen),
632            (VarLenType::BigVarBin, 8000, ColumnType::BigVarBin),
633            (VarLenType::BigVarChar, 8000, ColumnType::BigVarChar),
634            (VarLenType::BigBinary, 8000, ColumnType::BigBinary),
635            (VarLenType::BigChar, 8000, ColumnType::BigChar),
636            (VarLenType::NVarchar, 4000, ColumnType::NVarchar),
637            (VarLenType::NChar, 4000, ColumnType::NChar),
638            (VarLenType::Xml, 0, ColumnType::Xml),
639            (VarLenType::Udt, 0, ColumnType::Udt),
640            (VarLenType::Text, 0, ColumnType::Text),
641            (VarLenType::Image, 0, ColumnType::Image),
642            (VarLenType::NText, 0, ColumnType::NText),
643            (VarLenType::SSVariant, 0, ColumnType::SSVariant),
644        ];
645
646        for (ty, len, expected) in cases {
647            let ti = TypeInfo::VarLenSized(VarLenContext::new(ty, len, None));
648            assert_eq!(ColumnType::from(&ti), expected, "{:?} len {}", ty, len);
649        }
650    }
651
652    #[test]
653    fn column_type_from_var_len_sized_precision_type_info() {
654        use crate::tds::codec::VarLenType;
655
656        let cases = [
657            (VarLenType::Guid, ColumnType::Guid),
658            (VarLenType::Intn, ColumnType::Intn),
659            (VarLenType::Bitn, ColumnType::Bitn),
660            (VarLenType::Decimaln, ColumnType::Decimaln),
661            (VarLenType::Numericn, ColumnType::Numericn),
662            (VarLenType::Floatn, ColumnType::Floatn),
663            (VarLenType::Money, ColumnType::Money),
664            (VarLenType::Datetimen, ColumnType::Datetimen),
665            (VarLenType::BigVarBin, ColumnType::BigVarBin),
666            (VarLenType::BigVarChar, ColumnType::BigVarChar),
667            (VarLenType::BigBinary, ColumnType::BigBinary),
668            (VarLenType::BigChar, ColumnType::BigChar),
669            (VarLenType::NVarchar, ColumnType::NVarchar),
670            (VarLenType::NChar, ColumnType::NChar),
671            (VarLenType::Xml, ColumnType::Xml),
672            (VarLenType::Udt, ColumnType::Udt),
673            (VarLenType::Text, ColumnType::Text),
674            (VarLenType::Image, ColumnType::Image),
675            (VarLenType::NText, ColumnType::NText),
676            (VarLenType::SSVariant, ColumnType::SSVariant),
677        ];
678
679        for (ty, expected) in cases {
680            let ti = TypeInfo::VarLenSizedPrecision {
681                ty,
682                size: 38,
683                precision: 38,
684                scale: 2,
685            };
686            assert_eq!(ColumnType::from(&ti), expected, "{:?}", ty);
687        }
688    }
689
690    #[test]
691    fn column_type_from_xml_and_udt_type_info() {
692        use crate::tds::codec::UdtInfo;
693        use crate::tds::xml::XmlSchema;
694        use std::sync::Arc as StdArc;
695
696        let ti = TypeInfo::Xml {
697            schema: None::<StdArc<XmlSchema>>,
698            size: 0,
699        };
700        assert_eq!(ColumnType::from(&ti), ColumnType::Xml);
701
702        let ti = TypeInfo::Udt(UdtInfo {
703            max_byte_size: 0xffff,
704            db_name: "db".to_string(),
705            schema_name: "dbo".to_string(),
706            type_name: "geometry".to_string(),
707            assembly_qualified_name: "asm".to_string(),
708        });
709        assert_eq!(ColumnType::from(&ti), ColumnType::Udt);
710    }
711
712    #[cfg(feature = "tds73")]
713    #[test]
714    fn column_type_from_var_len_sized_tds73_type_info() {
715        use crate::tds::codec::VarLenType;
716        use crate::VarLenContext;
717
718        let cases = [
719            (VarLenType::Daten, ColumnType::Daten),
720            (VarLenType::Timen, ColumnType::Timen),
721            (VarLenType::Datetime2, ColumnType::Datetime2),
722            (VarLenType::DatetimeOffsetn, ColumnType::DatetimeOffsetn),
723        ];
724
725        for (ty, expected) in cases {
726            let ti = TypeInfo::VarLenSized(VarLenContext::new(ty, 8, None));
727            assert_eq!(ColumnType::from(&ti), expected, "{:?}", ty);
728        }
729    }
730
731    #[cfg(feature = "tds73")]
732    #[test]
733    fn column_type_from_var_len_sized_precision_tds73_type_info() {
734        use crate::tds::codec::VarLenType;
735
736        let cases = [
737            (VarLenType::Daten, ColumnType::Daten),
738            (VarLenType::Timen, ColumnType::Timen),
739            (VarLenType::Datetime2, ColumnType::Datetime2),
740            (VarLenType::DatetimeOffsetn, ColumnType::DatetimeOffsetn),
741        ];
742
743        for (ty, expected) in cases {
744            let ti = TypeInfo::VarLenSizedPrecision {
745                ty,
746                size: 8,
747                precision: 0,
748                scale: 7,
749            };
750            assert_eq!(ColumnType::from(&ti), expected, "{:?}", ty);
751        }
752    }
753}