spacetimedb_table/
static_layout.rs

1//! This module implements a fast path for converting certain row types between BFLATN <-> BSATN.
2//!
3//! The key insight is that a majority of row types will have a known fixed length,
4//! with no variable-length members.
5//! BFLATN is designed with this in mind, storing fixed-length portions of rows inline,
6//! at the expense of an indirection to reach var-length columns like strings.
7//! A majority of these types will also have a fixed BSATN length,
8//! but note that BSATN stores sum values (enums) without padding,
9//! so row types which contain sums may not have a fixed BSATN length
10//! if the sum's variants have different "live" unpadded lengths.
11//!
12//! For row types with fixed BSATN lengths, we can reduce the BFLATN <-> BSATN conversions
13//! to a series of `memcpy`s, skipping over padding sequences.
14//! This is potentially much faster than the more general
15//! [`crate::bflatn_from::serialize_row_from_page`] and [`crate::bflatn_to::write_row_to_page`] ,
16//! which both traverse a [`RowTypeLayout`] and dispatch on the type of each column.
17//!
18//! For example, to serialize a row of type `(u64, u64, u32, u64)`,
19//! [`bflatn_from`] will do four dispatches, three calls to `serialize_u64` and one to `serialize_u32`.
20//! This module will make 2 `memcpy`s (or actually, `<[u8]>::copy_from_slice`s):
21//! one of 20 bytes to copy the leading `(u64, u64, u32)`, which contains no padding,
22//! and then one of 8 bytes to copy the trailing `u64`, skipping over 4 bytes of padding in between.
23
24use super::{
25    indexes::{Byte, Bytes},
26    util::range_move,
27};
28use core::mem::MaybeUninit;
29use core::ptr;
30use smallvec::SmallVec;
31use spacetimedb_data_structures::slim_slice::SlimSmallSliceBox;
32use spacetimedb_sats::layout::{
33    AlgebraicTypeLayout, HasLayout, PrimitiveType, ProductTypeElementLayout, ProductTypeLayoutView, RowTypeLayout,
34    SumTypeLayout, SumTypeVariantLayout,
35};
36use spacetimedb_sats::memory_usage::MemoryUsage;
37
38/// A precomputed layout for a type whose encoded BSATN and BFLATN lengths are both known constants,
39/// enabling fast BFLATN <-> BSATN conversions.
40#[derive(PartialEq, Eq, Debug, Clone)]
41#[repr(align(8))]
42pub struct StaticLayout {
43    /// The length of the encoded BSATN representation of a row of this type,
44    /// in bytes.
45    ///
46    /// Storing this allows us to pre-allocate correctly-sized buffers,
47    /// avoiding potentially-expensive `realloc`s.
48    pub(crate) bsatn_length: u16,
49
50    /// A series of `memcpy` invocations from a BFLATN src/dst <-> a BSATN src/dst
51    /// which are sufficient to convert BSATN to BFLATN and vice versa.
52    fields: SlimSmallSliceBox<MemcpyField, 3>,
53}
54
55impl MemoryUsage for StaticLayout {
56    fn heap_usage(&self) -> usize {
57        let Self { bsatn_length, fields } = self;
58        bsatn_length.heap_usage() + fields.heap_usage()
59    }
60}
61
62impl StaticLayout {
63    /// Serialize `row` from BFLATN to BSATN into `buf`.
64    ///
65    /// # Safety
66    ///
67    /// - `buf` must be at least `self.bsatn_length` long.
68    /// - `row` must store a valid, initialized instance of the BFLATN row type
69    ///   for which `self` was computed.
70    ///   As a consequence of this, for every `field` in `self.fields`,
71    ///   `row[field.bflatn_offset .. field.bflatn_offset + length]` will be initialized.
72    unsafe fn serialize_row_into(&self, buf: &mut [MaybeUninit<Byte>], row: &Bytes) {
73        debug_assert!(buf.len() >= self.bsatn_length as usize);
74        for field in &*self.fields {
75            // SAFETY: forward caller requirements.
76            unsafe { field.copy_bflatn_to_bsatn(row, buf) };
77        }
78    }
79
80    /// Serialize `row` from BFLATN to BSATN into a `Vec<u8>`.
81    ///
82    /// # Safety
83    ///
84    /// - `row` must store a valid, initialized instance of the BFLATN row type
85    ///   for which `self` was computed.
86    ///   As a consequence of this, for every `field` in `self.fields`,
87    ///   `row[field.bflatn_offset .. field.bflatn_offset + length]` will be initialized.
88    pub(crate) unsafe fn serialize_row_into_vec(&self, row: &Bytes) -> Vec<u8> {
89        // Create an uninitialized buffer `buf` of the correct length.
90        let bsatn_len = self.bsatn_length as usize;
91        let mut buf = Vec::with_capacity(bsatn_len);
92        let sink = buf.spare_capacity_mut();
93
94        // (1) Write the row into the slice using a series of `memcpy`s.
95        // SAFETY:
96        // - Caller promised that `row` is valid for `self`.
97        // - `sink` was constructed with exactly the correct length above.
98        unsafe {
99            self.serialize_row_into(sink, row);
100        }
101
102        // SAFETY: In (1), we initialized `0..len`
103        // as `row` was valid for `self` per caller requirements.
104        unsafe { buf.set_len(bsatn_len) }
105        buf
106    }
107
108    /// Serialize `row` from BFLATN to BSATN, appending the BSATN to `buf`.
109    ///
110    /// # Safety
111    ///
112    /// - `row` must store a valid, initialized instance of the BFLATN row type
113    ///   for which `self` was computed.
114    ///   As a consequence of this, for every `field` in `self.fields`,
115    ///   `row[field.bflatn_offset .. field.bflatn_offset + length]` will be initialized.
116    pub(crate) unsafe fn serialize_row_extend(&self, buf: &mut Vec<u8>, row: &Bytes) {
117        // Get an uninitialized slice within `buf` of the correct length.
118        let start = buf.len();
119        let len = self.bsatn_length as usize;
120        buf.reserve(len);
121        let sink = &mut buf.spare_capacity_mut()[..len];
122
123        // (1) Write the row into the slice using a series of `memcpy`s.
124        // SAFETY:
125        // - Caller promised that `row` is valid for `self`.
126        // - `sink` was constructed with exactly the correct length above.
127        unsafe {
128            self.serialize_row_into(sink, row);
129        }
130
131        // SAFETY: In (1), we initialized `start .. start + len`
132        // as `row` was valid for `self` per caller requirements
133        // and we had initialized up to `start` before,
134        // so now we have initialized up to `start + len`.
135        unsafe { buf.set_len(start + len) }
136    }
137
138    #[allow(unused)]
139    /// Deserializes the BSATN-encoded `row` into the BFLATN-encoded `buf`.
140    ///
141    /// - `row` must be at least `self.bsatn_length` long.
142    /// - `buf` must be ready to store an instance of the BFLATN row type
143    ///   for which `self` was computed.
144    ///   As a consequence of this, for every `field` in `self.fields`,
145    ///   `field.bflatn_offset .. field.bflatn_offset + length` must be in-bounds of `buf`.
146    pub(crate) unsafe fn deserialize_row_into(&self, buf: &mut Bytes, row: &[u8]) {
147        for field in &*self.fields {
148            // SAFETY: forward caller requirements.
149            unsafe { field.copy_bsatn_to_bflatn(row, buf) };
150        }
151    }
152
153    /// Compares `row_a` for equality against `row_b`.
154    ///
155    /// # Safety
156    ///
157    /// - `row` must store a valid, initialized instance of the BFLATN row type
158    ///   for which `self` was computed.
159    ///   As a consequence of this, for every `field` in `self.fields`,
160    ///   `row[field.bflatn_offset .. field.bflatn_offset + field.length]` will be initialized.
161    pub(crate) unsafe fn eq(&self, row_a: &Bytes, row_b: &Bytes) -> bool {
162        // No need to check the lengths.
163        // We assume they are of the same length.
164        self.fields.iter().all(|field| {
165            // SAFETY: The consequence of what the caller promised is that
166            // `row_(a/b).len() >= field.bflatn_offset + field.length >= field.bflatn_offset`.
167            unsafe { field.eq(row_a, row_b) }
168        })
169    }
170
171    /// Construct a `StaticLayout` for converting BFLATN rows of `row_type` <-> BSATN.
172    ///
173    /// Returns `None` if `row_type` contains a column which does not have a constant length in BSATN,
174    /// either a [`VarLenType`]
175    /// or a [`SumTypeLayout`] whose variants do not have the same "live" unpadded length.
176    pub fn for_row_type(row_type: &RowTypeLayout) -> Option<Self> {
177        if !row_type.layout().fixed {
178            // Don't bother computing the static layout if there are variable components.
179            return None;
180        }
181
182        let mut builder = LayoutBuilder::new_builder();
183        builder.visit_product(row_type.product())?;
184        Some(builder.build())
185    }
186}
187
188/// An identifier for a series of bytes within a BFLATN row
189/// which can be directly copied into an output BSATN buffer
190/// with a known length and offset or vice versa.
191///
192/// Within the row type's BFLATN layout, `row[bflatn_offset .. (bflatn_offset + length)]`
193/// must not contain any padding bytes,
194/// i.e. all of those bytes must be fully initialized if the row is initialized.
195#[derive(PartialEq, Eq, Debug, Copy, Clone)]
196struct MemcpyField {
197    /// Offset in the BFLATN row from which to begin `memcpy`ing, in bytes.
198    bflatn_offset: u16,
199
200    /// Offset in the BSATN buffer to which to begin `memcpy`ing, in bytes.
201    // TODO(perf): Could be a running counter, but this way we just have all the `memcpy` args in one place.
202    // Should bench; I (pgoldman 2024-03-25) suspect this allows more insn parallelism and is therefore better.
203    bsatn_offset: u16,
204
205    /// Length to `memcpy`, in bytes.
206    length: u16,
207}
208
209impl MemoryUsage for MemcpyField {}
210
211impl MemcpyField {
212    /// Copies the bytes at `src[self.bflatn_offset .. self.bflatn_offset + self.length]`
213    /// into `dst[self.bsatn_offset .. self.bsatn_offset + self.length]`.
214    ///
215    /// # Safety
216    ///
217    /// 1. `src.len() >= self.bflatn_offset + self.length`.
218    /// 2. `dst.len() >= self.bsatn_offset + self.length`
219    unsafe fn copy_bflatn_to_bsatn(&self, src: &Bytes, dst: &mut [MaybeUninit<Byte>]) {
220        let src_offset = self.bflatn_offset as usize;
221        let dst_offset = self.bsatn_offset as usize;
222
223        let len = self.length as usize;
224        let src = src.as_ptr();
225        let dst = dst.as_mut_ptr();
226        // SAFETY: per 1., it follows that `src_offset` is in bounds of `src`.
227        let src = unsafe { src.add(src_offset) };
228        // SAFETY: per 2., it follows that `dst_offset` is in bounds of `dst`.
229        let dst = unsafe { dst.add(dst_offset) };
230        let dst = dst.cast();
231
232        // SAFETY:
233        // 1. `src` is valid for reads for `len` bytes per caller requirement 1.
234        //    and because `src` was derived from a shared slice.
235        // 2. `dst` is valid for writes for `len` bytes per caller requirement 2.
236        //    and because `dst` was derived from an exclusive slice.
237        // 3. Alignment for `u8` is trivially satisfied for any pointer.
238        // 4. As `src` and `dst` were derived from shared and exclusive slices, they cannot overlap.
239        unsafe { ptr::copy_nonoverlapping(src, dst, len) }
240    }
241
242    /// Copies the bytes at `src[self.bsatn_offset .. self.bsatn_offset + self.length]`
243    /// into `dst[self.bflatn_offset .. self.bflatn_offset + self.length]`.
244    ///
245    /// # Safety
246    ///
247    /// 1. `src.len() >= self.bsatn_offset + self.length`.
248    /// 2. `dst.len() >= self.bflatn_offset + self.length`
249    unsafe fn copy_bsatn_to_bflatn(&self, src: &Bytes, dst: &mut Bytes) {
250        let src_offset = self.bsatn_offset as usize;
251        let dst_offset = self.bflatn_offset as usize;
252
253        let len = self.length as usize;
254        let src = src.as_ptr();
255        let dst = dst.as_mut_ptr();
256        // SAFETY: per 1., it follows that `src_offset` is in bounds of `src`.
257        let src = unsafe { src.add(src_offset) };
258        // SAFETY: per 2., it follows that `dst_offset` is in bounds of `dst`.
259        let dst = unsafe { dst.add(dst_offset) };
260
261        // SAFETY:
262        // 1. `src` is valid for reads for `len` bytes per caller requirement 1.
263        //    and because `src` was derived from a shared slice.
264        // 2. `dst` is valid for writes for `len` bytes per caller requirement 2.
265        //    and because `dst` was derived from an exclusive slice.
266        // 3. Alignment for `u8` is trivially satisfied for any pointer.
267        // 4. As `src` and `dst` were derived from shared and exclusive slices, they cannot overlap.
268        unsafe { ptr::copy_nonoverlapping(src, dst, len) }
269    }
270
271    /// Compares `row_a` and `row_b` for equality in this field.
272    ///
273    /// # Safety
274    ///
275    /// - `row_a.len() >= self.bflatn_offset + self.length`
276    /// - `row_b.len() >= self.bflatn_offset + self.length`
277    unsafe fn eq(&self, row_a: &Bytes, row_b: &Bytes) -> bool {
278        let range = range_move(0..self.length as usize, self.bflatn_offset as usize);
279        let range2 = range.clone();
280        // SAFETY: The `range` is in bounds as
281        // `row_a.len() >= self.bflatn_offset + self.length >= self.bflatn_offset`.
282        let row_a_field = unsafe { row_a.get_unchecked(range) };
283        // SAFETY: The `range` is in bounds as
284        // `row_b.len() >= self.bflatn_offset + self.length >= self.bflatn_offset`.
285        let row_b_field = unsafe { row_b.get_unchecked(range2) };
286        row_a_field == row_b_field
287    }
288
289    fn is_empty(&self) -> bool {
290        self.length == 0
291    }
292}
293
294/// A builder for a [`StaticLayout`].
295struct LayoutBuilder {
296    /// Always at least one element.
297    fields: Vec<MemcpyField>,
298}
299
300impl LayoutBuilder {
301    fn new_builder() -> Self {
302        Self {
303            fields: vec![MemcpyField {
304                bflatn_offset: 0,
305                bsatn_offset: 0,
306                length: 0,
307            }],
308        }
309    }
310
311    fn build(self) -> StaticLayout {
312        let LayoutBuilder { fields } = self;
313        let fields: SmallVec<[_; 3]> = fields.into_iter().filter(|field| !field.is_empty()).collect();
314        let fields: SlimSmallSliceBox<MemcpyField, 3> = fields.into();
315        let bsatn_length = fields.last().map(|last| last.bsatn_offset + last.length).unwrap_or(0);
316
317        StaticLayout { bsatn_length, fields }
318    }
319
320    fn current_field(&self) -> &MemcpyField {
321        self.fields.last().unwrap()
322    }
323
324    fn current_field_mut(&mut self) -> &mut MemcpyField {
325        self.fields.last_mut().unwrap()
326    }
327
328    fn next_bflatn_offset(&self) -> u16 {
329        let last = self.current_field();
330        last.bflatn_offset + last.length
331    }
332
333    fn next_bsatn_offset(&self) -> u16 {
334        let last = self.current_field();
335        last.bsatn_offset + last.length
336    }
337
338    fn visit_product(&mut self, product: ProductTypeLayoutView) -> Option<()> {
339        let base_bflatn_offset = self.next_bflatn_offset();
340        for elt in product.elements.iter() {
341            self.visit_product_element(elt, base_bflatn_offset)?;
342        }
343        Some(())
344    }
345
346    fn visit_product_element(&mut self, elt: &ProductTypeElementLayout, product_base_offset: u16) -> Option<()> {
347        let elt_offset = product_base_offset + elt.offset;
348        let next_bflatn_offset = self.next_bflatn_offset();
349        if next_bflatn_offset != elt_offset {
350            // Padding between previous element and this element,
351            // so start a new field.
352            //
353            // Note that this is the only place we have to reason about alignment and padding
354            // because the enclosing `ProductTypeLayout` has already computed valid aligned offsets
355            // for the elements.
356
357            let bsatn_offset = self.next_bsatn_offset();
358            self.fields.push(MemcpyField {
359                bsatn_offset,
360                bflatn_offset: elt_offset,
361                length: 0,
362            });
363        }
364        self.visit_value(&elt.ty)
365    }
366
367    fn visit_value(&mut self, val: &AlgebraicTypeLayout) -> Option<()> {
368        match val {
369            AlgebraicTypeLayout::Sum(sum) => self.visit_sum(sum),
370            AlgebraicTypeLayout::Product(prod) => self.visit_product(prod.view()),
371            AlgebraicTypeLayout::Primitive(prim) => {
372                self.visit_primitive(prim);
373                Some(())
374            }
375
376            // Var-len types (obviously) don't have a known BSATN length,
377            // so fail.
378            AlgebraicTypeLayout::VarLen(_) => None,
379        }
380    }
381
382    fn visit_sum(&mut self, sum: &SumTypeLayout) -> Option<()> {
383        // If the sum has no variants, it's the never type, so there's no point in computing a layout.
384        let first_variant = sum.variants.first()?;
385
386        let variant_layout = |variant: &SumTypeVariantLayout| {
387            let mut builder = LayoutBuilder::new_builder();
388            builder.visit_value(&variant.ty)?;
389            Some(builder.build())
390        };
391
392        // Check that the variants all have the same `StaticLayout`.
393        // If they don't, bail.
394        let first_variant_layout = variant_layout(first_variant)?;
395        for later_variant in &sum.variants[1..] {
396            let later_variant_layout = variant_layout(later_variant)?;
397            if later_variant_layout != first_variant_layout {
398                return None;
399            }
400        }
401
402        if first_variant_layout.bsatn_length == 0 {
403            // For C-style enums (those without payloads),
404            // simply serialize the tag and move on.
405            self.current_field_mut().length += 1;
406            return Some(());
407        }
408
409        // Now that we've reached this point, we know that `first_variant_layout`
410        // applies to the values of all the variants.
411
412        let tag_bflatn_offset = self.next_bflatn_offset();
413        let payload_bflatn_offset = tag_bflatn_offset + sum.payload_offset;
414
415        let tag_bsatn_offset = self.next_bsatn_offset();
416        let payload_bsatn_offset = tag_bsatn_offset + 1;
417
418        // Serialize the tag, consolidating into the previous memcpy if possible.
419        self.visit_primitive(&PrimitiveType::U8);
420
421        if sum.payload_offset > 1 {
422            // Add an empty marker field to keep track of padding.
423            self.fields.push(MemcpyField {
424                bflatn_offset: payload_bflatn_offset,
425                bsatn_offset: payload_bsatn_offset,
426                length: 0,
427            });
428        } // Otherwise, nothing to do.
429
430        // Lay out the variants.
431        // Since all variants have the same layout, we just use the first one.
432        self.visit_value(&first_variant.ty)?;
433
434        Some(())
435    }
436
437    fn visit_primitive(&mut self, prim: &PrimitiveType) {
438        self.current_field_mut().length += prim.size() as u16
439    }
440}
441
442#[cfg(test)]
443mod test {
444    use super::*;
445    use crate::{blob_store::HashMapBlobStore, page_pool::PagePool};
446    use proptest::prelude::*;
447    use spacetimedb_sats::{bsatn, proptest::generate_typed_row, AlgebraicType, ProductType};
448
449    fn assert_expected_layout(ty: ProductType, bsatn_length: u16, fields: &[(u16, u16, u16)]) {
450        let expected_layout = StaticLayout {
451            bsatn_length,
452            fields: fields
453                .iter()
454                .copied()
455                .map(|(bflatn_offset, bsatn_offset, length)| MemcpyField {
456                    bflatn_offset,
457                    bsatn_offset,
458                    length,
459                })
460                .collect::<SmallVec<_>>()
461                .into(),
462        };
463        let row_type = RowTypeLayout::from(ty.clone());
464        let Some(computed_layout) = StaticLayout::for_row_type(&row_type) else {
465            panic!("assert_expected_layout: Computed `None` for row {row_type:#?}\nExpected:{expected_layout:#?}");
466        };
467        assert_eq!(
468            computed_layout, expected_layout,
469            "assert_expected_layout: Computed layout (left) doesn't match expected (right) for {ty:?}",
470        );
471    }
472
473    #[test]
474    fn known_types_expected_layout_plain() {
475        for prim in [
476            AlgebraicType::Bool,
477            AlgebraicType::U8,
478            AlgebraicType::I8,
479            AlgebraicType::U16,
480            AlgebraicType::I16,
481            AlgebraicType::U32,
482            AlgebraicType::I32,
483            AlgebraicType::U64,
484            AlgebraicType::I64,
485            AlgebraicType::U128,
486            AlgebraicType::I128,
487            AlgebraicType::U256,
488            AlgebraicType::I256,
489        ] {
490            let size = AlgebraicTypeLayout::from(prim.clone()).size() as u16;
491            assert_expected_layout(ProductType::from([prim]), size, &[(0, 0, size)]);
492        }
493    }
494
495    #[test]
496    fn known_types_expected_layout_complex() {
497        for (ty, bsatn_length, fields) in [
498            (ProductType::new([].into()), 0, &[][..]),
499            (
500                ProductType::from([AlgebraicType::sum([
501                    AlgebraicType::U8,
502                    AlgebraicType::I8,
503                    AlgebraicType::Bool,
504                ])]),
505                2,
506                // In BFLATN, sums have padding after the tag to the max alignment of any variant payload.
507                // In this case, 0 bytes of padding, because all payloads are aligned to 1.
508                // Since there's no padding, the memcpys can be consolidated.
509                &[(0, 0, 2)][..],
510            ),
511            (
512                ProductType::from([AlgebraicType::sum([
513                    AlgebraicType::product([
514                        AlgebraicType::U8,
515                        AlgebraicType::U8,
516                        AlgebraicType::U8,
517                        AlgebraicType::U8,
518                    ]),
519                    AlgebraicType::product([AlgebraicType::U16, AlgebraicType::U16]),
520                    AlgebraicType::U32,
521                ])]),
522                5,
523                // In BFLATN, sums have padding after the tag to the max alignment of any variant payload.
524                // In this case, 3 bytes of padding.
525                &[(0, 0, 1), (4, 1, 4)][..],
526            ),
527            (
528                ProductType::from([
529                    AlgebraicType::sum([AlgebraicType::U128, AlgebraicType::I128]),
530                    AlgebraicType::U32,
531                ]),
532                21,
533                // In BFLATN, sums have padding after the tag to the max alignment of any variant payload.
534                // In this case, 15 bytes of padding.
535                &[(0, 0, 1), (16, 1, 20)][..],
536            ),
537            (
538                ProductType::from([
539                    AlgebraicType::sum([AlgebraicType::U256, AlgebraicType::I256]),
540                    AlgebraicType::U32,
541                ]),
542                37,
543                // In BFLATN, sums have padding after the tag to the max alignment of any variant payload.
544                // In this case, 15 bytes of padding.
545                &[(0, 0, 1), (32, 1, 36)][..],
546            ),
547            (
548                ProductType::from([
549                    AlgebraicType::U256,
550                    AlgebraicType::U128,
551                    AlgebraicType::U64,
552                    AlgebraicType::U32,
553                    AlgebraicType::U16,
554                    AlgebraicType::U8,
555                ]),
556                63,
557                &[(0, 0, 63)][..],
558            ),
559            (
560                ProductType::from([
561                    AlgebraicType::U8,
562                    AlgebraicType::U16,
563                    AlgebraicType::U32,
564                    AlgebraicType::U64,
565                    AlgebraicType::U128,
566                ]),
567                31,
568                &[(0, 0, 1), (2, 1, 30)][..],
569            ),
570            // Make sure sums with no variant data are handled correctly.
571            (
572                ProductType::from([AlgebraicType::sum([AlgebraicType::product::<[AlgebraicType; 0]>([])])]),
573                1,
574                &[(0, 0, 1)][..],
575            ),
576            (
577                ProductType::from([AlgebraicType::sum([
578                    AlgebraicType::product::<[AlgebraicType; 0]>([]),
579                    AlgebraicType::product::<[AlgebraicType; 0]>([]),
580                ])]),
581                1,
582                &[(0, 0, 1)][..],
583            ),
584            // Various experiments with 1-byte-aligned payloads.
585            // These are particularly nice for memcpy consolidation as there's no padding.
586            (
587                ProductType::from([AlgebraicType::sum([
588                    AlgebraicType::product([AlgebraicType::U8, AlgebraicType::U8]),
589                    AlgebraicType::product([AlgebraicType::Bool, AlgebraicType::Bool]),
590                ])]),
591                3,
592                &[(0, 0, 3)][..],
593            ),
594            (
595                ProductType::from([
596                    AlgebraicType::sum([AlgebraicType::Bool, AlgebraicType::U8]),
597                    AlgebraicType::sum([AlgebraicType::U8, AlgebraicType::Bool]),
598                ]),
599                4,
600                &[(0, 0, 4)][..],
601            ),
602            (
603                ProductType::from([
604                    AlgebraicType::U16,
605                    AlgebraicType::sum([AlgebraicType::U8, AlgebraicType::Bool]),
606                    AlgebraicType::U16,
607                ]),
608                6,
609                &[(0, 0, 6)][..],
610            ),
611            (
612                ProductType::from([
613                    AlgebraicType::U32,
614                    AlgebraicType::sum([AlgebraicType::U16, AlgebraicType::I16]),
615                    AlgebraicType::U32,
616                ]),
617                11,
618                &[(0, 0, 5), (6, 5, 6)][..],
619            ),
620        ] {
621            assert_expected_layout(ty, bsatn_length, fields);
622        }
623    }
624
625    #[test]
626    fn known_types_not_applicable() {
627        for ty in [
628            AlgebraicType::String,
629            AlgebraicType::bytes(),
630            AlgebraicType::never(),
631            AlgebraicType::array(AlgebraicType::U16),
632            AlgebraicType::sum([AlgebraicType::U8, AlgebraicType::U16]),
633        ] {
634            let layout = RowTypeLayout::from(ProductType::from([ty]));
635            if let Some(computed) = StaticLayout::for_row_type(&layout) {
636                panic!("Expected row type not to have a constant BSATN layout!\nRow type: {layout:#?}\nBSATN layout: {computed:#?}");
637            }
638        }
639    }
640
641    proptest! {
642        // The tests `known_bsatn_same_as_bflatn_from`
643        // and `known_bflatn_same_as_pv_from` generate a lot of rejects,
644        // as a vast majority of the space of `ProductType` does not have a fixed BSATN length.
645        // Writing a proptest generator which produces only types that have a fixed BSATN length
646        // seems hard, because we'd have to generate sums with known matching layouts,
647        // so we just bump the `max_global_rejects` up as high as it'll go and move on with our lives.
648        //
649        // Note that I (pgoldman 2024-03-21) tried modifying `generate_typed_row`
650        // to not emit `String`, `Array` or `Map` types (the trivially var-len types),
651        // but did not see a meaningful decrease in the number of rejects.
652        // This is because a majority of the var-len BSATN types in the `generate_typed_row` space
653        // are due to sums with inconsistent payload layouts.
654        //
655        // We still include the test `known_bsatn_same_as_bsatn_from`
656        // because it tests row types not covered in `known_types_expected_layout`,
657        // especially larger types with unusual sequences of aligned fields.
658        #![proptest_config(ProptestConfig { max_global_rejects: 65536, ..Default::default()})]
659
660        #[test]
661        fn known_bsatn_same_as_bflatn_from((ty, val) in generate_typed_row()) {
662            let pool = PagePool::new_for_test();
663            let mut blob_store = HashMapBlobStore::default();
664            let mut table = crate::table::test::table(ty);
665            let Some(static_layout) = table.static_layout().cloned() else {
666                // `ty` has a var-len member or a sum with different payload lengths,
667                // so the fast path doesn't apply.
668                return Err(TestCaseError::reject("Var-length type"));
669            };
670
671            let (_, row_ref) = table.insert(&pool, &mut blob_store, &val).unwrap();
672            let bytes = row_ref.get_row_data();
673
674            let slow_path = bsatn::to_vec(&row_ref).unwrap();
675
676            let fast_path = unsafe {
677                static_layout.serialize_row_into_vec(bytes)
678            };
679
680            let mut fast_path2 = Vec::new();
681            unsafe {
682                static_layout.serialize_row_extend(&mut fast_path2, bytes)
683            };
684
685            assert_eq!(slow_path, fast_path);
686            assert_eq!(slow_path, fast_path2);
687        }
688
689        #[test]
690        fn known_bflatn_same_as_pv_from((ty, val) in generate_typed_row()) {
691            let pool = PagePool::new_for_test();
692            let mut blob_store = HashMapBlobStore::default();
693            let mut table = crate::table::test::table(ty);
694            let Some(static_layout) = table.static_layout().cloned() else {
695                // `ty` has a var-len member or a sum with different payload lengths,
696                // so the fast path doesn't apply.
697                return Err(TestCaseError::reject("Var-length type"));
698            };
699            let bsatn = bsatn::to_vec(&val).unwrap();
700
701            let (_, row_ref) = table.insert(&pool, &mut blob_store, &val).unwrap();
702            let slow_path = row_ref.get_row_data();
703
704            let mut fast_path = vec![0u8; slow_path.len()];
705            unsafe {
706                static_layout.deserialize_row_into(&mut fast_path, &bsatn);
707            };
708
709            assert_eq!(slow_path, fast_path);
710        }
711    }
712}