tds_protocol/
types.rs

1//! TDS data type definitions.
2//!
3//! This module defines the SQL Server data types as they appear in the TDS protocol.
4
5/// TDS data type identifiers.
6///
7/// These correspond to the type bytes sent in column metadata and parameter definitions.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9#[repr(u8)]
10pub enum TypeId {
11    // Fixed-length types (no length prefix)
12    /// Null type.
13    Null = 0x1F,
14    /// 8-bit signed integer.
15    Int1 = 0x30,
16    /// Bit (boolean).
17    Bit = 0x32,
18    /// 16-bit signed integer.
19    Int2 = 0x34,
20    /// 32-bit signed integer.
21    Int4 = 0x38,
22    /// 64-bit signed integer.
23    Int8 = 0x7F,
24    /// 4-byte datetime.
25    DateTimeN = 0x6F,
26    /// 32-bit floating point.
27    Float4 = 0x3B,
28    /// 64-bit floating point.
29    Float8 = 0x3E,
30    /// 8-byte money.
31    Money = 0x3C,
32    /// 4-byte money.
33    Money4 = 0x7A,
34    /// 4-byte datetime.
35    DateTime = 0x3D,
36    /// 4-byte small datetime.
37    DateTime4 = 0x3A,
38
39    // Variable-length types (with length prefix)
40    /// Variable-length GUID.
41    Guid = 0x24,
42    /// Variable-length integer.
43    IntN = 0x26,
44    /// Variable-length decimal.
45    Decimal = 0x37,
46    /// Variable-length numeric.
47    Numeric = 0x3F,
48    /// Variable-length bit.
49    BitN = 0x68,
50    /// Variable-length decimal (newer).
51    DecimalN = 0x6A,
52    /// Variable-length numeric (newer).
53    NumericN = 0x6C,
54    /// Variable-length float.
55    FloatN = 0x6D,
56    /// Variable-length money.
57    MoneyN = 0x6E,
58
59    // Byte-counted types
60    /// Fixed-length character.
61    Char = 0x2F,
62    /// Variable-length character.
63    VarChar = 0x27,
64    /// Fixed-length binary.
65    Binary = 0x2D,
66    /// Variable-length binary.
67    VarBinary = 0x25,
68
69    // Counted types with 2-byte length
70    /// Large variable-length character.
71    BigVarChar = 0xA7,
72    /// Large variable-length binary.
73    BigVarBinary = 0xA5,
74    /// Large fixed-length character.
75    BigChar = 0xAF,
76    /// Large fixed-length binary.
77    BigBinary = 0xAD,
78
79    // Unicode types
80    /// Fixed-length Unicode character.
81    NChar = 0xEF,
82    /// Variable-length Unicode character.
83    NVarChar = 0xE7,
84
85    // Large object types (PLP - Partially Length-Prefixed)
86    /// Text (deprecated, use varchar(max)).
87    Text = 0x23,
88    /// Image (deprecated, use varbinary(max)).
89    Image = 0x22,
90    /// NText (deprecated, use nvarchar(max)).
91    NText = 0x63,
92
93    // Date/time types (SQL Server 2008+)
94    /// Date (3 bytes).
95    Date = 0x28,
96    /// Time with variable precision.
97    Time = 0x29,
98    /// DateTime2 with variable precision.
99    DateTime2 = 0x2A,
100    /// DateTimeOffset with variable precision.
101    DateTimeOffset = 0x2B,
102
103    // Special types
104    /// SQL Variant.
105    Variant = 0x62,
106    /// User-defined type.
107    Udt = 0xF0,
108    /// XML type.
109    Xml = 0xF1,
110    /// Table-valued parameter.
111    Tvp = 0xF3,
112}
113
114impl TypeId {
115    /// Create a type ID from a raw byte.
116    pub fn from_u8(value: u8) -> Option<Self> {
117        match value {
118            0x1F => Some(Self::Null),
119            0x30 => Some(Self::Int1),
120            0x32 => Some(Self::Bit),
121            0x34 => Some(Self::Int2),
122            0x38 => Some(Self::Int4),
123            0x7F => Some(Self::Int8),
124            0x6F => Some(Self::DateTimeN),
125            0x3B => Some(Self::Float4),
126            0x3E => Some(Self::Float8),
127            0x3C => Some(Self::Money),
128            0x7A => Some(Self::Money4),
129            0x3D => Some(Self::DateTime),
130            0x3A => Some(Self::DateTime4),
131            0x24 => Some(Self::Guid),
132            0x26 => Some(Self::IntN),
133            0x37 => Some(Self::Decimal),
134            0x3F => Some(Self::Numeric),
135            0x68 => Some(Self::BitN),
136            0x6A => Some(Self::DecimalN),
137            0x6C => Some(Self::NumericN),
138            0x6D => Some(Self::FloatN),
139            0x6E => Some(Self::MoneyN),
140            0x2F => Some(Self::Char),
141            0x27 => Some(Self::VarChar),
142            0x2D => Some(Self::Binary),
143            0x25 => Some(Self::VarBinary),
144            0xA7 => Some(Self::BigVarChar),
145            0xA5 => Some(Self::BigVarBinary),
146            0xAF => Some(Self::BigChar),
147            0xAD => Some(Self::BigBinary),
148            0xEF => Some(Self::NChar),
149            0xE7 => Some(Self::NVarChar),
150            0x23 => Some(Self::Text),
151            0x22 => Some(Self::Image),
152            0x63 => Some(Self::NText),
153            0x28 => Some(Self::Date),
154            0x29 => Some(Self::Time),
155            0x2A => Some(Self::DateTime2),
156            0x2B => Some(Self::DateTimeOffset),
157            0x62 => Some(Self::Variant),
158            0xF0 => Some(Self::Udt),
159            0xF1 => Some(Self::Xml),
160            0xF3 => Some(Self::Tvp),
161            _ => None,
162        }
163    }
164
165    /// Check if this is a fixed-length type.
166    #[must_use]
167    pub const fn is_fixed_length(&self) -> bool {
168        matches!(
169            self,
170            Self::Null
171                | Self::Int1
172                | Self::Bit
173                | Self::Int2
174                | Self::Int4
175                | Self::Int8
176                | Self::Float4
177                | Self::Float8
178                | Self::Money
179                | Self::Money4
180                | Self::DateTime
181                | Self::DateTime4
182        )
183    }
184
185    /// Check if this is a variable-length type.
186    #[must_use]
187    pub const fn is_variable_length(&self) -> bool {
188        !self.is_fixed_length()
189    }
190
191    /// Check if this type always uses PLP (Partially Length-Prefixed) encoding.
192    ///
193    /// Note: `NVarChar`, `BigVarChar`, and `BigVarBinary` also use PLP encoding
194    /// when declared as MAX types (max_length == 0xFFFF). This function only
195    /// returns true for types that *always* use PLP.
196    #[must_use]
197    pub const fn is_plp(&self) -> bool {
198        matches!(self, Self::Text | Self::Image | Self::NText | Self::Xml)
199    }
200
201    /// Check if this type can use PLP encoding when declared as MAX.
202    ///
203    /// Returns true for types that use PLP when max_length == 0xFFFF:
204    /// - NVARCHAR(MAX)
205    /// - VARCHAR(MAX)
206    /// - VARBINARY(MAX)
207    #[must_use]
208    pub const fn can_be_max(&self) -> bool {
209        matches!(self, Self::NVarChar | Self::BigVarChar | Self::BigVarBinary)
210    }
211
212    /// Check if this is a Unicode type.
213    #[must_use]
214    pub const fn is_unicode(&self) -> bool {
215        matches!(self, Self::NChar | Self::NVarChar | Self::NText)
216    }
217
218    /// Check if this is a date/time type.
219    #[must_use]
220    pub const fn is_datetime(&self) -> bool {
221        matches!(
222            self,
223            Self::DateTime
224                | Self::DateTime4
225                | Self::DateTimeN
226                | Self::Date
227                | Self::Time
228                | Self::DateTime2
229                | Self::DateTimeOffset
230        )
231    }
232
233    /// Get the fixed size of this type in bytes, if applicable.
234    #[must_use]
235    pub const fn fixed_size(&self) -> Option<usize> {
236        match self {
237            Self::Null => Some(0),
238            Self::Int1 => Some(1),
239            Self::Bit => Some(1),
240            Self::Int2 => Some(2),
241            Self::Int4 => Some(4),
242            Self::Int8 => Some(8),
243            Self::Float4 => Some(4),
244            Self::Float8 => Some(8),
245            Self::Money => Some(8),
246            Self::Money4 => Some(4),
247            Self::DateTime => Some(8),
248            Self::DateTime4 => Some(4),
249            Self::Date => Some(3),
250            _ => None,
251        }
252    }
253}
254
255/// Column flags from COLMETADATA.
256#[derive(Debug, Clone, Copy, Default)]
257pub struct ColumnFlags {
258    /// Column is nullable.
259    pub nullable: bool,
260    /// Column allows case-sensitive comparison.
261    pub case_sensitive: bool,
262    /// Column is updateable.
263    pub updateable: Updateable,
264    /// Column is an identity column.
265    pub identity: bool,
266    /// Column is computed.
267    pub computed: bool,
268    /// Column has fixed-length CLR type.
269    pub fixed_len_clr_type: bool,
270    /// Column is sparse.
271    pub sparse_column_set: bool,
272    /// Column is encrypted (Always Encrypted).
273    pub encrypted: bool,
274    /// Column is hidden.
275    pub hidden: bool,
276    /// Column is a key column.
277    pub key: bool,
278    /// Column is nullable but unknown at query time.
279    pub nullable_unknown: bool,
280}
281
282/// Update mode for a column.
283#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
284pub enum Updateable {
285    /// Column is read-only.
286    #[default]
287    ReadOnly,
288    /// Column is read-write.
289    ReadWrite,
290    /// Updateability unknown.
291    Unknown,
292}
293
294impl ColumnFlags {
295    /// Parse column flags from the 2-byte flags field.
296    #[must_use]
297    pub fn from_bits(flags: u16) -> Self {
298        Self {
299            nullable: (flags & 0x0001) != 0,
300            case_sensitive: (flags & 0x0002) != 0,
301            updateable: match (flags >> 2) & 0x03 {
302                0 => Updateable::ReadOnly,
303                1 => Updateable::ReadWrite,
304                _ => Updateable::Unknown,
305            },
306            identity: (flags & 0x0010) != 0,
307            computed: (flags & 0x0020) != 0,
308            fixed_len_clr_type: (flags & 0x0100) != 0,
309            sparse_column_set: (flags & 0x0200) != 0,
310            encrypted: (flags & 0x0400) != 0,
311            hidden: (flags & 0x2000) != 0,
312            key: (flags & 0x4000) != 0,
313            nullable_unknown: (flags & 0x8000) != 0,
314        }
315    }
316
317    /// Convert flags back to bits.
318    #[must_use]
319    pub fn to_bits(&self) -> u16 {
320        let mut flags = 0u16;
321        if self.nullable {
322            flags |= 0x0001;
323        }
324        if self.case_sensitive {
325            flags |= 0x0002;
326        }
327        flags |= match self.updateable {
328            Updateable::ReadOnly => 0,
329            Updateable::ReadWrite => 1 << 2,
330            Updateable::Unknown => 2 << 2,
331        };
332        if self.identity {
333            flags |= 0x0010;
334        }
335        if self.computed {
336            flags |= 0x0020;
337        }
338        if self.fixed_len_clr_type {
339            flags |= 0x0100;
340        }
341        if self.sparse_column_set {
342            flags |= 0x0200;
343        }
344        if self.encrypted {
345            flags |= 0x0400;
346        }
347        if self.hidden {
348            flags |= 0x2000;
349        }
350        if self.key {
351            flags |= 0x4000;
352        }
353        if self.nullable_unknown {
354            flags |= 0x8000;
355        }
356        flags
357    }
358}
359
360#[cfg(test)]
361#[allow(clippy::unwrap_used)]
362mod tests {
363    use super::*;
364
365    #[test]
366    fn test_type_id_from_u8() {
367        assert_eq!(TypeId::from_u8(0x38), Some(TypeId::Int4));
368        assert_eq!(TypeId::from_u8(0xE7), Some(TypeId::NVarChar));
369        assert_eq!(TypeId::from_u8(0x99), None);
370    }
371
372    #[test]
373    fn test_fixed_length_detection() {
374        assert!(TypeId::Int4.is_fixed_length());
375        assert!(TypeId::Float8.is_fixed_length());
376        assert!(!TypeId::NVarChar.is_fixed_length());
377    }
378
379    #[test]
380    fn test_column_flags_roundtrip() {
381        let flags = ColumnFlags {
382            nullable: true,
383            identity: true,
384            key: true,
385            ..Default::default()
386        };
387        let bits = flags.to_bits();
388        let restored = ColumnFlags::from_bits(bits);
389        assert_eq!(flags.nullable, restored.nullable);
390        assert_eq!(flags.identity, restored.identity);
391        assert_eq!(flags.key, restored.key);
392    }
393}