Skip to main content

mssql_types/
decode.rs

1//! TDS binary decoding for SQL values.
2//!
3//! This module provides decoding of TDS wire format data into Rust values.
4
5// Allow expect() for chrono date construction with known-valid constant dates
6// (e.g., from_ymd_opt(1, 1, 1) for SQL Server epoch)
7#![allow(clippy::expect_used)]
8
9use bytes::{Buf, Bytes};
10
11use crate::error::TypeError;
12use crate::value::SqlValue;
13
14/// Trait for decoding values from TDS binary format.
15pub trait TdsDecode: Sized {
16    /// Decode a value from the buffer.
17    fn decode(buf: &mut Bytes, type_info: &TypeInfo) -> Result<Self, TypeError>;
18}
19
20/// TDS type information for decoding.
21#[derive(Debug, Clone)]
22pub struct TypeInfo {
23    /// The TDS type ID.
24    pub type_id: u8,
25    /// Length/precision for variable-length types.
26    pub length: Option<u32>,
27    /// Scale for decimal/time types.
28    pub scale: Option<u8>,
29    /// Precision for decimal types.
30    pub precision: Option<u8>,
31    /// Collation for string types.
32    pub collation: Option<Collation>,
33}
34
35/// SQL Server collation information.
36#[derive(Debug, Clone, Copy)]
37pub struct Collation {
38    /// Locale ID.
39    pub lcid: u32,
40    /// Collation flags.
41    pub flags: u8,
42}
43
44impl Collation {
45    /// Check if this collation uses UTF-8 encoding (SQL Server 2019+).
46    ///
47    /// UTF-8 collations have fUTF8, bit 26 (0x0400_0000), set in the
48    /// collation info field (bit 27 is FRESERVEDBIT per MS-TDS).
49    #[must_use]
50    pub fn is_utf8(&self) -> bool {
51        (self.lcid & 0x0400_0000) != 0
52    }
53
54    /// Get the encoding for this collation.
55    ///
56    /// Returns the appropriate `encoding_rs::Encoding` for the collation's LCID,
57    /// or `None` if the encoding is not supported.
58    #[cfg(feature = "encoding")]
59    #[must_use]
60    pub fn encoding(&self) -> Option<&'static encoding_rs::Encoding> {
61        encoding_for_lcid(self.lcid)
62    }
63}
64
65/// UTF-8 collation flag bit — fUTF8, bit 26 per the MS-TDS Collation Rule
66/// Definition (bit 27 is FRESERVEDBIT).
67#[cfg(feature = "encoding")]
68const UTF8_COLLATION_FLAG: u32 = 0x0400_0000;
69
70/// Get the encoding for an LCID value.
71#[cfg(feature = "encoding")]
72fn encoding_for_lcid(lcid: u32) -> Option<&'static encoding_rs::Encoding> {
73    // Check for UTF-8 collation first (SQL Server 2019+)
74    if (lcid & UTF8_COLLATION_FLAG) != 0 {
75        return Some(encoding_rs::UTF_8);
76    }
77
78    // Get code page from LCID
79    let code_page = code_page_for_lcid(lcid)?;
80
81    // Map code page to encoding
82    match code_page {
83        874 => Some(encoding_rs::WINDOWS_874),
84        932 => Some(encoding_rs::SHIFT_JIS),
85        936 => Some(encoding_rs::GB18030),
86        949 => Some(encoding_rs::EUC_KR),
87        950 => Some(encoding_rs::BIG5),
88        1250 => Some(encoding_rs::WINDOWS_1250),
89        1251 => Some(encoding_rs::WINDOWS_1251),
90        1252 => Some(encoding_rs::WINDOWS_1252),
91        1253 => Some(encoding_rs::WINDOWS_1253),
92        1254 => Some(encoding_rs::WINDOWS_1254),
93        1255 => Some(encoding_rs::WINDOWS_1255),
94        1256 => Some(encoding_rs::WINDOWS_1256),
95        1257 => Some(encoding_rs::WINDOWS_1257),
96        1258 => Some(encoding_rs::WINDOWS_1258),
97        _ => None,
98    }
99}
100
101/// Get the Windows code page for an LCID value.
102#[cfg(feature = "encoding")]
103fn code_page_for_lcid(lcid: u32) -> Option<u16> {
104    // Mask for primary language ID (lower 10 bits)
105    const PRIMARY_LANGUAGE_MASK: u32 = 0x3FF;
106    let primary_lang = lcid & PRIMARY_LANGUAGE_MASK;
107
108    match primary_lang {
109        0x0411 => Some(932),                   // Japanese - Shift_JIS
110        0x0804 | 0x1004 => Some(936),          // Chinese Simplified - GBK
111        0x0404 | 0x0C04 | 0x1404 => Some(950), // Chinese Traditional - Big5
112        0x0412 => Some(949),                   // Korean - EUC-KR
113        0x041E => Some(874),                   // Thai
114        0x042A => Some(1258),                  // Vietnamese
115
116        // Code Page 1250 - Central European
117        0x0405 | 0x0415 | 0x040E | 0x041A | 0x081A | 0x141A | 0x101A | 0x041B | 0x0424 | 0x0418
118        | 0x041C => Some(1250),
119
120        // Code Page 1251 - Cyrillic
121        0x0419 | 0x0422 | 0x0423 | 0x0402 | 0x042F | 0x0C1A | 0x201A | 0x0440 | 0x0843 | 0x0444
122        | 0x0450 | 0x0485 => Some(1251),
123
124        0x0408 => Some(1253),          // Greek
125        0x041F | 0x042C => Some(1254), // Turkish, Azerbaijani
126        0x040D => Some(1255),          // Hebrew
127
128        // Code Page 1256 - Arabic
129        0x0401 | 0x0801 | 0x0C01 | 0x1001 | 0x1401 | 0x1801 | 0x1C01 | 0x2001 | 0x2401 | 0x2801
130        | 0x2C01 | 0x3001 | 0x3401 | 0x3801 | 0x3C01 | 0x4001 | 0x0429 | 0x0420 | 0x048C
131        | 0x0463 => Some(1256),
132
133        // Code Page 1257 - Baltic
134        0x0425..=0x0427 => Some(1257),
135
136        // Default to 1252 (Western European) for English and related languages
137        0x0409 | 0x0809 | 0x0C09 | 0x1009 | 0x1409 | 0x1809 | 0x1C09 | 0x2009 | 0x2409 | 0x2809
138        | 0x2C09 | 0x3009 | 0x3409 | 0x0407 | 0x0807 | 0x0C07 | 0x1007 | 0x1407 | 0x040C
139        | 0x080C | 0x0C0C | 0x100C | 0x140C | 0x180C | 0x0410 | 0x0810 | 0x0413 | 0x0813
140        | 0x0416 | 0x0816 | 0x040A | 0x080A | 0x0C0A | 0x100A | 0x140A | 0x180A | 0x1C0A
141        | 0x200A | 0x240A | 0x280A | 0x2C0A | 0x300A | 0x340A | 0x380A | 0x3C0A | 0x400A
142        | 0x440A | 0x480A | 0x4C0A | 0x500A => Some(1252),
143
144        _ => Some(1252), // Default fallback
145    }
146}
147
148impl TypeInfo {
149    /// Create type info for a fixed-length integer type.
150    #[must_use]
151    pub fn int(type_id: u8) -> Self {
152        Self {
153            type_id,
154            length: None,
155            scale: None,
156            precision: None,
157            collation: None,
158        }
159    }
160
161    /// Create type info for a variable-length type.
162    #[must_use]
163    pub fn varchar(length: u32) -> Self {
164        Self {
165            type_id: 0xE7, // NVARCHARTYPE
166            length: Some(length),
167            scale: None,
168            precision: None,
169            collation: None,
170        }
171    }
172
173    /// Create type info for a decimal type.
174    #[must_use]
175    pub fn decimal(precision: u8, scale: u8) -> Self {
176        Self {
177            type_id: 0x6C,
178            length: None,
179            scale: Some(scale),
180            precision: Some(precision),
181            collation: None,
182        }
183    }
184
185    /// Create type info for a datetime type with scale.
186    #[must_use]
187    pub fn datetime_with_scale(type_id: u8, scale: u8) -> Self {
188        Self {
189            type_id,
190            length: None,
191            scale: Some(scale),
192            precision: None,
193            collation: None,
194        }
195    }
196}
197
198/// Decode a SQL value based on type information.
199pub fn decode_value(buf: &mut Bytes, type_info: &TypeInfo) -> Result<SqlValue, TypeError> {
200    match type_info.type_id {
201        // Fixed-length types
202        0x1F => Ok(SqlValue::Null),   // NULLTYPE
203        0x32 => decode_bit(buf),      // BITTYPE
204        0x30 => decode_tinyint(buf),  // INT1TYPE
205        0x34 => decode_smallint(buf), // INT2TYPE
206        0x38 => decode_int(buf),      // INT4TYPE
207        0x7F => decode_bigint(buf),   // INT8TYPE
208        0x3B => decode_float(buf),    // FLT4TYPE
209        0x3E => decode_double(buf),   // FLT8TYPE
210
211        // Nullable integer types (INTNTYPE)
212        0x26 => decode_intn(buf, type_info),
213
214        // Variable-length string types
215        0xE7 => decode_nvarchar(buf, type_info), // NVARCHARTYPE
216        0xAF => decode_varchar(buf, type_info),  // BIGCHARTYPE
217        0xA7 => decode_varchar(buf, type_info),  // BIGVARCHARTYPE
218
219        // Binary types
220        0xA5 => decode_varbinary(buf, type_info), // BIGVARBINTYPE
221        0xAD => decode_varbinary(buf, type_info), // BIGBINARYTYPE
222
223        // GUID
224        0x24 => decode_guid(buf),
225
226        // Decimal/Numeric
227        0x6C | 0x6A => decode_decimal(buf, type_info),
228
229        // Date/Time types
230        0x28 => decode_date(buf),                      // DATETYPE
231        0x29 => decode_time(buf, type_info),           // TIMETYPE
232        0x2A => decode_datetime2(buf, type_info),      // DATETIME2TYPE
233        0x2B => decode_datetimeoffset(buf, type_info), // DATETIMEOFFSETTYPE
234        0x3D => decode_datetime(buf),                  // DATETIMETYPE
235        0x3F => decode_smalldatetime(buf),             // SMALLDATETIMETYPE
236
237        // XML
238        0xF1 => decode_xml(buf),
239
240        _ => Err(TypeError::UnsupportedConversion {
241            from: format!("TDS type 0x{:02X}", type_info.type_id),
242            to: "SqlValue",
243        }),
244    }
245}
246
247fn decode_bit(buf: &mut Bytes) -> Result<SqlValue, TypeError> {
248    if buf.remaining() < 1 {
249        return Err(TypeError::BufferTooSmall {
250            needed: 1,
251            available: buf.remaining(),
252        });
253    }
254    Ok(SqlValue::Bool(buf.get_u8() != 0))
255}
256
257fn decode_tinyint(buf: &mut Bytes) -> Result<SqlValue, TypeError> {
258    if buf.remaining() < 1 {
259        return Err(TypeError::BufferTooSmall {
260            needed: 1,
261            available: buf.remaining(),
262        });
263    }
264    Ok(SqlValue::TinyInt(buf.get_u8()))
265}
266
267fn decode_smallint(buf: &mut Bytes) -> Result<SqlValue, TypeError> {
268    if buf.remaining() < 2 {
269        return Err(TypeError::BufferTooSmall {
270            needed: 2,
271            available: buf.remaining(),
272        });
273    }
274    Ok(SqlValue::SmallInt(buf.get_i16_le()))
275}
276
277fn decode_int(buf: &mut Bytes) -> Result<SqlValue, TypeError> {
278    if buf.remaining() < 4 {
279        return Err(TypeError::BufferTooSmall {
280            needed: 4,
281            available: buf.remaining(),
282        });
283    }
284    Ok(SqlValue::Int(buf.get_i32_le()))
285}
286
287fn decode_bigint(buf: &mut Bytes) -> Result<SqlValue, TypeError> {
288    if buf.remaining() < 8 {
289        return Err(TypeError::BufferTooSmall {
290            needed: 8,
291            available: buf.remaining(),
292        });
293    }
294    Ok(SqlValue::BigInt(buf.get_i64_le()))
295}
296
297fn decode_float(buf: &mut Bytes) -> Result<SqlValue, TypeError> {
298    if buf.remaining() < 4 {
299        return Err(TypeError::BufferTooSmall {
300            needed: 4,
301            available: buf.remaining(),
302        });
303    }
304    Ok(SqlValue::Float(buf.get_f32_le()))
305}
306
307fn decode_double(buf: &mut Bytes) -> Result<SqlValue, TypeError> {
308    if buf.remaining() < 8 {
309        return Err(TypeError::BufferTooSmall {
310            needed: 8,
311            available: buf.remaining(),
312        });
313    }
314    Ok(SqlValue::Double(buf.get_f64_le()))
315}
316
317fn decode_intn(buf: &mut Bytes, _type_info: &TypeInfo) -> Result<SqlValue, TypeError> {
318    if buf.remaining() < 1 {
319        return Err(TypeError::BufferTooSmall {
320            needed: 1,
321            available: buf.remaining(),
322        });
323    }
324
325    let actual_len = buf.get_u8() as usize;
326    if actual_len == 0 {
327        return Ok(SqlValue::Null);
328    }
329
330    if buf.remaining() < actual_len {
331        return Err(TypeError::BufferTooSmall {
332            needed: actual_len,
333            available: buf.remaining(),
334        });
335    }
336
337    match actual_len {
338        1 => Ok(SqlValue::TinyInt(buf.get_u8())),
339        2 => Ok(SqlValue::SmallInt(buf.get_i16_le())),
340        4 => Ok(SqlValue::Int(buf.get_i32_le())),
341        8 => Ok(SqlValue::BigInt(buf.get_i64_le())),
342        _ => Err(TypeError::InvalidBinary(format!(
343            "invalid INTN length: {actual_len}"
344        ))),
345    }
346}
347
348fn decode_nvarchar(buf: &mut Bytes, _type_info: &TypeInfo) -> Result<SqlValue, TypeError> {
349    if buf.remaining() < 2 {
350        return Err(TypeError::BufferTooSmall {
351            needed: 2,
352            available: buf.remaining(),
353        });
354    }
355
356    let byte_len = buf.get_u16_le() as usize;
357
358    // 0xFFFF indicates NULL
359    if byte_len == 0xFFFF {
360        return Ok(SqlValue::Null);
361    }
362
363    if buf.remaining() < byte_len {
364        return Err(TypeError::BufferTooSmall {
365            needed: byte_len,
366            available: buf.remaining(),
367        });
368    }
369
370    let utf16_data = buf.copy_to_bytes(byte_len);
371    let s = decode_utf16_string(&utf16_data)?;
372    Ok(SqlValue::String(s))
373}
374
375fn decode_varchar(buf: &mut Bytes, type_info: &TypeInfo) -> Result<SqlValue, TypeError> {
376    if buf.remaining() < 2 {
377        return Err(TypeError::BufferTooSmall {
378            needed: 2,
379            available: buf.remaining(),
380        });
381    }
382
383    let byte_len = buf.get_u16_le() as usize;
384
385    // 0xFFFF indicates NULL
386    if byte_len == 0xFFFF {
387        return Ok(SqlValue::Null);
388    }
389
390    if buf.remaining() < byte_len {
391        return Err(TypeError::BufferTooSmall {
392            needed: byte_len,
393            available: buf.remaining(),
394        });
395    }
396
397    let data = buf.copy_to_bytes(byte_len);
398
399    // Try UTF-8 first (most common case and zero-cost for ASCII)
400    if let Ok(s) = String::from_utf8(data.to_vec()) {
401        return Ok(SqlValue::String(s));
402    }
403
404    // If UTF-8 fails, try collation-aware decoding
405    #[cfg(feature = "encoding")]
406    if let Some(ref collation) = type_info.collation {
407        if let Some(encoding) = collation.encoding() {
408            let (decoded, _, had_errors) = encoding.decode(&data);
409            if !had_errors {
410                return Ok(SqlValue::String(decoded.into_owned()));
411            }
412        }
413    }
414
415    // Suppress unused warning when encoding feature is disabled
416    #[cfg(not(feature = "encoding"))]
417    let _ = type_info;
418
419    // Fallback: lossy UTF-8 conversion
420    Ok(SqlValue::String(
421        String::from_utf8_lossy(&data).into_owned(),
422    ))
423}
424
425fn decode_varbinary(buf: &mut Bytes, _type_info: &TypeInfo) -> Result<SqlValue, TypeError> {
426    if buf.remaining() < 2 {
427        return Err(TypeError::BufferTooSmall {
428            needed: 2,
429            available: buf.remaining(),
430        });
431    }
432
433    let byte_len = buf.get_u16_le() as usize;
434
435    // 0xFFFF indicates NULL
436    if byte_len == 0xFFFF {
437        return Ok(SqlValue::Null);
438    }
439
440    if buf.remaining() < byte_len {
441        return Err(TypeError::BufferTooSmall {
442            needed: byte_len,
443            available: buf.remaining(),
444        });
445    }
446
447    let data = buf.copy_to_bytes(byte_len);
448    Ok(SqlValue::Binary(data))
449}
450
451#[cfg(feature = "uuid")]
452fn decode_guid(buf: &mut Bytes) -> Result<SqlValue, TypeError> {
453    if buf.remaining() < 1 {
454        return Err(TypeError::BufferTooSmall {
455            needed: 1,
456            available: buf.remaining(),
457        });
458    }
459
460    let len = buf.get_u8() as usize;
461    if len == 0 {
462        return Ok(SqlValue::Null);
463    }
464
465    if len != 16 {
466        return Err(TypeError::InvalidBinary(format!(
467            "invalid GUID length: {len}"
468        )));
469    }
470
471    if buf.remaining() < 16 {
472        return Err(TypeError::BufferTooSmall {
473            needed: 16,
474            available: buf.remaining(),
475        });
476    }
477
478    // SQL Server stores UUIDs in mixed-endian format
479    let mut bytes = [0u8; 16];
480
481    // First 4 bytes - little-endian (reverse)
482    bytes[3] = buf.get_u8();
483    bytes[2] = buf.get_u8();
484    bytes[1] = buf.get_u8();
485    bytes[0] = buf.get_u8();
486
487    // Next 2 bytes - little-endian (reverse)
488    bytes[5] = buf.get_u8();
489    bytes[4] = buf.get_u8();
490
491    // Next 2 bytes - little-endian (reverse)
492    bytes[7] = buf.get_u8();
493    bytes[6] = buf.get_u8();
494
495    // Last 8 bytes - big-endian (keep as-is)
496    for byte in &mut bytes[8..16] {
497        *byte = buf.get_u8();
498    }
499
500    Ok(SqlValue::Uuid(uuid::Uuid::from_bytes(bytes)))
501}
502
503#[cfg(not(feature = "uuid"))]
504fn decode_guid(buf: &mut Bytes) -> Result<SqlValue, TypeError> {
505    // Skip the GUID data
506    if buf.remaining() < 1 {
507        return Err(TypeError::BufferTooSmall {
508            needed: 1,
509            available: buf.remaining(),
510        });
511    }
512
513    let len = buf.get_u8() as usize;
514    if len == 0 {
515        return Ok(SqlValue::Null);
516    }
517
518    if buf.remaining() < len {
519        return Err(TypeError::BufferTooSmall {
520            needed: len,
521            available: buf.remaining(),
522        });
523    }
524
525    let data = buf.copy_to_bytes(len);
526    Ok(SqlValue::Binary(data))
527}
528
529#[cfg(feature = "decimal")]
530fn decode_decimal(buf: &mut Bytes, type_info: &TypeInfo) -> Result<SqlValue, TypeError> {
531    use rust_decimal::Decimal;
532
533    if buf.remaining() < 1 {
534        return Err(TypeError::BufferTooSmall {
535            needed: 1,
536            available: buf.remaining(),
537        });
538    }
539
540    let len = buf.get_u8() as usize;
541    if len == 0 {
542        return Ok(SqlValue::Null);
543    }
544
545    if buf.remaining() < len {
546        return Err(TypeError::BufferTooSmall {
547            needed: len,
548            available: buf.remaining(),
549        });
550    }
551
552    // First byte is sign (0 = negative, 1 = positive)
553    let sign = buf.get_u8();
554    let remaining = len - 1;
555
556    // Read mantissa (little-endian)
557    let mut mantissa_bytes = [0u8; 16];
558    for byte in mantissa_bytes.iter_mut().take(remaining.min(16)) {
559        *byte = buf.get_u8();
560    }
561
562    let mantissa = u128::from_le_bytes(mantissa_bytes);
563    let scale = type_info.scale.unwrap_or(0) as u32;
564
565    // rust_decimal holds 96-bit mantissas with scale <= 28; SQL Server
566    // NUMERIC goes to 38 digits, so wire values (legitimate or hostile) can
567    // exceed both limits. Fall back to f64 rather than panic.
568    let decimal = i128::try_from(mantissa)
569        .ok()
570        .and_then(|m| Decimal::try_from_i128_with_scale(m, scale).ok());
571    match decimal {
572        Some(mut decimal) => {
573            if sign == 0 {
574                decimal.set_sign_negative(true);
575            }
576            Ok(SqlValue::Decimal(decimal))
577        }
578        None => {
579            let divisor = 10f64.powi(scale as i32);
580            let value = (mantissa as f64) / divisor;
581            let value = if sign == 0 { -value } else { value };
582            Ok(SqlValue::Double(value))
583        }
584    }
585}
586
587#[cfg(not(feature = "decimal"))]
588fn decode_decimal(buf: &mut Bytes, _type_info: &TypeInfo) -> Result<SqlValue, TypeError> {
589    // Skip decimal data and return as string
590    if buf.remaining() < 1 {
591        return Err(TypeError::BufferTooSmall {
592            needed: 1,
593            available: buf.remaining(),
594        });
595    }
596
597    let len = buf.get_u8() as usize;
598    if len == 0 {
599        return Ok(SqlValue::Null);
600    }
601
602    if buf.remaining() < len {
603        return Err(TypeError::BufferTooSmall {
604            needed: len,
605            available: buf.remaining(),
606        });
607    }
608
609    buf.advance(len);
610    Ok(SqlValue::String("DECIMAL (feature disabled)".to_string()))
611}
612
613#[cfg(feature = "chrono")]
614fn decode_date(buf: &mut Bytes) -> Result<SqlValue, TypeError> {
615    if buf.remaining() < 1 {
616        return Err(TypeError::BufferTooSmall {
617            needed: 1,
618            available: buf.remaining(),
619        });
620    }
621
622    let len = buf.get_u8() as usize;
623    if len == 0 {
624        return Ok(SqlValue::Null);
625    }
626
627    if len != 3 {
628        return Err(TypeError::InvalidDateTime(format!(
629            "invalid DATE length: {len}"
630        )));
631    }
632
633    if buf.remaining() < 3 {
634        return Err(TypeError::BufferTooSmall {
635            needed: 3,
636            available: buf.remaining(),
637        });
638    }
639
640    // 3 bytes little-endian representing days since 0001-01-01
641    let days = buf.get_u8() as u32 | ((buf.get_u8() as u32) << 8) | ((buf.get_u8() as u32) << 16);
642
643    let base = chrono::NaiveDate::from_ymd_opt(1, 1, 1).expect("valid date");
644    let date = base + chrono::Duration::days(days as i64);
645
646    Ok(SqlValue::Date(date))
647}
648
649#[cfg(not(feature = "chrono"))]
650fn decode_date(buf: &mut Bytes) -> Result<SqlValue, TypeError> {
651    if buf.remaining() < 1 {
652        return Err(TypeError::BufferTooSmall {
653            needed: 1,
654            available: buf.remaining(),
655        });
656    }
657
658    let len = buf.get_u8() as usize;
659    if len == 0 {
660        return Ok(SqlValue::Null);
661    }
662
663    if buf.remaining() < len {
664        return Err(TypeError::BufferTooSmall {
665            needed: len,
666            available: buf.remaining(),
667        });
668    }
669
670    buf.advance(len);
671    Ok(SqlValue::String("DATE (feature disabled)".to_string()))
672}
673
674#[cfg(feature = "chrono")]
675fn decode_time(buf: &mut Bytes, type_info: &TypeInfo) -> Result<SqlValue, TypeError> {
676    let scale = type_info.scale.unwrap_or(7);
677    let time_len = time_bytes_for_scale(scale);
678
679    if buf.remaining() < 1 {
680        return Err(TypeError::BufferTooSmall {
681            needed: 1,
682            available: buf.remaining(),
683        });
684    }
685
686    let len = buf.get_u8() as usize;
687    if len == 0 {
688        return Ok(SqlValue::Null);
689    }
690
691    if buf.remaining() < len {
692        return Err(TypeError::BufferTooSmall {
693            needed: len,
694            available: buf.remaining(),
695        });
696    }
697    // Reads below are driven by scale metadata, not by `len`: a short
698    // declared length must be an error, not a panic.
699    if len < time_len {
700        return Err(TypeError::InvalidDateTime(format!(
701            "TIME length {len} too short for scale {scale}"
702        )));
703    }
704
705    // Read time bytes (variable length based on scale)
706    let mut time_bytes = [0u8; 8];
707    for byte in time_bytes.iter_mut().take(time_len) {
708        *byte = buf.get_u8();
709    }
710
711    let intervals = u64::from_le_bytes(time_bytes);
712    let time = intervals_to_time(intervals, scale);
713
714    Ok(SqlValue::Time(time))
715}
716
717#[cfg(not(feature = "chrono"))]
718fn decode_time(buf: &mut Bytes, _type_info: &TypeInfo) -> Result<SqlValue, TypeError> {
719    if buf.remaining() < 1 {
720        return Err(TypeError::BufferTooSmall {
721            needed: 1,
722            available: buf.remaining(),
723        });
724    }
725
726    let len = buf.get_u8() as usize;
727    if len == 0 {
728        return Ok(SqlValue::Null);
729    }
730
731    if buf.remaining() < len {
732        return Err(TypeError::BufferTooSmall {
733            needed: len,
734            available: buf.remaining(),
735        });
736    }
737
738    buf.advance(len);
739    Ok(SqlValue::String("TIME (feature disabled)".to_string()))
740}
741
742#[cfg(feature = "chrono")]
743fn decode_datetime2(buf: &mut Bytes, type_info: &TypeInfo) -> Result<SqlValue, TypeError> {
744    let scale = type_info.scale.unwrap_or(7);
745    let time_len = time_bytes_for_scale(scale);
746
747    if buf.remaining() < 1 {
748        return Err(TypeError::BufferTooSmall {
749            needed: 1,
750            available: buf.remaining(),
751        });
752    }
753
754    let len = buf.get_u8() as usize;
755    if len == 0 {
756        return Ok(SqlValue::Null);
757    }
758
759    if buf.remaining() < len {
760        return Err(TypeError::BufferTooSmall {
761            needed: len,
762            available: buf.remaining(),
763        });
764    }
765    // Reads below are driven by scale metadata, not by `len`: a short
766    // declared length must be an error, not a panic.
767    if len < time_len + 3 {
768        return Err(TypeError::InvalidDateTime(format!(
769            "DATETIME2 length {len} too short for scale {scale}"
770        )));
771    }
772
773    // Decode time
774    let mut time_bytes = [0u8; 8];
775    for byte in time_bytes.iter_mut().take(time_len) {
776        *byte = buf.get_u8();
777    }
778    let intervals = u64::from_le_bytes(time_bytes);
779    let time = intervals_to_time(intervals, scale);
780
781    // Decode date
782    let days = buf.get_u8() as u32 | ((buf.get_u8() as u32) << 8) | ((buf.get_u8() as u32) << 16);
783    let base = chrono::NaiveDate::from_ymd_opt(1, 1, 1).expect("valid date");
784    let date = base + chrono::Duration::days(days as i64);
785
786    Ok(SqlValue::DateTime(date.and_time(time)))
787}
788
789#[cfg(not(feature = "chrono"))]
790fn decode_datetime2(buf: &mut Bytes, _type_info: &TypeInfo) -> Result<SqlValue, TypeError> {
791    if buf.remaining() < 1 {
792        return Err(TypeError::BufferTooSmall {
793            needed: 1,
794            available: buf.remaining(),
795        });
796    }
797
798    let len = buf.get_u8() as usize;
799    if len == 0 {
800        return Ok(SqlValue::Null);
801    }
802
803    if buf.remaining() < len {
804        return Err(TypeError::BufferTooSmall {
805            needed: len,
806            available: buf.remaining(),
807        });
808    }
809
810    buf.advance(len);
811    Ok(SqlValue::String("DATETIME2 (feature disabled)".to_string()))
812}
813
814#[cfg(feature = "chrono")]
815fn decode_datetimeoffset(buf: &mut Bytes, type_info: &TypeInfo) -> Result<SqlValue, TypeError> {
816    use chrono::TimeZone;
817
818    let scale = type_info.scale.unwrap_or(7);
819    let time_len = time_bytes_for_scale(scale);
820
821    if buf.remaining() < 1 {
822        return Err(TypeError::BufferTooSmall {
823            needed: 1,
824            available: buf.remaining(),
825        });
826    }
827
828    let len = buf.get_u8() as usize;
829    if len == 0 {
830        return Ok(SqlValue::Null);
831    }
832
833    if buf.remaining() < len {
834        return Err(TypeError::BufferTooSmall {
835            needed: len,
836            available: buf.remaining(),
837        });
838    }
839    // Reads below are driven by scale metadata, not by `len`: a short
840    // declared length must be an error, not a panic.
841    if len < time_len + 5 {
842        return Err(TypeError::InvalidDateTime(format!(
843            "DATETIMEOFFSET length {len} too short for scale {scale}"
844        )));
845    }
846
847    // Decode time
848    let mut time_bytes = [0u8; 8];
849    for byte in time_bytes.iter_mut().take(time_len) {
850        *byte = buf.get_u8();
851    }
852    let intervals = u64::from_le_bytes(time_bytes);
853    let time = intervals_to_time(intervals, scale);
854
855    // Decode date
856    let days = buf.get_u8() as u32 | ((buf.get_u8() as u32) << 8) | ((buf.get_u8() as u32) << 16);
857    let base = chrono::NaiveDate::from_ymd_opt(1, 1, 1).expect("valid date");
858    let date = base + chrono::Duration::days(days as i64);
859
860    // Decode timezone offset in minutes
861    let offset_minutes = buf.get_i16_le();
862    let offset = chrono::FixedOffset::east_opt((offset_minutes as i32) * 60)
863        .ok_or_else(|| TypeError::InvalidDateTime(format!("invalid offset: {offset_minutes}")))?;
864
865    // The wire date/time portion is UTC per MS-TDS §2.2.5.5.1.9; attach the
866    // offset without shifting the instant.
867    let datetime = offset.from_utc_datetime(&date.and_time(time));
868
869    Ok(SqlValue::DateTimeOffset(datetime))
870}
871
872#[cfg(not(feature = "chrono"))]
873fn decode_datetimeoffset(buf: &mut Bytes, _type_info: &TypeInfo) -> Result<SqlValue, TypeError> {
874    if buf.remaining() < 1 {
875        return Err(TypeError::BufferTooSmall {
876            needed: 1,
877            available: buf.remaining(),
878        });
879    }
880
881    let len = buf.get_u8() as usize;
882    if len == 0 {
883        return Ok(SqlValue::Null);
884    }
885
886    if buf.remaining() < len {
887        return Err(TypeError::BufferTooSmall {
888            needed: len,
889            available: buf.remaining(),
890        });
891    }
892
893    buf.advance(len);
894    Ok(SqlValue::String(
895        "DATETIMEOFFSET (feature disabled)".to_string(),
896    ))
897}
898
899#[cfg(feature = "chrono")]
900fn decode_datetime(buf: &mut Bytes) -> Result<SqlValue, TypeError> {
901    // DATETIME is 8 bytes: 4 bytes days since 1900-01-01 + 4 bytes 300ths of second
902    if buf.remaining() < 8 {
903        return Err(TypeError::BufferTooSmall {
904            needed: 8,
905            available: buf.remaining(),
906        });
907    }
908
909    let days = buf.get_i32_le();
910    let time_300ths = buf.get_u32_le();
911
912    let base = chrono::NaiveDate::from_ymd_opt(1900, 1, 1).expect("valid date");
913    // days comes from the wire: out-of-range is an error, never a panic.
914    let date = base
915        .checked_add_signed(chrono::Duration::days(days as i64))
916        .ok_or_else(|| TypeError::InvalidDateTime(format!("DATETIME days out of range: {days}")))?;
917
918    // Convert 300ths of second to time
919    let total_ms = (time_300ths as u64 * 1000) / 300;
920    let secs = (total_ms / 1000) as u32;
921    let nanos = ((total_ms % 1000) * 1_000_000) as u32;
922
923    let time = chrono::NaiveTime::from_num_seconds_from_midnight_opt(secs, nanos)
924        .ok_or_else(|| TypeError::InvalidDateTime("invalid DATETIME time".to_string()))?;
925
926    Ok(SqlValue::DateTime(date.and_time(time)))
927}
928
929#[cfg(not(feature = "chrono"))]
930fn decode_datetime(buf: &mut Bytes) -> Result<SqlValue, TypeError> {
931    if buf.remaining() < 8 {
932        return Err(TypeError::BufferTooSmall {
933            needed: 8,
934            available: buf.remaining(),
935        });
936    }
937
938    buf.advance(8);
939    Ok(SqlValue::String("DATETIME (feature disabled)".to_string()))
940}
941
942#[cfg(feature = "chrono")]
943fn decode_smalldatetime(buf: &mut Bytes) -> Result<SqlValue, TypeError> {
944    // SMALLDATETIME is 4 bytes: 2 bytes days since 1900-01-01 + 2 bytes minutes
945    if buf.remaining() < 4 {
946        return Err(TypeError::BufferTooSmall {
947            needed: 4,
948            available: buf.remaining(),
949        });
950    }
951
952    let days = buf.get_u16_le();
953    let minutes = buf.get_u16_le();
954
955    let base = chrono::NaiveDate::from_ymd_opt(1900, 1, 1).expect("valid date");
956    let date = base + chrono::Duration::days(days as i64);
957
958    let time = chrono::NaiveTime::from_num_seconds_from_midnight_opt((minutes as u32) * 60, 0)
959        .ok_or_else(|| TypeError::InvalidDateTime("invalid SMALLDATETIME time".to_string()))?;
960
961    Ok(SqlValue::DateTime(date.and_time(time)))
962}
963
964#[cfg(not(feature = "chrono"))]
965fn decode_smalldatetime(buf: &mut Bytes) -> Result<SqlValue, TypeError> {
966    if buf.remaining() < 4 {
967        return Err(TypeError::BufferTooSmall {
968            needed: 4,
969            available: buf.remaining(),
970        });
971    }
972
973    buf.advance(4);
974    Ok(SqlValue::String(
975        "SMALLDATETIME (feature disabled)".to_string(),
976    ))
977}
978
979fn decode_xml(buf: &mut Bytes) -> Result<SqlValue, TypeError> {
980    // XML is sent as UTF-16LE string with length prefix
981    if buf.remaining() < 2 {
982        return Err(TypeError::BufferTooSmall {
983            needed: 2,
984            available: buf.remaining(),
985        });
986    }
987
988    let byte_len = buf.get_u16_le() as usize;
989
990    if byte_len == 0xFFFF {
991        return Ok(SqlValue::Null);
992    }
993
994    if buf.remaining() < byte_len {
995        return Err(TypeError::BufferTooSmall {
996            needed: byte_len,
997            available: buf.remaining(),
998        });
999    }
1000
1001    let utf16_data = buf.copy_to_bytes(byte_len);
1002    let s = decode_utf16_string(&utf16_data)?;
1003    Ok(SqlValue::Xml(s))
1004}
1005
1006/// Decode a UTF-16LE string from bytes.
1007pub fn decode_utf16_string(data: &[u8]) -> Result<String, TypeError> {
1008    if data.len() % 2 != 0 {
1009        return Err(TypeError::InvalidEncoding(
1010            "UTF-16 data must have even length".to_string(),
1011        ));
1012    }
1013
1014    let utf16: Vec<u16> = data
1015        .chunks_exact(2)
1016        .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
1017        .collect();
1018
1019    String::from_utf16(&utf16).map_err(|e| TypeError::InvalidEncoding(e.to_string()))
1020}
1021
1022/// Calculate number of bytes needed for TIME based on scale.
1023#[cfg(feature = "chrono")]
1024fn time_bytes_for_scale(scale: u8) -> usize {
1025    match scale {
1026        0..=2 => 3,
1027        3..=4 => 4,
1028        5..=7 => 5,
1029        _ => 5, // Default to max precision
1030    }
1031}
1032
1033/// Convert 100-nanosecond intervals to NaiveTime.
1034#[cfg(feature = "chrono")]
1035fn intervals_to_time(intervals: u64, scale: u8) -> chrono::NaiveTime {
1036    // Scale determines the unit:
1037    // scale 0: seconds
1038    // scale 1: 100ms
1039    // scale 2: 10ms
1040    // scale 3: 1ms
1041    // scale 4: 100us
1042    // scale 5: 10us
1043    // scale 6: 1us
1044    // scale 7: 100ns
1045
1046    // Saturating: `intervals` comes from the wire, and a hostile value must
1047    // not overflow-panic in debug builds (saturation lands in the
1048    // out-of-range fallback below).
1049    let nanos = match scale {
1050        0 => intervals.saturating_mul(1_000_000_000),
1051        1 => intervals.saturating_mul(100_000_000),
1052        2 => intervals.saturating_mul(10_000_000),
1053        3 => intervals.saturating_mul(1_000_000),
1054        4 => intervals.saturating_mul(100_000),
1055        5 => intervals.saturating_mul(10_000),
1056        6 => intervals.saturating_mul(1_000),
1057        7 => intervals.saturating_mul(100),
1058        _ => intervals.saturating_mul(100),
1059    };
1060
1061    let secs = (nanos / 1_000_000_000) as u32;
1062    let nano_part = (nanos % 1_000_000_000) as u32;
1063
1064    chrono::NaiveTime::from_num_seconds_from_midnight_opt(secs, nano_part)
1065        .unwrap_or_else(|| chrono::NaiveTime::from_hms_opt(0, 0, 0).expect("valid time"))
1066}
1067
1068#[cfg(test)]
1069#[allow(clippy::unwrap_used, clippy::panic)]
1070mod tests {
1071    use super::*;
1072
1073    #[test]
1074    fn test_decode_int() {
1075        let mut buf = Bytes::from_static(&[42, 0, 0, 0]);
1076        let type_info = TypeInfo::int(0x38);
1077        let result = decode_value(&mut buf, &type_info).unwrap();
1078        assert_eq!(result, SqlValue::Int(42));
1079    }
1080
1081    #[cfg(feature = "chrono")]
1082    #[test]
1083    fn hostile_datetime_days_overflow_is_error_not_panic() {
1084        // days=i32::MAX from the wire overflows chrono's date range; must be
1085        // a TypeError, never a panic.
1086        let mut data = Vec::new();
1087        data.extend_from_slice(&i32::MAX.to_le_bytes());
1088        data.extend_from_slice(&0u32.to_le_bytes());
1089        let mut buf = Bytes::from(data);
1090        assert!(decode_datetime(&mut buf).is_err());
1091    }
1092
1093    /// Issue #152 regression: the DATETIMEOFFSET wire date/time portion is
1094    /// the UTC instant per MS-TDS §2.2.5.5.1.9; decoding must attach the
1095    /// offset without shifting it. Wire 10:00 UTC with +02:00 → 12:00+02:00.
1096    /// The previous from_local_datetime decode produced 10:00+02:00 (a
1097    /// different instant), which round-tripped only against our own equally
1098    /// inverted encoder.
1099    #[cfg(feature = "chrono")]
1100    #[test]
1101    fn test_datetimeoffset_decodes_wire_as_utc() {
1102        use chrono::TimeZone;
1103
1104        let mut data = Vec::new();
1105        data.push(10u8); // BYTELEN: 5 time + 3 date + 2 offset
1106        let intervals: u64 = 10 * 3600 * 10_000_000; // 10:00:00 UTC, scale 7
1107        for i in 0..5 {
1108            data.push(((intervals >> (8 * i)) & 0xFF) as u8);
1109        }
1110        let base = chrono::NaiveDate::from_ymd_opt(1, 1, 1).unwrap();
1111        let days = (chrono::NaiveDate::from_ymd_opt(2024, 3, 15).unwrap() - base).num_days() as u32;
1112        data.push((days & 0xFF) as u8);
1113        data.push(((days >> 8) & 0xFF) as u8);
1114        data.push(((days >> 16) & 0xFF) as u8);
1115        data.extend_from_slice(&120i16.to_le_bytes()); // +02:00
1116
1117        let type_info = TypeInfo {
1118            type_id: 0x2B,
1119            length: None,
1120            scale: Some(7),
1121            precision: None,
1122            collation: None,
1123        };
1124        let mut buf = Bytes::from(data);
1125        let value = decode_datetimeoffset(&mut buf, &type_info).unwrap();
1126
1127        let offset = chrono::FixedOffset::east_opt(2 * 3600).unwrap();
1128        let expected = offset.with_ymd_and_hms(2024, 3, 15, 12, 0, 0).unwrap();
1129        match value {
1130            SqlValue::DateTimeOffset(dt) => {
1131                assert_eq!(dt, expected);
1132                assert_eq!(dt.offset().local_minus_utc(), 7200);
1133                assert_eq!(
1134                    dt.naive_utc(),
1135                    chrono::NaiveDate::from_ymd_opt(2024, 3, 15)
1136                        .unwrap()
1137                        .and_hms_opt(10, 0, 0)
1138                        .unwrap()
1139                );
1140            }
1141            other => panic!("expected DateTimeOffset, got {other:?}"),
1142        }
1143    }
1144
1145    #[test]
1146    fn test_decode_utf16_string() {
1147        // "AB" in UTF-16LE
1148        let data = [0x41, 0x00, 0x42, 0x00];
1149        let result = decode_utf16_string(&data).unwrap();
1150        assert_eq!(result, "AB");
1151    }
1152
1153    #[test]
1154    fn test_decode_nvarchar() {
1155        // Length (4 bytes for "AB") + "AB" in UTF-16LE
1156        let mut buf = Bytes::from_static(&[4, 0, 0x41, 0x00, 0x42, 0x00]);
1157        let type_info = TypeInfo::varchar(100);
1158        let type_info = TypeInfo {
1159            type_id: 0xE7,
1160            ..type_info
1161        };
1162        let result = decode_value(&mut buf, &type_info).unwrap();
1163        assert_eq!(result, SqlValue::String("AB".to_string()));
1164    }
1165
1166    #[test]
1167    fn test_decode_null_nvarchar() {
1168        // 0xFFFF indicates NULL
1169        let mut buf = Bytes::from_static(&[0xFF, 0xFF]);
1170        let type_info = TypeInfo {
1171            type_id: 0xE7,
1172            length: Some(100),
1173            scale: None,
1174            precision: None,
1175            collation: None,
1176        };
1177        let result = decode_value(&mut buf, &type_info).unwrap();
1178        assert_eq!(result, SqlValue::Null);
1179    }
1180
1181    // ========================================================================
1182    // Targeted round-trip tests for negative decimals (work-item 1.3)
1183    // ========================================================================
1184
1185    #[cfg(feature = "decimal")]
1186    mod decimal_roundtrip {
1187        use super::*;
1188        use bytes::{BufMut, BytesMut};
1189        use rust_decimal::Decimal;
1190
1191        /// Encode a Decimal, prepend TDS length byte, then decode — verifying round-trip.
1192        fn roundtrip_decimal(value: Decimal, precision: u8, scale: u8) -> Decimal {
1193            // Encode
1194            let mut encode_buf = BytesMut::new();
1195            crate::encode::encode_decimal(value, &mut encode_buf);
1196            let encoded_len = encode_buf.len() as u8; // 17 bytes (1 sign + 16 mantissa)
1197
1198            // Build decode buffer: length prefix + encoded data
1199            let mut decode_buf = BytesMut::with_capacity(1 + encoded_len as usize);
1200            decode_buf.put_u8(encoded_len);
1201            decode_buf.extend_from_slice(&encode_buf);
1202
1203            let mut bytes = decode_buf.freeze();
1204            let type_info = TypeInfo::decimal(precision, scale);
1205            match decode_value(&mut bytes, &type_info).unwrap() {
1206                SqlValue::Decimal(d) => d,
1207                other => panic!("expected Decimal, got {other:?}"),
1208            }
1209        }
1210
1211        #[test]
1212        fn test_negative_decimal_17_80() {
1213            let d = Decimal::new(-1780, 2); // -17.80
1214            let result = roundtrip_decimal(d, 18, 2);
1215            assert_eq!(result, d, "round-trip of -17.80 must be exact");
1216        }
1217
1218        #[test]
1219        fn test_negative_decimal_0_01() {
1220            let d = Decimal::new(-1, 2); // -0.01
1221            let result = roundtrip_decimal(d, 18, 2);
1222            assert_eq!(result, d, "round-trip of -0.01 must be exact");
1223        }
1224
1225        #[test]
1226        fn test_negative_decimal_large() {
1227            let d = Decimal::new(-9999999999, 2); // -99999999.99
1228            let result = roundtrip_decimal(d, 18, 2);
1229            assert_eq!(result, d, "round-trip of -99999999.99 must be exact");
1230        }
1231
1232        #[test]
1233        fn test_positive_decimal() {
1234            let d = Decimal::new(1780, 2); // 17.80
1235            let result = roundtrip_decimal(d, 18, 2);
1236            assert_eq!(result, d, "round-trip of 17.80 must be exact");
1237        }
1238
1239        #[test]
1240        fn test_decimal_zero() {
1241            let d = Decimal::ZERO;
1242            let result = roundtrip_decimal(d, 18, 0);
1243            assert_eq!(result, d, "round-trip of 0 must be exact");
1244        }
1245
1246        #[test]
1247        fn test_decimal_max_precision() {
1248            // Large value that fits in 38-digit precision
1249            let d = Decimal::new(i64::MAX, 0);
1250            let result = roundtrip_decimal(d, 38, 0);
1251            assert_eq!(result, d, "round-trip of large positive must be exact");
1252        }
1253
1254        #[test]
1255        fn test_decimal_min_precision() {
1256            let d = Decimal::new(i64::MIN + 1, 0);
1257            let result = roundtrip_decimal(d, 38, 0);
1258            assert_eq!(result, d, "round-trip of large negative must be exact");
1259        }
1260    }
1261
1262    // ========================================================================
1263    // Date encoding tests (work-item 3.7)
1264    // ========================================================================
1265
1266    #[cfg(feature = "chrono")]
1267    mod date_tests {
1268        use bytes::{BufMut, BytesMut};
1269        use chrono::NaiveDate;
1270
1271        #[test]
1272        fn test_encode_date_pre_1900() {
1273            // This is the scenario where Tiberius panics
1274            let mut buf = BytesMut::new();
1275            let date = NaiveDate::from_ymd_opt(1753, 1, 1).unwrap();
1276            crate::encode::encode_date(date, &mut buf);
1277            assert_eq!(buf.len(), 3, "DATE encoding is always 3 bytes");
1278        }
1279
1280        #[test]
1281        fn test_encode_date_epoch() {
1282            // The DATE epoch: 0001-01-01
1283            let mut buf = BytesMut::new();
1284            let date = NaiveDate::from_ymd_opt(1, 1, 1).unwrap();
1285            crate::encode::encode_date(date, &mut buf);
1286            // Days since 0001-01-01 = 0
1287            assert_eq!(&buf[..], &[0, 0, 0]);
1288        }
1289
1290        #[test]
1291        fn test_encode_date_max() {
1292            // SQL Server max DATE: 9999-12-31
1293            let mut buf = BytesMut::new();
1294            let date = NaiveDate::from_ymd_opt(9999, 12, 31).unwrap();
1295            crate::encode::encode_date(date, &mut buf);
1296            assert_eq!(buf.len(), 3, "DATE encoding is always 3 bytes");
1297            // 3652058 days from 0001-01-01 — fits in 3 bytes (max ~16M)
1298            let days = buf[0] as u32 | ((buf[1] as u32) << 8) | ((buf[2] as u32) << 16);
1299            assert_eq!(days, 3_652_058);
1300        }
1301
1302        #[test]
1303        fn test_decode_datetime_pre_1900() {
1304            // DATETIME uses i32 days from 1900-01-01 epoch.
1305            // 1753-01-01 is ~53690 days before 1900-01-01.
1306            use super::*;
1307
1308            let base = NaiveDate::from_ymd_opt(1900, 1, 1).unwrap();
1309            let target = NaiveDate::from_ymd_opt(1753, 1, 1).unwrap();
1310            let days = target.signed_duration_since(base).num_days() as i32;
1311
1312            // Build DATETIME buffer: i32 days + u32 time_300ths
1313            let mut raw = BytesMut::new();
1314            raw.put_i32_le(days);
1315            raw.put_u32_le(0); // midnight
1316
1317            let mut buf = raw.freeze();
1318            let result = decode_datetime(&mut buf).unwrap();
1319
1320            match result {
1321                SqlValue::DateTime(dt) => {
1322                    assert_eq!(dt.date(), target);
1323                }
1324                other => panic!("expected DateTime, got {other:?}"),
1325            }
1326        }
1327
1328        #[test]
1329        fn test_decode_smalldatetime_1900() {
1330            // SMALLDATETIME: u16 days from 1900-01-01 + u16 minutes
1331            use super::*;
1332
1333            // Day 0, minute 0 = 1900-01-01 00:00:00
1334            let mut raw = BytesMut::new();
1335            raw.put_u16_le(0);
1336            raw.put_u16_le(0);
1337
1338            let mut buf = raw.freeze();
1339            let result = decode_smalldatetime(&mut buf).unwrap();
1340
1341            match result {
1342                SqlValue::DateTime(dt) => {
1343                    assert_eq!(
1344                        dt,
1345                        NaiveDate::from_ymd_opt(1900, 1, 1)
1346                            .unwrap()
1347                            .and_hms_opt(0, 0, 0)
1348                            .unwrap()
1349                    );
1350                }
1351                other => panic!("expected DateTime, got {other:?}"),
1352            }
1353        }
1354    }
1355
1356    // ========================================================================
1357    // Property-based tests (work-item 5.2)
1358    // ========================================================================
1359
1360    #[cfg(feature = "decimal")]
1361    mod proptest_decimal {
1362        use super::*;
1363        use bytes::{BufMut, BytesMut};
1364        use proptest::prelude::*;
1365        use rust_decimal::Decimal;
1366
1367        /// Encode a Decimal, prepend TDS length byte, then decode.
1368        fn roundtrip_decimal(value: Decimal, scale: u8) -> Decimal {
1369            let mut encode_buf = BytesMut::new();
1370            crate::encode::encode_decimal(value, &mut encode_buf);
1371            let encoded_len = encode_buf.len() as u8;
1372
1373            let mut decode_buf = BytesMut::with_capacity(1 + encoded_len as usize);
1374            decode_buf.put_u8(encoded_len);
1375            decode_buf.extend_from_slice(&encode_buf);
1376
1377            let mut bytes = decode_buf.freeze();
1378            let type_info = TypeInfo::decimal(38, scale);
1379            match decode_value(&mut bytes, &type_info).unwrap() {
1380                SqlValue::Decimal(d) => d,
1381                other => panic!("expected Decimal, got {other:?}"),
1382            }
1383        }
1384
1385        proptest! {
1386            #[test]
1387            fn decimal_roundtrip_scale0(mantissa in -999_999_999_999i64..=999_999_999_999i64) {
1388                let d = Decimal::new(mantissa, 0);
1389                let result = roundtrip_decimal(d, 0);
1390                prop_assert_eq!(result, d);
1391            }
1392
1393            #[test]
1394            fn decimal_roundtrip_scale2(mantissa in -999_999_999_999i64..=999_999_999_999i64) {
1395                let d = Decimal::new(mantissa, 2);
1396                let result = roundtrip_decimal(d, 2);
1397                prop_assert_eq!(result, d);
1398            }
1399
1400            #[test]
1401            fn decimal_roundtrip_various_scales(
1402                mantissa in -999_999_999i64..=999_999_999i64,
1403                scale in 0u8..=10u8,
1404            ) {
1405                let d = Decimal::new(mantissa, scale as u32);
1406                let result = roundtrip_decimal(d, scale);
1407                prop_assert_eq!(result, d);
1408            }
1409        }
1410    }
1411
1412    #[cfg(feature = "chrono")]
1413    mod proptest_date {
1414        use bytes::BytesMut;
1415        use chrono::NaiveDate;
1416        use proptest::prelude::*;
1417
1418        proptest! {
1419            #[test]
1420            fn date_encode_never_panics(
1421                year in 1i32..=9999i32,
1422                month in 1u32..=12u32,
1423                day in 1u32..=28u32, // 28 is always valid
1424            ) {
1425                let date = NaiveDate::from_ymd_opt(year, month, day).unwrap();
1426                let mut buf = BytesMut::new();
1427                crate::encode::encode_date(date, &mut buf);
1428                prop_assert_eq!(buf.len(), 3);
1429            }
1430        }
1431    }
1432}