use crate::vectorized::{
Batch, ColumnData, SelectionVector, record_vectorized_rows, set_vectorized_simd_utilization,
simd_path_label,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompareOp {
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
}
pub fn filter_batch_int64(
batch: &Batch,
column_idx: usize,
op: CompareOp,
value: i64,
) -> Result<SelectionVector, String> {
let column = batch
.columns()
.get(column_idx)
.ok_or_else(|| format!("column index {column_idx} out of bounds"))?;
let ColumnData::Int64(aligned) = &column.data else {
return Err(format!(
"column {column_idx} is not Int64, cannot apply int64 filter"
));
};
let data = aligned.as_slice();
let sel = batch.selection();
let simd_path = simd_path_label();
let mut result = Vec::with_capacity(sel.len());
for &idx in sel.as_slice() {
let row = usize::from(idx);
if row < data.len() && column.validity.is_valid(row) && compare_i64(data[row], op, value) {
result.push(idx);
}
}
let input_rows = sel.len() as u64;
let output_rows = result.len();
let selectivity_milli = (output_rows as u64 * 1000)
.checked_div(input_rows)
.unwrap_or(0);
record_vectorized_rows(input_rows);
update_filter_simd_utilization(simd_path);
let _span = tracing::debug_span!(
"vectorized_batch",
batch_size = input_rows,
selectivity = selectivity_milli as f64 / 1000.0,
simd_path = simd_path,
op = "filter_int64",
)
.entered();
tracing::debug!(
batch_size = input_rows,
output_rows,
selectivity = selectivity_milli as f64 / 1000.0,
simd_path,
"filter_int64 complete"
);
Ok(SelectionVector::from_indices(result))
}
pub fn filter_batch_float64(
batch: &Batch,
column_idx: usize,
op: CompareOp,
value: f64,
) -> Result<SelectionVector, String> {
let column = batch
.columns()
.get(column_idx)
.ok_or_else(|| format!("column index {column_idx} out of bounds"))?;
let ColumnData::Float64(aligned) = &column.data else {
return Err(format!(
"column {column_idx} is not Float64, cannot apply float64 filter"
));
};
let data = aligned.as_slice();
let sel = batch.selection();
let simd_path = simd_path_label();
let mut result = Vec::with_capacity(sel.len());
for &idx in sel.as_slice() {
let row = usize::from(idx);
if row < data.len() && column.validity.is_valid(row) && compare_f64(data[row], op, value) {
result.push(idx);
}
}
let input_rows = sel.len() as u64;
let output_rows = result.len();
let selectivity_milli = (output_rows as u64 * 1000)
.checked_div(input_rows)
.unwrap_or(0);
record_vectorized_rows(input_rows);
update_filter_simd_utilization(simd_path);
let _span = tracing::debug_span!(
"vectorized_batch",
batch_size = input_rows,
selectivity = selectivity_milli as f64 / 1000.0,
simd_path = simd_path,
op = "filter_float64",
)
.entered();
Ok(SelectionVector::from_indices(result))
}
pub fn project_batch(batch: &Batch, column_indices: &[usize]) -> Result<Batch, String> {
use crate::vectorized::BatchFormatError;
let simd_path = simd_path_label();
let input_rows = batch.selection().len() as u64;
let mut columns = Vec::with_capacity(column_indices.len());
for &idx in column_indices {
let column = batch
.columns()
.get(idx)
.ok_or_else(|| format!("project: column index {idx} out of bounds"))?;
columns.push(column.clone());
}
record_vectorized_rows(input_rows);
let _span = tracing::debug_span!(
"vectorized_batch",
batch_size = input_rows,
selectivity = 1.0_f64,
simd_path = simd_path,
op = "project",
)
.entered();
let projected = Batch::from_columns(
columns,
batch.row_count(),
batch.capacity(),
batch.selection().clone(),
)
.map_err(|e: BatchFormatError| e.to_string())?;
Ok(projected)
}
pub fn hash_batch_columns(batch: &Batch, column_indices: &[usize]) -> Result<Vec<u64>, String> {
let sel = batch.selection();
let simd_path = simd_path_label();
let input_rows = sel.len() as u64;
let mut hashes = Vec::with_capacity(sel.len());
for &idx in sel.as_slice() {
let row = usize::from(idx);
let mut h: u64 = 0xcbf2_9ce4_8422_2325; for &col_idx in column_indices {
let column = batch
.columns()
.get(col_idx)
.ok_or_else(|| format!("hash: column index {col_idx} out of bounds"))?;
h = hash_combine(h, hash_column_value(&column.data, row));
}
hashes.push(h);
}
record_vectorized_rows(input_rows);
let simd_milli = if simd_path == "avx2" {
850 } else if simd_path == "sse2" {
500
} else {
0
};
set_vectorized_simd_utilization(simd_milli);
let _span = tracing::debug_span!(
"vectorized_batch",
batch_size = input_rows,
selectivity = 1.0_f64,
simd_path = simd_path,
op = "hash",
)
.entered();
Ok(hashes)
}
#[must_use]
pub fn and_selection(lhs: &SelectionVector, rhs: &SelectionVector) -> SelectionVector {
let a = lhs.as_slice();
let b = rhs.as_slice();
let mut result = Vec::with_capacity(a.len().min(b.len()));
let (mut i, mut j) = (0, 0);
while i < a.len() && j < b.len() {
match a[i].cmp(&b[j]) {
std::cmp::Ordering::Less => i += 1,
std::cmp::Ordering::Greater => j += 1,
std::cmp::Ordering::Equal => {
result.push(a[i]);
i += 1;
j += 1;
}
}
}
SelectionVector::from_indices(result)
}
#[must_use]
pub fn or_selection(lhs: &SelectionVector, rhs: &SelectionVector) -> SelectionVector {
let a = lhs.as_slice();
let b = rhs.as_slice();
let mut result = Vec::with_capacity(a.len() + b.len());
let (mut i, mut j) = (0, 0);
while i < a.len() && j < b.len() {
match a[i].cmp(&b[j]) {
std::cmp::Ordering::Less => {
result.push(a[i]);
i += 1;
}
std::cmp::Ordering::Greater => {
result.push(b[j]);
j += 1;
}
std::cmp::Ordering::Equal => {
result.push(a[i]);
i += 1;
j += 1;
}
}
}
result.extend_from_slice(&a[i..]);
result.extend_from_slice(&b[j..]);
SelectionVector::from_indices(result)
}
pub fn filter_batch_int32(
batch: &Batch,
column_idx: usize,
op: CompareOp,
value: i32,
) -> Result<SelectionVector, String> {
let column = batch
.columns()
.get(column_idx)
.ok_or_else(|| format!("column index {column_idx} out of bounds"))?;
let ColumnData::Int32(aligned) = &column.data else {
return Err(format!(
"column {column_idx} is not Int32, cannot apply int32 filter"
));
};
let data = aligned.as_slice();
let sel = batch.selection();
let simd_path = simd_path_label();
let mut result = Vec::with_capacity(sel.len());
for &idx in sel.as_slice() {
let row = usize::from(idx);
if row < data.len() && column.validity.is_valid(row) && compare_i32(data[row], op, value) {
result.push(idx);
}
}
let input_rows = sel.len() as u64;
let output_rows = result.len();
let selectivity_milli = (output_rows as u64 * 1000)
.checked_div(input_rows)
.unwrap_or(0);
record_vectorized_rows(input_rows);
update_filter_simd_utilization(simd_path);
let _span = tracing::debug_span!(
"vectorized_batch",
batch_size = input_rows,
selectivity = selectivity_milli as f64 / 1000.0,
simd_path = simd_path,
op = "filter_int32",
)
.entered();
Ok(SelectionVector::from_indices(result))
}
pub fn filter_batch_text(
batch: &Batch,
column_idx: usize,
op: CompareOp,
value: &str,
) -> Result<SelectionVector, String> {
let column = batch
.columns()
.get(column_idx)
.ok_or_else(|| format!("column index {column_idx} out of bounds"))?;
let ColumnData::Text { offsets, data } = &column.data else {
return Err(format!(
"column {column_idx} is not Text, cannot apply text filter"
));
};
let sel = batch.selection();
let simd_path = simd_path_label();
let value_bytes = value.as_bytes();
let mut result = Vec::with_capacity(sel.len());
for &idx in sel.as_slice() {
let row = usize::from(idx);
if !column.validity.is_valid(row) {
continue;
}
let start = offsets.get(row).copied().unwrap_or(0) as usize;
let end = offsets.get(row + 1).copied().unwrap_or(0) as usize;
let row_bytes = data.get(start..end).unwrap_or(&[]);
if compare_bytes(row_bytes, op, value_bytes) {
result.push(idx);
}
}
let input_rows = sel.len() as u64;
record_vectorized_rows(input_rows);
let _span = tracing::debug_span!(
"vectorized_batch",
batch_size = input_rows,
simd_path = simd_path,
op = "filter_text",
)
.entered();
Ok(SelectionVector::from_indices(result))
}
pub fn filter_batch_binary(
batch: &Batch,
column_idx: usize,
op: CompareOp,
value: &[u8],
) -> Result<SelectionVector, String> {
let column = batch
.columns()
.get(column_idx)
.ok_or_else(|| format!("column index {column_idx} out of bounds"))?;
let ColumnData::Binary { offsets, data } = &column.data else {
return Err(format!(
"column {column_idx} is not Binary, cannot apply binary filter"
));
};
let sel = batch.selection();
let simd_path = simd_path_label();
let mut result = Vec::with_capacity(sel.len());
for &idx in sel.as_slice() {
let row = usize::from(idx);
if !column.validity.is_valid(row) {
continue;
}
let start = offsets.get(row).copied().unwrap_or(0) as usize;
let end = offsets.get(row + 1).copied().unwrap_or(0) as usize;
let row_bytes = data.get(start..end).unwrap_or(&[]);
if compare_bytes(row_bytes, op, value) {
result.push(idx);
}
}
let input_rows = sel.len() as u64;
record_vectorized_rows(input_rows);
let _span = tracing::debug_span!(
"vectorized_batch",
batch_size = input_rows,
simd_path = simd_path,
op = "filter_binary",
)
.entered();
Ok(SelectionVector::from_indices(result))
}
fn update_filter_simd_utilization(simd_path: &str) {
let milli = match simd_path {
"avx2" => 750,
"sse2" => 400,
_ => 0,
};
set_vectorized_simd_utilization(milli);
}
#[inline]
fn compare_i32(lhs: i32, op: CompareOp, rhs: i32) -> bool {
match op {
CompareOp::Eq => lhs == rhs,
CompareOp::Ne => lhs != rhs,
CompareOp::Lt => lhs < rhs,
CompareOp::Le => lhs <= rhs,
CompareOp::Gt => lhs > rhs,
CompareOp::Ge => lhs >= rhs,
}
}
#[inline]
fn compare_i64(lhs: i64, op: CompareOp, rhs: i64) -> bool {
match op {
CompareOp::Eq => lhs == rhs,
CompareOp::Ne => lhs != rhs,
CompareOp::Lt => lhs < rhs,
CompareOp::Le => lhs <= rhs,
CompareOp::Gt => lhs > rhs,
CompareOp::Ge => lhs >= rhs,
}
}
#[inline]
fn compare_f64(lhs: f64, op: CompareOp, rhs: f64) -> bool {
match op {
CompareOp::Eq => lhs == rhs,
CompareOp::Ne => lhs != rhs,
CompareOp::Lt => lhs < rhs,
CompareOp::Le => lhs <= rhs,
CompareOp::Gt => lhs > rhs,
CompareOp::Ge => lhs >= rhs,
}
}
#[inline]
fn compare_bytes(lhs: &[u8], op: CompareOp, rhs: &[u8]) -> bool {
match op {
CompareOp::Eq => lhs == rhs,
CompareOp::Ne => lhs != rhs,
CompareOp::Lt => lhs < rhs,
CompareOp::Le => lhs <= rhs,
CompareOp::Gt => lhs > rhs,
CompareOp::Ge => lhs >= rhs,
}
}
#[inline]
#[allow(clippy::cast_sign_loss)]
fn hash_column_value(data: &ColumnData, row: usize) -> u64 {
match data {
ColumnData::Int8(v) => v.as_slice().get(row).map_or(0, |&x| x as u64),
ColumnData::Int16(v) => v.as_slice().get(row).map_or(0, |&x| x as u64),
ColumnData::Int32(v) => v.as_slice().get(row).map_or(0, |&x| x as u64),
ColumnData::Int64(v) => v.as_slice().get(row).map_or(0, |&x| x as u64),
ColumnData::Float32(v) => v.as_slice().get(row).map_or(0, |&x| {
let val = if x == 0.0 { 0.0 } else { x };
u64::from(val.to_bits())
}),
ColumnData::Float64(v) => v.as_slice().get(row).map_or(0, |&x| {
let val = if x == 0.0 { 0.0 } else { x };
val.to_bits()
}),
ColumnData::Binary { offsets, data } | ColumnData::Text { offsets, data } => {
let start = offsets.get(row).copied().unwrap_or(0) as usize;
let end = offsets.get(row + 1).copied().unwrap_or(0) as usize;
fnv1a_bytes(data.get(start..end).unwrap_or(&[]))
}
}
}
#[inline]
fn fnv1a_bytes(bytes: &[u8]) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for &b in bytes {
h ^= u64::from(b);
h = h.wrapping_mul(0x0100_0000_01b3);
}
h
}
#[inline]
fn hash_combine(h: u64, val: u64) -> u64 {
let mixed = h ^ val.wrapping_mul(0x9e37_79b9_7f4a_7c15);
mixed.wrapping_mul(0x517c_c1b7_2722_0a95)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vectorized::{
Batch, ColumnSpec, ColumnVectorType, DEFAULT_BATCH_ROW_CAPACITY, reset_vectorized_metrics,
vectorized_metrics_snapshot,
};
use fsqlite_types::value::SqliteValue;
fn int64_batch(values: &[i64]) -> Batch {
let specs = vec![ColumnSpec::new("val", ColumnVectorType::Int64)];
let rows: Vec<Vec<SqliteValue>> = values
.iter()
.map(|&v| vec![SqliteValue::Integer(v)])
.collect();
Batch::from_rows(&rows, &specs, DEFAULT_BATCH_ROW_CAPACITY).expect("batch should build")
}
fn float64_batch(values: &[f64]) -> Batch {
let specs = vec![ColumnSpec::new("val", ColumnVectorType::Float64)];
let rows: Vec<Vec<SqliteValue>> = values
.iter()
.map(|&v| vec![SqliteValue::Float(v)])
.collect();
Batch::from_rows(&rows, &specs, DEFAULT_BATCH_ROW_CAPACITY).expect("batch should build")
}
fn multi_column_batch() -> Batch {
let specs = vec![
ColumnSpec::new("id", ColumnVectorType::Int64),
ColumnSpec::new("score", ColumnVectorType::Float64),
];
let rows = vec![
vec![SqliteValue::Integer(1), SqliteValue::Float(10.5)],
vec![SqliteValue::Integer(2), SqliteValue::Float(20.0)],
vec![SqliteValue::Integer(3), SqliteValue::Float(15.5)],
vec![SqliteValue::Integer(4), SqliteValue::Float(5.0)],
];
Batch::from_rows(&rows, &specs, DEFAULT_BATCH_ROW_CAPACITY).expect("batch should build")
}
#[test]
fn filter_int64_eq() {
let batch = int64_batch(&[10, 20, 30, 20, 40]);
let sel = filter_batch_int64(&batch, 0, CompareOp::Eq, 20).unwrap();
assert_eq!(sel.as_slice(), &[1, 3]);
}
#[test]
fn filter_int64_lt() {
let batch = int64_batch(&[10, 20, 30, 5, 40]);
let sel = filter_batch_int64(&batch, 0, CompareOp::Lt, 20).unwrap();
assert_eq!(sel.as_slice(), &[0, 3]);
}
#[test]
fn filter_int64_gt() {
let batch = int64_batch(&[10, 20, 30, 5, 40]);
let sel = filter_batch_int64(&batch, 0, CompareOp::Gt, 20).unwrap();
assert_eq!(sel.as_slice(), &[2, 4]);
}
#[test]
fn filter_int64_le() {
let batch = int64_batch(&[10, 20, 30, 5, 40]);
let sel = filter_batch_int64(&batch, 0, CompareOp::Le, 20).unwrap();
assert_eq!(sel.as_slice(), &[0, 1, 3]);
}
#[test]
fn filter_int64_ge() {
let batch = int64_batch(&[10, 20, 30, 5, 40]);
let sel = filter_batch_int64(&batch, 0, CompareOp::Ge, 20).unwrap();
assert_eq!(sel.as_slice(), &[1, 2, 4]);
}
#[test]
fn filter_int64_ne() {
let batch = int64_batch(&[10, 20, 30]);
let sel = filter_batch_int64(&batch, 0, CompareOp::Ne, 20).unwrap();
assert_eq!(sel.as_slice(), &[0, 2]);
}
#[test]
fn filter_float64_lt() {
let batch = float64_batch(&[1.0, 2.5, 3.0, 0.5]);
let sel = filter_batch_float64(&batch, 0, CompareOp::Lt, 2.0).unwrap();
assert_eq!(sel.as_slice(), &[0, 3]);
}
#[test]
fn filter_respects_existing_selection() {
let mut batch = int64_batch(&[10, 20, 30, 40, 50]);
batch
.apply_selection(SelectionVector::from_indices(vec![1, 2, 3]))
.unwrap();
let sel = filter_batch_int64(&batch, 0, CompareOp::Gt, 25).unwrap();
assert_eq!(sel.as_slice(), &[2, 3]);
}
#[test]
fn filter_empty_batch() {
let batch = int64_batch(&[]);
let sel = filter_batch_int64(&batch, 0, CompareOp::Eq, 0).unwrap();
assert!(sel.is_empty());
}
#[test]
fn filter_column_type_mismatch() {
let batch = float64_batch(&[1.0, 2.0]);
let err = filter_batch_int64(&batch, 0, CompareOp::Eq, 1).unwrap_err();
assert!(err.contains("not Int64"));
}
#[test]
fn filter_column_out_of_bounds() {
let batch = int64_batch(&[1, 2]);
let err = filter_batch_int64(&batch, 5, CompareOp::Eq, 1).unwrap_err();
assert!(err.contains("out of bounds"));
}
#[test]
fn filter_records_metrics() {
reset_vectorized_metrics();
let batch = int64_batch(&[10, 20, 30]);
let _ = filter_batch_int64(&batch, 0, CompareOp::Eq, 20).unwrap();
let metrics = vectorized_metrics_snapshot();
assert!(
metrics.vectorized_rows_total >= 3,
"should have recorded at least 3 rows"
);
}
#[test]
fn project_selects_column_subset() {
let batch = multi_column_batch();
let projected = project_batch(&batch, &[1]).unwrap();
assert_eq!(projected.columns().len(), 1);
assert_eq!(projected.columns()[0].spec.name, "score");
assert_eq!(projected.row_count(), 4);
}
#[test]
fn project_reorders_columns() {
let batch = multi_column_batch();
let projected = project_batch(&batch, &[1, 0]).unwrap();
assert_eq!(projected.columns().len(), 2);
assert_eq!(projected.columns()[0].spec.name, "score");
assert_eq!(projected.columns()[1].spec.name, "id");
}
#[test]
fn project_out_of_bounds() {
let batch = multi_column_batch();
let err = project_batch(&batch, &[5]).unwrap_err();
assert!(err.contains("out of bounds"));
}
#[test]
fn hash_produces_deterministic_values() {
let batch = int64_batch(&[10, 20, 30]);
let hashes1 = hash_batch_columns(&batch, &[0]).unwrap();
let hashes2 = hash_batch_columns(&batch, &[0]).unwrap();
assert_eq!(hashes1, hashes2, "hashes should be deterministic");
}
#[test]
fn hash_different_values_differ() {
let batch = int64_batch(&[10, 20]);
let hashes = hash_batch_columns(&batch, &[0]).unwrap();
assert_ne!(
hashes[0], hashes[1],
"different values should hash differently"
);
}
#[test]
fn hash_respects_selection_vector() {
let mut batch = int64_batch(&[10, 20, 30, 40]);
batch
.apply_selection(SelectionVector::from_indices(vec![0, 2]))
.unwrap();
let hashes = hash_batch_columns(&batch, &[0]).unwrap();
assert_eq!(hashes.len(), 2, "should only hash selected rows");
}
#[test]
fn hash_multi_column() {
let batch = multi_column_batch();
let hashes_single = hash_batch_columns(&batch, &[0]).unwrap();
let hashes_multi = hash_batch_columns(&batch, &[0, 1]).unwrap();
assert_ne!(hashes_single, hashes_multi);
}
#[test]
fn simd_path_detection() {
let label = simd_path_label();
#[cfg(target_arch = "x86_64")]
assert!(
label == "avx2" || label == "sse2",
"expected avx2 or sse2 on x86_64, got {label}"
);
#[cfg(not(target_arch = "x86_64"))]
assert_eq!(label, "scalar");
}
const BEAD_FILTER: &str = "bd-14vp7.3";
#[test]
fn and_selection_intersection() {
let a = SelectionVector::from_indices(vec![0, 1, 2, 3]);
let b = SelectionVector::from_indices(vec![1, 3, 4]);
let result = and_selection(&a, &b);
assert_eq!(
result.as_slice(),
&[1, 3],
"bead_id={BEAD_FILTER} case=and_intersection"
);
}
#[test]
fn and_selection_disjoint() {
let a = SelectionVector::from_indices(vec![0, 2]);
let b = SelectionVector::from_indices(vec![1, 3]);
let result = and_selection(&a, &b);
assert!(result.is_empty(), "bead_id={BEAD_FILTER} case=and_disjoint");
}
#[test]
fn and_selection_empty_input() {
let a = SelectionVector::from_indices(vec![0, 1, 2]);
let b = SelectionVector::from_indices(vec![]);
let result = and_selection(&a, &b);
assert!(
result.is_empty(),
"bead_id={BEAD_FILTER} case=and_empty_input"
);
}
#[test]
fn or_selection_union() {
let a = SelectionVector::from_indices(vec![0, 2]);
let b = SelectionVector::from_indices(vec![1, 2, 3]);
let result = or_selection(&a, &b);
assert_eq!(
result.as_slice(),
&[0, 1, 2, 3],
"bead_id={BEAD_FILTER} case=or_union"
);
}
#[test]
fn or_selection_identical() {
let a = SelectionVector::from_indices(vec![1, 3]);
let b = SelectionVector::from_indices(vec![1, 3]);
let result = or_selection(&a, &b);
assert_eq!(
result.as_slice(),
&[1, 3],
"bead_id={BEAD_FILTER} case=or_identical_no_dupes"
);
}
#[test]
fn or_selection_empty() {
let a = SelectionVector::from_indices(vec![]);
let b = SelectionVector::from_indices(vec![2, 5]);
let result = or_selection(&a, &b);
assert_eq!(
result.as_slice(),
&[2, 5],
"bead_id={BEAD_FILTER} case=or_empty_lhs"
);
}
#[test]
fn and_or_composition_multi_predicate() {
let batch = int64_batch(&[5, 10, 20, 30, 40, 50]);
let gt10 = filter_batch_int64(&batch, 0, CompareOp::Gt, 10).unwrap();
let lt40 = filter_batch_int64(&batch, 0, CompareOp::Lt, 40).unwrap();
let result = and_selection(>10, <40);
assert_eq!(
result.as_slice(),
&[2, 3],
"bead_id={BEAD_FILTER} case=and_composition"
);
}
#[test]
fn or_composition_multi_predicate() {
let batch = int64_batch(&[5, 10, 20, 30, 40, 50]);
let lt10 = filter_batch_int64(&batch, 0, CompareOp::Lt, 10).unwrap();
let gt40 = filter_batch_int64(&batch, 0, CompareOp::Gt, 40).unwrap();
let result = or_selection(<10, >40);
assert_eq!(
result.as_slice(),
&[0, 5],
"bead_id={BEAD_FILTER} case=or_composition"
);
}
fn int32_batch(values: &[i32]) -> Batch {
let specs = vec![ColumnSpec::new("val", ColumnVectorType::Int32)];
let rows: Vec<Vec<SqliteValue>> = values
.iter()
.map(|&v| vec![SqliteValue::Integer(i64::from(v))])
.collect();
Batch::from_rows(&rows, &specs, DEFAULT_BATCH_ROW_CAPACITY).expect("batch should build")
}
fn text_batch(values: &[&str]) -> Batch {
let specs = vec![ColumnSpec::new("name", ColumnVectorType::Text)];
let rows: Vec<Vec<SqliteValue>> = values
.iter()
.map(|&v| vec![SqliteValue::Text(v.into())])
.collect();
Batch::from_rows(&rows, &specs, DEFAULT_BATCH_ROW_CAPACITY).expect("batch should build")
}
fn binary_batch(values: &[&[u8]]) -> Batch {
let specs = vec![ColumnSpec::new("data", ColumnVectorType::Binary)];
let rows: Vec<Vec<SqliteValue>> = values
.iter()
.map(|v| vec![SqliteValue::Blob(v.to_vec().into())])
.collect();
Batch::from_rows(&rows, &specs, DEFAULT_BATCH_ROW_CAPACITY).expect("batch should build")
}
#[test]
fn filter_int32_eq() {
let batch = int32_batch(&[10, 20, 30, 20]);
let sel = filter_batch_int32(&batch, 0, CompareOp::Eq, 20).unwrap();
assert_eq!(
sel.as_slice(),
&[1, 3],
"bead_id={BEAD_FILTER} case=int32_eq"
);
}
#[test]
fn filter_int32_lt() {
let batch = int32_batch(&[5, 15, 25]);
let sel = filter_batch_int32(&batch, 0, CompareOp::Lt, 15).unwrap();
assert_eq!(sel.as_slice(), &[0], "bead_id={BEAD_FILTER} case=int32_lt");
}
#[test]
fn filter_int32_type_mismatch() {
let batch = int64_batch(&[1, 2]);
let err = filter_batch_int32(&batch, 0, CompareOp::Eq, 1).unwrap_err();
assert!(
err.contains("not Int32"),
"bead_id={BEAD_FILTER} case=int32_type_mismatch"
);
}
#[test]
fn filter_text_eq() {
let batch = text_batch(&["alice", "bob", "charlie", "alice"]);
let sel = filter_batch_text(&batch, 0, CompareOp::Eq, "alice").unwrap();
assert_eq!(
sel.as_slice(),
&[0, 3],
"bead_id={BEAD_FILTER} case=text_eq"
);
}
#[test]
fn filter_text_lt() {
let batch = text_batch(&["banana", "apple", "cherry"]);
let sel = filter_batch_text(&batch, 0, CompareOp::Lt, "banana").unwrap();
assert_eq!(sel.as_slice(), &[1], "bead_id={BEAD_FILTER} case=text_lt");
}
#[test]
fn filter_text_ge() {
let batch = text_batch(&["a", "b", "c", "d"]);
let sel = filter_batch_text(&batch, 0, CompareOp::Ge, "c").unwrap();
assert_eq!(
sel.as_slice(),
&[2, 3],
"bead_id={BEAD_FILTER} case=text_ge"
);
}
#[test]
fn filter_text_type_mismatch() {
let batch = int64_batch(&[1, 2]);
let err = filter_batch_text(&batch, 0, CompareOp::Eq, "x").unwrap_err();
assert!(
err.contains("not Text"),
"bead_id={BEAD_FILTER} case=text_type_mismatch"
);
}
#[test]
fn filter_binary_eq() {
let batch = binary_batch(&[&[1, 2], &[3, 4], &[1, 2]]);
let sel = filter_batch_binary(&batch, 0, CompareOp::Eq, &[1, 2]).unwrap();
assert_eq!(
sel.as_slice(),
&[0, 2],
"bead_id={BEAD_FILTER} case=binary_eq"
);
}
#[test]
fn filter_binary_ne() {
let batch = binary_batch(&[&[1, 2], &[3, 4], &[1, 2]]);
let sel = filter_batch_binary(&batch, 0, CompareOp::Ne, &[1, 2]).unwrap();
assert_eq!(sel.as_slice(), &[1], "bead_id={BEAD_FILTER} case=binary_ne");
}
#[test]
fn filter_binary_type_mismatch() {
let batch = int64_batch(&[1]);
let err = filter_batch_binary(&batch, 0, CompareOp::Eq, &[]).unwrap_err();
assert!(
err.contains("not Binary"),
"bead_id={BEAD_FILTER} case=binary_type_mismatch"
);
}
fn int64_batch_with_nulls(values: &[Option<i64>]) -> Batch {
let specs = vec![ColumnSpec::new("val", ColumnVectorType::Int64)];
let rows: Vec<Vec<SqliteValue>> = values
.iter()
.map(|v| match v {
Some(i) => vec![SqliteValue::Integer(*i)],
None => vec![SqliteValue::Null],
})
.collect();
Batch::from_rows(&rows, &specs, DEFAULT_BATCH_ROW_CAPACITY).expect("batch should build")
}
#[test]
fn filter_null_rows_excluded_from_eq() {
let batch = int64_batch_with_nulls(&[Some(10), None, Some(10), None]);
let sel = filter_batch_int64(&batch, 0, CompareOp::Eq, 10).unwrap();
assert_eq!(
sel.as_slice(),
&[0, 2],
"bead_id={BEAD_FILTER} case=null_excluded_eq"
);
}
#[test]
fn filter_null_rows_excluded_from_ne() {
let batch = int64_batch_with_nulls(&[Some(10), None, Some(20)]);
let sel = filter_batch_int64(&batch, 0, CompareOp::Ne, 10).unwrap();
assert_eq!(
sel.as_slice(),
&[2],
"bead_id={BEAD_FILTER} case=null_excluded_ne"
);
}
#[test]
fn filter_all_nulls_returns_empty() {
let batch = int64_batch_with_nulls(&[None, None, None]);
let sel = filter_batch_int64(&batch, 0, CompareOp::Ge, 0).unwrap();
assert!(
sel.is_empty(),
"bead_id={BEAD_FILTER} case=all_nulls_empty_result"
);
}
#[test]
fn filter_updates_simd_utilization() {
reset_vectorized_metrics();
let batch = int64_batch(&[10, 20, 30]);
let _ = filter_batch_int64(&batch, 0, CompareOp::Eq, 20).unwrap();
let metrics = vectorized_metrics_snapshot();
#[cfg(target_arch = "x86_64")]
assert!(
metrics.simd_utilization_milli > 0,
"bead_id={BEAD_FILTER} case=simd_utilization_nonzero"
);
#[cfg(not(target_arch = "x86_64"))]
assert_eq!(metrics.simd_utilization_milli, 0);
}
#[test]
#[allow(clippy::cast_possible_truncation)]
fn filter_matches_row_at_a_time_evaluation() {
let values = [5_i64, 10, 15, 20, 25, 30, 35, 40];
let batch = int64_batch(&values);
let vec_sel = filter_batch_int64(&batch, 0, CompareOp::Le, 20).unwrap();
let row_sel: Vec<u16> = values
.iter()
.enumerate()
.filter(|&(_, v)| *v <= 20)
.map(|(i, _)| i as u16)
.collect();
assert_eq!(
vec_sel.as_slice(),
&row_sel,
"bead_id={BEAD_FILTER} case=vectorized_matches_row_at_a_time"
);
}
#[test]
#[allow(clippy::cast_possible_truncation)]
fn filter_text_matches_row_at_a_time() {
let values = ["delta", "alpha", "gamma", "beta"];
let batch = text_batch(&values);
let vec_sel = filter_batch_text(&batch, 0, CompareOp::Lt, "delta").unwrap();
let row_sel: Vec<u16> = values
.iter()
.enumerate()
.filter(|&(_, v)| v.as_bytes() < b"delta".as_slice())
.map(|(i, _)| i as u16)
.collect();
assert_eq!(
vec_sel.as_slice(),
&row_sel,
"bead_id={BEAD_FILTER} case=text_vectorized_matches_row_at_a_time"
);
}
#[test]
fn chained_filter_on_multi_column_batch() {
let batch = multi_column_batch();
let id_filter = filter_batch_int64(&batch, 0, CompareOp::Gt, 1).unwrap();
let mut filtered_batch = batch;
filtered_batch.apply_selection(id_filter).unwrap();
let score_filter = filter_batch_float64(&filtered_batch, 1, CompareOp::Lt, 16.0).unwrap();
assert_eq!(
score_filter.as_slice(),
&[2, 3],
"bead_id={BEAD_FILTER} case=chained_multi_column"
);
}
}