use cratestack_core::CratestackError;
use crate::FilterExpr;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeysetColumn {
pub name: String,
pub descending: bool,
pub nullable: bool,
}
#[derive(Debug, Clone, Copy)]
pub enum KeysetLeaf<'a, V> {
Eq(&'a V),
Gt(&'a V),
Lt(&'a V),
IsNull,
}
pub fn parse_keyset_sort(
sort: Option<&str>,
primary_key: &str,
nullable_of: impl Fn(&str) -> Option<bool>,
) -> Result<Vec<KeysetColumn>, CratestackError> {
let mut columns: Vec<KeysetColumn> = Vec::new();
for raw in sort.into_iter().flat_map(|sort| sort.split(',')) {
let raw = raw.trim();
let (descending, name) = match raw.strip_prefix('-') {
Some(name) => (true, name),
None => (false, raw),
};
if name.is_empty() {
return Err(CratestackError::BadRequest(
"sort must not contain empty fields".to_owned(),
));
}
if name.contains('.') {
return Err(CratestackError::BadRequest(format!(
"cursor pagination does not support relation-path sort '{name}'"
)));
}
let Some(nullable) = nullable_of(name) else {
return Err(CratestackError::BadRequest(format!(
"cursor pagination does not support sort field '{name}'"
)));
};
if columns.iter().any(|column| column.name == name) {
return Err(CratestackError::BadRequest(format!(
"sort field '{name}' is repeated"
)));
}
columns.push(KeysetColumn {
name: name.to_owned(),
descending,
nullable,
});
}
if !columns.iter().any(|column| column.name == primary_key) {
columns.push(KeysetColumn {
name: primary_key.to_owned(),
descending: false,
nullable: false,
});
}
Ok(columns)
}
pub fn keyset_order_spec(columns: &[KeysetColumn]) -> String {
columns
.iter()
.map(|column| {
if column.descending {
format!("-{}", column.name)
} else {
column.name.clone()
}
})
.collect::<Vec<_>>()
.join(",")
}
pub fn keyset_after<V, E>(
columns: &[KeysetColumn],
values: &[Option<V>],
mut leaf: impl FnMut(&str, KeysetLeaf<'_, V>) -> Result<FilterExpr, E>,
) -> Result<FilterExpr, E>
where
E: From<CratestackError>,
{
if columns.len() != values.len() || columns.is_empty() {
return Err(
CratestackError::BadRequest("cursor does not match the sort order".to_owned()).into(),
);
}
let mut terms = Vec::new();
for (index, (column, value)) in columns.iter().zip(values).enumerate() {
let Some(value) = value else { continue };
let mut conjuncts = Vec::with_capacity(index + 1);
for (prior, prior_value) in columns[..index].iter().zip(values) {
conjuncts.push(match prior_value {
Some(prior_value) => leaf(&prior.name, KeysetLeaf::Eq(prior_value))?,
None => leaf(&prior.name, KeysetLeaf::IsNull)?,
});
}
let strictly_after = if column.descending {
leaf(&column.name, KeysetLeaf::Lt(value))?
} else {
leaf(&column.name, KeysetLeaf::Gt(value))?
};
conjuncts.push(if column.nullable {
FilterExpr::any([strictly_after, leaf(&column.name, KeysetLeaf::IsNull)?])
} else {
strictly_after
});
terms.push(FilterExpr::all(conjuncts));
}
Ok(FilterExpr::any(terms))
}
#[cfg(test)]
#[path = "keyset_tests.rs"]
mod tests;