1use std::collections::{BTreeMap, HashMap};
2use std::fmt;
3use std::hash::BuildHasher;
4
5use geo::Point;
6use rust_decimal::Decimal;
7use surrealdb_types::ToSql;
8
9use crate::expr::Kind;
10use crate::expr::kind::{GeometryKind, HasKind, KindLiteral};
11use crate::val::{
12 Array, Bytes, Closure, Datetime, Duration, File, Geometry, Null, Number, Object, Range,
13 RecordId, Regex, Set, SqlNone, Strand, TableName, Uuid, Value,
14};
15
16#[derive(Clone, Debug, PartialEq)]
18pub enum ElementPosition {
19 Index(usize),
21 Key(String),
23}
24
25#[derive(Clone, Debug)]
26pub enum CoerceError {
27 InvalidKind {
29 from: Value,
30 into: String,
31 },
32 InvalidLength {
33 len: usize,
34 into: String,
35 },
36 ElementOf {
38 inner: Box<CoerceError>,
39 into: String,
40 position: Option<ElementPosition>,
41 },
42}
43impl std::error::Error for CoerceError {}
44impl fmt::Display for CoerceError {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 match self {
47 CoerceError::InvalidKind {
48 from,
49 into,
50 } => {
51 write!(f, "Expected `{into}` but found `{from}`", from = from.to_sql())
52 }
53 CoerceError::ElementOf {
54 inner,
55 into,
56 position,
57 } => {
58 inner.fmt(f)?;
59 match position {
60 Some(ElementPosition::Index(i)) => {
61 write!(f, " when coercing element at index {i} of `{into}`")
62 }
63 Some(ElementPosition::Key(k)) => {
64 write!(f, " when coercing value for key '{k}' of `{into}`")
65 }
66 None => write!(f, " when coercing an element of `{into}`"),
67 }
68 }
69 CoerceError::InvalidLength {
70 len,
71 into,
72 } => {
73 write!(f, "Expected `{into}` but found a collection of length `{len}`")
74 }
75 }
76 }
77}
78
79pub trait CoerceErrorExt {
80 fn with_element_of<F>(self, f: F) -> Self
81 where
82 F: Fn() -> String;
83
84 fn with_element_of_at_index<F>(self, index: usize, f: F) -> Self
85 where
86 F: Fn() -> String;
87}
88
89impl<T> CoerceErrorExt for Result<T, CoerceError> {
90 fn with_element_of<F>(self, f: F) -> Self
91 where
92 F: Fn() -> String,
93 {
94 match self {
95 Ok(x) => Ok(x),
96 Err(e) => Err(CoerceError::ElementOf {
97 inner: Box::new(e),
98 into: f(),
99 position: None,
100 }),
101 }
102 }
103
104 fn with_element_of_at_index<F>(self, index: usize, f: F) -> Self
105 where
106 F: Fn() -> String,
107 {
108 match self {
109 Ok(x) => Ok(x),
110 Err(e) => Err(CoerceError::ElementOf {
111 inner: Box::new(e),
112 into: f(),
113 position: Some(ElementPosition::Index(index)),
114 }),
115 }
116 }
117}
118
119pub trait Coerce: Sized {
129 fn can_coerce(v: &Value) -> bool;
133
134 fn coerce(v: Value) -> Result<Self, CoerceError>;
136}
137
138impl Coerce for Value {
139 fn can_coerce(_: &Value) -> bool {
140 true
141 }
142
143 fn coerce(v: Value) -> Result<Self, CoerceError> {
144 Ok(v)
145 }
146}
147
148impl Coerce for SqlNone {
149 fn can_coerce(v: &Value) -> bool {
150 matches!(v, Value::None)
151 }
152
153 fn coerce(v: Value) -> Result<Self, CoerceError> {
154 match v {
155 Value::None => Ok(SqlNone),
156 x => Err(CoerceError::InvalidKind {
157 from: x,
158 into: "none".to_string(),
159 }),
160 }
161 }
162}
163
164impl Coerce for Null {
165 fn can_coerce(v: &Value) -> bool {
166 matches!(v, Value::Null)
167 }
168
169 fn coerce(v: Value) -> Result<Null, CoerceError> {
170 match v {
171 Value::Null => Ok(Null),
173 _ => Err(CoerceError::InvalidKind {
175 from: v,
176 into: "null".into(),
177 }),
178 }
179 }
180}
181
182impl Coerce for i64 {
183 fn can_coerce(v: &Value) -> bool {
184 let Value::Number(n) = v else {
185 return false;
186 };
187 match n {
188 Number::Int(_) => true,
189 Number::Float(f) => f.fract() == 0.0,
190 Number::Decimal(d) => i64::try_from(*d).is_ok(),
191 }
192 }
193
194 fn coerce(val: Value) -> Result<Self, CoerceError> {
195 match val {
196 Value::Number(Number::Int(v)) => Ok(v),
198 Value::Number(Number::Float(v)) if v.fract() == 0.0 => Ok(v as i64),
200 Value::Number(Number::Decimal(v)) if v.is_integer() => match v.try_into() {
202 Ok(v) => Ok(v),
204 _ => Err(CoerceError::InvalidKind {
206 from: val,
207 into: "int".into(),
208 }),
209 },
210 _ => Err(CoerceError::InvalidKind {
212 from: val,
213 into: "int".into(),
214 }),
215 }
216 }
217}
218
219impl Coerce for f64 {
220 fn can_coerce(v: &Value) -> bool {
221 let Value::Number(n) = v else {
222 return false;
223 };
224 match n {
225 Number::Int(_) | Number::Float(_) => true,
226 Number::Decimal(d) => f64::try_from(*d).is_ok(),
227 }
228 }
229
230 fn coerce(val: Value) -> Result<f64, CoerceError> {
232 match val {
233 Value::Number(Number::Float(v)) => Ok(v),
235 Value::Number(Number::Int(v)) => Ok(v as f64),
237 Value::Number(Number::Decimal(v)) => match v.try_into() {
239 Ok(v) => Ok(v),
241 _ => Err(CoerceError::InvalidKind {
243 from: val,
244 into: "float".into(),
245 }),
246 },
247 _ => Err(CoerceError::InvalidKind {
249 from: val,
250 into: "float".into(),
251 }),
252 }
253 }
254}
255
256impl Coerce for Decimal {
257 fn can_coerce(v: &Value) -> bool {
258 let Value::Number(n) = v else {
259 return false;
260 };
261 match n {
262 Number::Int(_) | Number::Decimal(_) => true,
263 Number::Float(f) => Decimal::try_from(*f).is_ok(),
264 }
265 }
266
267 fn coerce(val: Value) -> Result<Self, CoerceError> {
268 match val {
269 Value::Number(Number::Decimal(x)) => Ok(x),
271 Value::Number(Number::Int(v)) => Ok(Decimal::from(v)),
273 Value::Number(Number::Float(v)) => match Decimal::try_from(v).ok() {
275 Some(v) => Ok(v),
277 None => Err(CoerceError::InvalidKind {
279 from: val,
280 into: "decimal".into(),
281 }),
282 },
283 _ => Err(CoerceError::InvalidKind {
285 from: val,
286 into: "decimal".into(),
287 }),
288 }
289 }
290}
291
292impl Coerce for File {
293 fn can_coerce(v: &Value) -> bool {
294 matches!(v, Value::File(_))
295 }
296
297 fn coerce(v: Value) -> Result<Self, CoerceError> {
298 if let Value::File(x) = v {
299 Ok(x)
300 } else {
301 Err(CoerceError::InvalidKind {
302 from: v,
303 into: "file".to_string(),
304 })
305 }
306 }
307}
308
309impl Coerce for Point<f64> {
310 fn can_coerce(v: &Value) -> bool {
311 matches!(v, Value::Geometry(Geometry::Point(_)))
312 }
313
314 fn coerce(v: Value) -> Result<Self, CoerceError> {
315 if let Value::Geometry(Geometry::Point(x)) = v {
316 Ok(x)
317 } else {
318 Err(CoerceError::InvalidKind {
319 from: v,
320 into: "point".to_string(),
321 })
322 }
323 }
324}
325
326impl<T: Coerce + HasKind> Coerce for Option<T> {
327 fn can_coerce(v: &Value) -> bool {
328 if let Value::None = v {
329 return true;
330 }
331 T::can_coerce(v)
332 }
333
334 fn coerce(v: Value) -> Result<Self, CoerceError> {
335 match v {
336 Value::None => Ok(None),
337 x => Ok(Some(T::coerce(x)?)),
338 }
339 }
340}
341
342impl<T: Coerce + HasKind> Coerce for Vec<T> {
343 fn can_coerce(v: &Value) -> bool {
344 let Value::Array(a) = v else {
345 return false;
346 };
347 a.iter().all(T::can_coerce)
348 }
349
350 fn coerce(v: Value) -> Result<Self, CoerceError> {
351 if !v.is_array() {
352 return Err(CoerceError::InvalidKind {
353 from: v,
354 into: <Self as HasKind>::kind().to_sql(),
355 });
356 }
357 let array = v.into_array().expect("value type checked above");
359
360 let mut res = Vec::with_capacity(array.0.len());
361 for (i, x) in array.0.into_iter().enumerate() {
362 res.push(
363 x.coerce_to::<T>()
364 .with_element_of_at_index(i, || <Self as HasKind>::kind().to_sql())?,
365 );
366 }
367 Ok(res)
368 }
369}
370
371impl<T: Coerce + HasKind> Coerce for BTreeMap<String, T> {
372 fn can_coerce(v: &Value) -> bool {
373 let Value::Object(a) = v else {
374 return false;
375 };
376 a.values().all(T::can_coerce)
377 }
378
379 fn coerce(v: Value) -> Result<Self, CoerceError> {
380 if !v.is_object() {
381 return Err(CoerceError::InvalidKind {
382 from: v,
383 into: Object::kind().to_sql(),
384 });
385 };
386 let obj = v.into_object().expect("value type checked above");
388
389 let mut res = BTreeMap::new();
390 for (k, v) in obj.0 {
391 let key = k.into_string();
393 let value = match v.coerce_to::<T>() {
394 Ok(v) => v,
395 Err(e) => {
396 return Err(CoerceError::ElementOf {
397 inner: Box::new(e),
398 into: format!("object<{}>", <T as HasKind>::kind().to_sql()),
399 position: Some(ElementPosition::Key(key)),
400 });
401 }
402 };
403 res.insert(key, value);
404 }
405 Ok(res)
406 }
407}
408
409impl<T: Coerce + HasKind, S: BuildHasher + Default> Coerce for HashMap<String, T, S> {
410 fn can_coerce(v: &Value) -> bool {
411 let Value::Object(a) = v else {
412 return false;
413 };
414 a.values().all(T::can_coerce)
415 }
416
417 fn coerce(v: Value) -> Result<Self, CoerceError> {
418 if !v.is_object() {
419 return Err(CoerceError::InvalidKind {
420 from: v,
421 into: Kind::of::<Object>().to_sql(),
422 });
423 };
424 let obj = v.into_object().expect("value type checked above");
426
427 let mut res = HashMap::default();
428 for (k, v) in obj.0 {
429 let key = k.into_string();
431 let value = match v.coerce_to::<T>() {
432 Ok(v) => v,
433 Err(e) => {
434 return Err(CoerceError::ElementOf {
435 inner: Box::new(e),
436 into: format!("object<{}>", <T as HasKind>::kind().to_sql()),
437 position: Some(ElementPosition::Key(key)),
438 });
439 }
440 };
441 res.insert(key, value);
442 }
443 Ok(res)
444 }
445}
446
447macro_rules! impl_direct {
448 ($($name:ident => $inner:ty $(= $kind:ident)?),*$(,)?) => {
449 $(
450 impl Coerce for $inner {
451 fn can_coerce(v: &Value) -> bool{
452 matches!(v, Value::$name(_))
453 }
454
455 fn coerce(v: Value) -> Result<Self, CoerceError> {
456 if let Value::$name(x) = v {
457 return Ok(x);
458 } else {
459 return Err(CoerceError::InvalidKind{
460 from: v,
461 into: impl_direct!(@kindof $inner $(= $kind)?),
462 });
463 }
464 }
465 }
466 )*
467 };
468
469 (@kindof $inner:ty = $kind:ident) => {
470 Kind::of::<$kind>().to_sql()
471 };
472
473 (@kindof $inner:ty) => {
474 Kind::of::<$inner>().to_sql()
475 };
476}
477
478impl_direct! {
480 Bool => bool,
481 Number => Number,
482 Uuid => Uuid,
483 Closure => Box<Closure> = Closure,
484 Range => Box<Range> = Range,
485 Datetime => Datetime,
486 Duration => Duration,
487 Bytes => Bytes,
488 Object => Object,
489 Array => Array,
490 RecordId => RecordId,
491 String => Strand = String,
492 Geometry => Geometry,
493 Regex => Regex,
494 Table => TableName,
495}
496
497impl Coerce for Set {
498 fn can_coerce(v: &Value) -> bool {
499 matches!(v, Value::Set(_) | Value::Array(_))
500 }
501
502 fn coerce(v: Value) -> Result<Self, CoerceError> {
503 match v {
504 Value::Set(x) => Ok(x),
505 Value::Array(x) => Ok(Set::from(x.0)),
506 v => Err(CoerceError::InvalidKind {
507 from: v,
508 into: Kind::of::<Set>().to_sql(),
509 }),
510 }
511 }
512}
513
514impl Coerce for String {
517 fn can_coerce(v: &Value) -> bool {
518 matches!(v, Value::String(_))
519 }
520
521 fn coerce(v: Value) -> Result<Self, CoerceError> {
522 match v {
523 Value::String(s) => Ok(s.into_string()),
524 v => Err(CoerceError::InvalidKind {
525 from: v,
526 into: Kind::of::<String>().to_sql(),
527 }),
528 }
529 }
530}
531
532impl Value {
534 pub fn can_coerce_to<T: Coerce>(&self) -> bool {
535 T::can_coerce(self)
536 }
537
538 pub fn can_coerce_to_kind(&self, kind: &Kind) -> bool {
539 match kind {
540 Kind::Any => true,
541 Kind::None => self.can_coerce_to::<SqlNone>(),
542 Kind::Null => self.can_coerce_to::<Null>(),
543 Kind::Bool => self.can_coerce_to::<bool>(),
544 Kind::Int => self.can_coerce_to::<i64>(),
545 Kind::Float => self.can_coerce_to::<f64>(),
546 Kind::Decimal => self.can_coerce_to::<Decimal>(),
547 Kind::Number => self.can_coerce_to::<Number>(),
548 Kind::String => self.can_coerce_to::<String>(),
549 Kind::Datetime => self.can_coerce_to::<Datetime>(),
550 Kind::Duration => self.can_coerce_to::<Duration>(),
551 Kind::Object => self.can_coerce_to::<crate::val::Object>(),
552 Kind::Bytes => self.can_coerce_to::<Bytes>(),
553 Kind::Uuid => self.can_coerce_to::<Uuid>(),
554 Kind::Regex => self.can_coerce_to::<Regex>(),
555 Kind::Range => self.can_coerce_to::<Box<Range>>(),
556 Kind::Function(_, _) => self.can_coerce_to::<Box<Closure>>(),
557 Kind::Set(t, l) => match l {
558 Some(l) => self.can_coerce_to_set_len(t, *l),
559 None => self.can_coerce_to_set(t),
560 },
561 Kind::Array(t, l) => match l {
562 Some(l) => self.can_coerce_to_array_len(t, *l),
563 None => self.can_coerce_to_array(t),
564 },
565 Kind::Table(t) => {
566 if t.is_empty() {
567 self.can_coerce_to::<String>()
568 } else {
569 self.can_coerce_to_table(t)
570 }
571 }
572 Kind::Record(t) => {
573 if t.is_empty() {
574 self.can_coerce_to::<RecordId>()
575 } else {
576 self.can_coerce_to_record(t)
577 }
578 }
579 Kind::Geometry(t) => {
580 if t.is_empty() {
581 self.can_coerce_to::<Geometry>()
582 } else {
583 self.can_coerce_to_geometry(t)
584 }
585 }
586 Kind::Either(k) => k.iter().any(|x| self.can_coerce_to_kind(x)),
587 Kind::Literal(lit) => self.can_coerce_to_literal(lit),
588 Kind::File(buckets) => {
589 if buckets.is_empty() {
590 self.can_coerce_to::<File>()
591 } else {
592 self.can_coerce_to_file_buckets(buckets)
593 }
594 }
595 }
596 }
597
598 fn can_coerce_to_array_len(&self, kind: &Kind, len: u64) -> bool {
599 match self {
600 Value::Array(a) => {
601 a.len() as u64 == len && a.iter().all(|x| x.can_coerce_to_kind(kind))
602 }
603 _ => false,
604 }
605 }
606
607 fn can_coerce_to_array(&self, kind: &Kind) -> bool {
608 match self {
609 Value::Array(a) => a.iter().all(|x| x.can_coerce_to_kind(kind)),
610 _ => false,
611 }
612 }
613
614 fn can_coerce_to_set_len(&self, kind: &Kind, len: u64) -> bool {
615 match self {
616 Value::Set(s) => s.len() as u64 == len && s.iter().all(|x| x.can_coerce_to_kind(kind)),
617 Value::Array(a) => {
618 a.len() as u64 == len && a.iter().all(|x| x.can_coerce_to_kind(kind))
619 }
620 _ => false,
621 }
622 }
623
624 fn can_coerce_to_set(&self, kind: &Kind) -> bool {
625 match self {
626 Value::Set(s) => s.iter().all(|x| x.can_coerce_to_kind(kind)),
627 Value::Array(a) => a.iter().all(|x| x.can_coerce_to_kind(kind)),
628 _ => false,
629 }
630 }
631
632 fn can_coerce_to_table(&self, val: &[TableName]) -> bool {
633 match self {
634 Value::Table(t) => val.is_empty() || val.contains(t),
635 Value::String(s) => {
636 if val.is_empty() {
638 true
639 } else {
640 let s = TableName::from(s.clone());
641 val.contains(&s)
642 }
643 }
644 _ => false,
645 }
646 }
647
648 fn can_coerce_to_record(&self, val: &[TableName]) -> bool {
649 match self {
650 Value::RecordId(t) => val.is_empty() || val.contains(&t.table),
651 _ => false,
652 }
653 }
654
655 fn can_coerce_to_geometry(&self, val: &[GeometryKind]) -> bool {
656 self.is_geometry_type(val)
657 }
658
659 fn can_coerce_to_literal(&self, val: &KindLiteral) -> bool {
660 val.validate_value(self)
661 }
662
663 fn can_coerce_to_file_buckets(&self, buckets: &[String]) -> bool {
664 matches!(self, Value::File(f) if f.is_bucket_type(buckets))
665 }
666
667 pub fn coerce_to<T: Coerce>(self) -> Result<T, CoerceError> {
675 T::coerce(self)
676 }
677
678 pub fn coerce_to_kind(self, kind: &Kind) -> Result<Value, CoerceError> {
680 match kind {
682 Kind::Any => Ok(self),
683 Kind::None => self.coerce_to::<SqlNone>().map(|_| Value::None),
684 Kind::Null => self.coerce_to::<Null>().map(Value::from),
685 Kind::Bool => self.coerce_to::<bool>().map(Value::from),
686 Kind::Int => self.coerce_to::<i64>().map(Value::from),
687 Kind::Float => self.coerce_to::<f64>().map(Value::from),
688 Kind::Decimal => self.coerce_to::<Decimal>().map(Value::from),
689 Kind::Number => self.coerce_to::<Number>().map(Value::from),
690 Kind::String => self.coerce_to::<String>().map(Value::from),
691 Kind::Datetime => self.coerce_to::<Datetime>().map(Value::from),
692 Kind::Duration => self.coerce_to::<Duration>().map(Value::from),
693 Kind::Object => self.coerce_to::<crate::val::Object>().map(Value::from),
694 Kind::Bytes => self.coerce_to::<Bytes>().map(Value::from),
695 Kind::Uuid => self.coerce_to::<Uuid>().map(Value::from),
696 Kind::Regex => self.coerce_to::<Regex>().map(Value::from),
697 Kind::Range => self.coerce_to::<Box<Range>>().map(Value::from),
698 Kind::Function(_, _) => self.coerce_to::<Box<Closure>>().map(Value::from),
699 Kind::Set(t, l) => match l {
700 Some(l) => self.coerce_to_set_kind_len(t, *l).map(Value::from),
701 None => self.coerce_to_set_kind(t).map(Value::from),
702 },
703 Kind::Array(t, l) => match l {
704 Some(l) => self.coerce_to_array_type_len(t, *l).map(Value::from),
705 None => self.coerce_to_array_type(t).map(Value::from),
706 },
707 Kind::Table(t) => {
708 if t.is_empty() {
709 self.coerce_to::<String>()
710 .map(|s| Value::Table(surrealdb_strand::TableName::new(s)))
711 } else {
712 self.coerce_to_table_kind(t).map(Value::from)
713 }
714 }
715 Kind::Record(t) => {
716 if t.is_empty() {
717 self.coerce_to::<RecordId>().map(Value::from)
718 } else {
719 self.coerce_to_record_kind(t).map(Value::from)
720 }
721 }
722 Kind::Geometry(t) => {
723 if t.is_empty() {
724 self.coerce_to::<Geometry>().map(Value::from)
725 } else {
726 self.coerce_to_geometry_kind(t).map(Value::from)
727 }
728 }
729 Kind::Either(k) => {
730 let Some(k) = k.iter().find(|x| self.can_coerce_to_kind(x)) else {
732 return Err(CoerceError::InvalidKind {
733 from: self,
734 into: kind.to_sql(),
735 });
736 };
737
738 Ok(self.coerce_to_kind(k).expect(
739 "If can_coerce_to_kind returns true then coerce_to_kind must not error",
740 ))
741 }
742 Kind::Literal(lit) => self.coerce_to_literal(lit),
743 Kind::File(buckets) => {
744 if buckets.is_empty() {
745 self.coerce_to::<File>().map(Value::from)
746 } else {
747 self.coerce_to_file_buckets(buckets).map(Value::from)
748 }
749 }
750 }
751 }
752
753 pub fn coerce_to_literal(self, literal: &KindLiteral) -> Result<Value, CoerceError> {
756 if literal.validate_value(&self) {
757 Ok(self)
758 } else {
759 Err(CoerceError::InvalidKind {
760 from: self,
761 into: literal.to_sql(),
762 })
763 }
764 }
765
766 pub fn coerce_to_table_kind(
768 self,
769 val: &[TableName],
770 ) -> Result<surrealdb_strand::TableName, CoerceError> {
771 let this = match self {
772 Value::Table(v) => {
774 if val.is_empty() || val.contains(&v) {
775 return Ok(v);
776 } else {
777 Value::Table(v)
778 }
779 }
780 Value::String(s) => {
782 if val.is_empty() {
783 return Ok(surrealdb_strand::TableName::new(s));
784 }
785
786 let t = TableName::from(s);
787 if val.contains(&t) {
788 return Ok(t);
789 }
790
791 Value::String(t.into())
792 }
793 x => x,
794 };
795
796 let mut kind = "table<".to_string();
797 for (idx, t) in val.iter().enumerate() {
798 if idx != 0 {
799 kind.push('|');
800 }
801 kind.push_str(t.as_str())
802 }
803 kind.push('>');
804 Err(CoerceError::InvalidKind {
805 from: this,
806 into: kind,
807 })
808 }
809
810 pub fn coerce_to_record_kind(self, val: &[TableName]) -> Result<RecordId, CoerceError> {
812 let this = match self {
813 Value::RecordId(v) => {
815 if val.is_empty() || val.contains(&v.table) {
816 return Ok(v);
817 } else {
818 Value::RecordId(v)
819 }
820 }
821 x => x,
822 };
823
824 let mut kind = "record<".to_string();
825 for (idx, t) in val.iter().enumerate() {
826 if idx != 0 {
827 kind.push('|');
828 }
829 kind.push_str(t.as_str())
830 }
831 kind.push('>');
832 Err(CoerceError::InvalidKind {
833 from: this,
834 into: kind,
835 })
836 }
837
838 pub fn coerce_to_geometry_kind(self, val: &[GeometryKind]) -> Result<Geometry, CoerceError> {
840 if self.is_geometry_type(val) {
841 let Value::Geometry(x) = self else {
842 unreachable!()
844 };
845 Ok(x)
846 } else {
847 Err(CoerceError::InvalidKind {
848 from: self,
849 into: "geometry".into(),
850 })
851 }
852 }
853
854 pub fn coerce_to_array_type(self, kind: &Kind) -> Result<Array, CoerceError> {
856 self.coerce_to::<Array>()?
857 .into_iter()
858 .enumerate()
859 .map(|(i, value)| {
860 value
861 .coerce_to_kind(kind)
862 .with_element_of_at_index(i, || format!("array<{}>", kind.to_sql()))
863 })
864 .collect::<Result<Array, CoerceError>>()
865 }
866
867 pub fn coerce_to_array_type_len(self, kind: &Kind, len: u64) -> Result<Array, CoerceError> {
869 let array = self.coerce_to::<Array>()?;
870
871 if array.len() as u64 != len {
872 return Err(CoerceError::InvalidLength {
873 len: array.len(),
874 into: format!("array<{},{}>", kind.to_sql(), len),
875 });
876 }
877
878 array
879 .into_iter()
880 .enumerate()
881 .map(|(i, value)| {
882 value
883 .coerce_to_kind(kind)
884 .with_element_of_at_index(i, || format!("array<{}>", kind.to_sql()))
885 })
886 .collect::<Result<Array, CoerceError>>()
887 }
888
889 pub fn coerce_to_set_kind(self, kind: &Kind) -> Result<Set, CoerceError> {
891 self.coerce_to::<Set>()?
892 .into_iter()
893 .enumerate()
894 .map(|(i, value)| {
895 value
896 .coerce_to_kind(kind)
897 .with_element_of_at_index(i, || format!("set<{}>", kind.to_sql()))
898 })
899 .collect::<Result<Set, CoerceError>>()
900 }
901
902 pub fn coerce_to_set_kind_len(self, kind: &Kind, len: u64) -> Result<Set, CoerceError> {
904 let set = self
905 .coerce_to::<Set>()?
906 .into_iter()
907 .enumerate()
908 .map(|(i, value)| {
909 value
910 .coerce_to_kind(kind)
911 .with_element_of_at_index(i, || format!("set<{}>", kind.to_sql()))
912 })
913 .collect::<Result<Set, CoerceError>>()?;
914
915 if set.len() as u64 != len {
916 return Err(CoerceError::InvalidLength {
917 into: format!("set<{},{}>", kind.to_sql(), len),
918 len: set.len(),
919 });
920 }
921
922 Ok(set)
923 }
924
925 pub fn coerce_to_file_buckets(self, buckets: &[String]) -> Result<File, CoerceError> {
926 let v = self.coerce_to::<File>()?;
927
928 if v.is_bucket_type(buckets) {
929 return Ok(v);
930 }
931
932 let mut kind = "file<".to_owned();
933 for (idx, t) in buckets.iter().enumerate() {
934 if idx != 0 {
935 kind.push('|');
936 }
937 kind.push_str(t.as_str())
938 }
939 kind.push('>');
940 Err(CoerceError::InvalidKind {
941 from: v.into(),
942 into: kind,
943 })
944 }
945}
946
947#[cfg(test)]
948mod tests {
949 use surrealdb_strand::Strand;
950
951 use super::*;
952
953 #[test]
954 fn test_coerce_array_element_position_in_error() {
955 let value = Value::Array(Array(vec![
957 Value::Number(1.into()),
958 Value::String(Strand::new_static("bad")),
959 Value::Number(3.into()),
960 ]));
961 let kind = Kind::Int;
962 let err = value.coerce_to_array_type(&kind).unwrap_err();
963 let msg = err.to_string();
964 assert!(msg.contains("index 1"), "error message should mention index 1, got: {msg}");
965 }
966
967 #[test]
968 fn test_coerce_object_key_position_in_error() {
969 use std::collections::BTreeMap;
970
971 let mut map = BTreeMap::new();
972 map.insert(surrealdb_strand::Strand::new_static("valid"), Value::Number(1.into()));
973 map.insert(
974 surrealdb_strand::Strand::new_static("bad_field"),
975 Value::String(Strand::new_static("not_an_int")),
976 );
977 let value = Value::Object(Object(map.into()));
978
979 let err = value.coerce_to::<BTreeMap<String, i64>>().unwrap_err();
980 let msg = err.to_string();
981 assert!(
982 msg.contains("bad_field"),
983 "error message should mention the key 'bad_field', got: {msg}"
984 );
985 }
986
987 #[test]
988 fn test_coerce_vec_element_position_in_error() {
989 let value = Value::Array(Array(vec![
990 Value::Number(1.into()),
991 Value::Number(2.into()),
992 Value::String(Strand::new_static("oops")),
993 ]));
994
995 let err = value.coerce_to::<Vec<i64>>().unwrap_err();
996 let msg = err.to_string();
997 assert!(msg.contains("index 2"), "error message should mention index 2, got: {msg}");
998 }
999
1000 #[test]
1001 fn test_coerce_to_table_generic() {
1002 let value = Value::String(Strand::new_static("users"));
1004 let kind = Kind::Table(vec![]);
1005 let result = value.coerce_to_kind(&kind);
1006 assert!(result.is_ok());
1007 if let Ok(Value::Table(table)) = result {
1008 assert_eq!(table.as_str(), "users");
1009 }
1010 }
1011
1012 #[test]
1013 fn test_coerce_to_table_specific() {
1014 let value = Value::String(Strand::new_static("posts"));
1016 let kind = Kind::Table(vec!["users".into()]);
1017 let result = value.coerce_to_kind(&kind);
1018 assert!(result.is_err());
1021 }
1022
1023 #[test]
1024 fn test_coerce_table_to_table() {
1025 let value = Value::Table("users".into());
1027 let kind = Kind::Table(vec!["users".into()]);
1028 let result = value.coerce_to_kind(&kind);
1029 assert!(result.is_ok());
1030
1031 let value = Value::Table("posts".into());
1033 let kind = Kind::Table(vec!["users".into()]);
1034 let result = value.coerce_to_kind(&kind);
1035 assert!(result.is_err());
1036 }
1037
1038 #[test]
1039 fn test_can_coerce_to_table() {
1040 let value = Value::Table("users".into());
1042 let kind = Kind::Table(vec!["users".into()]);
1043 assert!(value.can_coerce_to_kind(&kind));
1044
1045 let value = Value::Table("posts".into());
1047 let kind = Kind::Table(vec!["users".into()]);
1048 assert!(!value.can_coerce_to_kind(&kind));
1049
1050 let value = Value::Number(42.into());
1052 let kind = Kind::Table(vec![]);
1053 assert!(!value.can_coerce_to_kind(&kind));
1054 }
1055
1056 #[test]
1057 fn test_coerce_table_empty_tables_list() {
1058 let value = Value::Table("anything".into());
1060 let kind = Kind::Table(vec![]);
1061 let result = value.coerce_to_kind(&kind);
1062 assert!(result.is_err()); }
1064}