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.
396pub fn encode_tvp_int(value: i64, size: u8, buf: &mut BytesMut) {
397 buf.put_u8(size); // Length
398 match size {
399 1 => buf.put_i8(value as i8),
400 2 => buf.put_i16_le(value as i16),
401 4 => buf.put_i32_le(value as i32),
402 8 => buf.put_i64_le(value),
403 _ => unreachable!("invalid int size"),
404 }
405}
406
407/// Encode a float value for TVP.
408pub fn encode_tvp_float(value: f64, size: u8, buf: &mut BytesMut) {
409 buf.put_u8(size); // Length
410 match size {
411 4 => buf.put_f32_le(value as f32),
412 8 => buf.put_f64_le(value),
413 _ => unreachable!("invalid float size"),
414 }
415}
416
417/// Encode a NVARCHAR value for TVP.
418pub fn encode_tvp_nvarchar(value: &str, max_length: u16, buf: &mut BytesMut) {
419 let utf16: Vec<u16> = value.encode_utf16().collect();
420 let byte_len = utf16.len() * 2;
421
422 if max_length == 0xFFFF {
423 // MAX type - use PLP format
424 buf.put_u64_le(byte_len as u64); // Total length
425 buf.put_u32_le(byte_len as u32); // Chunk length
426 for code_unit in utf16 {
427 buf.put_u16_le(code_unit);
428 }
429 buf.put_u32_le(0); // Terminator
430 } else {
431 // Regular type
432 buf.put_u16_le(byte_len as u16);
433 for code_unit in utf16 {
434 buf.put_u16_le(code_unit);
435 }
436 }
437}
438
439/// Encode a VARBINARY value for TVP.
440pub fn encode_tvp_varbinary(value: &[u8], max_length: u16, buf: &mut BytesMut) {
441 if max_length == 0xFFFF {
442 // MAX type - use PLP format
443 buf.put_u64_le(value.len() as u64);
444 buf.put_u32_le(value.len() as u32);
445 buf.put_slice(value);
446 buf.put_u32_le(0); // Terminator
447 } else {
448 buf.put_u16_le(value.len() as u16);
449 buf.put_slice(value);
450 }
451}
452
453/// Encode a UNIQUEIDENTIFIER value for TVP.
454///
455/// SQL Server uses mixed-endian format for UUIDs.
456pub fn encode_tvp_guid(uuid_bytes: &[u8; 16], buf: &mut BytesMut) {
457 buf.put_u8(16); // Length
458
459 // Mixed-endian: first 3 groups little-endian, last 2 groups big-endian
460 buf.put_u8(uuid_bytes[3]);
461 buf.put_u8(uuid_bytes[2]);
462 buf.put_u8(uuid_bytes[1]);
463 buf.put_u8(uuid_bytes[0]);
464
465 buf.put_u8(uuid_bytes[5]);
466 buf.put_u8(uuid_bytes[4]);
467
468 buf.put_u8(uuid_bytes[7]);
469 buf.put_u8(uuid_bytes[6]);
470
471 buf.put_slice(&uuid_bytes[8..16]);
472}
473
474/// Encode a DATE value for TVP (days since 0001-01-01).
475pub fn encode_tvp_date(days: u32, buf: &mut BytesMut) {
476 // DATE is 3 bytes
477 buf.put_u8((days & 0xFF) as u8);
478 buf.put_u8(((days >> 8) & 0xFF) as u8);
479 buf.put_u8(((days >> 16) & 0xFF) as u8);
480}
481
482/// Encode a TIME value for TVP.
483///
484/// Time is encoded as 100-nanosecond intervals since midnight.
485pub fn encode_tvp_time(intervals: u64, scale: u8, buf: &mut BytesMut) {
486 // Length depends on scale
487 let len = match scale {
488 0..=2 => 3,
489 3..=4 => 4,
490 5..=7 => 5,
491 _ => 5,
492 };
493 buf.put_u8(len);
494
495 for i in 0..len {
496 buf.put_u8((intervals >> (8 * i)) as u8);
497 }
498}
499
500/// Encode a DATETIME2 value for TVP.
501///
502/// DATETIME2 is TIME followed by DATE.
503pub fn encode_tvp_datetime2(time_intervals: u64, days: u32, scale: u8, buf: &mut BytesMut) {
504 // Length depends on scale (time bytes + 3 date bytes)
505 let time_len = match scale {
506 0..=2 => 3,
507 3..=4 => 4,
508 5..=7 => 5,
509 _ => 5,
510 };
511 buf.put_u8(time_len + 3);
512
513 // Time component
514 for i in 0..time_len {
515 buf.put_u8((time_intervals >> (8 * i)) as u8);
516 }
517
518 // Date component
519 buf.put_u8((days & 0xFF) as u8);
520 buf.put_u8(((days >> 8) & 0xFF) as u8);
521 buf.put_u8(((days >> 16) & 0xFF) as u8);
522}
523
524/// Encode a DATETIMEOFFSET value for TVP.
525///
526/// DATETIMEOFFSET is TIME followed by DATE followed by timezone offset.
527///
528/// # Arguments
529///
530/// * `time_intervals` - Time in 100-nanosecond intervals since midnight
531/// * `days` - Days since year 1 (0001-01-01)
532/// * `offset_minutes` - Timezone offset in minutes (e.g., -480 for UTC-8, 330 for UTC+5:30)
533/// * `scale` - Fractional seconds precision (0-7)
534pub fn encode_tvp_datetimeoffset(
535 time_intervals: u64,
536 days: u32,
537 offset_minutes: i16,
538 scale: u8,
539 buf: &mut BytesMut,
540) {
541 // Length depends on scale (time bytes + 3 date bytes + 2 offset bytes)
542 let time_len = match scale {
543 0..=2 => 3,
544 3..=4 => 4,
545 5..=7 => 5,
546 _ => 5,
547 };
548 buf.put_u8(time_len + 3 + 2); // time + date + offset
549
550 // Time component
551 for i in 0..time_len {
552 buf.put_u8((time_intervals >> (8 * i)) as u8);
553 }
554
555 // Date component
556 buf.put_u8((days & 0xFF) as u8);
557 buf.put_u8(((days >> 8) & 0xFF) as u8);
558 buf.put_u8(((days >> 16) & 0xFF) as u8);
559
560 // Timezone offset in minutes (signed 16-bit little-endian)
561 buf.put_i16_le(offset_minutes);
562}
563
564/// Encode a DECIMAL value for TVP.
565///
566/// # Arguments
567///
568/// * `sign` - 0 for negative, 1 for positive
569/// * `mantissa` - The absolute value as a 128-bit integer
570pub fn encode_tvp_decimal(sign: u8, mantissa: u128, buf: &mut BytesMut) {
571 buf.put_u8(17); // Length: 1 byte sign + 16 bytes mantissa
572 buf.put_u8(sign);
573 buf.put_u128_le(mantissa);
574}
575
576#[cfg(test)]
577#[allow(clippy::unwrap_used, clippy::expect_used)]
578mod tests {
579 use super::*;
580
581 #[test]
582 fn test_tvp_metadata_encoding() {
583 let columns = vec![TvpColumnDef::new(TvpWireType::Int { size: 4 })];
584
585 let encoder = TvpEncoder::new("dbo", "UserIdList", &columns);
586 let mut buf = BytesMut::new();
587
588 encoder.encode_metadata(&mut buf);
589
590 // Should start with TVP type ID
591 assert_eq!(buf[0], TVP_TYPE_ID);
592
593 // DbName should be empty (length 0)
594 assert_eq!(buf[1], 0);
595 }
596
597 #[test]
598 fn test_tvp_column_def_encoding() {
599 let col = TvpColumnDef::nullable(TvpWireType::Int { size: 4 });
600 let mut buf = BytesMut::new();
601
602 col.encode(&mut buf);
603
604 // UserType (4) + Flags (2) + TypeId (1) + MaxLen (1) + ColName (1)
605 assert!(buf.len() >= 9);
606
607 // UserType should be 0
608 assert_eq!(&buf[0..4], &[0, 0, 0, 0]);
609
610 // Flags should have nullable bit set
611 assert_eq!(buf[4], 0x01);
612 assert_eq!(buf[5], 0x00);
613 }
614
615 #[test]
616 fn test_tvp_nvarchar_encoding() {
617 let mut buf = BytesMut::new();
618 encode_tvp_nvarchar("test", 100, &mut buf);
619
620 // Length prefix (2) + UTF-16 data (4 chars * 2 bytes)
621 assert_eq!(buf.len(), 2 + 8);
622 assert_eq!(buf[0], 8); // Byte length
623 assert_eq!(buf[1], 0);
624 }
625
626 #[test]
627 fn test_tvp_int_encoding() {
628 let mut buf = BytesMut::new();
629 encode_tvp_int(42, 4, &mut buf);
630
631 // Length (1) + value (4)
632 assert_eq!(buf.len(), 5);
633 assert_eq!(buf[0], 4);
634 assert_eq!(buf[1], 42);
635 }
636}