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 uses PLP (Partially Length-Prefixed) encoding.
192    #[must_use]
193    pub const fn is_plp(&self) -> bool {
194        matches!(self, Self::Text | Self::Image | Self::NText | Self::Xml)
195    }
196
197    /// Check if this is a Unicode type.
198    #[must_use]
199    pub const fn is_unicode(&self) -> bool {
200        matches!(self, Self::NChar | Self::NVarChar | Self::NText)
201    }
202
203    /// Check if this is a date/time type.
204    #[must_use]
205    pub const fn is_datetime(&self) -> bool {
206        matches!(
207            self,
208            Self::DateTime
209                | Self::DateTime4
210                | Self::DateTimeN
211                | Self::Date
212                | Self::Time
213                | Self::DateTime2
214                | Self::DateTimeOffset
215        )
216    }
217
218    /// Get the fixed size of this type in bytes, if applicable.
219    #[must_use]
220    pub const fn fixed_size(&self) -> Option<usize> {
221        match self {
222            Self::Null => Some(0),
223            Self::Int1 => Some(1),
224            Self::Bit => Some(1),
225            Self::Int2 => Some(2),
226            Self::Int4 => Some(4),
227            Self::Int8 => Some(8),
228            Self::Float4 => Some(4),
229            Self::Float8 => Some(8),
230            Self::Money => Some(8),
231            Self::Money4 => Some(4),
232            Self::DateTime => Some(8),
233            Self::DateTime4 => Some(4),
234            Self::Date => Some(3),
235            _ => None,
236        }
237    }
238}
239
240/// Column flags from COLMETADATA.
241#[derive(Debug, Clone, Copy, Default)]
242pub struct ColumnFlags {
243    /// Column is nullable.
244    pub nullable: bool,
245    /// Column allows case-sensitive comparison.
246    pub case_sensitive: bool,
247    /// Column is updateable.
248    pub updateable: Updateable,
249    /// Column is an identity column.
250    pub identity: bool,
251    /// Column is computed.
252    pub computed: bool,
253    /// Column has fixed-length CLR type.
254    pub fixed_len_clr_type: bool,
255    /// Column is sparse.
256    pub sparse_column_set: bool,
257    /// Column is encrypted (Always Encrypted).
258    pub encrypted: bool,
259    /// Column is hidden.
260    pub hidden: bool,
261    /// Column is a key column.
262    pub key: bool,
263    /// Column is nullable but unknown at query time.
264    pub nullable_unknown: bool,
265}
266
267/// Update mode for a column.
268#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
269pub enum Updateable {
270    /// Column is read-only.
271    #[default]
272    ReadOnly,
273    /// Column is read-write.
274    ReadWrite,
275    /// Updateability unknown.
276    Unknown,
277}
278
279impl ColumnFlags {
280    /// Parse column flags from the 2-byte flags field.
281    #[must_use]
282    pub fn from_bits(flags: u16) -> Self {
283        Self {
284            nullable: (flags & 0x0001) != 0,
285            case_sensitive: (flags & 0x0002) != 0,
286            updateable: match (flags >> 2) & 0x03 {
287                0 => Updateable::ReadOnly,
288                1 => Updateable::ReadWrite,
289                _ => Updateable::Unknown,
290            },
291            identity: (flags & 0x0010) != 0,
292            computed: (flags & 0x0020) != 0,
293            fixed_len_clr_type: (flags & 0x0100) != 0,
294            sparse_column_set: (flags & 0x0200) != 0,
295            encrypted: (flags & 0x0400) != 0,
296            hidden: (flags & 0x2000) != 0,
297            key: (flags & 0x4000) != 0,
298            nullable_unknown: (flags & 0x8000) != 0,
299        }
300    }
301
302    /// Convert flags back to bits.
303    #[must_use]
304    pub fn to_bits(&self) -> u16 {
305        let mut flags = 0u16;
306        if self.nullable {
307            flags |= 0x0001;
308        }
309        if self.case_sensitive {
310            flags |= 0x0002;
311        }
312        flags |= match self.updateable {
313            Updateable::ReadOnly => 0,
314            Updateable::ReadWrite => 1 << 2,
315            Updateable::Unknown => 2 << 2,
316        };
317        if self.identity {
318            flags |= 0x0010;
319        }
320        if self.computed {
321            flags |= 0x0020;
322        }
323        if self.fixed_len_clr_type {
324            flags |= 0x0100;
325        }
326        if self.sparse_column_set {
327            flags |= 0x0200;
328        }
329        if self.encrypted {
330            flags |= 0x0400;
331        }
332        if self.hidden {
333            flags |= 0x2000;
334        }
335        if self.key {
336            flags |= 0x4000;
337        }
338        if self.nullable_unknown {
339            flags |= 0x8000;
340        }
341        flags
342    }
343}
344
345#[cfg(test)]
346#[allow(clippy::unwrap_used)]
347mod tests {
348    use super::*;
349
350    #[test]
351    fn test_type_id_from_u8() {
352        assert_eq!(TypeId::from_u8(0x38), Some(TypeId::Int4));
353        assert_eq!(TypeId::from_u8(0xE7), Some(TypeId::NVarChar));
354        assert_eq!(TypeId::from_u8(0x99), None);
355    }
356
357    #[test]
358    fn test_fixed_length_detection() {
359        assert!(TypeId::Int4.is_fixed_length());
360        assert!(TypeId::Float8.is_fixed_length());
361        assert!(!TypeId::NVarChar.is_fixed_length());
362    }
363
364    #[test]
365    fn test_column_flags_roundtrip() {
366        let flags = ColumnFlags {
367            nullable: true,
368            identity: true,
369            key: true,
370            ..Default::default()
371        };
372        let bits = flags.to_bits();
373        let restored = ColumnFlags::from_bits(bits);
374        assert_eq!(flags.nullable, restored.nullable);
375        assert_eq!(flags.identity, restored.identity);
376        assert_eq!(flags.key, restored.key);
377    }
378}