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