1use std::collections::HashMap;
25
26use prax_query::row::{RowError, RowRef};
27use sqlx::{Column, Row, TypeInfo};
28
29use crate::row::SqlxRow;
30
31enum Value {
34 Null,
35 Bool(bool),
36 I64(i64),
37 F64(f64),
38 Text(String),
39 Bytes(Vec<u8>),
40 DateTimeUtc(chrono::DateTime<chrono::Utc>),
42 NaiveDateTime(chrono::NaiveDateTime),
44 NaiveDate(chrono::NaiveDate),
45 NaiveTime(chrono::NaiveTime),
46 Uuid(uuid::Uuid),
47 Json(serde_json::Value),
48 Decimal(rust_decimal::Decimal),
49}
50
51pub struct SqlxRowRef {
55 values: HashMap<String, Value>,
56}
57
58impl SqlxRowRef {
59 pub fn from_sqlx(row: &SqlxRow) -> Result<Self, RowError> {
65 let mut values = HashMap::new();
66 match row {
67 #[cfg(feature = "postgres")]
68 SqlxRow::Postgres(r) => {
69 for (i, col) in r.columns().iter().enumerate() {
70 let name = col.name().to_string();
71 let v = decode_pg_cell(r, i)?;
72 values.insert(name, v);
73 }
74 }
75 #[cfg(feature = "mysql")]
76 SqlxRow::MySql(r) => {
77 for (i, col) in r.columns().iter().enumerate() {
78 let name = col.name().to_string();
79 let v = decode_mysql_cell(r, i)?;
80 values.insert(name, v);
81 }
82 }
83 #[cfg(feature = "sqlite")]
84 SqlxRow::Sqlite(r) => {
85 for (i, col) in r.columns().iter().enumerate() {
86 let name = col.name().to_string();
87 let v = decode_sqlite_cell(r, i)?;
88 values.insert(name, v);
89 }
90 }
91 }
92 Ok(Self { values })
93 }
94}
95
96fn tc(column: &str, msg: impl Into<String>) -> RowError {
97 RowError::TypeConversion {
98 column: column.into(),
99 message: msg.into(),
100 }
101}
102
103macro_rules! decode_cell {
111 ($row:expr, $idx:expr, $column:expr, $ty:ty, $wrap:expr) => {
112 match $row.try_get::<Option<$ty>, _>($idx) {
113 Ok(None) => Ok(Value::Null),
114 Ok(Some(v)) => Ok(($wrap)(v)),
115 Err(e) => Err(tc($column, e.to_string())),
116 }
117 };
118}
119
120#[cfg(feature = "postgres")]
123fn decode_pg_cell(r: &sqlx::postgres::PgRow, i: usize) -> Result<Value, RowError> {
124 let column = r.columns()[i].name();
125 match r.columns()[i].type_info().name() {
126 "TEXT" | "VARCHAR" | "CHAR" | "NAME" | "UNKNOWN" => {
129 decode_cell!(r, i, column, String, Value::Text)
130 }
131 "BOOL" => decode_cell!(r, i, column, bool, Value::Bool),
132 "INT2" => decode_cell!(r, i, column, i16, |v| Value::I64(v as i64)),
135 "INT4" => decode_cell!(r, i, column, i32, |v| Value::I64(v as i64)),
136 "INT8" => decode_cell!(r, i, column, i64, Value::I64),
137 "FLOAT4" => decode_cell!(r, i, column, f32, |v| Value::F64(v as f64)),
138 "FLOAT8" => decode_cell!(r, i, column, f64, Value::F64),
139 "NUMERIC" => decode_cell!(r, i, column, rust_decimal::Decimal, Value::Decimal),
140 "TIMESTAMPTZ" => {
141 decode_cell!(
142 r,
143 i,
144 column,
145 chrono::DateTime<chrono::Utc>,
146 Value::DateTimeUtc
147 )
148 }
149 "TIMESTAMP" => decode_cell!(r, i, column, chrono::NaiveDateTime, Value::NaiveDateTime),
150 "DATE" => decode_cell!(r, i, column, chrono::NaiveDate, Value::NaiveDate),
151 "TIME" => decode_cell!(r, i, column, chrono::NaiveTime, Value::NaiveTime),
152 "UUID" => decode_cell!(r, i, column, uuid::Uuid, Value::Uuid),
153 "JSON" | "JSONB" => decode_cell!(r, i, column, serde_json::Value, Value::Json),
154 "BYTEA" => decode_cell!(r, i, column, Vec<u8>, Value::Bytes),
155 other => {
156 match r.try_get::<Option<String>, _>(i) {
160 Ok(None) => Ok(Value::Null),
161 Ok(Some(s)) => Ok(Value::Text(s)),
162 Err(e) => Err(tc(
163 column,
164 format!("unsupported Postgres type {other}: {e}"),
165 )),
166 }
167 }
168 }
169}
170
171#[cfg(feature = "mysql")]
176fn decode_mysql_cell(r: &sqlx::mysql::MySqlRow, i: usize) -> Result<Value, RowError> {
177 let column = r.columns()[i].name();
178 let ty = r.columns()[i].type_info().name();
179 match ty {
180 "CHAR" | "VARCHAR" | "TINYTEXT" | "TEXT" | "MEDIUMTEXT" | "LONGTEXT" | "ENUM" => {
181 decode_cell!(r, i, column, String, Value::Text)
182 }
183 "BOOLEAN" => decode_cell!(r, i, column, bool, Value::Bool),
186 "TINYINT" | "SMALLINT" | "MEDIUMINT" | "INT" | "BIGINT" => {
190 decode_cell!(r, i, column, i64, Value::I64)
191 }
192 _ if ty.ends_with(" UNSIGNED") || ty == "YEAR" || ty == "BIT" => {
195 match r.try_get::<Option<u64>, _>(i) {
196 Ok(None) => Ok(Value::Null),
197 Ok(Some(v)) => i64::try_from(v)
198 .map(Value::I64)
199 .map_err(|_| tc(column, "u64 value overflows i64")),
200 Err(e) => Err(tc(column, e.to_string())),
201 }
202 }
203 "FLOAT" | "DOUBLE" => decode_cell!(r, i, column, f64, Value::F64),
204 "DECIMAL" => decode_cell!(r, i, column, rust_decimal::Decimal, Value::Decimal),
205 "TIMESTAMP" => {
208 decode_cell!(
209 r,
210 i,
211 column,
212 chrono::DateTime<chrono::Utc>,
213 Value::DateTimeUtc
214 )
215 }
216 "DATETIME" => decode_cell!(r, i, column, chrono::NaiveDateTime, Value::NaiveDateTime),
217 "DATE" => decode_cell!(r, i, column, chrono::NaiveDate, Value::NaiveDate),
218 "TIME" => decode_cell!(r, i, column, chrono::NaiveTime, Value::NaiveTime),
219 "JSON" => decode_cell!(r, i, column, serde_json::Value, Value::Json),
220 "BINARY" | "VARBINARY" | "TINYBLOB" | "BLOB" | "MEDIUMBLOB" | "LONGBLOB" => {
221 decode_cell!(r, i, column, Vec<u8>, Value::Bytes)
222 }
223 other => match r.try_get::<Option<String>, _>(i) {
224 Ok(None) => Ok(Value::Null),
225 Ok(Some(s)) => Ok(Value::Text(s)),
226 Err(e) => Err(tc(column, format!("unsupported MySQL type {other}: {e}"))),
227 },
228 }
229}
230
231#[cfg(feature = "sqlite")]
241fn decode_sqlite_cell(r: &sqlx::sqlite::SqliteRow, i: usize) -> Result<Value, RowError> {
242 let column = r.columns()[i].name();
243 match r.columns()[i].type_info().name() {
244 "TEXT" => decode_cell!(r, i, column, String, Value::Text),
245 "INTEGER" => decode_cell!(r, i, column, i64, Value::I64),
246 "REAL" => decode_cell!(r, i, column, f64, Value::F64),
247 "BLOB" => decode_cell!(r, i, column, Vec<u8>, Value::Bytes),
248 "BOOLEAN" => decode_cell!(r, i, column, bool, Value::Bool),
251 "DATETIME" => decode_cell!(r, i, column, chrono::NaiveDateTime, Value::NaiveDateTime),
252 "DATE" => decode_cell!(r, i, column, chrono::NaiveDate, Value::NaiveDate),
253 "TIME" => decode_cell!(r, i, column, chrono::NaiveTime, Value::NaiveTime),
254 _ => {
255 if r.try_get::<Option<Vec<u8>>, _>(i)
259 .is_ok_and(|v| v.is_none())
260 {
261 return Ok(Value::Null);
262 }
263 if let Ok(Some(s)) = r.try_get::<Option<String>, _>(i) {
264 return Ok(Value::Text(s));
265 }
266 if let Ok(Some(n)) = r.try_get::<Option<i64>, _>(i) {
267 return Ok(Value::I64(n));
268 }
269 if let Ok(Some(f)) = r.try_get::<Option<f64>, _>(i) {
270 return Ok(Value::F64(f));
271 }
272 if let Ok(Some(b)) = r.try_get::<Option<Vec<u8>>, _>(i) {
273 return Ok(Value::Bytes(b));
274 }
275 Err(tc(column, "unsupported SQLite value"))
276 }
277 }
278}
279
280impl RowRef for SqlxRowRef {
281 fn get_i32(&self, c: &str) -> Result<i32, RowError> {
282 match self
283 .values
284 .get(c)
285 .ok_or_else(|| RowError::ColumnNotFound(c.into()))?
286 {
287 Value::I64(i) => i32::try_from(*i).map_err(|_| tc(c, "i64 overflow")),
288 Value::Null => Err(RowError::UnexpectedNull(c.into())),
289 _ => Err(tc(c, "not an integer")),
290 }
291 }
292 fn get_i32_opt(&self, c: &str) -> Result<Option<i32>, RowError> {
293 match self.values.get(c) {
294 None => Err(RowError::ColumnNotFound(c.into())),
295 Some(Value::Null) => Ok(None),
296 Some(Value::I64(i)) => i32::try_from(*i)
297 .map(Some)
298 .map_err(|_| tc(c, "i64 overflow")),
299 Some(_) => Err(tc(c, "not an integer")),
300 }
301 }
302 fn get_i64(&self, c: &str) -> Result<i64, RowError> {
303 match self
304 .values
305 .get(c)
306 .ok_or_else(|| RowError::ColumnNotFound(c.into()))?
307 {
308 Value::I64(i) => Ok(*i),
309 Value::Null => Err(RowError::UnexpectedNull(c.into())),
310 _ => Err(tc(c, "not an integer")),
311 }
312 }
313 fn get_i64_opt(&self, c: &str) -> Result<Option<i64>, RowError> {
314 match self.values.get(c) {
315 None => Err(RowError::ColumnNotFound(c.into())),
316 Some(Value::Null) => Ok(None),
317 Some(Value::I64(i)) => Ok(Some(*i)),
318 Some(_) => Err(tc(c, "not an integer")),
319 }
320 }
321 fn get_f64(&self, c: &str) -> Result<f64, RowError> {
322 match self
323 .values
324 .get(c)
325 .ok_or_else(|| RowError::ColumnNotFound(c.into()))?
326 {
327 Value::F64(f) => Ok(*f),
328 Value::I64(i) => Ok(*i as f64),
329 Value::Null => Err(RowError::UnexpectedNull(c.into())),
330 _ => Err(tc(c, "not a number")),
331 }
332 }
333 fn get_f64_opt(&self, c: &str) -> Result<Option<f64>, RowError> {
334 match self.values.get(c) {
335 None => Err(RowError::ColumnNotFound(c.into())),
336 Some(Value::Null) => Ok(None),
337 Some(Value::F64(f)) => Ok(Some(*f)),
338 Some(Value::I64(i)) => Ok(Some(*i as f64)),
339 Some(_) => Err(tc(c, "not a number")),
340 }
341 }
342 fn get_bool(&self, c: &str) -> Result<bool, RowError> {
343 match self
344 .values
345 .get(c)
346 .ok_or_else(|| RowError::ColumnNotFound(c.into()))?
347 {
348 Value::Bool(b) => Ok(*b),
349 Value::I64(i) => Ok(*i != 0),
350 Value::Null => Err(RowError::UnexpectedNull(c.into())),
351 _ => Err(tc(c, "not a boolean")),
352 }
353 }
354 fn get_bool_opt(&self, c: &str) -> Result<Option<bool>, RowError> {
355 match self.values.get(c) {
356 None => Err(RowError::ColumnNotFound(c.into())),
357 Some(Value::Null) => Ok(None),
358 Some(Value::Bool(b)) => Ok(Some(*b)),
359 Some(Value::I64(i)) => Ok(Some(*i != 0)),
360 Some(_) => Err(tc(c, "not a boolean")),
361 }
362 }
363 fn get_str(&self, c: &str) -> Result<&str, RowError> {
364 match self
365 .values
366 .get(c)
367 .ok_or_else(|| RowError::ColumnNotFound(c.into()))?
368 {
369 Value::Text(s) => Ok(s.as_str()),
370 Value::Null => Err(RowError::UnexpectedNull(c.into())),
371 _ => Err(tc(c, "not text")),
372 }
373 }
374 fn get_str_opt(&self, c: &str) -> Result<Option<&str>, RowError> {
375 match self.values.get(c) {
376 None => Err(RowError::ColumnNotFound(c.into())),
377 Some(Value::Null) => Ok(None),
378 Some(Value::Text(s)) => Ok(Some(s.as_str())),
379 Some(_) => Err(tc(c, "not text")),
380 }
381 }
382 fn get_string(&self, c: &str) -> Result<String, RowError> {
389 match self
390 .values
391 .get(c)
392 .ok_or_else(|| RowError::ColumnNotFound(c.into()))?
393 {
394 Value::Text(s) => Ok(s.clone()),
395 Value::Uuid(u) => Ok(u.to_string()),
396 Value::Decimal(d) => Ok(d.to_string()),
397 Value::Json(j) => Ok(j.to_string()),
398 Value::DateTimeUtc(d) => Ok(d.to_rfc3339()),
399 Value::NaiveDateTime(d) => Ok(d.to_string()),
400 Value::NaiveDate(d) => Ok(d.to_string()),
401 Value::NaiveTime(t) => Ok(t.to_string()),
402 Value::Null => Err(RowError::UnexpectedNull(c.into())),
403 _ => Err(tc(c, "not text")),
404 }
405 }
406 fn get_string_opt(&self, c: &str) -> Result<Option<String>, RowError> {
407 match self.values.get(c) {
408 None => Err(RowError::ColumnNotFound(c.into())),
409 Some(Value::Null) => Ok(None),
410 Some(Value::Text(s)) => Ok(Some(s.clone())),
411 Some(Value::Uuid(u)) => Ok(Some(u.to_string())),
412 Some(Value::Decimal(d)) => Ok(Some(d.to_string())),
413 Some(Value::Json(j)) => Ok(Some(j.to_string())),
414 Some(Value::DateTimeUtc(d)) => Ok(Some(d.to_rfc3339())),
415 Some(Value::NaiveDateTime(d)) => Ok(Some(d.to_string())),
416 Some(Value::NaiveDate(d)) => Ok(Some(d.to_string())),
417 Some(Value::NaiveTime(t)) => Ok(Some(t.to_string())),
418 Some(_) => Err(tc(c, "not text")),
419 }
420 }
421 fn get_bytes(&self, c: &str) -> Result<&[u8], RowError> {
422 match self
423 .values
424 .get(c)
425 .ok_or_else(|| RowError::ColumnNotFound(c.into()))?
426 {
427 Value::Bytes(b) => Ok(b.as_slice()),
428 Value::Text(s) => Ok(s.as_bytes()),
429 Value::Null => Err(RowError::UnexpectedNull(c.into())),
430 _ => Err(tc(c, "not bytes")),
431 }
432 }
433 fn get_bytes_opt(&self, c: &str) -> Result<Option<&[u8]>, RowError> {
434 match self.values.get(c) {
435 None => Err(RowError::ColumnNotFound(c.into())),
436 Some(Value::Null) => Ok(None),
437 Some(Value::Bytes(b)) => Ok(Some(b.as_slice())),
438 Some(Value::Text(s)) => Ok(Some(s.as_bytes())),
439 Some(_) => Err(tc(c, "not bytes")),
440 }
441 }
442 fn get_datetime_utc(&self, c: &str) -> Result<chrono::DateTime<chrono::Utc>, RowError> {
443 match self
444 .values
445 .get(c)
446 .ok_or_else(|| RowError::ColumnNotFound(c.into()))?
447 {
448 Value::DateTimeUtc(d) => Ok(*d),
449 Value::NaiveDateTime(d) => Ok(d.and_utc()),
452 Value::Text(s) => chrono::DateTime::parse_from_rfc3339(s)
453 .map(|d| d.with_timezone(&chrono::Utc))
454 .map_err(|e| tc(c, e.to_string())),
455 Value::Null => Err(RowError::UnexpectedNull(c.into())),
456 _ => Err(tc(c, "not a datetime")),
457 }
458 }
459 fn get_datetime_utc_opt(
460 &self,
461 c: &str,
462 ) -> Result<Option<chrono::DateTime<chrono::Utc>>, RowError> {
463 match self.values.get(c) {
464 None => Err(RowError::ColumnNotFound(c.into())),
465 Some(Value::Null) => Ok(None),
466 Some(Value::DateTimeUtc(d)) => Ok(Some(*d)),
467 Some(Value::NaiveDateTime(d)) => Ok(Some(d.and_utc())),
468 Some(Value::Text(s)) => chrono::DateTime::parse_from_rfc3339(s)
469 .map(|d| Some(d.with_timezone(&chrono::Utc)))
470 .map_err(|e| tc(c, e.to_string())),
471 Some(_) => Err(tc(c, "not a datetime")),
472 }
473 }
474 fn get_naive_datetime(&self, c: &str) -> Result<chrono::NaiveDateTime, RowError> {
475 match self
476 .values
477 .get(c)
478 .ok_or_else(|| RowError::ColumnNotFound(c.into()))?
479 {
480 Value::NaiveDateTime(d) => Ok(*d),
481 Value::DateTimeUtc(d) => Ok(d.naive_utc()),
482 Value::Text(s) => chrono::DateTime::parse_from_rfc3339(s)
483 .map(|d| d.naive_utc())
484 .map_err(|e| tc(c, e.to_string())),
485 Value::Null => Err(RowError::UnexpectedNull(c.into())),
486 _ => Err(tc(c, "not a datetime")),
487 }
488 }
489 fn get_naive_datetime_opt(&self, c: &str) -> Result<Option<chrono::NaiveDateTime>, RowError> {
490 match self.values.get(c) {
491 None => Err(RowError::ColumnNotFound(c.into())),
492 Some(Value::Null) => Ok(None),
493 Some(Value::NaiveDateTime(d)) => Ok(Some(*d)),
494 Some(Value::DateTimeUtc(d)) => Ok(Some(d.naive_utc())),
495 Some(Value::Text(s)) => chrono::DateTime::parse_from_rfc3339(s)
496 .map(|d| Some(d.naive_utc()))
497 .map_err(|e| tc(c, e.to_string())),
498 Some(_) => Err(tc(c, "not a datetime")),
499 }
500 }
501 fn get_naive_date(&self, c: &str) -> Result<chrono::NaiveDate, RowError> {
502 match self
503 .values
504 .get(c)
505 .ok_or_else(|| RowError::ColumnNotFound(c.into()))?
506 {
507 Value::NaiveDate(d) => Ok(*d),
508 Value::Text(s) => s
509 .parse::<chrono::NaiveDate>()
510 .map_err(|e: chrono::ParseError| tc(c, e.to_string())),
511 Value::Null => Err(RowError::UnexpectedNull(c.into())),
512 _ => Err(tc(c, "not a date")),
513 }
514 }
515 fn get_naive_date_opt(&self, c: &str) -> Result<Option<chrono::NaiveDate>, RowError> {
516 match self.values.get(c) {
517 None => Err(RowError::ColumnNotFound(c.into())),
518 Some(Value::Null) => Ok(None),
519 Some(Value::NaiveDate(d)) => Ok(Some(*d)),
520 Some(Value::Text(s)) => s
521 .parse::<chrono::NaiveDate>()
522 .map(Some)
523 .map_err(|e: chrono::ParseError| tc(c, e.to_string())),
524 Some(_) => Err(tc(c, "not a date")),
525 }
526 }
527 fn get_naive_time(&self, c: &str) -> Result<chrono::NaiveTime, RowError> {
528 match self
529 .values
530 .get(c)
531 .ok_or_else(|| RowError::ColumnNotFound(c.into()))?
532 {
533 Value::NaiveTime(t) => Ok(*t),
534 Value::Text(s) => s
535 .parse::<chrono::NaiveTime>()
536 .map_err(|e: chrono::ParseError| tc(c, e.to_string())),
537 Value::Null => Err(RowError::UnexpectedNull(c.into())),
538 _ => Err(tc(c, "not a time")),
539 }
540 }
541 fn get_naive_time_opt(&self, c: &str) -> Result<Option<chrono::NaiveTime>, RowError> {
542 match self.values.get(c) {
543 None => Err(RowError::ColumnNotFound(c.into())),
544 Some(Value::Null) => Ok(None),
545 Some(Value::NaiveTime(t)) => Ok(Some(*t)),
546 Some(Value::Text(s)) => s
547 .parse::<chrono::NaiveTime>()
548 .map(Some)
549 .map_err(|e: chrono::ParseError| tc(c, e.to_string())),
550 Some(_) => Err(tc(c, "not a time")),
551 }
552 }
553 fn get_uuid(&self, c: &str) -> Result<uuid::Uuid, RowError> {
554 match self
555 .values
556 .get(c)
557 .ok_or_else(|| RowError::ColumnNotFound(c.into()))?
558 {
559 Value::Uuid(u) => Ok(*u),
560 Value::Text(s) => uuid::Uuid::parse_str(s).map_err(|e| tc(c, e.to_string())),
561 Value::Null => Err(RowError::UnexpectedNull(c.into())),
562 _ => Err(tc(c, "not a uuid")),
563 }
564 }
565 fn get_uuid_opt(&self, c: &str) -> Result<Option<uuid::Uuid>, RowError> {
566 match self.values.get(c) {
567 None => Err(RowError::ColumnNotFound(c.into())),
568 Some(Value::Null) => Ok(None),
569 Some(Value::Uuid(u)) => Ok(Some(*u)),
570 Some(Value::Text(s)) => uuid::Uuid::parse_str(s)
571 .map(Some)
572 .map_err(|e| tc(c, e.to_string())),
573 Some(_) => Err(tc(c, "not a uuid")),
574 }
575 }
576 fn get_json(&self, c: &str) -> Result<serde_json::Value, RowError> {
577 match self
578 .values
579 .get(c)
580 .ok_or_else(|| RowError::ColumnNotFound(c.into()))?
581 {
582 Value::Json(j) => Ok(j.clone()),
583 Value::Text(s) => serde_json::from_str(s).map_err(|e| tc(c, e.to_string())),
584 Value::Null => Err(RowError::UnexpectedNull(c.into())),
585 _ => Err(tc(c, "not json")),
586 }
587 }
588 fn get_json_opt(&self, c: &str) -> Result<Option<serde_json::Value>, RowError> {
589 match self.values.get(c) {
590 None => Err(RowError::ColumnNotFound(c.into())),
591 Some(Value::Null) => Ok(None),
592 Some(Value::Json(j)) => Ok(Some(j.clone())),
593 Some(Value::Text(s)) => serde_json::from_str(s)
594 .map(Some)
595 .map_err(|e| tc(c, e.to_string())),
596 Some(_) => Err(tc(c, "not json")),
597 }
598 }
599 fn get_decimal(&self, c: &str) -> Result<rust_decimal::Decimal, RowError> {
600 match self
601 .values
602 .get(c)
603 .ok_or_else(|| RowError::ColumnNotFound(c.into()))?
604 {
605 Value::Decimal(d) => Ok(*d),
606 Value::I64(i) => Ok(rust_decimal::Decimal::from(*i)),
607 Value::Text(s) => s
608 .parse::<rust_decimal::Decimal>()
609 .map_err(|e: rust_decimal::Error| tc(c, e.to_string())),
610 Value::Null => Err(RowError::UnexpectedNull(c.into())),
611 _ => Err(tc(c, "not a decimal")),
612 }
613 }
614 fn get_decimal_opt(&self, c: &str) -> Result<Option<rust_decimal::Decimal>, RowError> {
615 match self.values.get(c) {
616 None => Err(RowError::ColumnNotFound(c.into())),
617 Some(Value::Null) => Ok(None),
618 Some(Value::Decimal(d)) => Ok(Some(*d)),
619 Some(Value::I64(i)) => Ok(Some(rust_decimal::Decimal::from(*i))),
620 Some(Value::Text(s)) => s
621 .parse::<rust_decimal::Decimal>()
622 .map(Some)
623 .map_err(|e: rust_decimal::Error| tc(c, e.to_string())),
624 Some(_) => Err(tc(c, "not a decimal")),
625 }
626 }
627 fn is_null(&self, c: &str) -> Result<bool, RowError> {
635 match self.values.get(c) {
636 None => Err(RowError::ColumnNotFound(c.into())),
637 Some(v) => Ok(matches!(v, Value::Null)),
638 }
639 }
640}
641
642#[cfg(test)]
643mod tests {
644 use super::*;
645 use prax_query::row::FromColumn;
646
647 fn row(pairs: Vec<(&str, Value)>) -> SqlxRowRef {
648 SqlxRowRef {
649 values: pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect(),
650 }
651 }
652
653 fn naive_datetime() -> chrono::NaiveDateTime {
654 chrono::NaiveDate::from_ymd_opt(2024, 1, 2)
655 .unwrap()
656 .and_hms_opt(3, 4, 5)
657 .unwrap()
658 }
659
660 #[test]
661 fn is_null_reports_null_variant_as_null() {
662 let r = row(vec![("a", Value::Null)]);
663 assert!(r.is_null("a").unwrap());
664 }
665
666 #[test]
667 fn is_null_reports_every_non_null_variant_as_not_null() {
668 let r = row(vec![
669 ("bool", Value::Bool(true)),
670 ("i64", Value::I64(5)),
671 ("f64", Value::F64(1.5)),
672 ("text", Value::Text("hello".into())),
673 ("bytes", Value::Bytes(vec![1, 2, 3])),
674 (
675 "dt_utc",
676 Value::DateTimeUtc(chrono::DateTime::from_timestamp(1_700_000_000, 0).unwrap()),
677 ),
678 ("ndt", Value::NaiveDateTime(naive_datetime())),
679 (
680 "nd",
681 Value::NaiveDate(chrono::NaiveDate::from_ymd_opt(2024, 1, 2).unwrap()),
682 ),
683 (
684 "nt",
685 Value::NaiveTime(chrono::NaiveTime::from_hms_opt(3, 4, 5).unwrap()),
686 ),
687 ("uuid", Value::Uuid(uuid::Uuid::nil())),
688 ("json", Value::Json(serde_json::json!({"a": 1}))),
689 ("decimal", Value::Decimal(rust_decimal::Decimal::new(42, 1))),
690 ]);
691 for col in [
692 "bool", "i64", "f64", "text", "bytes", "dt_utc", "ndt", "nd", "nt", "uuid", "json",
693 "decimal",
694 ] {
695 assert!(!r.is_null(col).unwrap(), "{col} should not be null");
696 }
697 }
698
699 #[test]
700 fn is_null_missing_column_is_column_not_found() {
701 let r = row(vec![]);
702 assert!(matches!(
703 r.is_null("missing"),
704 Err(RowError::ColumnNotFound(_))
705 ));
706 }
707
708 #[test]
713 fn option_from_column_decodes_non_text_cells() {
714 let r = row(vec![
715 ("n", Value::I64(5)),
716 ("f", Value::F64(2.5)),
717 ("b", Value::Bool(true)),
718 ("null_i", Value::Null),
719 ]);
720 assert_eq!(Option::<i32>::from_column(&r, "n").unwrap(), Some(5));
721 assert_eq!(Option::<f64>::from_column(&r, "f").unwrap(), Some(2.5));
722 assert_eq!(Option::<bool>::from_column(&r, "b").unwrap(), Some(true));
723 assert_eq!(Option::<i32>::from_column(&r, "null_i").unwrap(), None);
724 }
725
726 #[test]
727 fn typed_variants_are_readable_through_their_getters() {
728 let uuid = uuid::Uuid::nil();
729 let ndt = naive_datetime();
730 let r = row(vec![
731 ("u", Value::Uuid(uuid)),
732 ("d", Value::Decimal(rust_decimal::Decimal::new(42, 1))),
733 ("j", Value::Json(serde_json::json!([1, 2]))),
734 ("t", Value::NaiveDateTime(ndt)),
735 ]);
736 assert_eq!(r.get_uuid("u").unwrap(), uuid);
737 assert_eq!(r.get_string("u").unwrap(), uuid.to_string());
740 assert_eq!(
741 r.get_decimal("d").unwrap(),
742 rust_decimal::Decimal::new(42, 1)
743 );
744 assert_eq!(r.get_json("j").unwrap(), serde_json::json!([1, 2]));
745 assert_eq!(r.get_naive_datetime("t").unwrap(), ndt);
746 assert_eq!(r.get_datetime_utc("t").unwrap(), ndt.and_utc());
748 }
749
750 }