use crate::{Junction, PageTarget};
use queryweave::{FilterOperator, SortDirectionModel};
pub const LIKE_ESCAPE: char = '!';
#[must_use]
pub const fn sql_operator(operator: FilterOperator) -> &'static str {
match operator {
FilterOperator::LessThan => "<",
FilterOperator::Equal => "=",
FilterOperator::GreaterThan => ">",
FilterOperator::StartsWith | FilterOperator::Contains | FilterOperator::EndsWith => "LIKE",
}
}
#[must_use]
pub const fn sql_junction(junction: Junction) -> &'static str {
match junction {
Junction::And => "AND",
Junction::Or => "OR",
}
}
#[must_use]
pub const fn sql_direction(direction: SortDirectionModel) -> &'static str {
match direction {
SortDirectionModel::Ascending => "ASC",
SortDirectionModel::Descending => "DESC",
}
}
#[must_use]
pub fn like_pattern(operator: FilterOperator, text: &str) -> Option<String> {
let text = escape_like(text);
match operator {
FilterOperator::StartsWith => Some(format!("{text}%")),
FilterOperator::Contains => Some(format!("%{text}%")),
FilterOperator::EndsWith => Some(format!("%{text}")),
FilterOperator::LessThan | FilterOperator::Equal | FilterOperator::GreaterThan => None,
}
}
#[must_use]
pub fn escape_like(value: &str) -> String {
let mut escaped = String::with_capacity(value.len());
for character in value.chars() {
if matches!(character, '%' | '_') || character == LIKE_ESCAPE {
escaped.push(LIKE_ESCAPE);
}
escaped.push(character);
}
escaped
}
#[must_use]
pub fn page_offset(page: u32, page_size: u32) -> i64 {
i64::try_from(u64::from(page) * u64::from(page_size)).unwrap_or(i64::MAX)
}
pub fn push_page<Q: PageTarget + ?Sized>(query: &mut Q, page: Option<u32>, page_size: Option<u32>) {
query.write_page(page, page_size);
}
#[cfg(test)]
mod tests {
use super::{escape_like, like_pattern, page_offset};
use pretty_assertions::assert_eq;
use queryweave::FilterOperator;
use rstest::rstest;
#[rstest]
#[case(FilterOperator::StartsWith, Some("needle%"))]
#[case(FilterOperator::Contains, Some("%needle%"))]
#[case(FilterOperator::EndsWith, Some("%needle"))]
#[case(FilterOperator::Equal, None)]
#[case(FilterOperator::LessThan, None)]
#[case(FilterOperator::GreaterThan, None)]
fn the_like_pattern_matches_the_operator(
#[case] operator: FilterOperator,
#[case] expected_pattern: Option<&str>,
) {
assert_eq!(
like_pattern(operator, "needle").as_deref(),
expected_pattern
);
}
#[rstest]
#[case("%", "!%")]
#[case("_", "!_")]
#[case("!", "!!")]
#[case(r"\", r"\")]
#[case(r"50%_off!\now", r"50!%!_off!!\now")]
#[case("plain", "plain")]
fn metacharacters_are_escaped_with_the_escape_character(
#[case] value: &str,
#[case] expected: &str,
) {
assert_eq!(escape_like(value), expected);
}
#[test]
fn the_pattern_wraps_the_escaped_text() {
assert_eq!(
like_pattern(FilterOperator::Contains, "50%").as_deref(),
Some("%50!%%")
);
}
#[rstest]
#[case(0, 10, 0)]
#[case(3, 10, 30)]
#[case(u32::MAX, 1, 4_294_967_295)]
#[case(u32::MAX, u32::MAX, i64::MAX)]
fn the_offset_saturates(#[case] page: u32, #[case] page_size: u32, #[case] expected: i64) {
assert_eq!(page_offset(page, page_size), expected);
}
}