Skip to main content

reifydb_codec/row/
bytes.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::ops::Deref;
5
6use reifydb_value::{
7	encoding::LeBytes,
8	util::cowvec::CowVec,
9	value::{datetime::DateTime, row_number::RowNumber},
10};
11use serde::{Deserialize, Serialize};
12
13use crate::row::shape::fingerprint::RowShapeFingerprint;
14
15const FINGERPRINT_SIZE: usize = 8;
16const CREATED_AT_OFFSET: usize = FINGERPRINT_SIZE;
17const UPDATED_AT_OFFSET: usize = CREATED_AT_OFFSET + DateTime::ENCODED_SIZE;
18const TIME_OFFSET: usize = UPDATED_AT_OFFSET + DateTime::ENCODED_SIZE;
19const FLAGS_OFFSET: usize = TIME_OFFSET + DateTime::ENCODED_SIZE;
20
21pub const SHAPE_HEADER_SIZE: usize = FLAGS_OFFSET + 1;
22
23pub const CATALOG_HEADER_SIZE: usize = FINGERPRINT_SIZE;
24
25const NOT_BEFORE_OFFSET: usize = SHAPE_HEADER_SIZE;
26
27pub const QUEUE_HEADER_SIZE: usize = NOT_BEFORE_OFFSET + DateTime::ENCODED_SIZE;
28
29const OUTCOME_OFFSET: usize = SHAPE_HEADER_SIZE;
30const LOST_OFFSET: usize = OUTCOME_OFFSET + 1;
31const FINISHED_AT_OFFSET: usize = LOST_OFFSET + 1;
32
33pub const QUEUE_ATTEMPT_HEADER_SIZE: usize = FINISHED_AT_OFFSET + DateTime::ENCODED_SIZE;
34
35const DEDUPLICATION_ROW_NUMBER_OFFSET: usize = SHAPE_HEADER_SIZE;
36const EXPIRES_AT_OFFSET: usize = DEDUPLICATION_ROW_NUMBER_OFFSET + RowNumber::ENCODED_SIZE;
37
38pub const QUEUE_DEDUPLICATION_HEADER_SIZE: usize = EXPIRES_AT_OFFSET + DateTime::ENCODED_SIZE;
39
40const HAS_TIME: u8 = 1 << 0;
41
42const HAS_NOT_BEFORE: u8 = 1 << 1;
43
44pub type EncodedBytesIter = Box<dyn EncodedBytesIterator>;
45
46pub trait EncodedBytesIterator: Iterator<Item = EncodedBytes> {}
47
48impl<I: Iterator<Item = EncodedBytes>> EncodedBytesIterator for I {}
49
50#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
51pub struct EncodedBytes(pub CowVec<u8>);
52
53impl AsRef<[u8]> for EncodedBytes {
54	fn as_ref(&self) -> &[u8] {
55		&self.0
56	}
57}
58
59impl Deref for EncodedBytes {
60	type Target = CowVec<u8>;
61
62	fn deref(&self) -> &Self::Target {
63		&self.0
64	}
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub(crate) struct EncodedRowBuilder(Vec<u8>);
69
70impl EncodedRowBuilder {
71	pub(crate) fn zeroed(size: usize) -> Self {
72		Self(vec![0u8; size])
73	}
74
75	pub(crate) fn freeze(self) -> EncodedBytes {
76		EncodedBytes(CowVec::new(self.0))
77	}
78}
79
80impl sealed::Sealed for EncodedRowBuilder {
81	fn buffer(&self) -> &Vec<u8> {
82		&self.0
83	}
84
85	fn buffer_mut(&mut self) -> &mut Vec<u8> {
86		&mut self.0
87	}
88
89	fn take_buffer(self) -> Vec<u8> {
90		self.0
91	}
92}
93
94pub(crate) mod sealed {
95	use std::ops::Range;
96
97	pub trait Sealed {
98		fn buffer(&self) -> &Vec<u8>;
99
100		fn buffer_mut(&mut self) -> &mut Vec<u8>;
101
102		fn take_buffer(self) -> Vec<u8>
103		where
104			Self: Sized;
105
106		#[inline]
107		fn set_valid_at(&mut self, header_size: usize, index: usize, valid: bool) {
108			let byte = header_size + index / 8;
109			let bit = index % 8;
110			let buffer = self.buffer_mut();
111			if valid {
112				buffer[byte] |= 1 << bit;
113			} else {
114				buffer[byte] &= !(1 << bit);
115			}
116		}
117
118		#[inline]
119		fn splice(&mut self, range: Range<usize>, data: impl IntoIterator<Item = u8>) {
120			self.buffer_mut().splice(range, data);
121		}
122	}
123}
124
125pub trait RowBuilder: sealed::Sealed {
126	fn as_slice(&self) -> &[u8];
127
128	fn as_mut_slice(&mut self) -> &mut [u8];
129
130	fn len(&self) -> usize;
131
132	fn is_empty(&self) -> bool;
133
134	fn extend_from_slice(&mut self, bytes: &[u8]);
135
136	fn freeze_bytes(self) -> EncodedBytes
137	where
138		Self: Sized;
139}
140
141impl<T: sealed::Sealed> RowBuilder for T {
142	#[inline]
143	fn as_slice(&self) -> &[u8] {
144		self.buffer()
145	}
146
147	#[inline]
148	fn as_mut_slice(&mut self) -> &mut [u8] {
149		self.buffer_mut()
150	}
151
152	#[inline]
153	fn len(&self) -> usize {
154		self.buffer().len()
155	}
156
157	#[inline]
158	fn is_empty(&self) -> bool {
159		self.buffer().is_empty()
160	}
161
162	#[inline]
163	fn extend_from_slice(&mut self, bytes: &[u8]) {
164		self.buffer_mut().extend_from_slice(bytes);
165	}
166
167	#[inline]
168	fn freeze_bytes(self) -> EncodedBytes {
169		EncodedBytes(CowVec::new(self.take_buffer()))
170	}
171}
172
173pub trait SourceRowBuilder: RowBuilder + Sized {
174	fn set_timestamps(&mut self, created_at: DateTime, updated_at: DateTime);
175
176	fn set_time(&mut self, time: DateTime);
177}
178
179impl Deref for EncodedRowBuilder {
180	type Target = [u8];
181
182	fn deref(&self) -> &Self::Target {
183		&self.0
184	}
185}
186
187#[inline]
188pub fn write_fingerprint(buf: &mut [u8], fingerprint: RowShapeFingerprint) {
189	buf[0..FINGERPRINT_SIZE].copy_from_slice(&fingerprint.to_le_bytes());
190}
191
192#[inline]
193pub fn write_timestamps(buf: &mut [u8], created_at: DateTime, updated_at: DateTime) {
194	buf[CREATED_AT_OFFSET..CREATED_AT_OFFSET + DateTime::ENCODED_SIZE].copy_from_slice(&created_at.to_le_bytes());
195	buf[UPDATED_AT_OFFSET..UPDATED_AT_OFFSET + DateTime::ENCODED_SIZE].copy_from_slice(&updated_at.to_le_bytes());
196}
197
198#[inline]
199pub fn write_storage_time(buf: &mut [u8], time: DateTime) {
200	buf[TIME_OFFSET..TIME_OFFSET + DateTime::ENCODED_SIZE].copy_from_slice(&time.to_le_bytes());
201	buf[FLAGS_OFFSET] |= HAS_TIME;
202}
203
204#[inline]
205pub fn write_not_before(buf: &mut [u8], not_before: DateTime) {
206	buf[NOT_BEFORE_OFFSET..NOT_BEFORE_OFFSET + DateTime::ENCODED_SIZE].copy_from_slice(&not_before.to_le_bytes());
207	buf[FLAGS_OFFSET] |= HAS_NOT_BEFORE;
208}
209
210#[inline]
211pub fn write_outcome(buf: &mut [u8], outcome: u8) {
212	buf[OUTCOME_OFFSET] = outcome;
213}
214
215#[inline]
216pub fn write_lost(buf: &mut [u8], lost: bool) {
217	buf[LOST_OFFSET] = lost as u8;
218}
219
220#[inline]
221pub fn write_finished_at(buf: &mut [u8], finished_at: DateTime) {
222	buf[FINISHED_AT_OFFSET..FINISHED_AT_OFFSET + DateTime::ENCODED_SIZE]
223		.copy_from_slice(&finished_at.to_le_bytes());
224}
225
226#[inline]
227pub fn write_deduplication_row_number(buf: &mut [u8], row_number: RowNumber) {
228	buf[DEDUPLICATION_ROW_NUMBER_OFFSET..DEDUPLICATION_ROW_NUMBER_OFFSET + RowNumber::ENCODED_SIZE]
229		.copy_from_slice(&row_number.to_le_bytes());
230}
231
232#[inline]
233pub fn write_expires_at(buf: &mut [u8], expires_at: DateTime) {
234	buf[EXPIRES_AT_OFFSET..EXPIRES_AT_OFFSET + DateTime::ENCODED_SIZE].copy_from_slice(&expires_at.to_le_bytes());
235}
236
237#[inline]
238pub fn read_deduplication_row_number(buf: &[u8]) -> RowNumber {
239	RowNumber::read_le(&buf[DEDUPLICATION_ROW_NUMBER_OFFSET..])
240}
241
242#[inline]
243pub fn read_outcome(buf: &[u8]) -> u8 {
244	buf[OUTCOME_OFFSET]
245}
246
247#[inline]
248pub fn read_lost(buf: &[u8]) -> bool {
249	buf[LOST_OFFSET] != 0
250}
251
252#[inline]
253pub fn read_defined_at(buf: &[u8], header_size: usize, index: usize) -> bool {
254	let byte = header_size + index / 8;
255	let bit = index % 8;
256	(buf[byte] & (1 << bit)) != 0
257}
258
259#[inline]
260pub fn read_fingerprint(buf: &[u8]) -> RowShapeFingerprint {
261	let bytes: [u8; FINGERPRINT_SIZE] = buf[0..FINGERPRINT_SIZE].try_into().unwrap();
262	RowShapeFingerprint::from_le_bytes(bytes)
263}
264
265#[inline]
266fn read_stamp(buf: &[u8], offset: usize) -> DateTime {
267	DateTime::from_le_bytes(buf[offset..offset + DateTime::ENCODED_SIZE].try_into().unwrap())
268}
269
270#[inline]
271fn read_time(buf: &[u8]) -> Option<DateTime> {
272	(buf[FLAGS_OFFSET] & HAS_TIME != 0).then(|| read_stamp(buf, TIME_OFFSET))
273}
274
275#[inline]
276pub fn read_storage_time(buf: &[u8]) -> Option<DateTime> {
277	read_time(buf)
278}
279
280#[inline]
281pub fn read_created_at(buf: &[u8]) -> DateTime {
282	read_stamp(buf, CREATED_AT_OFFSET)
283}
284
285#[inline]
286pub fn read_updated_at(buf: &[u8]) -> DateTime {
287	read_stamp(buf, UPDATED_AT_OFFSET)
288}
289
290#[inline]
291pub fn read_not_before(buf: &[u8]) -> Option<DateTime> {
292	(buf[FLAGS_OFFSET] & HAS_NOT_BEFORE != 0).then(|| read_stamp(buf, NOT_BEFORE_OFFSET))
293}
294
295#[inline]
296pub fn read_finished_at(buf: &[u8]) -> DateTime {
297	read_stamp(buf, FINISHED_AT_OFFSET)
298}
299
300#[inline]
301pub fn read_expires_at(buf: &[u8]) -> DateTime {
302	read_stamp(buf, EXPIRES_AT_OFFSET)
303}
304
305impl EncodedBytes {
306	pub(crate) fn thaw(self) -> EncodedRowBuilder {
307		EncodedRowBuilder(self.0.into_inner())
308	}
309}
310
311impl EncodedBytes {
312	pub fn make_mut(&mut self) -> &mut [u8] {
313		self.0.make_mut()
314	}
315}
316
317#[cfg(test)]
318mod tests {
319	use reifydb_value::{
320		encoding::LeBytes,
321		factory::time::at_nanos,
322		value::{datetime::DateTime, value_type::ValueType},
323	};
324
325	use crate::row::{
326		bytes::{
327			CREATED_AT_OFFSET, FINGERPRINT_SIZE, FLAGS_OFFSET, HAS_TIME, RowBuilder, SHAPE_HEADER_SIZE,
328			TIME_OFFSET, UPDATED_AT_OFFSET,
329		},
330		shape::{RowFamily, RowShape, RowShapeField},
331	};
332
333	fn shape(field_count: usize) -> RowShape {
334		RowShape::new(
335			RowFamily::Table,
336			(0..field_count)
337				.map(|i| RowShapeField::unconstrained(format!("f{i}"), ValueType::Uint8))
338				.collect(),
339		)
340	}
341
342	#[test]
343	fn time_round_trips_independently_of_created_at_and_updated_at() {
344		// The three stamps answer different questions (when the DB learned a row, last touched
345		// it, when the event happened), so overlapping slots would make one readable as another.
346		let shape = shape(1);
347		let mut row = shape.allocate_table();
348
349		row.set_timestamps(at_nanos(11), at_nanos(22));
350		row.set_time(at_nanos(33));
351
352		assert_eq!(shape.created_at(&row), at_nanos(11));
353		assert_eq!(shape.updated_at(&row), at_nanos(22));
354		assert_eq!(shape.time(&row), Some(at_nanos(33)));
355
356		row.set_time(at_nanos(44));
357		assert_eq!(shape.created_at(&row), at_nanos(11), "writing #time must not disturb created_at");
358		assert_eq!(shape.updated_at(&row), at_nanos(22), "writing #time must not disturb updated_at");
359		assert_eq!(shape.time(&row), Some(at_nanos(44)));
360
361		row.set_timestamps(at_nanos(55), at_nanos(66));
362		assert_eq!(shape.time(&row), Some(at_nanos(44)), "writing the wall stamps must not disturb #time");
363	}
364
365	#[test]
366	fn time_survives_a_verbatim_rewrite_that_refreshes_updated_at() {
367		// set_timestamps is the seal flush's verbatim-rewrite path. #time describes when the
368		// event happened, so re-stamping it locally would drift retention to wall clock.
369		let mut row = shape(1).allocate_table();
370		row.set_timestamps(at_nanos(7), at_nanos(7));
371		row.set_time(at_nanos(1_000));
372
373		let created_at = row.created_at();
374		row.set_timestamps(created_at, at_nanos(99));
375
376		assert_eq!(row.created_at(), at_nanos(7));
377		assert_eq!(row.updated_at(), at_nanos(99), "the rewrite refreshes updated_at");
378		assert_eq!(row.time(), Some(at_nanos(1_000)), "#time is propagated, never re-stamped locally");
379	}
380
381	#[test]
382	fn the_header_slots_end_before_the_bitvec_begins() {
383		// Accessors and layout derive from the same constants, so a round trip stays
384		// self-consistent even when the arithmetic is wrong. Only the boundary breaks: the
385		// slots must tile SHAPE_HEADER_SIZE exactly, leaving the bitvec and fields untouched.
386		assert_eq!(CREATED_AT_OFFSET, FINGERPRINT_SIZE, "the first stamp starts where the fingerprint ends");
387		assert_eq!(UPDATED_AT_OFFSET, CREATED_AT_OFFSET + DateTime::ENCODED_SIZE);
388		assert_eq!(TIME_OFFSET, UPDATED_AT_OFFSET + DateTime::ENCODED_SIZE);
389		assert_eq!(
390			FLAGS_OFFSET,
391			TIME_OFFSET + DateTime::ENCODED_SIZE,
392			"the flags byte sits after the last stamp, whatever a DateTime is worth"
393		);
394		assert_eq!(SHAPE_HEADER_SIZE, FLAGS_OFFSET + 1, "the bitvec must start after the flags byte");
395
396		let shape = shape(9);
397		let mut row = shape.allocate_table();
398
399		for i in 0..9 {
400			shape.set::<u64>(&mut row, i, (i as u64 + 1) * 1_000);
401		}
402		row.set_timestamps(at_nanos(1), at_nanos(2));
403		row.set_time(DateTime::MAX);
404
405		for i in 0..9 {
406			assert_eq!(shape.get::<u64>(&row, i), (i as u64 + 1) * 1_000, "field {i} misread");
407			assert!(row.is_defined(i), "field {i} lost its definedness bit to a header write");
408		}
409		assert_eq!(row.created_at(), at_nanos(1));
410		assert_eq!(row.updated_at(), at_nanos(2));
411		assert_eq!(row.time(), Some(DateTime::MAX));
412	}
413
414	#[test]
415	fn a_row_that_was_never_stamped_carries_no_time() {
416		// A zeroed slot is indistinguishable from a stamp of zero, so without a presence bit a
417		// time-less object cannot withhold #time and downstream resolves the ambiguity by
418		// substituting a wall clock.
419		let shape = shape(3);
420		let mut row = shape.allocate_table();
421
422		assert_eq!(shape.time(&row), None, "a freshly allocated row carries no #time");
423
424		shape.set::<u64>(&mut row, 0, 7u64);
425		row.set_timestamps(at_nanos(1), at_nanos(2));
426
427		assert_eq!(shape.time(&row), None, "writing fields and wall stamps must not conjure a #time");
428		assert_eq!(shape.time(row.clone().freeze().as_slice()), None, "absence must survive the freeze");
429	}
430
431	#[test]
432	fn an_epoch_stamp_is_a_real_time_not_an_absence() {
433		// Presence is decided by the flag, never by the value, so the epoch stays an ordinary
434		// coordinate. Treating it as a sentinel would make a row genuinely dated 1970 unreadable.
435		let mut row = shape(1).allocate_table();
436		row.set_time(DateTime::EPOCH);
437
438		assert_eq!(row.time(), Some(DateTime::EPOCH));
439		assert_ne!(row.time(), None, "an explicitly stamped epoch is present, not absent");
440	}
441
442	#[test]
443	fn stamping_time_leaves_every_other_flag_bit_clear() {
444		// Bits 1..7 are unassigned. Holding them at zero is what lets a future flag be introduced
445		// without a format migration: every row written today already reads as "that flag is off".
446		let shape = shape(4);
447		let mut row = shape.allocate_table();
448
449		assert_eq!(row.as_slice()[FLAGS_OFFSET], 0, "allocation must leave the flags byte clear");
450
451		row.set_time(at_nanos(5));
452		assert_eq!(row.as_slice()[FLAGS_OFFSET], HAS_TIME, "set_time must touch only its own bit");
453
454		row.set_timestamps(at_nanos(1), at_nanos(2));
455		row.set_fingerprint(shape.fingerprint());
456		shape.set::<u64>(&mut row, 3, 42u64);
457		assert_eq!(
458			row.as_slice()[FLAGS_OFFSET],
459			HAS_TIME,
460			"no other header or field write may reach the flags byte"
461		);
462	}
463
464	#[test]
465	fn the_flags_byte_is_not_the_first_bitvec_byte() {
466		// Both live at the tail of the header and are bit-addressed, so an off-by-one in
467		// SHAPE_HEADER_SIZE would silently alias field 0's definedness onto HAS_TIME.
468		let shape = shape(8);
469		let mut row = shape.allocate_table();
470
471		shape.set::<u64>(&mut row, 0, 1u64);
472		assert!(row.is_defined(0));
473		assert_eq!(row.time(), None, "defining field 0 must not set HAS_TIME");
474
475		let mut row = shape.allocate_table();
476		row.set_time(at_nanos(9));
477		for i in 0..8 {
478			assert!(!row.is_defined(i), "stamping #time must not define field {i}");
479		}
480	}
481
482	#[test]
483	fn time_consumes_no_definedness_bit() {
484		// #time is absent-representable, but through the header flag rather than the field bitvec: it
485		// lives outside user field space, costing no definedness bit and shifting no field index.
486		let shape = shape(9);
487		let mut row = shape.allocate_table();
488		row.set_time(DateTime::MAX);
489
490		for i in 0..9 {
491			assert!(!row.is_defined(i), "field {i} must start undefined regardless of #time");
492		}
493
494		shape.set::<u64>(&mut row, 3, 42u64);
495		assert!(row.is_defined(3), "bit 3 maps to user field 3, not to a system slot");
496		for i in (0..9).filter(|i| *i != 3) {
497			assert!(!row.is_defined(i), "defining field 3 must not define field {i}");
498		}
499
500		assert_eq!(row.time(), Some(DateTime::MAX), "#time is unaffected by definedness writes");
501		assert_eq!(shape.bitvec_size(), 2, "9 fields still need exactly 2 bitvec bytes");
502		assert_eq!(shape.data_offset(), SHAPE_HEADER_SIZE + 2);
503	}
504
505	#[test]
506	fn a_stamp_slot_holds_exactly_one_datetime_encoding() {
507		// Stamps go through DateTime's own byte form, not a local u64 cast, so widening
508		// DateTime moves the header with it instead of truncating into an old-width slot.
509		let mut row = shape(1).allocate_table();
510		let stamp = at_nanos(0x0102_0304_0506_0708);
511		row.set_time(stamp);
512
513		assert_eq!(&row.as_slice()[TIME_OFFSET..TIME_OFFSET + DateTime::ENCODED_SIZE], &stamp.to_le_bytes());
514		assert_eq!(DateTime::from_le_bytes(stamp.to_le_bytes()), stamp);
515	}
516}