1mod 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#[derive(Debug, Clone, PartialEq, Default)]
25pub enum MySQLValue<'a> {
26 #[default]
28 Null,
29 Bytes(Cow<'a, [u8]>),
31 Int(i64),
33 UInt(u64),
35 Float(f32),
37 Double(f64),
39 Date {
41 year: u16,
43 month: u8,
45 day: u8,
47 hour: u8,
49 minute: u8,
51 second: u8,
53 microseconds: u32,
55 },
56 Time {
58 negative: bool,
60 days: u32,
62 hours: u8,
64 minutes: u8,
66 seconds: u8,
68 microseconds: u32,
70 },
71}
72
73impl MySQLValue<'_> {
74 #[must_use]
76 pub const fn is_null(&self) -> bool {
77 matches!(self, Self::Null)
78 }
79
80 #[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 #[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 #[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 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 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}