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