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