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