use crate::types::*;
use std::io;
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(),
_ => {}
}
}
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;
}
_ => 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);
}
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();
}
_ => {}
}
}
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;
}
_ => 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);
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
}
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 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 fixed_start = 2 + layout.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)");
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()),
_ => unreachable!(),
}
}
}
#[inline]
pub fn patch_var_column_in_place(
bytes: &mut [u8],
layout: &RowLayout,
col_idx: usize,
new_value: Option<&[u8]>,
) -> Option<u16> {
let var_idx = layout.var_index[col_idx].expect("not a var column");
let n_var = layout.n_var;
let offset_table_start = 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 = 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;
bytes[0..2].copy_from_slice(&(new_row_len as u16).to_le_bytes());
Some(new_row_len as u16)
}
#[inline]
pub fn decode_row(schema: &Schema, data: &[u8]) -> Row {
let n_cols = schema.columns.len();
let bitmap_size = n_cols.div_ceil(8);
let mut pos = 2;
let null_bitmap = &data[pos..pos + bitmap_size];
pos += 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;
}
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()),
_ => unreachable!(),
};
}
values
}
#[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 <= 10, "overhead was {overhead}, expected <= 10");
}
#[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(_));
}
}