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,
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
209pub trait RowField: LeBytes {
210	const VALUE_TYPE: ValueType;
211}
212
213macro_rules! row_field {
214	($($ty:ty => $variant:ident),* $(,)?) => {
215		$(
216			impl RowField for $ty {
217				const VALUE_TYPE: ValueType = ValueType::$variant;
218			}
219		)*
220	};
221}
222
223row_field!(
224	bool => Boolean,
225	f32 => Float4,
226	f64 => Float8,
227	i8 => Int1,
228	i16 => Int2,
229	i32 => Int4,
230	i64 => Int8,
231	i128 => Int16,
232	u8 => Uint1,
233	u16 => Uint2,
234	u32 => Uint4,
235	u64 => Uint8,
236	u128 => Uint16,
237	Date => Date,
238	DateTime => DateTime,
239	Time => Time,
240	Duration => Duration,
241	Uuid4 => Uuid4,
242	Uuid7 => Uuid7,
243	IdentityId => IdentityId,
244);
245
246#[cfg(test)]
247mod tests {
248	use super::*;
249
250	#[test]
251	fn every_row_field_declares_the_value_type_its_own_bytes_fill() {
252		// `set::<T>` picks the slot from RowField, so a T mapped to the wrong ValueType writes
253		// into a wrongly sized slot. The slot-type check only runs under reifydb_assertions, so
254		// in release that corruption is silent and this is the only guard against it.
255		fn agree<T: RowField>() {
256			assert_eq!(
257				T::VALUE_TYPE.size(),
258				T::ENCODED_SIZE,
259				"{:?} declares a {}-byte slot but its LeBytes form is {} bytes",
260				T::VALUE_TYPE,
261				T::VALUE_TYPE.size(),
262				T::ENCODED_SIZE
263			);
264		}
265
266		agree::<bool>();
267		agree::<f32>();
268		agree::<f64>();
269		agree::<i8>();
270		agree::<i16>();
271		agree::<i32>();
272		agree::<i64>();
273		agree::<i128>();
274		agree::<u8>();
275		agree::<u16>();
276		agree::<u32>();
277		agree::<u64>();
278		agree::<u128>();
279		agree::<Date>();
280		agree::<DateTime>();
281		agree::<Time>();
282		agree::<Duration>();
283		agree::<Uuid4>();
284		agree::<Uuid7>();
285		agree::<IdentityId>();
286	}
287
288	#[test]
289	fn every_width_is_the_size_of_its_own_byte_array() {
290		// ENCODED_SIZE is derived from the byte array rather than declared per impl, so widening
291		// a type is one change and no layout can be left reading the old width.
292		assert_eq!(<u8 as LeBytes>::ENCODED_SIZE, 1);
293		assert_eq!(<bool as LeBytes>::ENCODED_SIZE, 1);
294		assert_eq!(<Date as LeBytes>::ENCODED_SIZE, 4);
295		assert_eq!(<f32 as LeBytes>::ENCODED_SIZE, 4);
296		assert_eq!(<DateTime as LeBytes>::ENCODED_SIZE, 8);
297		assert_eq!(<Time as LeBytes>::ENCODED_SIZE, 8);
298		assert_eq!(<RowNumber as LeBytes>::ENCODED_SIZE, 8);
299		assert_eq!(<Duration as LeBytes>::ENCODED_SIZE, 16);
300		assert_eq!(<Partition as LeBytes>::ENCODED_SIZE, 16);
301		assert_eq!(<Uuid4 as LeBytes>::ENCODED_SIZE, 16);
302		assert_eq!(<Uuid7 as LeBytes>::ENCODED_SIZE, 16);
303		assert_eq!(<IdentityId as LeBytes>::ENCODED_SIZE, 16);
304	}
305
306	#[test]
307	fn byte_order_is_little_endian_regardless_of_host() {
308		// A native-endian store reads back fine on the writing host and wrong everywhere else,
309		// which no round trip can see, so these pin the bytes. Caveat: on a little-endian host
310		// to_ne_bytes IS to_le_bytes, so a green run is not proof no impl reaches for native.
311		assert_eq!(0x0102_0304_0506_0708u64.to_le_bytes(), [8, 7, 6, 5, 4, 3, 2, 1]);
312		assert_eq!(
313			LeBytes::to_le_bytes(&DateTime::from_nanos(0x0102_0304_0506_0708)),
314			[8, 7, 6, 5, 4, 3, 2, 1]
315		);
316		assert_eq!(LeBytes::to_le_bytes(&RowNumber(0x0102_0304_0506_0708)), [8, 7, 6, 5, 4, 3, 2, 1]);
317		assert_eq!(LeBytes::to_le_bytes(&0x0102_0304i32), [4, 3, 2, 1]);
318	}
319
320	#[test]
321	fn every_implementor_round_trips_through_its_bytes() {
322		// Multi-field types like Duration only round-trip if every component sits at the offset
323		// the reader expects.
324		assert_eq!(bool::from_le_bytes(LeBytes::to_le_bytes(&true)), true);
325		assert_eq!(bool::from_le_bytes(LeBytes::to_le_bytes(&false)), false);
326
327		let dt = DateTime::from_nanos(1_700_000_123_456_789);
328		assert_eq!(DateTime::from_le_bytes(LeBytes::to_le_bytes(&dt)), dt);
329
330		let duration = Duration::new(13, 7, 1_234_567_890).unwrap();
331		assert_eq!(Duration::from_le_bytes(LeBytes::to_le_bytes(&duration)), duration);
332
333		let date = Date::from_days_since_epoch(19_000).unwrap();
334		assert_eq!(Date::from_le_bytes(LeBytes::to_le_bytes(&date)), date);
335
336		let time = Time::from_nanos_since_midnight(86_399_999_999_999).unwrap();
337		assert_eq!(Time::from_le_bytes(LeBytes::to_le_bytes(&time)), time);
338
339		let partition = Partition(0xdead_beef_cafe_babe_0123_4567_89ab_cdef);
340		assert_eq!(Partition::from_le_bytes(LeBytes::to_le_bytes(&partition)), partition);
341	}
342
343	#[test]
344	fn the_slice_helpers_agree_with_the_array_form() {
345		// The codec calls write_le/read_le against a slot at an offset, so they must produce the
346		// same bytes as the array form and must not reach outside the slot.
347		let mut buf = [0u8; 32];
348		let dt = DateTime::from_nanos(1_700_000_123_456_789);
349
350		dt.write_le(&mut buf[8..]);
351		assert_eq!(&buf[8..16], LeBytes::to_le_bytes(&dt).as_ref());
352		assert_eq!(DateTime::read_le(&buf[8..]), dt);
353		assert!(buf[0..8].iter().all(|b| *b == 0), "write_le must not reach before its slot");
354		assert!(buf[16..].iter().all(|b| *b == 0), "write_le must not reach past its slot");
355	}
356}