use super::{
COL_SEQNO, COL_USER_KEY, COL_VALUE, COL_VALUE_TYPE, Column, ColumnBatch, RowPageColumns,
TypeTag, column_batch_into_entries, column_batch_to_entries, entries_to_column_batch,
frame_value_cells, frame_value_cells_nullable, page_match_entries, unframe_value_cells,
unframe_value_cells_nullable, unframe_value_cells_with_defaults,
validate_columnar_ingest_batch,
};
use crate::{Slice, ValueType, config::ColumnEncoding, key::InternalKey, value::InternalValue};
#[test]
fn fixed_only_value_framing_has_no_overhead_and_round_trips() {
let tags = [TypeTag::Fixed(4), TypeTag::Fixed(4)];
let blob = frame_value_cells(&[
(TypeTag::Fixed(4), &[1, 0, 0, 0][..]),
(TypeTag::Fixed(4), &[2, 0, 0, 0][..]),
])
.expect("frame");
assert_eq!(blob, vec![1, 0, 0, 0, 2, 0, 0, 0], "no length prefixes");
let cells = unframe_value_cells(&blob, &tags).expect("unframe");
assert_eq!(cells, vec![&[1, 0, 0, 0][..], &[2, 0, 0, 0][..]]);
}
#[test]
fn mixed_value_framing_round_trips() {
let tags = [TypeTag::Bytes, TypeTag::Fixed(1), TypeTag::Bytes];
let blob = frame_value_cells(&[
(TypeTag::Bytes, b"abc"),
(TypeTag::Fixed(1), &[7][..]),
(TypeTag::Bytes, b""),
])
.expect("frame");
let cells = unframe_value_cells(&blob, &tags).expect("unframe");
assert_eq!(cells, vec![&b"abc"[..], &[7][..], &b""[..]]);
}
#[test]
fn empty_value_framing_round_trips() {
let blob = frame_value_cells(&[]).expect("frame");
assert_eq!(blob, Vec::<u8>::new());
assert_eq!(
unframe_value_cells(&blob, &[]).expect("unframe"),
Vec::<&[u8]>::new()
);
}
#[test]
fn unframe_rejects_truncated_blob() {
assert!(unframe_value_cells(&[1, 2], &[TypeTag::Fixed(4)]).is_err());
let mut blob = 8u32.to_le_bytes().to_vec();
blob.extend_from_slice(b"abc"); assert!(unframe_value_cells(&blob, &[TypeTag::Bytes]).is_err());
}
#[test]
fn unframe_rejects_trailing_bytes() {
assert!(unframe_value_cells(&[1, 2, 3], &[TypeTag::Fixed(2)]).is_err());
}
#[test]
fn frame_rejects_fixed_cell_length_mismatch() {
assert!(frame_value_cells(&[(TypeTag::Fixed(4), &[1, 2, 3][..])]).is_err());
}
#[test]
fn nullable_framing_round_trips_mixed_present_and_null() {
let tags = [TypeTag::Fixed(4), TypeTag::Bytes, TypeTag::Fixed(2)];
let blob = frame_value_cells_nullable(&[
(TypeTag::Fixed(4), Some(&[1, 0, 0, 0][..])),
(TypeTag::Bytes, None),
(TypeTag::Fixed(2), Some(&[9, 9][..])),
])
.expect("frame");
assert_eq!(blob[0], 0b0000_0101, "presence bitmap marks the null cell");
assert_eq!(
unframe_value_cells_nullable(&blob, &tags).expect("unframe"),
vec![Some(&[1, 0, 0, 0][..]), None, Some(&[9, 9][..])],
);
}
#[test]
fn nullable_framing_all_null_and_all_present() {
let tags = [TypeTag::Bytes, TypeTag::Fixed(1)];
let all_null = frame_value_cells_nullable(&[(TypeTag::Bytes, None), (TypeTag::Fixed(1), None)])
.expect("frame");
assert_eq!(all_null, vec![0u8], "bitmap only, no bodies");
assert_eq!(
unframe_value_cells_nullable(&all_null, &tags).expect("unframe"),
vec![None, None],
);
let all_present = frame_value_cells_nullable(&[
(TypeTag::Bytes, Some(&b"ab"[..])),
(TypeTag::Fixed(1), Some(&[7][..])),
])
.expect("frame");
assert_eq!(
unframe_value_cells_nullable(&all_present, &tags).expect("unframe"),
vec![Some(&b"ab"[..]), Some(&[7][..])],
);
}
#[test]
fn nullable_framing_rejects_truncated_and_trailing() {
let truncated = alloc::vec![0b0000_0001u8, 1, 2]; assert!(unframe_value_cells_nullable(&truncated, &[TypeTag::Fixed(4)]).is_err());
let mut trailing = frame_value_cells_nullable(&[(TypeTag::Bytes, None)]).expect("frame");
trailing.push(0);
assert!(unframe_value_cells_nullable(&trailing, &[TypeTag::Bytes]).is_err());
}
#[test]
fn nullable_framing_rejects_a_fixed_cell_length_mismatch() {
assert!(
frame_value_cells_nullable(&[(TypeTag::Fixed(4), Some(&[1, 2, 3][..]))]).is_err(),
"a present fixed cell whose length differs from its tag width must be rejected",
);
}
#[test]
fn unframe_with_defaults_substitutes_for_null_cells() {
let blob = frame_value_cells_nullable(&[
(TypeTag::Fixed(2), Some(&[7, 7][..])),
(TypeTag::Fixed(2), None),
])
.expect("frame");
let cells = unframe_value_cells_with_defaults(
&blob,
&[
(TypeTag::Fixed(2), &[7, 7][..]),
(TypeTag::Fixed(2), &[0, 0][..]),
],
)
.expect("unframe with defaults");
assert_eq!(cells, vec![&[7, 7][..], &[0, 0][..]]);
}
#[test]
fn first_user_key_rejects_a_non_key_first_column() {
let fixed_first = ColumnBatch {
row_count: 1,
columns: alloc::vec![Column {
column_id: COL_USER_KEY,
type_tag: TypeTag::Fixed(4),
validity: None,
data: alloc::vec![0, 0, 0, 0].into(),
}],
};
assert!(fixed_first.first_user_key().is_err());
let missing = ColumnBatch {
row_count: 1,
columns: alloc::vec![Column {
column_id: 99,
type_tag: TypeTag::Bytes,
validity: None,
data: alloc::vec![0, 0, 0, 0, 0, 0, 0, 0].into(),
}],
};
assert!(missing.first_user_key().is_err());
}
#[test]
fn first_user_key_returns_the_first_row_key() {
let mut data = Vec::new();
for off in [0u32, 2, 5] {
data.extend_from_slice(&off.to_le_bytes());
}
data.extend_from_slice(b"k0k11");
let batch = ColumnBatch {
row_count: 2,
columns: alloc::vec![Column {
column_id: COL_USER_KEY,
type_tag: TypeTag::Bytes,
validity: None,
data: data.into(),
}],
};
assert_eq!(batch.first_user_key().expect("first key"), Some(&b"k0"[..]));
}
fn entry(user_key: &[u8], seqno: u64, value_type: ValueType, value: &[u8]) -> InternalValue {
InternalValue {
key: InternalKey {
user_key: Slice::from(user_key),
seqno,
value_type,
},
value: Slice::from(value),
}
}
fn assert_entries_eq(a: &[InternalValue], b: &[InternalValue]) {
assert_eq!(a.len(), b.len(), "entry count mismatch");
for (x, y) in a.iter().zip(b) {
assert_eq!(x.key.user_key.as_ref(), y.key.user_key.as_ref());
assert_eq!(x.key.seqno, y.key.seqno);
assert!(x.key.value_type == y.key.value_type, "value_type mismatch");
assert_eq!(x.value.as_ref(), y.value.as_ref());
}
}
fn only_column_expression(batch: &ColumnBatch) -> super::Expression {
let encoded = batch.encode().expect("encode");
let column = &batch.columns[0];
let mut rest = &encoded[13..];
let len =
usize::try_from(crate::table::column_page::take_varint(&mut rest, 5).expect("length"))
.expect("a length a page holds");
super::Values::parse(column.type_tag, batch.row_count, &rest[..len])
.expect("parse")
.describe()
.expect("describe")
}
#[test]
fn a_seqno_column_is_encoded_by_its_ordinals_and_round_trips() {
let seqnos: [u64; 5] = [100, 105, 105, 200, 199];
let data: Vec<u8> = seqnos.iter().flat_map(|s| s.to_le_bytes()).collect();
let batch = ColumnBatch {
row_count: 5,
columns: vec![Column {
column_id: COL_SEQNO,
type_tag: TypeTag::Number(crate::table::columnar::Number::U64_LE),
validity: None,
data: data.clone().into(),
}],
};
assert!(
matches!(
only_column_expression(&batch),
super::Expression::Ordinals(_)
),
"a narrow range of u64 numbers is encoded by its ordinals",
);
let decoded = ColumnBatch::decode(&batch.encode().expect("encode").into()).expect("decode");
assert_eq!(decoded.columns[0].data, data, "the column must round-trip");
}
#[test]
fn an_opaque_fixed_column_is_never_encoded_by_ordinals() {
let data: Vec<u8> = (0u64..8).flat_map(|i| (i * 3).to_le_bytes()).collect();
let batch = ColumnBatch {
row_count: 8,
columns: vec![Column {
column_id: 99,
type_tag: TypeTag::Fixed(8),
validity: None,
data: data.clone().into(),
}],
};
assert_eq!(only_column_expression(&batch), super::Expression::Plain);
let decoded = ColumnBatch::decode(&batch.encode().expect("encode").into()).expect("decode");
assert_eq!(decoded.columns[0].data, data);
}
#[test]
fn from_column_batch_rejects_a_zero_row_batch() {
let empty = entries_to_column_batch(&[]).expect("transpose");
assert!(crate::table::data_block::DataBlock::from_column_batch([empty], 16, &mut 0).is_err());
}
#[test]
fn from_column_batch_rejects_a_group_without_row_pages() {
assert!(crate::table::data_block::DataBlock::from_column_batch([], 16, &mut 0).is_err());
}
#[test]
fn decode_projected_decodes_only_the_wanted_columns() {
let entries = vec![
entry(b"alpha", 10, ValueType::Value, b"v1"),
entry(b"bravo", 9, ValueType::Value, b"v2"),
];
let bytes = entries_to_column_batch(&entries)
.expect("transpose")
.encode()
.expect("encode");
let projected = ColumnBatch::decode_projected(&bytes.clone().into(), &[COL_USER_KEY])
.expect("decode_projected");
assert_eq!(projected.row_count, 2);
assert_eq!(
projected.columns.len(),
1,
"only the projected column is decoded"
);
assert_eq!(projected.columns[0].column_id, COL_USER_KEY);
assert!(
!projected.columns.iter().any(|c| c.column_id == COL_VALUE),
"a key-only projection must not decode the value column"
);
let all = [COL_USER_KEY, COL_SEQNO, COL_VALUE_TYPE, COL_VALUE];
assert_eq!(
ColumnBatch::decode_projected(&bytes.clone().into(), &all).expect("decode_projected all"),
ColumnBatch::decode(&bytes.into()).expect("decode"),
);
}
fn is_view_of(col: &Column, block: &Slice) -> bool {
let start = block.as_ptr() as usize;
let end = start + block.len();
let p = col.data.as_ptr() as usize;
p >= start && p < end
}
#[test]
fn decode_projected_detaches_a_narrow_projection_from_a_value_heavy_block() {
let entries = vec![
entry(b"a", 2, ValueType::Value, &[0xABu8; 4096]),
entry(b"b", 1, ValueType::Value, &[0xCDu8; 4096]),
];
let block: Slice = entries_to_column_batch(&entries)
.expect("transpose")
.encode()
.expect("encode")
.into();
let narrow =
ColumnBatch::decode_projected(&block, &[COL_USER_KEY]).expect("key-only projection");
assert!(
!is_view_of(&narrow.columns[0], &block),
"a key-only projection of a value-heavy block must not pin the block",
);
let full = ColumnBatch::decode(&block).expect("full decode");
let value_col = full
.columns
.iter()
.find(|c| c.column_id == COL_VALUE)
.expect("value column");
assert!(
is_view_of(value_col, &block),
"a full decode covers the block and stays zero-copy",
);
}
#[test]
fn intrinsic_transpose_round_trips_entries() {
let entries = vec![
entry(b"alpha", 10, ValueType::Value, b"v1"),
entry(b"bravo", 9, ValueType::Tombstone, b""),
entry(b"charlie", 8, ValueType::MergeOperand, b"+1"),
];
let batch = entries_to_column_batch(&entries).expect("transpose");
assert_eq!(batch.row_count, 3);
assert_eq!(batch.columns.len(), 4, "four intrinsic columns");
let back = column_batch_to_entries(&batch).expect("untranspose");
assert_entries_eq(&entries, &back);
let bytes = batch.encode().expect("encode");
let decoded = ColumnBatch::decode(&bytes.into()).expect("decode");
let back2 = column_batch_to_entries(&decoded).expect("untranspose decoded");
assert_entries_eq(&entries, &back2);
}
#[test]
fn a_batch_of_one_caller_sub_column_does_not_round_trip() {
let entries = vec![entry(b"alpha", 10, ValueType::Value, b"v1")];
let mut batch = entries_to_column_batch(&entries).expect("transpose");
batch.columns[3] = Column {
column_id: 3,
type_tag: TypeTag::Number(crate::table::column_type::Number::U64_LE),
validity: None,
data: Slice::from(7u64.to_le_bytes().to_vec()),
};
let rows = column_batch_to_entries(&batch).expect("untranspose");
assert!(!super::rows_round_trip(&batch));
assert!(
super::transpose_like(&batch, &rows)
.expect("transpose")
.is_none()
);
let whole = entries_to_column_batch(&entries).expect("transpose");
assert!(super::rows_round_trip(&whole));
}
#[test]
fn intrinsic_untranspose_rejects_wrong_layout() {
let bad = ColumnBatch {
row_count: 1,
columns: vec![Column {
column_id: 0,
type_tag: TypeTag::Fixed(4),
validity: None,
data: vec![0; 4].into(),
}],
};
assert!(column_batch_to_entries(&bad).is_err());
}
#[test]
fn intrinsic_untranspose_rejects_nullable_intrinsic() {
let mut batch =
entries_to_column_batch(&[entry(b"k", 1, ValueType::Value, b"v")]).expect("transpose");
batch.columns[0].validity = Some(vec![0b1]); assert!(column_batch_to_entries(&batch).is_err());
}
#[test]
fn intrinsic_untranspose_rejects_empty_key() {
let batch =
entries_to_column_batch(&[entry(b"", 1, ValueType::Value, b"v")]).expect("transpose");
assert!(column_batch_to_entries(&batch).is_err());
}
#[test]
fn intrinsic_untranspose_rejects_huge_row_count() {
let bad = ColumnBatch {
row_count: u32::MAX,
columns: vec![
Column {
column_id: 0,
type_tag: TypeTag::Bytes,
validity: None,
data: 0u32.to_le_bytes().to_vec().into(),
},
Column {
column_id: 1,
type_tag: TypeTag::Fixed(8),
validity: None,
data: Vec::new().into(),
},
Column {
column_id: 2,
type_tag: TypeTag::Fixed(1),
validity: None,
data: Vec::new().into(),
},
Column {
column_id: 3,
type_tag: TypeTag::Bytes,
validity: None,
data: 0u32.to_le_bytes().to_vec().into(),
},
],
};
assert!(column_batch_to_entries(&bad).is_err());
}
#[test]
fn untranspose_frames_multiple_value_subcolumns() {
let mut batch = entries_to_column_batch(&[
entry(b"k0", 1, ValueType::Value, b"ignored"),
entry(b"k1", 2, ValueType::Value, b"ignored"),
])
.expect("transpose");
batch.columns.pop();
batch.columns.push(Column {
column_id: 3,
type_tag: TypeTag::Fixed(4),
validity: None,
data: vec![1, 0, 0, 0, 2, 0, 0, 0].into(), });
let mut bytes_data = Vec::new();
for off in [0u32, 2, 5] {
bytes_data.extend_from_slice(&off.to_le_bytes());
}
bytes_data.extend_from_slice(b"aabbb"); batch.columns.push(Column {
column_id: 4,
type_tag: TypeTag::Bytes,
validity: None,
data: bytes_data.into(),
});
let entries = column_batch_to_entries(&batch).expect("untranspose");
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].key.user_key.as_ref(), b"k0");
assert_eq!(entries[1].key.seqno, 2);
let tags = [TypeTag::Fixed(4), TypeTag::Bytes];
assert_eq!(
unframe_value_cells(entries[0].value.as_ref(), &tags).expect("unframe 0"),
vec![&[1, 0, 0, 0][..], &b"aa"[..]],
);
assert_eq!(
unframe_value_cells(entries[1].value.as_ref(), &tags).expect("unframe 1"),
vec![&[2, 0, 0, 0][..], &b"bbb"[..]],
);
}
#[test]
fn untranspose_rejects_value_subcolumn_id_collisions() {
let make = |value_cols: Vec<Column>| -> ColumnBatch {
let mut batch = entries_to_column_batch(&[entry(b"k0", 1, ValueType::Value, b"ignored")])
.expect("transpose");
batch.columns.pop(); batch.columns.extend(value_cols);
batch
};
let collide_intrinsic = make(vec![Column {
column_id: COL_USER_KEY,
type_tag: TypeTag::Fixed(4),
validity: None,
data: vec![0, 0, 0, 0].into(),
}]);
assert!(
column_batch_to_entries(&collide_intrinsic).is_err(),
"a value sub-column reusing an intrinsic id must be rejected",
);
let duplicate = make(vec![
Column {
column_id: 5,
type_tag: TypeTag::Fixed(4),
validity: None,
data: vec![0, 0, 0, 0].into(),
},
Column {
column_id: 5,
type_tag: TypeTag::Fixed(4),
validity: None,
data: vec![1, 0, 0, 0].into(),
},
]);
assert!(
column_batch_to_entries(&duplicate).is_err(),
"duplicate value sub-column ids must be rejected",
);
}
#[test]
fn validate_ingest_rejects_an_invalid_value_type() {
let mut batch =
entries_to_column_batch(&[entry(b"k0", 0, ValueType::Value, b"v")]).expect("transpose");
let mut poisoned = batch.columns[2].data.to_vec();
poisoned[0] = 99; batch.columns[2].data = poisoned.into();
assert!(
validate_columnar_ingest_batch(&batch, &crate::comparator::default_comparator()).is_err(),
"an invalid value-type tag must be rejected during eager validation",
);
}
#[test]
fn untranspose_reconstructs_nullable_value_subcolumns() {
let mut batch = entries_to_column_batch(&[
InternalValue::from_components(b"k0", b"ignored", 0, ValueType::Value),
InternalValue::from_components(b"k1", b"ignored", 0, ValueType::Value),
])
.expect("transpose");
batch.columns.pop();
let mut bytes_data = Vec::new();
for off in [0u32, 2, 5] {
bytes_data.extend_from_slice(&off.to_le_bytes());
}
bytes_data.extend_from_slice(b"aabbb");
batch.columns.push(Column {
column_id: 3,
type_tag: TypeTag::Bytes,
validity: None,
data: bytes_data.into(),
});
batch.columns.push(Column {
column_id: 4,
type_tag: TypeTag::Fixed(4),
validity: Some(alloc::vec![0b0000_0001]),
data: alloc::vec![1, 0, 0, 0, 0, 0, 0, 0].into(),
});
let entries = column_batch_to_entries(&batch).expect("untranspose");
let tags = [TypeTag::Bytes, TypeTag::Fixed(4)];
assert_eq!(
unframe_value_cells_nullable(entries[0].value.as_ref(), &tags).expect("row 0"),
vec![Some(&b"aa"[..]), Some(&[1, 0, 0, 0][..])],
);
assert_eq!(
unframe_value_cells_nullable(entries[1].value.as_ref(), &tags).expect("row 1"),
vec![Some(&b"bbb"[..]), None],
"row 1's fixed sub-cell is absent",
);
}
#[test]
fn untranspose_reconstructs_a_nullable_bytes_subcolumn() {
let mut batch = entries_to_column_batch(&[
InternalValue::from_components(b"k0", b"ignored", 0, ValueType::Value),
InternalValue::from_components(b"k1", b"ignored", 0, ValueType::Value),
])
.expect("transpose");
batch.columns.pop();
let mut data = Vec::new();
for off in [0u32, 2, 2] {
data.extend_from_slice(&off.to_le_bytes());
}
data.extend_from_slice(b"hi");
batch.columns.push(Column {
column_id: 3,
type_tag: TypeTag::Bytes,
validity: Some(alloc::vec![0b0000_0001]), data: data.into(),
});
let entries = column_batch_to_entries(&batch).expect("untranspose");
let tags = [TypeTag::Bytes];
assert_eq!(
unframe_value_cells_nullable(entries[0].value.as_ref(), &tags).expect("row 0"),
vec![Some(&b"hi"[..])],
);
assert_eq!(
unframe_value_cells_nullable(entries[1].value.as_ref(), &tags).expect("row 1"),
vec![None],
"the null bytes row reconstructs as absent, not as an empty cell",
);
}
#[test]
fn concat_joins_fixed_bytes_and_nullable_columns() {
let build = |keys: [&[u8]; 2],
fixed: [u32; 2],
bytes: [&[u8]; 2],
nf2: [Option<[u8; 2]>; 2]|
-> ColumnBatch {
let mut batch = entries_to_column_batch(&[
InternalValue::from_components(keys[0], b"x", 0, ValueType::Value),
InternalValue::from_components(keys[1], b"x", 0, ValueType::Value),
])
.expect("transpose");
batch.columns.pop();
batch.columns.push(Column {
column_id: 3,
type_tag: TypeTag::Fixed(4),
validity: None,
data: fixed
.iter()
.flat_map(|v| v.to_le_bytes())
.collect::<Vec<u8>>()
.into(),
});
let mut bytes_data = Vec::new();
let mut acc = 0u32;
bytes_data.extend_from_slice(&acc.to_le_bytes());
for c in bytes {
acc += u32::try_from(c.len()).expect("bytes cell length fits u32");
bytes_data.extend_from_slice(&acc.to_le_bytes());
}
for c in bytes {
bytes_data.extend_from_slice(c);
}
batch.columns.push(Column {
column_id: 4,
type_tag: TypeTag::Bytes,
validity: None,
data: bytes_data.into(),
});
let mut nf2_data = Vec::new();
let mut nf2_valid = 0u8;
for (i, v) in nf2.iter().enumerate() {
nf2_data.extend_from_slice(&v.unwrap_or([0, 0]));
if v.is_some() {
nf2_valid |= 1 << i;
}
}
batch.columns.push(Column {
column_id: 5,
type_tag: TypeTag::Fixed(2),
validity: Some(alloc::vec![nf2_valid]),
data: nf2_data.into(),
});
batch
};
let a = build([b"k0", b"k1"], [1, 2], [b"a", b"bb"], [Some([9, 9]), None]);
let b = build(
[b"k2", b"k3"],
[3, 4],
[b"ccc", b"dddd"],
[Some([7, 7]), None],
);
let a = ColumnBatch::concat(vec![a, b]).expect("concat");
assert_eq!(a.row_count, 4);
let entries = column_batch_to_entries(&a).expect("untranspose combined");
let tags = [TypeTag::Fixed(4), TypeTag::Bytes, TypeTag::Fixed(2)];
let check =
|entry: &InternalValue, key: &[u8], fixed: [u8; 4], bytes: &[u8], nf2: Option<[u8; 2]>| {
assert_eq!(entry.key.user_key.as_ref(), key);
let cells = unframe_value_cells_nullable(entry.value.as_ref(), &tags).expect("unframe");
assert_eq!(cells[0], Some(&fixed[..]));
assert_eq!(cells[1], Some(bytes));
assert_eq!(cells[2], nf2.as_ref().map(|n| &n[..]));
};
check(&entries[0], b"k0", [1, 0, 0, 0], b"a", Some([9, 9]));
check(&entries[1], b"k1", [2, 0, 0, 0], b"bb", None);
check(&entries[2], b"k2", [3, 0, 0, 0], b"ccc", Some([7, 7]));
check(&entries[3], b"k3", [4, 0, 0, 0], b"dddd", None);
}
fn sample_batch() -> ColumnBatch {
let bytes_offsets: Vec<u8> = [0u32, 2, 2, 5]
.iter()
.flat_map(|o| o.to_le_bytes())
.collect();
let mut bytes_data = bytes_offsets;
bytes_data.extend_from_slice(b"hi"); bytes_data.extend_from_slice(b"abc"); ColumnBatch {
row_count: 3,
columns: vec![
Column {
column_id: 1,
type_tag: TypeTag::Fixed(4),
validity: Some(vec![0b0000_0101]), data: vec![10, 0, 0, 0, 0, 0, 0, 0, 30, 0, 0, 0].into(),
},
Column {
column_id: 2,
type_tag: TypeTag::Bytes,
validity: None,
data: bytes_data.into(),
},
],
}
}
fn split_into_row_pages(batch: &ColumnBatch, cuts: &[u32]) -> Vec<ColumnBatch> {
let mut start = 0;
cuts.iter()
.map(|&rows| {
let end = start + rows;
let columns = batch
.columns
.iter()
.map(|c| c.rows(batch.row_count, start, end).expect("rows"))
.collect();
start = end;
ColumnBatch {
row_count: rows,
columns,
}
})
.collect()
}
#[test]
fn a_batch_cut_into_row_pages_joins_back_unchanged() {
let batch = sample_batch();
for cuts in [&[3][..], &[1, 2], &[2, 1], &[1, 1, 1]] {
let pages = split_into_row_pages(&batch, cuts);
assert_eq!(pages.iter().map(|p| p.row_count).collect::<Vec<_>>(), cuts,);
let joined = ColumnBatch::concat(pages).expect("concat");
assert_eq!(joined, batch, "cut {cuts:?} must join back to the batch");
}
}
#[test]
fn a_row_page_of_one_row_holds_that_row() {
let batch = sample_batch();
let pages = split_into_row_pages(&batch, &[1, 1, 1]);
let middle = pages.get(1).expect("three pages");
let fixed = middle.columns.first().expect("fixed column");
assert_eq!(fixed.validity.as_deref(), Some(&[0u8][..]), "row 1 is null");
assert_eq!(&fixed.data[..], &[0, 0, 0, 0]);
let bytes = middle.columns.get(1).expect("bytes column");
assert_eq!(&bytes.data[..], &[0, 0, 0, 0, 0, 0, 0, 0], "one empty cell");
}
#[test]
fn column_rows_refuses_a_range_outside_the_column() {
let batch = sample_batch();
for col in &batch.columns {
assert!(col.rows(3, 2, 4).is_err(), "end past the rows");
assert!(col.rows(3, 2, 1).is_err(), "reversed range");
assert!(col.rows(3, 0, 3).is_ok(), "the whole column");
}
}
#[test]
fn row_page_cuts_close_a_page_once_it_reaches_the_size() {
let batch = sample_batch();
let cuts = |size| batch.row_page_cuts(size).expect("cuts");
assert_eq!(
cuts(0),
[1, 1, 1],
"a zero size puts every row on its own page"
);
assert_eq!(cuts(10), [1, 2], "the first row alone reaches 10 bytes");
assert_eq!(cuts(18), [2, 1], "the first two rows reach 18 bytes");
assert_eq!(cuts(1_000), [3], "a size no row reaches keeps one page");
}
#[test]
fn concat_refuses_no_pages_and_pages_with_other_columns() {
assert!(ColumnBatch::concat(Vec::new()).is_err(), "no row pages");
let batch = sample_batch();
let mut pages = split_into_row_pages(&batch, &[1, 2]);
pages
.get_mut(1)
.expect("two pages")
.columns
.first_mut()
.expect("a column")
.column_id = 9;
assert!(ColumnBatch::concat(pages).is_err(), "a renamed column");
let mut pages = split_into_row_pages(&batch, &[1, 2]);
pages.get_mut(1).expect("two pages").columns.pop();
assert!(ColumnBatch::concat(pages).is_err(), "a missing column");
}
#[test]
fn columnar_batch_round_trips() {
let batch = sample_batch();
let encoded = batch.encode().expect("encode");
let decoded = ColumnBatch::decode(&encoded.into()).expect("decode");
assert_eq!(decoded, batch, "columnar batch must survive a round-trip");
}
#[test]
fn columnar_decode_rejects_truncated_payload() {
let encoded = sample_batch().encode().expect("encode");
let truncated = &encoded[..encoded.len() - 1];
assert!(ColumnBatch::decode(&truncated.to_vec().into()).is_err());
}
#[test]
fn columnar_encode_rejects_fixed_width_length_mismatch() {
let bad = ColumnBatch {
row_count: 3,
columns: vec![Column {
column_id: 1,
type_tag: TypeTag::Fixed(4),
validity: None,
data: vec![0; 8].into(),
}],
};
assert!(bad.encode().is_err());
}
#[test]
fn sample_batch_encodes_as_the_layout_the_offset_tests_assume() {
let encoded = sample_batch().encode().expect("encode");
assert_eq!(encoded[12], 1, "the first column is nullable");
assert_eq!(encoded[15], 0, "the opaque fixed column is plain");
assert_eq!(encoded[31], 0, "the bytes column's width");
assert_eq!(encoded[34], 7, "the bytes column is encoded by its lengths");
assert_eq!(encoded[37], 2, "lengths 2, 0 and 3 pack in two bits");
}
#[test]
fn columnar_decode_rejects_an_unknown_operator() {
let mut encoded = sample_batch().encode().expect("encode");
encoded[15] = 0xFF;
assert!(ColumnBatch::decode(&encoded.clone().into()).is_err());
}
#[test]
fn columnar_encode_rejects_zero_width_fixed_column() {
let bad = ColumnBatch {
row_count: 1,
columns: vec![Column {
column_id: 1,
type_tag: TypeTag::Fixed(0),
validity: None,
data: Vec::new().into(),
}],
};
assert!(bad.encode().is_err());
}
#[test]
fn columnar_encode_rejects_wrong_validity_length() {
let bad = ColumnBatch {
row_count: 1,
columns: vec![Column {
column_id: 1,
type_tag: TypeTag::Fixed(1),
validity: Some(vec![0, 0]),
data: vec![7].into(),
}],
};
assert!(bad.encode().is_err());
}
#[test]
fn columnar_encode_rejects_validity_padding_bits() {
let bad = ColumnBatch {
row_count: 1,
columns: vec![Column {
column_id: 1,
type_tag: TypeTag::Fixed(1),
validity: Some(vec![0xFF]),
data: vec![7].into(),
}],
};
assert!(bad.encode().is_err());
}
#[test]
fn columnar_decode_rejects_a_bit_width_past_64() {
let mut encoded = sample_batch().encode().expect("encode");
encoded[37] = 65;
assert!(ColumnBatch::decode(&encoded.clone().into()).is_err());
}
#[test]
fn columnar_decode_rejects_trailing_bytes() {
let mut encoded = sample_batch().encode().expect("encode");
encoded.push(0); assert!(ColumnBatch::decode(&encoded.clone().into()).is_err());
}
#[test]
fn column_page_decode_refused_for_a_tail_copies_nothing() {
use crate::table::column_page::{PageId, PageStamp};
let batch = sample_batch();
let nullable = batch.columns.first().expect("the nullable fixed column");
let stamp = PageStamp {
group_tag: 7,
id: PageId {
column_id: nullable.column_id,
part: 0,
},
row_page: 0,
};
let mut page = nullable
.encode_page(batch.row_count, stamp, ColumnEncoding::Auto)
.expect("encode page");
page.push(0); let mut copied = 0usize;
let decoded = Column::decode_page(
&page.into(),
batch.row_count,
stamp,
&mut copied,
&mut super::DecodeBudget::default(),
);
assert!(
matches!(decoded, Err(crate::Error::InvalidHeader(m)) if m.contains("trailing bytes")),
"trailing bytes must be refused, got {decoded:?}",
);
assert_eq!(copied, 0, "the refusal came before any copy");
}
#[test]
fn column_page_decoding_past_what_a_writer_cuts_is_refused() {
use crate::table::column_page::{PageId, PageStamp};
let value = vec![7u8; 64 * 1024];
let bytes_column = |rows: u32| Column {
column_id: 3,
type_tag: TypeTag::Bytes,
validity: None,
data: super::build_bytes_column((0..rows).map(|_| value.as_slice()))
.expect("build")
.into(),
};
let stamp = PageStamp {
group_tag: 7,
id: PageId {
column_id: 3,
part: 0,
},
row_page: 0,
};
let page_of = |rows: u32| -> crate::Slice {
let page = bytes_column(rows)
.encode_page(rows, stamp, ColumnEncoding::Auto)
.expect("encode page");
assert!(page.len() < 2 * value.len(), "the rows encode as one value");
page.into()
};
let refused = |result: crate::Result<Column>| {
matches!(result, Err(crate::Error::DecompressedSizeTooLarge { .. }))
};
let forged = page_of(200);
Column::parse_page(&forged, 200, stamp)
.expect("a parse builds nothing, so it has nothing to refuse");
let mut copied = 0usize;
assert!(
refused(Column::decode_page(
&forged,
200,
stamp,
&mut copied,
&mut super::DecodeBudget::default(),
)),
"a page past the bound must be refused",
);
assert_eq!(copied, 0, "refused before anything was built");
let cut = page_of(64);
let mut group = super::DecodeBudget::default();
for page in 0..2 {
let column = Column::decode_page(&cut, 64, stamp, &mut copied, &mut group)
.unwrap_or_else(|e| panic!("page {page} of the group: {e:?}"));
assert_eq!(column.data.len(), 65 * 4 + 64 * value.len());
}
assert!(
refused(Column::decode_page(
&cut,
64,
stamp,
&mut copied,
&mut group
)),
"the third page takes the group past the bound",
);
Column::decode_page(
&cut,
64,
stamp,
&mut copied,
&mut super::DecodeBudget::default(),
)
.expect("the same page in a group of its own");
}
#[test]
fn columnar_entries_refused_after_rebuilding_a_value_still_count_it() {
let mut batch = entries_to_column_batch(&[
entry(b"k0", 1, ValueType::Value, b"ignored"),
entry(b"k1", 2, ValueType::Value, b"ignored"),
])
.expect("transpose");
batch.columns.pop();
batch.columns.push(Column {
column_id: 3,
type_tag: TypeTag::Fixed(4),
validity: None,
data: vec![1, 0, 0, 0, 2, 0, 0, 0].into(),
});
let mut bytes_data = Vec::new();
for off in [0u32, 2, 5] {
bytes_data.extend_from_slice(&off.to_le_bytes());
}
bytes_data.extend_from_slice(b"aabbb");
batch.columns.push(Column {
column_id: 4,
type_tag: TypeTag::Bytes,
validity: None,
data: bytes_data.into(),
});
let vt_col = batch.columns.get_mut(2).expect("value-type column");
vt_col.data = vec![u8::from(ValueType::Value), 0xEE].into();
let mut gathered = 0usize;
let refused = crate::table::data_block::DataBlock::column_batch_entries([batch], &mut gathered);
assert!(refused.is_err(), "the bad value-type tag must be refused");
let first_row = frame_value_cells(&[
(TypeTag::Fixed(4), &[1, 0, 0, 0][..]),
(TypeTag::Bytes, b"aa"),
])
.expect("frame");
assert_eq!(
gathered,
first_row.len(),
"the first row's value was rebuilt before the refusal",
);
}
#[test]
fn advance_bytes_offset_rejects_a_u32_offset_overflow() {
assert!(matches!(
super::advance_bytes_offset(u32::MAX - 3, 10),
Err(crate::Error::DecompressedSizeTooLarge { .. }),
));
#[cfg(target_pointer_width = "64")]
assert!(matches!(
super::advance_bytes_offset(0, (u32::MAX as usize) + 1),
Err(crate::Error::DecompressedSizeTooLarge { .. }),
));
assert_eq!(
super::advance_bytes_offset(100, 50).expect("within the u32 offset ceiling"),
150,
);
}
#[test]
fn frame_bytes_column_refuses_cells_that_change_between_passes() {
let calls = core::cell::Cell::new(0u32);
let short: &[&[u8]] = &[b"a", b"b"];
let long: &[&[u8]] = &[b"a", b"bbb"];
let fewer: &[&[u8]] = &[b"a"];
for second in [long, short, fewer] {
calls.set(0);
let framed = super::frame_bytes_column(2, || {
calls.set(calls.get() + 1);
if calls.get() == 1 { long } else { second }.iter().copied()
});
if core::ptr::eq(second, long) {
let framed = framed.expect("identical passes frame the cells");
let mut expected = Vec::new();
for off in [0u32, 1, 4] {
expected.extend_from_slice(&off.to_le_bytes());
}
expected.extend_from_slice(b"abbb");
assert_eq!(&*framed, &expected[..]);
} else {
assert!(
matches!(framed, Err(crate::Error::InvalidHeader(_))),
"a second pass yielding other cells must be refused",
);
}
}
assert!(super::frame_bytes_column(3, || short.iter().copied()).is_err());
}
#[test]
fn columnar_decode_rejects_huge_column_count() {
let mut payload = 0u32.to_le_bytes().to_vec();
payload.extend_from_slice(&u32::MAX.to_le_bytes());
assert!(ColumnBatch::decode(&payload.clone().into()).is_err());
}
#[test]
fn columnar_decode_rejects_non_boolean_validity_flag() {
let mut encoded = sample_batch().encode().expect("encode");
encoded[12] = 2;
assert!(ColumnBatch::decode(&encoded.clone().into()).is_err());
}
#[test]
fn columnar_decode_rejects_non_zero_bytes_width() {
let mut encoded = sample_batch().encode().expect("encode");
encoded[31] = 5;
assert!(ColumnBatch::decode(&encoded.clone().into()).is_err());
}
#[test]
fn column_batch_into_entries_rejects_an_empty_key_row() {
let mut batch =
entries_to_column_batch(&[entry(b"k", 5, ValueType::Value, b"v")]).expect("valid batch");
let key_col = batch.columns.get_mut(0).expect("key column");
key_col.data = alloc::vec![0u8; 8].into();
let err = column_batch_into_entries(batch, &mut 0).expect_err("empty key must be rejected");
assert!(
matches!(err, crate::Error::InvalidHeader(m) if m.contains("user key is empty")),
"expected an empty-key InvalidHeader, got {err:?}",
);
}
fn match_as_pages(
batch: &ColumnBatch,
needle: &[u8],
deletes: Option<(&crate::table::delete_bitmap::DeleteBitmap, u32)>,
copied: &mut usize,
) -> crate::Result<Vec<InternalValue>> {
match_as_pages_within(
batch,
needle,
deletes,
copied,
&mut super::DecodeBudget::default(),
)
}
fn match_as_pages_within(
batch: &ColumnBatch,
needle: &[u8],
deletes: Option<(&crate::table::delete_bitmap::DeleteBitmap, u32)>,
copied: &mut usize,
budget: &mut super::DecodeBudget,
) -> crate::Result<Vec<InternalValue>> {
use crate::table::column_page::{PageId, PageStamp};
let stamp = |column_id| PageStamp {
group_tag: 1,
id: PageId { column_id, part: 0 },
row_page: 0,
};
let pages = batch
.columns
.iter()
.map(|c| {
c.encode_page(batch.row_count, stamp(c.column_id), ColumnEncoding::Auto)
.map(Slice::from)
})
.collect::<crate::Result<Vec<_>>>()?;
let columns = pages
.iter()
.zip(&batch.columns)
.map(|(page, c)| {
budget.parsed(Column::parse_page(
page,
batch.row_count,
stamp(c.column_id),
)?)
})
.collect::<crate::Result<Vec<_>>>()?;
let rows = batch.row_count;
let mut columns: Vec<_> = columns;
let key = columns.remove(0);
let keys = budget.rows(key.values, TypeTag::Bytes, rows)?;
let run = super::key_rows(
rows,
needle,
&crate::comparator::default_comparator(),
|row| keys.get(TypeTag::Bytes, rows, row),
)?;
let mut out = Vec::new();
if run.is_empty() && rows > 0 {
return Ok(out);
}
page_match_entries(
RowPageColumns {
ordinal: 0,
start: 0,
rows,
columns,
},
run,
needle,
deletes,
copied,
budget,
&mut out,
)?;
Ok(out)
}
#[test]
fn a_lookup_holds_decoded_no_more_rows_than_its_group_budget() {
let entries: Vec<_> = (0..64u32)
.map(|i| {
entry(
b"dup",
u64::from(1_000 - i),
ValueType::Value,
&alloc::vec![b'v'; (i % 7) as usize],
)
})
.collect();
let batch = entries_to_column_batch(&entries).expect("batch");
let budget = |allowance| super::DecodeBudget {
allowance,
built: 0,
held: 0,
};
let refused = match_as_pages_within(&batch, b"dup", None, &mut 0, &mut budget(100));
assert!(
matches!(refused, Err(crate::Error::DecompressedSizeTooLarge { .. })),
"a lookup past the budget must be refused, got {refused:?}",
);
let found = match_as_pages_within(&batch, b"dup", None, &mut 0, &mut budget(1_000))
.expect("a lookup within the budget");
assert_eq!(found.len(), 64, "every version is found");
}
#[test]
fn page_match_entries_finds_every_version_of_a_key_and_nothing_else() {
let batch = entries_to_column_batch(&[
entry(b"a", 9, ValueType::Value, b"va"),
entry(b"m", 7, ValueType::Value, b"new"),
entry(b"m", 4, ValueType::Tombstone, b""),
entry(b"z", 2, ValueType::Value, b"vz"),
])
.expect("batch");
let found = match_as_pages(&batch, b"m", None, &mut 0).expect("lookup");
assert_entries_eq(
&found,
&[
entry(b"m", 7, ValueType::Value, b"new"),
entry(b"m", 4, ValueType::Tombstone, b""),
],
);
for absent in [&b"b"[..], b"zz", b"0"] {
assert!(
match_as_pages(&batch, absent, None, &mut 0)
.expect("lookup")
.is_empty(),
"no row holds {absent:?}",
);
}
}
#[test]
fn page_match_entries_rejects_an_empty_key_row() {
let mut batch =
entries_to_column_batch(&[entry(b"k", 5, ValueType::Value, b"v")]).expect("valid batch");
let key_col = batch.columns.get_mut(0).expect("key column");
key_col.data = alloc::vec![0u8; 8].into();
let err =
match_as_pages(&batch, b"", None, &mut 0).expect_err("matched empty key must be rejected");
assert!(
matches!(err, crate::Error::InvalidHeader(m) if m.contains("user key is empty")),
"expected an empty-key InvalidHeader, got {err:?}",
);
}
#[test]
fn page_match_entries_fails_closed_when_a_delete_mask_position_overflows() {
let batch = entries_to_column_batch(&[
entry(b"dup", 5, ValueType::Value, b"v0"),
entry(b"dup", 3, ValueType::Value, b"v1"),
])
.expect("two-row same-key batch");
let bitmap = crate::table::delete_bitmap::DeleteBitmap::new();
let err = match_as_pages(&batch, b"dup", Some((&bitmap, u32::MAX)), &mut 0)
.expect_err("an overflowing delete-mask position must fail closed");
assert!(
matches!(err, crate::Error::InvalidHeader(m) if m.contains("position exceeds u32::MAX")),
"expected an overflow InvalidHeader, got {err:?}",
);
}
#[test]
fn page_match_entries_counts_the_rows_it_copied_before_a_later_row_fails() {
let mut batch = entries_to_column_batch(&[
entry(b"dup", 5, ValueType::Value, b"v0"),
entry(b"dup", 3, ValueType::Value, b"v1"),
])
.expect("two-row same-key batch");
let vt_col = batch.columns.get_mut(2).expect("value-type column");
let mut vt = vt_col.data.to_vec();
vt[1] = 0xFF;
vt_col.data = vt.into();
let mut copied = 0;
let err = match_as_pages(&batch, b"dup", None, &mut copied)
.expect_err("the invalid value type must be refused");
assert!(
matches!(err, crate::Error::InvalidTag(("ValueType", 0xFF))),
"expected an invalid value-type tag, got {err:?}",
);
assert_eq!(
copied,
b"dup".len() + b"v0".len(),
"the first version was copied before the second failed",
);
}
#[test]
fn page_match_entries_refuses_a_run_outside_its_page() {
use crate::table::column_page::{PageId, PageStamp};
let batch = entries_to_column_batch(&[
entry(b"k", 5, ValueType::Value, b"v0"),
entry(b"k", 3, ValueType::Value, b"v1"),
])
.expect("batch");
let stamp = |column_id| PageStamp {
group_tag: 1,
id: PageId { column_id, part: 0 },
row_page: 0,
};
let pages = batch
.columns
.iter()
.map(|c| {
Slice::from(
c.encode_page(2, stamp(c.column_id), ColumnEncoding::Auto)
.expect("page"),
)
})
.collect::<Vec<_>>();
for run in [1..3, 2..2, 0..0] {
let columns = pages
.iter()
.zip(&batch.columns)
.skip(1)
.map(|(page, c)| Column::parse_page(page, 2, stamp(c.column_id)).expect("parse"))
.collect();
let page = RowPageColumns {
ordinal: 0,
start: 0,
rows: 2,
columns,
};
let err = page_match_entries(
page,
run.clone(),
b"k",
None,
&mut 0,
&mut super::DecodeBudget::default(),
&mut Vec::new(),
)
.expect_err("a run outside the page must be refused");
assert!(
matches!(err, crate::Error::InvalidHeader(m) if m.contains("key run outside")),
"run {run:?}: expected a run InvalidHeader, got {err:?}",
);
}
}
#[test]
fn page_match_entries_rejects_a_zero_row_block() {
let batch = entries_to_column_batch(&[]).expect("zero-row batch");
let err =
match_as_pages(&batch, b"x", None, &mut 0).expect_err("a zero-row block must fail closed");
assert!(
matches!(err, crate::Error::InvalidHeader(m) if m.contains("empty reconstructed data block")),
"expected an empty-block InvalidHeader, got {err:?}",
);
}
#[test]
fn zone_stats_records_every_ordered_column_and_omits_opaque_ones() {
let batch = entries_to_column_batch(&[
entry(b"alpha", 3, ValueType::Value, b"mval"),
entry(b"gamma", 2, ValueType::Value, b"aval"),
entry(b"omega", 1, ValueType::Value, b"zval"),
])
.expect("three-row batch");
let stats = batch.zone_stats();
let ids: Vec<u32> = stats.iter().map(|s| s.column_id).collect();
assert_eq!(
ids,
vec![
u32::from(COL_USER_KEY),
u32::from(COL_SEQNO),
u32::from(COL_VALUE)
],
"only the ordered columns get a zone-map entry, in column order",
);
assert_eq!(stats[1].min, 1u64.to_be_bytes());
assert_eq!(stats[1].max, 3u64.to_be_bytes());
let key_stat = &stats[0];
assert_eq!(
key_stat.min, b"alpha",
"user-key min is the first sorted key"
);
assert_eq!(
key_stat.max, b"omega",
"user-key max is the last sorted key"
);
assert_eq!(key_stat.row_count, 3);
assert_eq!(key_stat.null_count, 0);
assert_eq!(key_stat.type_tag, 1);
let val_stat = &stats[2];
assert_eq!(
val_stat.min, b"aval",
"value min is the byte-wise minimum value, independent of key order",
);
assert_eq!(
val_stat.max, b"zval",
"value max is the byte-wise maximum value",
);
assert_eq!(val_stat.row_count, 3);
assert_eq!(val_stat.null_count, 0);
let below = crate::table::columnar_predicate::ColumnRangePredicate {
column_id: COL_VALUE,
lower: None,
upper: Some(b"aaaa".to_vec()),
apply: crate::table::columnar_predicate::PredicateApply::Filter,
};
assert!(
below.can_skip_block(&stats),
"a value-column predicate below the block's value range skips the block",
);
}
#[test]
fn bytes_column_span_names_each_row_and_refuses_a_row_past_the_payload() {
use super::{bytes_column_row, bytes_column_span};
let mut data = Vec::new();
for offset in [0u32, 2, 3] {
data.extend_from_slice(&offset.to_le_bytes());
}
data.extend_from_slice(b"abc");
assert_eq!(bytes_column_span(&data, 2, 0).expect("row 0"), 12..14);
assert_eq!(bytes_column_row(&data, 2, 1).expect("row 1"), b"c");
let mut forged = data.clone();
forged[8..12].copy_from_slice(&9u32.to_le_bytes());
assert!(bytes_column_span(&forged, 2, 1).is_err());
let mut backwards = data;
backwards[4..8].copy_from_slice(&3u32.to_le_bytes());
backwards[8..12].copy_from_slice(&2u32.to_le_bytes());
assert!(bytes_column_span(&backwards, 2, 1).is_err());
}