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}