Skip to main content

clickhouse/
data_row.rs

1use crate::row::{Row, RowKind};
2use serde::ser::{SerializeStruct, Serializer};
3use std::sync::Arc;
4
5/// A dynamically-typed row returned by [`crate::query::DataRowCursor`].
6///
7/// Column names are shared across all rows from the same cursor via [`Arc`],
8/// making each row cheap to construct (one `Arc` clone + one `Vec` alloc).
9///
10/// # Example
11///
12/// ```ignore
13/// let mut cursor = client
14///     .query("SELECT number, toString(number) AS s FROM system.numbers LIMIT 3")
15///     .fetch_rows()?;
16///
17/// while let Some(row) = cursor.next().await? {
18///     for (col, val) in row.column_names.iter().zip(&row.values) {
19///         println!("{col}: {val:?}");
20///     }
21/// }
22/// ```
23#[cfg_attr(docsrs, doc(cfg(feature = "sea-ql")))]
24#[derive(Debug, Clone, PartialEq)]
25pub struct DataRow {
26    /// Column names in schema order, shared with all other rows from the same query.
27    pub column_names: Arc<[Arc<str>]>,
28    /// ClickHouse data types in schema order, shared with all other rows from the same query.
29    pub column_types: Arc<[clickhouse_types::DataTypeNode]>,
30    /// Per-column values decoded from `RowBinaryWithNamesAndTypes`.
31    pub values: Vec<sea_query::Value>,
32}
33
34impl DataRow {
35    /// Extract column `idx` as type `T`.
36    ///
37    /// `idx` can be a column name (`&str`) or a zero-based index (`usize`).
38    ///
39    /// Numeric conversions are flexible: any integer or float column can be decoded
40    /// into any compatible numeric type, with range-checked narrowing. Fractional
41    /// floats are truncated when converting to integers. `Option<T>` decodes `NULL`
42    /// as `None` instead of returning an error.
43    ///
44    /// # Errors
45    ///
46    /// Returns [`TypeError`] if the column is not found, the value is `NULL` (for
47    /// non-`Option` targets), or the conversion is not possible / out of range.
48    ///
49    /// # Examples
50    ///
51    /// ```ignore
52    /// let id: i64 = row.try_get("id")?;
53    /// let name: String = row.try_get("name")?;
54    /// let score: Option<f64> = row.try_get(2)?;
55    /// ```
56    pub fn try_get<T, I>(&self, idx: I) -> Result<T, TypeError>
57    where
58        T: FromValue,
59        I: ColumnIndex,
60    {
61        let i = idx.get_index(self)?;
62        T::from_value(&self.values[i])
63    }
64}
65
66/// A column-oriented batch of dynamically-typed rows.
67///
68/// Instead of one [`sea_query::Value`] vector per row (as in [`DataRow`]),
69/// `RowBatch` stores one `Vec<Value>` per column. This layout is efficient
70/// for columnar processing and is the natural precursor to Apache Arrow
71/// record batches.
72///
73/// All `column_data` vectors have exactly `num_rows` entries.
74///
75/// Obtain batches via [`crate::query::DataRowCursor::next_batch`].
76///
77/// # Example
78///
79/// ```ignore
80/// let mut cursor = client
81///     .query("SELECT number, toString(number) AS s FROM system.numbers LIMIT 100")
82///     .fetch_rows()?;
83///
84/// while let Some(batch) = cursor.next_batch(32).await? {
85///     println!("{} rows, {} columns", batch.num_rows, batch.column_names.len());
86///     for (name, col) in batch.column_names.iter().zip(&batch.column_data) {
87///         println!("  {name}: {} values", col.len());
88///     }
89/// }
90/// ```
91#[cfg_attr(docsrs, doc(cfg(feature = "sea-ql")))]
92#[derive(Debug, Clone, PartialEq)]
93pub struct RowBatch {
94    /// Column names in schema order, shared with all other batches from the same query.
95    pub column_names: Arc<[Arc<str>]>,
96    /// ClickHouse data types in schema order, shared with all other batches from the same query.
97    pub column_types: Arc<[clickhouse_types::DataTypeNode]>,
98    /// Per-column value vectors; `column_data[i]` holds all values for `column_names[i]`.
99    ///
100    /// Every inner `Vec` has exactly `num_rows` entries.
101    pub column_data: Vec<Vec<sea_query::Value>>,
102    /// Number of rows in this batch.
103    pub num_rows: usize,
104}
105
106// ── TypeError ─────────────────────────────────────────────────────────────────
107
108/// Error returned by [`DataRow::try_get`].
109#[cfg_attr(docsrs, doc(cfg(feature = "sea-ql")))]
110#[derive(Debug, thiserror::Error)]
111pub enum TypeError {
112    /// A column name passed to `try_get` does not exist in this row.
113    #[error("column '{0}' not found")]
114    ColumnNotFound(String),
115    /// A column index passed to `try_get` is out of bounds.
116    #[error("column index {0} is out of bounds")]
117    IndexOutOfBounds(usize),
118    /// The column value is `NULL` but the target type is not `Option<T>`.
119    #[error("column value is NULL")]
120    UnexpectedNull,
121    /// The stored `Value` variant cannot be converted to the requested type.
122    #[error("cannot convert {got} to {expected}")]
123    TypeMismatch {
124        expected: &'static str,
125        got: &'static str,
126    },
127    /// The stored numeric value is outside the representable range of the target type.
128    #[error("value out of range for {0}")]
129    OutOfRange(&'static str),
130}
131
132// ── ColumnIndex ────────────────────────────────────────────────────────────────
133
134mod col_index_sealed {
135    pub trait Sealed {}
136    impl Sealed for usize {}
137    impl Sealed for &str {}
138}
139
140/// Types that can index into a [`DataRow`] column: `usize` (position) or `&str` (name).
141#[cfg_attr(docsrs, doc(cfg(feature = "sea-ql")))]
142pub trait ColumnIndex: col_index_sealed::Sealed {
143    #[doc(hidden)]
144    fn get_index(&self, row: &DataRow) -> Result<usize, TypeError>;
145}
146
147impl ColumnIndex for usize {
148    fn get_index(&self, row: &DataRow) -> Result<usize, TypeError> {
149        if *self < row.values.len() {
150            Ok(*self)
151        } else {
152            Err(TypeError::IndexOutOfBounds(*self))
153        }
154    }
155}
156
157impl ColumnIndex for &str {
158    fn get_index(&self, row: &DataRow) -> Result<usize, TypeError> {
159        row.column_names
160            .iter()
161            .position(|c| c.as_ref() == *self)
162            .ok_or_else(|| TypeError::ColumnNotFound(self.to_string()))
163    }
164}
165
166// ── FromValue trait ────────────────────────────────────────────────────────────
167
168/// Extract a concrete Rust value from a [`sea_query::Value`].
169///
170/// Implemented for standard numeric and string types with flexible cross-type
171/// numeric conversions. Implement for custom types to use with [`DataRow::try_get`].
172#[cfg_attr(docsrs, doc(cfg(feature = "sea-ql")))]
173pub trait FromValue: Sized {
174    /// Convert a [`sea_query::Value`] reference into `Self`.
175    fn from_value(val: &sea_query::Value) -> Result<Self, TypeError>;
176}
177
178// ── Private helpers ────────────────────────────────────────────────────────────
179
180/// Outcome of converting a [`sea_query::Value`] to `i128`.
181enum NumericI128 {
182    Got(i128),
183    /// Numeric type but the value doesn't fit in `i128` (e.g. very large float).
184    Overflow,
185    /// The variant is not a numeric type.
186    NotNumeric,
187}
188
189fn try_as_i128(val: &sea_query::Value) -> NumericI128 {
190    use sea_query::Value as V;
191    match val {
192        V::Bool(Some(b)) => NumericI128::Got(*b as i128),
193        V::TinyInt(Some(v)) => NumericI128::Got(*v as i128),
194        V::SmallInt(Some(v)) => NumericI128::Got(*v as i128),
195        V::Int(Some(v)) => NumericI128::Got(*v as i128),
196        V::BigInt(Some(v)) => NumericI128::Got(*v as i128),
197        V::TinyUnsigned(Some(v)) => NumericI128::Got(*v as i128),
198        V::SmallUnsigned(Some(v)) => NumericI128::Got(*v as i128),
199        V::Unsigned(Some(v)) => NumericI128::Got(*v as i128),
200        V::BigUnsigned(Some(v)) => NumericI128::Got(*v as i128),
201        V::Float(Some(f)) => f64_truncate_to_i128(*f as f64),
202        V::Double(Some(f)) => f64_truncate_to_i128(*f),
203        #[cfg(feature = "rust_decimal")]
204        V::Decimal(Some(d)) => {
205            use sea_query::prelude::rust_decimal::prelude::ToPrimitive;
206            match d.to_i128() {
207                Some(i) => NumericI128::Got(i),
208                None => NumericI128::Overflow,
209            }
210        }
211        #[cfg(feature = "bigdecimal")]
212        V::BigDecimal(Some(d)) => {
213            use sea_query::prelude::bigdecimal::ToPrimitive;
214            match d.to_i128() {
215                Some(i) => NumericI128::Got(i),
216                None => NumericI128::Overflow,
217            }
218        }
219        _ => NumericI128::NotNumeric,
220    }
221}
222
223fn try_as_f64(val: &sea_query::Value) -> Option<f64> {
224    use sea_query::Value as V;
225    match val {
226        V::Bool(Some(b)) => Some(*b as u8 as f64),
227        V::TinyInt(Some(v)) => Some(*v as f64),
228        V::SmallInt(Some(v)) => Some(*v as f64),
229        V::Int(Some(v)) => Some(*v as f64),
230        V::BigInt(Some(v)) => Some(*v as f64),
231        V::TinyUnsigned(Some(v)) => Some(*v as f64),
232        V::SmallUnsigned(Some(v)) => Some(*v as f64),
233        V::Unsigned(Some(v)) => Some(*v as f64),
234        V::BigUnsigned(Some(v)) => Some(*v as f64),
235        V::Float(Some(f)) => Some(*f as f64),
236        V::Double(Some(f)) => Some(*f),
237        #[cfg(feature = "rust_decimal")]
238        V::Decimal(Some(d)) => {
239            use sea_query::prelude::rust_decimal::prelude::ToPrimitive;
240            d.to_f64()
241        }
242        #[cfg(feature = "bigdecimal")]
243        V::BigDecimal(Some(d)) => {
244            use sea_query::prelude::bigdecimal::ToPrimitive;
245            d.to_f64()
246        }
247        _ => None,
248    }
249}
250
251/// Truncate a finite `f64` to `i128`, returning `Overflow` if out of range.
252fn f64_truncate_to_i128(f: f64) -> NumericI128 {
253    if !f.is_finite() {
254        return NumericI128::Overflow;
255    }
256    // i128::MIN = -2^127 is exactly representable as f64 (power of two).
257    // -(i128::MIN as f64) = 2^127, which is i128::MAX + 1, so strict < gives the right bound.
258    const MIN: f64 = i128::MIN as f64;
259    const MAX_EXCL: f64 = -(i128::MIN as f64);
260    if f >= MIN && f < MAX_EXCL {
261        NumericI128::Got(f as i128)
262    } else {
263        NumericI128::Overflow
264    }
265}
266
267/// Returns `true` if `val` is a NULL variant of any type.
268fn is_null(val: &sea_query::Value) -> bool {
269    use sea_query::Value as V;
270    #[allow(unused_mut)]
271    let mut null = matches!(
272        val,
273        V::Bool(None)
274            | V::TinyInt(None)
275            | V::SmallInt(None)
276            | V::Int(None)
277            | V::BigInt(None)
278            | V::TinyUnsigned(None)
279            | V::SmallUnsigned(None)
280            | V::Unsigned(None)
281            | V::BigUnsigned(None)
282            | V::Float(None)
283            | V::Double(None)
284            | V::String(None)
285            | V::Char(None)
286            | V::Bytes(None)
287            | V::Json(None)
288    );
289    #[cfg(feature = "rust_decimal")]
290    {
291        null = null || matches!(val, V::Decimal(None));
292    }
293    #[cfg(feature = "bigdecimal")]
294    {
295        null = null || matches!(val, V::BigDecimal(None));
296    }
297    #[cfg(feature = "chrono")]
298    {
299        null = null
300            || matches!(
301                val,
302                V::ChronoDate(None) | V::ChronoDateTime(None) | V::ChronoTime(None)
303            );
304    }
305    #[cfg(feature = "time")]
306    {
307        null = null
308            || matches!(
309                val,
310                V::TimeDate(None) | V::TimeDateTime(None) | V::TimeTime(None)
311            );
312    }
313    #[cfg(feature = "uuid")]
314    {
315        null = null || matches!(val, V::Uuid(None));
316    }
317    null
318}
319
320/// Human-readable name of the [`sea_query::Value`] variant (for error messages).
321fn value_variant_name(val: &sea_query::Value) -> &'static str {
322    use sea_query::Value as V;
323    match val {
324        V::Bool(_) => "Bool",
325        V::TinyInt(_) => "TinyInt (i8)",
326        V::SmallInt(_) => "SmallInt (i16)",
327        V::Int(_) => "Int (i32)",
328        V::BigInt(_) => "BigInt (i64)",
329        V::TinyUnsigned(_) => "TinyUnsigned (u8)",
330        V::SmallUnsigned(_) => "SmallUnsigned (u16)",
331        V::Unsigned(_) => "Unsigned (u32)",
332        V::BigUnsigned(_) => "BigUnsigned (u64)",
333        V::Float(_) => "Float (f32)",
334        V::Double(_) => "Double (f64)",
335        V::String(_) => "String",
336        V::Char(_) => "Char",
337        V::Bytes(_) => "Bytes",
338        V::Json(_) => "Json",
339        #[cfg(feature = "rust_decimal")]
340        V::Decimal(_) => "Decimal",
341        #[cfg(feature = "bigdecimal")]
342        V::BigDecimal(_) => "BigDecimal",
343        #[cfg(feature = "uuid")]
344        V::Uuid(_) => "Uuid",
345        #[cfg(feature = "chrono")]
346        V::ChronoDate(_) => "ChronoDate",
347        #[cfg(feature = "chrono")]
348        V::ChronoDateTime(_) => "ChronoDateTime",
349        #[cfg(feature = "chrono")]
350        V::ChronoTime(_) => "ChronoTime",
351        #[cfg(feature = "time")]
352        V::TimeDate(_) => "TimeDate",
353        #[cfg(feature = "time")]
354        V::TimeDateTime(_) => "TimeDateTime",
355        #[cfg(feature = "time")]
356        V::TimeTime(_) => "TimeTime",
357        #[allow(unreachable_patterns)]
358        _ => "unknown",
359    }
360}
361
362// ── FromValue implementations ──────────────────────────────────────────────────
363
364impl FromValue for bool {
365    fn from_value(val: &sea_query::Value) -> Result<Self, TypeError> {
366        use sea_query::Value as V;
367        if is_null(val) {
368            return Err(TypeError::UnexpectedNull);
369        }
370        match val {
371            V::Bool(Some(b)) => Ok(*b),
372            _ => match try_as_i128(val) {
373                NumericI128::Got(0) => Ok(false),
374                NumericI128::Got(1) => Ok(true),
375                NumericI128::Got(_) | NumericI128::Overflow => Err(TypeError::OutOfRange("bool")),
376                NumericI128::NotNumeric => Err(TypeError::TypeMismatch {
377                    expected: "bool",
378                    got: value_variant_name(val),
379                }),
380            },
381        }
382    }
383}
384
385/// Implements `FromValue` for integer types via `i128` as an intermediate.
386macro_rules! impl_from_value_int {
387    ($t:ty) => {
388        impl FromValue for $t {
389            fn from_value(val: &sea_query::Value) -> Result<Self, TypeError> {
390                if is_null(val) {
391                    return Err(TypeError::UnexpectedNull);
392                }
393                match try_as_i128(val) {
394                    NumericI128::Got(n) => {
395                        <$t>::try_from(n).map_err(|_| TypeError::OutOfRange(stringify!($t)))
396                    }
397                    NumericI128::Overflow => Err(TypeError::OutOfRange(stringify!($t))),
398                    NumericI128::NotNumeric => Err(TypeError::TypeMismatch {
399                        expected: stringify!($t),
400                        got: value_variant_name(val),
401                    }),
402                }
403            }
404        }
405    };
406}
407
408impl_from_value_int!(i8);
409impl_from_value_int!(i16);
410impl_from_value_int!(i32);
411impl_from_value_int!(i64);
412impl_from_value_int!(i128);
413impl_from_value_int!(u8);
414impl_from_value_int!(u16);
415impl_from_value_int!(u32);
416impl_from_value_int!(u64);
417impl_from_value_int!(u128);
418
419impl FromValue for f64 {
420    fn from_value(val: &sea_query::Value) -> Result<Self, TypeError> {
421        if is_null(val) {
422            return Err(TypeError::UnexpectedNull);
423        }
424        try_as_f64(val).ok_or_else(|| TypeError::TypeMismatch {
425            expected: "f64",
426            got: value_variant_name(val),
427        })
428    }
429}
430
431impl FromValue for f32 {
432    fn from_value(val: &sea_query::Value) -> Result<Self, TypeError> {
433        use sea_query::Value as V;
434        if is_null(val) {
435            return Err(TypeError::UnexpectedNull);
436        }
437        // Prefer the native f32 to avoid precision changes for Float columns.
438        if let V::Float(Some(f)) = val {
439            return Ok(*f);
440        }
441        try_as_f64(val)
442            .map(|f| f as f32)
443            .ok_or_else(|| TypeError::TypeMismatch {
444                expected: "f32",
445                got: value_variant_name(val),
446            })
447    }
448}
449
450impl FromValue for String {
451    fn from_value(val: &sea_query::Value) -> Result<Self, TypeError> {
452        use sea_query::Value as V;
453        if is_null(val) {
454            return Err(TypeError::UnexpectedNull);
455        }
456        match val {
457            V::String(Some(s)) => Ok(s.clone()),
458            V::Char(Some(c)) => Ok(c.to_string()),
459            _ => Err(TypeError::TypeMismatch {
460                expected: "String",
461                got: value_variant_name(val),
462            }),
463        }
464    }
465}
466
467impl FromValue for Vec<u8> {
468    fn from_value(val: &sea_query::Value) -> Result<Self, TypeError> {
469        use sea_query::Value as V;
470        if is_null(val) {
471            return Err(TypeError::UnexpectedNull);
472        }
473        match val {
474            V::Bytes(Some(b)) => Ok(b.clone()),
475            _ => Err(TypeError::TypeMismatch {
476                expected: "Vec<u8>",
477                got: value_variant_name(val),
478            }),
479        }
480    }
481}
482
483#[cfg(feature = "rust_decimal")]
484impl FromValue for sea_query::value::prelude::Decimal {
485    fn from_value(val: &sea_query::Value) -> Result<Self, TypeError> {
486        use sea_query::Value as V;
487        use sea_query::value::prelude::Decimal;
488        if is_null(val) {
489            return Err(TypeError::UnexpectedNull);
490        }
491        match val {
492            V::Decimal(Some(d)) => return Ok(*d),
493            #[cfg(feature = "bigdecimal")]
494            V::BigDecimal(Some(bd)) => {
495                use std::str::FromStr;
496                return Decimal::from_str(&bd.to_string())
497                    .map_err(|_| TypeError::OutOfRange("Decimal"));
498            }
499            V::Float(Some(f)) => {
500                return Decimal::try_from(*f as f64).map_err(|_| TypeError::OutOfRange("Decimal"));
501            }
502            V::Double(Some(f)) => {
503                return Decimal::try_from(*f).map_err(|_| TypeError::OutOfRange("Decimal"));
504            }
505            _ => {}
506        }
507        // Integer / Bool variants
508        match try_as_i128(val) {
509            NumericI128::Got(n) => {
510                use std::str::FromStr;
511                Decimal::from_str(&n.to_string()).map_err(|_| TypeError::OutOfRange("Decimal"))
512            }
513            NumericI128::Overflow => Err(TypeError::OutOfRange("Decimal")),
514            NumericI128::NotNumeric => Err(TypeError::TypeMismatch {
515                expected: "Decimal",
516                got: value_variant_name(val),
517            }),
518        }
519    }
520}
521
522#[cfg(feature = "bigdecimal")]
523impl FromValue for sea_query::value::prelude::BigDecimal {
524    fn from_value(val: &sea_query::Value) -> Result<Self, TypeError> {
525        use sea_query::Value as V;
526        use sea_query::value::prelude::BigDecimal;
527        if is_null(val) {
528            return Err(TypeError::UnexpectedNull);
529        }
530        match val {
531            V::BigDecimal(Some(bd)) => return Ok((**bd).clone()),
532            #[cfg(feature = "rust_decimal")]
533            V::Decimal(Some(d)) => {
534                use std::str::FromStr;
535                return BigDecimal::from_str(&d.to_string())
536                    .map_err(|_| TypeError::OutOfRange("BigDecimal"));
537            }
538            V::Float(Some(f)) => {
539                let f = *f as f64;
540                if !f.is_finite() {
541                    return Err(TypeError::OutOfRange("BigDecimal"));
542                }
543                use std::str::FromStr;
544                return BigDecimal::from_str(&f.to_string())
545                    .map_err(|_| TypeError::OutOfRange("BigDecimal"));
546            }
547            V::Double(Some(f)) => {
548                if !f.is_finite() {
549                    return Err(TypeError::OutOfRange("BigDecimal"));
550                }
551                use std::str::FromStr;
552                return BigDecimal::from_str(&f.to_string())
553                    .map_err(|_| TypeError::OutOfRange("BigDecimal"));
554            }
555            _ => {}
556        }
557        // Integer / Bool variants
558        match try_as_i128(val) {
559            NumericI128::Got(n) => {
560                use sea_query::prelude::bigdecimal::num_bigint::BigInt;
561                Ok(BigDecimal::new(BigInt::from(n), 0))
562            }
563            NumericI128::Overflow => Err(TypeError::OutOfRange("BigDecimal")),
564            NumericI128::NotNumeric => Err(TypeError::TypeMismatch {
565                expected: "BigDecimal",
566                got: value_variant_name(val),
567            }),
568        }
569    }
570}
571
572#[cfg(feature = "uuid")]
573impl FromValue for sea_query::value::prelude::Uuid {
574    fn from_value(val: &sea_query::Value) -> Result<Self, TypeError> {
575        use sea_query::Value as V;
576        if is_null(val) {
577            return Err(TypeError::UnexpectedNull);
578        }
579        match val {
580            V::Uuid(Some(u)) => Ok(*u),
581            _ => Err(TypeError::TypeMismatch {
582                expected: "Uuid",
583                got: value_variant_name(val),
584            }),
585        }
586    }
587}
588
589impl FromValue for sea_query::prelude::serde_json::Value {
590    fn from_value(val: &sea_query::Value) -> Result<Self, TypeError> {
591        use sea_query::Value as V;
592        if is_null(val) {
593            return Err(TypeError::UnexpectedNull);
594        }
595        match val {
596            V::Json(Some(j)) => Ok((**j).clone()),
597            _ => Err(TypeError::TypeMismatch {
598                expected: "serde_json::Value",
599                got: value_variant_name(val),
600            }),
601        }
602    }
603}
604
605impl<T: FromValue> FromValue for Option<T> {
606    fn from_value(val: &sea_query::Value) -> Result<Self, TypeError> {
607        if is_null(val) {
608            return Ok(None);
609        }
610        T::from_value(val).map(Some)
611    }
612}
613
614/// This is only to satisfy the trait bounds of insert.
615impl Row for DataRow {
616    const NAME: &'static str = "DataRow";
617    /// Column names are dynamic; this is always empty.
618    /// Use [`crate::Client::insert_data_row`] which reads names from the instance.
619    const COLUMN_NAMES: &'static [&'static str] = &[];
620    const COLUMN_COUNT: usize = 0;
621    const KIND: RowKind = RowKind::Struct;
622    type Value<'a> = DataRow;
623}
624
625impl serde::Serialize for DataRow {
626    /// Serializes as a struct, emitting each value in column order.
627    ///
628    /// `None` values are serialized as `serialize_none()` (nullable NULL).
629    /// `Some(v)` values are serialized as the inner primitive (non-nullable style).
630    ///
631    /// **Limitation**: nullable columns with non-null values will be missing the null
632    /// flag byte. Use [`crate::Client::insert_data_row`] + [`crate::insert::DataRowInsert::write_row`]
633    /// for correct handling of all nullable columns.
634    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
635        let mut state = serializer.serialize_struct("DataRow", self.values.len())?;
636        for val in &self.values {
637            state.serialize_field("_", &SeaValueSer(val))?;
638        }
639        state.end()
640    }
641}
642
643/// Newtype wrapper that maps a [`sea_query::Value`] to the correct serde call
644/// for the `RowBinary` serializer.
645struct SeaValueSer<'a>(&'a sea_query::Value);
646
647impl serde::Serialize for SeaValueSer<'_> {
648    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
649        use sea_query::Value;
650        match self.0 {
651            // None -> nullable null
652            Value::Bool(None)
653            | Value::TinyInt(None)
654            | Value::SmallInt(None)
655            | Value::Int(None)
656            | Value::BigInt(None)
657            | Value::TinyUnsigned(None)
658            | Value::SmallUnsigned(None)
659            | Value::Unsigned(None)
660            | Value::BigUnsigned(None)
661            | Value::Float(None)
662            | Value::Double(None)
663            | Value::String(None)
664            | Value::Char(None)
665            | Value::Bytes(None)
666            | Value::Json(None) => serializer.serialize_none(),
667
668            // Primitives
669            Value::Bool(Some(v)) => serializer.serialize_bool(*v),
670            Value::TinyInt(Some(v)) => serializer.serialize_i8(*v),
671            Value::SmallInt(Some(v)) => serializer.serialize_i16(*v),
672            Value::Int(Some(v)) => serializer.serialize_i32(*v),
673            Value::BigInt(Some(v)) => serializer.serialize_i64(*v),
674            Value::TinyUnsigned(Some(v)) => serializer.serialize_u8(*v),
675            Value::SmallUnsigned(Some(v)) => serializer.serialize_u16(*v),
676            Value::Unsigned(Some(v)) => serializer.serialize_u32(*v),
677            Value::BigUnsigned(Some(v)) => serializer.serialize_u64(*v),
678            Value::Float(Some(v)) => serializer.serialize_f32(*v),
679            Value::Double(Some(v)) => serializer.serialize_f64(*v),
680            Value::String(Some(s)) => serializer.serialize_str(s),
681            Value::Bytes(Some(b)) => serializer.serialize_bytes(b),
682            Value::Json(Some(j)) => {
683                let s = j.to_string();
684                serializer.serialize_str(&s)
685            }
686
687            other => Err(serde::ser::Error::custom(format!(
688                "Cannot serialize {other:?} via serde; \
689                 use Client::insert_data_row for complex types (Date, UUID, Decimal, …)"
690            ))),
691        }
692    }
693}