1use std::fmt;
36
37use crate::value::Value;
38
39pub trait Queryable: Sized {
43 fn decode(value: &Value) -> Result<Self, DecodeError>;
44}
45
46#[derive(Debug, Clone, PartialEq)]
48pub struct DecodeError {
49 type_root: Option<String>,
54 path: Vec<String>,
57 kind: DecodeErrorKind,
58}
59
60#[derive(Debug, Clone, PartialEq)]
61pub enum DecodeErrorKind {
62 MissingField(String),
65 WrongType {
66 expected: &'static str,
67 actual: &'static str,
68 },
69 UnknownVariant(String),
71 OutOfRange { value: i64, target: &'static str },
74 Json(String),
76 Invalid(String),
79}
80
81impl DecodeError {
82 fn new(kind: DecodeErrorKind) -> Self {
83 Self {
84 type_root: None,
85 path: Vec::new(),
86 kind,
87 }
88 }
89
90 fn under(mut self, segment: impl Into<String>) -> Self {
93 self.path.insert(0, segment.into());
94 self
95 }
96
97 fn rooted_at(mut self, type_name: &str) -> Self {
101 self.type_root = Some(type_name.to_string());
102 self
103 }
104
105 pub fn kind(&self) -> &DecodeErrorKind {
106 &self.kind
107 }
108
109 pub fn custom(message: impl Into<String>) -> Self {
115 Self::new(DecodeErrorKind::Invalid(message.into()))
116 }
117
118 fn wrong_type(expected: &'static str, actual: &Value) -> Self {
119 Self::new(DecodeErrorKind::WrongType {
120 expected,
121 actual: type_name(actual),
122 })
123 }
124}
125
126impl fmt::Display for DecodeError {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 let mut location: Vec<&str> = Vec::with_capacity(self.path.len() + 1);
129 location.extend(self.type_root.as_deref());
130 location.extend(self.path.iter().map(String::as_str));
131 if location.is_empty() {
132 write!(f, "cannot decode query result: {}", self.kind)
133 } else {
134 write!(f, "cannot decode {}: {}", location.join("."), self.kind)
135 }
136 }
137}
138
139impl std::error::Error for DecodeError {}
140
141impl fmt::Display for DecodeErrorKind {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 match self {
144 Self::MissingField(name) => write!(f, "the query's shape has no '{name}'"),
145 Self::WrongType { expected, actual } => write!(f, "expected {expected}, got {actual}"),
146 Self::UnknownVariant(label) => write!(f, "'{label}' is not a known variant"),
147 Self::OutOfRange { value, target } => write!(f, "{value} is out of range for {target}"),
148 Self::Json(message) => write!(f, "{message}"),
149 Self::Invalid(message) => write!(f, "{message}"),
150 }
151 }
152}
153
154fn type_name(value: &Value) -> &'static str {
156 match value {
157 Value::Null => "an empty set",
158 Value::Bool(_) => "a bool",
159 Value::Int64(_) => "an integer",
160 Value::Float64(_) => "a float",
161 Value::Str(_) => "a string",
162 Value::Bytes(_) => "bytes",
163 Value::Uuid(_) => "a uuid",
164 Value::Decimal(_) => "a decimal",
165 Value::Duration { .. } => "a duration",
166 Value::Date(_) => "a date",
167 Value::Time(_) => "a time",
168 Value::Timestamp(_) => "a local datetime",
169 Value::Timestamptz(_) => "a datetime",
170 Value::Range(_) => "a range",
171 Value::Array(_) => "a set",
172 Value::Tuple(_) => "a tuple",
173 Value::Object(_) => "an object",
174 Value::Enum { .. } => "an enum value",
175 Value::Group(_) => "a group",
176 Value::VectorSearch { .. } => "a vector search result",
177 Value::FtsSearch { .. } => "an FTS search result",
178 }
179}
180
181impl Queryable for Value {
185 fn decode(value: &Value) -> Result<Self, DecodeError> {
186 Ok(value.clone())
187 }
188}
189
190impl<T: Queryable> Queryable for Option<T> {
192 fn decode(value: &Value) -> Result<Self, DecodeError> {
193 match value {
194 Value::Null => Ok(None),
195 other => T::decode(other).map(Some),
196 }
197 }
198}
199
200impl<T: Queryable> Queryable for Vec<T> {
205 fn decode(value: &Value) -> Result<Self, DecodeError> {
206 match value {
207 Value::Array(items) => items
208 .iter()
209 .enumerate()
210 .map(|(index, item)| T::decode(item).map_err(|error| error.under(index.to_string())))
211 .collect(),
212 Value::Null => Ok(Vec::new()),
213 single => T::decode(single).map(|decoded| vec![decoded]),
214 }
215 }
216}
217
218impl Queryable for bool {
219 fn decode(value: &Value) -> Result<Self, DecodeError> {
220 match value {
221 Value::Bool(b) => Ok(*b),
222 other => Err(DecodeError::wrong_type("a bool", other)),
223 }
224 }
225}
226
227impl Queryable for String {
228 fn decode(value: &Value) -> Result<Self, DecodeError> {
229 match value {
230 Value::Str(s) => Ok(s.clone()),
231 Value::Enum { value, .. } => Ok(value.clone()),
236 Value::Decimal(s) => Ok(s.clone()),
237 other => Err(DecodeError::wrong_type("a string", other)),
238 }
239 }
240}
241
242impl Queryable for Vec<u8> {
243 fn decode(value: &Value) -> Result<Self, DecodeError> {
244 match value {
245 Value::Bytes(b) => Ok(b.clone()),
246 other => Err(DecodeError::wrong_type("bytes", other)),
247 }
248 }
249}
250
251impl Queryable for uuid::Uuid {
252 fn decode(value: &Value) -> Result<Self, DecodeError> {
253 match value {
254 Value::Uuid(u) => Ok(*u),
255 other => Err(DecodeError::wrong_type("a uuid", other)),
256 }
257 }
258}
259
260impl Queryable for i64 {
261 fn decode(value: &Value) -> Result<Self, DecodeError> {
262 match value {
263 Value::Int64(n) => Ok(*n),
264 other => Err(DecodeError::wrong_type("an integer", other)),
265 }
266 }
267}
268
269macro_rules! queryable_narrow_int {
273 ($($target:ty),* $(,)?) => {
274 $(
275 impl Queryable for $target {
276 fn decode(value: &Value) -> Result<Self, DecodeError> {
277 match value {
278 Value::Int64(n) => <$target>::try_from(*n).map_err(|_| {
279 DecodeError::new(DecodeErrorKind::OutOfRange {
280 value: *n,
281 target: stringify!($target),
282 })
283 }),
284 other => Err(DecodeError::wrong_type("an integer", other)),
285 }
286 }
287 }
288 )*
289 };
290}
291
292queryable_narrow_int!(i16, i32, u16, u32, u64);
293
294impl Queryable for f64 {
295 fn decode(value: &Value) -> Result<Self, DecodeError> {
296 match value {
297 Value::Float64(f) => Ok(*f),
298 Value::Int64(n) => Ok(*n as f64),
302 other => Err(DecodeError::wrong_type("a float", other)),
303 }
304 }
305}
306
307impl Queryable for f32 {
308 fn decode(value: &Value) -> Result<Self, DecodeError> {
309 f64::decode(value).map(|f| f as f32)
310 }
311}
312
313impl Queryable for chrono::DateTime<chrono::Utc> {
314 fn decode(value: &Value) -> Result<Self, DecodeError> {
315 match value {
316 Value::Timestamptz(micros) => from_pg_micros(*micros).map(|naive| naive.and_utc()),
317 other => Err(DecodeError::wrong_type("a datetime", other)),
318 }
319 }
320}
321
322impl Queryable for chrono::NaiveDateTime {
323 fn decode(value: &Value) -> Result<Self, DecodeError> {
324 match value {
325 Value::Timestamp(micros) => from_pg_micros(*micros),
326 other => Err(DecodeError::wrong_type("a local datetime", other)),
327 }
328 }
329}
330
331impl Queryable for chrono::NaiveDate {
332 fn decode(value: &Value) -> Result<Self, DecodeError> {
333 match value {
334 Value::Date(days) => pg_epoch()
335 .checked_add_signed(chrono::Duration::days(i64::from(*days)))
336 .ok_or_else(|| {
337 DecodeError::new(DecodeErrorKind::Invalid(format!(
338 "date {days} days from 2000-01-01 is outside the supported range"
339 )))
340 }),
341 other => Err(DecodeError::wrong_type("a date", other)),
342 }
343 }
344}
345
346impl Queryable for chrono::NaiveTime {
347 fn decode(value: &Value) -> Result<Self, DecodeError> {
348 match value {
349 Value::Time(micros) => chrono::NaiveTime::from_hms_opt(0, 0, 0)
350 .and_then(|midnight| {
351 midnight
352 .overflowing_add_signed(chrono::Duration::microseconds(*micros))
353 .0
354 .into()
355 })
356 .ok_or_else(|| {
357 DecodeError::new(DecodeErrorKind::Invalid(format!(
358 "time {micros}\u{b5}s after midnight is not a valid time of day"
359 )))
360 }),
361 other => Err(DecodeError::wrong_type("a time", other)),
362 }
363 }
364}
365
366fn pg_epoch() -> chrono::NaiveDate {
367 chrono::NaiveDate::from_ymd_opt(2000, 1, 1).expect("2000-01-01 is a valid date")
369}
370
371fn from_pg_micros(micros: i64) -> Result<chrono::NaiveDateTime, DecodeError> {
372 pg_epoch()
373 .and_hms_opt(0, 0, 0)
374 .and_then(|midnight| midnight.checked_add_signed(chrono::Duration::microseconds(micros)))
375 .ok_or_else(|| {
376 DecodeError::new(DecodeErrorKind::Invalid(format!(
377 "timestamp {micros}\u{b5}s from 2000-01-01 is outside the supported range"
378 )))
379 })
380}
381
382#[doc(hidden)]
386pub mod derive {
387 use super::{DecodeError, DecodeErrorKind, Queryable};
388 use crate::value::{Object, Value};
389
390 pub fn object<'v>(value: &'v Value, container: &'static str) -> Result<&'v Object, DecodeError> {
391 match value {
392 Value::Object(object) => Ok(object),
393 other => Err(DecodeError::wrong_type("an object", other).rooted_at(container)),
394 }
395 }
396
397 fn missing(container: &'static str, name: &str) -> DecodeError {
401 DecodeError::new(DecodeErrorKind::MissingField(name.to_string())).rooted_at(container)
402 }
403
404 pub fn field<T: Queryable>(object: &Object, container: &'static str, name: &str) -> Result<T, DecodeError> {
405 let value = object.get(name).ok_or_else(|| missing(container, name))?;
406 T::decode(value).map_err(|error| error.under(name).rooted_at(container))
407 }
408
409 pub fn json_field<T: serde::de::DeserializeOwned>(
410 object: &Object,
411 container: &'static str,
412 name: &str,
413 ) -> Result<T, DecodeError> {
414 let value = object.get(name).ok_or_else(|| missing(container, name))?;
415 from_json_value(value).map_err(|error| error.under(name).rooted_at(container))
416 }
417
418 pub fn from_json<T: serde::de::DeserializeOwned>(value: &Value, container: &'static str) -> Result<T, DecodeError> {
419 from_json_value(value).map_err(|error| error.rooted_at(container))
420 }
421
422 fn from_json_value<T: serde::de::DeserializeOwned>(value: &Value) -> Result<T, DecodeError> {
430 serde_json::from_str(&crate::json::to_json(value))
431 .map_err(|error| DecodeError::new(DecodeErrorKind::Json(error.to_string())))
432 }
433
434 pub fn enum_label<'v>(value: &'v Value, container: &'static str) -> Result<&'v str, DecodeError> {
437 match value {
438 Value::Enum { value, .. } => Ok(value.as_str()),
439 Value::Str(label) => Ok(label.as_str()),
440 other => Err(DecodeError::wrong_type("an enum value", other).rooted_at(container)),
441 }
442 }
443
444 pub fn unknown_variant(container: &'static str, label: &str) -> DecodeError {
445 DecodeError::new(DecodeErrorKind::UnknownVariant(label.to_string())).rooted_at(container)
446 }
447}
448
449pub(crate) fn decode_rows<R: Queryable>(values: Vec<Value>) -> crate::Result<Vec<R>> {
453 values
454 .iter()
455 .map(|value| R::decode(value).map_err(crate::Error::from))
456 .collect()
457}
458
459pub(crate) fn decode_optional_row<R: Queryable>(value: Option<Value>) -> crate::Result<Option<R>> {
460 value.as_ref().map(R::decode).transpose().map_err(crate::Error::from)
461}
462
463pub(crate) fn decode_row<R: Queryable>(value: Value) -> crate::Result<R> {
464 R::decode(&value).map_err(crate::Error::from)
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470 use crate::value::Object;
471 use crate::{QueryArgs, named_args};
472
473 #[derive(Debug, PartialEq, crate::Queryable)]
477 #[pylon(crate_path = crate)]
478 struct Row {
479 id: uuid::Uuid,
480 attempt: i32,
481 last_error: Option<String>,
482 created_at: chrono::DateTime<chrono::Utc>,
483 }
484
485 #[derive(Debug, PartialEq, crate::Queryable)]
486 #[pylon(crate_path = crate)]
487 enum WebhookEvent {
488 #[pylon(rename = "contact.created")]
489 ContactCreated,
490 #[pylon(rename = "contact.updated")]
491 ContactUpdated,
492 Other,
493 }
494
495 #[derive(Debug, PartialEq, crate::Queryable)]
496 #[pylon(crate_path = crate)]
497 struct Nested {
498 latest_version: Option<Inner>,
499 #[pylon(rename = "type")]
500 kind: WebhookEvent,
501 }
502
503 #[derive(Debug, PartialEq, crate::Queryable)]
504 #[pylon(crate_path = crate)]
505 struct Inner {
506 runner: String,
507 }
508
509 fn object(fields: Vec<(&str, Value)>) -> Value {
510 Value::Object(Object {
511 type_name: Some("test::Row".to_string()),
512 fields: fields.into_iter().map(|(n, v)| (n.to_string(), v)).collect(),
513 implicit_id: false,
514 })
515 }
516
517 #[test]
518 fn decodes_a_row_struct() {
519 let id = uuid::Uuid::from_u128(7);
520 let row = Row::decode(&object(vec![
521 ("id", Value::Uuid(id)),
522 ("attempt", Value::Int64(3)),
523 ("last_error", Value::Null),
524 ("created_at", Value::Timestamptz(0)),
525 ]))
526 .unwrap();
527 assert_eq!(row.id, id);
528 assert_eq!(row.attempt, 3);
529 assert_eq!(row.last_error, None);
530 assert_eq!(row.created_at.to_rfc3339(), "2000-01-01T00:00:00+00:00");
531 }
532
533 #[test]
537 fn field_order_does_not_matter() {
538 let id = uuid::Uuid::from_u128(1);
539 let row = Row::decode(&object(vec![
540 ("created_at", Value::Timestamptz(0)),
541 ("last_error", Value::Str("boom".into())),
542 ("attempt", Value::Int64(1)),
543 ("id", Value::Uuid(id)),
544 ]))
545 .unwrap();
546 assert_eq!(row.last_error.as_deref(), Some("boom"));
547 assert_eq!(row.id, id);
548 }
549
550 #[test]
553 fn a_field_the_shape_omitted_is_an_error() {
554 let error = Row::decode(&object(vec![
555 ("id", Value::Uuid(uuid::Uuid::nil())),
556 ("attempt", Value::Int64(1)),
557 ("created_at", Value::Timestamptz(0)),
558 ]))
559 .unwrap_err();
560 assert_eq!(error.kind(), &DecodeErrorKind::MissingField("last_error".to_string()));
561 assert_eq!(
562 error.to_string(),
563 "cannot decode Row: the query's shape has no 'last_error'"
564 );
565 }
566
567 #[test]
568 fn nested_failures_name_their_whole_path() {
569 let error = Nested::decode(&object(vec![
570 ("latest_version", object(vec![("runner", Value::Int64(4))])),
571 ("type", Value::Str("contact.created".into())),
572 ]))
573 .unwrap_err();
574 assert_eq!(
575 error.to_string(),
576 "cannot decode Nested.latest_version.runner: expected a string, got an integer"
577 );
578 }
579
580 #[test]
581 fn decodes_a_renamed_enum_from_either_a_real_enum_or_a_str_cast() {
582 let from_enum = WebhookEvent::decode(&Value::Enum {
583 type_name: "integration::WebhookEvent".to_string(),
584 value: "contact.updated".to_string(),
585 })
586 .unwrap();
587 assert_eq!(from_enum, WebhookEvent::ContactUpdated);
588 let from_cast = WebhookEvent::decode(&Value::Str("contact.created".into())).unwrap();
591 assert_eq!(from_cast, WebhookEvent::ContactCreated);
592 assert_eq!(
594 WebhookEvent::decode(&Value::Str("Other".into())).unwrap(),
595 WebhookEvent::Other
596 );
597 }
598
599 #[test]
600 fn an_unknown_enum_label_names_the_label_it_saw() {
601 let error = WebhookEvent::decode(&Value::Str("contact.merged".into())).unwrap_err();
602 assert_eq!(
603 error.to_string(),
604 "cannot decode WebhookEvent: 'contact.merged' is not a known variant"
605 );
606 }
607
608 #[test]
611 fn decodes_doubly_optional_nesting() {
612 let row = Nested::decode(&object(vec![
613 ("latest_version", Value::Null),
614 ("type", Value::Str("Other".into())),
615 ]))
616 .unwrap();
617 assert_eq!(row.latest_version, None);
618 }
619
620 #[test]
623 fn a_custom_decode_failure_converts_into_the_crate_error() {
624 let error: crate::Error = DecodeError::custom("failed to decode query result JSON: eof").into();
625 assert_eq!(
626 error.to_string(),
627 "cannot decode query result: failed to decode query result JSON: eof"
628 );
629 }
630
631 #[test]
632 fn narrow_integers_range_check_instead_of_wrapping() {
633 assert_eq!(i32::decode(&Value::Int64(-5)).unwrap(), -5);
634 let error = i32::decode(&Value::Int64(i64::from(i32::MAX) + 1)).unwrap_err();
635 assert_eq!(
636 error.kind(),
637 &DecodeErrorKind::OutOfRange {
638 value: 2_147_483_648,
639 target: "i32"
640 }
641 );
642 }
643
644 #[test]
645 fn a_multi_pointer_decodes_into_a_vec() {
646 let rows: Vec<Inner> = Vec::decode(&Value::Array(vec![
647 object(vec![("runner", Value::Str("lambda".into()))]),
648 object(vec![("runner", Value::Str("firecracker".into()))]),
649 ]))
650 .unwrap();
651 assert_eq!(rows.len(), 2);
652 assert_eq!(rows[1].runner, "firecracker");
653 assert_eq!(Vec::<Inner>::decode(&Value::Null).unwrap(), vec![]);
655 }
656
657 #[test]
658 fn a_json_field_deserializes_from_a_natively_decoded_document() {
659 #[derive(Debug, PartialEq, crate::Queryable)]
660 #[pylon(crate_path = crate)]
661 struct WithJson {
662 #[pylon(json)]
663 payload: Option<serde_json::Value>,
664 }
665 let row = WithJson::decode(&object(vec![(
668 "payload",
669 Value::Object(Object {
670 type_name: None,
671 fields: vec![("email".to_string(), Value::Str("a@b.test".into()))],
672 implicit_id: false,
673 }),
674 )]))
675 .unwrap();
676 assert_eq!(row.payload.unwrap()["email"], "a@b.test");
677 }
678
679 #[test]
682 fn a_duration_binds_as_a_fixed_microsecond_interval() {
683 let bound = crate::QueryArg::to_decoded(&std::time::Duration::from_secs(90));
684 assert_eq!(
685 bound,
686 pylon_value::DecodedValue::Interval {
687 months: 0,
688 days: 0,
689 microseconds: 90_000_000
690 }
691 );
692 }
693
694 #[test]
695 fn positional_arguments_bind_by_index() {
696 let id = uuid::Uuid::from_u128(9);
697 let args = (id, "urgent", 4i32);
701 let params = args.to_params();
702 assert_eq!(params[0].0, "0");
703 assert_eq!(params[1], ("1", pylon_value::DecodedValue::Str("urgent".into())));
704 assert_eq!(params[2], ("2", pylon_value::DecodedValue::I64(4)));
705 assert!(().to_params().is_empty());
706 }
707
708 #[test]
709 fn named_arguments_accept_mixed_types_including_absent_optionals() {
710 let args = named_args! {
711 "id" => uuid::Uuid::nil(),
712 "width" => Option::<i32>::None,
713 "labels" => vec!["a".to_string()],
714 };
715 let params: std::collections::HashMap<_, _> = args.to_params().into_iter().collect();
716 assert_eq!(params["width"], pylon_value::DecodedValue::Null);
717 assert_eq!(
718 params["labels"],
719 pylon_value::DecodedValue::Array(vec![pylon_value::DecodedValue::Str("a".into())])
720 );
721 assert_eq!(params["id"], pylon_value::DecodedValue::Uuid([0; 16]));
722 }
723
724 #[test]
727 fn datetimes_round_trip_through_the_pg_epoch() {
728 let when = chrono::DateTime::parse_from_rfc3339("2026-09-24T12:34:56Z")
729 .unwrap()
730 .with_timezone(&chrono::Utc);
731 let bound = crate::QueryArg::to_decoded(&when);
732 let pylon_value::DecodedValue::Timestamptz(micros) = bound else {
733 panic!("a datetime must bind as a timestamptz, got {bound:?}");
734 };
735 assert_eq!(
736 chrono::DateTime::<chrono::Utc>::decode(&Value::Timestamptz(micros)).unwrap(),
737 when
738 );
739 }
740}