Skip to main content

mssql_types/
encode.rs

1//! TDS binary encoding for SQL values.
2//!
3//! This module provides encoding of Rust values into TDS wire format
4//! for transmission to SQL Server.
5
6// Allow expect() for chrono date construction with known-valid constant dates
7#![allow(clippy::expect_used)]
8
9use bytes::{BufMut, BytesMut};
10
11use crate::error::TypeError;
12use crate::value::SqlValue;
13
14/// Trait for encoding values to TDS binary format.
15pub trait TdsEncode {
16    /// Encode this value into the buffer in TDS format.
17    fn encode(&self, buf: &mut BytesMut) -> Result<(), TypeError>;
18
19    /// Get the TDS type ID for this value.
20    fn type_id(&self) -> u8;
21}
22
23impl TdsEncode for SqlValue {
24    fn encode(&self, buf: &mut BytesMut) -> Result<(), TypeError> {
25        match self {
26            SqlValue::Null => {
27                // NULL is represented by length indicator in most contexts
28                // For INTNTYPE, length 0 means NULL
29                Ok(())
30            }
31            SqlValue::Bool(v) => {
32                buf.put_u8(if *v { 1 } else { 0 });
33                Ok(())
34            }
35            SqlValue::TinyInt(v) => {
36                buf.put_u8(*v);
37                Ok(())
38            }
39            SqlValue::SmallInt(v) => {
40                buf.put_i16_le(*v);
41                Ok(())
42            }
43            SqlValue::Int(v) => {
44                buf.put_i32_le(*v);
45                Ok(())
46            }
47            SqlValue::BigInt(v) => {
48                buf.put_i64_le(*v);
49                Ok(())
50            }
51            SqlValue::Float(v) => {
52                buf.put_f32_le(*v);
53                Ok(())
54            }
55            SqlValue::Double(v) => {
56                buf.put_f64_le(*v);
57                Ok(())
58            }
59            SqlValue::String(s) => {
60                // Encode as UTF-16LE for NVARCHAR
61                encode_utf16_string(s, buf);
62                Ok(())
63            }
64            SqlValue::Binary(b) => {
65                // Length-prefixed binary data
66                if b.len() > u16::MAX as usize {
67                    return Err(TypeError::BufferTooSmall {
68                        needed: b.len(),
69                        available: u16::MAX as usize,
70                    });
71                }
72                buf.put_u16_le(b.len() as u16);
73                buf.put_slice(b);
74                Ok(())
75            }
76            #[cfg(feature = "decimal")]
77            SqlValue::Decimal(d) => {
78                encode_decimal(*d, buf);
79                Ok(())
80            }
81            #[cfg(feature = "decimal")]
82            SqlValue::Money(d) => encode_money(*d, buf),
83            #[cfg(feature = "decimal")]
84            SqlValue::SmallMoney(d) => encode_smallmoney(*d, buf),
85            #[cfg(feature = "uuid")]
86            SqlValue::Uuid(u) => {
87                encode_uuid(*u, buf);
88                Ok(())
89            }
90            #[cfg(feature = "chrono")]
91            SqlValue::Date(d) => {
92                encode_date(*d, buf);
93                Ok(())
94            }
95            #[cfg(feature = "chrono")]
96            SqlValue::Time(t) => {
97                encode_time(*t, buf);
98                Ok(())
99            }
100            #[cfg(feature = "chrono")]
101            SqlValue::DateTime(dt) => {
102                encode_datetime2(*dt, buf);
103                Ok(())
104            }
105            #[cfg(feature = "chrono")]
106            SqlValue::SmallDateTime(dt) => encode_smalldatetime(*dt, buf),
107            #[cfg(feature = "chrono")]
108            SqlValue::DateTimeOffset(dto) => {
109                encode_datetimeoffset(*dto, buf);
110                Ok(())
111            }
112            #[cfg(feature = "json")]
113            SqlValue::Json(j) => {
114                // JSON is sent as NVARCHAR string
115                let s = j.to_string();
116                encode_utf16_string(&s, buf);
117                Ok(())
118            }
119            SqlValue::Xml(x) => {
120                // XML is sent as UTF-16LE string
121                encode_utf16_string(x, buf);
122                Ok(())
123            }
124            SqlValue::Tvp(_) => {
125                // TVP encoding is handled at the RPC parameter level, not here.
126                // This method is for encoding the value data portion; TVPs have
127                // their own complex encoding structure that includes metadata.
128                // See tds-protocol crate for full TVP encoding.
129                Err(TypeError::UnsupportedConversion {
130                    from: "TvpData".to_string(),
131                    to: "raw bytes (use RPC parameter encoding)",
132                })
133            }
134        }
135    }
136
137    fn type_id(&self) -> u8 {
138        match self {
139            SqlValue::Null => 0x1F,        // NULLTYPE
140            SqlValue::Bool(_) => 0x32,     // BITTYPE
141            SqlValue::TinyInt(_) => 0x30,  // INT1TYPE
142            SqlValue::SmallInt(_) => 0x34, // INT2TYPE
143            SqlValue::Int(_) => 0x38,      // INT4TYPE
144            SqlValue::BigInt(_) => 0x7F,   // INT8TYPE
145            SqlValue::Float(_) => 0x3B,    // FLT4TYPE
146            SqlValue::Double(_) => 0x3E,   // FLT8TYPE
147            SqlValue::String(_) => 0xE7,   // NVARCHARTYPE
148            SqlValue::Binary(_) => 0xA5,   // BIGVARBINTYPE
149            #[cfg(feature = "decimal")]
150            SqlValue::Decimal(_) => 0x6C, // DECIMALTYPE
151            #[cfg(feature = "decimal")]
152            SqlValue::Money(_) => 0x6E, // MONEYNTYPE (8-byte payload)
153            #[cfg(feature = "decimal")]
154            SqlValue::SmallMoney(_) => 0x6E, // MONEYNTYPE (4-byte payload)
155            #[cfg(feature = "uuid")]
156            SqlValue::Uuid(_) => 0x24, // GUIDTYPE
157            #[cfg(feature = "chrono")]
158            SqlValue::Date(_) => 0x28, // DATETYPE
159            #[cfg(feature = "chrono")]
160            SqlValue::Time(_) => 0x29, // TIMETYPE
161            #[cfg(feature = "chrono")]
162            SqlValue::DateTime(_) => 0x2A, // DATETIME2TYPE
163            #[cfg(feature = "chrono")]
164            SqlValue::SmallDateTime(_) => 0x6F, // DATETIMENTYPE (4-byte payload)
165            #[cfg(feature = "chrono")]
166            SqlValue::DateTimeOffset(_) => 0x2B, // DATETIMEOFFSETTYPE
167            #[cfg(feature = "json")]
168            SqlValue::Json(_) => 0xE7, // NVARCHARTYPE (JSON as string)
169            SqlValue::Xml(_) => 0xF1,      // XMLTYPE
170            SqlValue::Tvp(_) => 0xF3,      // TVPTYPE
171        }
172    }
173}
174
175/// Encode a string as UTF-16LE with length prefix.
176pub fn encode_utf16_string(s: &str, buf: &mut BytesMut) {
177    let utf16: Vec<u16> = s.encode_utf16().collect();
178    let byte_len = utf16.len() * 2;
179
180    // Write byte length (not char length)
181    buf.put_u16_le(byte_len as u16);
182
183    // Write UTF-16LE bytes
184    for code_unit in utf16 {
185        buf.put_u16_le(code_unit);
186    }
187}
188
189/// Encode a string as UTF-16LE without length prefix (for fixed-length fields).
190pub fn encode_utf16_string_no_len(s: &str, buf: &mut BytesMut) {
191    for code_unit in s.encode_utf16() {
192        buf.put_u16_le(code_unit);
193    }
194}
195
196/// Encode a UUID in SQL Server's mixed-endian format.
197///
198/// SQL Server stores UUIDs in a unique byte order:
199/// - First 4 bytes: little-endian
200/// - Next 2 bytes: little-endian
201/// - Next 2 bytes: little-endian
202/// - Last 8 bytes: big-endian (as-is)
203#[cfg(feature = "uuid")]
204pub fn encode_uuid(uuid: uuid::Uuid, buf: &mut BytesMut) {
205    let bytes = uuid.as_bytes();
206
207    // First group (4 bytes) - reverse for little-endian
208    buf.put_u8(bytes[3]);
209    buf.put_u8(bytes[2]);
210    buf.put_u8(bytes[1]);
211    buf.put_u8(bytes[0]);
212
213    // Second group (2 bytes) - reverse for little-endian
214    buf.put_u8(bytes[5]);
215    buf.put_u8(bytes[4]);
216
217    // Third group (2 bytes) - reverse for little-endian
218    buf.put_u8(bytes[7]);
219    buf.put_u8(bytes[6]);
220
221    // Last 8 bytes - big-endian (keep as-is)
222    buf.put_slice(&bytes[8..16]);
223}
224
225/// Encode a decimal value.
226///
227/// TDS DECIMAL format:
228/// - 1 byte: sign (0 = negative, 1 = positive)
229/// - Remaining bytes: absolute value in little-endian
230#[cfg(feature = "decimal")]
231pub fn encode_decimal(decimal: rust_decimal::Decimal, buf: &mut BytesMut) {
232    let sign = if decimal.is_sign_negative() { 0u8 } else { 1u8 };
233    buf.put_u8(sign);
234
235    // Get the mantissa and encode as 128-bit integer
236    let mantissa = decimal.mantissa().unsigned_abs();
237    buf.put_u128_le(mantissa);
238}
239
240/// Rescale a decimal to MONEY's 4-decimal fixed-point representation.
241///
242/// Returns the signed 128-bit integer representing the value multiplied by
243/// 10_000. Excess precision past 4 decimal places is truncated toward zero.
244#[cfg(feature = "decimal")]
245fn decimal_to_money_cents(value: rust_decimal::Decimal) -> Result<i128, TypeError> {
246    let mantissa: i128 = value.mantissa();
247    let scale: u32 = value.scale();
248    if scale <= 4 {
249        let factor = 10_i128.pow(4 - scale);
250        mantissa.checked_mul(factor).ok_or(TypeError::OutOfRange {
251            target_type: "MONEY",
252        })
253    } else {
254        let factor = 10_i128.pow(scale - 4);
255        Ok(mantissa / factor)
256    }
257}
258
259/// Convert a decimal to the scaled i64 used on the MONEY wire.
260///
261/// This is the shared pre-encoding step for both RPC parameter encoding and
262/// TVP column encoding — each path knows how to frame the payload, but they
263/// agree on how to derive the scaled integer from the decimal.
264#[cfg(feature = "decimal")]
265pub fn decimal_to_money_cents_i64(value: rust_decimal::Decimal) -> Result<i64, TypeError> {
266    let cents_i128 = decimal_to_money_cents(value)?;
267    i64::try_from(cents_i128).map_err(|_| TypeError::OutOfRange {
268        target_type: "MONEY",
269    })
270}
271
272/// Convert a decimal to the scaled i32 used on the SMALLMONEY wire.
273#[cfg(feature = "decimal")]
274pub fn decimal_to_smallmoney_cents_i32(value: rust_decimal::Decimal) -> Result<i32, TypeError> {
275    let cents_i128 = decimal_to_money_cents(value)?;
276    i32::try_from(cents_i128).map_err(|_| TypeError::OutOfRange {
277        target_type: "SMALLMONEY",
278    })
279}
280
281/// Encode a decimal as MONEY (8 bytes): the signed 64-bit scaled integer is
282/// written as the high 32 bits LE followed by the low 32 bits LE, per
283/// MS-TDS §2.2.5.5.1.2.
284#[cfg(feature = "decimal")]
285pub fn encode_money(value: rust_decimal::Decimal, buf: &mut BytesMut) -> Result<(), TypeError> {
286    let cents = decimal_to_money_cents_i64(value)?;
287    let high = (cents >> 32) as i32;
288    let low = (cents & 0xFFFF_FFFF) as u32;
289    buf.put_i32_le(high);
290    buf.put_u32_le(low);
291    Ok(())
292}
293
294/// Encode a decimal as SMALLMONEY (4 bytes): the signed 32-bit scaled integer
295/// is written little-endian.
296#[cfg(feature = "decimal")]
297pub fn encode_smallmoney(
298    value: rust_decimal::Decimal,
299    buf: &mut BytesMut,
300) -> Result<(), TypeError> {
301    let cents = decimal_to_smallmoney_cents_i32(value)?;
302    buf.put_i32_le(cents);
303    Ok(())
304}
305
306/// Convert a NaiveDateTime to the DATETIME wire representation.
307///
308/// Returns `(days_since_1900_i32, ticks_u32)` where each tick is 1/300 second.
309/// This is the shared pre-encoding step for both RPC parameter encoding and
310/// TVP column encoding.
311#[cfg(feature = "chrono")]
312pub fn datetime_to_legacy_days_ticks(dt: chrono::NaiveDateTime) -> (i32, u32) {
313    use chrono::Timelike;
314    let epoch = chrono::NaiveDate::from_ymd_opt(1900, 1, 1).expect("epoch 1900-01-01 is valid");
315    let days = (dt.date() - epoch).num_days() as i32;
316
317    let since_midnight = dt.time().num_seconds_from_midnight() as u64 * 1000
318        + u64::from(dt.time().nanosecond()) / 1_000_000;
319    // Convert ms → 1/300s ticks: ticks = round(ms * 300 / 1000) = round(ms * 3 / 10)
320    let ticks = ((since_midnight * 3 + 5) / 10) as u32;
321    (days, ticks)
322}
323
324/// Encode a DATETIME value (8 bytes): days since 1900 (`i32` LE) + time units
325/// since midnight (`u32` LE) where each unit is 1/300 of a second.
326#[cfg(feature = "chrono")]
327pub fn encode_datetime_legacy(dt: chrono::NaiveDateTime, buf: &mut BytesMut) {
328    let (days, ticks) = datetime_to_legacy_days_ticks(dt);
329    buf.put_i32_le(days);
330    buf.put_u32_le(ticks);
331}
332
333/// Convert a NaiveDateTime to the SMALLDATETIME wire representation.
334///
335/// Returns `(days_since_1900_u16, minutes_since_midnight_u16)`. Seconds are
336/// rounded to the nearest minute (30s rounds up per SQL Server semantics); when
337/// that rounding lands on or past 24:00 the carry propagates into the next day
338/// — e.g. 23:59:45 → next day 00:00 — so the result stays within SQL Server's
339/// valid minute range of 0..1439. Returns `Err` if the resulting date is
340/// outside the SMALLDATETIME range (1900-01-01 through 2079-06-06).
341#[cfg(feature = "chrono")]
342pub fn datetime_to_smalldatetime_days_minutes(
343    dt: chrono::NaiveDateTime,
344) -> Result<(u16, u16), TypeError> {
345    use chrono::Timelike;
346    let epoch = chrono::NaiveDate::from_ymd_opt(1900, 1, 1).expect("epoch 1900-01-01 is valid");
347
348    let total_seconds = dt.time().hour() * 3600 + dt.time().minute() * 60 + dt.time().second();
349    let minutes_raw = (total_seconds + 30) / 60;
350    // Carry over into the next day when seconds round up past 24:00 so that
351    // the returned minute count stays within SQL Server's valid 0..1439 range.
352    // SQL Server itself does the same thing when casting 23:59:45 to
353    // SMALLDATETIME — sending minutes=1440 directly on the wire is rejected
354    // as "invalid instance of data type smalldatetime".
355    let (day_carry, minutes) = if minutes_raw >= 1440 {
356        (1i64, 0u16)
357    } else {
358        (0i64, minutes_raw as u16)
359    };
360
361    let days_i64 = (dt.date() - epoch).num_days() + day_carry;
362    let days: u16 = u16::try_from(days_i64).map_err(|_| {
363        TypeError::InvalidDateTime(format!(
364            "SMALLDATETIME year must be 1900-2079, got date with {days_i64} days since 1900-01-01"
365        ))
366    })?;
367
368    Ok((days, minutes))
369}
370
371/// Encode a SMALLDATETIME value (4 bytes): days since 1900 (`u16` LE) +
372/// minutes since midnight (`u16` LE). Seconds are rounded to the nearest
373/// minute (30s rounds up per SQL Server semantics).
374///
375/// Returns an error if the date is outside the SMALLDATETIME range
376/// (1900-01-01 through 2079-06-06).
377#[cfg(feature = "chrono")]
378pub fn encode_smalldatetime(
379    dt: chrono::NaiveDateTime,
380    buf: &mut BytesMut,
381) -> Result<(), TypeError> {
382    let (days, minutes) = datetime_to_smalldatetime_days_minutes(dt)?;
383    buf.put_u16_le(days);
384    buf.put_u16_le(minutes);
385    Ok(())
386}
387
388/// Encode a DATE value.
389///
390/// TDS DATE is the number of days since 0001-01-01.
391#[cfg(feature = "chrono")]
392pub fn encode_date(date: chrono::NaiveDate, buf: &mut BytesMut) {
393    // Calculate days since 0001-01-01
394    let base = chrono::NaiveDate::from_ymd_opt(1, 1, 1).expect("valid date");
395    let days = date.signed_duration_since(base).num_days() as u32;
396
397    // DATE is encoded as 3 bytes (little-endian)
398    buf.put_u8((days & 0xFF) as u8);
399    buf.put_u8(((days >> 8) & 0xFF) as u8);
400    buf.put_u8(((days >> 16) & 0xFF) as u8);
401}
402
403/// Encode a TIME value.
404///
405/// TDS TIME is encoded as 100-nanosecond intervals since midnight.
406#[cfg(feature = "chrono")]
407pub fn encode_time(time: chrono::NaiveTime, buf: &mut BytesMut) {
408    use chrono::Timelike;
409
410    // Calculate 100-ns intervals since midnight
411    // Scale = 7 (100-nanosecond precision)
412    let nanos = time.num_seconds_from_midnight() as u64 * 1_000_000_000 + time.nanosecond() as u64;
413    let intervals = nanos / 100;
414
415    // TIME with scale 7 uses 5 bytes
416    buf.put_u8((intervals & 0xFF) as u8);
417    buf.put_u8(((intervals >> 8) & 0xFF) as u8);
418    buf.put_u8(((intervals >> 16) & 0xFF) as u8);
419    buf.put_u8(((intervals >> 24) & 0xFF) as u8);
420    buf.put_u8(((intervals >> 32) & 0xFF) as u8);
421}
422
423/// Encode a DATETIME2 value.
424///
425/// DATETIME2 is encoded as TIME followed by DATE.
426#[cfg(feature = "chrono")]
427pub fn encode_datetime2(datetime: chrono::NaiveDateTime, buf: &mut BytesMut) {
428    encode_time(datetime.time(), buf);
429    encode_date(datetime.date(), buf);
430}
431
432/// Encode a DATETIMEOFFSET value.
433///
434/// DATETIMEOFFSET is encoded as TIME + DATE + offset (in minutes).
435#[cfg(feature = "chrono")]
436pub fn encode_datetimeoffset(datetime: chrono::DateTime<chrono::FixedOffset>, buf: &mut BytesMut) {
437    use chrono::Offset;
438
439    // Encode time and date components
440    encode_time(datetime.time(), buf);
441    encode_date(datetime.date_naive(), buf);
442
443    // Encode timezone offset in minutes (signed 16-bit)
444    let offset_seconds = datetime.offset().fix().local_minus_utc();
445    let offset_minutes = (offset_seconds / 60) as i16;
446    buf.put_i16_le(offset_minutes);
447}
448
449#[cfg(test)]
450#[allow(clippy::unwrap_used)]
451mod tests {
452    use super::*;
453
454    #[test]
455    fn test_encode_int() {
456        let mut buf = BytesMut::new();
457        SqlValue::Int(42).encode(&mut buf).unwrap();
458        assert_eq!(&buf[..], &[42, 0, 0, 0]);
459    }
460
461    #[test]
462    fn test_encode_bigint() {
463        let mut buf = BytesMut::new();
464        SqlValue::BigInt(0x0102030405060708)
465            .encode(&mut buf)
466            .unwrap();
467        assert_eq!(&buf[..], &[0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]);
468    }
469
470    #[test]
471    fn test_encode_utf16_string() {
472        let mut buf = BytesMut::new();
473        encode_utf16_string("AB", &mut buf);
474        // Length (4 bytes for 2 UTF-16 code units) + "AB" in UTF-16LE
475        assert_eq!(&buf[..], &[4, 0, 0x41, 0, 0x42, 0]);
476    }
477
478    #[cfg(feature = "uuid")]
479    #[test]
480    fn test_encode_uuid() {
481        let mut buf = BytesMut::new();
482        let uuid = uuid::Uuid::parse_str("12345678-1234-5678-1234-567812345678").unwrap();
483        encode_uuid(uuid, &mut buf);
484        // SQL Server mixed-endian format
485        assert_eq!(
486            &buf[..],
487            &[
488                0x78, 0x56, 0x34, 0x12, // First group reversed
489                0x34, 0x12, // Second group reversed
490                0x78, 0x56, // Third group reversed
491                0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78 // Last 8 bytes as-is
492            ]
493        );
494    }
495
496    #[cfg(feature = "chrono")]
497    #[test]
498    fn test_encode_date() {
499        let mut buf = BytesMut::new();
500        let date = chrono::NaiveDate::from_ymd_opt(2024, 1, 15).unwrap();
501        encode_date(date, &mut buf);
502        // Should be 3 bytes representing days since 0001-01-01
503        assert_eq!(buf.len(), 3);
504    }
505}