1use rainier_orm::sea_query::{ColumnRef, Expr, SimpleExpr, Value};
23
24#[derive(Debug, Clone)]
26pub enum Constraint {
27 Eq(String, Value),
29 Ne(String, Value),
31 Gt(String, Value),
33 Gte(String, Value),
35 Lt(String, Value),
37 Lte(String, Value),
39 Like(String, String),
41 NotLike(String, String),
43 In(String, Vec<Value>),
45 NotIn(String, Vec<Value>),
47 Null(String),
49 NotNull(String),
51}
52
53impl Constraint {
54 pub fn column(&self) -> &str {
56 match self {
57 Constraint::Eq(column, _)
58 | Constraint::Ne(column, _)
59 | Constraint::Gt(column, _)
60 | Constraint::Gte(column, _)
61 | Constraint::Lt(column, _)
62 | Constraint::Lte(column, _)
63 | Constraint::Like(column, _)
64 | Constraint::NotLike(column, _)
65 | Constraint::In(column, _)
66 | Constraint::NotIn(column, _)
67 | Constraint::Null(column)
68 | Constraint::NotNull(column) => column,
69 }
70 }
71
72 pub fn as_equality(&self) -> Option<(&str, &Value)> {
77 match self {
78 Constraint::Eq(column, value) => Some((column, value)),
79 _ => None,
80 }
81 }
82
83 pub fn to_expr(&self, column: ColumnRef) -> SimpleExpr {
85 let expr = Expr::col(column);
86 match self {
87 Constraint::Eq(_, value) => expr.eq(value.clone()),
88 Constraint::Ne(_, value) => expr.ne(value.clone()),
89 Constraint::Gt(_, value) => expr.gt(value.clone()),
90 Constraint::Gte(_, value) => expr.gte(value.clone()),
91 Constraint::Lt(_, value) => expr.lt(value.clone()),
92 Constraint::Lte(_, value) => expr.lte(value.clone()),
93 Constraint::Like(_, pattern) => expr.like(pattern.as_str()),
94 Constraint::NotLike(_, pattern) => expr.not_like(pattern.as_str()),
95 Constraint::In(_, values) => expr.is_in(values.clone()),
96 Constraint::NotIn(_, values) => expr.is_not_in(values.clone()),
97 Constraint::Null(_) => expr.is_null(),
98 Constraint::NotNull(_) => expr.is_not_null(),
99 }
100 }
101}
102
103#[derive(Debug, Clone, Default)]
108pub struct Criteria {
109 constraints: Vec<Constraint>,
110 joins: Vec<(String, String, String)>,
112 orders: Vec<(String, bool)>,
114 limit: Option<u64>,
115 offset: Option<u64>,
116}
117
118impl Criteria {
119 pub fn new() -> Self {
121 Self::default()
122 }
123
124 pub fn where_eq(mut self, column: impl Into<String>, value: impl Into<Value>) -> Self {
126 self.constraints.push(Constraint::Eq(column.into(), value.into()));
127 self
128 }
129
130 pub fn where_ne(mut self, column: impl Into<String>, value: impl Into<Value>) -> Self {
132 self.constraints.push(Constraint::Ne(column.into(), value.into()));
133 self
134 }
135
136 pub fn where_gt(mut self, column: impl Into<String>, value: impl Into<Value>) -> Self {
138 self.constraints.push(Constraint::Gt(column.into(), value.into()));
139 self
140 }
141
142 pub fn where_gte(mut self, column: impl Into<String>, value: impl Into<Value>) -> Self {
144 self.constraints.push(Constraint::Gte(column.into(), value.into()));
145 self
146 }
147
148 pub fn where_lt(mut self, column: impl Into<String>, value: impl Into<Value>) -> Self {
150 self.constraints.push(Constraint::Lt(column.into(), value.into()));
151 self
152 }
153
154 pub fn where_lte(mut self, column: impl Into<String>, value: impl Into<Value>) -> Self {
156 self.constraints.push(Constraint::Lte(column.into(), value.into()));
157 self
158 }
159
160 pub fn where_like(mut self, column: impl Into<String>, pattern: impl Into<String>) -> Self {
162 self.constraints.push(Constraint::Like(column.into(), pattern.into()));
163 self
164 }
165
166 pub fn where_not_like(mut self, column: impl Into<String>, pattern: impl Into<String>) -> Self {
168 self.constraints.push(Constraint::NotLike(column.into(), pattern.into()));
169 self
170 }
171
172 pub fn where_in<V: Into<Value>>(
174 mut self,
175 column: impl Into<String>,
176 values: impl IntoIterator<Item = V>,
177 ) -> Self {
178 let values = values.into_iter().map(Into::into).collect();
179 self.constraints.push(Constraint::In(column.into(), values));
180 self
181 }
182
183 pub fn where_not_in<V: Into<Value>>(
185 mut self,
186 column: impl Into<String>,
187 values: impl IntoIterator<Item = V>,
188 ) -> Self {
189 let values = values.into_iter().map(Into::into).collect();
190 self.constraints.push(Constraint::NotIn(column.into(), values));
191 self
192 }
193
194 pub fn where_null(mut self, column: impl Into<String>) -> Self {
196 self.constraints.push(Constraint::Null(column.into()));
197 self
198 }
199
200 pub fn where_not_null(mut self, column: impl Into<String>) -> Self {
202 self.constraints.push(Constraint::NotNull(column.into()));
203 self
204 }
205
206 pub fn join(
212 mut self,
213 table: impl Into<String>,
214 local: impl Into<String>,
215 foreign: impl Into<String>,
216 ) -> Self {
217 self.joins.push((table.into(), local.into(), foreign.into()));
218 self
219 }
220
221 pub fn order_by(mut self, column: impl Into<String>) -> Self {
223 self.orders.push((column.into(), false));
224 self
225 }
226
227 pub fn order_by_desc(mut self, column: impl Into<String>) -> Self {
229 self.orders.push((column.into(), true));
230 self
231 }
232
233 pub fn limit(mut self, n: u64) -> Self {
241 self.limit = Some(n.min(i64::MAX as u64));
242 self
243 }
244
245 pub fn offset(mut self, n: u64) -> Self {
248 self.offset = Some(n.min(i64::MAX as u64));
249 self
250 }
251
252 pub fn merge(mut self, other: Criteria) -> Self {
255 self.constraints.extend(other.constraints);
256 self.joins.extend(other.joins);
257 self.orders.extend(other.orders);
258 self.limit = other.limit.or(self.limit);
259 self.offset = other.offset.or(self.offset);
260 self
261 }
262
263 pub fn when(self, condition: bool, scope: impl FnOnce(Self) -> Self) -> Self {
265 if condition {
266 scope(self)
267 } else {
268 self
269 }
270 }
271
272 pub fn is_empty(&self) -> bool {
275 self.constraints.is_empty() && self.joins.is_empty()
276 }
277
278 pub fn constraints(&self) -> &[Constraint] {
280 &self.constraints
281 }
282
283 pub fn joins(&self) -> impl Iterator<Item = (&str, &str, &str)> {
285 self.joins.iter().map(|(t, l, f)| (t.as_str(), l.as_str(), f.as_str()))
286 }
287
288 pub fn orders(&self) -> &[(String, bool)] {
290 &self.orders
291 }
292
293 pub fn limit_value(&self) -> Option<u64> {
295 self.limit
296 }
297
298 pub fn offset_value(&self) -> Option<u64> {
300 self.offset
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn starts_empty() {
310 let criteria = Criteria::new();
311 assert!(criteria.is_empty());
312 assert_eq!(criteria.limit_value(), None);
313 assert!(criteria.constraints().is_empty());
314 }
315
316 #[test]
317 fn a_limit_beyond_what_a_driver_can_bind_is_clamped() {
318 let criteria = Criteria::new().limit(u64::MAX).offset(u64::MAX);
322
323 assert_eq!(criteria.limit_value(), Some(i64::MAX as u64));
324 assert_eq!(criteria.offset_value(), Some(i64::MAX as u64));
325 }
326
327 #[test]
328 fn records_constraints_in_order() {
329 let criteria = Criteria::new().where_eq("published", true).where_gt("views", 100_i64);
330
331 assert!(!criteria.is_empty());
332 assert_eq!(criteria.constraints().len(), 2);
333 assert_eq!(criteria.constraints()[0].column(), "published");
334 assert_eq!(criteria.constraints()[1].column(), "views");
335 }
336
337 #[test]
338 fn only_equalities_report_a_routable_pair() {
339 let criteria = Criteria::new().where_eq("user_id", 1_u64).where_gt("user_id", 1_u64);
340
341 assert!(criteria.constraints()[0].as_equality().is_some());
342 assert!(
343 criteria.constraints()[1].as_equality().is_none(),
344 "a range could span every shard"
345 );
346 }
347
348 #[test]
349 fn ordering_and_paging_alone_leave_it_empty() {
350 let criteria = Criteria::new().order_by_desc("id").limit(5);
351 assert!(criteria.is_empty(), "nothing is being filtered");
352 assert_eq!(criteria.limit_value(), Some(5));
353 }
354
355 #[test]
356 fn merge_appends_and_prefers_the_others_paging() {
357 let base = Criteria::new().where_eq("a", 1_i64).limit(5).offset(10);
358 let extra = Criteria::new().where_eq("b", 2_i64).limit(20);
359
360 let merged = base.merge(extra);
361 assert_eq!(merged.constraints().len(), 2);
362 assert_eq!(merged.limit_value(), Some(20), "the merged-in limit wins");
363 assert_eq!(merged.offset_value(), Some(10), "the original offset survives");
364 }
365
366 #[test]
367 fn merge_keeps_the_original_paging_when_the_other_sets_none() {
368 let merged = Criteria::new().limit(5).merge(Criteria::new().where_eq("a", 1_i64));
369 assert_eq!(merged.limit_value(), Some(5));
370 }
371
372 #[test]
373 fn when_applies_a_scope_conditionally() {
374 assert_eq!(Criteria::new().when(true, |c| c.where_eq("a", 1_i64)).constraints().len(), 1);
375 assert!(Criteria::new().when(false, |c| c.where_eq("a", 1_i64)).is_empty());
376 }
377
378 #[test]
379 fn joins_are_recorded_with_both_sides() {
380 let criteria = Criteria::new().join("authors", "author_id", "id");
381 assert!(!criteria.is_empty());
382 assert_eq!(criteria.joins().collect::<Vec<_>>(), vec![("authors", "author_id", "id")]);
383 }
384
385 #[test]
386 fn ordering_records_its_direction() {
387 let criteria = Criteria::new().order_by("a").order_by_desc("b");
388 assert_eq!(criteria.orders(), &[("a".to_string(), false), ("b".to_string(), true)]);
389 }
390}