tds_protocol/
version.rs

1//! TDS protocol version definitions.
2
3use core::fmt;
4
5/// TDS protocol version.
6///
7/// Represents the version of the TDS protocol used for communication
8/// with SQL Server.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
10pub struct TdsVersion(u32);
11
12impl TdsVersion {
13    /// TDS 7.0 (SQL Server 7.0)
14    pub const V7_0: Self = Self(0x70000000);
15
16    /// TDS 7.1 (SQL Server 2000)
17    pub const V7_1: Self = Self(0x71000000);
18
19    /// TDS 7.1 Revision 1 (SQL Server 2000 SP1)
20    pub const V7_1_REV1: Self = Self(0x71000001);
21
22    /// TDS 7.2 (SQL Server 2005)
23    pub const V7_2: Self = Self(0x72090002);
24
25    /// TDS 7.3A (SQL Server 2008)
26    pub const V7_3A: Self = Self(0x730A0003);
27
28    /// TDS 7.3B (SQL Server 2008 R2)
29    pub const V7_3B: Self = Self(0x730B0003);
30
31    /// TDS 7.4 (SQL Server 2012+)
32    pub const V7_4: Self = Self(0x74000004);
33
34    /// TDS 8.0 (SQL Server 2022+ strict encryption mode)
35    pub const V8_0: Self = Self(0x08000000);
36
37    /// Create a new TDS version from raw bytes.
38    #[must_use]
39    pub const fn new(version: u32) -> Self {
40        Self(version)
41    }
42
43    /// Get the raw version value.
44    #[must_use]
45    pub const fn raw(self) -> u32 {
46        self.0
47    }
48
49    /// Check if this version supports TDS 8.0 strict encryption.
50    #[must_use]
51    pub const fn is_tds_8(self) -> bool {
52        // TDS 8.0 uses a different version format
53        self.0 == Self::V8_0.0
54    }
55
56    /// Check if this version requires pre-login encryption negotiation.
57    ///
58    /// TDS 7.x versions negotiate encryption during pre-login.
59    /// TDS 8.0 requires TLS before any TDS traffic.
60    #[must_use]
61    pub const fn requires_prelogin_encryption_negotiation(self) -> bool {
62        !self.is_tds_8()
63    }
64
65    /// Check if this version is TDS 7.3 (SQL Server 2008/2008 R2).
66    ///
67    /// Returns true for both TDS 7.3A (SQL Server 2008) and TDS 7.3B (SQL Server 2008 R2).
68    #[must_use]
69    pub const fn is_tds_7_3(self) -> bool {
70        self.0 == Self::V7_3A.0 || self.0 == Self::V7_3B.0
71    }
72
73    /// Check if this version is TDS 7.4 (SQL Server 2012+).
74    #[must_use]
75    pub const fn is_tds_7_4(self) -> bool {
76        self.0 == Self::V7_4.0
77    }
78
79    /// Check if this version supports DATE, TIME, DATETIME2, and DATETIMEOFFSET types.
80    ///
81    /// These types were introduced in TDS 7.3 (SQL Server 2008).
82    /// Returns true for TDS 7.3+, TDS 7.4, and TDS 8.0.
83    #[must_use]
84    pub const fn supports_date_time_types(self) -> bool {
85        // TDS 7.3A is 0x730A0003, TDS 7.4 is 0x74000004, TDS 8.0 is 0x08000000
86        // Due to TDS 8.0's different encoding, we check explicitly
87        self.is_tds_8() || self.0 >= Self::V7_3A.0
88    }
89
90    /// Check if this version supports session recovery (connection resiliency).
91    ///
92    /// Session recovery was introduced in TDS 7.4 (SQL Server 2012).
93    #[must_use]
94    pub const fn supports_session_recovery(self) -> bool {
95        self.is_tds_8() || self.0 >= Self::V7_4.0
96    }
97
98    /// Check if this version supports column encryption (Always Encrypted).
99    ///
100    /// Column encryption was introduced in SQL Server 2016 (still TDS 7.4).
101    /// This checks protocol capability, not SQL Server version.
102    #[must_use]
103    pub const fn supports_column_encryption(self) -> bool {
104        // Column encryption is a feature extension available in TDS 7.4+
105        self.is_tds_8() || self.0 >= Self::V7_4.0
106    }
107
108    /// Check if this version supports UTF-8 (introduced in SQL Server 2019).
109    #[must_use]
110    pub const fn supports_utf8(self) -> bool {
111        self.is_tds_8() || self.0 >= Self::V7_4.0
112    }
113
114    /// Check if this is a legacy version (TDS 7.2 or earlier).
115    ///
116    /// Legacy versions (SQL Server 2005 and earlier) have different behaviors
117    /// for some protocol aspects. This driver's minimum supported version is
118    /// TDS 7.3 for full functionality.
119    #[must_use]
120    pub const fn is_legacy(self) -> bool {
121        // V7_2 is 0x72090002, anything less than V7_3A is legacy
122        !self.is_tds_8() && self.0 < Self::V7_3A.0
123    }
124
125    /// Get the minimum version between this version and another.
126    ///
127    /// Useful for version negotiation where the client and server
128    /// agree on the lowest common version.
129    ///
130    /// Note: TDS 8.0 uses a different encoding (0x08000000) which is numerically
131    /// lower than TDS 7.x versions, but semantically higher. This method handles
132    /// that special case correctly.
133    #[must_use]
134    pub const fn min(self, other: Self) -> Self {
135        // Special handling for TDS 8.0 which has a different encoding
136        // TDS 8.0 (0x08000000) is numerically lower but semantically higher than TDS 7.x
137        if self.is_tds_8() && !other.is_tds_8() {
138            // self is TDS 8.0, other is TDS 7.x - return TDS 7.x as the "lower" version
139            other
140        } else if !self.is_tds_8() && other.is_tds_8() {
141            // self is TDS 7.x, other is TDS 8.0 - return TDS 7.x as the "lower" version
142            self
143        } else if self.0 <= other.0 {
144            // Both are same type (both 7.x or both 8.0), compare numerically
145            self
146        } else {
147            other
148        }
149    }
150
151    /// Get the SQL Server version name for this TDS version.
152    ///
153    /// Returns a human-readable string describing the SQL Server version
154    /// that corresponds to this TDS protocol version.
155    #[must_use]
156    pub const fn sql_server_version_name(&self) -> &'static str {
157        match self.0 {
158            0x70000000 => "SQL Server 7.0",
159            0x71000000 | 0x71000001 => "SQL Server 2000",
160            0x72090002 => "SQL Server 2005",
161            0x730A0003 => "SQL Server 2008",
162            0x730B0003 => "SQL Server 2008 R2",
163            0x74000004 => "SQL Server 2012+",
164            0x08000000 => "SQL Server 2022+ (strict mode)",
165            _ => "Unknown SQL Server version",
166        }
167    }
168
169    /// Parse a TDS version from a string representation.
170    ///
171    /// Accepts formats like:
172    /// - "7.3", "7.3A", "7.3a", "7.3B", "7.3b" for TDS 7.3
173    /// - "7.4" for TDS 7.4
174    /// - "8.0", "8" for TDS 8.0
175    ///
176    /// Returns None if the string cannot be parsed.
177    #[must_use]
178    pub fn parse(s: &str) -> Option<Self> {
179        let s = s.trim().to_lowercase();
180        match s.as_str() {
181            "7.0" => Some(Self::V7_0),
182            "7.1" => Some(Self::V7_1),
183            "7.2" => Some(Self::V7_2),
184            "7.3" | "7.3a" => Some(Self::V7_3A),
185            "7.3b" => Some(Self::V7_3B),
186            "7.4" => Some(Self::V7_4),
187            "8.0" | "8" => Some(Self::V8_0),
188            _ => None,
189        }
190    }
191
192    /// Get the major version number.
193    ///
194    /// Returns 7 for TDS 7.x versions, 8 for TDS 8.0.
195    ///
196    /// Note: This extracts the major version from the wire format. All TDS 7.x
197    /// versions return 7, and TDS 8.0 returns 8.
198    #[must_use]
199    pub const fn major(self) -> u8 {
200        if self.is_tds_8() {
201            8
202        } else {
203            // TDS 7.x versions encode major version in high nibble of first byte
204            // 0x7X... where X encodes the sub-version (0, 1, 2, 3, 4)
205            7
206        }
207    }
208
209    /// Get the minor version number.
210    ///
211    /// Returns the TDS sub-version: 0, 1, 2, 3, or 4 for TDS 7.x, and 0 for TDS 8.0.
212    ///
213    /// Note: The wire format uses different encoding for different versions.
214    /// This method extracts the logical minor version (e.g., 3 for TDS 7.3).
215    #[must_use]
216    pub const fn minor(self) -> u8 {
217        match self.0 {
218            0x70000000 => 0,              // TDS 7.0
219            0x71000000 | 0x71000001 => 1, // TDS 7.1, 7.1 Rev 1
220            0x72090002 => 2,              // TDS 7.2
221            0x730A0003 | 0x730B0003 => 3, // TDS 7.3A, 7.3B
222            0x74000004 => 4,              // TDS 7.4
223            0x08000000 => 0,              // TDS 8.0
224            _ => {
225                // For unknown versions, extract from first byte's low nibble
226                // This is a best-effort fallback
227                ((self.0 >> 24) & 0x0F) as u8
228            }
229        }
230    }
231
232    /// Get the revision suffix for TDS 7.3 versions.
233    ///
234    /// Returns Some('A') for TDS 7.3A (SQL Server 2008),
235    /// Some('B') for TDS 7.3B (SQL Server 2008 R2),
236    /// and None for all other versions.
237    #[must_use]
238    pub const fn revision_suffix(self) -> Option<char> {
239        match self.0 {
240            0x730A0003 => Some('A'),
241            0x730B0003 => Some('B'),
242            _ => None,
243        }
244    }
245}
246
247impl Default for TdsVersion {
248    fn default() -> Self {
249        Self::V7_4
250    }
251}
252
253impl fmt::Display for TdsVersion {
254    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255        if self.is_tds_8() {
256            write!(f, "TDS 8.0")
257        } else if let Some(suffix) = self.revision_suffix() {
258            // TDS 7.3A or 7.3B
259            write!(f, "TDS {}.{}{}", self.major(), self.minor(), suffix)
260        } else {
261            write!(f, "TDS {}.{}", self.major(), self.minor())
262        }
263    }
264}
265
266impl From<u32> for TdsVersion {
267    fn from(value: u32) -> Self {
268        Self(value)
269    }
270}
271
272impl From<TdsVersion> for u32 {
273    fn from(version: TdsVersion) -> Self {
274        version.0
275    }
276}
277
278#[cfg(test)]
279#[allow(clippy::unwrap_used)]
280mod tests {
281    use super::*;
282
283    #[test]
284    fn test_version_comparison() {
285        assert!(TdsVersion::V7_4 > TdsVersion::V7_3B);
286        assert!(TdsVersion::V7_3B > TdsVersion::V7_3A);
287        assert!(TdsVersion::V7_3A > TdsVersion::V7_2);
288    }
289
290    #[test]
291    fn test_tds_8_detection() {
292        assert!(TdsVersion::V8_0.is_tds_8());
293        assert!(!TdsVersion::V7_4.is_tds_8());
294        assert!(!TdsVersion::V7_3A.is_tds_8());
295    }
296
297    #[test]
298    fn test_prelogin_requirement() {
299        assert!(TdsVersion::V7_4.requires_prelogin_encryption_negotiation());
300        assert!(TdsVersion::V7_3A.requires_prelogin_encryption_negotiation());
301        assert!(TdsVersion::V7_3B.requires_prelogin_encryption_negotiation());
302        assert!(!TdsVersion::V8_0.requires_prelogin_encryption_negotiation());
303    }
304
305    #[test]
306    fn test_is_tds_7_3() {
307        assert!(TdsVersion::V7_3A.is_tds_7_3());
308        assert!(TdsVersion::V7_3B.is_tds_7_3());
309        assert!(!TdsVersion::V7_4.is_tds_7_3());
310        assert!(!TdsVersion::V7_2.is_tds_7_3());
311        assert!(!TdsVersion::V8_0.is_tds_7_3());
312    }
313
314    #[test]
315    fn test_is_tds_7_4() {
316        assert!(TdsVersion::V7_4.is_tds_7_4());
317        assert!(!TdsVersion::V7_3A.is_tds_7_4());
318        assert!(!TdsVersion::V7_3B.is_tds_7_4());
319        assert!(!TdsVersion::V8_0.is_tds_7_4());
320    }
321
322    #[test]
323    fn test_supports_date_time_types() {
324        // TDS 7.3+ supports DATE, TIME, DATETIME2, DATETIMEOFFSET
325        assert!(TdsVersion::V7_3A.supports_date_time_types());
326        assert!(TdsVersion::V7_3B.supports_date_time_types());
327        assert!(TdsVersion::V7_4.supports_date_time_types());
328        assert!(TdsVersion::V8_0.supports_date_time_types());
329        // TDS 7.2 and earlier don't support these types
330        assert!(!TdsVersion::V7_2.supports_date_time_types());
331        assert!(!TdsVersion::V7_1.supports_date_time_types());
332    }
333
334    #[test]
335    fn test_supports_session_recovery() {
336        // Session recovery was introduced in TDS 7.4
337        assert!(TdsVersion::V7_4.supports_session_recovery());
338        assert!(TdsVersion::V8_0.supports_session_recovery());
339        assert!(!TdsVersion::V7_3A.supports_session_recovery());
340        assert!(!TdsVersion::V7_3B.supports_session_recovery());
341    }
342
343    #[test]
344    fn test_is_legacy() {
345        assert!(TdsVersion::V7_2.is_legacy());
346        assert!(TdsVersion::V7_1.is_legacy());
347        assert!(TdsVersion::V7_0.is_legacy());
348        assert!(!TdsVersion::V7_3A.is_legacy());
349        assert!(!TdsVersion::V7_3B.is_legacy());
350        assert!(!TdsVersion::V7_4.is_legacy());
351        assert!(!TdsVersion::V8_0.is_legacy());
352    }
353
354    #[test]
355    fn test_min_version() {
356        assert_eq!(TdsVersion::V7_4.min(TdsVersion::V7_3A), TdsVersion::V7_3A);
357        assert_eq!(TdsVersion::V7_3A.min(TdsVersion::V7_4), TdsVersion::V7_3A);
358        assert_eq!(TdsVersion::V7_3A.min(TdsVersion::V7_3B), TdsVersion::V7_3A);
359        // TDS 8.0 has special handling
360        assert_eq!(TdsVersion::V8_0.min(TdsVersion::V7_4), TdsVersion::V7_4);
361        assert_eq!(TdsVersion::V7_4.min(TdsVersion::V8_0), TdsVersion::V7_4);
362    }
363
364    #[test]
365    fn test_sql_server_version_name() {
366        assert_eq!(
367            TdsVersion::V7_3A.sql_server_version_name(),
368            "SQL Server 2008"
369        );
370        assert_eq!(
371            TdsVersion::V7_3B.sql_server_version_name(),
372            "SQL Server 2008 R2"
373        );
374        assert_eq!(
375            TdsVersion::V7_4.sql_server_version_name(),
376            "SQL Server 2012+"
377        );
378        assert_eq!(
379            TdsVersion::V8_0.sql_server_version_name(),
380            "SQL Server 2022+ (strict mode)"
381        );
382    }
383
384    #[test]
385    fn test_parse() {
386        assert_eq!(TdsVersion::parse("7.3"), Some(TdsVersion::V7_3A));
387        assert_eq!(TdsVersion::parse("7.3a"), Some(TdsVersion::V7_3A));
388        assert_eq!(TdsVersion::parse("7.3A"), Some(TdsVersion::V7_3A));
389        assert_eq!(TdsVersion::parse("7.3b"), Some(TdsVersion::V7_3B));
390        assert_eq!(TdsVersion::parse("7.3B"), Some(TdsVersion::V7_3B));
391        assert_eq!(TdsVersion::parse("7.4"), Some(TdsVersion::V7_4));
392        assert_eq!(TdsVersion::parse("8.0"), Some(TdsVersion::V8_0));
393        assert_eq!(TdsVersion::parse("8"), Some(TdsVersion::V8_0));
394        assert_eq!(TdsVersion::parse(" 7.4 "), Some(TdsVersion::V7_4)); // Whitespace handling
395        assert_eq!(TdsVersion::parse("invalid"), None);
396        assert_eq!(TdsVersion::parse("9.0"), None);
397    }
398
399    #[test]
400    fn test_display() {
401        assert_eq!(format!("{}", TdsVersion::V7_0), "TDS 7.0");
402        assert_eq!(format!("{}", TdsVersion::V7_1), "TDS 7.1");
403        assert_eq!(format!("{}", TdsVersion::V7_2), "TDS 7.2");
404        assert_eq!(format!("{}", TdsVersion::V7_3A), "TDS 7.3A");
405        assert_eq!(format!("{}", TdsVersion::V7_3B), "TDS 7.3B");
406        assert_eq!(format!("{}", TdsVersion::V7_4), "TDS 7.4");
407        assert_eq!(format!("{}", TdsVersion::V8_0), "TDS 8.0");
408    }
409
410    #[test]
411    fn test_major_minor() {
412        // All TDS 7.x versions have major = 7
413        assert_eq!(TdsVersion::V7_0.major(), 7);
414        assert_eq!(TdsVersion::V7_1.major(), 7);
415        assert_eq!(TdsVersion::V7_2.major(), 7);
416        assert_eq!(TdsVersion::V7_3A.major(), 7);
417        assert_eq!(TdsVersion::V7_3B.major(), 7);
418        assert_eq!(TdsVersion::V7_4.major(), 7);
419        assert_eq!(TdsVersion::V8_0.major(), 8);
420
421        // Minor version extracts the logical sub-version
422        assert_eq!(TdsVersion::V7_0.minor(), 0);
423        assert_eq!(TdsVersion::V7_1.minor(), 1);
424        assert_eq!(TdsVersion::V7_2.minor(), 2);
425        assert_eq!(TdsVersion::V7_3A.minor(), 3);
426        assert_eq!(TdsVersion::V7_3B.minor(), 3);
427        assert_eq!(TdsVersion::V7_4.minor(), 4);
428        assert_eq!(TdsVersion::V8_0.minor(), 0);
429    }
430
431    #[test]
432    fn test_revision_suffix() {
433        assert_eq!(TdsVersion::V7_0.revision_suffix(), None);
434        assert_eq!(TdsVersion::V7_1.revision_suffix(), None);
435        assert_eq!(TdsVersion::V7_2.revision_suffix(), None);
436        assert_eq!(TdsVersion::V7_3A.revision_suffix(), Some('A'));
437        assert_eq!(TdsVersion::V7_3B.revision_suffix(), Some('B'));
438        assert_eq!(TdsVersion::V7_4.revision_suffix(), None);
439        assert_eq!(TdsVersion::V8_0.revision_suffix(), None);
440    }
441}