Skip to main content

drizzle_mysql/values/
mod.rs

1//! `MySQL` values: [`MySQLValue`] (bound parameters and decoded cells), its
2//! owned form, and the insert and update field types used by generated
3//! models.
4
5mod conversions;
6mod insert;
7#[cfg(feature = "serde")]
8mod json;
9mod owned;
10mod update;
11
12pub use insert::{MySQLInsertValue, ValueWrapper};
13pub use owned::OwnedMySQLValue;
14pub use update::MySQLUpdateValue;
15
16use crate::prelude::*;
17use drizzle_core::{Dialect, MySQLDialect, SQL, SQLParam, ToSQL};
18
19/// A value in one of `MySQL`'s wire categories, possibly borrowing its bytes.
20///
21/// Used for bound parameters and for cells read from a row. The variants
22/// mirror the values of the `mysql` and `mysql_async` clients, so this crate
23/// does not depend on either. See [`OwnedMySQLValue`] for an owned form.
24#[derive(Debug, Clone, PartialEq, Default)]
25pub enum MySQLValue<'a> {
26    /// SQL `NULL`.
27    #[default]
28    Null,
29    /// Text, binary, decimal, JSON, enum, and set payloads.
30    Bytes(Cow<'a, [u8]>),
31    /// Signed integer payload.
32    Int(i64),
33    /// Unsigned integer payload.
34    UInt(u64),
35    /// Single-precision floating-point payload.
36    Float(f32),
37    /// Double-precision floating-point payload.
38    Double(f64),
39    /// Date, datetime, or timestamp payload.
40    Date {
41        /// Four-digit year.
42        year: u16,
43        /// Month in the range 1 through 12.
44        month: u8,
45        /// Day of the month.
46        day: u8,
47        /// Hour in the range 0 through 23.
48        hour: u8,
49        /// Minute in the range 0 through 59.
50        minute: u8,
51        /// Second in the range 0 through 59.
52        second: u8,
53        /// Fractional seconds in microseconds.
54        microseconds: u32,
55    },
56    /// Time or duration payload.
57    Time {
58        /// Whether the duration is negative.
59        negative: bool,
60        /// Whole days in the duration.
61        days: u32,
62        /// Remaining hours after whole days.
63        hours: u8,
64        /// Remaining minutes after whole hours.
65        minutes: u8,
66        /// Remaining seconds after whole minutes.
67        seconds: u8,
68        /// Fractional seconds in microseconds.
69        microseconds: u32,
70    },
71}
72
73impl MySQLValue<'_> {
74    /// Returns `true` if this value is SQL `NULL`.
75    #[must_use]
76    pub const fn is_null(&self) -> bool {
77        matches!(self, Self::Null)
78    }
79
80    /// Returns the bytes of a [`Bytes`](Self::Bytes) value (text, binary,
81    /// decimal, JSON, enum or set), or `None` for other variants.
82    #[must_use]
83    pub fn as_bytes(&self) -> Option<&[u8]> {
84        match self {
85            Self::Bytes(value) => Some(value.as_ref()),
86            _ => None,
87        }
88    }
89
90    /// Returns a borrowed view of this value.
91    #[must_use]
92    pub fn as_ref(&self) -> MySQLValue<'_> {
93        match self {
94            Self::Null => MySQLValue::Null,
95            Self::Bytes(value) => MySQLValue::Bytes(Cow::Borrowed(value.as_ref())),
96            Self::Int(value) => MySQLValue::Int(*value),
97            Self::UInt(value) => MySQLValue::UInt(*value),
98            Self::Float(value) => MySQLValue::Float(*value),
99            Self::Double(value) => MySQLValue::Double(*value),
100            Self::Date {
101                year,
102                month,
103                day,
104                hour,
105                minute,
106                second,
107                microseconds,
108            } => MySQLValue::Date {
109                year: *year,
110                month: *month,
111                day: *day,
112                hour: *hour,
113                minute: *minute,
114                second: *second,
115                microseconds: *microseconds,
116            },
117            Self::Time {
118                negative,
119                days,
120                hours,
121                minutes,
122                seconds,
123                microseconds,
124            } => MySQLValue::Time {
125                negative: *negative,
126                days: *days,
127                hours: *hours,
128                minutes: *minutes,
129                seconds: *seconds,
130                microseconds: *microseconds,
131            },
132        }
133    }
134
135    /// Converts this value into an owned representation.
136    #[must_use]
137    pub fn into_owned(self) -> OwnedMySQLValue {
138        self.into()
139    }
140}
141
142impl SQLParam for MySQLValue<'_> {
143    const DIALECT: Dialect = Dialect::MySQL;
144    type DialectMarker = MySQLDialect;
145
146    fn pagination_param(value: usize) -> Option<Self> {
147        u64::try_from(value).ok().map(Self::UInt)
148    }
149
150    fn write_literal(&self, buf: &mut String) -> bool {
151        use core::fmt::Write;
152        match self {
153            Self::Null => buf.push_str("NULL"),
154            Self::Int(value) => {
155                let _ = write!(buf, "{value}");
156            }
157            Self::UInt(value) => {
158                let _ = write!(buf, "{value}");
159            }
160            // MySQL has no NaN or infinity.
161            Self::Float(value) if !value.is_finite() => return false,
162            Self::Double(value) if !value.is_finite() => return false,
163            Self::Float(value) => {
164                let _ = write!(buf, "{value:?}");
165            }
166            Self::Double(value) => {
167                let _ = write!(buf, "{value:?}");
168            }
169            Self::Bytes(bytes) => match core::str::from_utf8(bytes) {
170                // Text is quoted with the same escaping the typed `query(..)`
171                // view definitions use; binary data is written in hex.
172                Ok(text) => {
173                    buf.push('\'');
174                    for ch in text.chars() {
175                        match ch {
176                            '\\' => buf.push_str("\\\\"),
177                            '\'' => buf.push_str("''"),
178                            '\0' => buf.push_str("\\0"),
179                            _ => buf.push(ch),
180                        }
181                    }
182                    buf.push('\'');
183                }
184                Err(_) => {
185                    buf.push_str("X'");
186                    for byte in bytes.iter() {
187                        let _ = write!(buf, "{byte:02X}");
188                    }
189                    buf.push('\'');
190                }
191            },
192            Self::Date {
193                year,
194                month,
195                day,
196                hour,
197                minute,
198                second,
199                microseconds,
200            } => {
201                let _ = write!(
202                    buf,
203                    "'{year:04}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02}.{microseconds:06}'"
204                );
205            }
206            Self::Time {
207                negative,
208                days,
209                hours,
210                minutes,
211                seconds,
212                microseconds,
213            } => {
214                let sign = if *negative { "-" } else { "" };
215                let hours = u64::from(*days) * 24 + u64::from(*hours);
216                let _ = write!(
217                    buf,
218                    "'{sign}{hours:02}:{minutes:02}:{seconds:02}.{microseconds:06}'"
219                );
220            }
221        }
222        true
223    }
224}
225
226impl<'a> ToSQL<'a, Self> for MySQLValue<'a> {
227    fn to_sql(&self) -> SQL<'a, Self> {
228        SQL::param(self.clone())
229    }
230}
231
232impl<'a> From<MySQLValue<'a>> for SQL<'a, MySQLValue<'a>> {
233    fn from(value: MySQLValue<'a>) -> Self {
234        SQL::param(value)
235    }
236}
237
238impl<'a, T> From<T> for MySQLValue<'a>
239where
240    T: crate::traits::DrizzleMySQLColumn,
241{
242    fn from(value: T) -> Self {
243        value.encode_owned().into()
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn borrowed_bytes_become_owned_without_changing_content() {
253        let value = MySQLValue::from("hello");
254        assert_eq!(value.as_bytes(), Some(b"hello".as_slice()));
255        assert_eq!(
256            value.into_owned(),
257            OwnedMySQLValue::Bytes(b"hello".to_vec())
258        );
259    }
260
261    #[test]
262    fn signed_and_unsigned_values_remain_distinct() {
263        assert_eq!(MySQLValue::from(-1_i64), MySQLValue::Int(-1));
264        assert_eq!(MySQLValue::from(u64::MAX), MySQLValue::UInt(u64::MAX));
265    }
266
267    #[test]
268    fn optional_values_encode_none_as_sql_null() {
269        assert_eq!(MySQLValue::from(Option::<u32>::None), MySQLValue::Null);
270        assert_eq!(MySQLValue::from(Some(7_u32)), MySQLValue::UInt(7));
271    }
272
273    #[test]
274    fn owned_values_accept_literals_and_optional_model_fields() {
275        assert_eq!(OwnedMySQLValue::from(-7_i32), OwnedMySQLValue::Int(-7));
276        assert_eq!(
277            OwnedMySQLValue::from("hello"),
278            OwnedMySQLValue::Bytes(b"hello".to_vec())
279        );
280        assert_eq!(
281            OwnedMySQLValue::from(Some(u64::MAX)),
282            OwnedMySQLValue::UInt(u64::MAX)
283        );
284        assert_eq!(
285            OwnedMySQLValue::from(Option::<String>::None),
286            OwnedMySQLValue::Null
287        );
288    }
289
290    #[cfg(feature = "chrono")]
291    #[test]
292    fn timestamp_values_are_normalized_to_utc() {
293        use chrono::{FixedOffset, TimeZone};
294
295        let offset = FixedOffset::east_opt(2 * 60 * 60).unwrap();
296        let local = offset.with_ymd_and_hms(2026, 8, 25, 15, 30, 45).unwrap();
297
298        assert_eq!(
299            MySQLValue::from(local),
300            MySQLValue::Date {
301                year: 2026,
302                month: 8,
303                day: 25,
304                hour: 13,
305                minute: 30,
306                second: 45,
307                microseconds: 0,
308            }
309        );
310    }
311
312    #[cfg(feature = "uuid")]
313    #[test]
314    fn uuid_values_use_the_compact_sixteen_byte_form() {
315        let uuid = uuid::Uuid::from_u128(0x12345678_90ab_cdef_1234_567890abcdef);
316        assert_eq!(MySQLValue::from(uuid).as_bytes().unwrap().len(), 16);
317    }
318}