1#[cfg(feature = "query")]
11use crate::traits::MySQLJsonStorage;
12use crate::{
13 prelude::*,
14 traits::DrizzleMySQLColumn,
15 values::{MySQLValue, OwnedMySQLValue},
16};
17use drizzle_core::{FromDrizzleRow, error::DrizzleError};
18
19#[cfg(not(feature = "std"))]
20use alloc::format;
21
22pub trait MySQLRowAccess {
29 fn value_at(&self, offset: usize) -> Result<Option<MySQLValue<'_>>, DrizzleError>;
35
36 fn is_bit_at(&self, _offset: usize) -> bool {
43 false
44 }
45}
46
47#[derive(Debug, Clone, Copy)]
70pub struct MySQLRow<'row, R: ?Sized> {
71 inner: &'row R,
72}
73
74impl<'row, R: MySQLRowAccess + ?Sized> MySQLRow<'row, R> {
75 #[must_use]
77 pub const fn new(inner: &'row R) -> Self {
78 Self { inner }
79 }
80
81 fn value_at(&self, offset: usize) -> Result<MySQLValue<'_>, DrizzleError> {
82 self.inner.value_at(offset)?.ok_or_else(|| {
83 DrizzleError::ConversionError(
84 format!("MySQL row has no available column at offset {offset}").into(),
85 )
86 })
87 }
88
89 fn decode_at<T: DecodeMySQLValue>(&self, offset: usize) -> Result<T, DrizzleError> {
90 let value = self.value_at(offset)?;
91 if self.inner.is_bit_at(offset) {
92 T::decode_bit(value)
93 } else {
94 T::decode(value)
95 }
96 }
97
98 pub fn is_null_at(&self, offset: usize) -> Result<bool, DrizzleError> {
105 Ok(self.value_at(offset)?.is_null())
106 }
107
108 #[doc(hidden)]
115 pub fn decode_column<T: DrizzleMySQLColumn>(&self, offset: usize) -> Result<T, DrizzleError> {
116 T::decode(self.value_at(offset)?)
117 }
118}
119
120impl MySQLRowAccess for [OwnedMySQLValue] {
121 fn value_at(&self, offset: usize) -> Result<Option<MySQLValue<'_>>, DrizzleError> {
122 Ok(self.get(offset).map(MySQLValue::from))
123 }
124}
125
126impl MySQLRowAccess for Vec<OwnedMySQLValue> {
127 fn value_at(&self, offset: usize) -> Result<Option<MySQLValue<'_>>, DrizzleError> {
128 self.as_slice().value_at(offset)
129 }
130}
131
132impl MySQLRowAccess for [MySQLValue<'_>] {
133 fn value_at(&self, offset: usize) -> Result<Option<MySQLValue<'_>>, DrizzleError> {
134 Ok(self.get(offset).map(MySQLValue::as_ref))
135 }
136}
137
138#[doc(hidden)]
139pub trait DecodeMySQLValue: Sized {
140 const EXPECTED: &'static str;
141
142 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError>;
143
144 fn decode_bit(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
145 Self::decode(value)
146 }
147}
148
149impl<T: DrizzleMySQLColumn> DecodeMySQLValue for T {
150 const EXPECTED: &'static str = T::SQL_TYPE;
151
152 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
153 T::decode(value)
154 }
155}
156
157#[cfg(feature = "query")]
163#[doc(hidden)]
164pub fn decode_blob<T: DecodeMySQLValue>(projected: &serde_json::Value) -> Result<T, DrizzleError> {
165 T::decode(MySQLValue::from(projected_binary(projected)?))
166}
167
168#[cfg(feature = "query")]
169fn projected_binary(projected: &serde_json::Value) -> Result<Vec<u8>, DrizzleError> {
170 let object = projected.as_object().ok_or_else(|| {
171 DrizzleError::ConversionError("projected MySQL binary value is not an object".into())
172 })?;
173 if object
174 .get("$drizzle_storage")
175 .and_then(serde_json::Value::as_str)
176 != Some("blob")
177 {
178 return Err(DrizzleError::ConversionError(
179 "projected MySQL binary value has an invalid storage tag".into(),
180 ));
181 }
182 let hex = object
183 .get("$drizzle_value")
184 .and_then(serde_json::Value::as_str)
185 .ok_or_else(|| {
186 DrizzleError::ConversionError(
187 "projected MySQL binary value is missing its hexadecimal payload".into(),
188 )
189 })?;
190 if hex.len() % 2 != 0 {
191 return Err(DrizzleError::ConversionError(
192 "projected MySQL binary value has an odd-length hexadecimal payload".into(),
193 ));
194 }
195
196 let mut bytes = Vec::with_capacity(hex.len() / 2);
197 for pair in hex.as_bytes().chunks(2) {
198 let high = decode_hex_nibble(pair[0])?;
199 let low = decode_hex_nibble(pair[1])?;
200 bytes.push((high << 4) | low);
201 }
202 Ok(bytes)
203}
204
205#[cfg(feature = "query")]
208#[doc(hidden)]
209pub fn decode_text<T: DecodeMySQLValue>(projected: &serde_json::Value) -> Result<T, DrizzleError> {
210 let text = projected.as_str().ok_or_else(|| {
211 DrizzleError::ConversionError("projected MySQL text value is not a string".into())
212 })?;
213 T::decode(MySQLValue::from(text))
214}
215
216#[cfg(feature = "query")]
218#[doc(hidden)]
219pub fn decode_projected<T: DrizzleMySQLColumn>(
220 projected: &serde_json::Value,
221) -> Result<T, DrizzleError> {
222 T::decode_json(projected)
223}
224
225#[cfg(feature = "query")]
226#[doc(hidden)]
227pub fn projected_value(
228 projected: &serde_json::Value,
229 storage: MySQLJsonStorage,
230) -> Result<OwnedMySQLValue, DrizzleError> {
231 match storage {
232 MySQLJsonStorage::Signed => projected
233 .as_i64()
234 .map(OwnedMySQLValue::Int)
235 .ok_or_else(|| projected_error("signed integer")),
236 MySQLJsonStorage::Unsigned => projected
237 .as_u64()
238 .map(OwnedMySQLValue::UInt)
239 .ok_or_else(|| projected_error("unsigned integer")),
240 MySQLJsonStorage::Double => projected
241 .as_f64()
242 .map(OwnedMySQLValue::Double)
243 .ok_or_else(|| projected_error("number")),
244 MySQLJsonStorage::Boolean => projected
245 .as_bool()
246 .map(i64::from)
247 .or_else(|| projected.as_i64().filter(|value| matches!(value, 0 | 1)))
248 .map(OwnedMySQLValue::Int)
249 .ok_or_else(|| projected_error("boolean")),
250 MySQLJsonStorage::Text => projected
251 .as_str()
252 .map(|value| OwnedMySQLValue::Bytes(value.as_bytes().to_vec()))
253 .ok_or_else(|| projected_error("string")),
254 MySQLJsonStorage::SignedText => projected_text(projected)?
255 .parse()
256 .map(OwnedMySQLValue::Int)
257 .map_err(|_| projected_error("signed integer string")),
258 MySQLJsonStorage::UnsignedText => projected_text(projected)?
259 .parse()
260 .map(OwnedMySQLValue::UInt)
261 .map_err(|_| projected_error("unsigned integer string")),
262 MySQLJsonStorage::FloatText => projected_text(projected)?
263 .parse()
264 .map(OwnedMySQLValue::Float)
265 .map_err(|_| projected_error("float string")),
266 MySQLJsonStorage::Binary => projected_binary(projected).map(OwnedMySQLValue::Bytes),
267 MySQLJsonStorage::Json => serde_json::to_vec(projected)
268 .map(OwnedMySQLValue::Bytes)
269 .map_err(Into::into),
270 MySQLJsonStorage::Date => projected_date(projected_text(projected)?),
271 MySQLJsonStorage::Time => projected_time(projected_text(projected)?),
272 MySQLJsonStorage::DateTime => projected_datetime(projected_text(projected)?),
273 }
274}
275
276#[cfg(feature = "query")]
277fn projected_text(projected: &serde_json::Value) -> Result<&str, DrizzleError> {
278 projected.as_str().ok_or_else(|| projected_error("string"))
279}
280
281#[cfg(feature = "query")]
282fn projected_date(value: &str) -> Result<OwnedMySQLValue, DrizzleError> {
283 let mut parts = value.split('-');
284 let year = parse_projected_part(parts.next(), "date")?;
285 let month = parse_projected_part(parts.next(), "date")?;
286 let day = parse_projected_part(parts.next(), "date")?;
287 if parts.next().is_some() {
288 return Err(projected_error("date string"));
289 }
290 Ok(OwnedMySQLValue::Date {
291 year,
292 month,
293 day,
294 hour: 0,
295 minute: 0,
296 second: 0,
297 microseconds: 0,
298 })
299}
300
301#[cfg(feature = "query")]
302fn projected_datetime(value: &str) -> Result<OwnedMySQLValue, DrizzleError> {
303 let (date, time) = value
304 .split_once(' ')
305 .ok_or_else(|| projected_error("datetime string"))?;
306 let OwnedMySQLValue::Date {
307 year, month, day, ..
308 } = projected_date(date)?
309 else {
310 unreachable!();
311 };
312 let (hour, minute, second, microseconds) = projected_clock(time, "datetime")?;
313 let hour = u8::try_from(hour).map_err(|_| projected_error("datetime string"))?;
314 Ok(OwnedMySQLValue::Date {
315 year,
316 month,
317 day,
318 hour,
319 minute,
320 second,
321 microseconds,
322 })
323}
324
325#[cfg(feature = "query")]
326fn projected_time(value: &str) -> Result<OwnedMySQLValue, DrizzleError> {
327 let (negative, value) = value
328 .strip_prefix('-')
329 .map_or((false, value), |value| (true, value));
330 let (hours, minutes, seconds, microseconds) = projected_clock(value, "time")?;
331 Ok(OwnedMySQLValue::Time {
332 negative,
333 days: hours / 24,
334 hours: u8::try_from(hours % 24).expect("hour remainder is below 24"),
335 minutes,
336 seconds,
337 microseconds,
338 })
339}
340
341#[cfg(feature = "query")]
342fn projected_clock(value: &str, kind: &str) -> Result<(u32, u8, u8, u32), DrizzleError> {
343 let mut parts = value.split(':');
344 let hour = parse_projected_part(parts.next(), kind)?;
345 let minute = parse_projected_part(parts.next(), kind)?;
346 let second = parts.next().ok_or_else(|| projected_error(kind))?;
347 if parts.next().is_some() {
348 return Err(projected_error(kind));
349 }
350 let (second, fraction) = second.split_once('.').unwrap_or((second, ""));
351 let second = second.parse().map_err(|_| projected_error(kind))?;
352 if minute > 59 || second > 59 {
353 return Err(projected_error(kind));
354 }
355 if fraction.len() > 6 || !fraction.bytes().all(|byte| byte.is_ascii_digit()) {
356 return Err(projected_error(kind));
357 }
358 let microseconds = if fraction.is_empty() {
359 0
360 } else {
361 let value: u32 = fraction.parse().map_err(|_| projected_error(kind))?;
362 value * 10_u32.pow(u32::try_from(6 - fraction.len()).expect("bounded fraction"))
363 };
364 Ok((hour, minute, second, microseconds))
365}
366
367#[cfg(feature = "query")]
368fn parse_projected_part<T: core::str::FromStr>(
369 value: Option<&str>,
370 kind: &str,
371) -> Result<T, DrizzleError> {
372 value
373 .ok_or_else(|| projected_error(kind))?
374 .parse()
375 .map_err(|_| projected_error(kind))
376}
377
378#[cfg(feature = "query")]
379fn projected_error(expected: &str) -> DrizzleError {
380 DrizzleError::ConversionError(format!("projected MySQL value is not a JSON {expected}").into())
381}
382
383#[cfg(feature = "query")]
384fn decode_hex_nibble(value: u8) -> Result<u8, DrizzleError> {
385 match value {
386 b'0'..=b'9' => Ok(value - b'0'),
387 b'a'..=b'f' => Ok(value - b'a' + 10),
388 b'A'..=b'F' => Ok(value - b'A' + 10),
389 _ => Err(DrizzleError::ConversionError(
390 "projected MySQL binary value contains non-hexadecimal text".into(),
391 )),
392 }
393}
394
395fn conversion_error(expected: &str, value: &MySQLValue<'_>) -> DrizzleError {
396 DrizzleError::ConversionError(
397 format!(
398 "expected {expected}, got MySQL {}",
399 match value {
400 MySQLValue::Null => "NULL",
401 MySQLValue::Bytes(_) => "bytes",
402 MySQLValue::Int(_) => "signed integer",
403 MySQLValue::UInt(_) => "unsigned integer",
404 MySQLValue::Float(_) => "float",
405 MySQLValue::Double(_) => "double",
406 MySQLValue::Date { .. } => "date/datetime",
407 MySQLValue::Time { .. } => "time",
408 }
409 )
410 .into(),
411 )
412}
413
414fn utf8(value: &[u8]) -> Result<&str, DrizzleError> {
415 core::str::from_utf8(value)
416 .map_err(|error| DrizzleError::ConversionError(error.to_string().into()))
417}
418
419macro_rules! impl_integer_decode {
420 ($($ty:ty),+ $(,)?) => {$(
421 impl DecodeMySQLValue for $ty {
422 const EXPECTED: &'static str = stringify!($ty);
423
424 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
425 match value {
426 MySQLValue::Int(value) => Self::try_from(value).map_err(Into::into),
427 MySQLValue::UInt(value) => Self::try_from(value).map_err(Into::into),
428 MySQLValue::Bytes(value) => utf8(value.as_ref())?
429 .parse::<Self>()
430 .map_err(Into::into),
431 other => Err(conversion_error(Self::EXPECTED, &other)),
432 }
433 }
434
435 fn decode_bit(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
436 match value {
437 MySQLValue::Bytes(value) => Self::try_from(mysql_bit_to_u64(value.as_ref())?)
438 .map_err(Into::into),
439 other => Self::decode(other),
440 }
441 }
442 }
443 )+};
444}
445
446impl_integer_decode!(i8, i16, i32, i64, isize, u8, u16, u32, u64, usize);
447
448impl DecodeMySQLValue for f32 {
449 const EXPECTED: &'static str = "f32";
450
451 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
452 match value {
453 MySQLValue::Float(value) => Ok(value),
454 MySQLValue::Bytes(value) => utf8(value.as_ref())?.parse().map_err(Into::into),
455 other => Err(conversion_error(Self::EXPECTED, &other)),
456 }
457 }
458}
459
460impl DecodeMySQLValue for f64 {
461 const EXPECTED: &'static str = "f64";
462
463 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
464 match value {
465 MySQLValue::Double(value) => Ok(value),
466 MySQLValue::Float(value) => Ok(value.into()),
467 MySQLValue::Bytes(value) => utf8(value.as_ref())?.parse().map_err(Into::into),
468 other => Err(conversion_error(Self::EXPECTED, &other)),
469 }
470 }
471}
472
473impl DecodeMySQLValue for bool {
474 const EXPECTED: &'static str = "bool";
475
476 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
477 match value {
478 MySQLValue::Int(value) => Ok(value != 0),
479 MySQLValue::UInt(value) => Ok(value != 0),
480 MySQLValue::Bytes(value) if value.as_ref() == b"0" => Ok(false),
481 MySQLValue::Bytes(value) if value.as_ref() == b"1" => Ok(true),
482 other => Err(conversion_error(Self::EXPECTED, &other)),
483 }
484 }
485
486 fn decode_bit(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
487 match value {
488 MySQLValue::Bytes(value) => Ok(mysql_bit_to_u64(value.as_ref())? != 0),
489 other => Self::decode(other),
490 }
491 }
492}
493
494impl DecodeMySQLValue for String {
495 const EXPECTED: &'static str = "UTF-8 text";
496
497 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
498 match value {
499 MySQLValue::Bytes(value) => Ok(String::from(utf8(value.as_ref())?)),
500 other => Err(conversion_error(Self::EXPECTED, &other)),
501 }
502 }
503}
504
505impl DecodeMySQLValue for char {
506 const EXPECTED: &'static str = "one Unicode scalar";
507
508 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
509 let text = String::decode(value)?;
510 let mut chars = text.chars();
511 match (chars.next(), chars.next()) {
512 (Some(value), None) => Ok(value),
513 _ => Err(DrizzleError::ConversionError(
514 "expected exactly one Unicode scalar".into(),
515 )),
516 }
517 }
518}
519
520impl DecodeMySQLValue for Vec<u8> {
521 const EXPECTED: &'static str = "bytes";
522
523 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
524 match value {
525 MySQLValue::Bytes(value) => Ok(value.into_owned()),
526 other => Err(conversion_error(Self::EXPECTED, &other)),
527 }
528 }
529}
530
531impl<const N: usize> DecodeMySQLValue for [u8; N] {
532 const EXPECTED: &'static str = "fixed-size byte array";
533
534 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
535 let value = Vec::<u8>::decode(value)?;
536 value.try_into().map_err(|value: Vec<u8>| {
537 DrizzleError::ConversionError(
538 format!("expected {N} bytes, got {} bytes", value.len(),).into(),
539 )
540 })
541 }
542}
543
544impl<const N: usize> DecodeMySQLValue for [char; N] {
545 const EXPECTED: &'static str = "fixed-size character array";
546
547 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
548 let value = String::decode(value)?;
549 let chars: Vec<char> = value.chars().collect();
550 chars.try_into().map_err(|chars: Vec<char>| {
551 DrizzleError::ConversionError(
552 format!("expected {N} characters, got {} characters", chars.len(),).into(),
553 )
554 })
555 }
556}
557
558macro_rules! impl_row_decode {
559 ($($ty:ty),+ $(,)?) => {$(
560 impl<'row, R: MySQLRowAccess + ?Sized> FromDrizzleRow<MySQLRow<'row, R>> for $ty {
561 const COLUMN_COUNT: usize = 1;
562
563 fn from_row_at(
564 row: &MySQLRow<'row, R>,
565 offset: usize,
566 ) -> Result<Self, DrizzleError> {
567 row.decode_at::<$ty>(offset)
568 }
569 }
570 )+};
571}
572
573impl_row_decode!(
574 i8,
575 i16,
576 i32,
577 i64,
578 isize,
579 u8,
580 u16,
581 u32,
582 u64,
583 usize,
584 f32,
585 f64,
586 bool,
587 char,
588 String,
589 Vec<u8>,
590);
591
592impl<'row, R: MySQLRowAccess + ?Sized, const N: usize> FromDrizzleRow<MySQLRow<'row, R>>
593 for [u8; N]
594{
595 const COLUMN_COUNT: usize = 1;
596
597 fn from_row_at(row: &MySQLRow<'row, R>, offset: usize) -> Result<Self, DrizzleError> {
598 row.decode_at::<Self>(offset)
599 }
600}
601
602impl<'row, R: MySQLRowAccess + ?Sized, const N: usize> FromDrizzleRow<MySQLRow<'row, R>>
603 for [char; N]
604{
605 const COLUMN_COUNT: usize = 1;
606
607 fn from_row_at(row: &MySQLRow<'row, R>, offset: usize) -> Result<Self, DrizzleError> {
608 row.decode_at::<Self>(offset)
609 }
610}
611
612impl<'row, R, T> FromDrizzleRow<MySQLRow<'row, R>> for Option<T>
613where
614 R: MySQLRowAccess + ?Sized,
615 T: FromDrizzleRow<MySQLRow<'row, R>>,
616{
617 const COLUMN_COUNT: usize = T::COLUMN_COUNT;
618
619 fn from_row_at(row: &MySQLRow<'row, R>, offset: usize) -> Result<Self, DrizzleError> {
622 for column in offset..offset + T::COLUMN_COUNT.max(1) {
623 if !row.is_null_at(column)? {
624 return T::from_row_at(row, offset).map(Some);
625 }
626 }
627 Ok(None)
628 }
629}
630
631#[cfg(feature = "compact-str")]
632impl DecodeMySQLValue for compact_str::CompactString {
633 const EXPECTED: &'static str = "UTF-8 text";
634
635 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
636 String::decode(value).map(Into::into)
637 }
638}
639
640#[cfg(feature = "compact-str")]
641impl_row_decode!(compact_str::CompactString);
642
643#[cfg(feature = "arrayvec")]
644impl<const N: usize> DecodeMySQLValue for arrayvec::ArrayString<N> {
645 const EXPECTED: &'static str = "bounded UTF-8 text";
646
647 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
648 let value = String::decode(value)?;
649 Self::try_from(value.as_str())
650 .map_err(|error| DrizzleError::ConversionError(error.to_string().into()))
651 }
652}
653
654#[cfg(feature = "arrayvec")]
655impl<'row, R: MySQLRowAccess + ?Sized, const N: usize> FromDrizzleRow<MySQLRow<'row, R>>
656 for arrayvec::ArrayString<N>
657{
658 const COLUMN_COUNT: usize = 1;
659
660 fn from_row_at(row: &MySQLRow<'row, R>, offset: usize) -> Result<Self, DrizzleError> {
661 row.decode_at::<Self>(offset)
662 }
663}
664
665#[cfg(feature = "arrayvec")]
666impl<const N: usize> DecodeMySQLValue for arrayvec::ArrayVec<u8, N> {
667 const EXPECTED: &'static str = "bounded bytes";
668
669 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
670 let value = Vec::<u8>::decode(value)?;
671 Self::try_from(value.as_slice())
672 .map_err(|error| DrizzleError::ConversionError(error.to_string().into()))
673 }
674}
675
676#[cfg(feature = "arrayvec")]
677impl<'row, R: MySQLRowAccess + ?Sized, const N: usize> FromDrizzleRow<MySQLRow<'row, R>>
678 for arrayvec::ArrayVec<u8, N>
679{
680 const COLUMN_COUNT: usize = 1;
681
682 fn from_row_at(row: &MySQLRow<'row, R>, offset: usize) -> Result<Self, DrizzleError> {
683 row.decode_at::<Self>(offset)
684 }
685}
686
687#[cfg(feature = "smallvec")]
688impl<const N: usize> DecodeMySQLValue for smallvec::SmallVec<[u8; N]> {
689 const EXPECTED: &'static str = "bytes";
690
691 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
692 Ok(Vec::<u8>::decode(value)?.into())
693 }
694}
695
696#[cfg(feature = "smallvec")]
697impl<'row, R: MySQLRowAccess + ?Sized, const N: usize> FromDrizzleRow<MySQLRow<'row, R>>
698 for smallvec::SmallVec<[u8; N]>
699{
700 const COLUMN_COUNT: usize = 1;
701
702 fn from_row_at(row: &MySQLRow<'row, R>, offset: usize) -> Result<Self, DrizzleError> {
703 row.decode_at::<Self>(offset)
704 }
705}
706
707#[cfg(feature = "bytes")]
708impl DecodeMySQLValue for bytes::Bytes {
709 const EXPECTED: &'static str = "bytes";
710
711 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
712 Vec::<u8>::decode(value).map(Into::into)
713 }
714}
715
716#[cfg(feature = "bytes")]
717impl DecodeMySQLValue for bytes::BytesMut {
718 const EXPECTED: &'static str = "bytes";
719
720 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
721 Vec::<u8>::decode(value).map(|value| Self::from(value.as_slice()))
722 }
723}
724
725#[cfg(feature = "bytes")]
726impl_row_decode!(bytes::Bytes, bytes::BytesMut);
727
728#[cfg(feature = "serde")]
729impl DecodeMySQLValue for serde_json::Value {
730 const EXPECTED: &'static str = "JSON text";
731
732 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
733 match value {
734 MySQLValue::Bytes(value) => serde_json::from_slice(value.as_ref()).map_err(Into::into),
735 other => Err(conversion_error(Self::EXPECTED, &other)),
736 }
737 }
738}
739
740#[cfg(feature = "serde")]
741impl_row_decode!(serde_json::Value);
742
743#[cfg(feature = "uuid")]
744impl DecodeMySQLValue for uuid::Uuid {
745 const EXPECTED: &'static str = "16-byte or textual UUID";
746
747 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
748 match value {
749 MySQLValue::Bytes(value) if value.len() == 16 => {
750 Self::from_slice(value.as_ref()).map_err(Into::into)
751 }
752 MySQLValue::Bytes(value) => Self::parse_str(utf8(value.as_ref())?).map_err(Into::into),
753 other => Err(conversion_error(Self::EXPECTED, &other)),
754 }
755 }
756}
757
758#[cfg(feature = "uuid")]
759impl_row_decode!(uuid::Uuid);
760
761#[cfg(feature = "rust-decimal")]
762impl DecodeMySQLValue for rust_decimal::Decimal {
763 const EXPECTED: &'static str = "decimal text within rust_decimal range";
764
765 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
766 match value {
767 MySQLValue::Bytes(value) => {
768 utf8(value.as_ref())?
769 .parse()
770 .map_err(|error: rust_decimal::Error| {
771 DrizzleError::ConversionError(error.to_string().into())
772 })
773 }
774 other => Err(conversion_error(Self::EXPECTED, &other)),
775 }
776 }
777}
778
779#[cfg(feature = "rust-decimal")]
780impl_row_decode!(rust_decimal::Decimal);
781
782#[cfg(feature = "chrono")]
783fn chrono_date(year: u16, month: u8, day: u8) -> Result<chrono::NaiveDate, DrizzleError> {
784 chrono::NaiveDate::from_ymd_opt(i32::from(year), u32::from(month), u32::from(day)).ok_or_else(
785 || {
786 DrizzleError::ConversionError(
787 format!("invalid MySQL date {year:04}-{month:02}-{day:02}").into(),
788 )
789 },
790 )
791}
792
793#[cfg(feature = "chrono")]
794fn chrono_time(
795 negative: bool,
796 days: u32,
797 hours: u8,
798 minutes: u8,
799 seconds: u8,
800 microseconds: u32,
801) -> Result<chrono::NaiveTime, DrizzleError> {
802 if negative || days != 0 {
803 return Err(DrizzleError::ConversionError(
804 "MySQL duration outside a single non-negative day cannot decode as chrono::NaiveTime"
805 .into(),
806 ));
807 }
808 chrono::NaiveTime::from_hms_micro_opt(
809 u32::from(hours),
810 u32::from(minutes),
811 u32::from(seconds),
812 microseconds,
813 )
814 .ok_or_else(|| DrizzleError::ConversionError("invalid MySQL time".into()))
815}
816
817#[cfg(feature = "chrono")]
818impl DecodeMySQLValue for chrono::NaiveDate {
819 const EXPECTED: &'static str = "MySQL DATE";
820
821 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
822 match value {
823 MySQLValue::Date {
824 year, month, day, ..
825 } => chrono_date(year, month, day),
826 MySQLValue::Bytes(value) => {
827 utf8(value.as_ref())?
828 .parse()
829 .map_err(|error: chrono::ParseError| {
830 DrizzleError::ConversionError(error.to_string().into())
831 })
832 }
833 other => Err(conversion_error(Self::EXPECTED, &other)),
834 }
835 }
836}
837
838#[cfg(feature = "chrono")]
839impl DecodeMySQLValue for chrono::NaiveTime {
840 const EXPECTED: &'static str = "MySQL TIME within one day";
841
842 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
843 match value {
844 MySQLValue::Time {
845 negative,
846 days,
847 hours,
848 minutes,
849 seconds,
850 microseconds,
851 } => chrono_time(negative, days, hours, minutes, seconds, microseconds),
852 MySQLValue::Bytes(value) => {
853 utf8(value.as_ref())?
854 .parse()
855 .map_err(|error: chrono::ParseError| {
856 DrizzleError::ConversionError(error.to_string().into())
857 })
858 }
859 other => Err(conversion_error(Self::EXPECTED, &other)),
860 }
861 }
862}
863
864#[cfg(feature = "chrono")]
865impl DecodeMySQLValue for chrono::NaiveDateTime {
866 const EXPECTED: &'static str = "MySQL DATETIME/TIMESTAMP";
867
868 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
869 match value {
870 MySQLValue::Date {
871 year,
872 month,
873 day,
874 hour,
875 minute,
876 second,
877 microseconds,
878 } => chrono_date(year, month, day)?
879 .and_hms_micro_opt(
880 u32::from(hour),
881 u32::from(minute),
882 u32::from(second),
883 microseconds,
884 )
885 .ok_or_else(|| DrizzleError::ConversionError("invalid MySQL datetime".into())),
886 MySQLValue::Bytes(value) => {
887 let value = utf8(value.as_ref())?;
888 chrono::NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S%.f")
889 .or_else(|_| value.parse())
890 .map_err(|error: chrono::ParseError| {
891 DrizzleError::ConversionError(error.to_string().into())
892 })
893 }
894 other => Err(conversion_error(Self::EXPECTED, &other)),
895 }
896 }
897}
898
899fn mysql_bit_to_u64(value: &[u8]) -> Result<u64, DrizzleError> {
900 if value.len() > core::mem::size_of::<u64>() {
901 return Err(DrizzleError::ConversionError(
902 format!("MySQL BIT value exceeds 64 bits: {} bytes", value.len()).into(),
903 ));
904 }
905 Ok(value
906 .iter()
907 .fold(0_u64, |result, byte| (result << 8) | u64::from(*byte)))
908}
909
910#[cfg(feature = "chrono")]
911impl DecodeMySQLValue for chrono::DateTime<chrono::Utc> {
912 const EXPECTED: &'static str = "UTC MySQL TIMESTAMP";
913
914 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
915 chrono::NaiveDateTime::decode(value)
916 .map(|value| Self::from_naive_utc_and_offset(value, chrono::Utc))
917 }
918}
919
920#[cfg(feature = "chrono")]
921impl DecodeMySQLValue for chrono::DateTime<chrono::FixedOffset> {
922 const EXPECTED: &'static str = "UTC MySQL TIMESTAMP";
923
924 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
925 let offset = chrono::FixedOffset::east_opt(0)
926 .ok_or_else(|| DrizzleError::ConversionError("invalid UTC offset".into()))?;
927 chrono::NaiveDateTime::decode(value)
928 .map(|value| Self::from_naive_utc_and_offset(value, offset))
929 }
930}
931
932#[cfg(feature = "chrono")]
933impl_row_decode!(
934 chrono::NaiveDate,
935 chrono::NaiveTime,
936 chrono::NaiveDateTime,
937 chrono::DateTime<chrono::Utc>,
938 chrono::DateTime<chrono::FixedOffset>,
939);
940
941#[cfg(feature = "time")]
942fn time_date(year: u16, month: u8, day: u8) -> Result<time::Date, DrizzleError> {
943 let month = time::Month::try_from(month)
944 .map_err(|error| DrizzleError::ConversionError(error.to_string().into()))?;
945 time::Date::from_calendar_date(i32::from(year), month, day)
946 .map_err(|error| DrizzleError::ConversionError(error.to_string().into()))
947}
948
949#[cfg(feature = "time")]
950fn time_time(
951 negative: bool,
952 days: u32,
953 hours: u8,
954 minutes: u8,
955 seconds: u8,
956 microseconds: u32,
957) -> Result<time::Time, DrizzleError> {
958 if negative || days != 0 {
959 return Err(DrizzleError::ConversionError(
960 "MySQL duration outside a single non-negative day cannot decode as time::Time".into(),
961 ));
962 }
963 time::Time::from_hms_micro(hours, minutes, seconds, microseconds)
964 .map_err(|error| DrizzleError::ConversionError(error.to_string().into()))
965}
966
967#[cfg(feature = "time")]
968impl DecodeMySQLValue for time::Date {
969 const EXPECTED: &'static str = "MySQL DATE";
970
971 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
972 match value {
973 MySQLValue::Date {
974 year, month, day, ..
975 } => time_date(year, month, day),
976 MySQLValue::Bytes(value) => time::Date::parse(
977 utf8(value.as_ref())?,
978 &time::format_description::well_known::Iso8601::DATE,
979 )
980 .map_err(|error| DrizzleError::ConversionError(error.to_string().into())),
981 other => Err(conversion_error(Self::EXPECTED, &other)),
982 }
983 }
984}
985
986#[cfg(feature = "time")]
987impl DecodeMySQLValue for time::Time {
988 const EXPECTED: &'static str = "MySQL TIME within one day";
989
990 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
991 match value {
992 MySQLValue::Time {
993 negative,
994 days,
995 hours,
996 minutes,
997 seconds,
998 microseconds,
999 } => time_time(negative, days, hours, minutes, seconds, microseconds),
1000 MySQLValue::Bytes(value) => {
1001 let text = utf8(value.as_ref())?;
1002 let description =
1003 time::macros::format_description!("[hour]:[minute]:[second].[subsecond]");
1004 time::Time::parse(text, description)
1005 .or_else(|_| {
1006 time::Time::parse(
1007 text,
1008 time::macros::format_description!("[hour]:[minute]:[second]"),
1009 )
1010 })
1011 .map_err(|error| DrizzleError::ConversionError(error.to_string().into()))
1012 }
1013 other => Err(conversion_error(Self::EXPECTED, &other)),
1014 }
1015 }
1016}
1017
1018#[cfg(feature = "time")]
1019impl DecodeMySQLValue for time::PrimitiveDateTime {
1020 const EXPECTED: &'static str = "MySQL DATETIME/TIMESTAMP";
1021
1022 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
1023 match value {
1024 MySQLValue::Date {
1025 year,
1026 month,
1027 day,
1028 hour,
1029 minute,
1030 second,
1031 microseconds,
1032 } => Ok(Self::new(
1033 time_date(year, month, day)?,
1034 time::Time::from_hms_micro(hour, minute, second, microseconds)
1035 .map_err(|error| DrizzleError::ConversionError(error.to_string().into()))?,
1036 )),
1037 MySQLValue::Bytes(value) => {
1038 let text = utf8(value.as_ref())?;
1039 Self::parse(
1040 text,
1041 time::macros::format_description!(
1042 "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond]"
1043 ),
1044 )
1045 .or_else(|_| {
1046 Self::parse(
1047 text,
1048 time::macros::format_description!(
1049 "[year]-[month]-[day] [hour]:[minute]:[second]"
1050 ),
1051 )
1052 })
1053 .map_err(|error| DrizzleError::ConversionError(error.to_string().into()))
1054 }
1055 other => Err(conversion_error(Self::EXPECTED, &other)),
1056 }
1057 }
1058}
1059
1060#[cfg(feature = "time")]
1061impl DecodeMySQLValue for time::OffsetDateTime {
1062 const EXPECTED: &'static str = "UTC MySQL TIMESTAMP";
1063
1064 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
1065 time::PrimitiveDateTime::decode(value).map(|value| value.assume_utc())
1066 }
1067}
1068
1069#[cfg(feature = "time")]
1070impl_row_decode!(
1071 time::Date,
1072 time::Time,
1073 time::PrimitiveDateTime,
1074 time::OffsetDateTime,
1075);
1076
1077#[cfg(feature = "jiff")]
1078fn jiff_error(error: jiff::Error) -> DrizzleError {
1079 DrizzleError::ConversionError(error.to_string().into())
1080}
1081
1082#[cfg(feature = "jiff")]
1083fn jiff_out_of_range() -> DrizzleError {
1084 DrizzleError::ConversionError("MySQL date or time part is out of range for jiff".into())
1085}
1086
1087#[cfg(feature = "jiff")]
1088fn jiff_date(year: u16, month: u8, day: u8) -> Result<jiff::civil::Date, DrizzleError> {
1089 jiff::civil::Date::new(
1090 i16::try_from(year).map_err(|_| jiff_out_of_range())?,
1091 i8::try_from(month).map_err(|_| jiff_out_of_range())?,
1092 i8::try_from(day).map_err(|_| jiff_out_of_range())?,
1093 )
1094 .map_err(jiff_error)
1095}
1096
1097#[cfg(feature = "jiff")]
1098fn jiff_time(
1099 hours: u8,
1100 minutes: u8,
1101 seconds: u8,
1102 microseconds: u32,
1103) -> Result<jiff::civil::Time, DrizzleError> {
1104 let nanoseconds = i32::try_from(microseconds)
1105 .ok()
1106 .and_then(|micros| micros.checked_mul(1_000))
1107 .ok_or_else(jiff_out_of_range)?;
1108 jiff::civil::Time::new(
1109 i8::try_from(hours).map_err(|_| jiff_out_of_range())?,
1110 i8::try_from(minutes).map_err(|_| jiff_out_of_range())?,
1111 i8::try_from(seconds).map_err(|_| jiff_out_of_range())?,
1112 nanoseconds,
1113 )
1114 .map_err(jiff_error)
1115}
1116
1117#[cfg(feature = "jiff")]
1118impl DecodeMySQLValue for jiff::civil::Date {
1119 const EXPECTED: &'static str = "MySQL DATE";
1120
1121 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
1122 match value {
1123 MySQLValue::Date {
1124 year, month, day, ..
1125 } => jiff_date(year, month, day),
1126 MySQLValue::Bytes(value) => utf8(value.as_ref())?.parse().map_err(jiff_error),
1127 other => Err(conversion_error(Self::EXPECTED, &other)),
1128 }
1129 }
1130}
1131
1132#[cfg(feature = "jiff")]
1133impl DecodeMySQLValue for jiff::civil::Time {
1134 const EXPECTED: &'static str = "MySQL TIME within one day";
1135
1136 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
1137 match value {
1138 MySQLValue::Time {
1139 negative,
1140 days,
1141 hours,
1142 minutes,
1143 seconds,
1144 microseconds,
1145 } => {
1146 if negative || days != 0 {
1147 return Err(DrizzleError::ConversionError(
1148 "MySQL duration outside a single non-negative day cannot decode as \
1149 jiff::civil::Time"
1150 .into(),
1151 ));
1152 }
1153 jiff_time(hours, minutes, seconds, microseconds)
1154 }
1155 MySQLValue::Bytes(value) => utf8(value.as_ref())?.parse().map_err(jiff_error),
1156 other => Err(conversion_error(Self::EXPECTED, &other)),
1157 }
1158 }
1159}
1160
1161#[cfg(feature = "jiff")]
1162impl DecodeMySQLValue for jiff::civil::DateTime {
1163 const EXPECTED: &'static str = "MySQL DATETIME/TIMESTAMP";
1164
1165 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
1166 match value {
1167 MySQLValue::Date {
1168 year,
1169 month,
1170 day,
1171 hour,
1172 minute,
1173 second,
1174 microseconds,
1175 } => Ok(jiff_date(year, month, day)?.to_datetime(jiff_time(
1176 hour,
1177 minute,
1178 second,
1179 microseconds,
1180 )?)),
1181 MySQLValue::Bytes(value) => utf8(value.as_ref())?.parse().map_err(jiff_error),
1182 other => Err(conversion_error(Self::EXPECTED, &other)),
1183 }
1184 }
1185}
1186
1187#[cfg(feature = "jiff")]
1188impl DecodeMySQLValue for jiff::Timestamp {
1189 const EXPECTED: &'static str = "UTC MySQL TIMESTAMP";
1190
1191 fn decode(value: MySQLValue<'_>) -> Result<Self, DrizzleError> {
1192 let utc = jiff::civil::DateTime::decode(value)?;
1193 jiff::tz::Offset::UTC.to_timestamp(utc).map_err(jiff_error)
1194 }
1195}
1196
1197#[cfg(feature = "jiff")]
1198impl_row_decode!(
1199 jiff::civil::Date,
1200 jiff::civil::Time,
1201 jiff::civil::DateTime,
1202 jiff::Timestamp,
1203);
1204
1205#[cfg(feature = "mysql-common")]
1206impl MySQLRowAccess for mysql_common::Row {
1207 fn value_at(&self, offset: usize) -> Result<Option<MySQLValue<'_>>, DrizzleError> {
1208 let Some(value) = self.as_ref(offset) else {
1209 return Ok(None);
1210 };
1211 Ok(Some(match value {
1212 mysql_common::Value::NULL => MySQLValue::Null,
1213 mysql_common::Value::Bytes(value) => MySQLValue::from(value.as_slice()),
1214 mysql_common::Value::Int(value) => MySQLValue::Int(*value),
1215 mysql_common::Value::UInt(value) => MySQLValue::UInt(*value),
1216 mysql_common::Value::Float(value) => MySQLValue::Float(*value),
1217 mysql_common::Value::Double(value) => MySQLValue::Double(*value),
1218 mysql_common::Value::Date(year, month, day, hour, minute, second, micros) => {
1219 MySQLValue::Date {
1220 year: *year,
1221 month: *month,
1222 day: *day,
1223 hour: *hour,
1224 minute: *minute,
1225 second: *second,
1226 microseconds: *micros,
1227 }
1228 }
1229 mysql_common::Value::Time(negative, days, hours, minutes, seconds, micros) => {
1230 MySQLValue::Time {
1231 negative: *negative,
1232 days: *days,
1233 hours: *hours,
1234 minutes: *minutes,
1235 seconds: *seconds,
1236 microseconds: *micros,
1237 }
1238 }
1239 }))
1240 }
1241
1242 fn is_bit_at(&self, offset: usize) -> bool {
1243 self.columns_ref().get(offset).is_some_and(|column| {
1244 column.column_type() == mysql_common::constants::ColumnType::MYSQL_TYPE_BIT
1245 })
1246 }
1247}
1248
1249#[cfg(feature = "mysql-common")]
1250impl From<MySQLValue<'_>> for mysql_common::Value {
1251 fn from(value: MySQLValue<'_>) -> Self {
1252 match value {
1253 MySQLValue::Null => Self::NULL,
1254 MySQLValue::Bytes(value) => Self::Bytes(value.into_owned()),
1255 MySQLValue::Int(value) => Self::Int(value),
1256 MySQLValue::UInt(value) => Self::UInt(value),
1257 MySQLValue::Float(value) => Self::Float(value),
1258 MySQLValue::Double(value) => Self::Double(value),
1259 MySQLValue::Date {
1260 year,
1261 month,
1262 day,
1263 hour,
1264 minute,
1265 second,
1266 microseconds,
1267 } => Self::Date(year, month, day, hour, minute, second, microseconds),
1268 MySQLValue::Time {
1269 negative,
1270 days,
1271 hours,
1272 minutes,
1273 seconds,
1274 microseconds,
1275 } => Self::Time(negative, days, hours, minutes, seconds, microseconds),
1276 }
1277 }
1278}
1279
1280#[cfg(test)]
1281mod tests {
1282 use super::*;
1283
1284 fn decode<T: for<'row> FromDrizzleRow<MySQLRow<'row, [OwnedMySQLValue]>>>(
1285 values: &[OwnedMySQLValue],
1286 ) -> Result<T, DrizzleError> {
1287 T::from_row(&MySQLRow::new(values))
1288 }
1289
1290 #[test]
1291 fn signed_unsigned_null_and_offsets_decode_without_panics() {
1292 let values = [
1293 OwnedMySQLValue::Int(-1),
1294 OwnedMySQLValue::UInt(u64::MAX),
1295 OwnedMySQLValue::Null,
1296 ];
1297 assert_eq!(
1298 decode::<(i64, u64, Option<i32>)>(&values).unwrap(),
1299 (-1, u64::MAX, None)
1300 );
1301 assert!(decode::<(u64,)>(&values).is_err());
1302 assert!(decode::<(i64, u64, i32)>(&values).is_err());
1303 assert!(decode::<(i64, u64, Option<i32>, i32)>(&values).is_err());
1304 }
1305
1306 #[test]
1307 fn text_protocol_numbers_are_checked_for_overflow() {
1308 let valid = [OwnedMySQLValue::Bytes(b"255".to_vec())];
1309 assert_eq!(decode::<u8>(&valid).unwrap(), 255);
1310 let overflow = [OwnedMySQLValue::Bytes(b"256".to_vec())];
1311 assert!(decode::<u8>(&overflow).is_err());
1312 let negative = [OwnedMySQLValue::Bytes(b"-1".to_vec())];
1313 assert!(decode::<u64>(&negative).is_err());
1314 }
1315
1316 #[test]
1317 fn invalid_utf8_is_a_conversion_error() {
1318 let values = [OwnedMySQLValue::Bytes(vec![0xff])];
1319 assert!(decode::<String>(&values).is_err());
1320 }
1321
1322 #[cfg(feature = "query")]
1323 #[test]
1324 fn projected_binary_values_follow_the_checked_wire_codec() {
1325 let tagged = serde_json::json!({
1326 "$drizzle_storage": "blob",
1327 "$drizzle_value": "0001FEFF"
1328 });
1329 assert_eq!(decode_blob::<Vec<u8>>(&tagged).unwrap(), [0, 1, 254, 255]);
1330 assert!(
1331 decode_blob::<Vec<u8>>(&serde_json::json!({
1332 "$drizzle_storage": "text",
1333 "$drizzle_value": "00"
1334 }))
1335 .is_err()
1336 );
1337 assert!(
1338 decode_blob::<Vec<u8>>(&serde_json::json!({
1339 "$drizzle_storage": "blob",
1340 "$drizzle_value": "0"
1341 }))
1342 .is_err()
1343 );
1344 }
1345
1346 #[cfg(feature = "query")]
1347 #[test]
1348 fn projected_text_values_recover_wire_shapes() {
1349 assert_eq!(
1350 projected_value(
1351 &serde_json::json!("-9223372036854775801"),
1352 MySQLJsonStorage::SignedText
1353 )
1354 .unwrap(),
1355 OwnedMySQLValue::Int(-9_223_372_036_854_775_801)
1356 );
1357 assert_eq!(
1358 projected_value(&serde_json::json!("1.25"), MySQLJsonStorage::FloatText).unwrap(),
1359 OwnedMySQLValue::Float(1.25)
1360 );
1361 assert_eq!(
1362 projected_value(
1363 &serde_json::json!("2026-08-27 12:34:56.1234"),
1364 MySQLJsonStorage::DateTime,
1365 )
1366 .unwrap(),
1367 OwnedMySQLValue::Date {
1368 year: 2026,
1369 month: 8,
1370 day: 27,
1371 hour: 12,
1372 minute: 34,
1373 second: 56,
1374 microseconds: 123_400,
1375 }
1376 );
1377 assert_eq!(
1378 projected_value(&serde_json::json!("-49:02:03.4"), MySQLJsonStorage::Time).unwrap(),
1379 OwnedMySQLValue::Time {
1380 negative: true,
1381 days: 2,
1382 hours: 1,
1383 minutes: 2,
1384 seconds: 3,
1385 microseconds: 400_000,
1386 }
1387 );
1388 }
1389
1390 #[cfg(all(
1391 feature = "arrayvec",
1392 feature = "bytes",
1393 feature = "compact-str",
1394 feature = "smallvec"
1395 ))]
1396 #[test]
1397 fn feature_forwarded_text_and_binary_types_decode_through_the_same_row_contract() {
1398 let text = [OwnedMySQLValue::Bytes(b"drizzle".to_vec())];
1399 assert_eq!(
1400 decode::<compact_str::CompactString>(&text).unwrap(),
1401 "drizzle"
1402 );
1403 assert_eq!(
1404 decode::<arrayvec::ArrayString<8>>(&text).unwrap().as_str(),
1405 "drizzle"
1406 );
1407 assert!(decode::<arrayvec::ArrayString<3>>(&text).is_err());
1408
1409 let binary = [OwnedMySQLValue::Bytes(vec![1, 2, 3])];
1410 assert_eq!(
1411 decode::<arrayvec::ArrayVec<u8, 3>>(&binary)
1412 .unwrap()
1413 .as_slice(),
1414 &[1, 2, 3]
1415 );
1416 assert!(decode::<arrayvec::ArrayVec<u8, 2>>(&binary).is_err());
1417 assert_eq!(
1418 decode::<smallvec::SmallVec<[u8; 2]>>(&binary)
1419 .unwrap()
1420 .as_slice(),
1421 &[1, 2, 3]
1422 );
1423 assert_eq!(
1424 decode::<bytes::Bytes>(&binary).unwrap().as_ref(),
1425 &[1, 2, 3]
1426 );
1427 }
1428
1429 #[cfg(feature = "uuid")]
1430 #[test]
1431 fn uuid_accepts_binary_and_text_forms_but_rejects_malformed_bytes() {
1432 let expected = uuid::Uuid::from_u128(0x12345678_90ab_cdef_1234_567890abcdef);
1433 assert_eq!(
1434 decode::<uuid::Uuid>(&[OwnedMySQLValue::Bytes(expected.as_bytes().to_vec())]).unwrap(),
1435 expected
1436 );
1437 assert_eq!(
1438 decode::<uuid::Uuid>(&[OwnedMySQLValue::Bytes(expected.to_string().into_bytes())])
1439 .unwrap(),
1440 expected
1441 );
1442 assert!(decode::<uuid::Uuid>(&[OwnedMySQLValue::Bytes(vec![1, 2, 3])]).is_err());
1443 }
1444
1445 #[cfg(feature = "serde")]
1446 #[test]
1447 fn json_decode_reports_invalid_payloads() {
1448 let value =
1449 decode::<serde_json::Value>(&[OwnedMySQLValue::Bytes(br#"{"ok":true}"#.to_vec())])
1450 .unwrap();
1451 assert_eq!(value["ok"], true);
1452 assert!(decode::<serde_json::Value>(&[OwnedMySQLValue::Bytes(b"{".to_vec())]).is_err());
1453 }
1454
1455 #[cfg(feature = "chrono")]
1456 #[test]
1457 fn chrono_round_trips_binary_dates_and_rejects_zero_dates() {
1458 let expected = chrono::NaiveDate::from_ymd_opt(2026, 8, 26)
1459 .unwrap()
1460 .and_hms_micro_opt(12, 34, 56, 789)
1461 .unwrap();
1462 assert_eq!(
1463 decode::<chrono::NaiveDateTime>(&[OwnedMySQLValue::from(expected)]).unwrap(),
1464 expected
1465 );
1466 assert_eq!(
1467 decode::<chrono::NaiveDateTime>(&[OwnedMySQLValue::Bytes(
1468 b"2026-08-26 12:34:56.000789".to_vec(),
1469 )])
1470 .unwrap(),
1471 expected
1472 );
1473 assert!(
1474 decode::<chrono::NaiveDate>(&[OwnedMySQLValue::Date {
1475 year: 0,
1476 month: 0,
1477 day: 0,
1478 hour: 0,
1479 minute: 0,
1480 second: 0,
1481 microseconds: 0,
1482 }])
1483 .is_err()
1484 );
1485 }
1486
1487 #[cfg(feature = "time")]
1488 #[test]
1489 fn time_round_trips_binary_dates_and_rejects_duration_as_clock_time() {
1490 let expected = time::Date::from_calendar_date(2026, time::Month::August, 26)
1491 .unwrap()
1492 .with_hms_micro(12, 34, 56, 789)
1493 .unwrap();
1494 assert_eq!(
1495 decode::<time::PrimitiveDateTime>(&[OwnedMySQLValue::from(expected)]).unwrap(),
1496 expected
1497 );
1498 assert!(
1499 decode::<time::Time>(&[OwnedMySQLValue::Time {
1500 negative: false,
1501 days: 1,
1502 hours: 0,
1503 minutes: 0,
1504 seconds: 0,
1505 microseconds: 0,
1506 }])
1507 .is_err()
1508 );
1509 }
1510
1511 #[cfg(feature = "jiff")]
1512 #[test]
1513 fn jiff_round_trips_binary_dates_and_rejects_duration_as_clock_time() {
1514 let expected = jiff::civil::date(2026, 8, 26).at(12, 34, 56, 789_000);
1515 assert_eq!(
1516 decode::<jiff::civil::DateTime>(&[OwnedMySQLValue::from(expected)]).unwrap(),
1517 expected
1518 );
1519 let instant: jiff::Timestamp = "2026-08-26T12:34:56.000789Z".parse().unwrap();
1520 assert_eq!(
1521 decode::<jiff::Timestamp>(&[OwnedMySQLValue::from(instant)]).unwrap(),
1522 instant
1523 );
1524 assert!(
1525 decode::<jiff::civil::Time>(&[OwnedMySQLValue::Time {
1526 negative: false,
1527 days: 1,
1528 hours: 0,
1529 minutes: 0,
1530 seconds: 0,
1531 microseconds: 0,
1532 }])
1533 .is_err()
1534 );
1535 }
1536
1537 #[cfg(feature = "rust-decimal")]
1538 #[test]
1539 fn decimal_boundaries_fail_instead_of_truncating() {
1540 let max = [OwnedMySQLValue::Bytes(
1541 b"79228162514264337593543950335".to_vec(),
1542 )];
1543 assert_eq!(
1544 decode::<rust_decimal::Decimal>(&max).unwrap(),
1545 rust_decimal::Decimal::MAX
1546 );
1547 let overflow = [OwnedMySQLValue::Bytes(
1548 b"79228162514264337593543950336".to_vec(),
1549 )];
1550 assert!(decode::<rust_decimal::Decimal>(&overflow).is_err());
1551 }
1552
1553 #[cfg(feature = "mysql-common")]
1554 #[test]
1555 fn mysql_bit_bytes_decode_as_big_endian_unsigned_values() {
1556 assert_eq!(mysql_bit_to_u64(&[]).unwrap(), 0);
1557 assert_eq!(mysql_bit_to_u64(&[0xff]).unwrap(), 255);
1558 assert_eq!(mysql_bit_to_u64(&[0x01, 0x00]).unwrap(), 256);
1559 assert_eq!(mysql_bit_to_u64(&[0xff; 8]).unwrap(), u64::MAX);
1560 assert!(mysql_bit_to_u64(&[0; 9]).is_err());
1561 }
1562
1563 #[test]
1564 fn bit_metadata_preserves_binary_targets_and_enables_numeric_targets() {
1565 struct PackedBitRow(OwnedMySQLValue);
1566
1567 impl MySQLRowAccess for PackedBitRow {
1568 fn value_at(&self, offset: usize) -> Result<Option<MySQLValue<'_>>, DrizzleError> {
1569 Ok((offset == 0).then(|| MySQLValue::from(&self.0)))
1570 }
1571
1572 fn is_bit_at(&self, offset: usize) -> bool {
1573 offset == 0
1574 }
1575 }
1576
1577 let row = PackedBitRow(OwnedMySQLValue::Bytes(vec![0xa5]));
1578 let row = MySQLRow::new(&row);
1579 assert_eq!(u64::from_row(&row).unwrap(), 0xa5);
1580 assert!(bool::from_row(&row).unwrap());
1581 assert_eq!(Vec::<u8>::from_row(&row).unwrap(), [0xa5]);
1582 }
1583}