use std::cmp::Ordering;
use crate::columnar::Columns;
use crate::error::Result;
use crate::filter::{self, FilterInput, FilterLogic};
use crate::pagination;
use crate::sort::{self, SortDirection, SortSpec};
use crate::value::Value;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Page {
pub indices: Vec<usize>,
pub total: u64,
pub page: u64,
pub pages: u64,
pub has_next: bool,
pub has_previous: bool,
}
pub fn offset_page(
items: &[Value],
columns: Option<&Columns>,
filter: Option<&FilterInput>,
sort_specs: &[SortSpec],
page: u64,
limit: u64,
) -> Result<Page> {
let filtered = filter_stage(items, columns, filter)?;
let indices = sort_stage(items, columns, filtered, sort_specs)?;
let total = indices.len() as u64;
let meta = pagination::offset_meta(page, limit, total);
let start = (pagination::offset(page, limit) as usize).min(indices.len());
let end = start.saturating_add(limit as usize).min(indices.len());
Ok(Page {
indices: indices[start..end].to_vec(),
total,
page,
pages: meta.pages,
has_next: meta.has_next,
has_previous: meta.has_previous,
})
}
fn filter_stage(
items: &[Value],
columns: Option<&Columns>,
filter: Option<&FilterInput>,
) -> Result<Vec<usize>> {
let Some(input) = filter else {
return Ok((0..items.len()).collect());
};
if let Some(indices) = columnar_filter(columns, input) {
return Ok(indices);
}
filter::filter_indices(items, input)
}
fn columnar_filter(columns: Option<&Columns>, filter: &FilterInput) -> Option<Vec<usize>> {
let cols = columns?;
let FilterInput::Flat(specs) = filter else {
return None;
};
if specs.is_empty() || specs.iter().any(|s| s.logic != FilterLogic::And) {
return None;
}
let mut result: Option<Vec<usize>> = None;
for spec in specs {
let matched = cols.filter(&spec.field, spec.op, &spec.value)?;
result = Some(match result {
Some(acc) => intersect_sorted(&acc, &matched),
None => matched,
});
}
result
}
fn intersect_sorted(a: &[usize], b: &[usize]) -> Vec<usize> {
let mut out = Vec::new();
let (mut i, mut j) = (0, 0);
while i < a.len() && j < b.len() {
match a[i].cmp(&b[j]) {
Ordering::Less => i += 1,
Ordering::Greater => j += 1,
Ordering::Equal => {
out.push(a[i]);
i += 1;
j += 1;
}
}
}
out
}
fn sort_stage(
items: &[Value],
columns: Option<&Columns>,
indices: Vec<usize>,
sort_specs: &[SortSpec],
) -> Result<Vec<usize>> {
if sort_specs.is_empty() {
return Ok(indices);
}
if let Some(sorted) = columnar_sort(columns, &indices, sort_specs) {
return Ok(sorted);
}
sort::sort_indices_of(items, indices, sort_specs)
}
fn columnar_sort(
columns: Option<&Columns>,
indices: &[usize],
sort_specs: &[SortSpec],
) -> Option<Vec<usize>> {
let keys: Vec<(&str, SortDirection)> = sort_specs
.iter()
.map(|spec| (spec.field.as_str(), spec.direction))
.collect();
columns?.sort_subset(indices, &keys)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::filter::{FilterLogic, FilterOp, FilterSpec};
use crate::sort::{NullsPosition, SortDirection};
use std::collections::BTreeMap;
fn item(n: i64) -> Value {
let mut map = BTreeMap::new();
map.insert("n".to_owned(), Value::Int(n));
Value::Map(map)
}
#[test]
fn filter_sort_paginate_in_one_pass() {
let items: Vec<Value> = (0..20).map(item).collect();
let filter = FilterInput::Flat(vec![FilterSpec {
field: "n".into(),
op: FilterOp::Gte,
value: Value::Int(5),
logic: FilterLogic::And,
}]);
let sorts = [SortSpec {
field: "n".into(),
direction: SortDirection::Desc,
nulls: NullsPosition::Last,
}];
let page = offset_page(&items, None, Some(&filter), &sorts, 1, 5).unwrap();
assert_eq!(page.total, 15);
assert_eq!(page.pages, 3);
assert!(page.has_next);
assert!(!page.has_previous);
assert_eq!(page.indices, vec![19, 18, 17, 16, 15]);
}
#[test]
fn page_past_the_end_is_empty() {
let items: Vec<Value> = (0..10).map(item).collect();
let page = offset_page(&items, None, None, &[], 99, 5).unwrap();
assert_eq!(page.total, 10);
assert!(page.indices.is_empty());
assert!(!page.has_next);
}
#[test]
fn columnar_path_matches_row_path() {
let items: Vec<Value> = (0..30).map(item).collect();
let cols = crate::columnar::Columns::build(&items);
let filter = FilterInput::Flat(vec![FilterSpec {
field: "n".into(),
op: FilterOp::Gte,
value: Value::Int(10),
logic: FilterLogic::And,
}]);
let sorts = [SortSpec {
field: "n".into(),
direction: SortDirection::Desc,
nulls: NullsPosition::Last,
}];
let with_cols = offset_page(&items, Some(&cols), Some(&filter), &sorts, 1, 7).unwrap();
let row = offset_page(&items, None, Some(&filter), &sorts, 1, 7).unwrap();
assert_eq!(with_cols, row);
}
#[test]
fn columnar_multi_filter_and_matches_row() {
let items: Vec<Value> = (0..30).map(item).collect();
let cols = crate::columnar::Columns::build(&items);
let filter = FilterInput::Flat(vec![
FilterSpec {
field: "n".into(),
op: FilterOp::Gte,
value: Value::Int(10),
logic: FilterLogic::And,
},
FilterSpec {
field: "n".into(),
op: FilterOp::Lt,
value: Value::Int(15),
logic: FilterLogic::And,
},
]);
let with_cols = offset_page(&items, Some(&cols), Some(&filter), &[], 1, 100).unwrap();
let row = offset_page(&items, None, Some(&filter), &[], 1, 100).unwrap();
assert_eq!(with_cols, row);
assert_eq!(with_cols.total, 5); }
#[test]
fn columnar_bool_filter_matches_row() {
let items: Vec<Value> = (0..30)
.map(|n| {
let mut map = BTreeMap::new();
map.insert("n".to_owned(), Value::Int(n));
map.insert("active".to_owned(), Value::Bool(n % 2 == 0));
Value::Map(map)
})
.collect();
let cols = crate::columnar::Columns::build(&items);
let filter = FilterInput::Flat(vec![FilterSpec {
field: "active".into(),
op: FilterOp::Eq,
value: Value::Bool(true),
logic: FilterLogic::And,
}]);
let with_cols = offset_page(&items, Some(&cols), Some(&filter), &[], 1, 100).unwrap();
let row = offset_page(&items, None, Some(&filter), &[], 1, 100).unwrap();
assert_eq!(with_cols, row);
assert_eq!(with_cols.total, 15); }
#[test]
fn columnar_bool_sort_matches_row() {
let items: Vec<Value> = (0..10)
.map(|n| {
let mut map = BTreeMap::new();
map.insert("n".to_owned(), Value::Int(n));
map.insert("active".to_owned(), Value::Bool(n % 3 == 0));
Value::Map(map)
})
.collect();
let cols = crate::columnar::Columns::build(&items);
let sorts = [SortSpec {
field: "active".into(),
direction: SortDirection::Desc,
nulls: NullsPosition::Last,
}];
let with_cols = offset_page(&items, Some(&cols), None, &sorts, 1, 100).unwrap();
let row = offset_page(&items, None, None, &sorts, 1, 100).unwrap();
assert_eq!(with_cols, row);
}
}