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}