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}