use crate::types::*;
use std::io;
pub const ROW_MAGIC: &[u8; 4] = b"PROW";
pub const ROW_FORMAT_VERSION: u16 = 1;
pub const ROW_FORMAT_VERSION_V2: u16 = 2;
pub const MAX_ROW_FORMAT_VERSION: u16 = ROW_FORMAT_VERSION_V2;
pub const ROW_PREFIX_SIZE: usize = 6;
pub const OVERFLOW_STUB_SIZE: usize = 24;
pub const OVERFLOW_STUB_VERSION: u8 = 1;
pub const OVERFLOW_MIN: usize = 256;
pub const MAX_VALUE_SIZE: usize = 64 * 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OverflowStub {
pub total_len: u64,
pub first_page: u32,
pub value_crc32: u32,
pub flags: u8,
pub stub_version: u8,
}
impl OverflowStub {
pub fn new(total_len: u64, first_page: u32, value_crc32: u32) -> Self {
OverflowStub {
total_len,
first_page,
value_crc32,
flags: 0,
stub_version: OVERFLOW_STUB_VERSION,
}
}
pub fn to_bytes(&self) -> [u8; OVERFLOW_STUB_SIZE] {
let mut b = [0u8; OVERFLOW_STUB_SIZE];
b[0..8].copy_from_slice(&self.total_len.to_le_bytes());
b[8..12].copy_from_slice(&self.first_page.to_le_bytes());
b[12..16].copy_from_slice(&self.value_crc32.to_le_bytes());
b[16] = self.flags;
b[17] = self.stub_version;
b
}
pub fn from_bytes(b: &[u8]) -> Option<OverflowStub> {
if b.len() != OVERFLOW_STUB_SIZE {
return None;
}
let stub_version = b[17];
if stub_version != OVERFLOW_STUB_VERSION {
return None;
}
Some(OverflowStub {
total_len: u64::from_le_bytes(b[0..8].try_into().expect("8-byte slice")),
first_page: u32::from_le_bytes(b[8..12].try_into().expect("4-byte slice")),
value_crc32: u32::from_le_bytes(b[12..16].try_into().expect("4-byte slice")),
flags: b[16],
stub_version,
})
}
}
#[inline]
pub fn row_format_version(data: &[u8]) -> io::Result<u16> {
if data.len() >= ROW_PREFIX_SIZE && &data[0..4] == ROW_MAGIC {
let version = u16::from_le_bytes(data[4..6].try_into().expect("2-byte row version"));
if version > MAX_ROW_FORMAT_VERSION {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("unsupported row format version: {version}"),
));
}
Ok(version)
} else {
Ok(0)
}
}
#[inline]
pub fn row_is_v2(data: &[u8]) -> bool {
data.len() >= ROW_PREFIX_SIZE
&& &data[0..4] == ROW_MAGIC
&& u16::from_le_bytes([data[4], data[5]]) == ROW_FORMAT_VERSION_V2
}
#[inline]
pub fn validate_row_format(data: &[u8]) -> io::Result<()> {
let _ = row_format_version(data)?;
Ok(())
}
#[inline]
fn row_body_offset(data: &[u8]) -> io::Result<usize> {
if data.len() >= ROW_PREFIX_SIZE && &data[0..4] == ROW_MAGIC {
row_format_version(data)?;
Ok(ROW_PREFIX_SIZE)
} else {
Ok(0)
}
}
#[inline]
fn row_body(data: &[u8]) -> &[u8] {
let offset = row_body_offset(data).expect("unsupported row format version");
&data[offset..]
}
fn prepend_row_prefix(out: &mut Vec<u8>) {
let body_len = out.len();
out.resize(body_len + ROW_PREFIX_SIZE, 0);
out.copy_within(0..body_len, ROW_PREFIX_SIZE);
out[0..4].copy_from_slice(ROW_MAGIC);
out[4..6].copy_from_slice(&ROW_FORMAT_VERSION.to_le_bytes());
}
pub fn encode_row(schema: &Schema, values: &[Value]) -> Vec<u8> {
let mut out = Vec::new();
encode_row_into(schema, values, &mut out);
out
}
pub fn try_encode_row(schema: &Schema, values: &[Value]) -> io::Result<Vec<u8>> {
let mut out = Vec::new();
try_encode_row_into(schema, values, &mut out)?;
Ok(out)
}
pub fn encode_row_into(schema: &Schema, values: &[Value], out: &mut Vec<u8>) {
let layout = RowLayout::new(schema);
encode_row_into_with_layout(schema, &layout, values, out);
}
pub fn try_encode_row_into(schema: &Schema, values: &[Value], out: &mut Vec<u8>) -> io::Result<()> {
let layout = RowLayout::new(schema);
try_encode_row_into_with_layout(schema, &layout, values, out)
}
#[inline]
pub fn encode_row_into_with_layout(
schema: &Schema,
layout: &RowLayout,
values: &[Value],
out: &mut Vec<u8>,
) {
debug_assert_eq!(values.len(), schema.columns.len());
let n_cols = schema.columns.len();
let bitmap_size = layout.bitmap_size;
let fixed_region_size = layout.fixed_region_size;
let n_var = layout.n_var;
let n_offsets = n_var + 1;
let mut bitmap_stack = [0u8; 32];
let mut bitmap_heap: Vec<u8>;
let bitmap_slice: &mut [u8] = if bitmap_size <= 32 {
&mut bitmap_stack[..bitmap_size]
} else {
bitmap_heap = vec![0u8; bitmap_size];
&mut bitmap_heap[..]
};
let mut var_data_size: usize = 0;
for (i, val) in values.iter().enumerate() {
match val {
Value::Empty => {
bitmap_slice[i >> 3] |= 1 << (i & 7);
}
Value::Str(s) => var_data_size += s.len(),
Value::Bytes(b) => var_data_size += b.len(),
Value::Json(b) => var_data_size += b.len(),
_ => {}
}
}
let body_size = bitmap_size + fixed_region_size + n_offsets * 2 + var_data_size;
let total_size = 2 + body_size;
debug_assert!(
total_size <= u16::MAX as usize,
"row too large: {total_size} bytes exceeds 64KB limit"
);
debug_assert!(
var_data_size <= u16::MAX as usize,
"variable data too large: {var_data_size} bytes exceeds 64KB limit"
);
out.clear();
out.resize(total_size, 0);
out[0..2].copy_from_slice(&(total_size as u16).to_le_bytes());
let bitmap_start = 2;
out[bitmap_start..bitmap_start + bitmap_size].copy_from_slice(bitmap_slice);
let fixed_start = bitmap_start + bitmap_size;
let offsets_start = fixed_start + fixed_region_size;
let var_data_start = offsets_start + n_offsets * 2;
let mut var_cursor: u16 = 0;
let mut off_slot: usize = 0;
for (i, val) in values.iter().enumerate().take(n_cols) {
if let Some(off) = layout.fixed_offsets[i] {
let pos = fixed_start + off;
match val {
Value::Empty => {}
Value::Int(v) => {
out[pos..pos + 8].copy_from_slice(&v.to_le_bytes());
}
Value::Float(v) => {
out[pos..pos + 8].copy_from_slice(&v.to_le_bytes());
}
Value::Bool(v) => {
out[pos] = if *v { 1 } else { 0 };
}
Value::DateTime(v) => {
out[pos..pos + 8].copy_from_slice(&v.to_le_bytes());
}
Value::Uuid(v) => {
out[pos..pos + 16].copy_from_slice(v);
}
_ => unreachable!("fixed column with non-fixed value"),
}
} else {
let off_pos = offsets_start + off_slot * 2;
out[off_pos..off_pos + 2].copy_from_slice(&var_cursor.to_le_bytes());
off_slot += 1;
match val {
Value::Empty => {} Value::Str(s) => {
let len = s.len();
let abs = var_data_start + var_cursor as usize;
out[abs..abs + len].copy_from_slice(s.as_bytes());
var_cursor += len as u16;
}
Value::Bytes(b) => {
let len = b.len();
let abs = var_data_start + var_cursor as usize;
out[abs..abs + len].copy_from_slice(b);
var_cursor += len as u16;
}
Value::Json(b) => {
let len = b.len();
let abs = var_data_start + var_cursor as usize;
out[abs..abs + len].copy_from_slice(b);
var_cursor += len as u16;
}
_ => unreachable!("variable column with non-variable value"),
}
}
}
let end_pos = offsets_start + off_slot * 2;
out[end_pos..end_pos + 2].copy_from_slice(&var_cursor.to_le_bytes());
debug_assert_eq!(out.len(), total_size);
prepend_row_prefix(out);
}
pub fn try_encode_row_into_with_layout(
schema: &Schema,
layout: &RowLayout,
values: &[Value],
out: &mut Vec<u8>,
) -> io::Result<()> {
let n_cols = schema.columns.len();
let bitmap_size = layout.bitmap_size;
let fixed_region_size = layout.fixed_region_size;
let n_var = layout.n_var;
let n_offsets = n_var + 1;
let mut bitmap_stack = [0u8; 32];
let mut bitmap_heap: Vec<u8>;
let bitmap_slice: &mut [u8] = if bitmap_size <= 32 {
&mut bitmap_stack[..bitmap_size]
} else {
bitmap_heap = vec![0u8; bitmap_size];
&mut bitmap_heap[..]
};
let mut var_data_size: usize = 0;
for (i, val) in values.iter().enumerate() {
match val {
Value::Empty => {
bitmap_slice[i >> 3] |= 1 << (i & 7);
}
Value::Str(s) => {
if s.len() > u16::MAX as usize {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"row too large: string value in column '{}' is {} bytes, exceeds 64KB limit",
schema.columns[i].name, s.len()
),
));
}
var_data_size += s.len();
}
Value::Bytes(b) => {
if b.len() > u16::MAX as usize {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"row too large: bytes value in column '{}' is {} bytes, exceeds 64KB limit",
schema.columns[i].name, b.len()
),
));
}
var_data_size += b.len();
}
Value::Json(b) => {
if b.len() > u16::MAX as usize {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"row too large: json value in column '{}' is {} bytes, exceeds 64KB limit",
schema.columns[i].name, b.len()
),
));
}
var_data_size += b.len();
}
_ => {}
}
}
let body_size = bitmap_size + fixed_region_size + n_offsets * 2 + var_data_size;
let total_size = 2 + body_size;
if total_size > u16::MAX as usize {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("row too large: {total_size} bytes exceeds 64KB limit"),
));
}
if var_data_size > u16::MAX as usize {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("row too large: variable data is {var_data_size} bytes, exceeds 64KB limit"),
));
}
out.clear();
out.resize(total_size, 0);
out[0..2].copy_from_slice(&(total_size as u16).to_le_bytes());
let bitmap_start = 2;
out[bitmap_start..bitmap_start + bitmap_size].copy_from_slice(bitmap_slice);
let fixed_start = bitmap_start + bitmap_size;
let offsets_start = fixed_start + fixed_region_size;
let var_data_start = offsets_start + n_offsets * 2;
let mut var_cursor: u16 = 0;
let mut off_slot: usize = 0;
for (i, val) in values.iter().enumerate().take(n_cols) {
if let Some(off) = layout.fixed_offsets[i] {
let pos = fixed_start + off;
match val {
Value::Empty => {}
Value::Int(v) => {
out[pos..pos + 8].copy_from_slice(&v.to_le_bytes());
}
Value::Float(v) => {
out[pos..pos + 8].copy_from_slice(&v.to_le_bytes());
}
Value::Bool(v) => {
out[pos] = if *v { 1 } else { 0 };
}
Value::DateTime(v) => {
out[pos..pos + 8].copy_from_slice(&v.to_le_bytes());
}
Value::Uuid(v) => {
out[pos..pos + 16].copy_from_slice(v);
}
_ => unreachable!("fixed column with non-fixed value"),
}
} else {
let off_pos = offsets_start + off_slot * 2;
out[off_pos..off_pos + 2].copy_from_slice(&var_cursor.to_le_bytes());
off_slot += 1;
match val {
Value::Empty => {}
Value::Str(s) => {
let len = s.len();
let abs = var_data_start + var_cursor as usize;
out[abs..abs + len].copy_from_slice(s.as_bytes());
var_cursor += len as u16;
}
Value::Bytes(b) => {
let len = b.len();
let abs = var_data_start + var_cursor as usize;
out[abs..abs + len].copy_from_slice(b);
var_cursor += len as u16;
}
Value::Json(b) => {
let len = b.len();
let abs = var_data_start + var_cursor as usize;
out[abs..abs + len].copy_from_slice(b);
var_cursor += len as u16;
}
_ => unreachable!("variable column with non-variable value"),
}
}
}
let end_pos = offsets_start + off_slot * 2;
out[end_pos..end_pos + 2].copy_from_slice(&var_cursor.to_le_bytes());
debug_assert_eq!(out.len(), total_size);
prepend_row_prefix(out);
Ok(())
}
pub struct RowLayout {
fixed_offsets: Vec<Option<usize>>,
fixed_region_size: usize,
var_index: Vec<Option<usize>>,
n_var: usize,
bitmap_size: usize,
}
impl RowLayout {
#[inline(always)]
pub fn fixed_offset(&self, col_idx: usize) -> Option<usize> {
self.fixed_offsets[col_idx]
}
#[inline(always)]
pub fn bitmap_size(&self) -> usize {
self.bitmap_size
}
#[inline(always)]
pub fn n_var(&self) -> usize {
self.n_var
}
#[inline(always)]
pub fn overflow_bitmap_size(&self) -> usize {
self.n_var.div_ceil(8)
}
#[inline(always)]
pub fn var_index(&self, col_idx: usize) -> Option<usize> {
self.var_index[col_idx]
}
pub fn new(schema: &Schema) -> Self {
let n_cols = schema.columns.len();
let bitmap_size = n_cols.div_ceil(8);
let mut fixed_offsets = vec![None; n_cols];
let mut var_index = vec![None; n_cols];
let mut fixed_pos: usize = 0;
let mut var_count: usize = 0;
for (i, col) in schema.columns.iter().enumerate() {
if is_fixed_size(col.type_id) {
fixed_offsets[i] = Some(fixed_pos);
fixed_pos += fixed_size(col.type_id)
.expect("invariant: is_fixed_size(type_id) is true in this branch");
} else {
var_index[i] = Some(var_count);
var_count += 1;
}
}
RowLayout {
fixed_offsets,
fixed_region_size: fixed_pos,
var_index,
n_var: var_count,
bitmap_size,
}
}
}
#[inline]
pub fn decode_column(schema: &Schema, layout: &RowLayout, data: &[u8], col_idx: usize) -> Value {
let is_v2 = row_is_v2(data);
let data = row_body(data);
let col = &schema.columns[col_idx];
let bitmap_start = 2; let is_null = (data[bitmap_start + col_idx / 8] >> (col_idx % 8)) & 1 == 1;
if is_null {
return Value::Empty;
}
let ovf_bitmap_size = if is_v2 {
layout.overflow_bitmap_size()
} else {
0
};
let ovf_start = 2 + layout.bitmap_size;
let fixed_start = ovf_start + ovf_bitmap_size;
if let Some(offset) = layout.fixed_offsets[col_idx] {
let pos = fixed_start + offset;
match col.type_id {
TypeId::Int => Value::Int(i64::from_le_bytes(
data[pos..pos + 8]
.try_into()
.expect("invariant: 8-byte slice"),
)),
TypeId::Float => Value::Float(f64::from_le_bytes(
data[pos..pos + 8]
.try_into()
.expect("invariant: 8-byte slice"),
)),
TypeId::Bool => Value::Bool(data[pos] != 0),
TypeId::DateTime => Value::DateTime(i64::from_le_bytes(
data[pos..pos + 8]
.try_into()
.expect("invariant: 8-byte slice"),
)),
TypeId::Uuid => {
let mut v = [0u8; 16];
v.copy_from_slice(&data[pos..pos + 16]);
Value::Uuid(v)
}
_ => unreachable!(),
}
} else {
let vi = layout.var_index[col_idx]
.expect("invariant: column is variable-length (not in fixed_offsets)");
if is_v2 {
let ovf_bitmap = &data[ovf_start..ovf_start + ovf_bitmap_size];
if (ovf_bitmap[vi >> 3] >> (vi & 7)) & 1 == 1 {
return Value::Empty;
}
}
let offset_table_start = fixed_start + layout.fixed_region_size;
let off_pos = offset_table_start + vi * 2;
let next_off_pos = offset_table_start + (vi + 1) * 2;
let var_offset = u16::from_le_bytes(
data[off_pos..off_pos + 2]
.try_into()
.expect("invariant: 2-byte slice"),
) as usize;
let var_next = u16::from_le_bytes(
data[next_off_pos..next_off_pos + 2]
.try_into()
.expect("invariant: 2-byte slice"),
) as usize;
let var_data_start = offset_table_start + (layout.n_var + 1) * 2;
let start = var_data_start + var_offset;
let end = var_data_start + var_next;
let bytes = &data[start..end];
match col.type_id {
TypeId::Str => Value::Str(String::from_utf8_lossy(bytes).into_owned()),
TypeId::Bytes => Value::Bytes(bytes.to_vec()),
TypeId::Json => Value::Json(bytes.into()),
_ => unreachable!(),
}
}
}
#[inline]
pub fn patch_var_column_in_place(
bytes: &mut [u8],
layout: &RowLayout,
col_idx: usize,
new_value: Option<&[u8]>,
) -> Option<u16> {
if row_is_v2(bytes) {
return None;
}
let base = row_body_offset(bytes).ok()?;
let var_idx = layout.var_index[col_idx].expect("not a var column");
let n_var = layout.n_var;
let offset_table_start = base + 2 + layout.bitmap_size + layout.fixed_region_size;
let var_data_start = offset_table_start + (n_var + 1) * 2;
let off_pos = offset_table_start + var_idx * 2;
let next_off_pos = offset_table_start + (var_idx + 1) * 2;
let old_var_offset = u16::from_le_bytes(
bytes[off_pos..off_pos + 2]
.try_into()
.expect("invariant: 2-byte slice"),
) as usize;
let old_var_next = u16::from_le_bytes(
bytes[next_off_pos..next_off_pos + 2]
.try_into()
.expect("invariant: 2-byte slice"),
) as usize;
let old_var_len = old_var_next - old_var_offset;
let new_var_len = new_value.map(|v| v.len()).unwrap_or(0);
if new_var_len > old_var_len {
return None; }
let delta = old_var_len - new_var_len;
let old_var_abs_start = var_data_start + old_var_offset;
let old_var_abs_end = var_data_start + old_var_next;
let old_row_len = bytes.len();
if let Some(v) = new_value {
bytes[old_var_abs_start..old_var_abs_start + new_var_len].copy_from_slice(v);
}
if delta > 0 {
bytes.copy_within(
old_var_abs_end..old_row_len,
old_var_abs_start + new_var_len,
);
for vi in (var_idx + 1)..=n_var {
let pos = offset_table_start + vi * 2;
let old_off = u16::from_le_bytes(
bytes[pos..pos + 2]
.try_into()
.expect("invariant: 2-byte slice"),
);
let new_off = old_off - delta as u16;
bytes[pos..pos + 2].copy_from_slice(&new_off.to_le_bytes());
}
}
let bitmap_byte = base + 2 + col_idx / 8;
let bit_mask = 1u8 << (col_idx % 8);
if new_value.is_none() {
bytes[bitmap_byte] |= bit_mask;
} else {
bytes[bitmap_byte] &= !bit_mask;
}
let new_row_len = old_row_len - delta;
let new_body_len = new_row_len - base;
bytes[base..base + 2].copy_from_slice(&(new_body_len as u16).to_le_bytes());
Some(new_row_len as u16)
}
#[inline]
pub fn decode_row(schema: &Schema, data: &[u8]) -> Row {
let is_v2 = row_is_v2(data);
let data = row_body(data);
let n_cols = schema.columns.len();
let bitmap_size = n_cols.div_ceil(8);
let n_var = schema
.columns
.iter()
.filter(|c| !is_fixed_size(c.type_id))
.count();
let ovf_bitmap_size = if is_v2 { n_var.div_ceil(8) } else { 0 };
let mut pos = 2;
let null_bitmap = &data[pos..pos + bitmap_size];
pos += bitmap_size;
let ovf_bitmap = &data[pos..pos + ovf_bitmap_size];
pos += ovf_bitmap_size;
let mut values = vec![Value::Empty; n_cols];
for (i, col) in schema.columns.iter().enumerate() {
if !is_fixed_size(col.type_id) {
continue;
}
let is_null = (null_bitmap[i / 8] >> (i % 8)) & 1 == 1;
let sz = fixed_size(col.type_id)
.expect("invariant: is_fixed_size(type_id) is true (non-fixed columns skipped above)");
if is_null {
pos += sz; } else {
values[i] = match col.type_id {
TypeId::Int => {
let v = i64::from_le_bytes(
data[pos..pos + 8]
.try_into()
.expect("invariant: 8-byte slice"),
);
Value::Int(v)
}
TypeId::Float => {
let v = f64::from_le_bytes(
data[pos..pos + 8]
.try_into()
.expect("invariant: 8-byte slice"),
);
Value::Float(v)
}
TypeId::Bool => Value::Bool(data[pos] != 0),
TypeId::DateTime => {
let v = i64::from_le_bytes(
data[pos..pos + 8]
.try_into()
.expect("invariant: 8-byte slice"),
);
Value::DateTime(v)
}
TypeId::Uuid => {
let mut v = [0u8; 16];
v.copy_from_slice(&data[pos..pos + 16]);
Value::Uuid(v)
}
_ => unreachable!(),
};
pos += sz;
}
}
let var_col_indices: Vec<usize> = schema
.columns
.iter()
.enumerate()
.filter(|(_, c)| !is_fixed_size(c.type_id))
.map(|(i, _)| i)
.collect();
let n_var = var_col_indices.len();
let n_offsets = n_var + 1;
let mut var_offsets = Vec::with_capacity(n_offsets);
for _ in 0..n_offsets {
let off = u16::from_le_bytes(
data[pos..pos + 2]
.try_into()
.expect("invariant: 2-byte slice"),
);
var_offsets.push(off as usize);
pos += 2;
}
let var_data_start = pos;
for (vi, &col_idx) in var_col_indices.iter().enumerate() {
let is_null = (null_bitmap[col_idx / 8] >> (col_idx % 8)) & 1 == 1;
if is_null {
continue;
}
if is_v2 && (ovf_bitmap[vi >> 3] >> (vi & 7)) & 1 == 1 {
continue;
}
let start = var_data_start + var_offsets[vi];
let end = var_data_start + var_offsets[vi + 1];
let bytes = &data[start..end];
values[col_idx] = match schema.columns[col_idx].type_id {
TypeId::Str => Value::Str(String::from_utf8_lossy(bytes).into_owned()),
TypeId::Bytes => Value::Bytes(bytes.to_vec()),
TypeId::Json => Value::Json(bytes.into()),
_ => unreachable!(),
};
}
values
}
pub fn encode_row_v2_into(
schema: &Schema,
layout: &RowLayout,
values: &[Value],
spilled: &[Option<OverflowStub>],
out: &mut Vec<u8>,
) {
debug_assert_eq!(values.len(), schema.columns.len());
debug_assert_eq!(spilled.len(), layout.n_var);
let n_cols = schema.columns.len();
let null_bitmap_size = layout.bitmap_size;
let ovf_bitmap_size = layout.overflow_bitmap_size();
let fixed_region_size = layout.fixed_region_size;
let n_var = layout.n_var;
let n_offsets = n_var + 1;
let mut null_bitmap = vec![0u8; null_bitmap_size];
let mut ovf_bitmap = vec![0u8; ovf_bitmap_size];
let mut var_data_size: usize = 0;
for (i, val) in values.iter().enumerate() {
match layout.var_index[i] {
None => {
if matches!(val, Value::Empty) {
null_bitmap[i >> 3] |= 1 << (i & 7);
}
}
Some(vi) => {
if let Some(_stub) = &spilled[vi] {
ovf_bitmap[vi >> 3] |= 1 << (vi & 7);
var_data_size += OVERFLOW_STUB_SIZE;
} else {
match val {
Value::Empty => null_bitmap[i >> 3] |= 1 << (i & 7),
Value::Str(s) => var_data_size += s.len(),
Value::Bytes(b) => var_data_size += b.len(),
Value::Json(b) => var_data_size += b.len(),
_ => {}
}
}
}
}
}
let body_size =
null_bitmap_size + ovf_bitmap_size + fixed_region_size + n_offsets * 2 + var_data_size;
let total_size = 2 + body_size;
out.clear();
out.resize(total_size, 0);
out[0..2].copy_from_slice(&(total_size as u16).to_le_bytes());
let null_start = 2;
out[null_start..null_start + null_bitmap_size].copy_from_slice(&null_bitmap);
let ovf_start = null_start + null_bitmap_size;
out[ovf_start..ovf_start + ovf_bitmap_size].copy_from_slice(&ovf_bitmap);
let fixed_start = ovf_start + ovf_bitmap_size;
let offsets_start = fixed_start + fixed_region_size;
let var_data_start = offsets_start + n_offsets * 2;
let mut var_cursor: u16 = 0;
let mut off_slot: usize = 0;
for (i, val) in values.iter().enumerate().take(n_cols) {
if let Some(off) = layout.fixed_offsets[i] {
let pos = fixed_start + off;
match val {
Value::Empty => {}
Value::Int(v) => out[pos..pos + 8].copy_from_slice(&v.to_le_bytes()),
Value::Float(v) => out[pos..pos + 8].copy_from_slice(&v.to_le_bytes()),
Value::Bool(v) => out[pos] = u8::from(*v),
Value::DateTime(v) => out[pos..pos + 8].copy_from_slice(&v.to_le_bytes()),
Value::Uuid(v) => out[pos..pos + 16].copy_from_slice(v),
_ => unreachable!("fixed column with non-fixed value"),
}
} else {
let vi = layout.var_index[i].expect("var column");
let off_pos = offsets_start + off_slot * 2;
out[off_pos..off_pos + 2].copy_from_slice(&var_cursor.to_le_bytes());
off_slot += 1;
if let Some(stub) = &spilled[vi] {
let abs = var_data_start + var_cursor as usize;
out[abs..abs + OVERFLOW_STUB_SIZE].copy_from_slice(&stub.to_bytes());
var_cursor += OVERFLOW_STUB_SIZE as u16;
} else {
match val {
Value::Empty => {}
Value::Str(s) => {
let len = s.len();
let abs = var_data_start + var_cursor as usize;
out[abs..abs + len].copy_from_slice(s.as_bytes());
var_cursor += len as u16;
}
Value::Bytes(b) => {
let len = b.len();
let abs = var_data_start + var_cursor as usize;
out[abs..abs + len].copy_from_slice(b);
var_cursor += len as u16;
}
Value::Json(b) => {
let len = b.len();
let abs = var_data_start + var_cursor as usize;
out[abs..abs + len].copy_from_slice(b);
var_cursor += len as u16;
}
_ => unreachable!("variable column with non-variable value"),
}
}
}
}
let end_pos = offsets_start + off_slot * 2;
out[end_pos..end_pos + 2].copy_from_slice(&var_cursor.to_le_bytes());
let body_len = out.len();
out.resize(body_len + ROW_PREFIX_SIZE, 0);
out.copy_within(0..body_len, ROW_PREFIX_SIZE);
out[0..4].copy_from_slice(ROW_MAGIC);
out[4..6].copy_from_slice(&ROW_FORMAT_VERSION_V2.to_le_bytes());
}
pub fn raw_stub(
schema: &Schema,
layout: &RowLayout,
data: &[u8],
col_idx: usize,
) -> Option<OverflowStub> {
if !row_is_v2(data) || col_idx >= schema.columns.len() {
return None;
}
let vi = layout.var_index[col_idx]?;
let body = &data[ROW_PREFIX_SIZE..];
let ovf_start = 2 + layout.bitmap_size;
let ovf_bitmap = &body[ovf_start..ovf_start + layout.overflow_bitmap_size()];
if (ovf_bitmap[vi >> 3] >> (vi & 7)) & 1 == 0 {
return None; }
let fixed_start = ovf_start + layout.overflow_bitmap_size();
let offsets_start = fixed_start + layout.fixed_region_size;
let off_pos = offsets_start + vi * 2;
let var_offset =
u16::from_le_bytes(body[off_pos..off_pos + 2].try_into().expect("2-byte")) as usize;
let var_data_start = offsets_start + (layout.n_var + 1) * 2;
let start = var_data_start + var_offset;
OverflowStub::from_bytes(&body[start..start + OVERFLOW_STUB_SIZE])
}
pub fn for_each_stub<F: FnMut(usize, OverflowStub)>(
schema: &Schema,
layout: &RowLayout,
data: &[u8],
mut f: F,
) {
if !row_is_v2(data) {
return;
}
for (col_idx, _col) in schema.columns.iter().enumerate() {
if let Some(stub) = raw_stub(schema, layout, data, col_idx) {
f(col_idx, stub);
}
}
}
pub fn decode_row_v2<F>(
schema: &Schema,
layout: &RowLayout,
data: &[u8],
mut fetch: F,
) -> io::Result<Row>
where
F: FnMut(&OverflowStub) -> io::Result<Vec<u8>>,
{
if !row_is_v2(data) {
return Ok(decode_row(schema, data));
}
let body = &data[ROW_PREFIX_SIZE..];
let n_cols = schema.columns.len();
let null_bitmap_size = layout.bitmap_size;
let ovf_bitmap_size = layout.overflow_bitmap_size();
let null_start = 2;
let null_bitmap = &body[null_start..null_start + null_bitmap_size];
let ovf_start = null_start + null_bitmap_size;
let ovf_bitmap = &body[ovf_start..ovf_start + ovf_bitmap_size];
let fixed_start = ovf_start + ovf_bitmap_size;
let offsets_start = fixed_start + layout.fixed_region_size;
let var_data_start = offsets_start + (layout.n_var + 1) * 2;
let mut values = vec![Value::Empty; n_cols];
for (i, col) in schema.columns.iter().enumerate() {
if let Some(off) = layout.fixed_offsets[i] {
let is_null = (null_bitmap[i >> 3] >> (i & 7)) & 1 == 1;
if is_null {
continue;
}
let pos = fixed_start + off;
values[i] = match col.type_id {
TypeId::Int => Value::Int(i64::from_le_bytes(
body[pos..pos + 8].try_into().expect("8-byte"),
)),
TypeId::Float => Value::Float(f64::from_le_bytes(
body[pos..pos + 8].try_into().expect("8-byte"),
)),
TypeId::Bool => Value::Bool(body[pos] != 0),
TypeId::DateTime => Value::DateTime(i64::from_le_bytes(
body[pos..pos + 8].try_into().expect("8-byte"),
)),
TypeId::Uuid => {
let mut v = [0u8; 16];
v.copy_from_slice(&body[pos..pos + 16]);
Value::Uuid(v)
}
_ => unreachable!(),
};
} else {
let vi = layout.var_index[i].expect("var column");
let is_null = (null_bitmap[i >> 3] >> (i & 7)) & 1 == 1;
if is_null {
continue;
}
let off_pos = offsets_start + vi * 2;
let next_off_pos = offsets_start + (vi + 1) * 2;
let var_offset =
u16::from_le_bytes(body[off_pos..off_pos + 2].try_into().expect("2-byte")) as usize;
let var_next = u16::from_le_bytes(
body[next_off_pos..next_off_pos + 2]
.try_into()
.expect("2-byte"),
) as usize;
let start = var_data_start + var_offset;
let end = var_data_start + var_next;
let is_spilled = (ovf_bitmap[vi >> 3] >> (vi & 7)) & 1 == 1;
let bytes: Vec<u8> = if is_spilled {
let stub = OverflowStub::from_bytes(&body[start..end]).ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidData, "malformed overflow stub")
})?;
fetch(&stub)?
} else {
body[start..end].to_vec()
};
values[i] = match col.type_id {
TypeId::Str => Value::Str(String::from_utf8_lossy(&bytes).into_owned()),
TypeId::Bytes => Value::Bytes(bytes),
TypeId::Json => Value::Json(bytes.into()),
_ => unreachable!(),
};
}
}
Ok(values)
}
pub fn rehydrate_v2_to_v1<F>(
schema: &Schema,
layout: &RowLayout,
data: &[u8],
fetch: F,
) -> io::Result<Vec<u8>>
where
F: FnMut(&OverflowStub) -> io::Result<Vec<u8>>,
{
if !row_is_v2(data) {
return Ok(data.to_vec());
}
let values = decode_row_v2(schema, layout, data, fetch)?;
let mut out = Vec::new();
try_encode_row_into_with_layout(schema, layout, &values, &mut out)?;
Ok(out)
}
pub fn v1_encoded_len(layout: &RowLayout, values: &[Value]) -> usize {
let n_offsets = layout.n_var + 1;
let mut var_data: usize = 0;
for val in values {
match val {
Value::Str(s) => var_data = var_data.saturating_add(s.len()),
Value::Bytes(b) => var_data = var_data.saturating_add(b.len()),
Value::Json(b) => var_data = var_data.saturating_add(b.len()),
_ => {}
}
}
ROW_PREFIX_SIZE + 2 + layout.bitmap_size + layout.fixed_region_size + n_offsets * 2 + var_data
}
pub fn plan_spill(
layout: &RowLayout,
values: &[Value],
v1_len: usize,
is_indexed: &[bool],
) -> Vec<usize> {
if v1_len <= crate::page::MAX_ROW_DATA_SIZE {
return Vec::new();
}
let mut cands: Vec<(usize, usize)> = values
.iter()
.enumerate()
.filter_map(|(i, v)| {
layout.var_index(i)?;
let len = match v {
Value::Str(s) => s.len(),
Value::Bytes(b) => b.len(),
Value::Json(b) => b.len(),
_ => return None,
};
if len == 0 {
None
} else {
Some((i, len))
}
})
.collect();
cands.sort_by(|a, b| {
let a_indexed = is_indexed.get(a.0).copied().unwrap_or(false);
let b_indexed = is_indexed.get(b.0).copied().unwrap_or(false);
let a_big = a.1 >= OVERFLOW_MIN;
let b_big = b.1 >= OVERFLOW_MIN;
a_indexed
.cmp(&b_indexed)
.then_with(|| b_big.cmp(&a_big))
.then_with(|| b.1.cmp(&a.1))
});
let mut est = v1_len + layout.overflow_bitmap_size();
let mut chosen = Vec::new();
for (col_idx, len) in cands {
if est <= crate::page::MAX_ROW_DATA_SIZE {
break;
}
est -= len.saturating_sub(OVERFLOW_STUB_SIZE);
chosen.push(col_idx);
}
chosen
}
#[cfg(test)]
mod tests {
use super::*;
fn user_schema() -> Schema {
Schema {
table_name: "users".into(),
columns: vec![
ColumnDef {
name: "name".into(),
type_id: TypeId::Str,
required: true,
position: 0,
},
ColumnDef {
name: "email".into(),
type_id: TypeId::Str,
required: true,
position: 1,
},
ColumnDef {
name: "age".into(),
type_id: TypeId::Int,
required: false,
position: 2,
},
ColumnDef {
name: "active".into(),
type_id: TypeId::Bool,
required: true,
position: 3,
},
],
}
}
#[test]
fn test_encode_decode_roundtrip() {
let schema = user_schema();
let row = vec![
Value::Str("Alice".into()),
Value::Str("alice@example.com".into()),
Value::Int(30),
Value::Bool(true),
];
let encoded = encode_row(&schema, &row);
let decoded = decode_row(&schema, &encoded);
assert_eq!(decoded.len(), 4);
assert_eq!(decoded[0], Value::Str("Alice".into()));
assert_eq!(decoded[1], Value::Str("alice@example.com".into()));
assert_eq!(decoded[2], Value::Int(30));
assert_eq!(decoded[3], Value::Bool(true));
}
#[test]
fn test_encode_with_empty_optional() {
let schema = user_schema();
let row = vec![
Value::Str("Bob".into()),
Value::Str("bob@example.com".into()),
Value::Empty,
Value::Bool(false),
];
let encoded = encode_row(&schema, &row);
let decoded = decode_row(&schema, &encoded);
assert_eq!(decoded[2], Value::Empty);
assert_eq!(decoded[3], Value::Bool(false));
assert_eq!(decoded[0], Value::Str("Bob".into()));
}
#[test]
fn test_all_empty() {
let schema = Schema {
table_name: "t".into(),
columns: vec![
ColumnDef {
name: "a".into(),
type_id: TypeId::Int,
required: false,
position: 0,
},
ColumnDef {
name: "b".into(),
type_id: TypeId::Str,
required: false,
position: 1,
},
],
};
let row = vec![Value::Empty, Value::Empty];
let encoded = encode_row(&schema, &row);
let decoded = decode_row(&schema, &encoded);
assert_eq!(decoded[0], Value::Empty);
assert_eq!(decoded[1], Value::Empty);
}
#[test]
fn test_compact_overhead() {
let schema = user_schema();
let row = vec![
Value::Str("Alice".into()),
Value::Str("alice@example.com".into()),
Value::Int(30),
Value::Bool(true),
];
let encoded = encode_row(&schema, &row);
let pure_data = 5 + 17 + 8 + 1; let overhead = encoded.len() - pure_data;
assert!(overhead <= 16, "overhead was {overhead}, expected <= 16");
}
#[test]
fn test_multiple_roundtrips() {
let schema = Schema {
table_name: "t".into(),
columns: vec![
ColumnDef {
name: "id".into(),
type_id: TypeId::Int,
required: true,
position: 0,
},
ColumnDef {
name: "name".into(),
type_id: TypeId::Str,
required: true,
position: 1,
},
ColumnDef {
name: "score".into(),
type_id: TypeId::Float,
required: false,
position: 2,
},
ColumnDef {
name: "uuid".into(),
type_id: TypeId::Uuid,
required: false,
position: 3,
},
],
};
for i in 0..100 {
let row = vec![
Value::Int(i),
Value::Str(format!("name_{i}")),
if i % 3 == 0 {
Value::Empty
} else {
Value::Float(i as f64 * 1.5)
},
if i % 5 == 0 {
Value::Uuid([i as u8; 16])
} else {
Value::Empty
},
];
let encoded = encode_row(&schema, &row);
let decoded = decode_row(&schema, &encoded);
assert_eq!(decoded, row, "roundtrip failed for i={i}");
}
}
#[test]
fn test_patch_var_column_same_size() {
let schema = user_schema();
let row = vec![
Value::Str("Alice".into()),
Value::Str("alice@example.com".into()),
Value::Int(30),
Value::Bool(true),
];
let mut encoded = encode_row(&schema, &row);
let layout = RowLayout::new(&schema);
let new_len = patch_var_column_in_place(&mut encoded, &layout, 0, Some(b"Bobby")).unwrap();
encoded.truncate(new_len as usize);
let decoded = decode_row(&schema, &encoded);
assert_eq!(decoded[0], Value::Str("Bobby".into()));
assert_eq!(decoded[1], Value::Str("alice@example.com".into()));
assert_eq!(decoded[2], Value::Int(30));
assert_eq!(decoded[3], Value::Bool(true));
}
#[test]
fn test_patch_var_column_shrink_first() {
let schema = user_schema();
let row = vec![
Value::Str("Alexandra".into()), Value::Str("alice@example.com".into()),
Value::Int(42),
Value::Bool(false),
];
let mut encoded = encode_row(&schema, &row);
let layout = RowLayout::new(&schema);
let new_len = patch_var_column_in_place(&mut encoded, &layout, 0, Some(b"Eve")).unwrap();
encoded.truncate(new_len as usize);
let decoded = decode_row(&schema, &encoded);
assert_eq!(decoded[0], Value::Str("Eve".into()));
assert_eq!(decoded[1], Value::Str("alice@example.com".into()));
assert_eq!(decoded[2], Value::Int(42));
assert_eq!(decoded[3], Value::Bool(false));
}
#[test]
fn test_patch_var_column_shrink_middle() {
let schema = Schema {
table_name: "U".into(),
columns: vec![
ColumnDef {
name: "name".into(),
type_id: TypeId::Str,
required: true,
position: 0,
},
ColumnDef {
name: "status".into(),
type_id: TypeId::Str,
required: true,
position: 1,
},
ColumnDef {
name: "email".into(),
type_id: TypeId::Str,
required: true,
position: 2,
},
ColumnDef {
name: "age".into(),
type_id: TypeId::Int,
required: false,
position: 3,
},
],
};
let row = vec![
Value::Str("user_42".into()),
Value::Str("inactive".into()), Value::Str("user_42@example.com".into()),
Value::Int(55),
];
let mut encoded = encode_row(&schema, &row);
let layout = RowLayout::new(&schema);
let new_len = patch_var_column_in_place(&mut encoded, &layout, 1, Some(b"senior")).unwrap();
encoded.truncate(new_len as usize);
let decoded = decode_row(&schema, &encoded);
assert_eq!(decoded[0], Value::Str("user_42".into()));
assert_eq!(decoded[1], Value::Str("senior".into()));
assert_eq!(decoded[2], Value::Str("user_42@example.com".into()));
assert_eq!(decoded[3], Value::Int(55));
}
#[test]
fn test_patch_var_column_grow_rejects() {
let schema = user_schema();
let row = vec![
Value::Str("Al".into()), Value::Str("alice@example.com".into()),
Value::Int(30),
Value::Bool(true),
];
let mut encoded = encode_row(&schema, &row);
let layout = RowLayout::new(&schema);
assert!(patch_var_column_in_place(&mut encoded, &layout, 0, Some(b"Alexandra")).is_none());
}
#[test]
fn test_patch_var_column_to_null() {
let schema = user_schema();
let row = vec![
Value::Str("Alice".into()),
Value::Str("alice@example.com".into()),
Value::Int(30),
Value::Bool(true),
];
let mut encoded = encode_row(&schema, &row);
let layout = RowLayout::new(&schema);
let new_len = patch_var_column_in_place(&mut encoded, &layout, 0, None).unwrap();
encoded.truncate(new_len as usize);
let decoded = decode_row(&schema, &encoded);
assert_eq!(decoded[0], Value::Empty);
assert_eq!(decoded[1], Value::Str("alice@example.com".into()));
}
#[test]
fn test_patch_var_column_clears_null_bit() {
let schema = Schema {
table_name: "U".into(),
columns: vec![
ColumnDef {
name: "label".into(),
type_id: TypeId::Str,
required: false,
position: 0,
},
ColumnDef {
name: "fill".into(),
type_id: TypeId::Str,
required: false,
position: 1,
},
],
};
let row = vec![Value::Empty, Value::Str("data".into())];
let mut encoded = encode_row(&schema, &row);
let layout = RowLayout::new(&schema);
assert!(patch_var_column_in_place(&mut encoded, &layout, 0, Some(b"x")).is_none());
}
#[test]
fn test_empty_string_vs_empty_set() {
let schema = Schema {
table_name: "t".into(),
columns: vec![ColumnDef {
name: "s".into(),
type_id: TypeId::Str,
required: false,
position: 0,
}],
};
let row_str = vec![Value::Str("".into())];
let row_empty = vec![Value::Empty];
let enc_str = encode_row(&schema, &row_str);
let enc_empty = encode_row(&schema, &row_empty);
let dec_str = decode_row(&schema, &enc_str);
let dec_empty = decode_row(&schema, &enc_empty);
assert_eq!(dec_str[0], Value::Str("".into()));
assert_eq!(dec_empty[0], Value::Empty);
assert_ne!(dec_str[0], dec_empty[0]); }
#[test]
fn test_try_encode_row_rejects_oversized_row() {
let schema = Schema {
table_name: "t".into(),
columns: vec![ColumnDef {
name: "big".into(),
type_id: TypeId::Str,
required: true,
position: 0,
}],
};
let big_string = "x".repeat(70_000);
let row = vec![Value::Str(big_string)];
let result = try_encode_row(&schema, &row);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
let msg = err.to_string();
assert!(
msg.contains("64KB") || msg.contains("too large"),
"unexpected error message: {msg}"
);
}
#[test]
fn test_try_encode_row_accepts_normal_row() {
let schema = user_schema();
let row = vec![
Value::Str("Alice".into()),
Value::Str("alice@example.com".into()),
Value::Int(30),
Value::Bool(true),
];
let result = try_encode_row(&schema, &row);
assert!(result.is_ok());
let encoded = result.unwrap();
let decoded = decode_row(&schema, &encoded);
assert_eq!(decoded[0], Value::Str("Alice".into()));
}
#[test]
fn test_safe_utf8_decode_handles_invalid_bytes() {
let schema = Schema {
table_name: "t".into(),
columns: vec![ColumnDef {
name: "s".into(),
type_id: TypeId::Str,
required: true,
position: 0,
}],
};
let mut encoded = encode_row(&schema, &[Value::Str("hello".into())]);
let var_data_start = 2 + 1 + 4; if var_data_start + 2 <= encoded.len() {
encoded[var_data_start] = 0xFF;
encoded[var_data_start + 1] = 0xFE;
}
let decoded = decode_row(&schema, &encoded);
matches!(decoded[0], Value::Str(_));
}
#[test]
fn test_overflow_stub_roundtrip() {
let stub = OverflowStub::new(1_234_567_890, 42, 0xDEAD_BEEF);
let bytes = stub.to_bytes();
assert_eq!(bytes.len(), OVERFLOW_STUB_SIZE);
assert_eq!(bytes[18..24], [0u8; 6], "reserved bytes must be zeroed");
assert_eq!(bytes[17], OVERFLOW_STUB_VERSION);
let back = OverflowStub::from_bytes(&bytes).unwrap();
assert_eq!(back, stub);
}
#[test]
fn test_overflow_stub_rejects_unknown_version() {
let mut bytes = OverflowStub::new(10, 1, 2).to_bytes();
bytes[17] = 9; assert!(OverflowStub::from_bytes(&bytes).is_none());
}
#[test]
fn test_row_format_gate_accepts_v2_rejects_v3() {
let schema = user_schema();
let layout = RowLayout::new(&schema);
let values = vec![
Value::Str("Alice".into()),
Value::Str("alice@example.com".into()),
Value::Int(30),
Value::Bool(true),
];
let spilled = vec![None; layout.n_var()];
let mut out = Vec::new();
encode_row_v2_into(&schema, &layout, &values, &spilled, &mut out);
assert_eq!(row_format_version(&out).unwrap(), 2);
assert!(row_is_v2(&out));
let mut v3 = out.clone();
v3[4..6].copy_from_slice(&3u16.to_le_bytes());
assert!(row_format_version(&v3).is_err());
}
#[test]
fn test_v2_all_inline_decodes_same_as_v1() {
let schema = user_schema();
let layout = RowLayout::new(&schema);
let values = vec![
Value::Str("Alice".into()),
Value::Str("alice@example.com".into()),
Value::Int(30),
Value::Bool(true),
];
let spilled = vec![None; layout.n_var()];
let mut v2 = Vec::new();
encode_row_v2_into(&schema, &layout, &values, &spilled, &mut v2);
let decoded = decode_row_v2(&schema, &layout, &v2, |_| {
panic!("no column is spilled; fetch must not run")
})
.unwrap();
assert_eq!(decoded, values);
let rehydrated = rehydrate_v2_to_v1(&schema, &layout, &v2, |_| {
panic!("no column is spilled; fetch must not run")
})
.unwrap();
let v1 = encode_row(&schema, &values);
assert_eq!(rehydrated, v1, "rehydrated v2 must equal the v1 encoding");
}
#[test]
fn test_v2_spilled_column_reassembles_via_fetch() {
let schema = user_schema();
let layout = RowLayout::new(&schema);
let big = "z".repeat(5000);
let values = vec![
Value::Str("Alice".into()),
Value::Str(big.clone()),
Value::Int(30),
Value::Bool(true),
];
let crc = crc32fast::hash(big.as_bytes());
let stub = OverflowStub::new(big.len() as u64, 77, crc);
let email_vi = layout.var_index(1).unwrap();
let mut spilled = vec![None; layout.n_var()];
spilled[email_vi] = Some(stub);
let mut v2 = Vec::new();
encode_row_v2_into(&schema, &layout, &values, &spilled, &mut v2);
assert!(row_is_v2(&v2));
assert!(v2.len() < 200, "v2 row should be small, got {}", v2.len());
let read = raw_stub(&schema, &layout, &v2, 1).unwrap();
assert_eq!(read, stub);
assert!(
raw_stub(&schema, &layout, &v2, 0).is_none(),
"name is inline"
);
let mut seen = Vec::new();
for_each_stub(&schema, &layout, &v2, |ci, s| seen.push((ci, s)));
assert_eq!(seen, vec![(1usize, stub)]);
let big_bytes = big.clone().into_bytes();
let decoded = decode_row_v2(&schema, &layout, &v2, |s| {
assert_eq!(*s, stub);
Ok(big_bytes.clone())
})
.unwrap();
assert_eq!(decoded[0], Value::Str("Alice".into()));
assert_eq!(decoded[1], Value::Str(big.clone()));
assert_eq!(decoded[2], Value::Int(30));
let v1 = rehydrate_v2_to_v1(&schema, &layout, &v2, |_| Ok(big_bytes.clone())).unwrap();
assert_eq!(row_format_version(&v1).unwrap(), 1);
let decoded_v1 = decode_row(&schema, &v1);
assert_eq!(decoded_v1, values);
}
#[test]
fn test_v2_multi_spill_and_null_mix() {
let schema = Schema {
table_name: "docs".into(),
columns: vec![
ColumnDef {
name: "a".into(),
type_id: TypeId::Str,
required: false,
position: 0,
},
ColumnDef {
name: "n".into(),
type_id: TypeId::Int,
required: false,
position: 1,
},
ColumnDef {
name: "b".into(),
type_id: TypeId::Bytes,
required: false,
position: 2,
},
ColumnDef {
name: "c".into(),
type_id: TypeId::Str,
required: false,
position: 3,
},
],
};
let layout = RowLayout::new(&schema);
let a = "a".repeat(4000);
let b = vec![7u8; 3000];
let values = vec![
Value::Str(a.clone()),
Value::Int(99),
Value::Bytes(b.clone()),
Value::Empty, ];
let a_vi = layout.var_index(0).unwrap();
let b_vi = layout.var_index(2).unwrap();
let mut spilled = vec![None; layout.n_var()];
spilled[a_vi] = Some(OverflowStub::new(
a.len() as u64,
10,
crc32fast::hash(a.as_bytes()),
));
spilled[b_vi] = Some(OverflowStub::new(b.len() as u64, 20, crc32fast::hash(&b)));
let mut v2 = Vec::new();
encode_row_v2_into(&schema, &layout, &values, &spilled, &mut v2);
let decoded = decode_row_v2(&schema, &layout, &v2, |s| {
if s.first_page == 10 {
Ok(a.clone().into_bytes())
} else {
Ok(b.clone())
}
})
.unwrap();
assert_eq!(decoded[0], Value::Str(a));
assert_eq!(decoded[1], Value::Int(99));
assert_eq!(decoded[2], Value::Bytes(b));
assert_eq!(decoded[3], Value::Empty);
}
}