Skip to main content

reifydb_value/
encoding.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use uuid::Uuid;
5
6use crate::value::{
7	date::Date,
8	datetime::DateTime,
9	duration::Duration,
10	identity::{IdentityId, IdentityKind},
11	partition::Partition,
12	row_number::RowNumber,
13	time::Time,
14	uuid::{Uuid4, Uuid7},
15	value_type::ValueType,
16};
17
18pub trait LeBytes: Sized {
19	type Bytes: AsRef<[u8]> + AsMut<[u8]> + Default + Copy;
20
21	const ENCODED_SIZE: usize = size_of::<Self::Bytes>();
22
23	fn to_le_bytes(&self) -> Self::Bytes;
24
25	fn from_le_bytes(bytes: Self::Bytes) -> Self;
26
27	#[inline]
28	fn write_le(&self, dst: &mut [u8]) {
29		dst[..Self::ENCODED_SIZE].copy_from_slice(self.to_le_bytes().as_ref());
30	}
31
32	#[inline]
33	fn read_le(src: &[u8]) -> Self {
34		let mut buf = Self::Bytes::default();
35		buf.as_mut().copy_from_slice(&src[..Self::ENCODED_SIZE]);
36		Self::from_le_bytes(buf)
37	}
38}
39
40macro_rules! le_bytes_for_primitive {
41	($($ty:ty),* $(,)?) => {
42		$(
43			impl LeBytes for $ty {
44				type Bytes = [u8; size_of::<$ty>()];
45
46				#[inline]
47				fn to_le_bytes(&self) -> Self::Bytes {
48					<$ty>::to_le_bytes(*self)
49				}
50
51				#[inline]
52				fn from_le_bytes(bytes: Self::Bytes) -> Self {
53					<$ty>::from_le_bytes(bytes)
54				}
55			}
56		)*
57	};
58}
59
60le_bytes_for_primitive!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, f64);
61
62impl LeBytes for bool {
63	type Bytes = [u8; 1];
64
65	#[inline]
66	fn to_le_bytes(&self) -> Self::Bytes {
67		[*self as u8]
68	}
69
70	#[inline]
71	fn from_le_bytes(bytes: Self::Bytes) -> Self {
72		bytes[0] != 0
73	}
74}
75
76impl LeBytes for DateTime {
77	type Bytes = [u8; 8];
78
79	#[inline]
80	fn to_le_bytes(&self) -> Self::Bytes {
81		self.to_nanos().to_le_bytes()
82	}
83
84	#[inline]
85	fn from_le_bytes(bytes: Self::Bytes) -> Self {
86		DateTime::from_nanos(u64::from_le_bytes(bytes))
87	}
88}
89
90impl LeBytes for Date {
91	type Bytes = [u8; 4];
92
93	#[inline]
94	fn to_le_bytes(&self) -> Self::Bytes {
95		self.to_days_since_epoch().to_le_bytes()
96	}
97
98	#[inline]
99	fn from_le_bytes(bytes: Self::Bytes) -> Self {
100		Date::from_days_since_epoch(i32::from_le_bytes(bytes)).expect("stored date must be valid")
101	}
102}
103
104impl LeBytes for Time {
105	type Bytes = [u8; 8];
106
107	#[inline]
108	fn to_le_bytes(&self) -> Self::Bytes {
109		self.to_nanos_since_midnight().to_le_bytes()
110	}
111
112	#[inline]
113	fn from_le_bytes(bytes: Self::Bytes) -> Self {
114		Time::from_nanos_since_midnight(u64::from_le_bytes(bytes)).expect("stored time must be valid")
115	}
116}
117
118impl LeBytes for Duration {
119	type Bytes = [u8; 16];
120
121	#[inline]
122	fn to_le_bytes(&self) -> Self::Bytes {
123		let mut out = [0u8; 16];
124		out[0..4].copy_from_slice(&self.get_months().to_le_bytes());
125		out[4..8].copy_from_slice(&self.get_days().to_le_bytes());
126		out[8..16].copy_from_slice(&self.get_nanos().to_le_bytes());
127		out
128	}
129
130	#[inline]
131	fn from_le_bytes(bytes: Self::Bytes) -> Self {
132		let months = i32::from_le_bytes(bytes[0..4].try_into().unwrap());
133		let days = i32::from_le_bytes(bytes[4..8].try_into().unwrap());
134		let nanos = i64::from_le_bytes(bytes[8..16].try_into().unwrap());
135		Duration::new(months, days, nanos).expect("stored duration must be valid")
136	}
137}
138
139impl LeBytes for RowNumber {
140	type Bytes = [u8; 8];
141
142	#[inline]
143	fn to_le_bytes(&self) -> Self::Bytes {
144		self.0.to_le_bytes()
145	}
146
147	#[inline]
148	fn from_le_bytes(bytes: Self::Bytes) -> Self {
149		RowNumber(u64::from_le_bytes(bytes))
150	}
151}
152
153impl LeBytes for Partition {
154	type Bytes = [u8; 16];
155
156	#[inline]
157	fn to_le_bytes(&self) -> Self::Bytes {
158		self.0.to_le_bytes()
159	}
160
161	#[inline]
162	fn from_le_bytes(bytes: Self::Bytes) -> Self {
163		Partition(u128::from_le_bytes(bytes))
164	}
165}
166
167impl LeBytes for Uuid4 {
168	type Bytes = [u8; 16];
169
170	#[inline]
171	fn to_le_bytes(&self) -> Self::Bytes {
172		*self.0.as_bytes()
173	}
174
175	#[inline]
176	fn from_le_bytes(bytes: Self::Bytes) -> Self {
177		Uuid4(Uuid::from_bytes(bytes))
178	}
179}
180
181impl LeBytes for Uuid7 {
182	type Bytes = [u8; 16];
183
184	#[inline]
185	fn to_le_bytes(&self) -> Self::Bytes {
186		*self.0.as_bytes()
187	}
188
189	#[inline]
190	fn from_le_bytes(bytes: Self::Bytes) -> Self {
191		Uuid7(Uuid::from_bytes(bytes))
192	}
193}
194
195impl LeBytes for IdentityId {
196	type Bytes = [u8; 16];
197
198	#[inline]
199	fn to_le_bytes(&self) -> Self::Bytes {
200		*self.0.0.as_bytes()
201	}
202
203	#[inline]
204	fn from_le_bytes(bytes: Self::Bytes) -> Self {
205		IdentityId(Uuid7(Uuid::from_bytes(bytes)))
206	}
207}
208
209impl LeBytes for IdentityKind {
210	type Bytes = [u8; 1];
211
212	#[inline]
213	fn to_le_bytes(&self) -> Self::Bytes {
214		[self.to_u8()]
215	}
216
217	#[inline]
218	fn from_le_bytes(bytes: Self::Bytes) -> Self {
219		IdentityKind::from_u8(bytes[0])
220	}
221}
222
223pub trait RowField: LeBytes {
224	const VALUE_TYPE: ValueType;
225}
226
227macro_rules! row_field {
228	($($ty:ty => $variant:ident),* $(,)?) => {
229		$(
230			impl RowField for $ty {
231				const VALUE_TYPE: ValueType = ValueType::$variant;
232			}
233		)*
234	};
235}
236
237row_field!(
238	bool => Boolean,
239	f32 => Float4,
240	f64 => Float8,
241	i8 => Int1,
242	i16 => Int2,
243	i32 => Int4,
244	i64 => Int8,
245	i128 => Int16,
246	u8 => Uint1,
247	u16 => Uint2,
248	u32 => Uint4,
249	u64 => Uint8,
250	u128 => Uint16,
251	Date => Date,
252	DateTime => DateTime,
253	Time => Time,
254	Duration => Duration,
255	Uuid4 => Uuid4,
256	Uuid7 => Uuid7,
257	IdentityId => IdentityId,
258	IdentityKind => Uint1,
259);
260
261#[cfg(test)]
262mod tests {
263	use super::*;
264
265	#[test]
266	fn every_row_field_declares_the_value_type_its_own_bytes_fill() {
267		// `set::<T>` picks the slot from RowField, so a T mapped to the wrong ValueType writes
268		// into a wrongly sized slot. The slot-type check only runs under reifydb_assertions, so
269		// in release that corruption is silent and this is the only guard against it.
270		fn agree<T: RowField>() {
271			assert_eq!(
272				T::VALUE_TYPE.size(),
273				T::ENCODED_SIZE,
274				"{:?} declares a {}-byte slot but its LeBytes form is {} bytes",
275				T::VALUE_TYPE,
276				T::VALUE_TYPE.size(),
277				T::ENCODED_SIZE
278			);
279		}
280
281		agree::<bool>();
282		agree::<f32>();
283		agree::<f64>();
284		agree::<i8>();
285		agree::<i16>();
286		agree::<i32>();
287		agree::<i64>();
288		agree::<i128>();
289		agree::<u8>();
290		agree::<u16>();
291		agree::<u32>();
292		agree::<u64>();
293		agree::<u128>();
294		agree::<Date>();
295		agree::<DateTime>();
296		agree::<Time>();
297		agree::<Duration>();
298		agree::<Uuid4>();
299		agree::<Uuid7>();
300		agree::<IdentityId>();
301	}
302
303	#[test]
304	fn every_width_is_the_size_of_its_own_byte_array() {
305		// ENCODED_SIZE is derived from the byte array rather than declared per impl, so widening
306		// a type is one change and no layout can be left reading the old width.
307		assert_eq!(<u8 as LeBytes>::ENCODED_SIZE, 1);
308		assert_eq!(<bool as LeBytes>::ENCODED_SIZE, 1);
309		assert_eq!(<Date as LeBytes>::ENCODED_SIZE, 4);
310		assert_eq!(<f32 as LeBytes>::ENCODED_SIZE, 4);
311		assert_eq!(<DateTime as LeBytes>::ENCODED_SIZE, 8);
312		assert_eq!(<Time as LeBytes>::ENCODED_SIZE, 8);
313		assert_eq!(<RowNumber as LeBytes>::ENCODED_SIZE, 8);
314		assert_eq!(<Duration as LeBytes>::ENCODED_SIZE, 16);
315		assert_eq!(<Partition as LeBytes>::ENCODED_SIZE, 16);
316		assert_eq!(<Uuid4 as LeBytes>::ENCODED_SIZE, 16);
317		assert_eq!(<Uuid7 as LeBytes>::ENCODED_SIZE, 16);
318		assert_eq!(<IdentityId as LeBytes>::ENCODED_SIZE, 16);
319	}
320
321	#[test]
322	fn byte_order_is_little_endian_regardless_of_host() {
323		// A native-endian store reads back fine on the writing host and wrong everywhere else,
324		// which no round trip can see, so these pin the bytes. Caveat: on a little-endian host
325		// to_ne_bytes IS to_le_bytes, so a green run is not proof no impl reaches for native.
326		assert_eq!(0x0102_0304_0506_0708u64.to_le_bytes(), [8, 7, 6, 5, 4, 3, 2, 1]);
327		assert_eq!(
328			LeBytes::to_le_bytes(&DateTime::from_nanos(0x0102_0304_0506_0708)),
329			[8, 7, 6, 5, 4, 3, 2, 1]
330		);
331		assert_eq!(LeBytes::to_le_bytes(&RowNumber(0x0102_0304_0506_0708)), [8, 7, 6, 5, 4, 3, 2, 1]);
332		assert_eq!(LeBytes::to_le_bytes(&0x0102_0304i32), [4, 3, 2, 1]);
333	}
334
335	#[test]
336	fn every_implementor_round_trips_through_its_bytes() {
337		// Multi-field types like Duration only round-trip if every component sits at the offset
338		// the reader expects.
339		assert_eq!(bool::from_le_bytes(LeBytes::to_le_bytes(&true)), true);
340		assert_eq!(bool::from_le_bytes(LeBytes::to_le_bytes(&false)), false);
341
342		let dt = DateTime::from_nanos(1_700_000_123_456_789);
343		assert_eq!(DateTime::from_le_bytes(LeBytes::to_le_bytes(&dt)), dt);
344
345		let duration = Duration::new(13, 7, 1_234_567_890).unwrap();
346		assert_eq!(Duration::from_le_bytes(LeBytes::to_le_bytes(&duration)), duration);
347
348		let date = Date::from_days_since_epoch(19_000).unwrap();
349		assert_eq!(Date::from_le_bytes(LeBytes::to_le_bytes(&date)), date);
350
351		let time = Time::from_nanos_since_midnight(86_399_999_999_999).unwrap();
352		assert_eq!(Time::from_le_bytes(LeBytes::to_le_bytes(&time)), time);
353
354		let partition = Partition(0xdead_beef_cafe_babe_0123_4567_89ab_cdef);
355		assert_eq!(Partition::from_le_bytes(LeBytes::to_le_bytes(&partition)), partition);
356	}
357
358	#[test]
359	fn the_slice_helpers_agree_with_the_array_form() {
360		// The codec calls write_le/read_le against a slot at an offset, so they must produce the
361		// same bytes as the array form and must not reach outside the slot.
362		let mut buf = [0u8; 32];
363		let dt = DateTime::from_nanos(1_700_000_123_456_789);
364
365		dt.write_le(&mut buf[8..]);
366		assert_eq!(&buf[8..16], LeBytes::to_le_bytes(&dt).as_ref());
367		assert_eq!(DateTime::read_le(&buf[8..]), dt);
368		assert!(buf[0..8].iter().all(|b| *b == 0), "write_le must not reach before its slot");
369		assert!(buf[16..].iter().all(|b| *b == 0), "write_le must not reach past its slot");
370	}
371}