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