clickhouse.rs
clickhouse.rs copied to clipboard
Support for `rust_decimal::Decimal` in RowBinaryWithNamesAndTypes Deserialization
💡 Support for rust_decimal::Decimal in RowBinaryWithNamesAndTypes Deserialization
Summary
After upgrading from clickhouse = 0.13.3 to 0.14.0, direct deserialization attempt of Decimal columns into rust_decimal::Decimal no longer works.
The driver now throws:
DeserializeAnyNotSupported
Environment
- clickhouse crate: 0.14.x
- rust_decimal: 1.x
- ClickHouse server: 24.x
- Rust: 1.89
Example Code
#[derive(clickhouse::Row, serde::Deserialize, Debug)]
struct SpotPrice {
hour_of_day: u8,
spot_price_eur: rust_decimal::Decimal,
}
let rows = ch_client
.query("SELECT hour_of_day, spot_price_eur FROM elspotprices LIMIT 5")
.fetch_all::<SpotPrice>()
.await?;
If I change the field type to f32, it works correctly, but that loses precision when working with financial or energy pricing data.
Perhaps this could be used as a workaround for now:
trait RawDecimalType {}
impl RawDecimalType for i32 {}
impl RawDecimalType for i64 {}
impl RawDecimalType for i128 {}
fn deserialize_rust_decimal<
'de,
D,
T: RawDecimalType + Deserialize<'de> + Into<i128> + Display,
const SCALE: u32,
>(
d: D,
) -> Result<Decimal, D::Error>
where
D: Deserializer<'de>,
{
let raw = T::deserialize(d)?;
Decimal::try_from_i128_with_scale(raw.into(), SCALE).map_err(|e| {
let sz = std::any::type_name::<T>();
DeserializeError::custom(format!(
"failed to deserialize Decimal with size {sz} and scale {SCALE}, cause: {e}"
))
})
}
fn serialize_rust_decimal<S, T: Serialize + TryFrom<i128>, const SCALE: u32>(
val: &Decimal,
s: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mantissa = val.mantissa();
let raw = T::try_from(mantissa)
.map_err(|_| S::Error::custom(format!("mantissa does not fit into i32: {mantissa}")))?;
raw.serialize(s)
}
which then can be used with serialize_with and deserialize_with:
use clickhouse::Row;
use rust_decimal::Decimal;
use serde::de::Error as DeserializeError;
use serde::ser::Error as SerializeError;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt::Display;
#[tokio::test]
async fn decimals() {
let table_name = "test";
#[derive(Clone, Debug, Row, Serialize, Deserialize, PartialEq)]
struct Data {
#[serde(
serialize_with = "serialize_rust_decimal::<_, i32, 4>",
deserialize_with = "deserialize_rust_decimal::<_, i32, 4>"
)]
decimal32_9_4: Decimal,
#[serde(
serialize_with = "serialize_rust_decimal::<_, i64, 8>",
deserialize_with = "deserialize_rust_decimal::<_, i64, 8>"
)]
decimal64_18_8: Decimal,
#[serde(
serialize_with = "serialize_rust_decimal::<_, i128, 12>",
deserialize_with = "deserialize_rust_decimal::<_, i128, 12>"
)]
decimal128_38_12: Decimal,
}
let client = prepare_database!();
client
.query(
"
CREATE OR REPLACE TABLE {table_name: Identifier} (
decimal32_9_4 Decimal32(4),
decimal64_18_8 Decimal64(8),
decimal128_38_12 Decimal128(12)
)
ENGINE = MergeTree
ORDER BY tuple()
",
)
.param("table_name", table_name)
.execute()
.await
.unwrap();
let rows = vec![Data {
// 42.1234
decimal32_9_4: Decimal::from_i128_with_scale(421234, 4),
// 144.56789012
decimal64_18_8: Decimal::from_i128_with_scale(14456789012, 8),
// -170141183460469.231731687303
decimal128_38_12: Decimal::from_i128_with_scale(-170141183460469231731687303, 12),
}];
let mut insert = client.insert::<Data>("test").await.unwrap();
for row in &rows {
insert.write(row).await.unwrap();
}
insert.end().await.unwrap();
let result = client
.query("SELECT ?fields FROM {table_name: Identifier} ORDER BY () ASC")
.param("table_name", table_name)
.fetch_all::<Data>()
.await
.unwrap();
assert_eq!(result, rows);
}
trait RawDecimalType {}
impl RawDecimalType for i32 {}
impl RawDecimalType for i64 {}
impl RawDecimalType for i128 {}
fn deserialize_rust_decimal<
'de,
D,
T: RawDecimalType + Deserialize<'de> + Into<i128> + Display,
const SCALE: u32,
>(
d: D,
) -> Result<Decimal, D::Error>
where
D: Deserializer<'de>,
{
let raw = T::deserialize(d)?;
Decimal::try_from_i128_with_scale(raw.into(), SCALE).map_err(|e| {
let sz = std::any::type_name::<T>();
DeserializeError::custom(format!(
"failed to deserialize Decimal with size {sz} and scale {SCALE}, cause: {e}"
))
})
}
fn serialize_rust_decimal<S, T: Serialize + TryFrom<i128>, const SCALE: u32>(
val: &Decimal,
s: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mantissa = val.mantissa();
let raw = T::try_from(mantissa)
.map_err(|_| S::Error::custom(format!("mantissa does not fit into i32: {mantissa}")))?;
raw.serialize(s)
}
However, looks like something we could also implement in the scope of #236 under the rust_decimal feature flag.
We should be able to support deserialize_any with the info RBWNAT gives us.