Skip to main content

tiberius/tds/codec/token/
token_col_metadata.rs

1use std::{borrow::Cow, fmt::Display};
2
3use crate::{
4    tds::codec::{encode_b_varchar, Encode, FixedLenType, TokenType, TypeInfo, VarLenType},
5    Column, ColumnData, ColumnType, Error, SqlReadBytes,
6};
7use asynchronous_codec::BytesMut;
8use bytes::BufMut;
9use enumflags2::{bitflags, BitFlags};
10
11#[derive(Debug, Clone)]
12pub struct TokenColMetaData<'a> {
13    pub columns: Vec<MetaDataColumn<'a>>,
14}
15
16/// Metadata for a single result/table column: its name plus the
17/// [`BaseMetaDataColumn`] describing its type, size and flags.
18#[derive(Debug, Clone)]
19pub struct MetaDataColumn<'a> {
20    /// The type and flag metadata for the column.
21    pub base: BaseMetaDataColumn,
22    /// The name of the column.
23    pub col_name: Cow<'a, str>,
24}
25
26impl<'a> MetaDataColumn<'a> {
27    /// The name of the column.
28    pub fn col_name(&self) -> &str {
29        self.col_name.as_ref()
30    }
31
32    /// The [`BaseMetaDataColumn`] describing the column's type and flags
33    /// (nullability, identity, etc.).
34    pub fn base(&self) -> &BaseMetaDataColumn {
35        &self.base
36    }
37}
38
39impl<'a> Display for MetaDataColumn<'a> {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        // Bracket-quote the identifier, escaping any literal `]` by doubling it
42        // (`]]`) per the T-SQL rule. Without this a column name containing `]`
43        // (e.g. `my]col`) would emit a malformed identifier `[my]col]`, breaking
44        // the `INSERT BULK (...)` column list this Display feeds into.
45        write!(f, "[{}] ", self.col_name.replace(']', "]]"))?;
46
47        match &self.base.ty {
48            TypeInfo::FixedLen(fixed) => match fixed {
49                FixedLenType::Int1 => write!(f, "tinyint")?,
50                FixedLenType::Bit => write!(f, "bit")?,
51                FixedLenType::Int2 => write!(f, "smallint")?,
52                FixedLenType::Int4 => write!(f, "int")?,
53                FixedLenType::Datetime4 => write!(f, "smalldatetime")?,
54                FixedLenType::Float4 => write!(f, "real")?,
55                FixedLenType::Money => write!(f, "money")?,
56                FixedLenType::Datetime => write!(f, "datetime")?,
57                FixedLenType::Float8 => write!(f, "float")?,
58                FixedLenType::Money4 => write!(f, "smallmoney")?,
59                FixedLenType::Int8 => write!(f, "bigint")?,
60                // The TDS "null" fixed type carries no value; surface it as the
61                // int it decodes to rather than panicking (a bare `SELECT NULL`
62                // produces such a column).
63                FixedLenType::Null => write!(f, "int")?,
64            },
65            TypeInfo::VarLenSized(ctx) => match ctx.r#type() {
66                VarLenType::Bitn => write!(f, "bit")?,
67                VarLenType::Guid => write!(f, "uniqueidentifier")?,
68                #[cfg(feature = "tds73")]
69                VarLenType::Daten => write!(f, "date")?,
70                #[cfg(feature = "tds73")]
71                VarLenType::Timen => write!(f, "time")?,
72                #[cfg(feature = "tds73")]
73                VarLenType::Datetime2 => write!(f, "datetime2({})", ctx.len())?,
74                VarLenType::Datetimen => write!(f, "datetime")?,
75                VarLenType::Money => match ctx.len() {
76                    4 => write!(f, "smallmoney")?,
77                    _ => write!(f, "money")?,
78                },
79                #[cfg(feature = "tds73")]
80                VarLenType::DatetimeOffsetn => write!(f, "datetimeoffset")?,
81                VarLenType::BigVarBin => {
82                    if ctx.len() <= 8000 {
83                        write!(f, "varbinary({})", ctx.len())?
84                    } else {
85                        write!(f, "varbinary(max)")?
86                    }
87                }
88                VarLenType::BigVarChar => {
89                    if ctx.len() <= 8000 {
90                        write!(f, "varchar({})", ctx.len())?
91                    } else {
92                        write!(f, "varchar(max)")?
93                    }
94                }
95                VarLenType::BigBinary => write!(f, "binary({})", ctx.len())?,
96                VarLenType::BigChar => write!(f, "char({})", ctx.len())?,
97                VarLenType::NVarchar => {
98                    if ctx.len() <= 4000 {
99                        write!(f, "nvarchar({})", ctx.len())?
100                    } else {
101                        write!(f, "nvarchar(max)")?
102                    }
103                }
104                VarLenType::NChar => write!(f, "nchar({})", ctx.len())?,
105                VarLenType::Text => write!(f, "text")?,
106                VarLenType::Image => write!(f, "image")?,
107                VarLenType::NText => write!(f, "ntext")?,
108                VarLenType::Intn => match ctx.len() {
109                    1 => write!(f, "tinyint")?,
110                    2 => write!(f, "smallint")?,
111                    4 => write!(f, "int")?,
112                    _ => write!(f, "bigint")?,
113                },
114                VarLenType::Floatn => match ctx.len() {
115                    4 => write!(f, "real")?,
116                    _ => write!(f, "float")?,
117                },
118                VarLenType::SSVariant => write!(f, "sql_variant")?,
119                // Any other var-len type (e.g. Decimaln/Numericn arriving
120                // without the precision/scale they need, or Xml/Udt appearing
121                // in a sized context) has no valid SQL type name we can emit
122                // here. Emitting a bogus name (such as the Debug name) would
123                // produce an invalid `INSERT BULK` statement, so return a
124                // formatting error instead. This keeps the library from ever
125                // panicking on server-supplied metadata while refusing to emit
126                // invalid SQL.
127                _ => return Err(std::fmt::Error),
128            },
129            TypeInfo::VarLenSizedPrecision {
130                ty,
131                size: _,
132                precision,
133                scale,
134            } => match ty {
135                VarLenType::Numericn => write!(f, "numeric({},{})", precision, scale)?,
136                // Decimaln, and any other precision-carrying type.
137                _ => write!(f, "decimal({},{})", precision, scale)?,
138            },
139            TypeInfo::Xml { .. } => write!(f, "xml")?,
140            TypeInfo::Udt(info) => write!(f, "{}.{}", info.schema_name, info.type_name)?,
141        }
142
143        Ok(())
144    }
145}
146
147/// Describes the type and flags of a column, exposing metadata such as the
148/// column type (including size, precision and scale), whether the column is
149/// nullable and whether it is an identity column.
150#[derive(Debug, Clone)]
151pub struct BaseMetaDataColumn {
152    /// The set of [`ColumnFlag`]s describing the column (nullability, identity,
153    /// updateability, and so on).
154    pub flags: BitFlags<ColumnFlag>,
155    /// The type of the column, including its size, precision and scale where
156    /// applicable.
157    pub ty: TypeInfo,
158    /// Destination table name, used only on the *encode* (bulk-load) path. Per
159    /// MS-TDS §2.2.7.4, `text`/`ntext`/`image` columns carry a `TableName`
160    /// element in COLMETADATA; the bulk-insert code sets this to the target
161    /// table so [`BaseMetaDataColumn::encode`] can emit it. It is always `None`
162    /// on the decode (server→client) path, which never needs it.
163    pub table_name: Option<String>,
164}
165
166impl BaseMetaDataColumn {
167    /// The type of the column, including its size, precision and scale where
168    /// applicable.
169    pub fn ty(&self) -> &TypeInfo {
170        &self.ty
171    }
172
173    /// The set of flags describing the column.
174    pub fn flags(&self) -> BitFlags<ColumnFlag> {
175        self.flags
176    }
177
178    /// `true` if the column accepts `NULL` values.
179    pub fn is_nullable(&self) -> bool {
180        self.flags.contains(ColumnFlag::Nullable)
181    }
182
183    /// `true` if the column is an identity column.
184    pub fn is_identity(&self) -> bool {
185        self.flags.contains(ColumnFlag::Identity)
186    }
187
188    /// `true` if the column is writeable (e.g. usable as a bulk-insert target).
189    ///
190    /// The COLMETADATA `usUpdateable` sub-field (MS-TDS §2.2.7.4) is one of
191    /// read-only (0), read/write (1), or **unknown** (2). SQL Server reports
192    /// `unknown` for many legitimate bulk-target columns rather than an explicit
193    /// read/write, so both read/write and unknown are treated as writeable here;
194    /// only an explicit read-only column (neither flag set) is excluded.
195    pub fn is_updateable(&self) -> bool {
196        self.flags.contains(ColumnFlag::Updateable)
197            || self.flags.contains(ColumnFlag::UpdateableUnknown)
198    }
199
200    pub(crate) fn null_value(&self) -> ColumnData<'static> {
201        match &self.ty {
202            TypeInfo::FixedLen(ty) => match ty {
203                FixedLenType::Null => ColumnData::I32(None),
204                FixedLenType::Int1 => ColumnData::U8(None),
205                FixedLenType::Bit => ColumnData::Bit(None),
206                FixedLenType::Int2 => ColumnData::I16(None),
207                FixedLenType::Int4 => ColumnData::I32(None),
208                FixedLenType::Datetime4 => ColumnData::SmallDateTime(None),
209                FixedLenType::Float4 => ColumnData::F32(None),
210                FixedLenType::Money => ColumnData::F64(None),
211                FixedLenType::Datetime => ColumnData::DateTime(None),
212                FixedLenType::Float8 => ColumnData::F64(None),
213                FixedLenType::Money4 => ColumnData::F32(None),
214                FixedLenType::Int8 => ColumnData::I64(None),
215            },
216            TypeInfo::VarLenSized(cx) => match cx.r#type() {
217                VarLenType::Guid => ColumnData::Guid(None),
218                VarLenType::Intn => match cx.len() {
219                    1 => ColumnData::U8(None),
220                    2 => ColumnData::I16(None),
221                    4 => ColumnData::I32(None),
222                    _ => ColumnData::I64(None),
223                },
224                VarLenType::Bitn => ColumnData::Bit(None),
225                VarLenType::Decimaln => ColumnData::Numeric(None),
226                VarLenType::Numericn => ColumnData::Numeric(None),
227                VarLenType::Floatn => match cx.len() {
228                    4 => ColumnData::F32(None),
229                    _ => ColumnData::F64(None),
230                },
231                VarLenType::Money => ColumnData::F64(None),
232                VarLenType::Datetimen => ColumnData::DateTime(None),
233                #[cfg(feature = "tds73")]
234                VarLenType::Daten => ColumnData::Date(None),
235                #[cfg(feature = "tds73")]
236                VarLenType::Timen => ColumnData::Time(None),
237                #[cfg(feature = "tds73")]
238                VarLenType::Datetime2 => ColumnData::DateTime2(None),
239                #[cfg(feature = "tds73")]
240                VarLenType::DatetimeOffsetn => ColumnData::DateTimeOffset(None),
241                VarLenType::BigVarBin => ColumnData::Binary(None),
242                VarLenType::BigVarChar => ColumnData::String(None),
243                VarLenType::BigBinary => ColumnData::Binary(None),
244                VarLenType::BigChar => ColumnData::String(None),
245                VarLenType::NVarchar => ColumnData::String(None),
246                VarLenType::NChar => ColumnData::String(None),
247                VarLenType::Xml => ColumnData::Xml(None),
248                // A null CLR UDT carries no payload; surface it as a null
249                // binary, matching `udt::decode` (which yields
250                // `ColumnData::Binary`).
251                VarLenType::Udt => ColumnData::Binary(None),
252                VarLenType::Text => ColumnData::String(None),
253                VarLenType::Image => ColumnData::Binary(None),
254                VarLenType::NText => ColumnData::String(None),
255                // A null `sql_variant` carries no base type, so surface a
256                // generic null value.
257                VarLenType::SSVariant => ColumnData::String(None),
258            },
259            TypeInfo::VarLenSizedPrecision { ty, .. } => match ty {
260                VarLenType::Guid => ColumnData::Guid(None),
261                VarLenType::Intn => ColumnData::I32(None),
262                VarLenType::Bitn => ColumnData::Bit(None),
263                VarLenType::Decimaln => ColumnData::Numeric(None),
264                VarLenType::Numericn => ColumnData::Numeric(None),
265                VarLenType::Floatn => ColumnData::F32(None),
266                VarLenType::Money => ColumnData::F64(None),
267                VarLenType::Datetimen => ColumnData::DateTime(None),
268                #[cfg(feature = "tds73")]
269                VarLenType::Daten => ColumnData::Date(None),
270                #[cfg(feature = "tds73")]
271                VarLenType::Timen => ColumnData::Time(None),
272                #[cfg(feature = "tds73")]
273                VarLenType::Datetime2 => ColumnData::DateTime2(None),
274                #[cfg(feature = "tds73")]
275                VarLenType::DatetimeOffsetn => ColumnData::DateTimeOffset(None),
276                VarLenType::BigVarBin => ColumnData::Binary(None),
277                VarLenType::BigVarChar => ColumnData::String(None),
278                VarLenType::BigBinary => ColumnData::Binary(None),
279                VarLenType::BigChar => ColumnData::String(None),
280                VarLenType::NVarchar => ColumnData::String(None),
281                VarLenType::NChar => ColumnData::String(None),
282                VarLenType::Xml => ColumnData::Xml(None),
283                // A null CLR UDT carries no payload; surface it as a null
284                // binary, matching `udt::decode` (which yields
285                // `ColumnData::Binary`).
286                VarLenType::Udt => ColumnData::Binary(None),
287                VarLenType::Text => ColumnData::String(None),
288                VarLenType::Image => ColumnData::Binary(None),
289                VarLenType::NText => ColumnData::String(None),
290                // A null `sql_variant` carries no base type, so surface a
291                // generic null value.
292                VarLenType::SSVariant => ColumnData::String(None),
293            },
294            TypeInfo::Xml { .. } => ColumnData::Xml(None),
295            TypeInfo::Udt(_) => ColumnData::Binary(None),
296        }
297    }
298}
299
300impl<'a> Encode<BytesMut> for TokenColMetaData<'a> {
301    fn encode(self, dst: &mut BytesMut) -> crate::Result<()> {
302        dst.put_u8(TokenType::ColMetaData as u8);
303        dst.put_u16_le(self.columns.len() as u16);
304
305        for col in self.columns.into_iter() {
306            col.encode(dst)?;
307        }
308
309        Ok(())
310    }
311}
312
313impl<'a> Encode<BytesMut> for MetaDataColumn<'a> {
314    fn encode(self, dst: &mut BytesMut) -> crate::Result<()> {
315        dst.put_u32_le(0);
316        self.base.encode(dst)?;
317
318        // ColName is a B_VARCHAR (u8 code-unit count); reject an over-long name
319        // rather than wrap the counter and desync the wire.
320        encode_b_varchar(dst, &self.col_name)?;
321
322        Ok(())
323    }
324}
325
326impl Encode<BytesMut> for BaseMetaDataColumn {
327    fn encode(self, dst: &mut BytesMut) -> crate::Result<()> {
328        dst.put_u16_le(BitFlags::bits(self.flags));
329
330        // `text`/`ntext`/`image` columns carry a TableName element after the
331        // TYPE_INFO. The client->server INSERT BULK COLMETADATA uses a DIFFERENT
332        // TableName shape than the server->client result COLMETADATA the decode
333        // side reads:
334        //
335        //   * READ  (server->client): NumParts BYTE, then NumParts US_VARCHARs
336        //     (see `BaseMetaDataColumn::decode` below and MS-TDS §2.2.7.4).
337        //   * WRITE (client->server bulk): a single bare US_VARCHAR carrying the
338        //     whole destination table name, with NO NumParts byte and no
339        //     splitting on `.`.
340        //
341        // This asymmetry matches Microsoft's own go-mssqldb bulk-copy path
342        // (`createColMetadata` in bulkcopy.go), verified against real SQL Server:
343        // a `uint16` code-unit count + UTF-16LE bytes, no NumParts. Emitting the
344        // spec/result-style `NumParts` byte here inserts one extra leading 0x01
345        // the server does not expect, mis-parsing the column and overrunning the
346        // row stream (TDS error 4804). Only these three types carry a TableName,
347        // so the desync hits text/ntext/image bulk exclusively.
348        //
349        // Capture whether this is such a column before `encode` consumes `self.ty`.
350        let emits_table_name = matches!(
351            &self.ty,
352            TypeInfo::VarLenSized(cx)
353                if matches!(cx.r#type(), VarLenType::Text | VarLenType::NText | VarLenType::Image)
354        );
355
356        self.ty.encode(dst)?;
357
358        if emits_table_name {
359            let table_name = self.table_name.as_deref().unwrap_or("");
360            encode_us_varchar(dst, table_name)?;
361        }
362
363        Ok(())
364    }
365}
366
367/// Encode a US_VARCHAR: a `u16` length in UTF-16 code units followed by the
368/// UTF-16LE characters. The length field is a `u16`, so a part longer than
369/// `u16::MAX` code units cannot be represented and is rejected.
370fn encode_us_varchar(dst: &mut BytesMut, s: &str) -> crate::Result<()> {
371    let units = s.encode_utf16().count();
372    if units > u16::MAX as usize {
373        return Err(Error::BulkInput(
374            format!("table name is too long ({units} UTF-16 code units, max 65535)").into(),
375        ));
376    }
377
378    dst.put_u16_le(units as u16);
379    for chr in s.encode_utf16() {
380        dst.put_u16_le(chr);
381    }
382
383    Ok(())
384}
385
386/// A setting a column can hold.
387#[bitflags]
388#[repr(u16)]
389#[derive(Debug, Clone, Copy, PartialEq, Eq)]
390pub enum ColumnFlag {
391    /// The column can be null.
392    Nullable = 1 << 0,
393    /// Set for string columns with binary collation and always for the XML data
394    /// type.
395    CaseSensitive = 1 << 1,
396    /// If column is writeable. This is value 1 (0b01) of the 2-bit
397    /// `usUpdateable` sub-field (MS-TDS §2.2.7.4), i.e. the low bit at position
398    /// 2 (0x04): `0 = read-only, 1 = read/write, 2 = unknown`.
399    Updateable = 1 << 2,
400    /// Column modification status unknown. This is value 2 (0b10) of the 2-bit
401    /// `usUpdateable` sub-field, i.e. the high bit at position 3 (0x08).
402    UpdateableUnknown = 1 << 3,
403    /// Column is an identity.
404    Identity = 1 << 4,
405    /// Column is computed. Per MS-TDS §2.2.7.4 `fComputed` is bit 5 (0x0020),
406    /// immediately after `fIdentity` and before the 2-bit `usReservedODBC`
407    /// field (bits 6-7). Introduced in TDS 7.2.
408    Computed = 1 << 5,
409    /// Column is a fixed-length common language runtime user-defined type (CLR
410    /// UDT). Per MS-TDS §2.2.7.4 `fFixedLenCLRType` is bit 8 (0x0100), directly
411    /// after `usReservedODBC` (bits 6-7). Introduced in TDS 7.2.
412    FixedLenClrType = 1 << 8,
413    /// Column is the special XML column for the sparse column set. Per
414    /// MS-TDS §2.2.7.4 `fSparseColumnSet` is bit 10 (0x0400): bit 9 is a
415    /// reserved bit (`FRESERVEDBIT`). Introduced in TDS 7.3.B.
416    SparseColumnSet = 1 << 10,
417    /// Column is encrypted transparently and has to be decrypted to view the
418    /// plaintext value. This flag is valid when the column encryption feature
419    /// is negotiated between client and server and is turned on. Per
420    /// MS-TDS §2.2.7.4 `fEncrypted` is bit 11 (0x0800), directly after
421    /// `fSparseColumnSet`. Introduced in TDS 7.4.
422    Encrypted = 1 << 11,
423    /// Column is part of a hidden primary key created to support a T-SQL SELECT
424    /// statement containing FOR BROWSE.
425    Hidden = 1 << 13,
426    /// Column is part of a primary key for the row and the T-SQL SELECT
427    /// statement contains FOR BROWSE.
428    Key = 1 << 14,
429    /// It is unknown whether the column might be nullable.
430    NullableUnknown = 1 << 15,
431}
432
433impl TokenColMetaData<'static> {
434    pub(crate) async fn decode<R>(src: &mut R) -> crate::Result<Self>
435    where
436        R: SqlReadBytes + Unpin,
437    {
438        let column_count = src.read_u16_le().await?;
439        // `column_count` is an untrusted u16 (up to 65535); cap the up-front
440        // reservation so a hostile COLMETADATA token can't force a large
441        // transient allocation before the column bodies arrive. The Vec still
442        // grows as real columns are decoded.
443        let mut columns = Vec::with_capacity(
444            (column_count as usize).min(crate::tds::codec::column_data::MAX_PREALLOC),
445        );
446
447        // `0xffff` is the "no metadata" sentinel; any other count drives the
448        // loop directly (a count of 0 simply iterates zero times).
449        if column_count < 0xffff {
450            for _ in 0..column_count {
451                let base = BaseMetaDataColumn::decode(src).await?;
452                let col_name = Cow::from(src.read_b_varchar().await?);
453
454                columns.push(MetaDataColumn { base, col_name });
455            }
456        }
457
458        Ok(TokenColMetaData { columns })
459    }
460}
461
462impl<'a> TokenColMetaData<'a> {
463    pub(crate) fn columns(&self) -> impl Iterator<Item = Column> + '_ {
464        self.columns.iter().map(|x| Column {
465            name: x.col_name.to_string(),
466            column_type: ColumnType::from(&x.base.ty),
467        })
468    }
469}
470
471impl BaseMetaDataColumn {
472    pub(crate) async fn decode<R>(src: &mut R) -> crate::Result<Self>
473    where
474        R: SqlReadBytes + Unpin,
475    {
476        use VarLenType::*;
477
478        let _user_ty = src.read_u32_le().await?;
479
480        // The COLMETADATA `Flags` field (MS-TDS §2.2.7.4) is a 16-bit field that
481        // includes reserved / ODBC bits the server may set and which future
482        // protocol revisions may extend. Truncate to the flags we model rather
483        // than rejecting the whole token on an unrecognized bit.
484        let flags = BitFlags::from_bits_truncate(src.read_u16_le().await?);
485
486        let ty = TypeInfo::decode(src).await?;
487
488        if let TypeInfo::VarLenSized(cx) = ty {
489            if let Text | NText | Image = cx.r#type() {
490                let num_of_parts = src.read_u8().await?;
491
492                // table name
493                for _ in 0..num_of_parts {
494                    src.read_us_varchar().await?;
495                }
496            };
497        };
498
499        Ok(BaseMetaDataColumn {
500            flags,
501            ty,
502            table_name: None,
503        })
504    }
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510    use crate::sql_read_bytes::test_utils::IntoSqlReadBytes;
511    use crate::tds::codec::type_info::VarLenContext;
512    use crate::tds::Collation;
513    use std::fmt::Write as _;
514
515    // Build the on-wire bytes a US_VARCHAR should produce for `s`.
516    fn us_varchar_bytes(s: &str) -> Vec<u8> {
517        let mut out = Vec::new();
518        let units: Vec<u16> = s.encode_utf16().collect();
519        out.extend_from_slice(&(units.len() as u16).to_le_bytes());
520        for u in units {
521            out.extend_from_slice(&u.to_le_bytes());
522        }
523        out
524    }
525
526    fn text_column(table_name: Option<&str>) -> BaseMetaDataColumn {
527        BaseMetaDataColumn {
528            flags: ColumnFlag::Nullable.into(),
529            ty: TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Text, 2147483647, None)),
530            table_name: table_name.map(str::to_string),
531        }
532    }
533
534    // On the client->server INSERT BULK path a `text` column's metadata must end
535    // with the TableName as a SINGLE bare US_VARCHAR carrying the whole name --
536    // NO NumParts byte and no splitting on `.` -- matching go-mssqldb's verified
537    // bulkcopy `createColMetadata`. A dotted name is sent verbatim as one part.
538    #[test]
539    fn text_column_emits_dotted_name_as_bare_us_varchar() {
540        let mut buf = BytesMut::new();
541        text_column(Some("dbo.MyTable")).encode(&mut buf).unwrap();
542
543        // Whole name as one US_VARCHAR, with no leading NumParts byte.
544        let expected_tail = us_varchar_bytes("dbo.MyTable");
545
546        assert!(
547            buf.ends_with(&expected_tail),
548            "buffer {:02x?} did not end with bare US_VARCHAR TableName {:02x?}",
549            &buf[..],
550            expected_tail
551        );
552    }
553
554    // A single, unqualified name is likewise a single bare US_VARCHAR (no
555    // NumParts byte).
556    #[test]
557    fn text_column_single_name_is_bare_us_varchar() {
558        let mut buf = BytesMut::new();
559        text_column(Some("##bulk_test")).encode(&mut buf).unwrap();
560
561        let expected_tail = us_varchar_bytes("##bulk_test");
562        assert!(buf.ends_with(&expected_tail), "got {:02x?}", &buf[..]);
563    }
564
565    // Non-text columns must not emit any TableName, even if one is set: the bytes
566    // must be identical with and without a table name.
567    #[test]
568    fn non_text_column_never_emits_table_name() {
569        let with = {
570            let mut buf = BytesMut::new();
571            BaseMetaDataColumn {
572                flags: ColumnFlag::Nullable.into(),
573                ty: TypeInfo::FixedLen(FixedLenType::Int4),
574                table_name: Some("dbo.MyTable".to_string()),
575            }
576            .encode(&mut buf)
577            .unwrap();
578            buf.to_vec()
579        };
580        let without = {
581            let mut buf = BytesMut::new();
582            BaseMetaDataColumn {
583                flags: ColumnFlag::Nullable.into(),
584                ty: TypeInfo::FixedLen(FixedLenType::Int4),
585                table_name: None,
586            }
587            .encode(&mut buf)
588            .unwrap();
589            buf.to_vec()
590        };
591        assert_eq!(with, without);
592    }
593
594    // A US_VARCHAR length field is a u16; an over-long name must error rather
595    // than wrap the length (which would desync the wire).
596    #[test]
597    fn text_column_rejects_over_long_table_name() {
598        let long = "a".repeat(u16::MAX as usize + 1);
599        let mut buf = BytesMut::new();
600        let err = text_column(Some(&long)).encode(&mut buf).unwrap_err();
601        assert!(matches!(err, Error::BulkInput(_)), "got {err:?}");
602    }
603
604    fn meta(ty: TypeInfo, name: &'static str) -> MetaDataColumn<'static> {
605        MetaDataColumn {
606            base: BaseMetaDataColumn {
607                flags: ColumnFlag::Nullable.into(),
608                ty,
609                table_name: None,
610            },
611            col_name: Cow::Borrowed(name),
612        }
613    }
614
615    #[test]
616    fn display_var_len_unknown_type_yields_err_not_panic() {
617        // A VarLenSized carrying a type with no valid sized SQL representation
618        // (e.g. Decimaln/Numericn without precision/scale) must NOT panic and
619        // must NOT emit a bogus SQL type name. Formatting it returns a
620        // `std::fmt::Error` so the caller gets an `Err`, never a panic and
621        // never invalid SQL.
622        for ty in [VarLenType::Decimaln, VarLenType::Numericn] {
623            let col = meta(TypeInfo::VarLenSized(VarLenContext::new(ty, 17, None)), "c");
624            let mut out = String::new();
625            let result = write!(out, "{col}");
626            assert!(
627                result.is_err(),
628                "expected Err for unhandled var-len type {ty:?}, got Ok({out:?})"
629            );
630        }
631    }
632
633    #[test]
634    fn display_money_columns_never_panic_and_render_expected() {
635        // MONEY/SMALLMONEY reach the Display impl both as FixedLen (Money /
636        // Money4) and as VarLenSized (Money with len 8 / 4). A money column
637        // must never fall through to a catch-all; each renders its exact SQL
638        // type name for the bulk `INSERT` column list. Assert the rendered type
639        // token (with a leading space so `money` can't match `smallmoney`) so
640        // the test is agnostic to how the column-name prefix is quoted.
641        let cases = vec![
642            (TypeInfo::FixedLen(FixedLenType::Money), " money"),
643            (TypeInfo::FixedLen(FixedLenType::Money4), " smallmoney"),
644            (
645                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Money, 8, None)),
646                " money",
647            ),
648            (
649                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Money, 4, None)),
650                " smallmoney",
651            ),
652        ];
653
654        for (ty, expected_suffix) in cases {
655            // Must not panic, and must end with the expected SQL type token.
656            let rendered = format!("{}", meta(ty, "c"));
657            assert!(
658                rendered.ends_with(expected_suffix),
659                "expected {rendered:?} to end with {expected_suffix:?}"
660            );
661        }
662    }
663
664    #[test]
665    fn display_text_ntext_image_render_expected_type_names() {
666        // The LOB var-len types feed the bulk `INSERT` column list and must
667        // render their exact SQL type names (not a sized form), complementing
668        // the encode-path `TableName` tests above. Assert on a leading-space
669        // suffix so the column-name prefix quoting is irrelevant.
670        let cases = [
671            (VarLenType::Text, " text"),
672            (VarLenType::NText, " ntext"),
673            (VarLenType::Image, " image"),
674        ];
675
676        for (ty, expected_suffix) in cases {
677            let rendered = format!(
678                "{}",
679                meta(
680                    TypeInfo::VarLenSized(VarLenContext::new(ty, 2147483647, None)),
681                    "c",
682                )
683            );
684            assert!(
685                rendered.ends_with(expected_suffix),
686                "expected {rendered:?} to end with {expected_suffix:?}",
687            );
688        }
689    }
690
691    fn column(name: &'static str) -> MetaDataColumn<'static> {
692        MetaDataColumn {
693            base: BaseMetaDataColumn {
694                flags: BitFlags::empty(),
695                ty: TypeInfo::FixedLen(FixedLenType::Int4),
696                table_name: None,
697            },
698            col_name: Cow::Borrowed(name),
699        }
700    }
701
702    // Bit-exact layout of the COLMETADATA `Flags` field per MS-TDS §2.2.7.4,
703    // in least-significant-bit order:
704    //   bit 0        fNullable
705    //   bit 1        fCaseSen
706    //   bits 2-3     usUpdateable (0=read-only, 1=read/write, 2=unknown)
707    //   bit 4        fIdentity
708    //   bit 5        fComputed
709    //   bits 6-7     usReservedODBC
710    //   bit 8        fFixedLenCLRType
711    //   ...
712    //   bit 13       fHidden
713    //   bit 14       fKey
714    //   bit 15       fNullableUnknown
715    // The `usUpdateable` sub-field is a 2-bit value, so read/write (value 1) is
716    // the low bit 0x04 and unknown (value 2) is the high bit 0x08. A server
717    // reporting a genuinely writeable column sets 0x04; `is_updateable()` must
718    // therefore key off 0x04, not 0x08.
719    #[test]
720    fn column_flag_bit_positions_match_ms_tds() {
721        assert_eq!(BitFlags::bits(BitFlags::from(ColumnFlag::Nullable)), 0x0001);
722        assert_eq!(
723            BitFlags::bits(BitFlags::from(ColumnFlag::CaseSensitive)),
724            0x0002
725        );
726        // usUpdateable: read/write = 0x04 (bit 2), unknown = 0x08 (bit 3).
727        assert_eq!(
728            BitFlags::bits(BitFlags::from(ColumnFlag::Updateable)),
729            0x0004
730        );
731        assert_eq!(
732            BitFlags::bits(BitFlags::from(ColumnFlag::UpdateableUnknown)),
733            0x0008
734        );
735        assert_eq!(BitFlags::bits(BitFlags::from(ColumnFlag::Identity)), 0x0010);
736    }
737
738    // `is_updateable()` means "not read-only", i.e. a valid bulk-insert target.
739    // usUpdateable read/write (1, wire bit 0x04) and unknown (2, wire bit 0x08)
740    // are both writeable; only explicit read-only (0, neither bit) is excluded.
741    // SQL Server reports `unknown` for many real bulk-target columns, so treating
742    // it as writeable is required (keying off the read/write bit alone drops
743    // every column and breaks bulk insert entirely).
744    #[test]
745    fn is_updateable_true_for_read_write_and_unknown_false_for_read_only() {
746        let read_write = BaseMetaDataColumn {
747            flags: BitFlags::from_bits_truncate(0x0004),
748            ty: TypeInfo::FixedLen(FixedLenType::Int4),
749            table_name: None,
750        };
751        assert!(read_write.is_updateable());
752
753        let unknown = BaseMetaDataColumn {
754            flags: BitFlags::from_bits_truncate(0x0008),
755            ty: TypeInfo::FixedLen(FixedLenType::Int4),
756            table_name: None,
757        };
758        assert!(unknown.is_updateable());
759
760        let read_only = BaseMetaDataColumn {
761            flags: BitFlags::from_bits_truncate(0x0000),
762            ty: TypeInfo::FixedLen(FixedLenType::Int4),
763            table_name: None,
764        };
765        assert!(!read_only.is_updateable());
766    }
767
768    // Byte-exact decode of a COLMETADATA `Flags` USHORT with a known set of
769    // sub-fields, constructed per the MS-TDS §2.2.7.4 LSB-first layout:
770    //   bit 0  fNullable            bit 5  fComputed
771    //   bit 1  fCaseSen             bits 6-7 usReservedODBC
772    //   bits 2-3 usUpdateable       bit 8  fFixedLenCLRType
773    //   bit 4  fIdentity            bit 9  (reserved)
774    //   bit 10 fSparseColumnSet     bit 13 fHidden
775    //   bit 11 fEncrypted           bit 14 fKey
776    //   bit 12 (usReserved3)        bit 15 fNullableUnknown
777    // This locks the wire layout: a regression that shifts any bit changes the
778    // decoded flag set and fails here.
779    #[test]
780    fn column_flags_decode_byte_exact_low_and_mid_bits() {
781        // fNullable(0) | usUpdateable=Read/Write(bit2) | fIdentity(4)
782        //   | fComputed(5) | fFixedLenCLRType(8) | fSparseColumnSet(10)
783        //   | fEncrypted(11)  ==>  0x0D35, little-endian on the wire.
784        let wire: [u8; 2] = [0x35, 0x0D];
785        let flags = BitFlags::<ColumnFlag>::from_bits_truncate(u16::from_le_bytes(wire));
786
787        let col = BaseMetaDataColumn {
788            flags,
789            ty: TypeInfo::FixedLen(FixedLenType::Int4),
790            table_name: None,
791        };
792
793        assert!(col.is_nullable());
794        assert!(col.is_updateable()); // usUpdateable == 1 (Read/Write, bit 2)
795        assert!(col.is_identity());
796        assert!(flags.contains(ColumnFlag::Computed));
797        assert!(flags.contains(ColumnFlag::FixedLenClrType));
798        assert!(flags.contains(ColumnFlag::SparseColumnSet));
799        assert!(flags.contains(ColumnFlag::Encrypted));
800
801        // Bits that were NOT set must not read back as set.
802        assert!(!flags.contains(ColumnFlag::CaseSensitive));
803        assert!(!flags.contains(ColumnFlag::UpdateableUnknown));
804        assert!(!flags.contains(ColumnFlag::Hidden));
805        assert!(!flags.contains(ColumnFlag::Key));
806        assert!(!flags.contains(ColumnFlag::NullableUnknown));
807    }
808
809    // Companion to the above, exercising the CaseSen bit, the *high* bit of
810    // usUpdateable (value 2 = unknown) and the top three flags introduced in
811    // TDS 7.2 (fHidden=13, fKey=14, fNullableUnknown=15).
812    #[test]
813    fn column_flags_decode_byte_exact_high_bits() {
814        // fCaseSen(1) | usUpdateable=Unknown(bit3) | fHidden(13) | fKey(14)
815        //   | fNullableUnknown(15)  ==>  0xE00A, little-endian on the wire.
816        let wire: [u8; 2] = [0x0A, 0xE0];
817        let flags = BitFlags::<ColumnFlag>::from_bits_truncate(u16::from_le_bytes(wire));
818
819        assert!(flags.contains(ColumnFlag::CaseSensitive));
820        assert!(flags.contains(ColumnFlag::UpdateableUnknown));
821        assert!(flags.contains(ColumnFlag::Hidden));
822        assert!(flags.contains(ColumnFlag::Key));
823        assert!(flags.contains(ColumnFlag::NullableUnknown));
824
825        // usUpdateable == 2 (unknown) means NOT read/write.
826        assert!(!flags.contains(ColumnFlag::Updateable));
827        assert!(!flags.contains(ColumnFlag::Nullable));
828        assert!(!flags.contains(ColumnFlag::Identity));
829        assert!(!flags.contains(ColumnFlag::Computed));
830    }
831
832    // ColName is a B_VARCHAR (u8 length); a name longer than 255 UTF-16 code
833    // units must error rather than wrap the counter and desync the wire.
834    #[test]
835    fn metadata_column_rejects_over_long_col_name() {
836        let col = MetaDataColumn {
837            base: BaseMetaDataColumn {
838                flags: BitFlags::empty(),
839                ty: TypeInfo::FixedLen(FixedLenType::Int4),
840                table_name: None,
841            },
842            col_name: Cow::Owned("a".repeat(256)),
843        };
844
845        let mut buf = BytesMut::new();
846        let err = col.encode(&mut buf).unwrap_err();
847        assert!(matches!(err, Error::Protocol(_)), "got {err:?}");
848    }
849
850    // A col_name of exactly 255 units is the boundary and must still encode with
851    // the correct u8 length prefix following the u32 user-type and TYPE_INFO.
852    #[test]
853    fn metadata_column_accepts_max_length_col_name() {
854        let col = MetaDataColumn {
855            base: BaseMetaDataColumn {
856                flags: BitFlags::empty(),
857                ty: TypeInfo::FixedLen(FixedLenType::Int4),
858                table_name: None,
859            },
860            col_name: Cow::Owned("a".repeat(255)),
861        };
862
863        let mut buf = BytesMut::new();
864        col.encode(&mut buf).expect("255-unit col_name must encode");
865        // Layout: u32 user-type (4) + flags u16 (2) + FixedLen TYPE_INFO (1) +
866        // B_VARCHAR length prefix.
867        assert_eq!(buf[7], 255);
868    }
869
870    #[test]
871    fn display_escapes_closing_bracket_in_column_name() {
872        // A `]` in the column name must be doubled so the bracket-quoted
873        // identifier stays well-formed for the `INSERT BULK (...)` column list.
874        assert_eq!(format!("{}", column("my]col")), "[my]]col] int");
875    }
876
877    #[test]
878    fn display_leaves_plain_column_name_unchanged() {
879        assert_eq!(format!("{}", column("foo")), "[foo] int");
880    }
881
882    #[tokio::test]
883    async fn round_trip_via_encode_decode() {
884        let cmd = TokenColMetaData {
885            columns: vec![
886                meta(TypeInfo::FixedLen(FixedLenType::Int4), "id"),
887                meta(
888                    TypeInfo::VarLenSized(VarLenContext::new(
889                        VarLenType::NVarchar,
890                        4000,
891                        Some(Collation::new(13632521, 52)),
892                    )),
893                    "name",
894                ),
895            ],
896        };
897
898        // Build a decodable buffer: column count followed by each column. The
899        // MetaDataColumn encoder writes the leading user-type u32 that the
900        // decoder expects.
901        let mut buf = BytesMut::new();
902        buf.put_u16_le(cmd.columns.len() as u16);
903        for col in cmd.columns.iter().cloned() {
904            col.encode(&mut buf).unwrap();
905        }
906
907        let decoded = TokenColMetaData::decode(&mut buf.into_sql_read_bytes())
908            .await
909            .unwrap();
910
911        assert_eq!(decoded.columns.len(), 2);
912        assert_eq!(decoded.columns[0].col_name, "id");
913        assert_eq!(decoded.columns[1].col_name, "name");
914
915        let columns: Vec<_> = decoded.columns().collect();
916        assert_eq!(columns.len(), 2);
917        assert_eq!(columns[0].name(), "id");
918    }
919
920    #[test]
921    fn encode_writes_token_header_and_column_count() {
922        let cmd = TokenColMetaData {
923            columns: vec![
924                meta(TypeInfo::FixedLen(FixedLenType::Int4), "id"),
925                meta(TypeInfo::FixedLen(FixedLenType::Bit), "flag"),
926            ],
927        };
928
929        let mut buf = BytesMut::new();
930        cmd.encode(&mut buf).unwrap();
931
932        // First the ColMetaData token byte, then the little-endian column count.
933        assert_eq!(buf[0], TokenType::ColMetaData as u8);
934        assert_eq!(u16::from_le_bytes([buf[1], buf[2]]), 2);
935        // The two column bodies follow the 3-byte header.
936        assert!(buf.len() > 3);
937    }
938
939    #[tokio::test]
940    async fn zero_columns_yields_empty() {
941        let mut buf = BytesMut::new();
942        buf.put_u16_le(0);
943
944        let decoded = TokenColMetaData::decode(&mut buf.into_sql_read_bytes())
945            .await
946            .unwrap();
947        assert!(decoded.columns.is_empty());
948    }
949
950    #[tokio::test]
951    async fn text_column_reads_table_name_parts() {
952        let mut buf = BytesMut::new();
953        buf.put_u16_le(1); // one column
954
955        // user_ty + flags
956        buf.put_u32_le(0);
957        buf.put_u16_le(BitFlags::bits(BitFlags::from(ColumnFlag::Nullable)));
958
959        // type info for a text column with collation
960        let ti = TypeInfo::VarLenSized(VarLenContext::new(
961            VarLenType::Text,
962            2147483647,
963            Some(Collation::new(13632521, 52)),
964        ));
965        ti.encode(&mut buf).unwrap();
966
967        // table name: one part, us_varchar "dbo"
968        buf.put_u8(1);
969        let part: Vec<u16> = "dbo".encode_utf16().collect();
970        buf.put_u16_le(part.len() as u16);
971        for c in part {
972            buf.put_u16_le(c);
973        }
974
975        // column name (b_varchar)
976        let name: Vec<u16> = "body".encode_utf16().collect();
977        buf.put_u8(name.len() as u8);
978        for c in name {
979            buf.put_u16_le(c);
980        }
981
982        let decoded = TokenColMetaData::decode(&mut buf.into_sql_read_bytes())
983            .await
984            .unwrap();
985
986        assert_eq!(decoded.columns.len(), 1);
987        assert_eq!(decoded.columns[0].col_name, "body");
988    }
989
990    #[test]
991    fn display_formats_various_types() {
992        let cases = vec![
993            (TypeInfo::FixedLen(FixedLenType::Int4), "c int"),
994            (TypeInfo::FixedLen(FixedLenType::Bit), "c bit"),
995            (TypeInfo::FixedLen(FixedLenType::Float8), "c float"),
996            (
997                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Intn, 1, None)),
998                "c tinyint",
999            ),
1000            (
1001                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Intn, 4, None)),
1002                "c int",
1003            ),
1004            (
1005                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Floatn, 4, None)),
1006                "c real",
1007            ),
1008            (
1009                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Guid, 16, None)),
1010                "c uniqueidentifier",
1011            ),
1012            (
1013                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::BigVarBin, 100, None)),
1014                "c varbinary(100)",
1015            ),
1016            (
1017                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::BigVarBin, 100000, None)),
1018                "c varbinary(max)",
1019            ),
1020            (
1021                TypeInfo::VarLenSizedPrecision {
1022                    ty: VarLenType::Decimaln,
1023                    size: 17,
1024                    precision: 18,
1025                    scale: 2,
1026                },
1027                "c decimal(18,2)",
1028            ),
1029            // Numericn must render as `numeric(...)`, distinct from the
1030            // `decimal(...)` fallback that every other precision type uses.
1031            (
1032                TypeInfo::VarLenSizedPrecision {
1033                    ty: VarLenType::Numericn,
1034                    size: 9,
1035                    precision: 10,
1036                    scale: 4,
1037                },
1038                "c numeric(10,4)",
1039            ),
1040            (
1041                TypeInfo::Xml {
1042                    schema: None,
1043                    size: 0,
1044                },
1045                "c xml",
1046            ),
1047        ];
1048
1049        for (ty, expected) in cases {
1050            // Display brackets the column name for use in bulk `INSERT` statements.
1051            let expected = expected.replacen("c ", "[c] ", 1);
1052            assert_eq!(format!("{}", meta(ty, "c")), expected);
1053        }
1054    }
1055
1056    #[test]
1057    fn null_value_maps_types() {
1058        let fixed = BaseMetaDataColumn {
1059            flags: BitFlags::empty(),
1060            ty: TypeInfo::FixedLen(FixedLenType::Int4),
1061            table_name: None,
1062        };
1063        assert_eq!(fixed.null_value(), ColumnData::I32(None));
1064
1065        let varlen = BaseMetaDataColumn {
1066            flags: BitFlags::empty(),
1067            ty: TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Intn, 2, None)),
1068            table_name: None,
1069        };
1070        assert_eq!(varlen.null_value(), ColumnData::I16(None));
1071
1072        // Each Intn width maps to a distinct integer column; 1 and 4 sit either
1073        // side of the `_ => I64` fallback and pin their own arms.
1074        let tinyint = BaseMetaDataColumn {
1075            flags: BitFlags::empty(),
1076            ty: TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Intn, 1, None)),
1077            table_name: None,
1078        };
1079        assert_eq!(tinyint.null_value(), ColumnData::U8(None));
1080
1081        let int4 = BaseMetaDataColumn {
1082            flags: BitFlags::empty(),
1083            ty: TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Intn, 4, None)),
1084            table_name: None,
1085        };
1086        assert_eq!(int4.null_value(), ColumnData::I32(None));
1087
1088        let int8 = BaseMetaDataColumn {
1089            flags: BitFlags::empty(),
1090            ty: TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Intn, 8, None)),
1091            table_name: None,
1092        };
1093        assert_eq!(int8.null_value(), ColumnData::I64(None));
1094
1095        // Floatn splits on width too: 4 bytes is F32, anything else F64.
1096        let real = BaseMetaDataColumn {
1097            flags: BitFlags::empty(),
1098            ty: TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Floatn, 4, None)),
1099            table_name: None,
1100        };
1101        assert_eq!(real.null_value(), ColumnData::F32(None));
1102
1103        let double = BaseMetaDataColumn {
1104            flags: BitFlags::empty(),
1105            ty: TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Floatn, 8, None)),
1106            table_name: None,
1107        };
1108        assert_eq!(double.null_value(), ColumnData::F64(None));
1109
1110        let guid = BaseMetaDataColumn {
1111            flags: BitFlags::empty(),
1112            ty: TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Guid, 16, None)),
1113            table_name: None,
1114        };
1115        assert_eq!(guid.null_value(), ColumnData::Guid(None));
1116    }
1117
1118    #[test]
1119    fn null_value_maps_precision_and_xml_and_udt() {
1120        let precision = BaseMetaDataColumn {
1121            flags: BitFlags::empty(),
1122            ty: TypeInfo::VarLenSizedPrecision {
1123                ty: VarLenType::Numericn,
1124                size: 17,
1125                precision: 18,
1126                scale: 2,
1127            },
1128            table_name: None,
1129        };
1130        assert_eq!(precision.null_value(), ColumnData::Numeric(None));
1131
1132        let xml = BaseMetaDataColumn {
1133            flags: BitFlags::empty(),
1134            ty: TypeInfo::Xml {
1135                schema: None,
1136                size: 0,
1137            },
1138            table_name: None,
1139        };
1140        assert_eq!(xml.null_value(), ColumnData::Xml(None));
1141
1142        let udt = BaseMetaDataColumn {
1143            flags: BitFlags::empty(),
1144            ty: TypeInfo::Udt(crate::tds::codec::type_info::UdtInfo {
1145                max_byte_size: 0,
1146                db_name: "db".into(),
1147                schema_name: "dbo".into(),
1148                type_name: "T".into(),
1149                assembly_qualified_name: "A".into(),
1150            }),
1151            table_name: None,
1152        };
1153        assert_eq!(udt.null_value(), ColumnData::Binary(None));
1154    }
1155
1156    #[test]
1157    fn base_meta_data_column_flag_accessors() {
1158        let base = BaseMetaDataColumn {
1159            flags: ColumnFlag::Nullable | ColumnFlag::Identity | ColumnFlag::Updateable,
1160            ty: TypeInfo::FixedLen(FixedLenType::Int4),
1161            table_name: None,
1162        };
1163
1164        assert!(base.is_nullable());
1165        assert!(base.is_identity());
1166        assert!(base.is_updateable());
1167        assert_eq!(base.ty(), &TypeInfo::FixedLen(FixedLenType::Int4));
1168        assert_eq!(base.flags(), base.flags);
1169
1170        let base2 = BaseMetaDataColumn {
1171            flags: BitFlags::empty(),
1172            ty: TypeInfo::FixedLen(FixedLenType::Int4),
1173            table_name: None,
1174        };
1175        assert!(!base2.is_nullable());
1176        assert!(!base2.is_identity());
1177        assert!(!base2.is_updateable());
1178    }
1179
1180    #[test]
1181    fn meta_data_column_accessors() {
1182        let m = meta(TypeInfo::FixedLen(FixedLenType::Int4), "id");
1183        assert_eq!(m.col_name(), "id");
1184        assert!(m.base().is_nullable());
1185    }
1186
1187    #[test]
1188    fn display_formats_more_types() {
1189        let cases = vec![
1190            (
1191                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Bitn, 1, None)),
1192                "c bit",
1193            ),
1194            (
1195                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Datetimen, 8, None)),
1196                "c datetime",
1197            ),
1198            (
1199                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Money, 4, None)),
1200                "c smallmoney",
1201            ),
1202            (
1203                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Money, 8, None)),
1204                "c money",
1205            ),
1206            (
1207                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::BigVarChar, 100, None)),
1208                "c varchar(100)",
1209            ),
1210            (
1211                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::BigVarChar, 100000, None)),
1212                "c varchar(max)",
1213            ),
1214            (
1215                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::BigBinary, 10, None)),
1216                "c binary(10)",
1217            ),
1218            (
1219                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::BigChar, 10, None)),
1220                "c char(10)",
1221            ),
1222            (
1223                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::NVarchar, 100, None)),
1224                "c nvarchar(100)",
1225            ),
1226            (
1227                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::NVarchar, 100000, None)),
1228                "c nvarchar(max)",
1229            ),
1230            (
1231                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::NChar, 10, None)),
1232                "c nchar(10)",
1233            ),
1234            (
1235                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Text, 0, None)),
1236                "c text",
1237            ),
1238            (
1239                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Image, 0, None)),
1240                "c image",
1241            ),
1242            (
1243                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::NText, 0, None)),
1244                "c ntext",
1245            ),
1246            (
1247                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Intn, 2, None)),
1248                "c smallint",
1249            ),
1250            (
1251                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Intn, 8, None)),
1252                "c bigint",
1253            ),
1254            (
1255                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Floatn, 8, None)),
1256                "c float",
1257            ),
1258            (
1259                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::SSVariant, 0, None)),
1260                "c sql_variant",
1261            ),
1262        ];
1263
1264        for (ty, expected) in cases {
1265            let expected = expected.replacen("c ", "[c] ", 1);
1266            assert_eq!(format!("{}", meta(ty, "c")), expected);
1267        }
1268    }
1269
1270    #[test]
1271    fn display_formats_udt_and_decimaln() {
1272        let udt = TypeInfo::Udt(crate::tds::codec::type_info::UdtInfo {
1273            max_byte_size: 0,
1274            db_name: "db".into(),
1275            schema_name: "dbo".into(),
1276            type_name: "MyType".into(),
1277            assembly_qualified_name: "asm".into(),
1278        });
1279        assert_eq!(format!("{}", meta(udt, "c")), "[c] dbo.MyType");
1280
1281        let decimaln = TypeInfo::VarLenSizedPrecision {
1282            ty: VarLenType::Decimaln,
1283            size: 17,
1284            precision: 10,
1285            scale: 4,
1286        };
1287        assert_eq!(format!("{}", meta(decimaln, "c")), "[c] decimal(10,4)");
1288    }
1289
1290    #[tokio::test]
1291    async fn decode_all_ones_column_count_yields_empty() {
1292        // column_count == 0xffff is treated as "no columns" (guards against a
1293        // sentinel/placeholder value rather than a real column list).
1294        let mut buf = BytesMut::new();
1295        buf.put_u16_le(0xffff);
1296
1297        let decoded = TokenColMetaData::decode(&mut buf.into_sql_read_bytes())
1298            .await
1299            .unwrap();
1300        assert!(decoded.columns.is_empty());
1301    }
1302
1303    #[test]
1304    fn display_formats_fixed_len_money_and_datetime_types() {
1305        // Covers the FixedLenType Display arms not exercised elsewhere:
1306        // tinyint/smallint/smalldatetime/real/money/datetime/smallmoney/bigint
1307        // and the `Null` sentinel (which surfaces as `int`).
1308        let cases = vec![
1309            (TypeInfo::FixedLen(FixedLenType::Int1), "c tinyint"),
1310            (TypeInfo::FixedLen(FixedLenType::Int2), "c smallint"),
1311            (
1312                TypeInfo::FixedLen(FixedLenType::Datetime4),
1313                "c smalldatetime",
1314            ),
1315            (TypeInfo::FixedLen(FixedLenType::Float4), "c real"),
1316            (TypeInfo::FixedLen(FixedLenType::Money), "c money"),
1317            (TypeInfo::FixedLen(FixedLenType::Datetime), "c datetime"),
1318            (TypeInfo::FixedLen(FixedLenType::Money4), "c smallmoney"),
1319            (TypeInfo::FixedLen(FixedLenType::Int8), "c bigint"),
1320            (TypeInfo::FixedLen(FixedLenType::Null), "c int"),
1321        ];
1322
1323        for (ty, expected) in cases {
1324            let expected = expected.replacen("c ", "[c] ", 1);
1325            assert_eq!(format!("{}", meta(ty, "c")), expected);
1326        }
1327    }
1328
1329    #[cfg(feature = "tds73")]
1330    #[test]
1331    fn display_formats_tds73_date_time_types() {
1332        // date/time/datetime2/datetimeoffset Display arms (tds73-only).
1333        let cases = vec![
1334            (
1335                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Daten, 3, None)),
1336                "c date",
1337            ),
1338            (
1339                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Timen, 7, None)),
1340                "c time",
1341            ),
1342            (
1343                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Datetime2, 7, None)),
1344                "c datetime2(7)",
1345            ),
1346            (
1347                TypeInfo::VarLenSized(VarLenContext::new(VarLenType::DatetimeOffsetn, 7, None)),
1348                "c datetimeoffset",
1349            ),
1350        ];
1351
1352        for (ty, expected) in cases {
1353            let expected = expected.replacen("c ", "[c] ", 1);
1354            assert_eq!(format!("{}", meta(ty, "c")), expected);
1355        }
1356    }
1357
1358    #[test]
1359    fn null_value_all_fixed_len() {
1360        use FixedLenType::*;
1361        let cases = [
1362            (Null, ColumnData::I32(None)),
1363            (Int1, ColumnData::U8(None)),
1364            (Bit, ColumnData::Bit(None)),
1365            (Int2, ColumnData::I16(None)),
1366            (Int4, ColumnData::I32(None)),
1367            (Datetime4, ColumnData::SmallDateTime(None)),
1368            (Float4, ColumnData::F32(None)),
1369            (Money, ColumnData::F64(None)),
1370            (Datetime, ColumnData::DateTime(None)),
1371            (Float8, ColumnData::F64(None)),
1372            (Money4, ColumnData::F32(None)),
1373            (Int8, ColumnData::I64(None)),
1374        ];
1375
1376        for (ty, expected) in cases {
1377            let base = BaseMetaDataColumn {
1378                flags: BitFlags::empty(),
1379                ty: TypeInfo::FixedLen(ty),
1380                table_name: None,
1381            };
1382            assert_eq!(base.null_value(), expected);
1383        }
1384    }
1385
1386    fn vsize_null(ty: VarLenType, len: usize) -> ColumnData<'static> {
1387        BaseMetaDataColumn {
1388            flags: BitFlags::empty(),
1389            ty: TypeInfo::VarLenSized(VarLenContext::new(ty, len, None)),
1390            table_name: None,
1391        }
1392        .null_value()
1393    }
1394
1395    #[test]
1396    fn null_value_all_var_len_sized() {
1397        use VarLenType::*;
1398        assert_eq!(vsize_null(Guid, 16), ColumnData::Guid(None));
1399        assert_eq!(vsize_null(Bitn, 1), ColumnData::Bit(None));
1400        assert_eq!(vsize_null(Decimaln, 17), ColumnData::Numeric(None));
1401        assert_eq!(vsize_null(Numericn, 17), ColumnData::Numeric(None));
1402        assert_eq!(vsize_null(Money, 8), ColumnData::F64(None));
1403        assert_eq!(vsize_null(Datetimen, 8), ColumnData::DateTime(None));
1404        assert_eq!(vsize_null(BigVarBin, 100), ColumnData::Binary(None));
1405        assert_eq!(vsize_null(BigVarChar, 100), ColumnData::String(None));
1406        assert_eq!(vsize_null(BigBinary, 10), ColumnData::Binary(None));
1407        assert_eq!(vsize_null(BigChar, 10), ColumnData::String(None));
1408        assert_eq!(vsize_null(NVarchar, 100), ColumnData::String(None));
1409        assert_eq!(vsize_null(NChar, 10), ColumnData::String(None));
1410        assert_eq!(vsize_null(Xml, 0), ColumnData::Xml(None));
1411        assert_eq!(vsize_null(Udt, 0), ColumnData::Binary(None));
1412        assert_eq!(vsize_null(Text, 0), ColumnData::String(None));
1413        assert_eq!(vsize_null(Image, 0), ColumnData::Binary(None));
1414        assert_eq!(vsize_null(NText, 0), ColumnData::String(None));
1415        assert_eq!(vsize_null(SSVariant, 0), ColumnData::String(None));
1416    }
1417
1418    #[cfg(feature = "tds73")]
1419    #[test]
1420    fn null_value_var_len_sized_tds73() {
1421        use VarLenType::*;
1422        assert_eq!(vsize_null(Daten, 3), ColumnData::Date(None));
1423        assert_eq!(vsize_null(Timen, 7), ColumnData::Time(None));
1424        assert_eq!(vsize_null(Datetime2, 7), ColumnData::DateTime2(None));
1425        assert_eq!(
1426            vsize_null(DatetimeOffsetn, 7),
1427            ColumnData::DateTimeOffset(None)
1428        );
1429    }
1430
1431    fn vprec_null(ty: VarLenType) -> ColumnData<'static> {
1432        BaseMetaDataColumn {
1433            flags: BitFlags::empty(),
1434            ty: TypeInfo::VarLenSizedPrecision {
1435                ty,
1436                size: 8,
1437                precision: 18,
1438                scale: 2,
1439            },
1440            table_name: None,
1441        }
1442        .null_value()
1443    }
1444
1445    #[test]
1446    fn null_value_all_var_len_precision() {
1447        use VarLenType::*;
1448        assert_eq!(vprec_null(Guid), ColumnData::Guid(None));
1449        assert_eq!(vprec_null(Intn), ColumnData::I32(None));
1450        assert_eq!(vprec_null(Bitn), ColumnData::Bit(None));
1451        assert_eq!(vprec_null(Decimaln), ColumnData::Numeric(None));
1452        assert_eq!(vprec_null(Numericn), ColumnData::Numeric(None));
1453        assert_eq!(vprec_null(Floatn), ColumnData::F32(None));
1454        assert_eq!(vprec_null(Money), ColumnData::F64(None));
1455        assert_eq!(vprec_null(Datetimen), ColumnData::DateTime(None));
1456        assert_eq!(vprec_null(BigVarBin), ColumnData::Binary(None));
1457        assert_eq!(vprec_null(BigVarChar), ColumnData::String(None));
1458        assert_eq!(vprec_null(BigBinary), ColumnData::Binary(None));
1459        assert_eq!(vprec_null(BigChar), ColumnData::String(None));
1460        assert_eq!(vprec_null(NVarchar), ColumnData::String(None));
1461        assert_eq!(vprec_null(NChar), ColumnData::String(None));
1462        assert_eq!(vprec_null(Xml), ColumnData::Xml(None));
1463        assert_eq!(vprec_null(Udt), ColumnData::Binary(None));
1464        assert_eq!(vprec_null(Text), ColumnData::String(None));
1465        assert_eq!(vprec_null(Image), ColumnData::Binary(None));
1466        assert_eq!(vprec_null(NText), ColumnData::String(None));
1467        assert_eq!(vprec_null(SSVariant), ColumnData::String(None));
1468    }
1469
1470    #[cfg(feature = "tds73")]
1471    #[test]
1472    fn null_value_var_len_precision_tds73() {
1473        use VarLenType::*;
1474        assert_eq!(vprec_null(Daten), ColumnData::Date(None));
1475        assert_eq!(vprec_null(Timen), ColumnData::Time(None));
1476        assert_eq!(vprec_null(Datetime2), ColumnData::DateTime2(None));
1477        assert_eq!(
1478            vprec_null(DatetimeOffsetn),
1479            ColumnData::DateTimeOffset(None)
1480        );
1481    }
1482
1483    #[test]
1484    fn column_flag_bits_are_distinct() {
1485        let all = ColumnFlag::Nullable
1486            | ColumnFlag::CaseSensitive
1487            | ColumnFlag::Updateable
1488            | ColumnFlag::UpdateableUnknown
1489            | ColumnFlag::Identity
1490            | ColumnFlag::Computed
1491            | ColumnFlag::FixedLenClrType
1492            | ColumnFlag::SparseColumnSet
1493            | ColumnFlag::Encrypted
1494            | ColumnFlag::Hidden
1495            | ColumnFlag::Key
1496            | ColumnFlag::NullableUnknown;
1497
1498        assert!(all.contains(ColumnFlag::Nullable));
1499        assert!(all.contains(ColumnFlag::NullableUnknown));
1500        assert_eq!(BitFlags::bits(all).count_ones(), 12);
1501    }
1502}