Skip to main content

spate_clickhouse/
types.rs

1//! Wire wrapper types for ClickHouse columns whose RowBinary encoding is a
2//! plain integer or byte layout but whose *meaning* the Rust type system
3//! should carry: dates, times, decimals, 256-bit integers, and geo shapes.
4//!
5//! Every wrapper here is a documentation-carrying newtype: the encoding is
6//! transparently the inner value, written through
7//! `serialize_newtype_struct` so the wrapper's name stays observable to
8//! schema validation (see the crate's `schema` support) at zero wire cost.
9//!
10//! For `uuid`/`chrono`/`time` ecosystem types, use the field-attribute
11//! modules under [`crate::serde`] instead.
12
13use serde::ser::Serializer;
14use serde::{Deserialize, Serialize};
15
16/// Defines a doc-carrying wire newtype: `Serialize` writes the inner value
17/// through `serialize_newtype_struct` (transparent bytes, observable name),
18/// `Deserialize` reads the inner value back.
19macro_rules! wire_newtype {
20    ($(#[$doc:meta])* $name:ident($inner:ty)) => {
21        $(#[$doc])*
22        #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
23        pub struct $name(pub $inner);
24
25        impl Serialize for $name {
26            fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
27                serializer.serialize_newtype_struct(stringify!($name), &self.0)
28            }
29        }
30
31        impl<'de> Deserialize<'de> for $name {
32            fn deserialize<D: ::serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
33                <$inner as Deserialize<'de>>::deserialize(d).map($name)
34            }
35        }
36    };
37}
38
39wire_newtype! {
40    /// Days since the Unix epoch, matching a `Date` column's wire
41    /// representation (`UInt16`, so 1970-01-01 through 2149-06-06).
42    DateDays(u16)
43}
44
45wire_newtype! {
46    /// Days since the Unix epoch (signed), matching a `Date32` column's
47    /// wire representation (`Int32`; the server accepts 1900-01-01 through
48    /// 2299-12-31).
49    Date32Days(i32)
50}
51
52wire_newtype! {
53    /// Seconds since the Unix epoch, matching a `DateTime` column's wire
54    /// representation (`UInt32`).
55    DateTimeSeconds(u32)
56}
57
58wire_newtype! {
59    /// Seconds since the Unix epoch, matching a `DateTime64(0)` column's
60    /// wire representation (`Int64`).
61    DateTime64Secs(i64)
62}
63
64wire_newtype! {
65    /// Milliseconds since the Unix epoch, matching a `DateTime64(3)`
66    /// column's wire representation (`Int64`).
67    DateTime64Millis(i64)
68}
69
70wire_newtype! {
71    /// Microseconds since the Unix epoch, matching a `DateTime64(6)`
72    /// column's wire representation (`Int64`).
73    DateTime64Micros(i64)
74}
75
76wire_newtype! {
77    /// Nanoseconds since the Unix epoch, matching a `DateTime64(9)`
78    /// column's wire representation (`Int64`).
79    DateTime64Nanos(i64)
80}
81
82wire_newtype! {
83    /// Seconds, matching a `Time` column's wire representation (`Int32`;
84    /// the server accepts -999:59:59 through 999:59:59).
85    TimeSeconds(i32)
86}
87
88wire_newtype! {
89    /// Seconds, matching a `Time64(0)` column's wire representation
90    /// (`Int64`).
91    Time64Secs(i64)
92}
93
94wire_newtype! {
95    /// Milliseconds, matching a `Time64(3)` column's wire representation
96    /// (`Int64`).
97    Time64Millis(i64)
98}
99
100wire_newtype! {
101    /// Microseconds, matching a `Time64(6)` column's wire representation
102    /// (`Int64`).
103    Time64Micros(i64)
104}
105
106wire_newtype! {
107    /// Nanoseconds, matching a `Time64(9)` column's wire representation
108    /// (`Int64`).
109    Time64Nanos(i64)
110}
111
112/// Defines a pre-scaled decimal wire newtype over a fixed-width integer.
113///
114/// The scale is a const generic: `Decimal64<2>(150)` is `1.50` in a
115/// `Decimal(18, 2)` column. Making the scale part of the *type* keeps
116/// mixed-scale arithmetic from compiling, which is the whole guarantee a
117/// decimal wants; the wire format is the raw little-endian scaled integer.
118macro_rules! decimal_newtype {
119    ($(#[$doc:meta])* $name:ident($inner:ty), max_scale = $max:literal) => {
120        $(#[$doc])*
121        #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
122        pub struct $name<const SCALE: u32>(pub $inner);
123
124        impl<const SCALE: u32> $name<SCALE> {
125            /// The scale (fractional digits) this type carries: `raw =
126            /// value × 10^SCALE`.
127            pub const SCALE: u32 = {
128                assert!(
129                    SCALE <= $max,
130                    concat!(
131                        stringify!($name),
132                        " scale exceeds the column type's maximum of ",
133                        stringify!($max)
134                    )
135                );
136                SCALE
137            };
138        }
139
140        impl<const SCALE: u32> Serialize for $name<SCALE> {
141            fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
142                // Fails compilation (at monomorphization) for scales the
143                // column type cannot represent.
144                let _ = Self::SCALE;
145                serializer.serialize_newtype_struct(stringify!($name), &self.0)
146            }
147        }
148
149        impl<'de, const SCALE: u32> Deserialize<'de> for $name<SCALE> {
150            fn deserialize<D: ::serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
151                let _ = Self::SCALE;
152                <$inner as Deserialize<'de>>::deserialize(d).map($name)
153            }
154        }
155    };
156}
157
158decimal_newtype! {
159    /// A pre-scaled `Decimal32(S)` / `Decimal(P ≤ 9, S)` value: the inner
160    /// `i32` is `value × 10^SCALE`, written little-endian.
161    Decimal32(i32), max_scale = 9
162}
163
164decimal_newtype! {
165    /// A pre-scaled `Decimal64(S)` / `Decimal(P ≤ 18, S)` value: the inner
166    /// `i64` is `value × 10^SCALE`, written little-endian.
167    Decimal64(i64), max_scale = 18
168}
169
170decimal_newtype! {
171    /// A pre-scaled `Decimal128(S)` / `Decimal(P ≤ 38, S)` value: the
172    /// inner `i128` is `value × 10^SCALE`, written little-endian.
173    Decimal128(i128), max_scale = 38
174}
175
176/// A `rust_decimal::Decimal` could not be converted into a pre-scaled
177/// decimal wrapper.
178#[cfg(feature = "rust_decimal")]
179#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
180#[non_exhaustive]
181pub enum DecimalConvertError {
182    /// The value cannot be represented at the target scale —
183    /// `rust_decimal`'s 96-bit mantissa ran out of precision. (This is
184    /// also why `Decimal128` columns with large precision cannot always
185    /// be filled from a `rust_decimal::Decimal`.)
186    #[error("{value} cannot be rescaled to {scale} fractional digits")]
187    Rescale {
188        /// The original value.
189        value: rust_decimal::Decimal,
190        /// The wrapper's scale.
191        scale: u32,
192    },
193    /// The rescaled mantissa overflows the column's integer width.
194    #[error("scaled mantissa of {value} overflows the column's integer width")]
195    Overflow {
196        /// The original value.
197        value: rust_decimal::Decimal,
198    },
199    /// A wrapper's raw value exceeds `rust_decimal`'s range (96-bit
200    /// mantissa, scale ≤ 28) when converting back out of the wire type.
201    #[error("raw decimal {raw} at scale {scale} exceeds rust_decimal's range")]
202    Unrepresentable {
203        /// The wrapper's raw pre-scaled integer.
204        raw: i128,
205        /// The wrapper's scale.
206        scale: u32,
207    },
208}
209
210/// Conversions between `rust_decimal::Decimal` and the pre-scaled
211/// wrappers. Rescaling delegates to `rust_decimal::Decimal::rescale`,
212/// which rounds midpoints away from zero (`1.505` at scale 2 → `1.51`);
213/// conversions are checked, never panicking. Convert in operator code,
214/// before the row struct — the encode hot path stays a plain integer
215/// write.
216#[cfg(feature = "rust_decimal")]
217mod rust_decimal_conv {
218    use super::{Decimal32, Decimal64, Decimal128, DecimalConvertError};
219    use rust_decimal::Decimal;
220
221    macro_rules! decimal_conversions {
222        ($wrapper:ident, $int:ty) => {
223            impl<const SCALE: u32> TryFrom<Decimal> for $wrapper<SCALE> {
224                type Error = DecimalConvertError;
225
226                fn try_from(value: Decimal) -> Result<Self, Self::Error> {
227                    // Compile-time scale bound of the wrapper itself.
228                    let _ = Self::SCALE;
229                    let mut scaled = value;
230                    scaled.rescale(SCALE);
231                    if scaled.scale() != SCALE {
232                        // rescale clamps when the 96-bit mantissa cannot
233                        // carry the requested fractional digits.
234                        return Err(DecimalConvertError::Rescale {
235                            value,
236                            scale: SCALE,
237                        });
238                    }
239                    <$int>::try_from(scaled.mantissa())
240                        .map($wrapper)
241                        .map_err(|_| DecimalConvertError::Overflow { value })
242                }
243            }
244
245            impl<const SCALE: u32> TryFrom<$wrapper<SCALE>> for Decimal {
246                type Error = DecimalConvertError;
247
248                fn try_from(value: $wrapper<SCALE>) -> Result<Self, Self::Error> {
249                    Decimal::try_from_i128_with_scale(i128::from(value.0), SCALE).map_err(|_| {
250                        DecimalConvertError::Unrepresentable {
251                            raw: i128::from(value.0),
252                            scale: SCALE,
253                        }
254                    })
255                }
256            }
257        };
258    }
259
260    decimal_conversions!(Decimal32, i32);
261    decimal_conversions!(Decimal64, i64);
262    decimal_conversions!(Decimal128, i128);
263}
264
265/// An `Int256` column value: 32 bytes, little-endian, two's complement.
266///
267/// Rust has no native 256-bit integer; this wrapper carries the raw wire
268/// layout. Build one from an `i128` (sign-extended) or from little-endian
269/// bytes produced by a big-integer crate. Also the documented escape hatch
270/// for `Decimal256(S)` columns: store `value × 10^S` as an `Int256`.
271#[derive(Clone, Copy, Debug, PartialEq, Eq)]
272pub struct Int256(pub [u8; 32]);
273
274impl Int256 {
275    /// Sign-extend an `i128` into the full 256-bit range.
276    #[must_use]
277    pub const fn from_i128(v: i128) -> Self {
278        let mut bytes = [if v < 0 { 0xff } else { 0x00 }; 32];
279        let le = v.to_le_bytes();
280        let mut i = 0;
281        while i < 16 {
282            bytes[i] = le[i];
283            i += 1;
284        }
285        Int256(bytes)
286    }
287
288    /// Wrap raw little-endian two's-complement bytes.
289    #[must_use]
290    pub const fn from_le_bytes(bytes: [u8; 32]) -> Self {
291        Int256(bytes)
292    }
293
294    /// The raw little-endian two's-complement bytes.
295    #[must_use]
296    pub const fn to_le_bytes(self) -> [u8; 32] {
297        self.0
298    }
299}
300
301impl Serialize for Int256 {
302    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
303        // The [u8; 32] array serializes as a fixed-width tuple: 32 raw
304        // bytes, no length prefix (serialize_bytes would LEB128-prefix).
305        serializer.serialize_newtype_struct("Int256", &self.0)
306    }
307}
308
309impl<'de> Deserialize<'de> for Int256 {
310    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
311        <[u8; 32]>::deserialize(d).map(Int256)
312    }
313}
314
315/// A `UInt256` column value: 32 bytes, little-endian.
316///
317/// See [`Int256`]; this is the unsigned counterpart.
318#[derive(Clone, Copy, Debug, PartialEq, Eq)]
319pub struct UInt256(pub [u8; 32]);
320
321impl UInt256 {
322    /// Zero-extend a `u128` into the full 256-bit range.
323    #[must_use]
324    pub const fn from_u128(v: u128) -> Self {
325        let mut bytes = [0u8; 32];
326        let le = v.to_le_bytes();
327        let mut i = 0;
328        while i < 16 {
329            bytes[i] = le[i];
330            i += 1;
331        }
332        UInt256(bytes)
333    }
334
335    /// Wrap raw little-endian bytes.
336    #[must_use]
337    pub const fn from_le_bytes(bytes: [u8; 32]) -> Self {
338        UInt256(bytes)
339    }
340
341    /// The raw little-endian bytes.
342    #[must_use]
343    pub const fn to_le_bytes(self) -> [u8; 32] {
344        self.0
345    }
346}
347
348impl Serialize for UInt256 {
349    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
350        serializer.serialize_newtype_struct("UInt256", &self.0)
351    }
352}
353
354impl<'de> Deserialize<'de> for UInt256 {
355    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
356        <[u8; 32]>::deserialize(d).map(UInt256)
357    }
358}
359
360/// A `Point` column: `(x, y)` as two `Float64`s.
361pub type Point = (f64, f64);
362/// A `Ring` column: a closed sequence of points (`Array(Point)`).
363pub type Ring = Vec<Point>;
364/// A `LineString` column: an open sequence of points (`Array(Point)`).
365pub type LineString = Vec<Point>;
366/// A `Polygon` column: an outer ring plus hole rings (`Array(Ring)`).
367pub type Polygon = Vec<Ring>;
368/// A `MultiLineString` column: `Array(LineString)`.
369pub type MultiLineString = Vec<LineString>;
370/// A `MultiPolygon` column: `Array(Polygon)`.
371pub type MultiPolygon = Vec<Polygon>;
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376    use crate::rowbinary::serialize_row;
377    use bytes::BytesMut;
378
379    fn enc<T: Serialize>(v: &T) -> Vec<u8> {
380        let mut buf = BytesMut::new();
381        serialize_row(v, &mut buf).expect("serialize");
382        buf.to_vec()
383    }
384
385    #[test]
386    fn date_and_time_newtypes_are_transparent_integers() {
387        assert_eq!(enc(&DateDays(1)), 1u16.to_le_bytes());
388        assert_eq!(enc(&Date32Days(-25567)), (-25567i32).to_le_bytes());
389        assert_eq!(enc(&DateTimeSeconds(42)), 42u32.to_le_bytes());
390        assert_eq!(enc(&DateTime64Secs(-1)), (-1i64).to_le_bytes());
391        assert_eq!(enc(&DateTime64Millis(1_000)), 1_000i64.to_le_bytes());
392        assert_eq!(enc(&DateTime64Micros(7)), 7i64.to_le_bytes());
393        assert_eq!(enc(&DateTime64Nanos(7)), 7i64.to_le_bytes());
394        assert_eq!(enc(&TimeSeconds(-3599)), (-3599i32).to_le_bytes());
395        assert_eq!(enc(&Time64Nanos(1)), 1i64.to_le_bytes());
396    }
397
398    #[test]
399    fn decimals_write_the_raw_scaled_integer() {
400        assert_eq!(enc(&Decimal32::<2>(999)), 999i32.to_le_bytes());
401        assert_eq!(enc(&Decimal64::<4>(-15_000)), (-15_000i64).to_le_bytes());
402        assert_eq!(enc(&Decimal128::<10>(1)), 1i128.to_le_bytes());
403        // Scale bounds are compile-time: Decimal32::<10> fails to build
404        // (post-monomorphization const assert), so there is no runtime case
405        // to test here.
406    }
407
408    #[test]
409    fn int256_layouts() {
410        assert_eq!(Int256::from_i128(-1).0, [0xff; 32]);
411        let one = UInt256::from_u128(1);
412        let mut expected = [0u8; 32];
413        expected[0] = 1;
414        assert_eq!(one.0, expected);
415
416        // Sign extension keeps the i128 value's magnitude in the low half.
417        let v = Int256::from_i128(i128::MIN);
418        assert_eq!(&v.0[..16], &i128::MIN.to_le_bytes());
419        assert_eq!(&v.0[16..], &[0xff; 16]);
420
421        // Wire = the 32 raw bytes, no length prefix.
422        assert_eq!(enc(&one), expected);
423        assert_eq!(enc(&Int256::from_i128(-1)), [0xff; 32]);
424    }
425
426    #[cfg(feature = "rust_decimal")]
427    #[test]
428    fn rust_decimal_conversions_are_checked_and_round_trip() {
429        use rust_decimal::Decimal;
430
431        // 1.505 at scale 2: rescale rounds midpoints away from zero ->
432        // 1.51 -> raw 151.
433        let d = Decimal::new(1505, 3);
434        assert_eq!(Decimal64::<2>::try_from(d), Ok(Decimal64::<2>(151)));
435
436        // Round trip through the wrapper and back.
437        let wrapped = Decimal64::<4>::try_from(Decimal::new(-15_000, 4)).unwrap();
438        assert_eq!(wrapped, Decimal64::<4>(-15_000));
439        assert_eq!(
440            Decimal::try_from(wrapped).unwrap(),
441            Decimal::new(-15_000, 4)
442        );
443
444        // Mantissa wider than the column's integer.
445        assert!(matches!(
446            Decimal32::<0>::try_from(Decimal::MAX),
447            Err(DecimalConvertError::Overflow { .. })
448        ));
449
450        // 96-bit mantissa cannot take 10 more fractional digits.
451        assert!(matches!(
452            Decimal128::<10>::try_from(Decimal::MAX),
453            Err(DecimalConvertError::Rescale { scale: 10, .. })
454        ));
455
456        // A raw i128 beyond rust_decimal's 96-bit range fails the back
457        // conversion instead of panicking.
458        assert!(matches!(
459            Decimal::try_from(Decimal128::<2>(i128::MAX)),
460            Err(DecimalConvertError::Unrepresentable { .. })
461        ));
462    }
463
464    #[test]
465    fn geo_shapes_encode_as_nested_arrays_of_points() {
466        let p: Point = (1.0, 2.0);
467        let mut expected = 1.0f64.to_le_bytes().to_vec();
468        expected.extend_from_slice(&2.0f64.to_le_bytes());
469        assert_eq!(enc(&p), expected);
470
471        let ring: Ring = vec![(1.0, 2.0), (3.0, 4.0)];
472        let bytes = enc(&ring);
473        assert_eq!(bytes[0], 2, "LEB128 point count");
474        assert_eq!(bytes.len(), 1 + 2 * 16);
475
476        let poly: Polygon = vec![ring.clone()];
477        let bytes = enc(&poly);
478        assert_eq!(bytes[0], 1, "one ring");
479        assert_eq!(bytes[1], 2, "two points");
480
481        let multi: MultiPolygon = vec![poly];
482        assert_eq!(enc(&multi)[0], 1);
483    }
484}