Skip to main content

sqlx_mssql_odbc_core/
arguments.rs

1use crate::DataTypeExt;
2use odbc_api::{
3    IntoParameter, Nullable,
4    parameter::{InputParameter, VarBinaryBox, VarCharBox, VarWCharBox, WithDataType},
5};
6
7/// Values that can currently be bound to MSSQL ODBC parameters.
8#[derive(Debug, Clone, PartialEq)]
9pub enum MssqlArgumentValue {
10    /// UTF-8 text parameter.
11    Text(String),
12    /// Binary parameter.
13    Bytes(Vec<u8>),
14    /// Signed integer parameter (all integer types including u8/u16/u32/u64
15    /// that fit within `i64` range are encoded here; unsigned values above
16    /// `i64::MAX` are rejected at encode time).
17    Int(i64),
18    /// Boolean parameter.
19    Bit(bool),
20    /// Floating point parameter.
21    Float(f64),
22    /// Date parameter.
23    Date(odbc_api::sys::Date),
24    /// Time parameter.
25    Time(odbc_api::sys::Time),
26    /// Timestamp parameter.
27    Timestamp(odbc_api::sys::Timestamp),
28    /// Typed NULL parameter.
29    Null(crate::MssqlTypeInfo),
30}
31
32/// Values that can be bound to MSSQL ODBC parameters.
33#[derive(Debug, Default, Clone, PartialEq)]
34pub struct MssqlArguments {
35    values: Vec<MssqlArgumentValue>,
36}
37
38/// Owned MSSQL ODBC parameter storage ready to bind with `odbc-api`.
39///
40/// `odbc-api` implements `ParameterCollectionRef` for `&[Box<dyn InputParameter>]`, so executor
41/// code can pass `collection.as_slice()` to `Connection::execute` or `Preallocated::execute`.
42#[derive(Default)]
43pub struct MssqlParameterCollection {
44    parameters: Vec<Box<dyn InputParameter>>,
45}
46
47impl std::fmt::Debug for MssqlParameterCollection {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        f.debug_struct("MssqlParameterCollection")
50            .field("len", &self.parameters.len())
51            .finish()
52    }
53}
54
55impl MssqlParameterCollection {
56    /// Converts raw SQLx MSSQL ODBC argument values into owned `odbc-api` input parameters.
57    pub fn from_values(values: &[MssqlArgumentValue]) -> Self {
58        let parameters = values.iter().map(value_to_parameter).collect();
59
60        Self { parameters }
61    }
62
63    /// Returns the number of parameters.
64    pub fn len(&self) -> usize {
65        self.parameters.len()
66    }
67
68    /// Returns `true` when no parameters are present.
69    pub fn is_empty(&self) -> bool {
70        self.parameters.is_empty()
71    }
72
73    /// Returns the parameter slice accepted by `odbc-api` execution methods.
74    pub fn as_slice(&self) -> &[Box<dyn InputParameter>] {
75        &self.parameters
76    }
77}
78
79impl MssqlArguments {
80    /// Adds a raw ODBC argument value.
81    pub fn add_value(&mut self, value: MssqlArgumentValue) {
82        self.values.push(value);
83    }
84
85    /// Returns the number of arguments.
86    pub fn len(&self) -> usize {
87        self.values.len()
88    }
89
90    /// Returns `true` when no arguments have been added.
91    pub fn is_empty(&self) -> bool {
92        self.values.is_empty()
93    }
94
95    /// Returns the raw argument values.
96    pub fn values(&self) -> &[MssqlArgumentValue] {
97        &self.values
98    }
99
100    /// Converts these arguments into owned `odbc-api` parameters.
101    pub fn to_odbc_parameter_collection(&self) -> MssqlParameterCollection {
102        MssqlParameterCollection::from_values(&self.values)
103    }
104}
105
106impl sqlx_core::arguments::Arguments for MssqlArguments {
107    type Database = crate::Mssql;
108
109    fn reserve(&mut self, additional: usize, _size: usize) {
110        self.values.reserve(additional);
111    }
112
113    fn add<'t, T>(&mut self, value: T) -> Result<(), sqlx_core::error::BoxDynError>
114    where
115        T: sqlx_core::encode::Encode<'t, Self::Database> + sqlx_core::types::Type<Self::Database>,
116    {
117        let _ = value.encode(&mut self.values)?;
118        Ok(())
119    }
120
121    fn len(&self) -> usize {
122        self.values.len()
123    }
124}
125
126sqlx_core::impl_into_arguments_for_arguments!(MssqlArguments);
127
128impl<'q, T> sqlx_core::encode::Encode<'q, crate::Mssql> for Option<T>
129where
130    T: sqlx_core::encode::Encode<'q, crate::Mssql> + sqlx_core::types::Type<crate::Mssql> + 'q,
131{
132    fn encode(
133        self,
134        buf: &mut Vec<MssqlArgumentValue>,
135    ) -> Result<sqlx_core::encode::IsNull, sqlx_core::error::BoxDynError> {
136        match self {
137            Some(value) => value.encode(buf),
138            None => {
139                buf.push(MssqlArgumentValue::Null(T::type_info()));
140                Ok(sqlx_core::encode::IsNull::Yes)
141            }
142        }
143    }
144
145    fn encode_by_ref(
146        &self,
147        buf: &mut Vec<MssqlArgumentValue>,
148    ) -> Result<sqlx_core::encode::IsNull, sqlx_core::error::BoxDynError> {
149        match self {
150            Some(value) => value.encode_by_ref(buf),
151            None => {
152                buf.push(MssqlArgumentValue::Null(T::type_info()));
153                Ok(sqlx_core::encode::IsNull::Yes)
154            }
155        }
156    }
157
158    fn produces(&self) -> Option<crate::MssqlTypeInfo> {
159        match self {
160            Some(value) => value.produces(),
161            None => Some(T::type_info()),
162        }
163    }
164}
165
166macro_rules! impl_integer {
167    ($ty:ty, $type_info:expr, $($compatible:pat_param)|+ $(,)?) => {
168        impl sqlx_core::types::Type<crate::Mssql> for $ty {
169            fn type_info() -> crate::MssqlTypeInfo {
170                crate::MssqlTypeInfo::new($type_info)
171            }
172
173            fn compatible(ty: &crate::MssqlTypeInfo) -> bool {
174                matches!(
175                    ty.data_type(),
176                    $($compatible)|+
177                        | odbc_api::DataType::Numeric { .. }
178                        | odbc_api::DataType::Decimal { .. }
179                ) || ty.data_type().accepts_numeric_data()
180            }
181        }
182
183        impl<'q> sqlx_core::encode::Encode<'q, crate::Mssql> for $ty {
184            fn encode_by_ref(
185                &self,
186                buf: &mut Vec<MssqlArgumentValue>,
187            ) -> Result<sqlx_core::encode::IsNull, sqlx_core::error::BoxDynError> {
188                buf.push(MssqlArgumentValue::Int(i64::from(*self)));
189                Ok(sqlx_core::encode::IsNull::No)
190            }
191        }
192    };
193}
194
195impl_integer!(
196    i8,
197    odbc_api::DataType::TinyInt,
198    odbc_api::DataType::TinyInt
199        | odbc_api::DataType::SmallInt
200        | odbc_api::DataType::Integer
201        | odbc_api::DataType::BigInt,
202);
203impl_integer!(
204    i16,
205    odbc_api::DataType::SmallInt,
206    odbc_api::DataType::TinyInt
207        | odbc_api::DataType::SmallInt
208        | odbc_api::DataType::Integer
209        | odbc_api::DataType::BigInt,
210);
211impl_integer!(
212    i32,
213    odbc_api::DataType::Integer,
214    odbc_api::DataType::TinyInt
215        | odbc_api::DataType::SmallInt
216        | odbc_api::DataType::Integer
217        | odbc_api::DataType::BigInt,
218);
219impl_integer!(
220    i64,
221    odbc_api::DataType::BigInt,
222    odbc_api::DataType::TinyInt
223        | odbc_api::DataType::SmallInt
224        | odbc_api::DataType::Integer
225        | odbc_api::DataType::BigInt,
226);
227
228macro_rules! impl_unsigned {
229    ($ty:ty, $type_info:expr, $($compatible:pat_param)|+ $(,)?) => {
230        impl sqlx_core::types::Type<crate::Mssql> for $ty {
231            fn type_info() -> crate::MssqlTypeInfo {
232                crate::MssqlTypeInfo::new($type_info)
233            }
234
235            fn compatible(ty: &crate::MssqlTypeInfo) -> bool {
236                matches!(
237                    ty.data_type(),
238                    $($compatible)|+
239                        | odbc_api::DataType::Numeric { .. }
240                        | odbc_api::DataType::Decimal { .. }
241                ) || ty.data_type().accepts_numeric_data()
242            }
243        }
244
245        impl<'q> sqlx_core::encode::Encode<'q, crate::Mssql> for $ty {
246            fn encode_by_ref(
247                &self,
248                buf: &mut Vec<MssqlArgumentValue>,
249            ) -> Result<sqlx_core::encode::IsNull, sqlx_core::error::BoxDynError> {
250                buf.push(MssqlArgumentValue::Int(i64::from(*self)));
251                Ok(sqlx_core::encode::IsNull::No)
252            }
253        }
254    };
255}
256
257impl_unsigned!(
258    u8,
259    odbc_api::DataType::TinyInt,
260    odbc_api::DataType::TinyInt
261        | odbc_api::DataType::SmallInt
262        | odbc_api::DataType::Integer
263        | odbc_api::DataType::BigInt,
264);
265impl_unsigned!(
266    u16,
267    odbc_api::DataType::SmallInt,
268    odbc_api::DataType::SmallInt | odbc_api::DataType::Integer | odbc_api::DataType::BigInt,
269);
270impl_unsigned!(
271    u32,
272    odbc_api::DataType::Integer,
273    odbc_api::DataType::Integer | odbc_api::DataType::BigInt,
274);
275
276impl sqlx_core::types::Type<crate::Mssql> for u64 {
277    fn type_info() -> crate::MssqlTypeInfo {
278        crate::MssqlTypeInfo::BIGINT
279    }
280
281    fn compatible(ty: &crate::MssqlTypeInfo) -> bool {
282        matches!(
283            ty.data_type(),
284            odbc_api::DataType::Integer
285                | odbc_api::DataType::BigInt
286                | odbc_api::DataType::Numeric { .. }
287                | odbc_api::DataType::Decimal { .. }
288        ) || ty.data_type().accepts_numeric_data()
289    }
290}
291
292impl<'q> sqlx_core::encode::Encode<'q, crate::Mssql> for u64 {
293    fn encode_by_ref(
294        &self,
295        buf: &mut Vec<MssqlArgumentValue>,
296    ) -> Result<sqlx_core::encode::IsNull, sqlx_core::error::BoxDynError> {
297        let value = i64::try_from(*self).map_err(|_| {
298            format!(
299                "u64 value {self} exceeds BIGINT range (max {}) and cannot be \
300                 encoded for MSSQL; use NUMERIC/DECIMAL via rust_decimal for \
301                 values above 2^63-1",
302                i64::MAX
303            )
304        })?;
305        buf.push(MssqlArgumentValue::Int(value));
306        Ok(sqlx_core::encode::IsNull::No)
307    }
308}
309
310impl sqlx_core::types::Type<crate::Mssql> for bool {
311    fn type_info() -> crate::MssqlTypeInfo {
312        crate::MssqlTypeInfo::new(odbc_api::DataType::Bit)
313    }
314
315    fn compatible(ty: &crate::MssqlTypeInfo) -> bool {
316        ty.data_type().accepts_numeric_data()
317    }
318}
319
320impl<'q> sqlx_core::encode::Encode<'q, crate::Mssql> for bool {
321    fn encode_by_ref(
322        &self,
323        buf: &mut Vec<MssqlArgumentValue>,
324    ) -> Result<sqlx_core::encode::IsNull, sqlx_core::error::BoxDynError> {
325        buf.push(MssqlArgumentValue::Bit(*self));
326        Ok(sqlx_core::encode::IsNull::No)
327    }
328}
329
330impl sqlx_core::types::Type<crate::Mssql> for f32 {
331    fn type_info() -> crate::MssqlTypeInfo {
332        crate::MssqlTypeInfo::new(odbc_api::DataType::Real)
333    }
334
335    fn compatible(ty: &crate::MssqlTypeInfo) -> bool {
336        ty.data_type().accepts_numeric_data()
337    }
338}
339
340impl<'q> sqlx_core::encode::Encode<'q, crate::Mssql> for f32 {
341    fn encode_by_ref(
342        &self,
343        buf: &mut Vec<MssqlArgumentValue>,
344    ) -> Result<sqlx_core::encode::IsNull, sqlx_core::error::BoxDynError> {
345        buf.push(MssqlArgumentValue::Float(f64::from(*self)));
346        Ok(sqlx_core::encode::IsNull::No)
347    }
348}
349
350impl sqlx_core::types::Type<crate::Mssql> for f64 {
351    fn type_info() -> crate::MssqlTypeInfo {
352        crate::MssqlTypeInfo::new(odbc_api::DataType::Double)
353    }
354
355    fn compatible(ty: &crate::MssqlTypeInfo) -> bool {
356        ty.data_type().accepts_numeric_data()
357    }
358}
359
360impl<'q> sqlx_core::encode::Encode<'q, crate::Mssql> for f64 {
361    fn encode_by_ref(
362        &self,
363        buf: &mut Vec<MssqlArgumentValue>,
364    ) -> Result<sqlx_core::encode::IsNull, sqlx_core::error::BoxDynError> {
365        buf.push(MssqlArgumentValue::Float(*self));
366        Ok(sqlx_core::encode::IsNull::No)
367    }
368}
369
370impl sqlx_core::types::Type<crate::Mssql> for str {
371    fn type_info() -> crate::MssqlTypeInfo {
372        crate::MssqlTypeInfo::new(odbc_api::DataType::WVarchar { length: None })
373    }
374
375    fn compatible(ty: &crate::MssqlTypeInfo) -> bool {
376        if ty.data_type().accepts_character_data() {
377            return true;
378        }
379        // MSSQL-specific types reported as DataType::Other that the driver
380        // fetches as text — we can decode them as String.
381        matches!(
382            ty.data_type(),
383            odbc_api::DataType::Other { data_type, .. }
384                if matches!(data_type.0, -150 | -151 | -152 | -156)
385        )
386    }
387}
388
389impl sqlx_core::types::Type<crate::Mssql> for String {
390    fn type_info() -> crate::MssqlTypeInfo {
391        <str as sqlx_core::types::Type<crate::Mssql>>::type_info()
392    }
393
394    fn compatible(ty: &crate::MssqlTypeInfo) -> bool {
395        <str as sqlx_core::types::Type<crate::Mssql>>::compatible(ty)
396    }
397}
398
399impl<'q> sqlx_core::encode::Encode<'q, crate::Mssql> for &'q str {
400    fn encode_by_ref(
401        &self,
402        buf: &mut Vec<MssqlArgumentValue>,
403    ) -> Result<sqlx_core::encode::IsNull, sqlx_core::error::BoxDynError> {
404        buf.push(MssqlArgumentValue::Text((*self).to_owned()));
405        Ok(sqlx_core::encode::IsNull::No)
406    }
407}
408
409impl<'q> sqlx_core::encode::Encode<'q, crate::Mssql> for String {
410    fn encode_by_ref(
411        &self,
412        buf: &mut Vec<MssqlArgumentValue>,
413    ) -> Result<sqlx_core::encode::IsNull, sqlx_core::error::BoxDynError> {
414        buf.push(MssqlArgumentValue::Text(self.clone()));
415        Ok(sqlx_core::encode::IsNull::No)
416    }
417}
418
419impl sqlx_core::types::Type<crate::Mssql> for [u8] {
420    fn type_info() -> crate::MssqlTypeInfo {
421        crate::MssqlTypeInfo::new(odbc_api::DataType::Varbinary { length: None })
422    }
423
424    fn compatible(ty: &crate::MssqlTypeInfo) -> bool {
425        ty.data_type().accepts_binary_data()
426    }
427}
428
429impl sqlx_core::types::Type<crate::Mssql> for Vec<u8> {
430    fn type_info() -> crate::MssqlTypeInfo {
431        <[u8] as sqlx_core::types::Type<crate::Mssql>>::type_info()
432    }
433
434    fn compatible(ty: &crate::MssqlTypeInfo) -> bool {
435        <[u8] as sqlx_core::types::Type<crate::Mssql>>::compatible(ty)
436    }
437}
438
439impl<'q> sqlx_core::encode::Encode<'q, crate::Mssql> for &'q [u8] {
440    fn encode_by_ref(
441        &self,
442        buf: &mut Vec<MssqlArgumentValue>,
443    ) -> Result<sqlx_core::encode::IsNull, sqlx_core::error::BoxDynError> {
444        buf.push(MssqlArgumentValue::Bytes((*self).to_owned()));
445        Ok(sqlx_core::encode::IsNull::No)
446    }
447}
448
449impl<'q> sqlx_core::encode::Encode<'q, crate::Mssql> for Vec<u8> {
450    fn encode_by_ref(
451        &self,
452        buf: &mut Vec<MssqlArgumentValue>,
453    ) -> Result<sqlx_core::encode::IsNull, sqlx_core::error::BoxDynError> {
454        buf.push(MssqlArgumentValue::Bytes(self.clone()));
455        Ok(sqlx_core::encode::IsNull::No)
456    }
457}
458
459impl sqlx_core::types::Type<crate::Mssql> for odbc_api::sys::Date {
460    fn type_info() -> crate::MssqlTypeInfo {
461        crate::MssqlTypeInfo::DATE
462    }
463
464    fn compatible(ty: &crate::MssqlTypeInfo) -> bool {
465        matches!(ty.data_type(), odbc_api::DataType::Date)
466    }
467}
468
469impl<'q> sqlx_core::encode::Encode<'q, crate::Mssql> for odbc_api::sys::Date {
470    fn encode_by_ref(
471        &self,
472        buf: &mut Vec<MssqlArgumentValue>,
473    ) -> Result<sqlx_core::encode::IsNull, sqlx_core::error::BoxDynError> {
474        buf.push(MssqlArgumentValue::Date(*self));
475        Ok(sqlx_core::encode::IsNull::No)
476    }
477}
478
479impl sqlx_core::types::Type<crate::Mssql> for odbc_api::sys::Time {
480    fn type_info() -> crate::MssqlTypeInfo {
481        crate::MssqlTypeInfo::TIME
482    }
483
484    fn compatible(ty: &crate::MssqlTypeInfo) -> bool {
485        matches!(ty.data_type(), odbc_api::DataType::Time { .. })
486    }
487}
488
489impl<'q> sqlx_core::encode::Encode<'q, crate::Mssql> for odbc_api::sys::Time {
490    fn encode_by_ref(
491        &self,
492        buf: &mut Vec<MssqlArgumentValue>,
493    ) -> Result<sqlx_core::encode::IsNull, sqlx_core::error::BoxDynError> {
494        buf.push(MssqlArgumentValue::Time(*self));
495        Ok(sqlx_core::encode::IsNull::No)
496    }
497}
498
499impl sqlx_core::types::Type<crate::Mssql> for odbc_api::sys::Timestamp {
500    fn type_info() -> crate::MssqlTypeInfo {
501        crate::MssqlTypeInfo::TIMESTAMP
502    }
503
504    fn compatible(ty: &crate::MssqlTypeInfo) -> bool {
505        matches!(ty.data_type(), odbc_api::DataType::Timestamp { .. })
506    }
507}
508
509impl<'q> sqlx_core::encode::Encode<'q, crate::Mssql> for odbc_api::sys::Timestamp {
510    fn encode_by_ref(
511        &self,
512        buf: &mut Vec<MssqlArgumentValue>,
513    ) -> Result<sqlx_core::encode::IsNull, sqlx_core::error::BoxDynError> {
514        buf.push(MssqlArgumentValue::Timestamp(*self));
515        Ok(sqlx_core::encode::IsNull::No)
516    }
517}
518
519fn value_to_parameter(value: &MssqlArgumentValue) -> Box<dyn InputParameter> {
520    match value {
521        // Use wide (UTF-16) binding for all text parameters to avoid
522        // codepage-dependent corruption of non-ASCII data in narrow binding.
523        MssqlArgumentValue::Text(value) => Box::new(VarWCharBox::from_str_slice(value)),
524        MssqlArgumentValue::Bytes(value) => Box::new(value.clone().into_parameter()),
525        MssqlArgumentValue::Int(value) => Box::new(Some(*value).into_parameter()),
526        MssqlArgumentValue::Bit(value) => Box::new(odbc_api::Bit::from_bool(*value)),
527        MssqlArgumentValue::Float(value) => Box::new(Some(*value).into_parameter()),
528        MssqlArgumentValue::Date(value) => Box::new(Nullable::new(*value).into_parameter()),
529        MssqlArgumentValue::Time(value) => Box::new(
530            WithDataType::new(
531                Nullable::new(*value),
532                odbc_api::DataType::Time { precision: 0 },
533            )
534            .into_parameter(),
535        ),
536        MssqlArgumentValue::Timestamp(value) => Box::new(
537            WithDataType::new(
538                Nullable::new(*value),
539                odbc_api::DataType::Timestamp { precision: 6 },
540            )
541            .into_parameter(),
542        ),
543        MssqlArgumentValue::Null(type_info) => null_parameter(type_info.data_type()),
544    }
545}
546
547fn null_parameter(data_type: odbc_api::DataType) -> Box<dyn InputParameter> {
548    match data_type {
549        odbc_api::DataType::TinyInt => Box::new(Nullable::<i8>::null()),
550        odbc_api::DataType::SmallInt => Box::new(Nullable::<i16>::null()),
551        odbc_api::DataType::Integer => Box::new(Nullable::<i32>::null()),
552        odbc_api::DataType::BigInt => Box::new(Nullable::<i64>::null()),
553        odbc_api::DataType::Bit => Box::new(Nullable::<odbc_api::Bit>::null()),
554        odbc_api::DataType::Real => Box::new(Nullable::<f32>::null()),
555        odbc_api::DataType::Double => Box::new(Nullable::<f64>::null()),
556        // For typed NULLs the ODBC driver only inspects the indicator
557        // (SQL_NULL_DATA), not the value buffer, so the f64 buffer width
558        // is harmless even when the underlying column is REAL (f32).
559        odbc_api::DataType::Float { .. } => {
560            Box::new(WithDataType::new(Nullable::<f64>::null(), data_type))
561        }
562        odbc_api::DataType::Date => Box::new(Nullable::<odbc_api::sys::Date>::null()),
563        odbc_api::DataType::Time { .. } => Box::new(WithDataType::new(
564            Nullable::<odbc_api::sys::Time>::null(),
565            data_type,
566        )),
567        odbc_api::DataType::Timestamp { .. } => Box::new(WithDataType::new(
568            Nullable::<odbc_api::sys::Timestamp>::null(),
569            data_type,
570        )),
571        odbc_api::DataType::Varbinary { .. }
572        | odbc_api::DataType::LongVarbinary { .. }
573        | odbc_api::DataType::Binary { .. } => {
574            Box::new(WithDataType::new(VarBinaryBox::null(), data_type))
575        }
576        odbc_api::DataType::WVarchar { .. }
577        | odbc_api::DataType::WLongVarchar { .. }
578        | odbc_api::DataType::WChar { .. } => {
579            Box::new(WithDataType::new(VarWCharBox::null(), data_type))
580        }
581        odbc_api::DataType::Char { .. }
582        | odbc_api::DataType::Varchar { .. }
583        | odbc_api::DataType::LongVarchar { .. }
584        | odbc_api::DataType::Numeric { .. }
585        | odbc_api::DataType::Decimal { .. }
586        | odbc_api::DataType::Unknown
587        | odbc_api::DataType::Other { .. } => {
588            Box::new(WithDataType::new(VarCharBox::null(), data_type))
589        }
590    }
591}
592
593#[cfg(test)]
594mod tests {
595    use super::*;
596    use odbc_api::{
597        ParameterCollectionRef,
598        handles::{CData, HasDataType},
599    };
600
601    #[test]
602    fn argument_buffer_tracks_values_in_order() {
603        let mut arguments = MssqlArguments::default();
604
605        arguments.add_value(MssqlArgumentValue::Int(7));
606        arguments.add_value(MssqlArgumentValue::Text("abc".to_owned()));
607        arguments.add_value(MssqlArgumentValue::Null(crate::MssqlTypeInfo::new(
608            odbc_api::DataType::Integer,
609        )));
610
611        assert_eq!(arguments.len(), 3);
612        assert_eq!(
613            arguments.values(),
614            &[
615                MssqlArgumentValue::Int(7),
616                MssqlArgumentValue::Text("abc".to_owned()),
617                MssqlArgumentValue::Null(crate::MssqlTypeInfo::new(odbc_api::DataType::Integer))
618            ]
619        );
620    }
621
622    #[test]
623    fn sqlx_arguments_add_encodes_basic_scalars() {
624        let mut arguments = MssqlArguments::default();
625
626        sqlx_core::arguments::Arguments::add(&mut arguments, 7_i32).unwrap();
627        sqlx_core::arguments::Arguments::add(&mut arguments, "abc").unwrap();
628        sqlx_core::arguments::Arguments::add(&mut arguments, vec![1_u8, 2, 3]).unwrap();
629
630        assert_eq!(
631            arguments.values(),
632            &[
633                MssqlArgumentValue::Int(7),
634                MssqlArgumentValue::Text("abc".to_owned()),
635                MssqlArgumentValue::Bytes(vec![1, 2, 3])
636            ]
637        );
638    }
639
640    #[test]
641    fn sqlx_arguments_add_encodes_large_text_and_binary_slices() {
642        let mut arguments = MssqlArguments::default();
643        let text = "abc123".repeat(16 * 1024);
644        let bytes = [0_u8, 1, 2, 127, 128, 254, 255];
645
646        sqlx_core::arguments::Arguments::add(&mut arguments, text.as_str()).unwrap();
647        sqlx_core::arguments::Arguments::add(&mut arguments, &bytes[..]).unwrap();
648
649        assert_eq!(
650            arguments.values(),
651            &[
652                MssqlArgumentValue::Text(text),
653                MssqlArgumentValue::Bytes(bytes.to_vec())
654            ]
655        );
656    }
657
658    #[test]
659    fn byte_types_are_compatible_with_text_and_binary_columns() {
660        use sqlx_core::types::Type;
661
662        let binary = crate::MssqlTypeInfo::new(odbc_api::DataType::Varbinary { length: None });
663        let text = crate::MssqlTypeInfo::new(odbc_api::DataType::WVarchar { length: None });
664        let integer = crate::MssqlTypeInfo::new(odbc_api::DataType::Integer);
665
666        assert!(<[u8] as Type<crate::Mssql>>::compatible(&binary));
667        assert!(<[u8] as Type<crate::Mssql>>::compatible(&text));
668        assert!(!<[u8] as Type<crate::Mssql>>::compatible(&integer));
669        assert!(<Vec<u8> as Type<crate::Mssql>>::compatible(&binary));
670        assert!(<Vec<u8> as Type<crate::Mssql>>::compatible(&text));
671        assert!(!<Vec<u8> as Type<crate::Mssql>>::compatible(&integer));
672    }
673
674    #[test]
675    fn sqlx_arguments_add_rejects_unsigned_values_exceeding_bigint_range() {
676        let mut arguments = MssqlArguments::default();
677
678        let result = sqlx_core::arguments::Arguments::add(&mut arguments, u64::MAX);
679
680        assert!(result.is_err());
681        let error = result.unwrap_err();
682        let msg = format!("{error}");
683        assert!(
684            msg.contains("exceeds BIGINT range"),
685            "expected range error, got: {msg}"
686        );
687    }
688
689    #[test]
690    fn sqlx_arguments_add_encodes_unsigned_values_within_bigint_range() {
691        let mut arguments = MssqlArguments::default();
692
693        sqlx_core::arguments::Arguments::add(&mut arguments, 42_u64).unwrap();
694
695        assert_eq!(arguments.values(), &[MssqlArgumentValue::Int(42)]);
696    }
697
698    #[test]
699    fn sqlx_arguments_add_encodes_temporal_scalars() {
700        let mut arguments = MssqlArguments::default();
701        let date = odbc_api::sys::Date {
702            year: 2026,
703            month: 5,
704            day: 29,
705        };
706        let time = odbc_api::sys::Time {
707            hour: 12,
708            minute: 30,
709            second: 45,
710        };
711        let timestamp = odbc_api::sys::Timestamp {
712            year: 2026,
713            month: 5,
714            day: 29,
715            hour: 12,
716            minute: 30,
717            second: 45,
718            fraction: 123_456_000,
719        };
720
721        sqlx_core::arguments::Arguments::add(&mut arguments, date).unwrap();
722        sqlx_core::arguments::Arguments::add(&mut arguments, time).unwrap();
723        sqlx_core::arguments::Arguments::add(&mut arguments, timestamp).unwrap();
724
725        assert_eq!(
726            arguments.values(),
727            &[
728                MssqlArgumentValue::Date(date),
729                MssqlArgumentValue::Time(time),
730                MssqlArgumentValue::Timestamp(timestamp)
731            ]
732        );
733    }
734
735    #[test]
736    fn sqlx_arguments_add_encodes_typed_null_option() {
737        let mut arguments = MssqlArguments::default();
738
739        sqlx_core::arguments::Arguments::add(&mut arguments, Option::<i32>::None).unwrap();
740
741        assert_eq!(
742            arguments.values(),
743            &[MssqlArgumentValue::Null(crate::MssqlTypeInfo::new(
744                odbc_api::DataType::Integer
745            ))]
746        );
747
748        let collection = arguments.to_odbc_parameter_collection();
749        assert_eq!(
750            collection.as_slice()[0].data_type(),
751            odbc_api::DataType::Integer
752        );
753    }
754
755    #[test]
756    fn sqlx_arguments_reserve_and_len_work() {
757        let mut arguments = MssqlArguments::default();
758
759        sqlx_core::arguments::Arguments::reserve(&mut arguments, 2, 16);
760        sqlx_core::arguments::Arguments::add(&mut arguments, true).unwrap();
761        sqlx_core::arguments::Arguments::add(&mut arguments, 1.5_f64).unwrap();
762
763        assert_eq!(sqlx_core::arguments::Arguments::len(&arguments), 2);
764        assert_eq!(
765            arguments.values(),
766            &[
767                MssqlArgumentValue::Bit(true),
768                MssqlArgumentValue::Float(1.5)
769            ]
770        );
771    }
772
773    #[test]
774    fn parameter_collection_converts_basic_values_to_odbc_parameters() {
775        let values = [
776            MssqlArgumentValue::Text("abc".to_owned()),
777            MssqlArgumentValue::Bytes(vec![1, 2, 3]),
778            MssqlArgumentValue::Int(7),
779            MssqlArgumentValue::Int(8),
780            MssqlArgumentValue::Bit(true),
781            MssqlArgumentValue::Float(1.5),
782        ];
783
784        let collection = MssqlParameterCollection::from_values(&values);
785
786        assert_eq!(collection.len(), values.len());
787        assert!(matches!(
788            collection.as_slice()[0].data_type(),
789            odbc_api::DataType::Varchar { .. }
790                | odbc_api::DataType::WVarchar { .. }
791                | odbc_api::DataType::WLongVarchar { .. }
792        ));
793        assert!(matches!(
794            collection.as_slice()[1].data_type(),
795            odbc_api::DataType::Varbinary { .. }
796        ));
797        assert_eq!(
798            collection.as_slice()[2].data_type(),
799            odbc_api::DataType::BigInt
800        );
801        assert_eq!(
802            collection.as_slice()[3].data_type(),
803            odbc_api::DataType::BigInt
804        );
805        assert_eq!(
806            collection.as_slice()[4].data_type(),
807            odbc_api::DataType::Bit
808        );
809        assert_eq!(
810            collection.as_slice()[5].data_type(),
811            odbc_api::DataType::Double
812        );
813    }
814
815    #[test]
816    fn parameter_collection_converts_temporal_values_to_typed_odbc_parameters() {
817        let values = [
818            MssqlArgumentValue::Date(odbc_api::sys::Date {
819                year: 2026,
820                month: 5,
821                day: 29,
822            }),
823            MssqlArgumentValue::Time(odbc_api::sys::Time {
824                hour: 12,
825                minute: 30,
826                second: 45,
827            }),
828            MssqlArgumentValue::Timestamp(odbc_api::sys::Timestamp {
829                year: 2026,
830                month: 5,
831                day: 29,
832                hour: 12,
833                minute: 30,
834                second: 45,
835                fraction: 123_456_789,
836            }),
837        ];
838
839        let collection = MssqlParameterCollection::from_values(&values);
840
841        assert_eq!(
842            collection.as_slice()[0].data_type(),
843            odbc_api::DataType::Date
844        );
845        assert_eq!(
846            collection.as_slice()[1].data_type(),
847            odbc_api::DataType::Time { precision: 0 }
848        );
849        assert_eq!(
850            collection.as_slice()[2].data_type(),
851            odbc_api::DataType::Timestamp { precision: 6 }
852        );
853    }
854
855    #[test]
856    fn parameter_collection_converts_typed_nulls_to_requested_data_types() {
857        let values = [
858            MssqlArgumentValue::Null(crate::MssqlTypeInfo::new(odbc_api::DataType::Integer)),
859            MssqlArgumentValue::Null(crate::MssqlTypeInfo::new(odbc_api::DataType::WVarchar {
860                length: None,
861            })),
862            MssqlArgumentValue::Null(crate::MssqlTypeInfo::new(odbc_api::DataType::Decimal {
863                precision: 10,
864                scale: 2,
865            })),
866        ];
867
868        let collection = MssqlParameterCollection::from_values(&values);
869
870        assert_eq!(
871            collection.as_slice()[0].data_type(),
872            odbc_api::DataType::Integer
873        );
874        assert_eq!(
875            collection.as_slice()[1].data_type(),
876            odbc_api::DataType::WVarchar { length: None }
877        );
878        assert_eq!(
879            collection.as_slice()[2].data_type(),
880            odbc_api::DataType::Decimal {
881                precision: 10,
882                scale: 2
883            }
884        );
885    }
886
887    #[test]
888    fn parameter_collection_slice_matches_odbc_api_binding_shape() {
889        fn assert_parameter_collection_ref<T: ParameterCollectionRef>(_parameters: T) {}
890
891        let mut arguments = MssqlArguments::default();
892        sqlx_core::arguments::Arguments::add(&mut arguments, "abc").unwrap();
893        let collection = arguments.to_odbc_parameter_collection();
894
895        assert_parameter_collection_ref(collection.as_slice());
896    }
897
898    #[test]
899    fn fixed_sized_parameter_uses_explicit_non_null_indicator() {
900        let mut arguments = MssqlArguments::default();
901
902        sqlx_core::arguments::Arguments::add(&mut arguments, 5_i32).unwrap();
903
904        let collection = arguments.to_odbc_parameter_collection();
905        assert_eq!(collection.len(), 1);
906        assert!(!collection.as_slice()[0].indicator_ptr().is_null());
907    }
908}