1use std::collections::HashMap;
19
20use chrono::{DateTime, NaiveDate, Utc};
21use rainier_orm::{ColumnType, Row};
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct ColumnRequest {
26 pub name: String,
28 pub ty: ColumnType,
30}
31
32impl ColumnRequest {
33 pub fn new(name: impl Into<String>, ty: ColumnType) -> Self {
35 Self { name: name.into(), ty }
36 }
37
38 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#[derive(Debug, Clone, PartialEq)]
47pub enum Cell {
48 Null,
50 Bool(bool),
52 Int(i64),
54 Uint(u64),
56 Float(f64),
58 Text(String),
60 Bytes(Vec<u8>),
62 Timestamp(DateTime<Utc>),
64 Date(NaiveDate),
66}
67
68impl Cell {
69 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 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#[derive(Debug, Clone, Default, PartialEq)]
147pub struct OwnedRow {
148 columns: HashMap<String, Cell>,
149}
150
151impl OwnedRow {
152 pub fn new() -> Self {
154 Self::default()
155 }
156
157 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 pub fn set(&mut self, name: impl Into<String>, value: impl Into<Cell>) {
165 self.columns.insert(name.into(), value.into());
166 }
167
168 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 pub fn is_empty(&self) -> bool {
196 self.columns.is_empty()
197 }
198
199 pub fn len(&self) -> usize {
201 self.columns.len()
202 }
203
204 pub fn cell(&self, name: &str) -> Option<&Cell> {
206 self.columns.get(name)
207 }
208
209 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 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 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 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 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 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 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}