Skip to main content

surrealdb_expr/val/
record_id.rs

1use std::cmp::Ordering;
2use std::ops::Bound;
3
4use common::fmt::{EscapeIdent, EscapeRidKey};
5use rand::seq::IndexedRandom;
6use revision::revisioned;
7use storekey::{BorrowDecode, Encode};
8use surrealdb_cnf::ID_CHARS;
9use surrealdb_types::{SqlFormat, ToSql, write_sql};
10use ulid::Ulid;
11
12use crate::expr::{self};
13use crate::val::table_name_public::{IntoPublicTable, IntoTableName};
14use crate::val::{Array, IndexFormat, Number, Object, Range, Strand, TableName, Uuid, Value};
15
16#[revisioned(revision = 1)]
17#[derive(Clone, Debug, Eq, PartialEq, Hash, Encode, BorrowDecode)]
18#[storekey(format = "()")]
19#[storekey(format = "IndexFormat")]
20pub struct RecordIdKeyRange {
21	pub start: Bound<RecordIdKey>,
22	pub end: Bound<RecordIdKey>,
23}
24
25impl PartialOrd for RecordIdKeyRange {
26	fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
27		Some(self.cmp(other))
28	}
29}
30
31impl Ord for RecordIdKeyRange {
32	fn cmp(&self, other: &Self) -> Ordering {
33		fn compare_bounds(a: &Bound<RecordIdKey>, b: &Bound<RecordIdKey>) -> Ordering {
34			match a {
35				Bound::Unbounded => match b {
36					Bound::Unbounded => Ordering::Equal,
37					_ => Ordering::Less,
38				},
39				Bound::Included(a) => match b {
40					Bound::Unbounded => Ordering::Greater,
41					Bound::Included(b) => a.cmp(b),
42					Bound::Excluded(_) => Ordering::Less,
43				},
44				Bound::Excluded(a) => match b {
45					Bound::Excluded(b) => a.cmp(b),
46					_ => Ordering::Greater,
47				},
48			}
49		}
50		match compare_bounds(&self.start, &other.end) {
51			Ordering::Equal => compare_bounds(&self.end, &other.end),
52			x => x,
53		}
54	}
55}
56
57impl ToSql for RecordIdKeyRange {
58	fn fmt_sql(&self, f: &mut String, sql_fmt: SqlFormat) {
59		match self.start {
60			Bound::Unbounded => {}
61			Bound::Included(ref x) => write_sql!(f, sql_fmt, "{x}"),
62			Bound::Excluded(ref x) => write_sql!(f, sql_fmt, "{x}>"),
63		}
64		write_sql!(f, sql_fmt, "..");
65		match self.end {
66			Bound::Unbounded => {}
67			Bound::Included(ref x) => write_sql!(f, sql_fmt, "={x}"),
68			Bound::Excluded(ref x) => write_sql!(f, sql_fmt, "{x}"),
69		}
70	}
71}
72
73impl TryFrom<RecordIdKeyRange> for crate::types::PublicRecordIdKeyRange {
74	type Error = anyhow::Error;
75
76	fn try_from(value: RecordIdKeyRange) -> Result<Self, Self::Error> {
77		Ok(crate::types::PublicRecordIdKeyRange {
78			start: match value.start {
79				Bound::Included(x) => Bound::Included(x.try_into()?),
80				Bound::Excluded(x) => Bound::Excluded(x.try_into()?),
81				Bound::Unbounded => Bound::Unbounded,
82			},
83			end: match value.end {
84				Bound::Included(x) => Bound::Included(x.try_into()?),
85				Bound::Excluded(x) => Bound::Excluded(x.try_into()?),
86				Bound::Unbounded => Bound::Unbounded,
87			},
88		})
89	}
90}
91
92impl From<crate::types::PublicRecordIdKeyRange> for RecordIdKeyRange {
93	fn from(value: crate::types::PublicRecordIdKeyRange) -> Self {
94		RecordIdKeyRange {
95			start: value.start.map(|x| x.into()),
96			end: value.end.map(|x| x.into()),
97		}
98	}
99}
100
101impl RecordIdKeyRange {
102	pub fn into_literal(self) -> expr::RecordIdKeyRangeLit {
103		let start = self.start.map(|x| x.into_literal());
104		let end = self.end.map(|x| x.into_literal());
105		expr::RecordIdKeyRangeLit {
106			start,
107			end,
108		}
109	}
110
111	/// Convertes a record id key range into the range from a normal value.
112	pub fn into_value_range(self) -> Range {
113		Range {
114			start: self.start.map(|x| x.into_value()),
115			end: self.end.map(|x| x.into_value()),
116		}
117	}
118
119	/// Convertes a record id key range into the range from a normal value.
120	pub fn from_value_range(range: Range) -> Option<Self> {
121		let start = match range.start {
122			Bound::Included(x) => Bound::Included(RecordIdKey::from_value(x)?),
123			Bound::Excluded(x) => Bound::Excluded(RecordIdKey::from_value(x)?),
124			Bound::Unbounded => Bound::Unbounded,
125		};
126		let end = match range.end {
127			Bound::Included(x) => Bound::Included(RecordIdKey::from_value(x)?),
128			Bound::Excluded(x) => Bound::Excluded(RecordIdKey::from_value(x)?),
129			Bound::Unbounded => Bound::Unbounded,
130		};
131
132		Some(RecordIdKeyRange {
133			start,
134			end,
135		})
136	}
137}
138
139impl PartialEq<Range> for RecordIdKeyRange {
140	fn eq(&self, other: &Range) -> bool {
141		(match self.start {
142			Bound::Included(ref a) => {
143				if let Bound::Included(ref b) = other.start {
144					a == b
145				} else {
146					false
147				}
148			}
149			Bound::Excluded(ref a) => {
150				if let Bound::Excluded(ref b) = other.start {
151					a == b
152				} else {
153					false
154				}
155			}
156			Bound::Unbounded => matches!(other.start, Bound::Unbounded),
157		}) && (match self.end {
158			Bound::Included(ref a) => {
159				if let Bound::Included(ref b) = other.end {
160					a == b
161				} else {
162					false
163				}
164			}
165			Bound::Excluded(ref a) => {
166				if let Bound::Excluded(ref b) = other.end {
167					a == b
168				} else {
169					false
170				}
171			}
172			Bound::Unbounded => matches!(other.end, Bound::Unbounded),
173		})
174	}
175}
176
177#[revisioned(revision = 1)]
178#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Encode, BorrowDecode)]
179#[storekey(format = "()")]
180#[storekey(format = "IndexFormat")]
181pub enum RecordIdKey {
182	Number(i64),
183	String(Strand),
184	Uuid(Uuid),
185	Array(Array),
186	Object(Object),
187	Range(Box<RecordIdKeyRange>),
188}
189
190/// A record id carried inside a key as an *identity*, encoded losslessly
191/// whatever format the enclosing key pins.
192///
193/// [`IndexFormat`] erases a number's kind deliberately: an index key stands for
194/// its whole equality class, so a lookup for `1` has to find a row stored as
195/// `1f`. A record id in a key is not matched that way — it addresses a record —
196/// so erasing its type is precisely what breaks it. Two distinct records
197/// collapse onto one key, and an id decoded back out names a record that was
198/// never stored under it, because the record itself is addressed by the default
199/// codec, which carries the number's kind.
200///
201/// Encoding and decoding therefore go through the default codec under every
202/// format, so an identity cannot pick up the value encoding by sitting in a key
203/// that pins one. Key layouts that carry a record id use this rather than
204/// [`RecordIdKey`] directly; the bare type stays available for the value side,
205/// where the erasure is what is wanted.
206#[derive(Clone, Debug)]
207pub struct RecordIdentity(pub RecordIdKey);
208
209/// The bytes a record is addressed by.
210///
211/// A key that will not encode cannot be stored under one either, but the
212/// comparisons built on this have to stay total, so that case falls back to the
213/// debug spelling — which distinguishes the numeric variants too, so the
214/// fallback upholds the same property.
215fn addressed_by(key: &RecordIdKey) -> Vec<u8> {
216	storekey::encode_vec(key).unwrap_or_else(|_| format!("{key:?}").into_bytes())
217}
218
219impl RecordIdentity {
220	/// The bytes this identity is addressed by.
221	///
222	/// Equality, ordering and hashing all go through the encoding rather than
223	/// through [`RecordIdKey`]'s own comparison, which is cross-variant: `[1]`
224	/// and `[1dec]` compare equal there while addressing two different records,
225	/// and keeping those apart is what this type exists for. Deriving the
226	/// comparisons would reintroduce, between identities, the collapse the type
227	/// is here to prevent.
228	fn addressed_by(&self) -> Vec<u8> {
229		addressed_by(&self.0)
230	}
231}
232
233impl PartialEq for RecordIdentity {
234	fn eq(&self, other: &Self) -> bool {
235		self.addressed_by() == other.addressed_by()
236	}
237}
238
239impl Eq for RecordIdentity {}
240
241impl PartialOrd for RecordIdentity {
242	fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
243		Some(self.cmp(other))
244	}
245}
246
247impl Ord for RecordIdentity {
248	fn cmp(&self, other: &Self) -> std::cmp::Ordering {
249		self.addressed_by().cmp(&other.addressed_by())
250	}
251}
252
253impl std::hash::Hash for RecordIdentity {
254	fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
255		self.addressed_by().hash(state);
256	}
257}
258
259impl RecordIdentity {
260	pub fn into_inner(self) -> RecordIdKey {
261		self.0
262	}
263}
264
265impl AsRef<RecordIdKey> for RecordIdentity {
266	fn as_ref(&self) -> &RecordIdKey {
267		&self.0
268	}
269}
270
271impl From<RecordIdKey> for RecordIdentity {
272	fn from(id: RecordIdKey) -> Self {
273		Self(id)
274	}
275}
276
277impl From<RecordIdentity> for RecordIdKey {
278	fn from(id: RecordIdentity) -> Self {
279		id.0
280	}
281}
282
283impl<F> Encode<F> for RecordIdentity {
284	fn encode<W: std::io::Write>(
285		&self,
286		w: &mut storekey::Writer<W>,
287	) -> std::result::Result<(), storekey::EncodeError> {
288		Encode::<()>::encode(&self.0, w)
289	}
290}
291
292impl<'de, F> BorrowDecode<'de, F> for RecordIdentity {
293	fn borrow_decode(
294		r: &mut storekey::BorrowReader<'de>,
295	) -> std::result::Result<Self, storekey::DecodeError> {
296		Ok(Self(BorrowDecode::<'de, ()>::borrow_decode(r)?))
297	}
298}
299
300impl RecordIdKey {
301	/// Whether `self` and `other` address the same record.
302	///
303	/// **Not `==`.** [`Number`]'s equality is cross-variant, so `[1] == [1dec]`
304	/// while the two are stored, fetched and indexed under different keys — an
305	/// equality check therefore accepts exactly the collision a caller asking
306	/// this question is trying to rule out. The comparison is on the encoding,
307	/// which is what addresses the record.
308	///
309	/// This is the comparison behind [`RecordIdentity`]'s own equality; it is
310	/// exposed separately for the callers that hold bare keys, chiefly the
311	/// doc-ID mapping, where a forward hit is adopted only once the reverse
312	/// mapping agrees it names the record asked for.
313	pub fn addresses_same_record(&self, other: &Self) -> bool {
314		addressed_by(self) == addressed_by(other)
315	}
316
317	/// [`Value::hash_agrees_with_eq`] for a record id's key.
318	///
319	/// [`RecordIdKey::Number`] is an `i64`, not a [`crate::val::Number`], so plain
320	/// record ids are always decisive; only a compound id can hold one.
321	pub fn hash_agrees_with_eq(&self) -> bool {
322		match self {
323			RecordIdKey::Number(_) | RecordIdKey::String(_) | RecordIdKey::Uuid(_) => true,
324			RecordIdKey::Array(a) => a.iter().all(Value::hash_agrees_with_eq),
325			RecordIdKey::Object(o) => o.values().all(Value::hash_agrees_with_eq),
326			RecordIdKey::Range(_) => false,
327		}
328	}
329
330	/// Generate a new random ID
331	pub fn rand() -> Self {
332		let id: String = crate::rnd::with_rng(|rng| {
333			(0..20).map(|_| *ID_CHARS[..].choose(&mut *rng).unwrap_or(&'0')).collect()
334		});
335		Self::String(id.into())
336	}
337	/// Generate a new random ULID
338	pub fn ulid() -> Self {
339		Self::String(Ulid::new().to_string().into())
340	}
341	/// Generate a new random UUID
342	pub fn uuid() -> Self {
343		Self::Uuid(Uuid::new_v7())
344	}
345
346	/// Returns if this key is a range.
347	pub fn is_range(&self) -> bool {
348		matches!(self, RecordIdKey::Range(_))
349	}
350
351	/// Returns surrealql value of this key.
352	pub fn into_value(self) -> Value {
353		match self {
354			RecordIdKey::Number(n) => Value::Number(Number::Int(n)),
355			RecordIdKey::String(s) => Value::String(s),
356			RecordIdKey::Uuid(u) => Value::Uuid(u),
357			RecordIdKey::Object(object) => Value::Object(object),
358			RecordIdKey::Array(array) => Value::Array(array),
359			RecordIdKey::Range(range) => Value::Range(Box::new(Range {
360				start: range.start.map(RecordIdKey::into_value),
361				end: range.end.map(RecordIdKey::into_value),
362			})),
363		}
364	}
365
366	/// Tries to convert a value into a record id key,
367	///
368	/// Returns None if the value cannot be converted.
369	pub fn from_value(value: Value) -> Option<Self> {
370		// NOTE: This method dictates how coversion between values and record id keys
371		// behave. This method is reimplementing previous (before expr inversion pr)
372		// behavior but I am not sure if it is the right one, float and decimal
373		// generaly implicitly convert to other number types but here they are
374		// rejected.
375		match value {
376			Value::Number(Number::Int(i)) => Some(RecordIdKey::Number(i)),
377			Value::String(strand) => Some(RecordIdKey::String(strand)),
378			// NOTE: This was previously (before expr inversion pr) also rejected in this
379			// conversion, a bug I assume.
380			Value::Uuid(uuid) => Some(RecordIdKey::Uuid(uuid)),
381			Value::Array(array) => Some(RecordIdKey::Array(array)),
382			Value::Object(object) => Some(RecordIdKey::Object(object)),
383			Value::Range(range) => {
384				RecordIdKeyRange::from_value_range(*range).map(|x| RecordIdKey::Range(Box::new(x)))
385			}
386			_ => None,
387		}
388	}
389
390	/// Returns the expression which evaluates to the same value
391	pub fn into_literal(self) -> expr::RecordIdKeyLit {
392		match self {
393			RecordIdKey::Number(n) => expr::RecordIdKeyLit::Number(n),
394			RecordIdKey::String(s) => expr::RecordIdKeyLit::String(s),
395			RecordIdKey::Uuid(uuid) => expr::RecordIdKeyLit::Uuid(uuid),
396			RecordIdKey::Object(object) => expr::RecordIdKeyLit::Object(object.into_literal()),
397			RecordIdKey::Array(array) => expr::RecordIdKeyLit::Array(array.into_literal()),
398			RecordIdKey::Range(range) => {
399				expr::RecordIdKeyLit::Range(Box::new(range.into_literal()))
400			}
401		}
402	}
403}
404
405impl From<i64> for RecordIdKey {
406	fn from(value: i64) -> Self {
407		RecordIdKey::Number(value)
408	}
409}
410
411impl From<String> for RecordIdKey {
412	fn from(value: String) -> Self {
413		RecordIdKey::String(value.into())
414	}
415}
416
417impl From<Strand> for RecordIdKey {
418	fn from(value: Strand) -> Self {
419		RecordIdKey::String(value)
420	}
421}
422
423impl From<Uuid> for RecordIdKey {
424	fn from(value: Uuid) -> Self {
425		RecordIdKey::Uuid(value)
426	}
427}
428impl From<Object> for RecordIdKey {
429	fn from(value: Object) -> Self {
430		RecordIdKey::Object(value)
431	}
432}
433impl From<Array> for RecordIdKey {
434	fn from(value: Array) -> Self {
435		RecordIdKey::Array(value)
436	}
437}
438impl From<Box<RecordIdKeyRange>> for RecordIdKey {
439	fn from(value: Box<RecordIdKeyRange>) -> Self {
440		RecordIdKey::Range(value)
441	}
442}
443
444impl From<crate::types::PublicRecordIdKey> for RecordIdKey {
445	fn from(value: crate::types::PublicRecordIdKey) -> Self {
446		match value {
447			crate::types::PublicRecordIdKey::Number(x) => Self::Number(x),
448			crate::types::PublicRecordIdKey::String(x) => Self::String(x.into()),
449			crate::types::PublicRecordIdKey::Uuid(x) => Self::Uuid(x.into()),
450			crate::types::PublicRecordIdKey::Array(x) => Self::Array(x.into()),
451			crate::types::PublicRecordIdKey::Object(x) => Self::Object(x.into()),
452			crate::types::PublicRecordIdKey::Range(x) => Self::Range(Box::new((*x).into())),
453		}
454	}
455}
456
457impl TryFrom<RecordIdKey> for crate::types::PublicRecordIdKey {
458	type Error = anyhow::Error;
459
460	fn try_from(value: RecordIdKey) -> Result<Self, Self::Error> {
461		Ok(match value {
462			RecordIdKey::Number(x) => Self::Number(x),
463			RecordIdKey::String(x) => Self::String(x.into()),
464			RecordIdKey::Uuid(x) => Self::Uuid(x.into()),
465			RecordIdKey::Array(x) => Self::Array(x.try_into()?),
466			RecordIdKey::Object(x) => Self::Object(x.try_into()?),
467			RecordIdKey::Range(x) => Self::Range(Box::new((*x).try_into()?)),
468		})
469	}
470}
471
472impl PartialEq<Value> for RecordIdKey {
473	fn eq(&self, other: &Value) -> bool {
474		match self {
475			RecordIdKey::Number(a) => Value::Number(Number::Int(*a)) == *other,
476			RecordIdKey::String(a) => {
477				if let Value::String(b) = other {
478					a.as_str() == b.as_str()
479				} else {
480					false
481				}
482			}
483			RecordIdKey::Uuid(a) => {
484				if let Value::Uuid(b) = other {
485					a == b
486				} else {
487					false
488				}
489			}
490			RecordIdKey::Object(a) => {
491				if let Value::Object(b) = other {
492					a == b
493				} else {
494					false
495				}
496			}
497			RecordIdKey::Array(a) => {
498				if let Value::Array(b) = other {
499					a == b
500				} else {
501					false
502				}
503			}
504			RecordIdKey::Range(a) => {
505				if let Value::Range(b) = other {
506					**a == **b
507				} else {
508					false
509				}
510			}
511		}
512	}
513}
514
515impl ToSql for RecordIdKey {
516	fn fmt_sql(&self, f: &mut String, sql_fmt: SqlFormat) {
517		match self {
518			RecordIdKey::Number(n) => write_sql!(f, sql_fmt, "{n}"),
519			RecordIdKey::String(v) => write_sql!(f, sql_fmt, "{}", EscapeRidKey(v.as_str())),
520			RecordIdKey::Uuid(uuid) => write_sql!(f, sql_fmt, "{}", uuid),
521			RecordIdKey::Object(object) => write_sql!(f, sql_fmt, "{}", object),
522			RecordIdKey::Array(array) => write_sql!(f, sql_fmt, "{}", array),
523			RecordIdKey::Range(rid) => write_sql!(f, sql_fmt, "{}", rid),
524		}
525	}
526}
527
528#[revisioned(revision = 1)]
529#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Encode, BorrowDecode)]
530#[storekey(format = "()")]
531#[storekey(format = "IndexFormat")]
532pub struct RecordId {
533	pub table: TableName,
534	pub key: RecordIdKey,
535}
536
537impl RecordId {
538	/// Creates a new record id from the given table and key
539	pub fn new<K>(table: TableName, key: K) -> Self
540	where
541		RecordIdKey: From<K>,
542	{
543		RecordId {
544			table,
545			key: key.into(),
546		}
547	}
548
549	/// Turns the record id into a literal which resolves to the same value.
550	pub fn into_literal(self) -> expr::RecordIdLit {
551		expr::RecordIdLit {
552			table: self.table,
553			key: self.key.into_literal(),
554		}
555	}
556
557	pub fn is_table_type(&self, tables: &[TableName]) -> bool {
558		tables.is_empty() || tables.contains(&self.table)
559	}
560}
561
562impl TryFrom<RecordId> for crate::types::PublicRecordId {
563	type Error = anyhow::Error;
564
565	fn try_from(value: RecordId) -> Result<Self, Self::Error> {
566		Ok(crate::types::PublicRecordId {
567			table: value.table.into_public_table(),
568			key: value.key.try_into()?,
569		})
570	}
571}
572
573impl From<crate::types::PublicRecordId> for RecordId {
574	fn from(value: crate::types::PublicRecordId) -> Self {
575		RecordId {
576			table: value.table.into_table_name(),
577			key: RecordIdKey::from(value.key),
578		}
579	}
580}
581
582impl ToSql for RecordId {
583	fn fmt_sql(&self, f: &mut String, sql_fmt: SqlFormat) {
584		// The table half is an identifier, so it escapes like one: `EscapeRidKey`
585		// is for the key half, where an all-digit name must be quoted to keep it
586		// from reading as an integer, and where a reserved word or a leading
587		// digit is harmless. In table position both of those must be quoted or
588		// the record id cannot be read back.
589		write_sql!(f, sql_fmt, "{}:{}", EscapeIdent(self.table.as_str()), self.key)
590	}
591}
592
593#[cfg(test)]
594mod tests {
595	use super::*;
596
597	/// A record id must render so that it reads back as the same record id.
598	///
599	/// The table half was rendered with `EscapeRidKey`, which is the escaper
600	/// for the *key* half: it backticks an all-digit key so it cannot be
601	/// mistaken for an integer, but it checks neither reserved words nor a
602	/// leading digit, which is what a table name needs. So `select:1` and
603	/// `1a:1` were emitted, and neither parses back.
604	///
605	/// This is not only a display concern. Export writes `record.data.to_sql()`
606	/// into `INSERT` statements, so a dump of such a table could not be
607	/// re-imported.
608	#[test]
609	fn record_ids_render_so_they_can_be_read_back() {
610		for name in ["select", "1a", "9lives", "table", "person", "_123", "a-b"] {
611			let rid = RecordId {
612				table: TableName::from(name),
613				key: RecordIdKey::Number(1),
614			};
615			let rendered = rid.to_sql();
616			let reparsed = crate::syn::expr(&rendered)
617				.unwrap_or_else(|e| panic!("{rendered} does not parse back: {e}"));
618			let crate::sql::Expr::Literal(crate::sql::Literal::RecordId(lit)) = &reparsed else {
619				panic!("{rendered} parsed as {reparsed:?}, not a record id");
620			};
621			assert_eq!(lit.table.as_str(), name, "{rendered} round-tripped to a different table");
622		}
623	}
624
625	/// `IndexFormat` erases a number's [`NumberKind`], so a compound record id
626	/// does not survive a round trip through it: every nested `Int`/`Float`
627	/// decodes back as a `Decimal`, and two ids differing only in numeric
628	/// variant encode to the same bytes.
629	///
630	/// That is intended for an ordinary index key, which is only ever compared
631	/// and ordered. It is not safe for a key whose id component is decoded back
632	/// into a record identity and used to fetch the record: the fetch re-encodes
633	/// the id in the default format, which *does* carry the kind, so the
634	/// decimalised id addresses a key that no record was stored under.
635	///
636	/// The assertions are on encoded bytes rather than on `==`, because
637	/// [`Number`]'s equality is approximate across variants — `Float(1.5)`
638	/// compares equal to `Decimal(1.5)` while their record keys differ.
639	#[test]
640	fn index_format_does_not_round_trip_a_compound_record_id() {
641		fn arr(n: Number) -> RecordIdKey {
642			RecordIdKey::Array(Array::from(vec![Value::Number(n)]))
643		}
644		/// The bytes a record is actually stored and fetched under.
645		fn record_key(key: &RecordIdKey) -> Vec<u8> {
646			storekey::encode_vec(key).unwrap()
647		}
648		/// The id as it comes back out of an `IndexFormat` key.
649		fn through_index_format(key: &RecordIdKey) -> RecordIdKey {
650			let enc = storekey::encode_vec_format::<IndexFormat, _>(key).unwrap();
651			storekey::decode_borrow_format::<IndexFormat, _>(&enc).unwrap()
652		}
653
654		let int = arr(Number::Int(1));
655		let float = arr(Number::Float(1.5));
656		let dec = arr(Number::Decimal(rust_decimal::Decimal::from(1)));
657
658		// An Int comes back as a Decimal, and so addresses a different record key.
659		assert!(matches!(through_index_format(&int), RecordIdKey::Array(ref a)
660			if matches!(a.first(), Some(Value::Number(Number::Decimal(_))))));
661		assert_ne!(
662			record_key(&through_index_format(&int)),
663			record_key(&int),
664			"`[1]` does not address its own record after an IndexFormat round trip"
665		);
666		assert_ne!(
667			record_key(&through_index_format(&float)),
668			record_key(&float),
669			"`[1.5f]` does not address its own record after an IndexFormat round trip"
670		);
671		// A Decimal is the fixed point, which is why only decimal ids survive today.
672		assert_eq!(record_key(&through_index_format(&dec)), record_key(&dec));
673
674		// The encoding is also not injective: `[1]` and `[1dec]` are distinct
675		// records that collide on one key.
676		assert_ne!(record_key(&int), record_key(&dec), "they are distinct records");
677		assert_eq!(
678			storekey::encode_vec_format::<IndexFormat, _>(&int).unwrap(),
679			storekey::encode_vec_format::<IndexFormat, _>(&dec).unwrap(),
680			"`[1]` and `[1dec]` encode to the same IndexFormat key"
681		);
682
683		// The default format carries the kind, so it does round trip.
684		let back: RecordIdKey = storekey::decode_borrow(&record_key(&int)).unwrap();
685		assert_eq!(record_key(&back), record_key(&int));
686	}
687
688	/// [`RecordIdentity`] is what a key uses instead, and it holds the property
689	/// the bare type does not: it round trips and stays injective even when the
690	/// enclosing key pins [`IndexFormat`].
691	#[test]
692	fn record_identity_round_trips_under_index_format() {
693		fn arr(n: Number) -> RecordIdKey {
694			RecordIdKey::Array(Array::from(vec![Value::Number(n)]))
695		}
696		fn through_index_format(key: &RecordIdKey) -> RecordIdKey {
697			let id = RecordIdentity(key.clone());
698			let enc = storekey::encode_vec_format::<IndexFormat, _>(&id).unwrap();
699			storekey::decode_borrow_format::<IndexFormat, RecordIdentity>(&enc)
700				.unwrap()
701				.into_inner()
702		}
703
704		let int = arr(Number::Int(1));
705		let float = arr(Number::Float(1.5));
706		let dec = arr(Number::Decimal(rust_decimal::Decimal::from(1)));
707		let nested = RecordIdKey::Object(
708			[("k".to_owned(), Value::Number(Number::Int(2)))].into_iter().collect(),
709		);
710
711		for id in [&int, &float, &dec, &nested] {
712			assert_eq!(
713				storekey::encode_vec(&through_index_format(id)).unwrap(),
714				storekey::encode_vec(id).unwrap(),
715				"{id:?} must address its own record after the round trip"
716			);
717		}
718
719		// And distinct records stay on distinct keys.
720		assert_ne!(
721			storekey::encode_vec_format::<IndexFormat, _>(&RecordIdentity(int)).unwrap(),
722			storekey::encode_vec_format::<IndexFormat, _>(&RecordIdentity(dec)).unwrap(),
723			"`[1]` and `[1dec]` must not share a key"
724		);
725	}
726}
727
728surrealdb_kvs::impl_kv_value_revisioned!(RecordId);
729surrealdb_kvs::impl_kv_value_revisioned!(RecordIdKey);
730
731#[cfg(test)]
732mod record_identity_comparison_tests {
733	use std::collections::HashSet;
734
735	use super::*;
736
737	fn identity(n: Number) -> RecordIdentity {
738		RecordIdentity(RecordIdKey::Array(vec![Value::Number(n)].into()))
739	}
740
741	/// The type exists to keep two ids apart that [`RecordIdKey`] itself calls
742	/// equal, so its own comparison must not inherit that. Deriving it does,
743	/// which would put both records in one slot of any set or map keyed by it.
744	#[test]
745	fn identities_differing_only_in_numeric_variant_are_not_equal() {
746		let int = identity(Number::Int(1));
747		let dec = identity(Number::Decimal(1.into()));
748
749		assert_ne!(int, dec, "the identities must not collapse");
750		assert_ne!(
751			storekey::encode_vec(&int.0).unwrap(),
752			storekey::encode_vec(&dec.0).unwrap(),
753			"...because they are stored under different keys"
754		);
755		assert_eq!(
756			HashSet::from([int.clone(), dec.clone()]).len(),
757			2,
758			"and a set keyed by identity must hold both"
759		);
760		assert_ne!(int.cmp(&dec), std::cmp::Ordering::Equal, "ordering must agree");
761		assert_eq!(int, int.clone(), "an identity still equals itself");
762	}
763}