Skip to main content

mssql_types/
value.rs

1//! SQL value representation.
2
3use bytes::Bytes;
4
5use crate::tvp::TvpData;
6
7/// A SQL value that can represent any SQL Server data type.
8///
9/// This enum provides a type-safe way to handle SQL values that may be
10/// of various types, including NULL.
11#[derive(Debug, Clone, PartialEq)]
12#[non_exhaustive]
13pub enum SqlValue {
14    /// NULL value.
15    Null,
16    /// Boolean value (BIT).
17    Bool(bool),
18    /// 8-bit unsigned integer (TINYINT).
19    TinyInt(u8),
20    /// 16-bit signed integer (SMALLINT).
21    SmallInt(i16),
22    /// 32-bit signed integer (INT).
23    Int(i32),
24    /// 64-bit signed integer (BIGINT).
25    BigInt(i64),
26    /// 32-bit floating point (REAL).
27    Float(f32),
28    /// 64-bit floating point (FLOAT).
29    Double(f64),
30    /// String value (CHAR, VARCHAR, NCHAR, NVARCHAR, TEXT, NTEXT).
31    String(String),
32    /// Binary value (BINARY, VARBINARY, IMAGE).
33    Binary(Bytes),
34    /// Decimal value (DECIMAL, NUMERIC).
35    #[cfg(feature = "decimal")]
36    Decimal(rust_decimal::Decimal),
37    /// Money value (MONEY — fixed-point scaled by 10_000, signed 64-bit range).
38    ///
39    /// Distinct from [`Self::Decimal`] so that RPC parameter encoding can
40    /// select the MONEY wire format (type 0x6E, 8-byte scaled integer) rather
41    /// than the generic DECIMAL format. MONEY columns returned from queries
42    /// decode back to [`Self::Decimal`] — the distinction is only meaningful
43    /// on the send path.
44    #[cfg(feature = "decimal")]
45    Money(rust_decimal::Decimal),
46    /// SmallMoney value (SMALLMONEY — fixed-point scaled by 10_000, signed 32-bit range).
47    #[cfg(feature = "decimal")]
48    SmallMoney(rust_decimal::Decimal),
49    /// UUID value (UNIQUEIDENTIFIER).
50    #[cfg(feature = "uuid")]
51    Uuid(uuid::Uuid),
52    /// Date value (DATE).
53    #[cfg(feature = "chrono")]
54    Date(chrono::NaiveDate),
55    /// Time value (TIME).
56    #[cfg(feature = "chrono")]
57    Time(chrono::NaiveTime),
58    /// DateTime value (DATETIME, DATETIME2).
59    #[cfg(feature = "chrono")]
60    DateTime(chrono::NaiveDateTime),
61    /// SmallDateTime value (SMALLDATETIME — minute precision, 1900-01-01..2079-06-06).
62    ///
63    /// Distinct from [`Self::DateTime`] so that RPC parameter encoding can
64    /// select the SMALLDATETIME wire format (type 0x6F, 4-byte days+minutes)
65    /// rather than DATETIME2. SMALLDATETIME columns returned from queries
66    /// decode back to [`Self::DateTime`].
67    #[cfg(feature = "chrono")]
68    SmallDateTime(chrono::NaiveDateTime),
69    /// DateTimeOffset value (DATETIMEOFFSET).
70    #[cfg(feature = "chrono")]
71    DateTimeOffset(chrono::DateTime<chrono::FixedOffset>),
72    /// JSON value (JSON type in SQL Server 2016+).
73    #[cfg(feature = "json")]
74    Json(serde_json::Value),
75    /// XML value (XML type).
76    Xml(String),
77    /// Table-Valued Parameter (TVP).
78    ///
79    /// TVPs allow passing collections of structured data to SQL Server stored
80    /// procedures. Boxed due to large size.
81    Tvp(Box<TvpData>),
82}
83
84impl SqlValue {
85    /// Check if the value is NULL.
86    #[must_use]
87    pub fn is_null(&self) -> bool {
88        matches!(self, Self::Null)
89    }
90
91    /// Get the value as a bool, if it is one.
92    #[must_use]
93    pub fn as_bool(&self) -> Option<bool> {
94        match self {
95            Self::Bool(v) => Some(*v),
96            _ => None,
97        }
98    }
99
100    /// Get the value as an i32, if it is one.
101    #[must_use]
102    pub fn as_i32(&self) -> Option<i32> {
103        match self {
104            Self::Int(v) => Some(*v),
105            Self::SmallInt(v) => Some(*v as i32),
106            Self::TinyInt(v) => Some(*v as i32),
107            _ => None,
108        }
109    }
110
111    /// Get the value as an i64, if it is one.
112    #[must_use]
113    pub fn as_i64(&self) -> Option<i64> {
114        match self {
115            Self::BigInt(v) => Some(*v),
116            Self::Int(v) => Some(*v as i64),
117            Self::SmallInt(v) => Some(*v as i64),
118            Self::TinyInt(v) => Some(*v as i64),
119            _ => None,
120        }
121    }
122
123    /// Get the value as an f64, if it is one.
124    #[must_use]
125    pub fn as_f64(&self) -> Option<f64> {
126        match self {
127            Self::Double(v) => Some(*v),
128            Self::Float(v) => Some(*v as f64),
129            _ => None,
130        }
131    }
132
133    /// Get the value as a string slice, if it is one.
134    #[must_use]
135    pub fn as_str(&self) -> Option<&str> {
136        match self {
137            Self::String(v) => Some(v),
138            Self::Xml(v) => Some(v),
139            _ => None,
140        }
141    }
142
143    /// Get the value as bytes, if it is binary.
144    #[must_use]
145    pub fn as_bytes(&self) -> Option<&[u8]> {
146        match self {
147            Self::Binary(v) => Some(v),
148            _ => None,
149        }
150    }
151
152    /// Get the type name as a string.
153    #[must_use]
154    pub fn type_name(&self) -> &'static str {
155        match self {
156            Self::Null => "NULL",
157            Self::Bool(_) => "BIT",
158            Self::TinyInt(_) => "TINYINT",
159            Self::SmallInt(_) => "SMALLINT",
160            Self::Int(_) => "INT",
161            Self::BigInt(_) => "BIGINT",
162            Self::Float(_) => "REAL",
163            Self::Double(_) => "FLOAT",
164            Self::String(_) => "NVARCHAR",
165            Self::Binary(_) => "VARBINARY",
166            #[cfg(feature = "decimal")]
167            Self::Decimal(_) => "DECIMAL",
168            #[cfg(feature = "decimal")]
169            Self::Money(_) => "MONEY",
170            #[cfg(feature = "decimal")]
171            Self::SmallMoney(_) => "SMALLMONEY",
172            #[cfg(feature = "uuid")]
173            Self::Uuid(_) => "UNIQUEIDENTIFIER",
174            #[cfg(feature = "chrono")]
175            Self::Date(_) => "DATE",
176            #[cfg(feature = "chrono")]
177            Self::Time(_) => "TIME",
178            #[cfg(feature = "chrono")]
179            Self::DateTime(_) => "DATETIME2",
180            #[cfg(feature = "chrono")]
181            Self::SmallDateTime(_) => "SMALLDATETIME",
182            #[cfg(feature = "chrono")]
183            Self::DateTimeOffset(_) => "DATETIMEOFFSET",
184            #[cfg(feature = "json")]
185            Self::Json(_) => "JSON",
186            Self::Xml(_) => "XML",
187            Self::Tvp(_) => "TVP",
188        }
189    }
190
191    /// Get the value as a TVP, if it is one.
192    #[must_use]
193    pub fn as_tvp(&self) -> Option<&TvpData> {
194        match self {
195            Self::Tvp(v) => Some(v),
196            _ => None,
197        }
198    }
199}
200
201impl Default for SqlValue {
202    fn default() -> Self {
203        Self::Null
204    }
205}
206
207impl From<bool> for SqlValue {
208    fn from(v: bool) -> Self {
209        Self::Bool(v)
210    }
211}
212
213impl From<i32> for SqlValue {
214    fn from(v: i32) -> Self {
215        Self::Int(v)
216    }
217}
218
219impl From<i64> for SqlValue {
220    fn from(v: i64) -> Self {
221        Self::BigInt(v)
222    }
223}
224
225impl From<f32> for SqlValue {
226    fn from(v: f32) -> Self {
227        Self::Float(v)
228    }
229}
230
231impl From<f64> for SqlValue {
232    fn from(v: f64) -> Self {
233        Self::Double(v)
234    }
235}
236
237impl From<String> for SqlValue {
238    fn from(v: String) -> Self {
239        Self::String(v)
240    }
241}
242
243impl From<&str> for SqlValue {
244    fn from(v: &str) -> Self {
245        Self::String(v.to_owned())
246    }
247}
248
249impl<T> From<Option<T>> for SqlValue
250where
251    T: Into<SqlValue>,
252{
253    fn from(v: Option<T>) -> Self {
254        match v {
255            Some(v) => v.into(),
256            None => Self::Null,
257        }
258    }
259}
260
261#[cfg(feature = "uuid")]
262impl From<uuid::Uuid> for SqlValue {
263    fn from(v: uuid::Uuid) -> Self {
264        Self::Uuid(v)
265    }
266}
267
268#[cfg(feature = "decimal")]
269impl From<rust_decimal::Decimal> for SqlValue {
270    fn from(v: rust_decimal::Decimal) -> Self {
271        Self::Decimal(v)
272    }
273}
274
275#[cfg(feature = "chrono")]
276impl From<chrono::NaiveDate> for SqlValue {
277    fn from(v: chrono::NaiveDate) -> Self {
278        Self::Date(v)
279    }
280}
281
282#[cfg(feature = "chrono")]
283impl From<chrono::NaiveDateTime> for SqlValue {
284    fn from(v: chrono::NaiveDateTime) -> Self {
285        Self::DateTime(v)
286    }
287}
288
289#[cfg(feature = "json")]
290impl From<serde_json::Value> for SqlValue {
291    fn from(v: serde_json::Value) -> Self {
292        Self::Json(v)
293    }
294}
295
296impl From<TvpData> for SqlValue {
297    fn from(v: TvpData) -> Self {
298        Self::Tvp(Box::new(v))
299    }
300}
301
302/// Wrapper that sends its inner [`rust_decimal::Decimal`] as SQL Server MONEY
303/// (signed 64-bit fixed-point scaled by 10_000) instead of DECIMAL.
304///
305/// Wrap a `Decimal` in `Money` when binding RPC parameters to force the MONEY
306/// wire format (type 0x6E, 8 bytes) — a plain `Decimal` would bind as the
307/// generic DECIMAL type (0x6C) and incur an implicit conversion on the server.
308#[cfg(feature = "decimal")]
309#[derive(Debug, Clone, Copy, PartialEq, Eq)]
310pub struct Money(pub rust_decimal::Decimal);
311
312/// Wrapper that sends its inner [`rust_decimal::Decimal`] as SQL Server
313/// SMALLMONEY (signed 32-bit fixed-point scaled by 10_000).
314#[cfg(feature = "decimal")]
315#[derive(Debug, Clone, Copy, PartialEq, Eq)]
316pub struct SmallMoney(pub rust_decimal::Decimal);
317
318/// Wrapper that sends its inner [`chrono::NaiveDateTime`] as SQL Server
319/// SMALLDATETIME (4-byte days-since-1900 + minutes-since-midnight) instead of
320/// DATETIME2.
321///
322/// SMALLDATETIME has minute precision — seconds are rounded to the nearest
323/// minute on the wire (30s rounds up per SQL Server semantics). The valid
324/// range is 1900-01-01 through 2079-06-06.
325#[cfg(feature = "chrono")]
326#[derive(Debug, Clone, Copy, PartialEq, Eq)]
327pub struct SmallDateTime(pub chrono::NaiveDateTime);
328
329#[cfg(feature = "decimal")]
330impl From<Money> for SqlValue {
331    fn from(v: Money) -> Self {
332        Self::Money(v.0)
333    }
334}
335
336#[cfg(feature = "decimal")]
337impl From<SmallMoney> for SqlValue {
338    fn from(v: SmallMoney) -> Self {
339        Self::SmallMoney(v.0)
340    }
341}
342
343#[cfg(feature = "chrono")]
344impl From<SmallDateTime> for SqlValue {
345    fn from(v: SmallDateTime) -> Self {
346        Self::SmallDateTime(v.0)
347    }
348}