spacetimedb_table/
indexes.rs

1//! Provides primitive types and definitions around
2//! bytes, row hashes, (page) sizes, offsets, and indices.
3
4use super::util::range_move;
5use crate::static_assert_size;
6use ahash::RandomState;
7use core::fmt;
8use core::ops::{AddAssign, Div, Range, SubAssign};
9use derive_more::{Add, Sub};
10use spacetimedb_data_structures::map::ValidAsIdentityHash;
11use spacetimedb_sats::layout::Size;
12use spacetimedb_sats::memory_usage::MemoryUsage;
13use spacetimedb_sats::{impl_deserialize, impl_serialize};
14
15/// A byte is a `u8`.
16///
17/// Previous implementations used `MaybeUninit<u8>` here,
18/// but it became necessary to serialize pages to enable snapshotting,
19/// so we require that all bytes in a page be valid `u8`s, never uninit.
20pub type Byte = u8;
21
22/// A slice of [`Byte`]s.
23pub type Bytes = [Byte];
24
25/// Total size of a page, incl. header.
26///
27/// Defined as 64 KiB.
28pub(crate) const PAGE_SIZE: usize = u16::MAX as usize + 1;
29
30/// Total size of a page header.
31///
32/// 64 as the header is aligned to 64 bytes.
33pub(crate) const PAGE_HEADER_SIZE: usize = 64;
34
35/// The size of the data segment of a [`Page`](super::page::Page).
36///
37/// Currently 64KiB - 64 bytes.
38/// The 64 bytes are used for the header of the `Page`.
39// pub for benchmarks
40pub const PAGE_DATA_SIZE: usize = PAGE_SIZE - PAGE_HEADER_SIZE;
41
42/// The content hash of a row.
43///
44/// Notes:
45/// - The hash is not cryptographically secure.
46///
47/// - The hash is valid only for the lifetime of a `Table`.
48///   This entails that it should not be persisted to disk
49///   or used as a stable identifier over the network.
50///   For example, the hashing algorithm could be different
51///   on different machines based on availability of hardware instructions.
52///   Moreover, due to random seeds, when restarting from disk,
53///   the hashes may be different for the same rows.
54#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
55#[cfg_attr(any(test, feature = "proptest"), derive(proptest_derive::Arbitrary))]
56pub struct RowHash(pub u64);
57
58impl MemoryUsage for RowHash {}
59
60static_assert_size!(RowHash, 8);
61
62/// `RowHash` is already a hash, so no need to hash again.
63impl ValidAsIdentityHash for RowHash {}
64
65impl RowHash {
66    /// Returns a `Hasher` builder that yields the type of hashes that `RowHash` stores.
67    pub fn hasher_builder() -> RandomState {
68        // For equal `row`s, all calls within the same process will yield the same hash.
69        RandomState::with_seed(0x42)
70    }
71}
72
73/// An offset into a [`Page`].
74#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Add, Sub, bytemuck::NoUninit)]
75#[repr(transparent)]
76#[cfg_attr(any(test, feature = "proptest"), derive(proptest_derive::Arbitrary))]
77pub struct PageOffset(
78    #[cfg_attr(any(test, feature = "proptest"), proptest(strategy = "0..PageOffset::PAGE_END.0"))] pub u16,
79);
80
81impl MemoryUsage for PageOffset {}
82
83static_assert_size!(PageOffset, 2);
84
85// We need to ser/de `PageOffset`s because they appear within the `PageHeader`.
86impl_serialize!([] PageOffset, (self, ser) => self.0.serialize(ser));
87impl_deserialize!([] PageOffset, de => u16::deserialize(de).map(PageOffset));
88
89impl PageOffset {
90    /// Returns the offset as a `usize` index.
91    #[inline]
92    pub const fn idx(self) -> usize {
93        self.0 as usize
94    }
95
96    /// Is this offset the null offset, which refers to an empty var-len object?
97    ///
98    /// The null `PageOffset` is used as a sentinel in `VarLenRef`s and `VarLenGranule`s
99    /// as the empty list.
100    /// `VAR_LEN_NULL` can never refer to an allocated `VarLenGranule`,
101    /// because the existence of a `VarLenGranule` implies the existence of at least one fixed-len row,
102    /// which means the fixed-len high water mark is strictly greater than zero.
103    ///
104    /// Note that the null `PageOffset` refers to a valid fixed-len row slot.
105    /// It may only be used as a sentinel value when referring to var-len allocations.
106    #[inline]
107    pub const fn is_var_len_null(self) -> bool {
108        self.0 == Self::VAR_LEN_NULL.0
109    }
110
111    /// The null offset, pointing to the beginning of a page.
112    ///
113    /// The null `PageOffset` is used as a sentinel in `VarLenRef`s and `VarLenGranule`s
114    /// as the empty list.
115    /// `VAR_LEN_NULL` can never refer to an allocated `VarLenGranule`,
116    /// because the existence of a `VarLenGranule` implies the existence of at least one fixed-len row,
117    /// which means the fixed-len high water mark is strictly greater than zero.
118    ///
119    /// Note that the null `PageOffset` refers to a valid fixed-len row slot.
120    /// It may only be used as a sentinel value when referring to var-len allocations.
121    pub const VAR_LEN_NULL: Self = Self(0);
122
123    /// Is this offset at the [`PageOffset::PAGE_END`]?
124    #[inline]
125    pub const fn is_at_end(self) -> bool {
126        self.0 == Self::PAGE_END.0
127    }
128
129    /// The offset one past the end of the page.
130    /// That is, for `row_data: [Byte; PAGE_DATA_SIZE]`, this is `row_data.len()`.
131    ///
132    /// This also means that `PAGE_END` is **not** a page offset
133    /// that can be used for indexing, but it can be used as a sentinel.
134    pub const PAGE_END: Self = Self(PAGE_DATA_SIZE as u16);
135
136    /// Returns a range from this offset lasting `size` bytes.
137    #[inline]
138    pub const fn range(self, size: Size) -> Range<usize> {
139        range_move(0..size.len(), self.idx())
140    }
141}
142
143impl PartialEq<Size> for PageOffset {
144    #[inline]
145    fn eq(&self, other: &Size) -> bool {
146        self.0 == other.0
147    }
148}
149
150impl AddAssign<Size> for PageOffset {
151    #[inline]
152    fn add_assign(&mut self, rhs: Size) {
153        self.0 += rhs.0;
154    }
155}
156
157impl SubAssign<Size> for PageOffset {
158    #[inline]
159    fn sub_assign(&mut self, rhs: Size) {
160        self.0 -= rhs.0;
161    }
162}
163
164impl Div<Size> for PageOffset {
165    type Output = usize;
166
167    #[inline]
168    fn div(self, size: Size) -> Self::Output {
169        self.idx() / size.len()
170    }
171}
172
173impl fmt::LowerHex for PageOffset {
174    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175        fmt::LowerHex::fmt(&self.0, f)
176    }
177}
178
179/// Returns the maximum number of rows with `fixed_row_size` that a page can hold.
180#[inline]
181pub fn max_rows_in_page(fixed_row_size: Size) -> usize {
182    PageOffset::PAGE_END.idx().div_ceil(fixed_row_size.len())
183}
184
185/// The index of a [`Page`] within a [`Pages`].
186#[cfg_attr(any(test, feature = "proptest"), derive(proptest_derive::Arbitrary))]
187#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
188pub struct PageIndex(#[cfg_attr(any(test, feature = "proptest"), proptest(strategy = "0..MASK_PI"))] pub u64);
189
190impl MemoryUsage for PageIndex {}
191
192static_assert_size!(PageIndex, 8);
193
194impl PageIndex {
195    /// The maximum page index, currently `(1 << 39) - 1`.
196    ///
197    /// Limiting `PageIndex` to 39 bits allows it to be packed into `RowPointer`'s 64 bits
198    /// alongside a `PageOffset`, a `SquashedOffset` and a reserved bit.
199    pub const MAX: Self = Self(MASK_PI);
200
201    /// Returns this index as a `usize`.
202    #[inline]
203    pub const fn idx(self) -> usize {
204        self.0 as usize
205    }
206}
207
208/// Indicates which version of a `Table` is referred to by a `RowPointer`.
209///
210/// Currently, `SquashedOffset` has two meaningful values,
211/// [`SquashedOffset::TX_STATE`] and [`SquashedOffset::COMMITTED_STATE`],
212/// which refer to the TX scratchpad and the committed state respectively.
213///
214/// In the future, `SquashedOffset` will be extended to capture
215/// which savepoint within a transaction the pointer refers to,
216/// or which committed-unsquashed preceding transaction.
217#[derive(Clone, Copy, PartialEq, Eq, Debug)]
218#[cfg_attr(any(test, feature = "proptest"), derive(proptest_derive::Arbitrary))]
219pub struct SquashedOffset(pub u8);
220
221impl MemoryUsage for SquashedOffset {}
222
223static_assert_size!(SquashedOffset, 1);
224
225impl SquashedOffset {
226    /// Does this `SquahsedOffset` refer to the TX scratchpad?
227    #[inline]
228    pub const fn is_tx_state(self) -> bool {
229        self.0 == Self::TX_STATE.0
230    }
231
232    /// The `SquashedOffset` for the TX scratchpad.
233    pub const TX_STATE: Self = Self(0);
234
235    /// Does this `SquashedOffset` refer to the committed (squashed) state?
236    #[inline]
237    pub const fn is_committed_state(self) -> bool {
238        self.0 == Self::COMMITTED_STATE.0
239    }
240
241    /// The `SquashedOffset` for the committed (squashed) state.
242    pub const COMMITTED_STATE: Self = Self(1);
243}
244
245/// Offset to a buffer inside `Pages` referring
246/// to the index of a specific page
247/// and the offset within the page.
248#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
249#[repr(transparent)]
250pub struct RowPointer(pub u64);
251
252impl MemoryUsage for RowPointer {}
253
254static_assert_size!(RowPointer, 8);
255
256// Offsets and bits for the various components of `RowPointer`.
257const OFFSET_RB: u64 = 0;
258const BITS_RB: u64 = 1;
259// ^-- Bit 1 is reserved so it can be used in `OffsetOrCollider` ("reserved bit").
260const OFFSET_PI: u64 = OFFSET_RB + BITS_RB;
261const BITS_PI: u64 = 39;
262const OFFSET_PO: u64 = OFFSET_PI + BITS_PI;
263const BITS_PO: u64 = 16;
264const OFFSET_SQ: u64 = OFFSET_PO + BITS_PO;
265const BITS_SQ: u64 = 8;
266
267// Extracting masks for the various components of `RowPointer`.
268const MASK_RB: u64 = (1 << BITS_RB) - 1;
269const MASK_PI: u64 = (1 << BITS_PI) - 1;
270const MASK_PO: u64 = (1 << BITS_PO) - 1;
271const MASK_SQ: u64 = (1 << BITS_SQ) - 1;
272
273// Zeroing masks for the various components of `RowPointer`.
274const MASK_ZERO_RB: u64 = !(MASK_RB << OFFSET_RB);
275const MASK_ZERO_PI: u64 = !(MASK_PI << OFFSET_PI);
276const MASK_ZERO_PO: u64 = !(MASK_PO << OFFSET_PO);
277const MASK_ZERO_SQ: u64 = !(MASK_SQ << OFFSET_SQ);
278
279impl RowPointer {
280    /// Returns a row pointer that is at the given `page_offset`,
281    /// in the page with `page_index`,
282    /// and with the `squashed_offset` (savepoint offset).
283    #[inline(always)]
284    pub const fn new(
285        reserved_bit: bool,
286        page_index: PageIndex,
287        page_offset: PageOffset,
288        squashed_offset: SquashedOffset,
289    ) -> Self {
290        Self(0)
291            .with_reserved_bit(reserved_bit)
292            .with_squashed_offset(squashed_offset)
293            .with_page_index(page_index)
294            .with_page_offset(page_offset)
295    }
296
297    /// Returns the reserved bit.
298    #[inline(always)]
299    pub const fn reserved_bit(self) -> bool {
300        ((self.0 >> OFFSET_RB) & MASK_RB) != 0
301    }
302
303    /// Returns the index of the page.
304    #[inline(always)]
305    pub const fn page_index(self) -> PageIndex {
306        PageIndex((self.0 >> OFFSET_PI) & MASK_PI)
307    }
308
309    /// Returns the offset within the page.
310    #[inline(always)]
311    pub const fn page_offset(self) -> PageOffset {
312        PageOffset(((self.0 >> OFFSET_PO) & MASK_PO) as u16)
313    }
314
315    /// Returns the squashed offset, i.e., the savepoint offset.
316    #[inline(always)]
317    pub const fn squashed_offset(self) -> SquashedOffset {
318        SquashedOffset(((self.0 >> OFFSET_SQ) & MASK_SQ) as u8)
319    }
320
321    /// Returns a new row pointer
322    /// with its reserved bit changed to `reserved_bit`.
323    #[inline(always)]
324    pub const fn with_reserved_bit(self, reserved_bit: bool) -> Self {
325        Self::with(self, reserved_bit as u64, MASK_RB, OFFSET_RB, MASK_ZERO_RB)
326    }
327
328    /// Returns a new row pointer
329    /// with its `PageIndex` changed to `page_index`.
330    #[inline(always)]
331    pub const fn with_page_index(self, page_index: PageIndex) -> Self {
332        Self::with(self, page_index.0, MASK_PI, OFFSET_PI, MASK_ZERO_PI)
333    }
334
335    /// Returns a new row pointer
336    /// with its `PageOffset` changed to `page_offset`.
337    #[inline(always)]
338    pub const fn with_page_offset(self, page_offset: PageOffset) -> Self {
339        Self::with(self, page_offset.0 as u64, MASK_PO, OFFSET_PO, MASK_ZERO_PO)
340    }
341
342    /// Returns a new row pointer
343    /// with its `SquashedOffset` changed to `squashed_offset`.
344    #[inline(always)]
345    pub const fn with_squashed_offset(self, squashed_offset: SquashedOffset) -> Self {
346        Self::with(self, squashed_offset.0 as u64, MASK_SQ, OFFSET_SQ, MASK_ZERO_SQ)
347    }
348
349    #[inline(always)]
350    const fn with(self, v: u64, mask: u64, offset: u64, zero: u64) -> Self {
351        let vmoved = (v & mask) << offset;
352        Self((self.0 & zero) | vmoved)
353    }
354}
355
356impl fmt::Debug for RowPointer {
357    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358        write!(
359            f,
360            "RowPointer(r: {:?}, pi: {:?}, po: {:?}, so: {:?})",
361            self.reserved_bit() as u8,
362            self.page_index().idx(),
363            self.page_offset().idx(),
364            self.squashed_offset().0,
365        )
366    }
367}
368
369#[cfg(test)]
370mod test {
371    use super::*;
372    use proptest::prelude::*;
373
374    proptest! {
375        #[test]
376        fn row_pointer_ops_work(
377            ((rb1, pi1, po1, so1), (rb2, pi2, po2, so2)) in (
378                (any::<bool>(), any::<PageIndex>(), any::<PageOffset>(), any::<SquashedOffset>()),
379                (any::<bool>(), any::<PageIndex>(), any::<PageOffset>(), any::<SquashedOffset>()),
380        )) {
381            let check = |rb, pi, po, so, ptr: RowPointer| {
382                prop_assert_eq!(rb, ptr.reserved_bit());
383                prop_assert_eq!(pi, ptr.page_index());
384                prop_assert_eq!(po, ptr.page_offset());
385                prop_assert_eq!(so, ptr.squashed_offset());
386                Ok(())
387            };
388            let ptr = RowPointer::new(rb1, pi1, po1, so1);
389            check(rb1, pi1, po1, so1, ptr)?;
390            check(rb2, pi1, po1, so1, ptr.with_reserved_bit(rb2))?;
391            check(rb1, pi2, po1, so1, ptr.with_page_index(pi2))?;
392            check(rb1, pi1, po2, so1, ptr.with_page_offset(po2))?;
393            check(rb1, pi1, po1, so2, ptr.with_squashed_offset(so2))?;
394        }
395    }
396}