Skip to main content

tds_protocol/
tvp.rs

1//! Table-Valued Parameter (TVP) wire format encoding.
2//!
3//! This module provides TDS protocol-level encoding for Table-Valued Parameters.
4//! TVPs allow passing collections of structured data to SQL Server stored procedures.
5//!
6//! ## Wire Format
7//!
8//! TVPs are encoded as type `0xF3` with this structure:
9//!
10//! ```text
11//! TVP_TYPE_INFO = TVPTYPE TVP_TYPENAME TVP_COLMETADATA TVP_END_TOKEN *TVP_ROW TVP_END_TOKEN
12//!
13//! TVPTYPE = %xF3
14//! TVP_TYPENAME = DbName OwningSchema TypeName (all B_VARCHAR)
15//! TVP_COLMETADATA = TVP_NULL_TOKEN / (Count TvpColumnMetaData*)
16//! TVP_NULL_TOKEN = %xFFFF
17//! TvpColumnMetaData = UserType Flags TYPE_INFO ColName
18//! TVP_ROW = TVP_ROW_TOKEN AllColumnData
19//! TVP_ROW_TOKEN = %x01
20//! TVP_END_TOKEN = %x00
21//! ```
22//!
23//! ## Important Constraints
24//!
25//! - `DbName` MUST be a zero-length string (empty)
26//! - `ColName` MUST be a zero-length string in each column definition
27//! - TVPs can only be used as input parameters (not output)
28//! - Requires TDS 7.3 or later
29//!
30//! ## References
31//!
32//! - [MS-TDS 2.2.6.9](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/c264db71-c1ec-4fe8-b5ef-19d54b1e6566)
33
34use bytes::{BufMut, BytesMut};
35
36use crate::codec::write_utf16_string;
37use crate::prelude::*;
38
39/// TVP type identifier in TDS.
40pub const TVP_TYPE_ID: u8 = 0xF3;
41
42/// Token indicating end of TVP metadata or rows.
43pub const TVP_END_TOKEN: u8 = 0x00;
44
45/// Token indicating a TVP row follows.
46pub const TVP_ROW_TOKEN: u8 = 0x01;
47
48/// Token indicating no columns (NULL TVP metadata).
49pub const TVP_NULL_TOKEN: u16 = 0xFFFF;
50
51/// Default collation for string types in TVPs.
52///
53/// This is Latin1_General_CI_AS equivalent.
54pub const DEFAULT_COLLATION: [u8; 5] = [0x09, 0x04, 0xD0, 0x00, 0x34];
55
56/// TVP column type for wire encoding.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58#[non_exhaustive]
59pub enum TvpWireType {
60    /// BIT type.
61    Bit,
62    /// Integer type with size (1, 2, 4, or 8 bytes).
63    Int {
64        /// Size in bytes.
65        size: u8,
66    },
67    /// Floating point type with size (4 or 8 bytes).
68    Float {
69        /// Size in bytes.
70        size: u8,
71    },
72    /// Decimal/Numeric type.
73    Decimal {
74        /// Maximum number of digits.
75        precision: u8,
76        /// Number of digits after decimal point.
77        scale: u8,
78    },
79    /// Unicode string (NVARCHAR).
80    NVarChar {
81        /// Maximum length in bytes. Use 0xFFFF for MAX.
82        max_length: u16,
83    },
84    /// ASCII string (VARCHAR).
85    VarChar {
86        /// Maximum length in bytes. Use 0xFFFF for MAX.
87        max_length: u16,
88    },
89    /// Binary data (VARBINARY).
90    VarBinary {
91        /// Maximum length in bytes. Use 0xFFFF for MAX.
92        max_length: u16,
93    },
94    /// UNIQUEIDENTIFIER (UUID).
95    Guid,
96    /// DATE type.
97    Date,
98    /// TIME type with scale.
99    Time {
100        /// Fractional seconds precision (0-7).
101        scale: u8,
102    },
103    /// DATETIME2 type with scale.
104    DateTime2 {
105        /// Fractional seconds precision (0-7).
106        scale: u8,
107    },
108    /// DATETIMEOFFSET type with scale.
109    DateTimeOffset {
110        /// Fractional seconds precision (0-7).
111        scale: u8,
112    },
113    /// MONEY type (8-byte scaled integer, scale 4 implicit, via MONEYN / 0x6E).
114    Money,
115    /// SMALLMONEY type (4-byte scaled integer, scale 4 implicit, via MONEYN / 0x6E).
116    SmallMoney,
117    /// Legacy DATETIME type (8 bytes: days since 1900 + 1/300s ticks, via DATETIMEN / 0x6F).
118    DateTime,
119    /// SMALLDATETIME type (4 bytes: days since 1900 + minutes, via DATETIMEN / 0x6F).
120    SmallDateTime,
121    /// XML type.
122    Xml,
123}
124
125impl TvpWireType {
126    /// Get the TDS type ID.
127    #[must_use]
128    pub const fn type_id(&self) -> u8 {
129        match self {
130            Self::Bit => 0x68,                            // BITNTYPE
131            Self::Int { .. } => 0x26,                     // INTNTYPE
132            Self::Float { .. } => 0x6D,                   // FLTNTYPE
133            Self::Decimal { .. } => 0x6C,                 // DECIMALNTYPE
134            Self::NVarChar { .. } => 0xE7,                // NVARCHARTYPE
135            Self::VarChar { .. } => 0xA7,                 // BIGVARCHARTYPE
136            Self::VarBinary { .. } => 0xA5,               // BIGVARBINTYPE
137            Self::Guid => 0x24,                           // GUIDTYPE
138            Self::Date => 0x28,                           // DATETYPE
139            Self::Time { .. } => 0x29,                    // TIMETYPE
140            Self::DateTime2 { .. } => 0x2A,               // DATETIME2TYPE
141            Self::DateTimeOffset { .. } => 0x2B,          // DATETIMEOFFSETTYPE
142            Self::Money | Self::SmallMoney => 0x6E,       // MONEYNTYPE
143            Self::DateTime | Self::SmallDateTime => 0x6F, // DATETIMNTYPE
144            Self::Xml => 0xF1,                            // XMLTYPE
145        }
146    }
147
148    /// Encode the TYPE_INFO for this column type.
149    pub fn encode_type_info(&self, buf: &mut BytesMut) {
150        buf.put_u8(self.type_id());
151
152        match self {
153            Self::Bit => {
154                buf.put_u8(1); // Max length
155            }
156            Self::Int { size } | Self::Float { size } => {
157                buf.put_u8(*size);
158            }
159            Self::Decimal { precision, scale } => {
160                buf.put_u8(17); // Max length for decimal
161                buf.put_u8(*precision);
162                buf.put_u8(*scale);
163            }
164            Self::NVarChar { max_length } => {
165                buf.put_u16_le(*max_length);
166                buf.put_slice(&DEFAULT_COLLATION);
167            }
168            Self::VarChar { max_length } => {
169                buf.put_u16_le(*max_length);
170                buf.put_slice(&DEFAULT_COLLATION);
171            }
172            Self::VarBinary { max_length } => {
173                buf.put_u16_le(*max_length);
174            }
175            Self::Guid => {
176                buf.put_u8(16); // Fixed 16 bytes
177            }
178            Self::Date => {
179                // No additional info needed
180            }
181            Self::Time { scale } | Self::DateTime2 { scale } | Self::DateTimeOffset { scale } => {
182                buf.put_u8(*scale);
183            }
184            Self::Money | Self::DateTime => {
185                buf.put_u8(8); // Fixed 8 bytes
186            }
187            Self::SmallMoney | Self::SmallDateTime => {
188                buf.put_u8(4); // Fixed 4 bytes
189            }
190            Self::Xml => {
191                // XML schema info - we use no schema
192                buf.put_u8(0); // No schema collection
193            }
194        }
195    }
196}
197
198/// Column flags for TVP columns.
199#[derive(Debug, Clone, Copy, Default)]
200#[non_exhaustive]
201pub struct TvpColumnFlags {
202    /// Column is nullable.
203    pub nullable: bool,
204}
205
206impl TvpColumnFlags {
207    /// Create a new set of column flags.
208    #[must_use]
209    pub const fn new(nullable: bool) -> Self {
210        Self { nullable }
211    }
212
213    /// Encode flags to 2-byte value.
214    #[must_use]
215    pub const fn to_bits(&self) -> u16 {
216        let mut flags = 0u16;
217        if self.nullable {
218            flags |= 0x0001;
219        }
220        flags
221    }
222}
223
224/// TVP column definition for wire encoding.
225#[derive(Debug, Clone)]
226#[non_exhaustive]
227pub struct TvpColumnDef {
228    /// Column type.
229    pub wire_type: TvpWireType,
230    /// Column flags.
231    pub flags: TvpColumnFlags,
232}
233
234impl TvpColumnDef {
235    /// Create a new TVP column definition.
236    #[must_use]
237    pub const fn new(wire_type: TvpWireType) -> Self {
238        Self {
239            wire_type,
240            flags: TvpColumnFlags { nullable: false },
241        }
242    }
243
244    /// Create a nullable TVP column definition.
245    #[must_use]
246    pub const fn nullable(wire_type: TvpWireType) -> Self {
247        Self {
248            wire_type,
249            flags: TvpColumnFlags { nullable: true },
250        }
251    }
252
253    /// Encode the column metadata.
254    ///
255    /// Format: UserType (4) + Flags (2) + TYPE_INFO + ColName (B_VARCHAR, must be empty)
256    pub fn encode(&self, buf: &mut BytesMut) {
257        // UserType (always 0 for TVP columns)
258        buf.put_u32_le(0);
259
260        // Flags
261        buf.put_u16_le(self.flags.to_bits());
262
263        // TYPE_INFO
264        self.wire_type.encode_type_info(buf);
265
266        // ColName - MUST be zero-length per MS-TDS spec
267        buf.put_u8(0);
268    }
269}
270
271/// TVP value encoder.
272///
273/// This provides the complete TVP encoding logic for RPC parameters.
274#[derive(Debug)]
275pub struct TvpEncoder<'a> {
276    /// Database schema (e.g., "dbo"). Empty for default.
277    pub schema: &'a str,
278    /// Type name as defined in the database.
279    pub type_name: &'a str,
280    /// Column definitions.
281    pub columns: &'a [TvpColumnDef],
282}
283
284impl<'a> TvpEncoder<'a> {
285    /// Create a new TVP encoder.
286    #[must_use]
287    pub const fn new(schema: &'a str, type_name: &'a str, columns: &'a [TvpColumnDef]) -> Self {
288        Self {
289            schema,
290            type_name,
291            columns,
292        }
293    }
294
295    /// Encode the complete TVP type info and metadata.
296    ///
297    /// This encodes:
298    /// - TVP type ID (0xF3)
299    /// - TVP_TYPENAME (DbName, OwningSchema, TypeName)
300    /// - TVP_COLMETADATA
301    /// - TVP_END_TOKEN (marks end of column metadata)
302    ///
303    /// After calling this, use [`Self::encode_row`] for each row, then
304    /// [`Self::encode_end`] to finish.
305    pub fn encode_metadata(&self, buf: &mut BytesMut) {
306        // TVP type ID
307        buf.put_u8(TVP_TYPE_ID);
308
309        // TVP_TYPENAME
310        // DbName - MUST be empty per MS-TDS spec
311        buf.put_u8(0);
312
313        // OwningSchema (B_VARCHAR)
314        let schema_len = self.schema.encode_utf16().count() as u8;
315        buf.put_u8(schema_len);
316        if schema_len > 0 {
317            write_utf16_string(buf, self.schema);
318        }
319
320        // TypeName (B_VARCHAR)
321        let type_len = self.type_name.encode_utf16().count() as u8;
322        buf.put_u8(type_len);
323        if type_len > 0 {
324            write_utf16_string(buf, self.type_name);
325        }
326
327        // TVP_COLMETADATA
328        if self.columns.is_empty() {
329            // No columns - use null token
330            buf.put_u16_le(TVP_NULL_TOKEN);
331        } else {
332            // Column count (2 bytes)
333            buf.put_u16_le(self.columns.len() as u16);
334
335            // Encode each column
336            for col in self.columns {
337                col.encode(buf);
338            }
339        }
340
341        // Optional: TVP_ORDER_UNIQUE and TVP_COLUMN_ORDERING could go here
342        // We don't use them for now
343
344        // TVP_END_TOKEN marks end of metadata
345        buf.put_u8(TVP_END_TOKEN);
346    }
347
348    /// Encode a TVP row.
349    ///
350    /// # Arguments
351    ///
352    /// * `encode_values` - A closure that encodes the column values into the buffer.
353    ///   Each value should be encoded according to its type (similar to RPC param encoding).
354    pub fn encode_row<F>(&self, buf: &mut BytesMut, encode_values: F)
355    where
356        F: FnOnce(&mut BytesMut),
357    {
358        // TVP_ROW_TOKEN
359        buf.put_u8(TVP_ROW_TOKEN);
360
361        // AllColumnData - caller provides the value encoding
362        encode_values(buf);
363    }
364
365    /// Encode the TVP end marker.
366    ///
367    /// This must be called after all rows have been encoded.
368    pub fn encode_end(&self, buf: &mut BytesMut) {
369        buf.put_u8(TVP_END_TOKEN);
370    }
371}
372
373/// Encode a NULL value for a TVP column.
374///
375/// Different types use different NULL indicators.
376pub fn encode_tvp_null(wire_type: &TvpWireType, buf: &mut BytesMut) {
377    match wire_type {
378        TvpWireType::NVarChar { max_length } | TvpWireType::VarChar { max_length } => {
379            if *max_length == 0xFFFF {
380                // MAX type uses PLP NULL
381                buf.put_u64_le(0xFFFFFFFFFFFFFFFF);
382            } else {
383                // Regular type uses 0xFFFF
384                buf.put_u16_le(0xFFFF);
385            }
386        }
387        TvpWireType::VarBinary { max_length } => {
388            if *max_length == 0xFFFF {
389                buf.put_u64_le(0xFFFFFFFFFFFFFFFF);
390            } else {
391                buf.put_u16_le(0xFFFF);
392            }
393        }
394        TvpWireType::Xml => {
395            // XML uses PLP NULL
396            buf.put_u64_le(0xFFFFFFFFFFFFFFFF);
397        }
398        _ => {
399            // Most types use 0 length
400            buf.put_u8(0);
401        }
402    }
403}
404
405/// Encode a BIT value for TVP.
406pub fn encode_tvp_bit(value: bool, buf: &mut BytesMut) {
407    buf.put_u8(1); // Length
408    buf.put_u8(if value { 1 } else { 0 });
409}
410
411/// Encode an integer value for TVP.
412///
413/// # Panics
414///
415/// Panics if `size` is not 1, 2, 4, or 8. Callers must use sizes derived
416/// from `TvpWireType::Int { size }` which are always valid.
417pub fn encode_tvp_int(value: i64, size: u8, buf: &mut BytesMut) {
418    buf.put_u8(size); // Length
419    match size {
420        1 => buf.put_i8(value as i8),
421        2 => buf.put_i16_le(value as i16),
422        4 => buf.put_i32_le(value as i32),
423        8 => buf.put_i64_le(value),
424        _ => unreachable!("encode_tvp_int called with invalid size {size}; expected 1, 2, 4, or 8"),
425    }
426}
427
428/// Encode a float value for TVP.
429///
430/// # Panics
431///
432/// Panics if `size` is not 4 or 8. Callers must use sizes derived
433/// from `TvpWireType::Float { size }` which are always valid.
434pub fn encode_tvp_float(value: f64, size: u8, buf: &mut BytesMut) {
435    buf.put_u8(size); // Length
436    match size {
437        4 => buf.put_f32_le(value as f32),
438        8 => buf.put_f64_le(value),
439        _ => unreachable!("encode_tvp_float called with invalid size {size}; expected 4 or 8"),
440    }
441}
442
443/// Encode a NVARCHAR value for TVP.
444pub fn encode_tvp_nvarchar(value: &str, max_length: u16, buf: &mut BytesMut) {
445    let utf16: Vec<u16> = value.encode_utf16().collect();
446    let byte_len = utf16.len() * 2;
447
448    if max_length == 0xFFFF {
449        // MAX type - use PLP format
450        buf.put_u64_le(byte_len as u64); // Total length
451        buf.put_u32_le(byte_len as u32); // Chunk length
452        for code_unit in utf16 {
453            buf.put_u16_le(code_unit);
454        }
455        buf.put_u32_le(0); // Terminator
456    } else {
457        // Regular type
458        buf.put_u16_le(byte_len as u16);
459        for code_unit in utf16 {
460            buf.put_u16_le(code_unit);
461        }
462    }
463}
464
465/// Encode a VARCHAR value for TVP using single-byte codepage encoding.
466///
467/// SQL Server stores VARCHAR data as single-byte characters using the column collation
468/// code page. Passing UTF-16 bytes (as NVARCHAR) into a VARCHAR column corrupts every
469/// character: "abc" would be stored as "a\0b\0c\0".
470///
471/// Encodes using Windows-1252 (Latin1_General_CI_AS), matching [`DEFAULT_COLLATION`]
472/// declared in TVP column metadata.
473pub fn encode_tvp_varchar(value: &str, max_length: u16, buf: &mut BytesMut) {
474    let encoded = crate::collation::encode_str_for_collation(value, None);
475    let byte_len = encoded.len();
476
477    if max_length == 0xFFFF {
478        // MAX type - use PLP format
479        buf.put_u64_le(byte_len as u64); // Total length
480        buf.put_u32_le(byte_len as u32); // Chunk length
481        buf.put_slice(&encoded);
482        buf.put_u32_le(0); // Terminator
483    } else {
484        // Regular type
485        buf.put_u16_le(byte_len as u16);
486        buf.put_slice(&encoded);
487    }
488}
489
490/// Encode a VARBINARY value for TVP.
491pub fn encode_tvp_varbinary(value: &[u8], max_length: u16, buf: &mut BytesMut) {
492    if max_length == 0xFFFF {
493        // MAX type - use PLP format
494        buf.put_u64_le(value.len() as u64);
495        buf.put_u32_le(value.len() as u32);
496        buf.put_slice(value);
497        buf.put_u32_le(0); // Terminator
498    } else {
499        buf.put_u16_le(value.len() as u16);
500        buf.put_slice(value);
501    }
502}
503
504/// Encode a UNIQUEIDENTIFIER value for TVP.
505///
506/// SQL Server uses mixed-endian format for UUIDs.
507pub fn encode_tvp_guid(uuid_bytes: &[u8; 16], buf: &mut BytesMut) {
508    buf.put_u8(16); // Length
509
510    // Mixed-endian: first 3 groups little-endian, last 2 groups big-endian
511    buf.put_u8(uuid_bytes[3]);
512    buf.put_u8(uuid_bytes[2]);
513    buf.put_u8(uuid_bytes[1]);
514    buf.put_u8(uuid_bytes[0]);
515
516    buf.put_u8(uuid_bytes[5]);
517    buf.put_u8(uuid_bytes[4]);
518
519    buf.put_u8(uuid_bytes[7]);
520    buf.put_u8(uuid_bytes[6]);
521
522    buf.put_slice(&uuid_bytes[8..16]);
523}
524
525/// Encode a DATE value for TVP (days since 0001-01-01).
526pub fn encode_tvp_date(days: u32, buf: &mut BytesMut) {
527    // DATE is 3 bytes
528    buf.put_u8((days & 0xFF) as u8);
529    buf.put_u8(((days >> 8) & 0xFF) as u8);
530    buf.put_u8(((days >> 16) & 0xFF) as u8);
531}
532
533/// Encode a TIME value for TVP.
534///
535/// Time is encoded as 100-nanosecond intervals since midnight.
536pub fn encode_tvp_time(intervals: u64, scale: u8, buf: &mut BytesMut) {
537    // Length depends on scale
538    let len = match scale {
539        0..=2 => 3,
540        3..=4 => 4,
541        5..=7 => 5,
542        _ => 5,
543    };
544    buf.put_u8(len);
545
546    for i in 0..len {
547        buf.put_u8((intervals >> (8 * i)) as u8);
548    }
549}
550
551/// Encode a DATETIME2 value for TVP.
552///
553/// DATETIME2 is TIME followed by DATE.
554pub fn encode_tvp_datetime2(time_intervals: u64, days: u32, scale: u8, buf: &mut BytesMut) {
555    // Length depends on scale (time bytes + 3 date bytes)
556    let time_len = match scale {
557        0..=2 => 3,
558        3..=4 => 4,
559        5..=7 => 5,
560        _ => 5,
561    };
562    buf.put_u8(time_len + 3);
563
564    // Time component
565    for i in 0..time_len {
566        buf.put_u8((time_intervals >> (8 * i)) as u8);
567    }
568
569    // Date component
570    buf.put_u8((days & 0xFF) as u8);
571    buf.put_u8(((days >> 8) & 0xFF) as u8);
572    buf.put_u8(((days >> 16) & 0xFF) as u8);
573}
574
575/// Encode a DATETIMEOFFSET value for TVP.
576///
577/// DATETIMEOFFSET is TIME followed by DATE followed by timezone offset.
578///
579/// # Arguments
580///
581/// * `time_intervals` - Time in 100-nanosecond intervals since midnight
582/// * `days` - Days since year 1 (0001-01-01)
583/// * `offset_minutes` - Timezone offset in minutes (e.g., -480 for UTC-8, 330 for UTC+5:30)
584/// * `scale` - Fractional seconds precision (0-7)
585pub fn encode_tvp_datetimeoffset(
586    time_intervals: u64,
587    days: u32,
588    offset_minutes: i16,
589    scale: u8,
590    buf: &mut BytesMut,
591) {
592    // Length depends on scale (time bytes + 3 date bytes + 2 offset bytes)
593    let time_len = match scale {
594        0..=2 => 3,
595        3..=4 => 4,
596        5..=7 => 5,
597        _ => 5,
598    };
599    buf.put_u8(time_len + 3 + 2); // time + date + offset
600
601    // Time component
602    for i in 0..time_len {
603        buf.put_u8((time_intervals >> (8 * i)) as u8);
604    }
605
606    // Date component
607    buf.put_u8((days & 0xFF) as u8);
608    buf.put_u8(((days >> 8) & 0xFF) as u8);
609    buf.put_u8(((days >> 16) & 0xFF) as u8);
610
611    // Timezone offset in minutes (signed 16-bit little-endian)
612    buf.put_i16_le(offset_minutes);
613}
614
615/// Encode a DECIMAL value for TVP.
616///
617/// # Arguments
618///
619/// * `sign` - 0 for negative, 1 for positive
620/// * `mantissa` - The absolute value as a 128-bit integer
621pub fn encode_tvp_decimal(sign: u8, mantissa: u128, buf: &mut BytesMut) {
622    buf.put_u8(17); // Length: 1 byte sign + 16 bytes mantissa
623    buf.put_u8(sign);
624    buf.put_u128_le(mantissa);
625}
626
627/// Encode a MONEY value for TVP (8 bytes).
628///
629/// The MONEY wire format is a 64-bit signed integer scaled by 10_000, written
630/// as the high 32 bits little-endian followed by the low 32 bits little-endian
631/// (MS-TDS §2.2.5.5.1.2). `scaled` is the already-scaled cents value — callers
632/// that hold a `Decimal` should multiply by 10_000 and truncate to `i64` before
633/// calling this (see `mssql_types::encode::encode_money`).
634pub fn encode_tvp_money(scaled: i64, buf: &mut BytesMut) {
635    buf.put_u8(8); // Length
636    let high = (scaled >> 32) as i32;
637    let low = (scaled & 0xFFFF_FFFF) as u32;
638    buf.put_i32_le(high);
639    buf.put_u32_le(low);
640}
641
642/// Encode a SMALLMONEY value for TVP (4 bytes).
643///
644/// `scaled` is the 32-bit signed integer scaled by 10_000, written
645/// little-endian.
646pub fn encode_tvp_smallmoney(scaled: i32, buf: &mut BytesMut) {
647    buf.put_u8(4); // Length
648    buf.put_i32_le(scaled);
649}
650
651/// Encode a legacy DATETIME value for TVP (8 bytes).
652///
653/// DATETIME wire format: days since 1900-01-01 (i32 LE) + time units since
654/// midnight (u32 LE) where each unit is 1/300 of a second.
655pub fn encode_tvp_datetime(days: i32, ticks: u32, buf: &mut BytesMut) {
656    buf.put_u8(8); // Length
657    buf.put_i32_le(days);
658    buf.put_u32_le(ticks);
659}
660
661/// Encode a SMALLDATETIME value for TVP (4 bytes).
662///
663/// SMALLDATETIME wire format: days since 1900-01-01 (u16 LE) + minutes since
664/// midnight (u16 LE). Sub-minute precision is discarded by the caller.
665pub fn encode_tvp_smalldatetime(days: u16, minutes: u16, buf: &mut BytesMut) {
666    buf.put_u8(4); // Length
667    buf.put_u16_le(days);
668    buf.put_u16_le(minutes);
669}
670
671#[cfg(test)]
672#[allow(clippy::unwrap_used, clippy::expect_used)]
673mod tests {
674    use super::*;
675
676    #[test]
677    fn test_tvp_metadata_encoding() {
678        let columns = vec![TvpColumnDef::new(TvpWireType::Int { size: 4 })];
679
680        let encoder = TvpEncoder::new("dbo", "UserIdList", &columns);
681        let mut buf = BytesMut::new();
682
683        encoder.encode_metadata(&mut buf);
684
685        // Should start with TVP type ID
686        assert_eq!(buf[0], TVP_TYPE_ID);
687
688        // DbName should be empty (length 0)
689        assert_eq!(buf[1], 0);
690    }
691
692    #[test]
693    fn test_tvp_column_def_encoding() {
694        let col = TvpColumnDef::nullable(TvpWireType::Int { size: 4 });
695        let mut buf = BytesMut::new();
696
697        col.encode(&mut buf);
698
699        // UserType (4) + Flags (2) + TypeId (1) + MaxLen (1) + ColName (1)
700        assert!(buf.len() >= 9);
701
702        // UserType should be 0
703        assert_eq!(&buf[0..4], &[0, 0, 0, 0]);
704
705        // Flags should have nullable bit set
706        assert_eq!(buf[4], 0x01);
707        assert_eq!(buf[5], 0x00);
708    }
709
710    #[test]
711    fn test_tvp_nvarchar_encoding() {
712        let mut buf = BytesMut::new();
713        encode_tvp_nvarchar("test", 100, &mut buf);
714
715        // Length prefix (2) + UTF-16 data (4 chars * 2 bytes)
716        assert_eq!(buf.len(), 2 + 8);
717        assert_eq!(buf[0], 8); // Byte length
718        assert_eq!(buf[1], 0);
719    }
720
721    #[test]
722    fn test_tvp_int_encoding() {
723        let mut buf = BytesMut::new();
724        encode_tvp_int(42, 4, &mut buf);
725
726        // Length (1) + value (4)
727        assert_eq!(buf.len(), 5);
728        assert_eq!(buf[0], 4);
729        assert_eq!(buf[1], 42);
730    }
731
732    #[test]
733    fn test_tvp_money_encoding_matches_rpc_layout() {
734        // $12.3400 → 123_400 cents (10_000 per unit)
735        let mut buf = BytesMut::new();
736        encode_tvp_money(123_400, &mut buf);
737
738        assert_eq!(buf.len(), 9, "length byte + 8-byte payload");
739        assert_eq!(buf[0], 8, "MONEYN length byte is 8 for MONEY");
740        // High 32 bits LE then low 32 bits LE, per MS-TDS §2.2.5.5.1.2.
741        assert_eq!(&buf[1..5], &[0, 0, 0, 0], "high word zero for small value");
742        assert_eq!(&buf[5..9], &123_400i32.to_le_bytes());
743    }
744
745    #[test]
746    fn test_tvp_money_encoding_negative_value() {
747        // -$1.2300 → -12_300 cents
748        let mut buf = BytesMut::new();
749        encode_tvp_money(-12_300, &mut buf);
750
751        assert_eq!(buf.len(), 9);
752        assert_eq!(buf[0], 8);
753        // Verify the whole 8-byte payload reconstructs to -12_300
754        let high = i32::from_le_bytes(buf[1..5].try_into().unwrap());
755        let low = u32::from_le_bytes(buf[5..9].try_into().unwrap());
756        let reconstructed = ((high as i64) << 32) | (low as i64 & 0xFFFF_FFFF);
757        assert_eq!(reconstructed, -12_300i64);
758    }
759
760    #[test]
761    fn test_tvp_money_encoding_max_value() {
762        // MONEY max: ~922_337_203_685_477.5807 → i64::MAX cents
763        let mut buf = BytesMut::new();
764        encode_tvp_money(i64::MAX, &mut buf);
765
766        assert_eq!(buf.len(), 9);
767        let high = i32::from_le_bytes(buf[1..5].try_into().unwrap());
768        let low = u32::from_le_bytes(buf[5..9].try_into().unwrap());
769        let reconstructed = ((high as i64) << 32) | (low as i64 & 0xFFFF_FFFF);
770        assert_eq!(reconstructed, i64::MAX);
771    }
772
773    #[test]
774    fn test_tvp_smallmoney_encoding() {
775        // $1.2345 → 12_345 cents (10_000 per unit)
776        let mut buf = BytesMut::new();
777        encode_tvp_smallmoney(12_345, &mut buf);
778
779        assert_eq!(buf.len(), 5, "length byte + 4-byte payload");
780        assert_eq!(buf[0], 4);
781        assert_eq!(&buf[1..5], &12_345i32.to_le_bytes());
782    }
783
784    #[test]
785    fn test_tvp_smallmoney_encoding_negative() {
786        let mut buf = BytesMut::new();
787        encode_tvp_smallmoney(-1, &mut buf);
788
789        assert_eq!(buf.len(), 5);
790        assert_eq!(buf[0], 4);
791        assert_eq!(
792            i32::from_le_bytes(buf[1..5].try_into().unwrap()),
793            -1,
794            "SMALLMONEY wraps as signed 32-bit LE"
795        );
796    }
797
798    #[test]
799    fn test_tvp_datetime_encoding() {
800        // 2020-01-01 00:00:00 (41_275 days since 1900-01-01, 0 ticks)
801        let mut buf = BytesMut::new();
802        encode_tvp_datetime(41_275, 0, &mut buf);
803
804        assert_eq!(buf.len(), 9, "length byte + 8-byte payload");
805        assert_eq!(buf[0], 8);
806        assert_eq!(&buf[1..5], &41_275i32.to_le_bytes());
807        assert_eq!(&buf[5..9], &0u32.to_le_bytes());
808    }
809
810    #[test]
811    fn test_tvp_datetime_encoding_pre_1900() {
812        // 1899-12-31 = -1 days since 1900-01-01
813        let mut buf = BytesMut::new();
814        encode_tvp_datetime(-1, 0, &mut buf);
815
816        assert_eq!(buf.len(), 9);
817        assert_eq!(
818            i32::from_le_bytes(buf[1..5].try_into().unwrap()),
819            -1,
820            "pre-1900 DATETIME uses negative days"
821        );
822    }
823
824    #[test]
825    fn test_tvp_smalldatetime_encoding() {
826        // 2020-01-01 00:00:00 = 43_830 days since 1900-01-01, 0 minutes
827        let mut buf = BytesMut::new();
828        encode_tvp_smalldatetime(43_830, 0, &mut buf);
829
830        assert_eq!(buf.len(), 5, "length byte + 4-byte payload");
831        assert_eq!(buf[0], 4);
832        assert_eq!(&buf[1..3], &43_830u16.to_le_bytes());
833        assert_eq!(&buf[3..5], &0u16.to_le_bytes());
834    }
835
836    #[test]
837    fn test_tvp_money_type_info_encoding() {
838        let mut buf = BytesMut::new();
839        TvpWireType::Money.encode_type_info(&mut buf);
840        assert_eq!(
841            &buf[..],
842            &[0x6E, 8],
843            "MONEY = MONEYN type_id with max_length 8"
844        );
845    }
846
847    #[test]
848    fn test_tvp_smallmoney_type_info_encoding() {
849        let mut buf = BytesMut::new();
850        TvpWireType::SmallMoney.encode_type_info(&mut buf);
851        assert_eq!(
852            &buf[..],
853            &[0x6E, 4],
854            "SMALLMONEY = MONEYN type_id with max_length 4"
855        );
856    }
857
858    #[test]
859    fn test_tvp_datetime_type_info_encoding() {
860        let mut buf = BytesMut::new();
861        TvpWireType::DateTime.encode_type_info(&mut buf);
862        assert_eq!(
863            &buf[..],
864            &[0x6F, 8],
865            "DATETIME = DATETIMEN type_id with max_length 8"
866        );
867    }
868
869    #[test]
870    fn test_tvp_smalldatetime_type_info_encoding() {
871        let mut buf = BytesMut::new();
872        TvpWireType::SmallDateTime.encode_type_info(&mut buf);
873        assert_eq!(
874            &buf[..],
875            &[0x6F, 4],
876            "SMALLDATETIME = DATETIMEN type_id with max_length 4"
877        );
878    }
879
880    #[test]
881    fn test_tvp_null_for_money_is_length_zero() {
882        let mut buf = BytesMut::new();
883        encode_tvp_null(&TvpWireType::Money, &mut buf);
884        assert_eq!(&buf[..], &[0], "MONEYN NULL is a single length-zero byte");
885
886        let mut buf = BytesMut::new();
887        encode_tvp_null(&TvpWireType::SmallDateTime, &mut buf);
888        assert_eq!(
889            &buf[..],
890            &[0],
891            "DATETIMEN NULL is a single length-zero byte"
892        );
893    }
894}