Skip to main content

tds_driver/tds/codec/token/
token_col_metadata.rs

1use std::{borrow::Cow, fmt::Display};
2
3use crate::{
4    tds::codec::{put_b_varchar, Encode, FixedLenType, TokenType, TypeInfo, VarLenType},
5    tds::Collation,
6    Column, ColumnData, ColumnType, SqlReadBytes, VarLenContext,
7};
8use asynchronous_codec::BytesMut;
9use bytes::BufMut;
10use enumflags2::{bitflags, BitFlags};
11
12#[derive(Debug, Clone)]
13pub struct TokenColMetaData<'a> {
14    pub columns: Vec<MetaDataColumn<'a>>,
15}
16
17/// Metadata for a single result/table column: its name plus the
18/// [`BaseMetaDataColumn`] describing its type, size and flags.
19#[derive(Debug, Clone)]
20pub struct MetaDataColumn<'a> {
21    /// The type and flag metadata for the column.
22    pub base: BaseMetaDataColumn,
23    /// The name of the column.
24    pub col_name: Cow<'a, str>,
25}
26
27impl<'a> MetaDataColumn<'a> {
28    /// The name of the column.
29    pub fn col_name(&self) -> &str {
30        self.col_name.as_ref()
31    }
32
33    /// The [`BaseMetaDataColumn`] describing the column's type and flags
34    /// (nullability, identity, etc.).
35    pub fn base(&self) -> &BaseMetaDataColumn {
36        &self.base
37    }
38}
39
40impl<'a> Display for MetaDataColumn<'a> {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        // The output is an INSERT BULK column definition: the name is quoted
43        // the way SqlBulkCopy quotes it, the type is the column's own.
44        write!(f, "[{}] ", self.col_name.replace(']', "]]"))?;
45
46        match &self.base.ty {
47            TypeInfo::FixedLen(fixed) => match fixed {
48                FixedLenType::Int1 => write!(f, "tinyint")?,
49                FixedLenType::Bit => write!(f, "bit")?,
50                FixedLenType::Int2 => write!(f, "smallint")?,
51                FixedLenType::Int4 => write!(f, "int")?,
52                FixedLenType::Datetime4 => write!(f, "smalldatetime")?,
53                FixedLenType::Float4 => write!(f, "real")?,
54                FixedLenType::Money => write!(f, "money")?,
55                FixedLenType::Datetime => write!(f, "datetime")?,
56                FixedLenType::Float8 => write!(f, "float")?,
57                FixedLenType::Money4 => write!(f, "smallmoney")?,
58                FixedLenType::Int8 => write!(f, "bigint")?,
59                // The TDS "null" fixed type carries no value; surface it as the
60                // int it decodes to rather than panicking (a bare `SELECT NULL`
61                // produces such a column).
62                FixedLenType::Null => write!(f, "int")?,
63            },
64            TypeInfo::VarLenSized(ctx) => match ctx.r#type() {
65                VarLenType::Bitn => write!(f, "bit")?,
66                VarLenType::Guid => write!(f, "uniqueidentifier")?,
67                #[cfg(feature = "tds73")]
68                VarLenType::Daten => write!(f, "date")?,
69                #[cfg(feature = "tds73")]
70                VarLenType::Timen => write!(f, "time({})", ctx.len())?,
71                #[cfg(feature = "tds73")]
72                VarLenType::Datetime2 => write!(f, "datetime2({})", ctx.len())?,
73                VarLenType::Datetimen => match ctx.len() {
74                    4 => write!(f, "smalldatetime")?,
75                    _ => write!(f, "datetime")?,
76                },
77                VarLenType::Money => match ctx.len() {
78                    4 => write!(f, "smallmoney")?,
79                    _ => write!(f, "money")?,
80                },
81                #[cfg(feature = "tds73")]
82                VarLenType::DatetimeOffsetn => write!(f, "datetimeoffset({})", ctx.len())?,
83                VarLenType::BigVarBin => {
84                    if ctx.len() <= 8000 {
85                        write!(f, "varbinary({})", ctx.len())?
86                    } else {
87                        write!(f, "varbinary(max)")?
88                    }
89                }
90                VarLenType::BigVarChar => {
91                    if ctx.len() <= 8000 {
92                        write!(f, "varchar({})", ctx.len())?
93                    } else {
94                        write!(f, "varchar(max)")?
95                    }
96                }
97                VarLenType::BigBinary => write!(f, "binary({})", ctx.len())?,
98                VarLenType::BigChar => write!(f, "char({})", ctx.len())?,
99                // Lengths of n-types count bytes; the declaration counts characters.
100                VarLenType::NVarchar => {
101                    if ctx.len() <= 8000 {
102                        write!(f, "nvarchar({})", ctx.len() / 2)?
103                    } else {
104                        write!(f, "nvarchar(max)")?
105                    }
106                }
107                VarLenType::NChar => write!(f, "nchar({})", ctx.len() / 2)?,
108                VarLenType::Text => write!(f, "text")?,
109                VarLenType::Image => write!(f, "image")?,
110                VarLenType::NText => write!(f, "ntext")?,
111                VarLenType::Intn => match ctx.len() {
112                    1 => write!(f, "tinyint")?,
113                    2 => write!(f, "smallint")?,
114                    4 => write!(f, "int")?,
115                    _ => write!(f, "bigint")?,
116                },
117                VarLenType::Floatn => match ctx.len() {
118                    4 => write!(f, "real")?,
119                    _ => write!(f, "float")?,
120                },
121                VarLenType::SSVariant => write!(f, "sql_variant")?,
122                // Any other var-len type: emit its debug name rather than
123                // panicking, so formatting metadata never crashes.
124                other => write!(f, "{other:?}")?,
125            },
126            TypeInfo::VarLenSizedPrecision {
127                ty,
128                size: _,
129                precision,
130                scale,
131            } => match ty {
132                VarLenType::Numericn => write!(f, "numeric({},{})", precision, scale)?,
133                // Decimaln, and any other precision-carrying type.
134                _ => write!(f, "decimal({},{})", precision, scale)?,
135            },
136            TypeInfo::Xml { .. } => write!(f, "xml")?,
137            TypeInfo::Udt(info) => write!(f, "{}.{}", info.schema_name, info.type_name)?,
138        }
139
140        Ok(())
141    }
142}
143
144/// Describes the type and flags of a column, exposing metadata such as the
145/// column type (including size, precision and scale), whether the column is
146/// nullable and whether it is an identity column.
147#[derive(Debug, Clone)]
148pub struct BaseMetaDataColumn {
149    /// The set of [`ColumnFlag`]s describing the column (nullability, identity,
150    /// updateability, and so on).
151    pub flags: BitFlags<ColumnFlag>,
152    /// The type of the column, including its size, precision and scale where
153    /// applicable.
154    pub ty: TypeInfo,
155}
156
157impl BaseMetaDataColumn {
158    /// The type of the column, including its size, precision and scale where
159    /// applicable.
160    pub fn ty(&self) -> &TypeInfo {
161        &self.ty
162    }
163
164    /// The set of flags describing the column.
165    pub fn flags(&self) -> BitFlags<ColumnFlag> {
166        self.flags
167    }
168
169    /// `true` if the column accepts `NULL` values.
170    pub fn is_nullable(&self) -> bool {
171        self.flags.contains(ColumnFlag::Nullable)
172    }
173
174    /// `true` if the column is an identity column.
175    pub fn is_identity(&self) -> bool {
176        self.flags.contains(ColumnFlag::Identity)
177    }
178
179    /// `true` if the column is writeable (e.g. usable as a bulk-insert target).
180    pub fn is_updateable(&self) -> bool {
181        self.flags.contains(ColumnFlag::Updateable)
182    }
183
184    /// char, varchar or text: the server reads their bytes in the code page
185    /// of the collation INSERT BULK declares.
186    pub(crate) fn is_non_unicode_text(&self) -> bool {
187        matches!(&self.ty, TypeInfo::VarLenSized(cx)
188            if matches!(cx.r#type(), VarLenType::BigChar | VarLenType::BigVarChar | VarLenType::Text))
189    }
190
191    /// COLMETADATA gives a sql_variant column no collation; its string values
192    /// need one, so the encoder finds the database default here.
193    pub(crate) fn set_variant_collation(&mut self, collation: Option<Collation>) {
194        if let TypeInfo::VarLenSized(cx) = &mut self.ty {
195            if cx.r#type() == VarLenType::SSVariant {
196                *cx = VarLenContext::new(VarLenType::SSVariant, cx.len(), collation);
197            }
198        }
199    }
200
201    pub(crate) fn null_value(&self) -> ColumnData<'static> {
202        match &self.ty {
203            TypeInfo::FixedLen(ty) => match ty {
204                FixedLenType::Null => ColumnData::I32(None),
205                FixedLenType::Int1 => ColumnData::U8(None),
206                FixedLenType::Bit => ColumnData::Bit(None),
207                FixedLenType::Int2 => ColumnData::I16(None),
208                FixedLenType::Int4 => ColumnData::I32(None),
209                FixedLenType::Datetime4 => ColumnData::SmallDateTime(None),
210                FixedLenType::Float4 => ColumnData::F32(None),
211                FixedLenType::Money => ColumnData::Numeric(None),
212                FixedLenType::Datetime => ColumnData::DateTime(None),
213                FixedLenType::Float8 => ColumnData::F64(None),
214                FixedLenType::Money4 => ColumnData::Numeric(None),
215                FixedLenType::Int8 => ColumnData::I64(None),
216            },
217            TypeInfo::VarLenSized(cx) => match cx.r#type() {
218                VarLenType::Guid => ColumnData::Guid(None),
219                VarLenType::Intn => match cx.len() {
220                    1 => ColumnData::U8(None),
221                    2 => ColumnData::I16(None),
222                    4 => ColumnData::I32(None),
223                    _ => ColumnData::I64(None),
224                },
225                VarLenType::Bitn => ColumnData::Bit(None),
226                VarLenType::Decimaln => ColumnData::Numeric(None),
227                VarLenType::Numericn => ColumnData::Numeric(None),
228                VarLenType::Floatn => match cx.len() {
229                    4 => ColumnData::F32(None),
230                    _ => ColumnData::F64(None),
231                },
232                VarLenType::Money => ColumnData::Numeric(None),
233                // Must match `datetimen::decode` (ROW) for NBCROW nulls.
234                VarLenType::Datetimen => match cx.len() {
235                    4 => ColumnData::SmallDateTime(None),
236                    _ => ColumnData::DateTime(None),
237                },
238                #[cfg(feature = "tds73")]
239                VarLenType::Daten => ColumnData::Date(None),
240                #[cfg(feature = "tds73")]
241                VarLenType::Timen => ColumnData::Time(None),
242                #[cfg(feature = "tds73")]
243                VarLenType::Datetime2 => ColumnData::DateTime2(None),
244                #[cfg(feature = "tds73")]
245                VarLenType::DatetimeOffsetn => ColumnData::DateTimeOffset(None),
246                VarLenType::BigVarBin => ColumnData::Binary(None),
247                VarLenType::BigVarChar => ColumnData::String(None),
248                VarLenType::BigBinary => ColumnData::Binary(None),
249                VarLenType::BigChar => ColumnData::String(None),
250                VarLenType::NVarchar => ColumnData::String(None),
251                VarLenType::NChar => ColumnData::String(None),
252                VarLenType::Xml => ColumnData::Xml(None),
253                // A null CLR UDT carries no payload; surface it as a null
254                // binary, matching `udt::decode` (which yields
255                // `ColumnData::Binary`). Previously this panicked via `todo!()`,
256                // which a bulk insert of a NULL UDT column could reach.
257                VarLenType::Udt => ColumnData::Binary(None),
258                VarLenType::Text => ColumnData::String(None),
259                VarLenType::Image => ColumnData::Binary(None),
260                VarLenType::NText => ColumnData::String(None),
261                // A null `sql_variant` carries no base type, so surface a
262                // generic null value.
263                VarLenType::SSVariant => ColumnData::String(None),
264            },
265            TypeInfo::VarLenSizedPrecision { ty, .. } => match ty {
266                VarLenType::Guid => ColumnData::Guid(None),
267                VarLenType::Intn => ColumnData::I32(None),
268                VarLenType::Bitn => ColumnData::Bit(None),
269                VarLenType::Decimaln => ColumnData::Numeric(None),
270                VarLenType::Numericn => ColumnData::Numeric(None),
271                VarLenType::Floatn => ColumnData::F32(None),
272                VarLenType::Money => ColumnData::Numeric(None),
273                VarLenType::Datetimen => ColumnData::DateTime(None),
274                #[cfg(feature = "tds73")]
275                VarLenType::Daten => ColumnData::Date(None),
276                #[cfg(feature = "tds73")]
277                VarLenType::Timen => ColumnData::Time(None),
278                #[cfg(feature = "tds73")]
279                VarLenType::Datetime2 => ColumnData::DateTime2(None),
280                #[cfg(feature = "tds73")]
281                VarLenType::DatetimeOffsetn => ColumnData::DateTimeOffset(None),
282                VarLenType::BigVarBin => ColumnData::Binary(None),
283                VarLenType::BigVarChar => ColumnData::String(None),
284                VarLenType::BigBinary => ColumnData::Binary(None),
285                VarLenType::BigChar => ColumnData::String(None),
286                VarLenType::NVarchar => ColumnData::String(None),
287                VarLenType::NChar => ColumnData::String(None),
288                VarLenType::Xml => ColumnData::Xml(None),
289                // A null CLR UDT carries no payload; surface it as a null
290                // binary, matching `udt::decode` (which yields
291                // `ColumnData::Binary`). Previously this panicked via `todo!()`,
292                // which a bulk insert of a NULL UDT column could reach.
293                VarLenType::Udt => ColumnData::Binary(None),
294                VarLenType::Text => ColumnData::String(None),
295                VarLenType::Image => ColumnData::Binary(None),
296                VarLenType::NText => ColumnData::String(None),
297                // A null `sql_variant` carries no base type, so surface a
298                // generic null value.
299                VarLenType::SSVariant => ColumnData::String(None),
300            },
301            TypeInfo::Xml { .. } => ColumnData::Xml(None),
302            TypeInfo::Udt(_) => ColumnData::Binary(None),
303        }
304    }
305}
306
307impl<'a> Encode<BytesMut> for TokenColMetaData<'a> {
308    fn encode(self, dst: &mut BytesMut) -> crate::Result<()> {
309        dst.put_u8(TokenType::ColMetaData as u8);
310        dst.put_u16_le(self.columns.len() as u16);
311
312        for col in self.columns.into_iter() {
313            col.encode(dst)?;
314        }
315
316        Ok(())
317    }
318}
319
320impl<'a> Encode<BytesMut> for MetaDataColumn<'a> {
321    fn encode(self, dst: &mut BytesMut) -> crate::Result<()> {
322        let long = matches!(&self.base.ty, TypeInfo::VarLenSized(cx)
323            if matches!(cx.r#type(), VarLenType::Text | VarLenType::NText | VarLenType::Image));
324
325        dst.put_u32_le(0);
326
327        match self.base.ty {
328            // The server rejects an xml TYPE_INFO here (4816). SqlClient's
329            // WriteBulkCopyMetaData sends nvarchar(max) with a zero collation
330            // (s_xmlMetadataSubstituteSequence, TdsParser.cs:136 and :11643,
331            // dotnet/SqlClient@5394363); xml values are the same PLP UTF-16.
332            TypeInfo::Xml { .. } => {
333                dst.put_u16_le(self.base.flags.bits());
334                dst.extend_from_slice(&[VarLenType::NVarchar as u8, 0xff, 0xff, 0, 0, 0, 0, 0]);
335            }
336            _ => self.base.encode(dst)?,
337        }
338
339        if long {
340            // TableName. SqlClient's WriteBulkCopyMetaData sends one
341            // US_VARCHAR here, not the NumParts list the server sends.
342            dst.put_u16_le(0);
343        }
344
345        put_b_varchar(dst, &self.col_name, "column name")
346    }
347}
348
349impl Encode<BytesMut> for BaseMetaDataColumn {
350    fn encode(self, dst: &mut BytesMut) -> crate::Result<()> {
351        dst.put_u16_le(BitFlags::bits(self.flags));
352        self.ty.encode(dst)?;
353
354        Ok(())
355    }
356}
357
358/// A setting a column can hold.
359#[bitflags]
360#[repr(u16)]
361#[derive(Debug, Clone, Copy, PartialEq, Eq)]
362pub enum ColumnFlag {
363    /// The column can be null.
364    Nullable = 1,
365    /// Set for string columns with binary collation and always for the XML data
366    /// type.
367    CaseSensitive = 1 << 1,
368    /// If column is writeable.
369    Updateable = 1 << 3,
370    /// Column modification status unknown.
371    UpdateableUnknown = 1 << 2,
372    /// Column is an identity.
373    Identity = 1 << 4,
374    /// Coulumn is computed.
375    Computed = 1 << 7,
376    /// Column is a fixed-length common language runtime user-defined type (CLR
377    /// UDT).
378    FixedLenClrType = 1 << 10,
379    /// Column is the special XML column for the sparse column set.
380    SparseColumnSet = 1 << 11,
381    /// Column is encrypted transparently and has to be decrypted to view the
382    /// plaintext value. This flag is valid when the column encryption feature
383    /// is negotiated between client and server and is turned on.
384    Encrypted = 1 << 12,
385    /// Column is part of a hidden primary key created to support a T-SQL SELECT
386    /// statement containing FOR BROWSE.
387    Hidden = 1 << 13,
388    /// Column is part of a primary key for the row and the T-SQL SELECT
389    /// statement contains FOR BROWSE.
390    Key = 1 << 14,
391    /// It is unknown whether the column might be nullable.
392    NullableUnknown = 1 << 15,
393}
394
395impl TokenColMetaData<'static> {
396    pub(crate) async fn decode<R>(src: &mut R) -> crate::Result<Self>
397    where
398        R: SqlReadBytes + Unpin,
399    {
400        let column_count = src.read_u16_le().await?;
401        // `column_count` is an untrusted u16 (up to 65535); cap the up-front
402        // reservation so a hostile COLMETADATA token can't force a large
403        // transient allocation before the column bodies arrive. The Vec still
404        // grows as real columns are decoded.
405        let mut columns = Vec::with_capacity(
406            (column_count as usize).min(crate::tds::codec::column_data::MAX_PREALLOC),
407        );
408
409        // `0xffff` is the "no metadata" sentinel; any other count drives the
410        // loop directly (a count of 0 simply iterates zero times).
411        if column_count < 0xffff {
412            for _ in 0..column_count {
413                let base = BaseMetaDataColumn::decode(src).await?;
414                let col_name = Cow::from(src.read_b_varchar().await?);
415
416                columns.push(MetaDataColumn { base, col_name });
417            }
418        }
419
420        Ok(TokenColMetaData { columns })
421    }
422}
423
424impl<'a> TokenColMetaData<'a> {
425    pub(crate) fn columns(&self) -> impl Iterator<Item = Column> + '_ {
426        self.columns.iter().map(|x| Column {
427            name: x.col_name.to_string(),
428            column_type: ColumnType::from(&x.base.ty),
429        })
430    }
431}
432
433impl BaseMetaDataColumn {
434    pub(crate) async fn decode<R>(src: &mut R) -> crate::Result<Self>
435    where
436        R: SqlReadBytes + Unpin,
437    {
438        use VarLenType::*;
439
440        let _user_ty = src.read_u32_le().await?;
441
442        // The COLMETADATA `Flags` field (MS-TDS §2.2.7.4) is a 16-bit field that
443        // includes reserved / ODBC bits the server may set and which future
444        // protocol revisions may extend. Truncate to the flags we model rather
445        // than rejecting the whole token on an unrecognized bit.
446        let flags = BitFlags::from_bits_truncate(src.read_u16_le().await?);
447
448        let ty = TypeInfo::decode(src).await?;
449
450        if let TypeInfo::VarLenSized(cx) = ty {
451            if let Text | NText | Image = cx.r#type() {
452                let num_of_parts = src.read_u8().await?;
453
454                // table name
455                for _ in 0..num_of_parts {
456                    src.read_us_varchar().await?;
457                }
458            };
459        };
460
461        Ok(BaseMetaDataColumn { flags, ty })
462    }
463}
464
465#[cfg(test)]
466mod tests {
467    use super::*;
468    use crate::sql_read_bytes::test_utils::IntoSqlReadBytes;
469    use crate::tds::Collation;
470    use crate::VarLenContext;
471
472    fn meta(ty: TypeInfo, name: &'static str) -> MetaDataColumn<'static> {
473        MetaDataColumn {
474            base: BaseMetaDataColumn {
475                flags: ColumnFlag::Nullable.into(),
476                ty,
477            },
478            col_name: Cow::Borrowed(name),
479        }
480    }
481
482    #[tokio::test]
483    async fn round_trip_via_encode_decode() {
484        let cmd = TokenColMetaData {
485            columns: vec![
486                meta(TypeInfo::FixedLen(FixedLenType::Int4), "id"),
487                meta(
488                    TypeInfo::VarLenSized(VarLenContext::new(
489                        VarLenType::NVarchar,
490                        4000,
491                        Some(Collation::new(13632521, 52)),
492                    )),
493                    "name",
494                ),
495            ],
496        };
497
498        // Build a decodable buffer: column count followed by each column. The
499        // MetaDataColumn encoder writes the leading user-type u32 that the
500        // decoder expects.
501        let mut buf = BytesMut::new();
502        buf.put_u16_le(cmd.columns.len() as u16);
503        for col in cmd.columns.iter().cloned() {
504            col.encode(&mut buf).unwrap();
505        }
506
507        let decoded = TokenColMetaData::decode(&mut buf.into_sql_read_bytes())
508            .await
509            .unwrap();
510
511        assert_eq!(decoded.columns.len(), 2);
512        assert_eq!(decoded.columns[0].col_name, "id");
513        assert_eq!(decoded.columns[1].col_name, "name");
514
515        let columns: Vec<_> = decoded.columns().collect();
516        assert_eq!(columns.len(), 2);
517        assert_eq!(columns[0].name(), "id");
518    }
519
520    #[test]
521    fn encode_writes_token_header_and_column_count() {
522        let cmd = TokenColMetaData {
523            columns: vec![
524                meta(TypeInfo::FixedLen(FixedLenType::Int4), "id"),
525                meta(TypeInfo::FixedLen(FixedLenType::Bit), "flag"),
526            ],
527        };
528
529        let mut buf = BytesMut::new();
530        cmd.encode(&mut buf).unwrap();
531
532        // First the ColMetaData token byte, then the little-endian column count.
533        assert_eq!(buf[0], TokenType::ColMetaData as u8);
534        assert_eq!(u16::from_le_bytes([buf[1], buf[2]]), 2);
535        // The two column bodies follow the 3-byte header.
536        assert!(buf.len() > 3);
537    }
538
539    #[tokio::test]
540    async fn zero_columns_yields_empty() {
541        let mut buf = BytesMut::new();
542        buf.put_u16_le(0);
543
544        let decoded = TokenColMetaData::decode(&mut buf.into_sql_read_bytes())
545            .await
546            .unwrap();
547        assert!(decoded.columns.is_empty());
548    }
549
550    #[tokio::test]
551    async fn text_column_reads_table_name_parts() {
552        let mut buf = BytesMut::new();
553        buf.put_u16_le(1); // one column
554
555        // user_ty + flags
556        buf.put_u32_le(0);
557        buf.put_u16_le(BitFlags::bits(BitFlags::from(ColumnFlag::Nullable)));
558
559        // type info for a text column with collation
560        let ti = TypeInfo::VarLenSized(VarLenContext::new(
561            VarLenType::Text,
562            2147483647,
563            Some(Collation::new(13632521, 52)),
564        ));
565        ti.encode(&mut buf).unwrap();
566
567        // table name: one part, us_varchar "dbo"
568        buf.put_u8(1);
569        let part: Vec<u16> = "dbo".encode_utf16().collect();
570        buf.put_u16_le(part.len() as u16);
571        for c in part {
572            buf.put_u16_le(c);
573        }
574
575        // column name (b_varchar)
576        let name: Vec<u16> = "body".encode_utf16().collect();
577        buf.put_u8(name.len() as u8);
578        for c in name {
579            buf.put_u16_le(c);
580        }
581
582        let decoded = TokenColMetaData::decode(&mut buf.into_sql_read_bytes())
583            .await
584            .unwrap();
585
586        assert_eq!(decoded.columns.len(), 1);
587        assert_eq!(decoded.columns[0].col_name, "body");
588    }
589
590    #[test]
591    fn display_formats_various_types() {
592        let cases = vec![
593            (TypeInfo::FixedLen(FixedLenType::Int4), "c int"),
594            (TypeInfo::FixedLen(FixedLenType::Bit), "c bit"),
595            (TypeInfo::FixedLen(FixedLenType::Float8), "c float"),
596            (
597                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Intn, 1, None)),
598                "c tinyint",
599            ),
600            (
601                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Intn, 4, None)),
602                "c int",
603            ),
604            (
605                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Floatn, 4, None)),
606                "c real",
607            ),
608            (
609                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Guid, 16, None)),
610                "c uniqueidentifier",
611            ),
612            (
613                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::BigVarBin, 100, None)),
614                "c varbinary(100)",
615            ),
616            (
617                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::BigVarBin, 100000, None)),
618                "c varbinary(max)",
619            ),
620            (
621                TypeInfo::VarLenSizedPrecision {
622                    ty: VarLenType::Decimaln,
623                    size: 17,
624                    precision: 18,
625                    scale: 2,
626                },
627                "c decimal(18,2)",
628            ),
629            // Numericn must render as `numeric(...)`, distinct from the
630            // `decimal(...)` fallback that every other precision type uses.
631            (
632                TypeInfo::VarLenSizedPrecision {
633                    ty: VarLenType::Numericn,
634                    size: 9,
635                    precision: 10,
636                    scale: 4,
637                },
638                "c numeric(10,4)",
639            ),
640            (
641                TypeInfo::Xml {
642                    schema: None,
643                    size: 0,
644                },
645                "c xml",
646            ),
647        ];
648
649        for (ty, expected) in cases {
650            // Display brackets the column name for use in bulk `INSERT` statements.
651            let expected = expected.replacen("c ", "[c] ", 1);
652            assert_eq!(format!("{}", meta(ty, "c")), expected);
653        }
654    }
655
656    #[test]
657    fn null_value_maps_types() {
658        let fixed = BaseMetaDataColumn {
659            flags: BitFlags::empty(),
660            ty: TypeInfo::FixedLen(FixedLenType::Int4),
661        };
662        assert_eq!(fixed.null_value(), ColumnData::I32(None));
663
664        let varlen = BaseMetaDataColumn {
665            flags: BitFlags::empty(),
666            ty: TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Intn, 2, None)),
667        };
668        assert_eq!(varlen.null_value(), ColumnData::I16(None));
669
670        // Each Intn width maps to a distinct integer column; 1 and 4 sit either
671        // side of the `_ => I64` fallback and pin their own arms.
672        let tinyint = BaseMetaDataColumn {
673            flags: BitFlags::empty(),
674            ty: TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Intn, 1, None)),
675        };
676        assert_eq!(tinyint.null_value(), ColumnData::U8(None));
677
678        let int4 = BaseMetaDataColumn {
679            flags: BitFlags::empty(),
680            ty: TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Intn, 4, None)),
681        };
682        assert_eq!(int4.null_value(), ColumnData::I32(None));
683
684        let int8 = BaseMetaDataColumn {
685            flags: BitFlags::empty(),
686            ty: TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Intn, 8, None)),
687        };
688        assert_eq!(int8.null_value(), ColumnData::I64(None));
689
690        // Floatn splits on width too: 4 bytes is F32, anything else F64.
691        let real = BaseMetaDataColumn {
692            flags: BitFlags::empty(),
693            ty: TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Floatn, 4, None)),
694        };
695        assert_eq!(real.null_value(), ColumnData::F32(None));
696
697        let double = BaseMetaDataColumn {
698            flags: BitFlags::empty(),
699            ty: TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Floatn, 8, None)),
700        };
701        assert_eq!(double.null_value(), ColumnData::F64(None));
702
703        let guid = BaseMetaDataColumn {
704            flags: BitFlags::empty(),
705            ty: TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Guid, 16, None)),
706        };
707        assert_eq!(guid.null_value(), ColumnData::Guid(None));
708    }
709
710    #[test]
711    fn null_value_maps_precision_and_xml_and_udt() {
712        let precision = BaseMetaDataColumn {
713            flags: BitFlags::empty(),
714            ty: TypeInfo::VarLenSizedPrecision {
715                ty: VarLenType::Numericn,
716                size: 17,
717                precision: 18,
718                scale: 2,
719            },
720        };
721        assert_eq!(precision.null_value(), ColumnData::Numeric(None));
722
723        let xml = BaseMetaDataColumn {
724            flags: BitFlags::empty(),
725            ty: TypeInfo::Xml {
726                schema: None,
727                size: 0,
728            },
729        };
730        assert_eq!(xml.null_value(), ColumnData::Xml(None));
731
732        let udt = BaseMetaDataColumn {
733            flags: BitFlags::empty(),
734            ty: TypeInfo::Udt(crate::tds::codec::type_info::UdtInfo {
735                max_byte_size: 0,
736                db_name: "db".into(),
737                schema_name: "dbo".into(),
738                type_name: "T".into(),
739                assembly_qualified_name: "A".into(),
740            }),
741        };
742        assert_eq!(udt.null_value(), ColumnData::Binary(None));
743    }
744
745    #[test]
746    fn base_meta_data_column_flag_accessors() {
747        let base = BaseMetaDataColumn {
748            flags: ColumnFlag::Nullable | ColumnFlag::Identity | ColumnFlag::Updateable,
749            ty: TypeInfo::FixedLen(FixedLenType::Int4),
750        };
751
752        assert!(base.is_nullable());
753        assert!(base.is_identity());
754        assert!(base.is_updateable());
755        assert_eq!(base.ty(), &TypeInfo::FixedLen(FixedLenType::Int4));
756        assert_eq!(base.flags(), base.flags);
757
758        let base2 = BaseMetaDataColumn {
759            flags: BitFlags::empty(),
760            ty: TypeInfo::FixedLen(FixedLenType::Int4),
761        };
762        assert!(!base2.is_nullable());
763        assert!(!base2.is_identity());
764        assert!(!base2.is_updateable());
765    }
766
767    #[test]
768    fn meta_data_column_accessors() {
769        let m = meta(TypeInfo::FixedLen(FixedLenType::Int4), "id");
770        assert_eq!(m.col_name(), "id");
771        assert!(m.base().is_nullable());
772    }
773
774    #[test]
775    fn display_formats_more_types() {
776        let cases = vec![
777            (
778                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Bitn, 1, None)),
779                "c bit",
780            ),
781            (
782                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Datetimen, 8, None)),
783                "c datetime",
784            ),
785            (
786                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Money, 4, None)),
787                "c smallmoney",
788            ),
789            (
790                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Money, 8, None)),
791                "c money",
792            ),
793            (
794                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::BigVarChar, 100, None)),
795                "c varchar(100)",
796            ),
797            (
798                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::BigVarChar, 100000, None)),
799                "c varchar(max)",
800            ),
801            (
802                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::BigBinary, 10, None)),
803                "c binary(10)",
804            ),
805            (
806                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::BigChar, 10, None)),
807                "c char(10)",
808            ),
809            (
810                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::NVarchar, 100, None)),
811                "c nvarchar(50)",
812            ),
813            (
814                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::NVarchar, 8000, None)),
815                "c nvarchar(4000)",
816            ),
817            (
818                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::NVarchar, 100000, None)),
819                "c nvarchar(max)",
820            ),
821            (
822                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::NChar, 10, None)),
823                "c nchar(5)",
824            ),
825            (
826                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Datetimen, 4, None)),
827                "c smalldatetime",
828            ),
829            (
830                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Text, 0, None)),
831                "c text",
832            ),
833            (
834                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Image, 0, None)),
835                "c image",
836            ),
837            (
838                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::NText, 0, None)),
839                "c ntext",
840            ),
841            (
842                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Intn, 2, None)),
843                "c smallint",
844            ),
845            (
846                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Intn, 8, None)),
847                "c bigint",
848            ),
849            (
850                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Floatn, 8, None)),
851                "c float",
852            ),
853            (
854                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::SSVariant, 0, None)),
855                "c sql_variant",
856            ),
857        ];
858
859        for (ty, expected) in cases {
860            let expected = expected.replacen("c ", "[c] ", 1);
861            assert_eq!(format!("{}", meta(ty, "c")), expected);
862        }
863    }
864
865    #[test]
866    fn display_formats_udt_and_decimaln() {
867        let udt = TypeInfo::Udt(crate::tds::codec::type_info::UdtInfo {
868            max_byte_size: 0,
869            db_name: "db".into(),
870            schema_name: "dbo".into(),
871            type_name: "MyType".into(),
872            assembly_qualified_name: "asm".into(),
873        });
874        assert_eq!(format!("{}", meta(udt, "c")), "[c] dbo.MyType");
875
876        let decimaln = TypeInfo::VarLenSizedPrecision {
877            ty: VarLenType::Decimaln,
878            size: 17,
879            precision: 10,
880            scale: 4,
881        };
882        assert_eq!(format!("{}", meta(decimaln, "c")), "[c] decimal(10,4)");
883    }
884
885    #[tokio::test]
886    async fn decode_all_ones_column_count_yields_empty() {
887        // column_count == 0xffff is treated as "no columns" (guards against a
888        // sentinel/placeholder value rather than a real column list).
889        let mut buf = BytesMut::new();
890        buf.put_u16_le(0xffff);
891
892        let decoded = TokenColMetaData::decode(&mut buf.into_sql_read_bytes())
893            .await
894            .unwrap();
895        assert!(decoded.columns.is_empty());
896    }
897
898    #[test]
899    fn display_formats_fixed_len_money_and_datetime_types() {
900        // Covers the FixedLenType Display arms not exercised elsewhere:
901        // tinyint/smallint/smalldatetime/real/money/datetime/smallmoney/bigint
902        // and the `Null` sentinel (which surfaces as `int`).
903        let cases = vec![
904            (TypeInfo::FixedLen(FixedLenType::Int1), "c tinyint"),
905            (TypeInfo::FixedLen(FixedLenType::Int2), "c smallint"),
906            (
907                TypeInfo::FixedLen(FixedLenType::Datetime4),
908                "c smalldatetime",
909            ),
910            (TypeInfo::FixedLen(FixedLenType::Float4), "c real"),
911            (TypeInfo::FixedLen(FixedLenType::Money), "c money"),
912            (TypeInfo::FixedLen(FixedLenType::Datetime), "c datetime"),
913            (TypeInfo::FixedLen(FixedLenType::Money4), "c smallmoney"),
914            (TypeInfo::FixedLen(FixedLenType::Int8), "c bigint"),
915            (TypeInfo::FixedLen(FixedLenType::Null), "c int"),
916        ];
917
918        for (ty, expected) in cases {
919            let expected = expected.replacen("c ", "[c] ", 1);
920            assert_eq!(format!("{}", meta(ty, "c")), expected);
921        }
922    }
923
924    #[cfg(feature = "tds73")]
925    #[test]
926    fn display_formats_tds73_date_time_types() {
927        // date/time/datetime2/datetimeoffset Display arms (tds73-only).
928        let cases = vec![
929            (
930                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Daten, 3, None)),
931                "c date",
932            ),
933            (
934                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Timen, 7, None)),
935                "c time(7)",
936            ),
937            (
938                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Timen, 3, None)),
939                "c time(3)",
940            ),
941            (
942                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Datetime2, 7, None)),
943                "c datetime2(7)",
944            ),
945            (
946                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::DatetimeOffsetn, 0, None)),
947                "c datetimeoffset(0)",
948            ),
949        ];
950
951        for (ty, expected) in cases {
952            let expected = expected.replacen("c ", "[c] ", 1);
953            assert_eq!(format!("{}", meta(ty, "c")), expected);
954        }
955    }
956
957    #[test]
958    fn display_quotes_the_column_name() {
959        let m = meta(TypeInfo::FixedLen(FixedLenType::Int4), "a]b");
960        assert_eq!(format!("{m}"), "[a]]b] int");
961    }
962
963    #[test]
964    fn text_column_metadata_carries_a_table_name() {
965        let text = meta(
966            TypeInfo::VarLenSized(VarLenContext::new(
967                VarLenType::Text,
968                2147483647,
969                Some(Collation::new(13632521, 52)),
970            )),
971            "c",
972        );
973        let mut buf = BytesMut::new();
974        text.encode(&mut buf).unwrap();
975
976        // user type 4, flags 2, type 1, length 4, collation 5, table name 2,
977        // column name 1 + 2.
978        assert_eq!(buf.len(), 21);
979        assert_eq!(&buf[16..18], &[0, 0]);
980    }
981
982    #[test]
983    fn variant_collation_is_kept_off_the_wire() {
984        let mut base = BaseMetaDataColumn {
985            flags: BitFlags::empty(),
986            ty: TypeInfo::VarLenSized(VarLenContext::new(VarLenType::SSVariant, 8016, None)),
987        };
988        let mut plain = BytesMut::new();
989        base.clone().encode(&mut plain).unwrap();
990
991        base.set_variant_collation(Some(Collation::new(13632521, 52)));
992        let mut stamped = BytesMut::new();
993        base.clone().encode(&mut stamped).unwrap();
994
995        assert_eq!(plain, stamped);
996        match base.ty {
997            TypeInfo::VarLenSized(cx) => assert!(cx.collation().is_some()),
998            _ => unreachable!(),
999        }
1000    }
1001
1002    #[test]
1003    fn display_var_len_other_fallback_uses_debug_name() {
1004        // A VarLenSized carrying a type not matched by any explicit Display arm
1005        // (e.g. Decimaln) hits the `other => {other:?}` fallback rather than
1006        // panicking.
1007        let ty = TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Decimaln, 17, None));
1008        assert_eq!(format!("{}", meta(ty, "c")), "[c] Decimaln");
1009    }
1010
1011    #[test]
1012    fn null_value_all_fixed_len() {
1013        use FixedLenType::*;
1014        let cases = [
1015            (Null, ColumnData::I32(None)),
1016            (Int1, ColumnData::U8(None)),
1017            (Bit, ColumnData::Bit(None)),
1018            (Int2, ColumnData::I16(None)),
1019            (Int4, ColumnData::I32(None)),
1020            (Datetime4, ColumnData::SmallDateTime(None)),
1021            (Float4, ColumnData::F32(None)),
1022            (Money, ColumnData::Numeric(None)),
1023            (Datetime, ColumnData::DateTime(None)),
1024            (Float8, ColumnData::F64(None)),
1025            (Money4, ColumnData::Numeric(None)),
1026            (Int8, ColumnData::I64(None)),
1027        ];
1028
1029        for (ty, expected) in cases {
1030            let base = BaseMetaDataColumn {
1031                flags: BitFlags::empty(),
1032                ty: TypeInfo::FixedLen(ty),
1033            };
1034            assert_eq!(base.null_value(), expected);
1035        }
1036    }
1037
1038    fn vsize_null(ty: VarLenType, len: usize) -> ColumnData<'static> {
1039        BaseMetaDataColumn {
1040            flags: BitFlags::empty(),
1041            ty: TypeInfo::VarLenSized(VarLenContext::new(ty, len, None)),
1042        }
1043        .null_value()
1044    }
1045
1046    #[test]
1047    fn null_value_all_var_len_sized() {
1048        use VarLenType::*;
1049        assert_eq!(vsize_null(Guid, 16), ColumnData::Guid(None));
1050        assert_eq!(vsize_null(Bitn, 1), ColumnData::Bit(None));
1051        assert_eq!(vsize_null(Decimaln, 17), ColumnData::Numeric(None));
1052        assert_eq!(vsize_null(Numericn, 17), ColumnData::Numeric(None));
1053        assert_eq!(vsize_null(Money, 4), ColumnData::Numeric(None));
1054        assert_eq!(vsize_null(Money, 8), ColumnData::Numeric(None));
1055        assert_eq!(vsize_null(Datetimen, 4), ColumnData::SmallDateTime(None));
1056        assert_eq!(vsize_null(Datetimen, 8), ColumnData::DateTime(None));
1057        assert_eq!(vsize_null(BigVarBin, 100), ColumnData::Binary(None));
1058        assert_eq!(vsize_null(BigVarChar, 100), ColumnData::String(None));
1059        assert_eq!(vsize_null(BigBinary, 10), ColumnData::Binary(None));
1060        assert_eq!(vsize_null(BigChar, 10), ColumnData::String(None));
1061        assert_eq!(vsize_null(NVarchar, 100), ColumnData::String(None));
1062        assert_eq!(vsize_null(NChar, 10), ColumnData::String(None));
1063        assert_eq!(vsize_null(Xml, 0), ColumnData::Xml(None));
1064        assert_eq!(vsize_null(Udt, 0), ColumnData::Binary(None));
1065        assert_eq!(vsize_null(Text, 0), ColumnData::String(None));
1066        assert_eq!(vsize_null(Image, 0), ColumnData::Binary(None));
1067        assert_eq!(vsize_null(NText, 0), ColumnData::String(None));
1068        assert_eq!(vsize_null(SSVariant, 0), ColumnData::String(None));
1069    }
1070
1071    #[cfg(feature = "tds73")]
1072    #[test]
1073    fn null_value_var_len_sized_tds73() {
1074        use VarLenType::*;
1075        assert_eq!(vsize_null(Daten, 3), ColumnData::Date(None));
1076        assert_eq!(vsize_null(Timen, 7), ColumnData::Time(None));
1077        assert_eq!(vsize_null(Datetime2, 7), ColumnData::DateTime2(None));
1078        assert_eq!(
1079            vsize_null(DatetimeOffsetn, 7),
1080            ColumnData::DateTimeOffset(None)
1081        );
1082    }
1083
1084    fn vprec_null(ty: VarLenType) -> ColumnData<'static> {
1085        BaseMetaDataColumn {
1086            flags: BitFlags::empty(),
1087            ty: TypeInfo::VarLenSizedPrecision {
1088                ty,
1089                size: 8,
1090                precision: 18,
1091                scale: 2,
1092            },
1093        }
1094        .null_value()
1095    }
1096
1097    #[test]
1098    fn null_value_all_var_len_precision() {
1099        use VarLenType::*;
1100        assert_eq!(vprec_null(Guid), ColumnData::Guid(None));
1101        assert_eq!(vprec_null(Intn), ColumnData::I32(None));
1102        assert_eq!(vprec_null(Bitn), ColumnData::Bit(None));
1103        assert_eq!(vprec_null(Decimaln), ColumnData::Numeric(None));
1104        assert_eq!(vprec_null(Numericn), ColumnData::Numeric(None));
1105        assert_eq!(vprec_null(Floatn), ColumnData::F32(None));
1106        assert_eq!(vprec_null(Money), ColumnData::Numeric(None));
1107        assert_eq!(vprec_null(Datetimen), ColumnData::DateTime(None));
1108        assert_eq!(vprec_null(BigVarBin), ColumnData::Binary(None));
1109        assert_eq!(vprec_null(BigVarChar), ColumnData::String(None));
1110        assert_eq!(vprec_null(BigBinary), ColumnData::Binary(None));
1111        assert_eq!(vprec_null(BigChar), ColumnData::String(None));
1112        assert_eq!(vprec_null(NVarchar), ColumnData::String(None));
1113        assert_eq!(vprec_null(NChar), ColumnData::String(None));
1114        assert_eq!(vprec_null(Xml), ColumnData::Xml(None));
1115        assert_eq!(vprec_null(Udt), ColumnData::Binary(None));
1116        assert_eq!(vprec_null(Text), ColumnData::String(None));
1117        assert_eq!(vprec_null(Image), ColumnData::Binary(None));
1118        assert_eq!(vprec_null(NText), ColumnData::String(None));
1119        assert_eq!(vprec_null(SSVariant), ColumnData::String(None));
1120    }
1121
1122    #[cfg(feature = "tds73")]
1123    #[test]
1124    fn null_value_var_len_precision_tds73() {
1125        use VarLenType::*;
1126        assert_eq!(vprec_null(Daten), ColumnData::Date(None));
1127        assert_eq!(vprec_null(Timen), ColumnData::Time(None));
1128        assert_eq!(vprec_null(Datetime2), ColumnData::DateTime2(None));
1129        assert_eq!(
1130            vprec_null(DatetimeOffsetn),
1131            ColumnData::DateTimeOffset(None)
1132        );
1133    }
1134
1135    #[test]
1136    fn column_flag_bits_are_distinct() {
1137        let all = ColumnFlag::Nullable
1138            | ColumnFlag::CaseSensitive
1139            | ColumnFlag::Updateable
1140            | ColumnFlag::UpdateableUnknown
1141            | ColumnFlag::Identity
1142            | ColumnFlag::Computed
1143            | ColumnFlag::FixedLenClrType
1144            | ColumnFlag::SparseColumnSet
1145            | ColumnFlag::Encrypted
1146            | ColumnFlag::Hidden
1147            | ColumnFlag::Key
1148            | ColumnFlag::NullableUnknown;
1149
1150        assert!(all.contains(ColumnFlag::Nullable));
1151        assert!(all.contains(ColumnFlag::NullableUnknown));
1152        assert_eq!(BitFlags::bits(all).count_ones(), 12);
1153    }
1154
1155    #[test]
1156    fn column_name_over_255_units_is_an_error() {
1157        // 256 overflowed the u8 length: a debug-build panic, 0 in release.
1158        let column = |name: String| MetaDataColumn {
1159            base: BaseMetaDataColumn {
1160                flags: ColumnFlag::Nullable.into(),
1161                ty: TypeInfo::FixedLen(FixedLenType::Int4),
1162            },
1163            col_name: name.into(),
1164        };
1165
1166        for name in ["a".repeat(256), "\u{1F600}".repeat(128)] {
1167            let err = column(name).encode(&mut BytesMut::new()).unwrap_err();
1168            assert!(matches!(err, crate::Error::Conversion(_)), "{err:?}");
1169        }
1170
1171        // UserType, Flags, TYPE_INFO, then the name's length.
1172        let mut buf = BytesMut::new();
1173        column("a".repeat(255)).encode(&mut buf).unwrap();
1174        assert_eq!(buf[7], 255);
1175    }
1176}