Skip to main content

prax_scylladb/
row.rs

1//! Row deserialization for `ScyllaDB` results.
2
3use scylla::frame::response::result::{CqlValue, Row};
4use serde::de::DeserializeOwned;
5
6use crate::error::{ScyllaError, ScyllaResult};
7use crate::types::ScyllaValue;
8
9/// Trait for types that can be constructed from a `ScyllaDB` row.
10pub trait FromScyllaRow: Sized {
11    /// Construct an instance from a row.
12    fn from_row(row: &Row) -> ScyllaResult<Self>;
13}
14
15/// A helper for extracting values from a row by index.
16pub struct RowAccessor<'a> {
17    row: &'a Row,
18}
19
20impl<'a> RowAccessor<'a> {
21    /// Create a new accessor for a row.
22    #[must_use]
23    pub fn new(row: &'a Row) -> Self {
24        Self { row }
25    }
26
27    /// Get a value by column index.
28    pub fn get<T: FromCqlValue>(&self, index: usize) -> ScyllaResult<T> {
29        self.row
30            .columns
31            .get(index)
32            .ok_or_else(|| {
33                ScyllaError::deserialization(format!("Column index {index} out of bounds"))
34            })?
35            .as_ref()
36            .map(|v| T::from_cql(v))
37            .transpose()?
38            .ok_or_else(|| ScyllaError::deserialization(format!("Column {index} is null")))
39    }
40
41    /// Get an optional value by column index.
42    pub fn get_opt<T: FromCqlValue>(&self, index: usize) -> ScyllaResult<Option<T>> {
43        match self.row.columns.get(index) {
44            Some(Some(value)) => Ok(Some(T::from_cql(value)?)),
45            Some(None) | None => Ok(None),
46        }
47    }
48
49    /// Get the number of columns.
50    #[must_use]
51    pub fn len(&self) -> usize {
52        self.row.columns.len()
53    }
54
55    /// Check if the row has no columns.
56    #[must_use]
57    pub fn is_empty(&self) -> bool {
58        self.row.columns.is_empty()
59    }
60}
61
62/// Trait for types that can be extracted from a CQL value.
63pub trait FromCqlValue: Sized {
64    /// Extract a value from a CQL value.
65    fn from_cql(value: &CqlValue) -> ScyllaResult<Self>;
66}
67
68impl FromCqlValue for bool {
69    fn from_cql(value: &CqlValue) -> ScyllaResult<Self> {
70        match value {
71            CqlValue::Boolean(v) => Ok(*v),
72            _ => Err(ScyllaError::type_conversion("Expected boolean")),
73        }
74    }
75}
76
77impl FromCqlValue for i8 {
78    fn from_cql(value: &CqlValue) -> ScyllaResult<Self> {
79        match value {
80            CqlValue::TinyInt(v) => Ok(*v),
81            _ => Err(ScyllaError::type_conversion("Expected tinyint")),
82        }
83    }
84}
85
86impl FromCqlValue for i16 {
87    fn from_cql(value: &CqlValue) -> ScyllaResult<Self> {
88        match value {
89            CqlValue::SmallInt(v) => Ok(*v),
90            _ => Err(ScyllaError::type_conversion("Expected smallint")),
91        }
92    }
93}
94
95impl FromCqlValue for i32 {
96    fn from_cql(value: &CqlValue) -> ScyllaResult<Self> {
97        match value {
98            CqlValue::Int(v) => Ok(*v),
99            _ => Err(ScyllaError::type_conversion("Expected int")),
100        }
101    }
102}
103
104impl FromCqlValue for i64 {
105    fn from_cql(value: &CqlValue) -> ScyllaResult<Self> {
106        match value {
107            CqlValue::BigInt(v) => Ok(*v),
108            CqlValue::Counter(v) => Ok(v.0),
109            _ => Err(ScyllaError::type_conversion("Expected bigint")),
110        }
111    }
112}
113
114impl FromCqlValue for f32 {
115    fn from_cql(value: &CqlValue) -> ScyllaResult<Self> {
116        match value {
117            CqlValue::Float(v) => Ok(*v),
118            _ => Err(ScyllaError::type_conversion("Expected float")),
119        }
120    }
121}
122
123impl FromCqlValue for f64 {
124    fn from_cql(value: &CqlValue) -> ScyllaResult<Self> {
125        match value {
126            CqlValue::Double(v) => Ok(*v),
127            CqlValue::Float(v) => Ok(f64::from(*v)),
128            _ => Err(ScyllaError::type_conversion("Expected double")),
129        }
130    }
131}
132
133impl FromCqlValue for String {
134    fn from_cql(value: &CqlValue) -> ScyllaResult<Self> {
135        match value {
136            CqlValue::Text(v) | CqlValue::Ascii(v) => Ok(v.clone()),
137            _ => Err(ScyllaError::type_conversion("Expected text")),
138        }
139    }
140}
141
142impl FromCqlValue for Vec<u8> {
143    fn from_cql(value: &CqlValue) -> ScyllaResult<Self> {
144        match value {
145            CqlValue::Blob(v) => Ok(v.clone()),
146            _ => Err(ScyllaError::type_conversion("Expected blob")),
147        }
148    }
149}
150
151impl FromCqlValue for uuid::Uuid {
152    fn from_cql(value: &CqlValue) -> ScyllaResult<Self> {
153        match value {
154            CqlValue::Uuid(v) => Ok(*v),
155            CqlValue::Timeuuid(v) => Ok((*v).into()),
156            _ => Err(ScyllaError::type_conversion("Expected uuid")),
157        }
158    }
159}
160
161impl FromCqlValue for chrono::DateTime<chrono::Utc> {
162    fn from_cql(value: &CqlValue) -> ScyllaResult<Self> {
163        match value {
164            CqlValue::Timestamp(ts) => chrono::DateTime::from_timestamp_millis(ts.0)
165                .ok_or_else(|| ScyllaError::type_conversion("Invalid timestamp")),
166            _ => Err(ScyllaError::type_conversion("Expected timestamp")),
167        }
168    }
169}
170
171impl FromCqlValue for chrono::NaiveDate {
172    fn from_cql(value: &CqlValue) -> ScyllaResult<Self> {
173        match value {
174            // CQL `date` is an unsigned day count offset by 2^31 from the Unix
175            // epoch; chrono counts days from 0001-01-01 (1970-01-01 = 719_163).
176            CqlValue::Date(d) => {
177                let days_from_ce = i64::from(d.0) - (1i64 << 31) + 719_163;
178                i32::try_from(days_from_ce)
179                    .ok()
180                    .and_then(chrono::NaiveDate::from_num_days_from_ce_opt)
181                    .ok_or_else(|| ScyllaError::type_conversion("Invalid date"))
182            }
183            _ => Err(ScyllaError::type_conversion("Expected date")),
184        }
185    }
186}
187
188impl FromCqlValue for std::net::IpAddr {
189    fn from_cql(value: &CqlValue) -> ScyllaResult<Self> {
190        match value {
191            CqlValue::Inet(v) => Ok(*v),
192            _ => Err(ScyllaError::type_conversion("Expected inet")),
193        }
194    }
195}
196
197impl FromCqlValue for ScyllaValue {
198    fn from_cql(value: &CqlValue) -> ScyllaResult<Self> {
199        Ok(value.clone().into())
200    }
201}
202
203impl<T: FromCqlValue> FromCqlValue for Option<T> {
204    fn from_cql(value: &CqlValue) -> ScyllaResult<Self> {
205        match value {
206            CqlValue::Empty => Ok(None),
207            _ => Ok(Some(T::from_cql(value)?)),
208        }
209    }
210}
211
212impl<T: FromCqlValue> FromCqlValue for Vec<T> {
213    fn from_cql(value: &CqlValue) -> ScyllaResult<Self> {
214        match value {
215            CqlValue::List(items) | CqlValue::Set(items) => {
216                items.iter().map(|v| T::from_cql(v)).collect()
217            }
218            _ => Err(ScyllaError::type_conversion("Expected list or set")),
219        }
220    }
221}
222
223impl FromCqlValue for serde_json::Value {
224    fn from_cql(value: &CqlValue) -> ScyllaResult<Self> {
225        let scylla_value: ScyllaValue = value.clone().into();
226        Ok(scylla_value.into())
227    }
228}
229
230/// Implement `FromScyllaRow` for types that implement `DeserializeOwned`.
231///
232/// This requires converting the row to JSON first, which may not be efficient
233/// for all use cases.
234///
235/// The driver's `Row` carries only bare column values — no column names or
236/// other metadata — so values are deserialized **positionally**: tuple
237/// structs and sequences map directly, and named-field structs are matched
238/// in field-declaration order via serde's sequence support. The selected
239/// columns must therefore line up exactly with the target type's field
240/// order; prefer explicit column lists over `SELECT *`, and use
241/// [`RowAccessor`] or [`impl_from_row!`](crate::impl_from_row) for
242/// index-based extraction independent of declaration order.
243impl<T: DeserializeOwned> FromScyllaRow for T {
244    fn from_row(row: &Row) -> ScyllaResult<Self> {
245        // Convert the row to a positional JSON array for serde. Column names
246        // are unavailable here (`Row` has no metadata), so a name-keyed JSON
247        // object can't be built; serde maps the array onto tuple structs
248        // directly and onto named structs in field-declaration order.
249        let values: Vec<serde_json::Value> = row
250            .columns
251            .iter()
252            .map(|col| {
253                col.as_ref().map_or(serde_json::Value::Null, |v| {
254                    let sv: ScyllaValue = v.clone().into();
255                    sv.into()
256                })
257            })
258            .collect();
259
260        serde_json::from_value(serde_json::Value::Array(values))
261            .map_err(|e| ScyllaError::deserialization(e.to_string()))
262    }
263}
264
265/// A macro to implement `FromScyllaRow` for a struct with named fields.
266///
267/// Usage:
268/// ```text
269/// impl_from_row!(User {
270///     id: uuid::Uuid,
271///     email: String,
272///     name: Option<String>,
273///     created_at: chrono::DateTime<chrono::Utc>,
274/// });
275/// ```
276#[macro_export]
277macro_rules! impl_from_row {
278    ($name:ident { $($field:ident: $ty:ty),* $(,)? }) => {
279        impl $crate::row::FromScyllaRow for $name {
280            fn from_row(row: &scylla::frame::response::result::Row) -> $crate::error::ScyllaResult<Self> {
281                let accessor = $crate::row::RowAccessor::new(row);
282                let mut idx = 0;
283                Ok(Self {
284                    $(
285                        $field: {
286                            let val = accessor.get::<$ty>(idx)?;
287                            idx += 1;
288                            val
289                        },
290                    )*
291                })
292            }
293        }
294    };
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    #[test]
302    fn test_from_cql_primitives() {
303        assert!(bool::from_cql(&CqlValue::Boolean(true)).unwrap());
304        assert_eq!(i32::from_cql(&CqlValue::Int(42)).unwrap(), 42);
305        assert_eq!(i64::from_cql(&CqlValue::BigInt(100)).unwrap(), 100);
306        assert!((f64::from_cql(&CqlValue::Double(3.14)).unwrap() - 3.14).abs() < f64::EPSILON);
307        assert_eq!(
308            String::from_cql(&CqlValue::Text("hello".into())).unwrap(),
309            "hello"
310        );
311    }
312
313    #[test]
314    fn test_from_cql_optional() {
315        let result: Option<i32> = Option::<i32>::from_cql(&CqlValue::Int(42)).unwrap();
316        assert_eq!(result, Some(42));
317
318        let result: Option<i32> = Option::<i32>::from_cql(&CqlValue::Empty).unwrap();
319        assert_eq!(result, None);
320    }
321
322    #[test]
323    fn test_from_cql_list() {
324        let list = CqlValue::List(vec![CqlValue::Int(1), CqlValue::Int(2), CqlValue::Int(3)]);
325        let result: Vec<i32> = Vec::<i32>::from_cql(&list).unwrap();
326        assert_eq!(result, vec![1, 2, 3]);
327    }
328
329    #[test]
330    fn test_from_cql_date_round_trip() {
331        use chrono::NaiveDate;
332        use scylla::frame::value::CqlDate;
333
334        // CQL encodes `date` as unsigned days since 1970-01-01 offset by 2^31,
335        // so the epoch boundary itself is exactly 2^31 on the wire.
336        let epoch = NaiveDate::from_cql(&CqlValue::Date(CqlDate(1u32 << 31))).unwrap();
337        assert_eq!(epoch, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap());
338
339        // Modern date: 2000-01-01 is 10_957 days after the epoch.
340        let modern = NaiveDate::from_cql(&CqlValue::Date(CqlDate((1u32 << 31) + 10_957))).unwrap();
341        assert_eq!(modern, NaiveDate::from_ymd_opt(2000, 1, 1).unwrap());
342
343        // Pre-1970 date: 1900-01-01 is 25_567 days before the epoch.
344        let past = NaiveDate::from_cql(&CqlValue::Date(CqlDate((1u32 << 31) - 25_567))).unwrap();
345        assert_eq!(past, NaiveDate::from_ymd_opt(1900, 1, 1).unwrap());
346    }
347
348    #[test]
349    fn test_from_cql_date_out_of_range() {
350        use chrono::NaiveDate;
351        use scylla::frame::value::CqlDate;
352
353        // Raw value 0 is ~5.9M years BCE, far outside chrono's range.
354        assert!(NaiveDate::from_cql(&CqlValue::Date(CqlDate(0))).is_err());
355    }
356
357    #[test]
358    fn test_from_scylla_row_named_struct() {
359        #[derive(Debug, PartialEq, serde::Deserialize)]
360        struct UserRow {
361            id: i32,
362            name: String,
363            email: Option<String>,
364        }
365
366        // Columns map onto fields positionally, in declaration order.
367        let row = Row {
368            columns: vec![
369                Some(CqlValue::Int(7)),
370                Some(CqlValue::Text("alice".into())),
371                None,
372            ],
373        };
374
375        let user = UserRow::from_row(&row).unwrap();
376        assert_eq!(
377            user,
378            UserRow {
379                id: 7,
380                name: "alice".to_string(),
381                email: None,
382            }
383        );
384    }
385
386    #[test]
387    fn test_from_scylla_row_tuple_struct() {
388        #[derive(Debug, PartialEq, serde::Deserialize)]
389        struct Pair(i32, String);
390
391        let row = Row {
392            columns: vec![Some(CqlValue::Int(1)), Some(CqlValue::Text("one".into()))],
393        };
394
395        assert_eq!(Pair::from_row(&row).unwrap(), Pair(1, "one".to_string()));
396    }
397}