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;
37
38/// TVP type identifier in TDS.
39pub const TVP_TYPE_ID: u8 = 0xF3;
40
41/// Token indicating end of TVP metadata or rows.
42pub const TVP_END_TOKEN: u8 = 0x00;
43
44/// Token indicating a TVP row follows.
45pub const TVP_ROW_TOKEN: u8 = 0x01;
46
47/// Token indicating no columns (NULL TVP metadata).
48pub const TVP_NULL_TOKEN: u16 = 0xFFFF;
49
50/// Default collation for string types in TVPs.
51///
52/// This is Latin1_General_CI_AS equivalent.
53pub const DEFAULT_COLLATION: [u8; 5] = [0x09, 0x04, 0xD0, 0x00, 0x34];
54
55/// TVP column type for wire encoding.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum TvpWireType {
58    /// BIT type.
59    Bit,
60    /// Integer type with size (1, 2, 4, or 8 bytes).
61    Int {
62        /// Size in bytes.
63        size: u8,
64    },
65    /// Floating point type with size (4 or 8 bytes).
66    Float {
67        /// Size in bytes.
68        size: u8,
69    },
70    /// Decimal/Numeric type.
71    Decimal {
72        /// Maximum number of digits.
73        precision: u8,
74        /// Number of digits after decimal point.
75        scale: u8,
76    },
77    /// Unicode string (NVARCHAR).
78    NVarChar {
79        /// Maximum length in bytes. Use 0xFFFF for MAX.
80        max_length: u16,
81    },
82    /// ASCII string (VARCHAR).
83    VarChar {
84        /// Maximum length in bytes. Use 0xFFFF for MAX.
85        max_length: u16,
86    },
87    /// Binary data (VARBINARY).
88    VarBinary {
89        /// Maximum length in bytes. Use 0xFFFF for MAX.
90        max_length: u16,
91    },
92    /// UNIQUEIDENTIFIER (UUID).
93    Guid,
94    /// DATE type.
95    Date,
96    /// TIME type with scale.
97    Time {
98        /// Fractional seconds precision (0-7).
99        scale: u8,
100    },
101    /// DATETIME2 type with scale.
102    DateTime2 {
103        /// Fractional seconds precision (0-7).
104        scale: u8,
105    },
106    /// DATETIMEOFFSET type with scale.
107    DateTimeOffset {
108        /// Fractional seconds precision (0-7).
109        scale: u8,
110    },
111    /// XML type.
112    Xml,
113}
114
115impl TvpWireType {
116    /// Get the TDS type ID.
117    #[must_use]
118    pub const fn type_id(&self) -> u8 {
119        match self {
120            Self::Bit => 0x68,                   // BITNTYPE
121            Self::Int { .. } => 0x26,            // INTNTYPE
122            Self::Float { .. } => 0x6D,          // FLTNTYPE
123            Self::Decimal { .. } => 0x6C,        // DECIMALNTYPE
124            Self::NVarChar { .. } => 0xE7,       // NVARCHARTYPE
125            Self::VarChar { .. } => 0xA7,        // BIGVARCHARTYPE
126            Self::VarBinary { .. } => 0xA5,      // BIGVARBINTYPE
127            Self::Guid => 0x24,                  // GUIDTYPE
128            Self::Date => 0x28,                  // DATETYPE
129            Self::Time { .. } => 0x29,           // TIMETYPE
130            Self::DateTime2 { .. } => 0x2A,      // DATETIME2TYPE
131            Self::DateTimeOffset { .. } => 0x2B, // DATETIMEOFFSETTYPE
132            Self::Xml => 0xF1,                   // XMLTYPE
133        }
134    }
135
136    /// Encode the TYPE_INFO for this column type.
137    pub fn encode_type_info(&self, buf: &mut BytesMut) {
138        buf.put_u8(self.type_id());
139
140        match self {
141            Self::Bit => {
142                buf.put_u8(1); // Max length
143            }
144            Self::Int { size } | Self::Float { size } => {
145                buf.put_u8(*size);
146            }
147            Self::Decimal { precision, scale } => {
148                buf.put_u8(17); // Max length for decimal
149                buf.put_u8(*precision);
150                buf.put_u8(*scale);
151            }
152            Self::NVarChar { max_length } => {
153                buf.put_u16_le(*max_length);
154                buf.put_slice(&DEFAULT_COLLATION);
155            }
156            Self::VarChar { max_length } => {
157                buf.put_u16_le(*max_length);
158                buf.put_slice(&DEFAULT_COLLATION);
159            }
160            Self::VarBinary { max_length } => {
161                buf.put_u16_le(*max_length);
162            }
163            Self::Guid => {
164                buf.put_u8(16); // Fixed 16 bytes
165            }
166            Self::Date => {
167                // No additional info needed
168            }
169            Self::Time { scale } | Self::DateTime2 { scale } | Self::DateTimeOffset { scale } => {
170                buf.put_u8(*scale);
171            }
172            Self::Xml => {
173                // XML schema info - we use no schema
174                buf.put_u8(0); // No schema collection
175            }
176        }
177    }
178}
179
180/// Column flags for TVP columns.
181#[derive(Debug, Clone, Copy, Default)]
182pub struct TvpColumnFlags {
183    /// Column is nullable.
184    pub nullable: bool,
185}
186
187impl TvpColumnFlags {
188    /// Encode flags to 2-byte value.
189    #[must_use]
190    pub const fn to_bits(&self) -> u16 {
191        let mut flags = 0u16;
192        if self.nullable {
193            flags |= 0x0001;
194        }
195        flags
196    }
197}
198
199/// TVP column definition for wire encoding.
200#[derive(Debug, Clone)]
201pub struct TvpColumnDef {
202    /// Column type.
203    pub wire_type: TvpWireType,
204    /// Column flags.
205    pub flags: TvpColumnFlags,
206}
207
208impl TvpColumnDef {
209    /// Create a new TVP column definition.
210    #[must_use]
211    pub const fn new(wire_type: TvpWireType) -> Self {
212        Self {
213            wire_type,
214            flags: TvpColumnFlags { nullable: false },
215        }
216    }
217
218    /// Create a nullable TVP column definition.
219    #[must_use]
220    pub const fn nullable(wire_type: TvpWireType) -> Self {
221        Self {
222            wire_type,
223            flags: TvpColumnFlags { nullable: true },
224        }
225    }
226
227    /// Encode the column metadata.
228    ///
229    /// Format: UserType (4) + Flags (2) + TYPE_INFO + ColName (B_VARCHAR, must be empty)
230    pub fn encode(&self, buf: &mut BytesMut) {
231        // UserType (always 0 for TVP columns)
232        buf.put_u32_le(0);
233
234        // Flags
235        buf.put_u16_le(self.flags.to_bits());
236
237        // TYPE_INFO
238        self.wire_type.encode_type_info(buf);
239
240        // ColName - MUST be zero-length per MS-TDS spec
241        buf.put_u8(0);
242    }
243}
244
245/// TVP value encoder.
246///
247/// This provides the complete TVP encoding logic for RPC parameters.
248#[derive(Debug)]
249pub struct TvpEncoder<'a> {
250    /// Database schema (e.g., "dbo"). Empty for default.
251    pub schema: &'a str,
252    /// Type name as defined in the database.
253    pub type_name: &'a str,
254    /// Column definitions.
255    pub columns: &'a [TvpColumnDef],
256}
257
258impl<'a> TvpEncoder<'a> {
259    /// Create a new TVP encoder.
260    #[must_use]
261    pub const fn new(schema: &'a str, type_name: &'a str, columns: &'a [TvpColumnDef]) -> Self {
262        Self {
263            schema,
264            type_name,
265            columns,
266        }
267    }
268
269    /// Encode the complete TVP type info and metadata.
270    ///
271    /// This encodes:
272    /// - TVP type ID (0xF3)
273    /// - TVP_TYPENAME (DbName, OwningSchema, TypeName)
274    /// - TVP_COLMETADATA
275    /// - TVP_END_TOKEN (marks end of column metadata)
276    ///
277    /// After calling this, use [`Self::encode_row`] for each row, then
278    /// [`Self::encode_end`] to finish.
279    pub fn encode_metadata(&self, buf: &mut BytesMut) {
280        // TVP type ID
281        buf.put_u8(TVP_TYPE_ID);
282
283        // TVP_TYPENAME
284        // DbName - MUST be empty per MS-TDS spec
285        buf.put_u8(0);
286
287        // OwningSchema (B_VARCHAR)
288        let schema_len = self.schema.encode_utf16().count() as u8;
289        buf.put_u8(schema_len);
290        if schema_len > 0 {
291            write_utf16_string(buf, self.schema);
292        }
293
294        // TypeName (B_VARCHAR)
295        let type_len = self.type_name.encode_utf16().count() as u8;
296        buf.put_u8(type_len);
297        if type_len > 0 {
298            write_utf16_string(buf, self.type_name);
299        }
300
301        // TVP_COLMETADATA
302        if self.columns.is_empty() {
303            // No columns - use null token
304            buf.put_u16_le(TVP_NULL_TOKEN);
305        } else {
306            // Column count (2 bytes)
307            buf.put_u16_le(self.columns.len() as u16);
308
309            // Encode each column
310            for col in self.columns {
311                col.encode(buf);
312            }
313        }
314
315        // Optional: TVP_ORDER_UNIQUE and TVP_COLUMN_ORDERING could go here
316        // We don't use them for now
317
318        // TVP_END_TOKEN marks end of metadata
319        buf.put_u8(TVP_END_TOKEN);
320    }
321
322    /// Encode a TVP row.
323    ///
324    /// # Arguments
325    ///
326    /// * `encode_values` - A closure that encodes the column values into the buffer.
327    ///   Each value should be encoded according to its type (similar to RPC param encoding).
328    pub fn encode_row<F>(&self, buf: &mut BytesMut, encode_values: F)
329    where
330        F: FnOnce(&mut BytesMut),
331    {
332        // TVP_ROW_TOKEN
333        buf.put_u8(TVP_ROW_TOKEN);
334
335        // AllColumnData - caller provides the value encoding
336        encode_values(buf);
337    }
338
339    /// Encode the TVP end marker.
340    ///
341    /// This must be called after all rows have been encoded.
342    pub fn encode_end(&self, buf: &mut BytesMut) {
343        buf.put_u8(TVP_END_TOKEN);
344    }
345}
346
347/// Encode a NULL value for a TVP column.
348///
349/// Different types use different NULL indicators.
350pub fn encode_tvp_null(wire_type: &TvpWireType, buf: &mut BytesMut) {
351    match wire_type {
352        TvpWireType::NVarChar { max_length } | TvpWireType::VarChar { max_length } => {
353            if *max_length == 0xFFFF {
354                // MAX type uses PLP NULL
355                buf.put_u64_le(0xFFFFFFFFFFFFFFFF);
356            } else {
357                // Regular type uses 0xFFFF
358                buf.put_u16_le(0xFFFF);
359            }
360        }
361        TvpWireType::VarBinary { max_length } => {
362            if *max_length == 0xFFFF {
363                buf.put_u64_le(0xFFFFFFFFFFFFFFFF);
364            } else {
365                buf.put_u16_le(0xFFFF);
366            }
367        }
368        TvpWireType::Xml => {
369            // XML uses PLP NULL
370            buf.put_u64_le(0xFFFFFFFFFFFFFFFF);
371        }
372        _ => {
373            // Most types use 0 length
374            buf.put_u8(0);
375        }
376    }
377}
378
379/// Encode a BIT value for TVP.
380pub fn encode_tvp_bit(value: bool, buf: &mut BytesMut) {
381    buf.put_u8(1); // Length
382    buf.put_u8(if value { 1 } else { 0 });
383}
384
385/// Encode an integer value for TVP.
386pub fn encode_tvp_int(value: i64, size: u8, buf: &mut BytesMut) {
387    buf.put_u8(size); // Length
388    match size {
389        1 => buf.put_i8(value as i8),
390        2 => buf.put_i16_le(value as i16),
391        4 => buf.put_i32_le(value as i32),
392        8 => buf.put_i64_le(value),
393        _ => unreachable!("invalid int size"),
394    }
395}
396
397/// Encode a float value for TVP.
398pub fn encode_tvp_float(value: f64, size: u8, buf: &mut BytesMut) {
399    buf.put_u8(size); // Length
400    match size {
401        4 => buf.put_f32_le(value as f32),
402        8 => buf.put_f64_le(value),
403        _ => unreachable!("invalid float size"),
404    }
405}
406
407/// Encode a NVARCHAR value for TVP.
408pub fn encode_tvp_nvarchar(value: &str, max_length: u16, buf: &mut BytesMut) {
409    let utf16: Vec<u16> = value.encode_utf16().collect();
410    let byte_len = utf16.len() * 2;
411
412    if max_length == 0xFFFF {
413        // MAX type - use PLP format
414        buf.put_u64_le(byte_len as u64); // Total length
415        buf.put_u32_le(byte_len as u32); // Chunk length
416        for code_unit in utf16 {
417            buf.put_u16_le(code_unit);
418        }
419        buf.put_u32_le(0); // Terminator
420    } else {
421        // Regular type
422        buf.put_u16_le(byte_len as u16);
423        for code_unit in utf16 {
424            buf.put_u16_le(code_unit);
425        }
426    }
427}
428
429/// Encode a VARBINARY value for TVP.
430pub fn encode_tvp_varbinary(value: &[u8], max_length: u16, buf: &mut BytesMut) {
431    if max_length == 0xFFFF {
432        // MAX type - use PLP format
433        buf.put_u64_le(value.len() as u64);
434        buf.put_u32_le(value.len() as u32);
435        buf.put_slice(value);
436        buf.put_u32_le(0); // Terminator
437    } else {
438        buf.put_u16_le(value.len() as u16);
439        buf.put_slice(value);
440    }
441}
442
443/// Encode a UNIQUEIDENTIFIER value for TVP.
444///
445/// SQL Server uses mixed-endian format for UUIDs.
446pub fn encode_tvp_guid(uuid_bytes: &[u8; 16], buf: &mut BytesMut) {
447    buf.put_u8(16); // Length
448
449    // Mixed-endian: first 3 groups little-endian, last 2 groups big-endian
450    buf.put_u8(uuid_bytes[3]);
451    buf.put_u8(uuid_bytes[2]);
452    buf.put_u8(uuid_bytes[1]);
453    buf.put_u8(uuid_bytes[0]);
454
455    buf.put_u8(uuid_bytes[5]);
456    buf.put_u8(uuid_bytes[4]);
457
458    buf.put_u8(uuid_bytes[7]);
459    buf.put_u8(uuid_bytes[6]);
460
461    buf.put_slice(&uuid_bytes[8..16]);
462}
463
464/// Encode a DATE value for TVP (days since 0001-01-01).
465pub fn encode_tvp_date(days: u32, buf: &mut BytesMut) {
466    // DATE is 3 bytes
467    buf.put_u8((days & 0xFF) as u8);
468    buf.put_u8(((days >> 8) & 0xFF) as u8);
469    buf.put_u8(((days >> 16) & 0xFF) as u8);
470}
471
472/// Encode a TIME value for TVP.
473///
474/// Time is encoded as 100-nanosecond intervals since midnight.
475pub fn encode_tvp_time(intervals: u64, scale: u8, buf: &mut BytesMut) {
476    // Length depends on scale
477    let len = match scale {
478        0..=2 => 3,
479        3..=4 => 4,
480        5..=7 => 5,
481        _ => 5,
482    };
483    buf.put_u8(len);
484
485    for i in 0..len {
486        buf.put_u8((intervals >> (8 * i)) as u8);
487    }
488}
489
490/// Encode a DATETIME2 value for TVP.
491///
492/// DATETIME2 is TIME followed by DATE.
493pub fn encode_tvp_datetime2(time_intervals: u64, days: u32, scale: u8, buf: &mut BytesMut) {
494    // Length depends on scale (time bytes + 3 date bytes)
495    let time_len = match scale {
496        0..=2 => 3,
497        3..=4 => 4,
498        5..=7 => 5,
499        _ => 5,
500    };
501    buf.put_u8(time_len + 3);
502
503    // Time component
504    for i in 0..time_len {
505        buf.put_u8((time_intervals >> (8 * i)) as u8);
506    }
507
508    // Date component
509    buf.put_u8((days & 0xFF) as u8);
510    buf.put_u8(((days >> 8) & 0xFF) as u8);
511    buf.put_u8(((days >> 16) & 0xFF) as u8);
512}
513
514/// Encode a DATETIMEOFFSET value for TVP.
515///
516/// DATETIMEOFFSET is TIME followed by DATE followed by timezone offset.
517///
518/// # Arguments
519///
520/// * `time_intervals` - Time in 100-nanosecond intervals since midnight
521/// * `days` - Days since year 1 (0001-01-01)
522/// * `offset_minutes` - Timezone offset in minutes (e.g., -480 for UTC-8, 330 for UTC+5:30)
523/// * `scale` - Fractional seconds precision (0-7)
524pub fn encode_tvp_datetimeoffset(
525    time_intervals: u64,
526    days: u32,
527    offset_minutes: i16,
528    scale: u8,
529    buf: &mut BytesMut,
530) {
531    // Length depends on scale (time bytes + 3 date bytes + 2 offset bytes)
532    let time_len = match scale {
533        0..=2 => 3,
534        3..=4 => 4,
535        5..=7 => 5,
536        _ => 5,
537    };
538    buf.put_u8(time_len + 3 + 2); // time + date + offset
539
540    // Time component
541    for i in 0..time_len {
542        buf.put_u8((time_intervals >> (8 * i)) as u8);
543    }
544
545    // Date component
546    buf.put_u8((days & 0xFF) as u8);
547    buf.put_u8(((days >> 8) & 0xFF) as u8);
548    buf.put_u8(((days >> 16) & 0xFF) as u8);
549
550    // Timezone offset in minutes (signed 16-bit little-endian)
551    buf.put_i16_le(offset_minutes);
552}
553
554/// Encode a DECIMAL value for TVP.
555///
556/// # Arguments
557///
558/// * `sign` - 0 for negative, 1 for positive
559/// * `mantissa` - The absolute value as a 128-bit integer
560pub fn encode_tvp_decimal(sign: u8, mantissa: u128, buf: &mut BytesMut) {
561    buf.put_u8(17); // Length: 1 byte sign + 16 bytes mantissa
562    buf.put_u8(sign);
563    buf.put_u128_le(mantissa);
564}
565
566#[cfg(test)]
567#[allow(clippy::unwrap_used, clippy::expect_used)]
568mod tests {
569    use super::*;
570
571    #[test]
572    fn test_tvp_metadata_encoding() {
573        let columns = vec![TvpColumnDef::new(TvpWireType::Int { size: 4 })];
574
575        let encoder = TvpEncoder::new("dbo", "UserIdList", &columns);
576        let mut buf = BytesMut::new();
577
578        encoder.encode_metadata(&mut buf);
579
580        // Should start with TVP type ID
581        assert_eq!(buf[0], TVP_TYPE_ID);
582
583        // DbName should be empty (length 0)
584        assert_eq!(buf[1], 0);
585    }
586
587    #[test]
588    fn test_tvp_column_def_encoding() {
589        let col = TvpColumnDef::nullable(TvpWireType::Int { size: 4 });
590        let mut buf = BytesMut::new();
591
592        col.encode(&mut buf);
593
594        // UserType (4) + Flags (2) + TypeId (1) + MaxLen (1) + ColName (1)
595        assert!(buf.len() >= 9);
596
597        // UserType should be 0
598        assert_eq!(&buf[0..4], &[0, 0, 0, 0]);
599
600        // Flags should have nullable bit set
601        assert_eq!(buf[4], 0x01);
602        assert_eq!(buf[5], 0x00);
603    }
604
605    #[test]
606    fn test_tvp_nvarchar_encoding() {
607        let mut buf = BytesMut::new();
608        encode_tvp_nvarchar("test", 100, &mut buf);
609
610        // Length prefix (2) + UTF-16 data (4 chars * 2 bytes)
611        assert_eq!(buf.len(), 2 + 8);
612        assert_eq!(buf[0], 8); // Byte length
613        assert_eq!(buf[1], 0);
614    }
615
616    #[test]
617    fn test_tvp_int_encoding() {
618        let mut buf = BytesMut::new();
619        encode_tvp_int(42, 4, &mut buf);
620
621        // Length (1) + value (4)
622        assert_eq!(buf.len(), 5);
623        assert_eq!(buf[0], 4);
624        assert_eq!(buf[1], 42);
625    }
626}