Skip to main content

surrealdb_expr/val/value/convert/
coerce.rs

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/// Identifies which element of a collection caused a coercion failure.
17#[derive(Clone, Debug, PartialEq)]
18pub enum ElementPosition {
19	/// Zero-based index into an array or set.
20	Index(usize),
21	/// Key of an object field.
22	Key(String),
23}
24
25#[derive(Clone, Debug)]
26pub enum CoerceError {
27	// Coercion error at the end.
28	InvalidKind {
29		from: Value,
30		into: String,
31	},
32	InvalidLength {
33		len: usize,
34		into: String,
35	},
36	// Coerce failed because element of type didn't match.
37	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
119/// Trait for converting the value using coercion rules.
120///
121/// Coercion rules are applied whenever a Value needs to be of a specific
122/// [`Kind`]. This happens when a value is applied to a place with a type like
123/// table fields and function parameters.
124///
125/// Coercion rules are more strict then casting rules.
126/// Calling this method will succeed if the value can be unified with the kind
127/// of the target
128pub trait Coerce: Sized {
129	/// Returns if calling coerce on the value will succeed or not.
130	///
131	/// If `T::can_coerce(&v)` returns `false` then `T::coerce(v) should not
132	fn can_coerce(v: &Value) -> bool;
133
134	/// Coerce a value.
135	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			// Allow any null value
172			Value::Null => Ok(Null),
173			// Anything else raises an error
174			_ => 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			// Allow any int number
197			Value::Number(Number::Int(v)) => Ok(v),
198			// Attempt to convert an float number
199			Value::Number(Number::Float(v)) if v.fract() == 0.0 => Ok(v as i64),
200			// Attempt to convert a decimal number
201			Value::Number(Number::Decimal(v)) if v.is_integer() => match v.try_into() {
202				// The Decimal can be represented as an i64
203				Ok(v) => Ok(v),
204				// The Decimal is out of bounds
205				_ => Err(CoerceError::InvalidKind {
206					from: val,
207					into: "int".into(),
208				}),
209			},
210			// Anything else raises an error
211			_ => 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	/// Try to coerce this value to an `f64`
231	fn coerce(val: Value) -> Result<f64, CoerceError> {
232		match val {
233			// Allow any float number
234			Value::Number(Number::Float(v)) => Ok(v),
235			// Attempt to convert an int number
236			Value::Number(Number::Int(v)) => Ok(v as f64),
237			// Attempt to convert a decimal number
238			Value::Number(Number::Decimal(v)) => match v.try_into() {
239				// The Decimal can be represented as a f64
240				Ok(v) => Ok(v),
241				// This Decimal loses precision
242				_ => Err(CoerceError::InvalidKind {
243					from: val,
244					into: "float".into(),
245				}),
246			},
247			// Anything else raises an error
248			_ => 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			// Allow any decimal number
270			Value::Number(Number::Decimal(x)) => Ok(x),
271			// Attempt to convert an int number
272			Value::Number(Number::Int(v)) => Ok(Decimal::from(v)),
273			// Attempt to convert an float number
274			Value::Number(Number::Float(v)) => match Decimal::try_from(v).ok() {
275				// The Float can be represented as a Decimal
276				Some(v) => Ok(v),
277				// This Float does not convert to a Decimal
278				None => Err(CoerceError::InvalidKind {
279					from: val,
280					into: "decimal".into(),
281				}),
282			},
283			// Anything else raises an error
284			_ => 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		// checked above
358		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		// checked above
387		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			// object<T> kinds don't actually exist in surql.
392			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		// checked above
425		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			// object<T> kinds don't actually exist in surql.
430			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
478// Types which directly match one enum variant or fail
479impl_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
514/// `String` coercion delegates to `Strand` and converts on success
515/// so existing `coerce_to::<String>()` call sites keep working.
516impl 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
532// Coerce to runtime value implementations
533impl 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				// Allow strings to be coerced to tables
637				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	/// Convert the value using coercion rules.
668	///
669	/// Coercion rules are more strict then coverting rules.
670	/// Calling this method will succeed if the value can by unified with the
671	/// kind of the target
672	///
673	/// This method is a shorthand for `T::coerce(self)`
674	pub fn coerce_to<T: Coerce>(self) -> Result<T, CoerceError> {
675		T::coerce(self)
676	}
677
678	/// Try to coerce this value to the specified `Kind`
679	pub fn coerce_to_kind(self, kind: &Kind) -> Result<Value, CoerceError> {
680		// Attempt to convert to the desired type
681		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				// Check first for valid kind, then convert to not consume the value
731				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	/// Try to coerce this value to a Literal, returns a `Value` with the
754	/// coerced value
755	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	/// Try to coerce this value to a Table of a certain type
767	pub fn coerce_to_table_kind(
768		self,
769		val: &[TableName],
770	) -> Result<surrealdb_strand::TableName, CoerceError> {
771		let this = match self {
772			// Tables are allowed if correct type
773			Value::Table(v) => {
774				if val.is_empty() || val.contains(&v) {
775					return Ok(v);
776				} else {
777					Value::Table(v)
778				}
779			}
780			// Allow strings to be coerced to tables
781			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	/// Try to coerce this value to a Record of a certain type
811	pub fn coerce_to_record_kind(self, val: &[TableName]) -> Result<RecordId, CoerceError> {
812		let this = match self {
813			// Records are allowed if correct type
814			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	/// Try to coerce this value to a `Geometry` of a certain type
839	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				// Checked above in is_geometry_type
843				unreachable!()
844			};
845			Ok(x)
846		} else {
847			Err(CoerceError::InvalidKind {
848				from: self,
849				into: "geometry".into(),
850			})
851		}
852	}
853
854	/// Try to coerce this value to an `Array` of a certain type
855	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	/// Try to coerce this value to an `Array` of a certain type, and length
868	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	/// Try to coerce this value to a `Set` of a certain type
890	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	/// Try to coerce this value to a `Set` of a certain type and length
903	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		// An array where the second element (index 1) is the wrong type.
956		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		// Test coercing string to generic table type
1003		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		// Coercion should fail for wrong table name (more strict than cast)
1015		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		// Coercion from string to specific table type should fail because
1019		// coercion is stricter and only allows exact matches
1020		assert!(result.is_err());
1021	}
1022
1023	#[test]
1024	fn test_coerce_table_to_table() {
1025		// Test coercing table value to matching table type
1026		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		// Test coercing table value to non-matching table type
1032		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		// Test can_coerce_to_kind for tables
1041		let value = Value::Table("users".into());
1042		let kind = Kind::Table(vec!["users".into()]);
1043		assert!(value.can_coerce_to_kind(&kind));
1044
1045		// Wrong table name
1046		let value = Value::Table("posts".into());
1047		let kind = Kind::Table(vec!["users".into()]);
1048		assert!(!value.can_coerce_to_kind(&kind));
1049
1050		// Wrong type
1051		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		// Test with empty tables list (should accept any table)
1059		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()); // Coercion from string is strict
1063	}
1064}