1use arrow_schema::Schema;
4
5use crate::schema::{PlannedSchema, plan_arrow_schema_to_mssql_schema};
6use crate::write::PlanOptions;
7use crate::{PlanOutcome, Result};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11#[non_exhaustive]
12pub enum MssqlVersion {
13 SqlServer2016,
15 SqlServer2017,
17 SqlServer2019,
19 SqlServer2022,
21 SqlServer2025,
23}
24
25impl MssqlVersion {
26 pub const fn default_compatibility_level(self) -> CompatibilityLevel {
29 match self {
30 Self::SqlServer2016 => CompatibilityLevel::SQL_SERVER_2016,
31 Self::SqlServer2017 => CompatibilityLevel::SQL_SERVER_2017,
32 Self::SqlServer2019 => CompatibilityLevel::SQL_SERVER_2019,
33 Self::SqlServer2022 => CompatibilityLevel::SQL_SERVER_2022,
34 Self::SqlServer2025 => CompatibilityLevel::SQL_SERVER_2025,
35 }
36 }
37
38 pub const fn supports_compatibility_level(
41 self,
42 compatibility_level: CompatibilityLevel,
43 ) -> bool {
44 match self {
45 Self::SqlServer2016 => compatibility_level.as_u16() <= 130,
46 Self::SqlServer2017 => compatibility_level.as_u16() <= 140,
47 Self::SqlServer2019 => compatibility_level.as_u16() <= 150,
48 Self::SqlServer2022 => compatibility_level.as_u16() <= 160,
49 Self::SqlServer2025 => compatibility_level.as_u16() <= 170,
50 }
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
56pub struct CompatibilityLevel(u16);
57
58impl CompatibilityLevel {
59 pub const SQL_SERVER_2008: Self = Self(100);
61 pub const SQL_SERVER_2012: Self = Self(110);
63 pub const SQL_SERVER_2014: Self = Self(120);
65 pub const SQL_SERVER_2016: Self = Self(130);
67 pub const SQL_SERVER_2017: Self = Self(140);
69 pub const SQL_SERVER_2019: Self = Self(150);
71 pub const SQL_SERVER_2022: Self = Self(160);
73 pub const SQL_SERVER_2025: Self = Self(170);
75
76 pub fn new(level: u16) -> Result<Self> {
78 if !Self::is_supported(level) {
79 return Err(crate::Error::InvalidCompatibilityLevel { level });
80 }
81
82 Ok(Self(level))
83 }
84
85 pub const fn as_u16(self) -> u16 {
87 self.0
88 }
89
90 const fn is_supported(level: u16) -> bool {
91 matches!(level, 100 | 110 | 120 | 130 | 140 | 150 | 160 | 170)
92 }
93}
94
95impl TryFrom<u16> for CompatibilityLevel {
96 type Error = crate::Error;
97
98 fn try_from(value: u16) -> Result<Self> {
99 Self::new(value)
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
105pub struct MssqlProfile {
106 version: MssqlVersion,
107 compatibility_level: CompatibilityLevel,
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
126#[allow(dead_code)]
127pub(crate) enum DateTimeRounding {
128 LegacyPre130,
135 Compat130Plus,
141}
142
143impl MssqlProfile {
144 pub fn new(version: MssqlVersion, compatibility_level: CompatibilityLevel) -> Result<Self> {
147 if !version.supports_compatibility_level(compatibility_level) {
148 return Err(crate::Error::UnsupportedCompatibilityLevel {
149 version,
150 compatibility_level: compatibility_level.as_u16(),
151 });
152 }
153
154 Ok(Self {
155 version,
156 compatibility_level,
157 })
158 }
159
160 pub const fn sql_server_2016_compat_100() -> Self {
163 Self {
164 version: MssqlVersion::SqlServer2016,
165 compatibility_level: CompatibilityLevel::SQL_SERVER_2008,
166 }
167 }
168
169 pub const fn sql_server_2017_compat_100() -> Self {
172 Self {
173 version: MssqlVersion::SqlServer2017,
174 compatibility_level: CompatibilityLevel::SQL_SERVER_2008,
175 }
176 }
177
178 pub const fn sql_server_2017_compat_110() -> Self {
181 Self {
182 version: MssqlVersion::SqlServer2017,
183 compatibility_level: CompatibilityLevel::SQL_SERVER_2012,
184 }
185 }
186
187 pub const fn sql_server_2017_compat_120() -> Self {
190 Self {
191 version: MssqlVersion::SqlServer2017,
192 compatibility_level: CompatibilityLevel::SQL_SERVER_2014,
193 }
194 }
195
196 pub const fn sql_server_2017_compat_130() -> Self {
199 Self {
200 version: MssqlVersion::SqlServer2017,
201 compatibility_level: CompatibilityLevel::SQL_SERVER_2016,
202 }
203 }
204
205 pub const fn sql_server_2017_compat_140() -> Self {
208 Self {
209 version: MssqlVersion::SqlServer2017,
210 compatibility_level: CompatibilityLevel::SQL_SERVER_2017,
211 }
212 }
213
214 pub fn plan_arrow_schema(
216 self,
217 schema: impl AsRef<Schema>,
218 options: PlanOptions,
219 ) -> Result<PlanOutcome<PlannedSchema>> {
220 plan_arrow_schema_to_mssql_schema(schema, self, options)
221 }
222
223 pub const fn version(self) -> MssqlVersion {
225 self.version
226 }
227
228 pub const fn compatibility_level(self) -> CompatibilityLevel {
230 self.compatibility_level
231 }
232
233 #[allow(dead_code)]
238 pub(crate) const fn datetime_rounding(self) -> DateTimeRounding {
239 if self.compatibility_level.as_u16() < 130 {
240 DateTimeRounding::LegacyPre130
241 } else {
242 DateTimeRounding::Compat130Plus
243 }
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::{CompatibilityLevel, DateTimeRounding, MssqlProfile, MssqlVersion};
250
251 #[test]
252 fn constructs_sql_server_2016_compat_100_profile() {
253 let profile = MssqlProfile::sql_server_2016_compat_100();
254
255 assert_eq!(profile.version(), MssqlVersion::SqlServer2016);
256 assert_eq!(profile.compatibility_level().as_u16(), 100);
257 }
258
259 #[test]
260 fn constructs_sql_server_2017_profiles() {
261 let cases = [
262 (MssqlProfile::sql_server_2017_compat_100(), 100),
263 (MssqlProfile::sql_server_2017_compat_110(), 110),
264 (MssqlProfile::sql_server_2017_compat_120(), 120),
265 (MssqlProfile::sql_server_2017_compat_130(), 130),
266 (MssqlProfile::sql_server_2017_compat_140(), 140),
267 ];
268
269 for (profile, compatibility_level) in cases {
270 assert_eq!(profile.version(), MssqlVersion::SqlServer2017);
271 assert_eq!(profile.compatibility_level().as_u16(), compatibility_level);
272 }
273 }
274
275 #[test]
276 fn reports_default_compatibility_level_by_version() {
277 let cases = [
278 (MssqlVersion::SqlServer2016, 130),
279 (MssqlVersion::SqlServer2017, 140),
280 (MssqlVersion::SqlServer2019, 150),
281 (MssqlVersion::SqlServer2022, 160),
282 (MssqlVersion::SqlServer2025, 170),
283 ];
284
285 for (version, compatibility_level) in cases {
286 assert_eq!(
287 version.default_compatibility_level().as_u16(),
288 compatibility_level
289 );
290 }
291 }
292
293 #[test]
294 fn creates_profiles_for_supported_version_compatibility_pairs() {
295 let cases: &[(MssqlVersion, &[u16])] = &[
296 (MssqlVersion::SqlServer2016, &[100, 110, 120, 130]),
297 (MssqlVersion::SqlServer2017, &[100, 110, 120, 130, 140]),
298 (MssqlVersion::SqlServer2019, &[100, 110, 120, 130, 140, 150]),
299 (
300 MssqlVersion::SqlServer2022,
301 &[100, 110, 120, 130, 140, 150, 160],
302 ),
303 (
304 MssqlVersion::SqlServer2025,
305 &[100, 110, 120, 130, 140, 150, 160, 170],
306 ),
307 ];
308
309 for (version, compatibility_levels) in cases {
310 for compatibility_level in *compatibility_levels {
311 let compatibility_level = CompatibilityLevel::new(*compatibility_level).unwrap();
312 let profile = MssqlProfile::new(*version, compatibility_level).unwrap();
313
314 assert_eq!(profile.version(), *version);
315 assert_eq!(profile.compatibility_level(), compatibility_level);
316 }
317 }
318 }
319
320 #[test]
321 fn rejects_version_compatibility_pairs_not_supported_by_sql_server() {
322 let cases = [
323 (MssqlVersion::SqlServer2016, 140),
324 (MssqlVersion::SqlServer2017, 150),
325 (MssqlVersion::SqlServer2019, 160),
326 (MssqlVersion::SqlServer2022, 170),
327 ];
328
329 for (version, compatibility_level) in cases {
330 let level = CompatibilityLevel::new(compatibility_level).unwrap();
331 let err = MssqlProfile::new(version, level).expect_err("profile should be rejected");
332
333 assert!(
334 err.to_string().contains(&format!(
335 "does not support database compatibility level {compatibility_level}"
336 )),
337 "unexpected error: {err}"
338 );
339 }
340 }
341
342 #[test]
343 fn accepts_supported_compatibility_levels() {
344 let cases = [
345 (100, CompatibilityLevel::SQL_SERVER_2008),
346 (110, CompatibilityLevel::SQL_SERVER_2012),
347 (120, CompatibilityLevel::SQL_SERVER_2014),
348 (130, CompatibilityLevel::SQL_SERVER_2016),
349 (140, CompatibilityLevel::SQL_SERVER_2017),
350 (150, CompatibilityLevel::SQL_SERVER_2019),
351 (160, CompatibilityLevel::SQL_SERVER_2022),
352 (170, CompatibilityLevel::SQL_SERVER_2025),
353 ];
354
355 for (value, expected) in cases {
356 let level = CompatibilityLevel::new(value).unwrap();
357
358 assert_eq!(level, expected);
359 assert_eq!(level.as_u16(), value);
360 }
361 }
362
363 #[test]
364 fn try_from_accepts_supported_compatibility_levels() {
365 for value in [100, 110, 120, 130, 140, 150, 160, 170] {
366 let level = CompatibilityLevel::try_from(value).unwrap();
367
368 assert_eq!(level.as_u16(), value);
369 }
370 }
371
372 #[test]
373 fn rejects_unsupported_compatibility_level() {
374 let err = CompatibilityLevel::new(90).expect_err("level should be rejected");
375
376 assert!(err.to_string().contains("invalid compatibility level 90"));
377 }
378
379 #[test]
380 fn rejects_nearby_and_extreme_compatibility_levels() {
381 for level in [
382 0,
383 99,
384 101,
385 109,
386 111,
387 119,
388 121,
389 129,
390 131,
391 139,
392 141,
393 149,
394 151,
395 159,
396 161,
397 169,
398 171,
399 u16::MAX,
400 ] {
401 let err = CompatibilityLevel::new(level).expect_err("level should be rejected");
402
403 assert!(
404 err.to_string()
405 .contains(&format!("invalid compatibility level {level}")),
406 "unexpected error: {err}"
407 );
408 }
409 }
410
411 #[test]
412 fn selects_datetime_rounding_by_compatibility_level() {
413 let cases = [
414 (
415 MssqlProfile::sql_server_2016_compat_100(),
416 DateTimeRounding::LegacyPre130,
417 ),
418 (
419 MssqlProfile::sql_server_2017_compat_100(),
420 DateTimeRounding::LegacyPre130,
421 ),
422 (
423 MssqlProfile::sql_server_2017_compat_110(),
424 DateTimeRounding::LegacyPre130,
425 ),
426 (
427 MssqlProfile::sql_server_2017_compat_120(),
428 DateTimeRounding::LegacyPre130,
429 ),
430 (
431 MssqlProfile::sql_server_2017_compat_130(),
432 DateTimeRounding::Compat130Plus,
433 ),
434 (
435 MssqlProfile::sql_server_2017_compat_140(),
436 DateTimeRounding::Compat130Plus,
437 ),
438 (
439 MssqlProfile::new(
440 MssqlVersion::SqlServer2019,
441 CompatibilityLevel::SQL_SERVER_2019,
442 )
443 .unwrap(),
444 DateTimeRounding::Compat130Plus,
445 ),
446 (
447 MssqlProfile::new(
448 MssqlVersion::SqlServer2022,
449 CompatibilityLevel::SQL_SERVER_2022,
450 )
451 .unwrap(),
452 DateTimeRounding::Compat130Plus,
453 ),
454 (
455 MssqlProfile::new(
456 MssqlVersion::SqlServer2025,
457 CompatibilityLevel::SQL_SERVER_2025,
458 )
459 .unwrap(),
460 DateTimeRounding::Compat130Plus,
461 ),
462 ];
463
464 for (profile, expected) in cases {
465 assert_eq!(profile.datetime_rounding(), expected);
466 }
467 }
468}