1use crate::{Junction, PageTarget};
11use queryweave::{FilterOperator, SortDirectionModel};
12
13pub const LIKE_ESCAPE: char = '!';
20
21#[must_use]
23pub const fn sql_operator(operator: FilterOperator) -> &'static str {
24 match operator {
25 FilterOperator::LessThan => "<",
26 FilterOperator::Equal => "=",
27 FilterOperator::GreaterThan => ">",
28 FilterOperator::StartsWith | FilterOperator::Contains | FilterOperator::EndsWith => "LIKE",
29 }
30}
31
32#[must_use]
34pub const fn sql_junction(junction: Junction) -> &'static str {
35 match junction {
36 Junction::And => "AND",
37 Junction::Or => "OR",
38 }
39}
40
41#[must_use]
43pub const fn sql_direction(direction: SortDirectionModel) -> &'static str {
44 match direction {
45 SortDirectionModel::Ascending => "ASC",
46 SortDirectionModel::Descending => "DESC",
47 }
48}
49
50#[must_use]
58pub fn like_pattern(operator: FilterOperator, text: &str) -> Option<String> {
59 let text = escape_like(text);
60
61 match operator {
62 FilterOperator::StartsWith => Some(format!("{text}%")),
63 FilterOperator::Contains => Some(format!("%{text}%")),
64 FilterOperator::EndsWith => Some(format!("%{text}")),
65 FilterOperator::LessThan | FilterOperator::Equal | FilterOperator::GreaterThan => None,
66 }
67}
68
69#[must_use]
72pub fn escape_like(value: &str) -> String {
73 let mut escaped = String::with_capacity(value.len());
74
75 for character in value.chars() {
76 if matches!(character, '%' | '_') || character == LIKE_ESCAPE {
77 escaped.push(LIKE_ESCAPE);
78 }
79 escaped.push(character);
80 }
81
82 escaped
83}
84
85#[must_use]
89pub fn page_offset(page: u32, page_size: u32) -> i64 {
90 i64::try_from(u64::from(page) * u64::from(page_size)).unwrap_or(i64::MAX)
91}
92
93pub fn push_page<Q: PageTarget + ?Sized>(query: &mut Q, page: Option<u32>, page_size: Option<u32>) {
98 query.write_page(page, page_size);
99}
100
101#[cfg(test)]
102mod tests {
103 use super::{escape_like, like_pattern, page_offset};
104 use pretty_assertions::assert_eq;
105 use queryweave::FilterOperator;
106 use rstest::rstest;
107
108 #[rstest]
113 #[case(FilterOperator::StartsWith, Some("needle%"))]
114 #[case(FilterOperator::Contains, Some("%needle%"))]
115 #[case(FilterOperator::EndsWith, Some("%needle"))]
116 #[case(FilterOperator::Equal, None)]
117 #[case(FilterOperator::LessThan, None)]
118 #[case(FilterOperator::GreaterThan, None)]
119 fn the_like_pattern_matches_the_operator(
120 #[case] operator: FilterOperator,
121 #[case] expected_pattern: Option<&str>,
122 ) {
123 assert_eq!(
124 like_pattern(operator, "needle").as_deref(),
125 expected_pattern
126 );
127 }
128
129 #[rstest]
132 #[case("%", "!%")]
133 #[case("_", "!_")]
134 #[case("!", "!!")]
135 #[case(r"\", r"\")]
136 #[case(r"50%_off!\now", r"50!%!_off!!\now")]
137 #[case("plain", "plain")]
138 fn metacharacters_are_escaped_with_the_escape_character(
139 #[case] value: &str,
140 #[case] expected: &str,
141 ) {
142 assert_eq!(escape_like(value), expected);
143 }
144
145 #[test]
147 fn the_pattern_wraps_the_escaped_text() {
148 assert_eq!(
149 like_pattern(FilterOperator::Contains, "50%").as_deref(),
150 Some("%50!%%")
151 );
152 }
153
154 #[rstest]
156 #[case(0, 10, 0)]
157 #[case(3, 10, 30)]
158 #[case(u32::MAX, 1, 4_294_967_295)]
159 #[case(u32::MAX, u32::MAX, i64::MAX)]
160 fn the_offset_saturates(#[case] page: u32, #[case] page_size: u32, #[case] expected: i64) {
161 assert_eq!(page_offset(page, page_size), expected);
162 }
163}