Skip to main content

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