Skip to main content

rainier_database/
row.rs

1//! Owned row snapshots — [`OwnedRow`], [`Cell`] and [`ColumnRequest`].
2//!
3//! Rainier ORM hands back `Box<dyn Row>`, and `Row` carries no `Send` bound —
4//! correctly, since a driver's row may borrow from a `!Send` connection. But a
5//! value that is not `Send` cannot be the output of a `Send` future, so a row
6//! in that form can never leave the request path's thread.
7//!
8//! So the connection port copies each row into an [`OwnedRow`] before returning
9//! it: a plain map of name → [`Cell`], which is `Send`, and which implements
10//! Rainier ORM's own [`Row`] trait so the derived `Entity::from_row` decoder
11//! reads it unchanged. Decoding still belongs entirely to the ORM; only the
12//! transport of the raw values is ours.
13//!
14//! Reading a row requires knowing which columns to ask for, because `Row` is
15//! addressed by name and type rather than enumerable — hence
16//! [`ColumnRequest`], which the caller builds from `E::columns()`.
17
18use std::collections::HashMap;
19
20use chrono::{DateTime, NaiveDate, Utc};
21use rainier_orm::{ColumnType, Row};
22
23/// A column to read out of a driver row, and the type to read it as.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct ColumnRequest {
26    /// The column's name.
27    pub name: String,
28    /// The type its getter should use.
29    pub ty: ColumnType,
30}
31
32impl ColumnRequest {
33    /// Request `name` as `ty`.
34    pub fn new(name: impl Into<String>, ty: ColumnType) -> Self {
35        Self { name: name.into(), ty }
36    }
37
38    /// Every column of an entity, in declaration order — what a `SELECT *`
39    /// of that entity should be read back as.
40    pub fn for_entity<E: rainier_orm::Entity>() -> Vec<ColumnRequest> {
41        E::columns().iter().map(|column| ColumnRequest::new(column.name, column.ty)).collect()
42    }
43}
44
45/// One cell's value.
46#[derive(Debug, Clone, PartialEq)]
47pub enum Cell {
48    /// SQL `NULL`, or a column the driver did not return.
49    Null,
50    /// A boolean.
51    Bool(bool),
52    /// A signed integer.
53    Int(i64),
54    /// An unsigned integer.
55    Uint(u64),
56    /// A floating-point number.
57    Float(f64),
58    /// Text.
59    Text(String),
60    /// Binary.
61    Bytes(Vec<u8>),
62    /// A UTC timestamp.
63    Timestamp(DateTime<Utc>),
64    /// A calendar date.
65    Date(NaiveDate),
66}
67
68impl Cell {
69    /// The cell as an unsigned count, for a `COUNT(*)` column.
70    ///
71    /// A driver may report the count as signed or unsigned depending on the
72    /// engine; a negative one is impossible, so it clamps rather than failing.
73    pub fn as_u64(&self) -> Option<u64> {
74        match self {
75            Cell::Int(value) => Some((*value).max(0) as u64),
76            Cell::Uint(value) => Some(*value),
77            Cell::Float(value) => Some(value.max(0.0) as u64),
78            Cell::Text(value) => value.parse().ok(),
79            _ => None,
80        }
81    }
82
83    /// The cell as a bindable [`Value`](rainier_orm::sea_query::Value), for
84    /// feeding one query's output into the next — reading a pivot, then the
85    /// rows it points at.
86    ///
87    /// `Null` is `None`: a NULL key links nothing, and binding it would make
88    /// `IN (NULL)` match no row while looking like it might.
89    pub fn to_value(&self) -> Option<rainier_orm::sea_query::Value> {
90        use rainier_orm::sea_query::Value as V;
91        match self {
92            Cell::Null => None,
93            Cell::Bool(value) => Some(V::Bool(Some(*value))),
94            Cell::Int(value) => Some(V::BigInt(Some(*value))),
95            Cell::Uint(value) => Some(V::BigUnsigned(Some(*value))),
96            Cell::Float(value) => Some(V::Double(Some(*value))),
97            Cell::Text(value) => Some(V::String(Some(Box::new(value.clone())))),
98            Cell::Bytes(value) => Some(V::Bytes(Some(Box::new(value.clone())))),
99            Cell::Timestamp(value) => Some(V::ChronoDateTimeUtc(Some(Box::new(*value)))),
100            Cell::Date(value) => Some(V::ChronoDate(Some(Box::new(*value)))),
101        }
102    }
103}
104
105macro_rules! cell_from {
106    ($ty:ty, $variant:ident) => {
107        impl From<$ty> for Cell {
108            fn from(value: $ty) -> Self {
109                Cell::$variant(value)
110            }
111        }
112    };
113    ($ty:ty, $variant:ident, as $cast:ty) => {
114        impl From<$ty> for Cell {
115            fn from(value: $ty) -> Self {
116                Cell::$variant(value as $cast)
117            }
118        }
119    };
120}
121
122cell_from!(bool, Bool);
123cell_from!(i32, Int, as i64);
124cell_from!(i64, Int);
125cell_from!(u32, Uint, as u64);
126cell_from!(u64, Uint);
127cell_from!(f64, Float);
128cell_from!(String, Text);
129cell_from!(Vec<u8>, Bytes);
130cell_from!(DateTime<Utc>, Timestamp);
131cell_from!(NaiveDate, Date);
132
133impl From<&str> for Cell {
134    fn from(value: &str) -> Self {
135        Cell::Text(value.to_string())
136    }
137}
138
139impl<T: Into<Cell>> From<Option<T>> for Cell {
140    fn from(value: Option<T>) -> Self {
141        value.map(Into::into).unwrap_or(Cell::Null)
142    }
143}
144
145/// A row copied out of a driver, addressable by column name.
146#[derive(Debug, Clone, Default, PartialEq)]
147pub struct OwnedRow {
148    columns: HashMap<String, Cell>,
149}
150
151impl OwnedRow {
152    /// An empty row.
153    pub fn new() -> Self {
154        Self::default()
155    }
156
157    /// Set a column.
158    pub fn with(mut self, name: impl Into<String>, value: impl Into<Cell>) -> Self {
159        self.columns.insert(name.into(), value.into());
160        self
161    }
162
163    /// Set a column in place.
164    pub fn set(&mut self, name: impl Into<String>, value: impl Into<Cell>) {
165        self.columns.insert(name.into(), value.into());
166    }
167
168    /// Copy the requested columns out of a driver row.
169    ///
170    /// Each column is read through the getter its [`ColumnType`] names, which
171    /// is the same dispatch Rainier ORM's own decoder performs — so a value that
172    /// round-trips through here decodes identically to one read directly.
173    pub fn snapshot(row: &dyn Row, columns: &[ColumnRequest]) -> rainier_orm::Result<Self> {
174        let mut owned = OwnedRow::new();
175        for request in columns {
176            let name = request.name.as_str();
177            let cell = match request.ty {
178                ColumnType::Bool => Cell::from(row.get_bool(name)?),
179                ColumnType::Int => Cell::from(row.get_i32(name)?),
180                ColumnType::BigInt => Cell::from(row.get_i64(name)?),
181                ColumnType::Uint => Cell::from(row.get_u32(name)?),
182                ColumnType::BigUint => Cell::from(row.get_u64(name)?),
183                ColumnType::Double => Cell::from(row.get_f64(name)?),
184                ColumnType::Text => Cell::from(row.get_string(name)?),
185                ColumnType::Binary => Cell::from(row.get_bytes(name)?),
186                ColumnType::Timestamp => Cell::from(row.get_datetime(name)?),
187                ColumnType::Date => Cell::from(row.get_naive_date(name)?),
188            };
189            owned.set(request.name.clone(), cell);
190        }
191        Ok(owned)
192    }
193
194    /// Whether the row holds no columns.
195    pub fn is_empty(&self) -> bool {
196        self.columns.is_empty()
197    }
198
199    /// How many columns the row holds.
200    pub fn len(&self) -> usize {
201        self.columns.len()
202    }
203
204    /// The raw cell at `name`, if it was read.
205    pub fn cell(&self, name: &str) -> Option<&Cell> {
206        self.columns.get(name)
207    }
208
209    /// The cell at `name`, treating `Null` and "absent" alike — the two are
210    /// indistinguishable to a caller asking for a value.
211    fn value(&self, name: &str) -> Option<&Cell> {
212        match self.columns.get(name) {
213            Some(Cell::Null) | None => None,
214            Some(cell) => Some(cell),
215        }
216    }
217}
218
219fn wrong_type<T>(column: &str, cell: &Cell, wanted: &str) -> rainier_orm::Result<T> {
220    Err(anyhow::anyhow!("column `{column}` holds {cell:?}, which is not {wanted}"))
221}
222
223impl Row for OwnedRow {
224    fn get_bool(&self, col: &str) -> rainier_orm::Result<Option<bool>> {
225        match self.value(col) {
226            None => Ok(None),
227            Some(Cell::Bool(value)) => Ok(Some(*value)),
228            // SQLite and MySQL both store booleans as integers.
229            Some(Cell::Int(value)) => Ok(Some(*value != 0)),
230            Some(Cell::Uint(value)) => Ok(Some(*value != 0)),
231            Some(other) => wrong_type(col, other, "a boolean"),
232        }
233    }
234
235    fn get_i32(&self, col: &str) -> rainier_orm::Result<Option<i32>> {
236        Ok(self.get_i64(col)?.map(|value| value as i32))
237    }
238
239    fn get_i64(&self, col: &str) -> rainier_orm::Result<Option<i64>> {
240        match self.value(col) {
241            None => Ok(None),
242            Some(Cell::Int(value)) => Ok(Some(*value)),
243            Some(Cell::Uint(value)) => Ok(Some(*value as i64)),
244            Some(Cell::Bool(value)) => Ok(Some(*value as i64)),
245            Some(other) => wrong_type(col, other, "an integer"),
246        }
247    }
248
249    fn get_u32(&self, col: &str) -> rainier_orm::Result<Option<u32>> {
250        Ok(self.get_u64(col)?.map(|value| value as u32))
251    }
252
253    fn get_u64(&self, col: &str) -> rainier_orm::Result<Option<u64>> {
254        match self.value(col) {
255            None => Ok(None),
256            Some(Cell::Uint(value)) => Ok(Some(*value)),
257            // SQLite and Postgres have no unsigned types, so an unsigned
258            // column comes back signed and is cast — the same accommodation
259            // Rainier ORM makes in its own drivers.
260            Some(Cell::Int(value)) => Ok(Some(*value as u64)),
261            Some(Cell::Bool(value)) => Ok(Some(*value as u64)),
262            Some(other) => wrong_type(col, other, "an unsigned integer"),
263        }
264    }
265
266    fn get_f64(&self, col: &str) -> rainier_orm::Result<Option<f64>> {
267        match self.value(col) {
268            None => Ok(None),
269            Some(Cell::Float(value)) => Ok(Some(*value)),
270            Some(Cell::Int(value)) => Ok(Some(*value as f64)),
271            Some(Cell::Uint(value)) => Ok(Some(*value as f64)),
272            Some(other) => wrong_type(col, other, "a number"),
273        }
274    }
275
276    fn get_string(&self, col: &str) -> rainier_orm::Result<Option<String>> {
277        match self.value(col) {
278            None => Ok(None),
279            Some(Cell::Text(value)) => Ok(Some(value.clone())),
280            Some(other) => wrong_type(col, other, "text"),
281        }
282    }
283
284    fn get_bytes(&self, col: &str) -> rainier_orm::Result<Option<Vec<u8>>> {
285        match self.value(col) {
286            None => Ok(None),
287            Some(Cell::Bytes(value)) => Ok(Some(value.clone())),
288            Some(Cell::Text(value)) => Ok(Some(value.as_bytes().to_vec())),
289            Some(other) => wrong_type(col, other, "bytes"),
290        }
291    }
292
293    fn get_datetime(&self, col: &str) -> rainier_orm::Result<Option<DateTime<Utc>>> {
294        match self.value(col) {
295            None => Ok(None),
296            Some(Cell::Timestamp(value)) => Ok(Some(*value)),
297            // SQLite stores timestamps as text.
298            Some(Cell::Text(value)) => value
299                .parse::<DateTime<Utc>>()
300                .map(Some)
301                .map_err(|e| anyhow::anyhow!("column `{col}` is not a timestamp: {e}")),
302            Some(other) => wrong_type(col, other, "a timestamp"),
303        }
304    }
305
306    fn get_naive_date(&self, col: &str) -> rainier_orm::Result<Option<NaiveDate>> {
307        match self.value(col) {
308            None => Ok(None),
309            Some(Cell::Date(value)) => Ok(Some(*value)),
310            Some(Cell::Text(value)) => NaiveDate::parse_from_str(value, "%Y-%m-%d")
311                .map(Some)
312                .map_err(|e| anyhow::anyhow!("column `{col}` is not a date: {e}")),
313            Some(other) => wrong_type(col, other, "a date"),
314        }
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321    use rainier_orm::Entity as _;
322
323    #[derive(rainier_orm::Entity, Clone, Debug, PartialEq)]
324    #[orm(table = "posts")]
325    struct Post {
326        #[orm(pk, auto_increment)]
327        id: u64,
328        title: String,
329        views: Option<i64>,
330    }
331
332    #[test]
333    fn an_owned_row_is_send() {
334        // The whole reason this type exists.
335        fn assert_send<T: Send>() {}
336        assert_send::<OwnedRow>();
337        assert_send::<Vec<OwnedRow>>();
338    }
339
340    #[test]
341    fn reads_back_every_cell_type() {
342        let row = OwnedRow::new()
343            .with("b", true)
344            .with("i", 7_i64)
345            .with("u", 9_u64)
346            .with("f", 1.5_f64)
347            .with("s", "text")
348            .with("bytes", vec![1_u8, 2]);
349
350        assert_eq!(row.get_bool("b").unwrap(), Some(true));
351        assert_eq!(row.get_i64("i").unwrap(), Some(7));
352        assert_eq!(row.get_u64("u").unwrap(), Some(9));
353        assert_eq!(row.get_f64("f").unwrap(), Some(1.5));
354        assert_eq!(row.get_string("s").unwrap().as_deref(), Some("text"));
355        assert_eq!(row.get_bytes("bytes").unwrap(), Some(vec![1, 2]));
356    }
357
358    #[test]
359    fn null_and_absent_both_read_as_none() {
360        let row = OwnedRow::new().with("nickname", None::<String>);
361        assert_eq!(row.get_string("nickname").unwrap(), None);
362        assert_eq!(row.get_string("never_selected").unwrap(), None);
363    }
364
365    #[test]
366    fn integers_widen_across_the_signedness_the_dialects_disagree_about() {
367        // SQLite and Postgres have no unsigned types, so an unsigned column
368        // arrives signed.
369        let row = OwnedRow::new().with("count", 5_i64);
370        assert_eq!(row.get_u64("count").unwrap(), Some(5));
371
372        let row = OwnedRow::new().with("count", 5_u64);
373        assert_eq!(row.get_i64("count").unwrap(), Some(5));
374    }
375
376    #[test]
377    fn booleans_read_from_the_integers_sqlite_stores() {
378        assert_eq!(OwnedRow::new().with("f", 0_i64).get_bool("f").unwrap(), Some(false));
379        assert_eq!(OwnedRow::new().with("f", 1_i64).get_bool("f").unwrap(), Some(true));
380    }
381
382    #[test]
383    fn a_type_mismatch_is_an_error_rather_than_a_silent_default() {
384        let row = OwnedRow::new().with("title", "not a number");
385        let err = row.get_i64("title").unwrap_err();
386        assert!(err.to_string().contains("title"), "{err}");
387    }
388
389    #[test]
390    fn timestamps_parse_from_the_text_sqlite_stores() {
391        let row = OwnedRow::new().with("at", "2026-07-25T10:30:00Z");
392        assert!(row.get_datetime("at").unwrap().is_some());
393
394        let row = OwnedRow::new().with("on", "2026-07-25");
395        assert_eq!(row.get_naive_date("on").unwrap(), NaiveDate::from_ymd_opt(2026, 7, 25));
396        assert!(OwnedRow::new().with("at", "not a date").get_datetime("at").is_err());
397    }
398
399    #[test]
400    fn column_requests_come_from_the_entity_metadata() {
401        let requested = ColumnRequest::for_entity::<Post>();
402        assert_eq!(requested.len(), 3);
403        assert_eq!(requested[0], ColumnRequest::new("id", ColumnType::BigUint));
404        assert_eq!(requested[1].name, "title");
405        assert_eq!(requested[2].ty, ColumnType::BigInt, "Option<i64> is still a BigInt column");
406    }
407
408    #[test]
409    fn a_snapshot_decodes_back_into_its_entity() {
410        // The round trip that matters: driver row → OwnedRow → Entity.
411        let driver_row =
412            OwnedRow::new().with("id", 7_u64).with("title", "Hello").with("views", None::<i64>);
413
414        let snapshot =
415            OwnedRow::snapshot(&driver_row, &ColumnRequest::for_entity::<Post>()).unwrap();
416        let post = Post::from_row(&snapshot).unwrap();
417
418        assert_eq!(post, Post { id: 7, title: "Hello".into(), views: None });
419    }
420
421    #[test]
422    fn a_snapshot_only_copies_what_was_requested() {
423        let driver_row = OwnedRow::new().with("id", 1_u64).with("secret", "hidden");
424        let snapshot =
425            OwnedRow::snapshot(&driver_row, &[ColumnRequest::new("id", ColumnType::BigUint)])
426                .unwrap();
427
428        assert_eq!(snapshot.len(), 1);
429        assert!(snapshot.cell("secret").is_none());
430    }
431
432    #[test]
433    fn snapshotting_a_missing_column_records_a_null_rather_than_failing() {
434        let driver_row = OwnedRow::new().with("id", 1_u64);
435        let snapshot =
436            OwnedRow::snapshot(&driver_row, &[ColumnRequest::new("absent", ColumnType::Text)])
437                .unwrap();
438
439        assert_eq!(snapshot.cell("absent"), Some(&Cell::Null));
440    }
441}