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}