1use std::{cmp::Ordering, collections::HashMap, marker::PhantomData, str::FromStr};
2
3use rustrails_support::{database, runtime};
4use sea_orm::{
5 ColumnTrait, Condition, ConnectionTrait, DatabaseConnection, DbBackend, EntityTrait, ExprTrait,
6 FromQueryResult, IdenStatic, Iterable, Order, QueryFilter, QueryOrder, QuerySelect, QueryTrait,
7 Select, Statement, Value,
8 sea_query::{ColumnType, Expr, SimpleExpr},
9};
10use serde_json::{Value as JsonValue, json};
11
12use crate::{OrderDirection, Record, RecordError, RecordState};
13
14#[derive(Debug, Clone, Default)]
15struct ConditionGroup {
16 positive: Vec<(String, JsonValue)>,
17 negative: Vec<(String, JsonValue)>,
18}
19
20impl ConditionGroup {
21 fn from_positive(conditions: HashMap<String, JsonValue>) -> Self {
22 Self {
23 positive: conditions.into_iter().collect(),
24 negative: Vec::new(),
25 }
26 }
27
28 fn is_empty(&self) -> bool {
29 self.positive.is_empty() && self.negative.is_empty()
30 }
31}
32
33#[derive(Debug)]
35pub struct Relation<T: Record> {
36 where_groups: Vec<ConditionGroup>,
37 order_by: Vec<(String, OrderDirection)>,
38 group_columns: Vec<String>,
39 having_conditions: Vec<String>,
40 is_distinct: bool,
41 select_columns: Vec<String>,
42 join_associations: Vec<String>,
43 included_associations: Vec<String>,
44 limit_val: Option<u64>,
45 offset_val: Option<u64>,
46 _phantom: PhantomData<T>,
47}
48
49impl<T: Record> Clone for Relation<T> {
50 fn clone(&self) -> Self {
51 Self {
52 where_groups: self.where_groups.clone(),
53 order_by: self.order_by.clone(),
54 group_columns: self.group_columns.clone(),
55 having_conditions: self.having_conditions.clone(),
56 is_distinct: self.is_distinct,
57 select_columns: self.select_columns.clone(),
58 join_associations: self.join_associations.clone(),
59 included_associations: self.included_associations.clone(),
60 limit_val: self.limit_val,
61 offset_val: self.offset_val,
62 _phantom: PhantomData,
63 }
64 }
65}
66
67impl<T: Record> Default for Relation<T> {
68 fn default() -> Self {
69 Self {
70 where_groups: vec![ConditionGroup::default()],
71 order_by: Vec::new(),
72 group_columns: Vec::new(),
73 having_conditions: Vec::new(),
74 is_distinct: false,
75 select_columns: Vec::new(),
76 join_associations: Vec::new(),
77 included_associations: Vec::new(),
78 limit_val: None,
79 offset_val: None,
80 _phantom: PhantomData,
81 }
82 }
83}
84
85impl<T: Record> Relation<T> {
86 pub fn new() -> Self {
88 Self::default()
89 }
90
91 pub fn r#where(mut self, conditions: HashMap<String, JsonValue>) -> Self {
93 self.add_positive_conditions(conditions);
94 self
95 }
96
97 pub fn order(mut self, column: &str, dir: OrderDirection) -> Self {
99 self.order_by.push((column.to_owned(), dir));
100 self
101 }
102
103 pub fn group(mut self, column: &str) -> Self {
105 self.group_columns.push(column.to_owned());
106 self
107 }
108
109 pub fn having(mut self, condition: &str) -> Self {
111 self.having_conditions.push(condition.to_owned());
112 self
113 }
114
115 pub fn distinct(mut self) -> Self {
117 self.is_distinct = true;
118 self
119 }
120
121 pub fn select_columns(mut self, columns: &[&str]) -> Self {
123 self.select_columns
124 .extend(columns.iter().map(|column| (*column).to_owned()));
125 self
126 }
127
128 pub fn select(self, columns: &[&str]) -> Self {
130 self.select_columns(columns)
131 }
132
133 pub fn joins(mut self, association: &str) -> Self {
135 self.join_associations.push(association.to_owned());
136 self
137 }
138
139 pub fn includes(mut self, association: &str) -> Self {
141 self.included_associations.push(association.to_owned());
142 self
143 }
144
145 pub fn reorder(mut self, column: &str, dir: OrderDirection) -> Self {
147 self.order_by.clear();
148 self.order_by.push((column.to_owned(), dir));
149 self
150 }
151
152 pub fn reselect(mut self, columns: &[&str]) -> Self {
154 self.select_columns.clear();
155 self.select_columns
156 .extend(columns.iter().map(|column| (*column).to_owned()));
157 self
158 }
159
160 pub fn rewhere(mut self, conditions: HashMap<String, JsonValue>) -> Self {
162 self.where_groups = vec![ConditionGroup::from_positive(conditions)];
163 self
164 }
165
166 pub fn limit(mut self, n: u64) -> Self {
168 self.limit_val = Some(n);
169 self
170 }
171
172 pub fn offset(mut self, n: u64) -> Self {
174 self.offset_val = Some(n);
175 self
176 }
177
178 pub fn not_where(mut self, conditions: HashMap<String, JsonValue>) -> Self {
180 self.add_negative_conditions(conditions);
181 self
182 }
183
184 pub fn not(self, conditions: HashMap<String, JsonValue>) -> Self {
186 self.not_where(conditions)
187 }
188
189 pub fn or_where(mut self, conditions: HashMap<String, JsonValue>) -> Self {
191 if conditions.is_empty() {
192 return self;
193 }
194
195 let group = ConditionGroup::from_positive(conditions);
196 if self.where_groups.len() == 1 && self.where_groups[0].is_empty() {
197 self.where_groups[0] = group;
198 } else {
199 self.where_groups.push(group);
200 }
201 self
202 }
203
204 fn add_positive_conditions(&mut self, conditions: HashMap<String, JsonValue>) {
205 if conditions.is_empty() {
206 return;
207 }
208
209 let entries = conditions.into_iter().collect::<Vec<_>>();
210 if self.where_groups.is_empty() {
211 self.where_groups.push(ConditionGroup::default());
212 }
213 for group in &mut self.where_groups {
214 group.positive.extend(entries.iter().cloned());
215 }
216 }
217
218 fn add_negative_conditions(&mut self, conditions: HashMap<String, JsonValue>) {
219 if conditions.is_empty() {
220 return;
221 }
222
223 let entries = conditions.into_iter().collect::<Vec<_>>();
224 if self.where_groups.is_empty() {
225 self.where_groups.push(ConditionGroup::default());
226 }
227 for group in &mut self.where_groups {
228 group.negative.extend(entries.iter().cloned());
229 }
230 }
231
232 pub async fn load(&self, db: &DatabaseConnection) -> Result<Vec<T>, RecordError>
234 where
235 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
236 {
237 let models = self.build_select()?.all(db).await?;
238 Ok(models
239 .into_iter()
240 .map(|model| {
241 let mut record = T::from_sea_model(model);
242 record.set_record_state(RecordState::Persisted);
243 record
244 })
245 .collect())
246 }
247
248 pub fn load_sync(&self) -> Result<Vec<T>, RecordError>
250 where
251 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
252 {
253 database::with_db(|db| runtime::block_on(self.load(db)))
254 }
255
256 pub async fn find_each<F>(
258 &self,
259 batch_size: u64,
260 db: &DatabaseConnection,
261 mut f: F,
262 ) -> Result<(), RecordError>
263 where
264 F: FnMut(T),
265 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
266 {
267 self.find_in_batches(batch_size, db, |batch| {
268 for record in batch {
269 f(record);
270 }
271 })
272 .await
273 }
274
275 pub fn find_each_sync<F>(&self, batch_size: u64, mut f: F) -> Result<(), RecordError>
277 where
278 F: FnMut(T),
279 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
280 {
281 database::with_db(|db| runtime::block_on(self.find_each(batch_size, db, &mut f)))
282 }
283
284 pub async fn find_in_batches<F>(
286 &self,
287 batch_size: u64,
288 db: &DatabaseConnection,
289 mut f: F,
290 ) -> Result<(), RecordError>
291 where
292 F: FnMut(Vec<T>),
293 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
294 {
295 if batch_size == 0 {
296 return Err(RecordError::Invalid(
297 "batch size must be greater than zero".to_owned(),
298 ));
299 }
300
301 let mut offset = self.offset_val.unwrap_or(0);
302 let mut remaining = self.limit_val;
303
304 loop {
305 let current_batch_size =
306 remaining.map_or(batch_size, |limit| Ord::min(limit, batch_size));
307 if current_batch_size == 0 {
308 break;
309 }
310
311 let batch = Self::clone(self)
312 .limit(current_batch_size)
313 .offset(offset)
314 .load(db)
315 .await?;
316
317 if batch.is_empty() {
318 break;
319 }
320
321 let loaded = batch.len() as u64;
322 f(batch);
323
324 if loaded < current_batch_size {
325 break;
326 }
327
328 offset += loaded;
329 if let Some(remaining_count) = remaining.as_mut() {
330 *remaining_count = remaining_count.saturating_sub(loaded);
331 if *remaining_count == 0 {
332 break;
333 }
334 }
335 }
336
337 Ok(())
338 }
339
340 pub fn find_in_batches_sync<F>(&self, batch_size: u64, mut f: F) -> Result<(), RecordError>
342 where
343 F: FnMut(Vec<T>),
344 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
345 {
346 database::with_db(|db| runtime::block_on(self.find_in_batches(batch_size, db, &mut f)))
347 }
348
349 pub async fn first(&self, db: &DatabaseConnection) -> Result<Option<T>, RecordError>
351 where
352 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
353 {
354 let model = self.build_select()?.limit(1).one(db).await?;
355 Ok(model.map(|model| {
356 let mut record = T::from_sea_model(model);
357 record.set_record_state(RecordState::Persisted);
358 record
359 }))
360 }
361
362 pub fn first_sync(&self) -> Result<Option<T>, RecordError>
364 where
365 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
366 {
367 database::with_db(|db| runtime::block_on(self.first(db)))
368 }
369
370 pub async fn last(&self, db: &DatabaseConnection) -> Result<Option<T>, RecordError>
372 where
373 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
374 {
375 let mut records = self.load(db).await?;
376 Ok(records.pop())
377 }
378
379 pub fn last_sync(&self) -> Result<Option<T>, RecordError>
381 where
382 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
383 {
384 database::with_db(|db| runtime::block_on(self.last(db)))
385 }
386
387 pub async fn count(&self, db: &DatabaseConnection) -> Result<u64, RecordError>
389 where
390 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
391 <T::Entity as EntityTrait>::Model: FromQueryResult + Send + Sync,
392 {
393 self.build_select()?
394 .all(db)
395 .await
396 .map(|models| models.len() as u64)
397 .map_err(Into::into)
398 }
399
400 pub fn count_sync(&self) -> Result<u64, RecordError>
402 where
403 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
404 <T::Entity as EntityTrait>::Model: FromQueryResult + Send + Sync,
405 {
406 database::with_db(|db| runtime::block_on(self.count(db)))
407 }
408
409 pub async fn exists(&self, db: &DatabaseConnection) -> Result<bool, RecordError>
411 where
412 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
413 {
414 Ok(self.first(db).await?.is_some())
415 }
416
417 pub fn exists_sync(&self) -> Result<bool, RecordError>
419 where
420 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
421 {
422 database::with_db(|db| runtime::block_on(self.exists(db)))
423 }
424
425 pub async fn sole(&self, db: &DatabaseConnection) -> Result<T, RecordError>
427 where
428 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
429 {
430 let mut records = self.clone().limit(2).load(db).await?;
431 match records.len() {
432 0 => Err(RecordError::NotFound),
433 1 => Ok(records.pop().expect("exactly one row should be loaded")),
434 _ => Err(RecordError::SoleRecordExceeded),
435 }
436 }
437
438 pub fn sole_sync(&self) -> Result<T, RecordError>
440 where
441 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
442 {
443 database::with_db(|db| runtime::block_on(self.sole(db)))
444 }
445
446 pub async fn explain(&self, db: &DatabaseConnection) -> Result<String, RecordError>
448 where
449 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
450 {
451 let backend = db.get_database_backend();
452 let statement = self.build_select()?.build(backend);
453 let explain_statement = Statement {
454 sql: format!("{} {}", explain_prefix(backend), statement.sql),
455 values: statement.values,
456 db_backend: statement.db_backend,
457 };
458 let rows = db.query_all_raw(explain_statement).await?;
459
460 let lines = rows
461 .into_iter()
462 .filter_map(|row| {
463 if backend == DbBackend::Sqlite {
464 return row.try_get::<String>("", "detail").ok();
465 }
466
467 row.try_get_by_index::<String>(0).ok()
468 })
469 .collect::<Vec<_>>();
470
471 Ok(lines.join("\n"))
472 }
473
474 pub fn explain_sync(&self) -> Result<String, RecordError>
476 where
477 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
478 {
479 database::with_db(|db| runtime::block_on(self.explain(db)))
480 }
481
482 pub async fn sum(&self, column: &str, db: &DatabaseConnection) -> Result<f64, RecordError>
484 where
485 T: serde::Serialize,
486 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
487 {
488 self.pluck(column, db)
489 .await?
490 .into_iter()
491 .try_fold(0.0, |total, value| {
492 Ok(total + json_value_to_f64(&value, column)?.unwrap_or(0.0))
493 })
494 }
495
496 pub fn sum_sync(&self, column: &str) -> Result<f64, RecordError>
498 where
499 T: serde::Serialize,
500 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
501 {
502 database::with_db(|db| runtime::block_on(self.sum(column, db)))
503 }
504
505 pub async fn average(&self, column: &str, db: &DatabaseConnection) -> Result<f64, RecordError>
507 where
508 T: serde::Serialize,
509 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
510 {
511 let (total, count) = self.pluck(column, db).await?.into_iter().try_fold(
512 (0.0, 0_u64),
513 |(total, count), value| {
514 Ok::<(f64, u64), RecordError>(match json_value_to_f64(&value, column)? {
515 Some(number) => (total + number, count + 1),
516 None => (total, count),
517 })
518 },
519 )?;
520
521 if count == 0 {
522 return Ok(0.0);
523 }
524
525 Ok(total / count as f64)
526 }
527
528 pub fn average_sync(&self, column: &str) -> Result<f64, RecordError>
530 where
531 T: serde::Serialize,
532 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
533 {
534 database::with_db(|db| runtime::block_on(self.average(column, db)))
535 }
536
537 pub async fn minimum(
539 &self,
540 column: &str,
541 db: &DatabaseConnection,
542 ) -> Result<Option<JsonValue>, RecordError>
543 where
544 T: serde::Serialize,
545 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
546 {
547 relation_extreme(self.pluck(column, db).await?, column, Ordering::Less)
548 }
549
550 pub fn minimum_sync(&self, column: &str) -> Result<Option<JsonValue>, RecordError>
552 where
553 T: serde::Serialize,
554 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
555 {
556 database::with_db(|db| runtime::block_on(self.minimum(column, db)))
557 }
558
559 pub async fn maximum(
561 &self,
562 column: &str,
563 db: &DatabaseConnection,
564 ) -> Result<Option<JsonValue>, RecordError>
565 where
566 T: serde::Serialize,
567 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
568 {
569 relation_extreme(self.pluck(column, db).await?, column, Ordering::Greater)
570 }
571
572 pub fn maximum_sync(&self, column: &str) -> Result<Option<JsonValue>, RecordError>
574 where
575 T: serde::Serialize,
576 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
577 {
578 database::with_db(|db| runtime::block_on(self.maximum(column, db)))
579 }
580
581 pub async fn group_count(
583 &self,
584 db: &DatabaseConnection,
585 ) -> Result<HashMap<JsonValue, JsonValue>, RecordError>
586 where
587 T: serde::Serialize,
588 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
589 {
590 self.primary_group_column()?;
591 let grouped = self.filtered_grouped_records(db).await?;
592 Ok(grouped
593 .into_iter()
594 .map(|(key, records)| (key, json!(records.len() as i64)))
595 .collect())
596 }
597
598 pub async fn group_sum(
600 &self,
601 column: &str,
602 db: &DatabaseConnection,
603 ) -> Result<HashMap<JsonValue, JsonValue>, RecordError>
604 where
605 T: serde::Serialize,
606 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
607 {
608 let _group_column = self.primary_group_column()?;
609 let grouped = self.filtered_grouped_records(db).await?;
610 grouped
611 .into_iter()
612 .map(|(key, records)| Ok((key, aggregate_sum_value(&records, column)?)))
613 .collect()
614 }
615
616 pub async fn group_average(
618 &self,
619 column: &str,
620 db: &DatabaseConnection,
621 ) -> Result<HashMap<JsonValue, JsonValue>, RecordError>
622 where
623 T: serde::Serialize,
624 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
625 {
626 let _group_column = self.primary_group_column()?;
627 let grouped = self.filtered_grouped_records(db).await?;
628 grouped
629 .into_iter()
630 .map(|(key, records)| Ok((key, json!(aggregate_average_value(&records, column)?))))
631 .collect()
632 }
633
634 pub async fn group_minimum(
636 &self,
637 column: &str,
638 db: &DatabaseConnection,
639 ) -> Result<HashMap<JsonValue, JsonValue>, RecordError>
640 where
641 T: serde::Serialize,
642 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
643 {
644 let _group_column = self.primary_group_column()?;
645 let grouped = self.filtered_grouped_records(db).await?;
646 grouped
647 .into_iter()
648 .map(|(key, records)| {
649 let value = aggregate_extreme_value(&records, column, Ordering::Less)?.ok_or_else(
650 || RecordError::Invalid(format!("group `{key}` has no values for `{column}`")),
651 )?;
652 Ok((key, value))
653 })
654 .collect()
655 }
656
657 pub async fn group_maximum(
659 &self,
660 column: &str,
661 db: &DatabaseConnection,
662 ) -> Result<HashMap<JsonValue, JsonValue>, RecordError>
663 where
664 T: serde::Serialize,
665 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
666 {
667 let _group_column = self.primary_group_column()?;
668 let grouped = self.filtered_grouped_records(db).await?;
669 grouped
670 .into_iter()
671 .map(|(key, records)| {
672 let value = aggregate_extreme_value(&records, column, Ordering::Greater)?
673 .ok_or_else(|| {
674 RecordError::Invalid(format!("group `{key}` has no values for `{column}`"))
675 })?;
676 Ok((key, value))
677 })
678 .collect()
679 }
680
681 pub async fn pluck_columns(
683 &self,
684 columns: &[&str],
685 db: &DatabaseConnection,
686 ) -> Result<Vec<Vec<JsonValue>>, RecordError>
687 where
688 T: serde::Serialize,
689 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
690 {
691 if columns.is_empty() {
692 return Err(RecordError::Invalid(
693 "pluck_columns requires at least one column".to_owned(),
694 ));
695 }
696
697 let records = self.load(db).await?;
698 records
699 .iter()
700 .map(|record| {
701 columns
702 .iter()
703 .map(|column| serialized_record_field(record, column))
704 .collect()
705 })
706 .collect()
707 }
708
709 pub fn pluck_columns_sync(&self, columns: &[&str]) -> Result<Vec<Vec<JsonValue>>, RecordError>
711 where
712 T: serde::Serialize,
713 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
714 {
715 database::with_db(|db| runtime::block_on(self.pluck_columns(columns, db)))
716 }
717
718 pub async fn pluck(
720 &self,
721 column: &str,
722 db: &DatabaseConnection,
723 ) -> Result<Vec<JsonValue>, RecordError>
724 where
725 T: serde::Serialize,
726 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
727 {
728 self.load(db)
729 .await?
730 .iter()
731 .map(|record| serialized_record_field(record, column))
732 .collect()
733 }
734
735 pub fn pluck_sync(&self, column: &str) -> Result<Vec<JsonValue>, RecordError>
737 where
738 T: serde::Serialize,
739 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
740 {
741 database::with_db(|db| runtime::block_on(self.pluck(column, db)))
742 }
743
744 pub async fn pick(
746 &self,
747 column: &str,
748 db: &DatabaseConnection,
749 ) -> Result<Option<JsonValue>, RecordError>
750 where
751 T: serde::Serialize,
752 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
753 {
754 self.first(db)
755 .await?
756 .as_ref()
757 .map(|record| serialized_record_field(record, column))
758 .transpose()
759 }
760
761 pub fn pick_sync(&self, column: &str) -> Result<Option<JsonValue>, RecordError>
763 where
764 T: serde::Serialize,
765 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
766 {
767 database::with_db(|db| runtime::block_on(self.pick(column, db)))
768 }
769
770 pub async fn ids(&self, db: &DatabaseConnection) -> Result<Vec<i64>, RecordError>
772 where
773 T: serde::Serialize,
774 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
775 {
776 self.pluck(T::primary_key_name(), db)
777 .await?
778 .into_iter()
779 .map(|value| json_value_to_i64(&value, T::primary_key_name()))
780 .collect()
781 }
782
783 pub fn ids_sync(&self) -> Result<Vec<i64>, RecordError>
785 where
786 T: serde::Serialize,
787 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
788 {
789 database::with_db(|db| runtime::block_on(self.ids(db)))
790 }
791
792 fn primary_group_column(&self) -> Result<&str, RecordError> {
793 self.group_columns
794 .first()
795 .map(String::as_str)
796 .ok_or_else(|| {
797 RecordError::Invalid(
798 "grouped calculation requires at least one group column".to_owned(),
799 )
800 })
801 }
802
803 async fn grouped_records_for_calculation(
804 &self,
805 db: &DatabaseConnection,
806 ) -> Result<HashMap<JsonValue, Vec<JsonValue>>, RecordError>
807 where
808 T: serde::Serialize,
809 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
810 {
811 let group_column = self.primary_group_column()?;
812 let mut relation = self.clone();
813 relation.group_columns.clear();
814 relation.having_conditions.clear();
815 relation.select_columns.clear();
816
817 let mut grouped = HashMap::new();
818 for record in relation.load(db).await? {
819 let serialized = serde_json::to_value(&record)
820 .map_err(|error| RecordError::Invalid(error.to_string()))?;
821 let key = serialized_record_object_field(&serialized, group_column)?;
822 grouped.entry(key).or_insert_with(Vec::new).push(serialized);
823 }
824 Ok(grouped)
825 }
826
827 async fn filtered_grouped_records(
828 &self,
829 db: &DatabaseConnection,
830 ) -> Result<HashMap<JsonValue, Vec<JsonValue>>, RecordError>
831 where
832 T: serde::Serialize,
833 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
834 {
835 let group_column = self.primary_group_column()?;
836 let grouped = self.grouped_records_for_calculation(db).await?;
837 apply_having_conditions(grouped, group_column, &self.having_conditions)
838 }
839
840 pub(crate) fn condition(&self) -> Result<Condition, RecordError>
841 where
842 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
843 {
844 let active_groups = self
845 .where_groups
846 .iter()
847 .filter(|group| !group.is_empty())
848 .collect::<Vec<_>>();
849
850 if active_groups.is_empty() {
851 return Ok(Condition::all());
852 }
853
854 let mut disjunction = Condition::any();
855 for group in active_groups {
856 let mut conjunction = Condition::all();
857 for (column, value) in &group.positive {
858 conjunction = conjunction.add(build_filter::<T>(column, value, false)?);
859 }
860 for (column, value) in &group.negative {
861 conjunction = conjunction.add(build_filter::<T>(column, value, true)?);
862 }
863 disjunction = disjunction.add(conjunction);
864 }
865 Ok(disjunction)
866 }
867
868 fn build_select(&self) -> Result<Select<T::Entity>, RecordError>
869 where
870 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
871 {
872 let mut query = T::Entity::find().filter(self.condition()?);
873
874 if !self.select_columns.is_empty() {
875 query = query.select_only();
876 for column in <T::Entity as EntityTrait>::Column::iter() {
877 if self
878 .select_columns
879 .iter()
880 .any(|selected| selected == column.as_str())
881 {
882 query = query.column(column);
883 continue;
884 }
885
886 if let Some(default_expr) = default_projection_expr(&column) {
887 query = query.expr_as(default_expr, column.as_str());
888 } else {
889 query = query.column(column);
890 }
891 }
892 }
893
894 if self.is_distinct {
895 query = query.distinct();
896 }
897
898 for column in &self.group_columns {
899 query = query.group_by(resolve_column::<T>(column)?);
900 }
901
902 for condition in &self.having_conditions {
903 query = query.having(Expr::cust(condition.clone()));
904 }
905
906 for (column, dir) in &self.order_by {
907 query = query.order_by(resolve_column::<T>(column)?, sea_order(*dir));
908 }
909
910 if let Some(limit) = self.limit_val {
911 query = query.limit(limit);
912 }
913 if let Some(offset) = self.offset_val {
914 if self.limit_val.is_none() {
915 query = query.limit(i64::MAX as u64);
916 }
917 query = query.offset(offset);
918 }
919 Ok(query)
920 }
921}
922
923#[derive(Debug, Clone, Copy)]
924enum HavingComparison {
925 Eq,
926 NotEq,
927 Gt,
928 Gte,
929 Lt,
930 Lte,
931}
932
933#[derive(Debug, Clone)]
934enum HavingExpression {
935 GroupKey,
936 Count,
937 Sum(String),
938 Average(String),
939 Minimum(String),
940 Maximum(String),
941}
942
943#[derive(Debug, Clone)]
944struct HavingPredicate {
945 left: HavingExpression,
946 comparison: HavingComparison,
947 right: JsonValue,
948}
949
950fn serialized_record_object_field(
951 record: &JsonValue,
952 column: &str,
953) -> Result<JsonValue, RecordError> {
954 let object = record
955 .as_object()
956 .ok_or_else(|| RecordError::Invalid("record must serialize to a JSON object".to_owned()))?;
957
958 object
959 .get(column)
960 .cloned()
961 .ok_or_else(|| RecordError::Invalid(format!("unknown column: {column}")))
962}
963
964fn serialized_record_field<T: serde::Serialize>(
965 record: &T,
966 column: &str,
967) -> Result<JsonValue, RecordError> {
968 let value =
969 serde_json::to_value(record).map_err(|error| RecordError::Invalid(error.to_string()))?;
970 serialized_record_object_field(&value, column)
971}
972
973fn json_value_to_f64(value: &JsonValue, column: &str) -> Result<Option<f64>, RecordError> {
974 match value {
975 JsonValue::Null => Ok(None),
976 JsonValue::Number(number) => number.as_f64().map(Some).ok_or_else(|| {
977 RecordError::Invalid(format!("column `{column}` must contain numeric values"))
978 }),
979 _ => Err(RecordError::Invalid(format!(
980 "column `{column}` must contain numeric values"
981 ))),
982 }
983}
984
985fn json_value_to_i64(value: &JsonValue, column: &str) -> Result<i64, RecordError> {
986 match value {
987 JsonValue::Number(number) => {
988 if let Some(value) = number.as_i64() {
989 Ok(value)
990 } else if let Some(value) = number.as_u64() {
991 i64::try_from(value).map_err(|_| {
992 RecordError::Invalid(format!("column `{column}` does not fit in i64"))
993 })
994 } else {
995 Err(RecordError::Invalid(format!(
996 "column `{column}` must contain integer values"
997 )))
998 }
999 }
1000 _ => Err(RecordError::Invalid(format!(
1001 "column `{column}` must contain integer values"
1002 ))),
1003 }
1004}
1005
1006fn extracted_group_values(
1007 records: &[JsonValue],
1008 column: &str,
1009) -> Result<Vec<JsonValue>, RecordError> {
1010 records
1011 .iter()
1012 .map(|record| serialized_record_object_field(record, column))
1013 .collect()
1014}
1015
1016fn json_value_is_integral(value: &JsonValue) -> bool {
1017 matches!(value, JsonValue::Number(number) if number.as_i64().is_some() || number.as_u64().is_some())
1018}
1019
1020fn numeric_json_value(value: f64, prefer_integer: bool) -> JsonValue {
1021 if prefer_integer
1022 && value.fract() == 0.0
1023 && value >= i64::MIN as f64
1024 && value <= i64::MAX as f64
1025 {
1026 json!(value as i64)
1027 } else {
1028 json!(value)
1029 }
1030}
1031
1032fn aggregate_sum_value(records: &[JsonValue], column: &str) -> Result<JsonValue, RecordError> {
1033 let values = extracted_group_values(records, column)?;
1034 let mut total = 0.0;
1035 let mut all_integral = true;
1036
1037 for value in values {
1038 if let Some(number) = json_value_to_f64(&value, column)? {
1039 total += number;
1040 all_integral &= json_value_is_integral(&value);
1041 }
1042 }
1043
1044 Ok(numeric_json_value(total, all_integral))
1045}
1046
1047fn aggregate_average_value(records: &[JsonValue], column: &str) -> Result<f64, RecordError> {
1048 let values = extracted_group_values(records, column)?;
1049 let mut total = 0.0;
1050 let mut count = 0_u64;
1051
1052 for value in values {
1053 if let Some(number) = json_value_to_f64(&value, column)? {
1054 total += number;
1055 count += 1;
1056 }
1057 }
1058
1059 if count == 0 {
1060 Ok(0.0)
1061 } else {
1062 Ok(total / count as f64)
1063 }
1064}
1065
1066fn aggregate_extreme_value(
1067 records: &[JsonValue],
1068 column: &str,
1069 preferred: Ordering,
1070) -> Result<Option<JsonValue>, RecordError> {
1071 relation_extreme(extracted_group_values(records, column)?, column, preferred)
1072}
1073
1074fn apply_having_conditions(
1075 grouped: HashMap<JsonValue, Vec<JsonValue>>,
1076 group_column: &str,
1077 conditions: &[String],
1078) -> Result<HashMap<JsonValue, Vec<JsonValue>>, RecordError> {
1079 if conditions.is_empty() {
1080 return Ok(grouped);
1081 }
1082
1083 let predicates = conditions
1084 .iter()
1085 .map(|condition| parse_having_predicate(condition, group_column))
1086 .collect::<Result<Vec<_>, _>>()?;
1087
1088 let mut filtered = HashMap::new();
1089 for (key, records) in grouped {
1090 let mut keep = true;
1091 for predicate in &predicates {
1092 let left = evaluate_having_expression(&predicate.left, &key, &records)?;
1093 if !compare_having_values(&left, &predicate.right, predicate.comparison)? {
1094 keep = false;
1095 break;
1096 }
1097 }
1098
1099 if keep {
1100 filtered.insert(key, records);
1101 }
1102 }
1103 Ok(filtered)
1104}
1105
1106fn parse_having_predicate(
1107 condition: &str,
1108 group_column: &str,
1109) -> Result<HavingPredicate, RecordError> {
1110 let trimmed = condition.trim();
1111 let (operator, comparison) = [
1112 (">=", HavingComparison::Gte),
1113 ("<=", HavingComparison::Lte),
1114 ("!=", HavingComparison::NotEq),
1115 ("=", HavingComparison::Eq),
1116 (">", HavingComparison::Gt),
1117 ("<", HavingComparison::Lt),
1118 ]
1119 .into_iter()
1120 .find(|(operator, _)| trimmed.contains(operator))
1121 .ok_or_else(|| RecordError::Invalid(format!("unsupported HAVING clause: {condition}")))?;
1122
1123 let (left, right) = trimmed
1124 .split_once(operator)
1125 .ok_or_else(|| RecordError::Invalid(format!("unsupported HAVING clause: {condition}")))?;
1126
1127 Ok(HavingPredicate {
1128 left: parse_having_expression(left.trim(), group_column)?,
1129 comparison,
1130 right: parse_having_value(right.trim())?,
1131 })
1132}
1133
1134fn parse_having_expression(
1135 expression: &str,
1136 group_column: &str,
1137) -> Result<HavingExpression, RecordError> {
1138 let normalized = expression.trim();
1139 if normalized.eq_ignore_ascii_case(group_column)
1140 || normalized
1141 .rsplit('.')
1142 .next()
1143 .is_some_and(|column| column.eq_ignore_ascii_case(group_column))
1144 {
1145 return Ok(HavingExpression::GroupKey);
1146 }
1147
1148 let open = normalized.find('(').ok_or_else(|| {
1149 RecordError::Invalid(format!("unsupported HAVING expression: {expression}"))
1150 })?;
1151 let close = normalized.rfind(')').ok_or_else(|| {
1152 RecordError::Invalid(format!("unsupported HAVING expression: {expression}"))
1153 })?;
1154 let function = normalized[..open].trim().to_ascii_uppercase();
1155 let argument = normalized[open + 1..close].trim();
1156
1157 match function.as_str() {
1158 "COUNT" if argument == "*" => Ok(HavingExpression::Count),
1159 "SUM" => Ok(HavingExpression::Sum(argument.to_owned())),
1160 "AVG" | "AVERAGE" => Ok(HavingExpression::Average(argument.to_owned())),
1161 "MIN" | "MINIMUM" => Ok(HavingExpression::Minimum(argument.to_owned())),
1162 "MAX" | "MAXIMUM" => Ok(HavingExpression::Maximum(argument.to_owned())),
1163 _ => Err(RecordError::Invalid(format!(
1164 "unsupported HAVING expression: {expression}"
1165 ))),
1166 }
1167}
1168
1169fn parse_having_value(value: &str) -> Result<JsonValue, RecordError> {
1170 if (value.starts_with('\'') && value.ends_with('\''))
1171 || (value.starts_with('"') && value.ends_with('"'))
1172 {
1173 return Ok(json!(value[1..value.len() - 1].to_owned()));
1174 }
1175
1176 if value.eq_ignore_ascii_case("true") {
1177 return Ok(json!(true));
1178 }
1179 if value.eq_ignore_ascii_case("false") {
1180 return Ok(json!(false));
1181 }
1182 if let Ok(number) = value.parse::<i64>() {
1183 return Ok(json!(number));
1184 }
1185 if let Ok(number) = value.parse::<f64>() {
1186 return Ok(json!(number));
1187 }
1188
1189 Err(RecordError::Invalid(format!(
1190 "unsupported HAVING value: {value}"
1191 )))
1192}
1193
1194fn evaluate_having_expression(
1195 expression: &HavingExpression,
1196 key: &JsonValue,
1197 records: &[JsonValue],
1198) -> Result<JsonValue, RecordError> {
1199 match expression {
1200 HavingExpression::GroupKey => Ok(key.clone()),
1201 HavingExpression::Count => Ok(json!(records.len() as i64)),
1202 HavingExpression::Sum(column) => aggregate_sum_value(records, column),
1203 HavingExpression::Average(column) => Ok(json!(aggregate_average_value(records, column)?)),
1204 HavingExpression::Minimum(column) => {
1205 aggregate_extreme_value(records, column, Ordering::Less)?
1206 .ok_or_else(|| RecordError::Invalid(format!("group has no values for `{column}`")))
1207 }
1208 HavingExpression::Maximum(column) => {
1209 aggregate_extreme_value(records, column, Ordering::Greater)?
1210 .ok_or_else(|| RecordError::Invalid(format!("group has no values for `{column}`")))
1211 }
1212 }
1213}
1214
1215fn compare_having_values(
1216 left: &JsonValue,
1217 right: &JsonValue,
1218 comparison: HavingComparison,
1219) -> Result<bool, RecordError> {
1220 match comparison {
1221 HavingComparison::Eq => Ok(left == right),
1222 HavingComparison::NotEq => Ok(left != right),
1223 HavingComparison::Gt => {
1224 Ok(compare_json_values(left, right, "HAVING")? == Ordering::Greater)
1225 }
1226 HavingComparison::Gte => Ok(matches!(
1227 compare_json_values(left, right, "HAVING")?,
1228 Ordering::Greater | Ordering::Equal
1229 )),
1230 HavingComparison::Lt => Ok(compare_json_values(left, right, "HAVING")? == Ordering::Less),
1231 HavingComparison::Lte => Ok(matches!(
1232 compare_json_values(left, right, "HAVING")?,
1233 Ordering::Less | Ordering::Equal
1234 )),
1235 }
1236}
1237
1238fn default_projection_expr<C>(column: &C) -> Option<SimpleExpr>
1239where
1240 C: ColumnTrait,
1241{
1242 Some(match column.def().get_column_type() {
1243 ColumnType::Char(_)
1244 | ColumnType::String(_)
1245 | ColumnType::Text
1246 | ColumnType::Custom(_)
1247 | ColumnType::Enum { .. } => Expr::val(""),
1248 ColumnType::TinyInteger
1249 | ColumnType::SmallInteger
1250 | ColumnType::Integer
1251 | ColumnType::BigInteger
1252 | ColumnType::TinyUnsigned
1253 | ColumnType::SmallUnsigned
1254 | ColumnType::Unsigned
1255 | ColumnType::BigUnsigned
1256 | ColumnType::Year => Expr::val(0),
1257 ColumnType::Float | ColumnType::Double | ColumnType::Decimal(_) | ColumnType::Money(_) => {
1258 Expr::val(0.0)
1259 }
1260 ColumnType::Boolean => Expr::val(false),
1261 _ => return None,
1262 })
1263}
1264
1265pub(crate) fn resolve_column<T: Record>(
1266 name: &str,
1267) -> Result<<T::Entity as EntityTrait>::Column, RecordError>
1268where
1269 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
1270{
1271 <<T::Entity as EntityTrait>::Column as FromStr>::from_str(name)
1272 .map_err(|_| RecordError::Invalid(format!("unknown column: {name}")))
1273}
1274
1275pub(crate) fn json_to_sea_value(value: &JsonValue) -> Result<Value, RecordError> {
1276 match value {
1277 JsonValue::Null => Err(RecordError::Invalid(
1278 "null values are not supported in this context".to_owned(),
1279 )),
1280 JsonValue::Bool(flag) => Ok((*flag).into()),
1281 JsonValue::Number(number) => {
1282 if let Some(value) = number.as_i64() {
1283 Ok(value.into())
1284 } else if let Some(value) = number.as_u64() {
1285 Ok(value.into())
1286 } else if let Some(value) = number.as_f64() {
1287 Ok(value.into())
1288 } else {
1289 Err(RecordError::Invalid(format!(
1290 "unsupported numeric value: {number}"
1291 )))
1292 }
1293 }
1294 JsonValue::String(text) => Ok(text.clone().into()),
1295 JsonValue::Array(_) | JsonValue::Object(_) => Err(RecordError::Invalid(
1296 "only scalar JSON values are supported in query conditions".to_owned(),
1297 )),
1298 }
1299}
1300
1301fn relation_extreme(
1302 values: Vec<JsonValue>,
1303 column: &str,
1304 preferred: Ordering,
1305) -> Result<Option<JsonValue>, RecordError> {
1306 let mut values = values.into_iter().filter(|value| !value.is_null());
1307 let Some(mut extreme) = values.next() else {
1308 return Ok(None);
1309 };
1310
1311 for value in values {
1312 if compare_json_values(&value, &extreme, column)? == preferred {
1313 extreme = value;
1314 }
1315 }
1316
1317 Ok(Some(extreme))
1318}
1319
1320fn compare_json_values(
1321 left: &JsonValue,
1322 right: &JsonValue,
1323 column: &str,
1324) -> Result<Ordering, RecordError> {
1325 match (left, right) {
1326 (JsonValue::Number(left), JsonValue::Number(right)) => left
1327 .as_f64()
1328 .zip(right.as_f64())
1329 .and_then(|(left, right)| left.partial_cmp(&right))
1330 .ok_or_else(|| {
1331 RecordError::Invalid(format!(
1332 "column `{column}` must contain comparable numeric values"
1333 ))
1334 }),
1335 (JsonValue::String(left), JsonValue::String(right)) => Ok(left.cmp(right)),
1336 (JsonValue::Bool(left), JsonValue::Bool(right)) => Ok(left.cmp(right)),
1337 (JsonValue::Null, JsonValue::Null) => Ok(Ordering::Equal),
1338 _ => Err(RecordError::Invalid(format!(
1339 "column `{column}` must contain comparable scalar values"
1340 ))),
1341 }
1342}
1343
1344fn sea_order(dir: OrderDirection) -> Order {
1345 match dir {
1346 OrderDirection::Asc => Order::Asc,
1347 OrderDirection::Desc => Order::Desc,
1348 }
1349}
1350
1351fn explain_prefix(backend: DbBackend) -> &'static str {
1352 match backend {
1353 DbBackend::Sqlite => "EXPLAIN QUERY PLAN",
1354 DbBackend::MySql | DbBackend::Postgres => "EXPLAIN",
1355 _ => "EXPLAIN",
1356 }
1357}
1358
1359fn build_filter<T: Record>(
1360 column_name: &str,
1361 value: &JsonValue,
1362 negated: bool,
1363) -> Result<SimpleExpr, RecordError>
1364where
1365 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
1366{
1367 let column = resolve_column::<T>(column_name)?;
1368 let expr = Expr::col(column);
1369 let filter = match value {
1370 JsonValue::Null => {
1371 if negated {
1372 expr.is_not_null()
1373 } else {
1374 expr.is_null()
1375 }
1376 }
1377 _ => {
1378 let value = json_to_sea_value(value)?;
1379 if negated {
1380 expr.ne(value)
1381 } else {
1382 expr.eq(value)
1383 }
1384 }
1385 };
1386 Ok(filter)
1387}
1388
1389#[cfg(test)]
1390mod tests {
1391 use std::collections::HashMap;
1392
1393 use rustrails_support::{database, runtime};
1394 use sea_orm::{ActiveModelTrait, ActiveValue::Set, ConnectionTrait, Schema};
1395 use serde_json::{Value, json};
1396
1397 use super::Relation;
1398 use crate::{
1399 OrderDirection, RecordState,
1400 base::test_support::{TestUser, seed_users, test_user},
1401 };
1402
1403 fn run_relation_test(seed: bool, test: impl FnOnce() + Send + 'static) {
1404 std::thread::spawn(move || {
1405 let _rt = runtime::init_runtime();
1406 database::establish("sqlite::memory:")
1407 .expect("sqlite in-memory connection should succeed");
1408 runtime::block_on(async {
1409 let db = database::db();
1410 let schema = Schema::new(db.get_database_backend());
1411 db.execute(&schema.create_table_from_entity(test_user::Entity))
1412 .await
1413 .expect("test_users table should be created");
1414 if seed {
1415 seed_users(&db).await;
1416 }
1417 });
1418 test();
1419 })
1420 .join()
1421 .unwrap();
1422 }
1423
1424 fn run_seeded_relation_test(test: impl FnOnce() + Send + 'static) {
1425 run_relation_test(true, test);
1426 }
1427
1428 fn run_empty_relation_test(test: impl FnOnce() + Send + 'static) {
1429 run_relation_test(false, test);
1430 }
1431
1432 fn load_relation(relation: Relation<TestUser>) -> Result<Vec<TestUser>, crate::RecordError> {
1433 relation.load_sync()
1434 }
1435
1436 fn first_relation(
1437 relation: Relation<TestUser>,
1438 ) -> Result<Option<TestUser>, crate::RecordError> {
1439 relation.first_sync()
1440 }
1441
1442 fn last_relation(relation: Relation<TestUser>) -> Result<Option<TestUser>, crate::RecordError> {
1443 relation.last_sync()
1444 }
1445
1446 fn count_relation(relation: Relation<TestUser>) -> Result<u64, crate::RecordError> {
1447 relation.count_sync()
1448 }
1449
1450 fn exists_relation(relation: Relation<TestUser>) -> Result<bool, crate::RecordError> {
1451 relation.exists_sync()
1452 }
1453
1454 fn explain_relation(relation: Relation<TestUser>) -> Result<String, crate::RecordError> {
1455 relation.explain_sync()
1456 }
1457
1458 fn sum_relation(relation: Relation<TestUser>, column: &str) -> Result<f64, crate::RecordError> {
1459 relation.sum_sync(column)
1460 }
1461
1462 fn average_relation(
1463 relation: Relation<TestUser>,
1464 column: &str,
1465 ) -> Result<f64, crate::RecordError> {
1466 relation.average_sync(column)
1467 }
1468
1469 fn minimum_relation(
1470 relation: Relation<TestUser>,
1471 column: &str,
1472 ) -> Result<Option<Value>, crate::RecordError> {
1473 relation.minimum_sync(column)
1474 }
1475
1476 fn maximum_relation(
1477 relation: Relation<TestUser>,
1478 column: &str,
1479 ) -> Result<Option<Value>, crate::RecordError> {
1480 relation.maximum_sync(column)
1481 }
1482
1483 fn pluck_relation(
1484 relation: Relation<TestUser>,
1485 column: &str,
1486 ) -> Result<Vec<Value>, crate::RecordError> {
1487 relation.pluck_sync(column)
1488 }
1489
1490 fn pick_relation(
1491 relation: Relation<TestUser>,
1492 column: &str,
1493 ) -> Result<Option<Value>, crate::RecordError> {
1494 relation.pick_sync(column)
1495 }
1496
1497 fn relation_ids(relation: Relation<TestUser>) -> Result<Vec<i64>, crate::RecordError> {
1498 relation.ids_sync()
1499 }
1500
1501 fn sole_relation(relation: Relation<TestUser>) -> Result<TestUser, crate::RecordError> {
1502 relation.sole_sync()
1503 }
1504
1505 fn pluck_columns_relation(
1506 relation: Relation<TestUser>,
1507 columns: &[&str],
1508 ) -> Result<Vec<Vec<Value>>, crate::RecordError> {
1509 relation.pluck_columns_sync(columns)
1510 }
1511
1512 fn group_count_relation(
1513 relation: Relation<TestUser>,
1514 ) -> Result<HashMap<Value, Value>, crate::RecordError> {
1515 database::with_db(|db| runtime::block_on(relation.group_count(db)))
1516 }
1517
1518 fn group_sum_relation(
1519 relation: Relation<TestUser>,
1520 column: &str,
1521 ) -> Result<HashMap<Value, Value>, crate::RecordError> {
1522 database::with_db(|db| runtime::block_on(relation.group_sum(column, db)))
1523 }
1524
1525 fn group_average_relation(
1526 relation: Relation<TestUser>,
1527 column: &str,
1528 ) -> Result<HashMap<Value, Value>, crate::RecordError> {
1529 database::with_db(|db| runtime::block_on(relation.group_average(column, db)))
1530 }
1531
1532 fn group_minimum_relation(
1533 relation: Relation<TestUser>,
1534 column: &str,
1535 ) -> Result<HashMap<Value, Value>, crate::RecordError> {
1536 database::with_db(|db| runtime::block_on(relation.group_minimum(column, db)))
1537 }
1538
1539 fn group_maximum_relation(
1540 relation: Relation<TestUser>,
1541 column: &str,
1542 ) -> Result<HashMap<Value, Value>, crate::RecordError> {
1543 database::with_db(|db| runtime::block_on(relation.group_maximum(column, db)))
1544 }
1545
1546 fn insert_user(name: &str, email: &str) {
1547 database::with_db(|db| {
1548 runtime::block_on(async {
1549 test_user::ActiveModel {
1550 name: Set(name.to_owned()),
1551 email: Set(email.to_owned()),
1552 ..Default::default()
1553 }
1554 .insert(db)
1555 .await
1556 .expect("fixture insert should succeed");
1557 });
1558 });
1559 }
1560
1561 fn relation_names(relation: Relation<TestUser>) -> Vec<String> {
1562 load_relation(relation)
1563 .expect("relation should load")
1564 .into_iter()
1565 .map(|user| user.name)
1566 .collect()
1567 }
1568
1569 fn relation_name_email_pairs(relation: Relation<TestUser>) -> Vec<(String, String)> {
1570 load_relation(relation)
1571 .expect("relation should load")
1572 .into_iter()
1573 .map(|user| (user.name, user.email))
1574 .collect()
1575 }
1576
1577 fn find_each_names(
1578 relation: Relation<TestUser>,
1579 batch_size: u64,
1580 ) -> Result<Vec<String>, crate::RecordError> {
1581 let mut names = Vec::new();
1582 relation.find_each_sync(batch_size, |user| names.push(user.name))?;
1583 Ok(names)
1584 }
1585
1586 fn find_batch_names(
1587 relation: Relation<TestUser>,
1588 batch_size: u64,
1589 ) -> Result<Vec<Vec<String>>, crate::RecordError> {
1590 let mut batches = Vec::new();
1591 relation.find_in_batches_sync(batch_size, |users| {
1592 batches.push(users.into_iter().map(|user| user.name).collect());
1593 })?;
1594 Ok(batches)
1595 }
1596
1597 #[test]
1598 fn relation_load_returns_all_rows_when_scope_empty() {
1599 run_seeded_relation_test(|| {
1600 let users = load_relation(Relation::<TestUser>::new()).expect("relation should load");
1601 assert_eq!(users.len(), 3);
1602 assert_eq!(users[0].name, "Alice");
1603 assert_eq!(users[2].name, "Carol");
1604 });
1605 }
1606
1607 #[test]
1608 fn relation_load_returns_empty_vec_when_table_empty() {
1609 run_empty_relation_test(|| {
1610 let users = load_relation(Relation::<TestUser>::new()).expect("relation should load");
1611 assert!(users.is_empty());
1612 });
1613 }
1614
1615 #[test]
1616 fn relation_where_matches_single_name_condition() {
1617 run_seeded_relation_test(|| {
1618 let users = load_relation(
1619 Relation::<TestUser>::new()
1620 .r#where(HashMap::from([("name".to_owned(), json!("Bob"))])),
1621 )
1622 .expect("relation should load");
1623
1624 assert_eq!(users.len(), 1);
1625 assert_eq!(users[0].email, "bob@example.com");
1626 });
1627 }
1628
1629 #[test]
1630 fn relation_where_matches_single_email_condition() {
1631 run_seeded_relation_test(|| {
1632 let users = load_relation(Relation::<TestUser>::new().r#where(HashMap::from([(
1633 "email".to_owned(),
1634 json!("carol@example.com"),
1635 )])))
1636 .expect("relation should load");
1637
1638 assert_eq!(users.len(), 1);
1639 assert_eq!(users[0].name, "Carol");
1640 });
1641 }
1642
1643 #[test]
1644 fn relation_where_with_multiple_conditions_requires_all_matches() {
1645 run_seeded_relation_test(|| {
1646 let users = load_relation(Relation::<TestUser>::new().r#where(HashMap::from([
1647 ("name".to_owned(), json!("Bob")),
1648 ("email".to_owned(), json!("bob@example.com")),
1649 ])))
1650 .expect("relation should load");
1651
1652 assert_eq!(users.len(), 1);
1653 assert_eq!(users[0].id, Some(2));
1654 });
1655 }
1656
1657 #[test]
1658 fn relation_where_with_multiple_conditions_returns_no_partial_matches() {
1659 run_seeded_relation_test(|| {
1660 let users = load_relation(Relation::<TestUser>::new().r#where(HashMap::from([
1661 ("name".to_owned(), json!("Bob")),
1662 ("email".to_owned(), json!("alice@example.com")),
1663 ])))
1664 .expect("relation should load");
1665
1666 assert!(users.is_empty());
1667 });
1668 }
1669
1670 #[test]
1671 fn relation_order_ascending_returns_expected_sequence() {
1672 run_seeded_relation_test(|| {
1673 assert_eq!(
1674 relation_names(Relation::<TestUser>::new().order("id", OrderDirection::Asc)),
1675 vec!["Alice", "Bob", "Carol"],
1676 );
1677 });
1678 }
1679
1680 #[test]
1681 fn relation_order_descending_returns_expected_sequence() {
1682 run_seeded_relation_test(|| {
1683 assert_eq!(
1684 relation_names(Relation::<TestUser>::new().order("id", OrderDirection::Desc)),
1685 vec!["Carol", "Bob", "Alice"],
1686 );
1687 });
1688 }
1689
1690 #[test]
1691 fn relation_reorder_replaces_existing_sort_clauses() {
1692 run_seeded_relation_test(|| {
1693 assert_eq!(
1694 relation_name_email_pairs(
1695 Relation::<TestUser>::new()
1696 .order("name", OrderDirection::Asc)
1697 .reorder("id", OrderDirection::Desc),
1698 ),
1699 vec![
1700 ("Carol".to_owned(), "carol@example.com".to_owned()),
1701 ("Bob".to_owned(), "bob@example.com".to_owned()),
1702 ("Alice".to_owned(), "alice@example.com".to_owned()),
1703 ],
1704 );
1705 });
1706 }
1707
1708 #[test]
1709 fn relation_where_order_limit_offset_can_be_chained() {
1710 run_seeded_relation_test(|| {
1711 insert_user("Bob", "bobby@example.com");
1712
1713 let users = load_relation(
1714 Relation::<TestUser>::new()
1715 .r#where(HashMap::from([("name".to_owned(), json!("Bob"))]))
1716 .order("id", OrderDirection::Desc)
1717 .offset(1)
1718 .limit(1),
1719 )
1720 .expect("relation should load");
1721
1722 assert_eq!(users.len(), 1);
1723 assert_eq!(users[0].email, "bob@example.com");
1724 });
1725 }
1726
1727 #[test]
1728 fn relation_where_order_distinct_limit_can_be_chained() {
1729 run_seeded_relation_test(|| {
1730 insert_user("Bob", "bobby@example.com");
1731
1732 let users = load_relation(
1733 Relation::<TestUser>::new()
1734 .r#where(HashMap::from([("name".to_owned(), json!("Bob"))]))
1735 .order("id", OrderDirection::Desc)
1736 .distinct()
1737 .limit(5),
1738 )
1739 .expect("relation should load");
1740
1741 assert_eq!(users.len(), 2);
1742 assert_eq!(users[0].email, "bobby@example.com");
1743 assert_eq!(users[1].email, "bob@example.com");
1744 });
1745 }
1746
1747 #[test]
1748 fn relation_find_in_batches_yields_two_batches_for_three_rows() {
1749 run_seeded_relation_test(|| {
1750 let batches = find_batch_names(
1751 Relation::<TestUser>::new().order("id", OrderDirection::Asc),
1752 2,
1753 )
1754 .expect("batch query should succeed");
1755
1756 assert_eq!(
1757 batches,
1758 vec![
1759 vec!["Alice".to_owned(), "Bob".to_owned()],
1760 vec!["Carol".to_owned()],
1761 ]
1762 );
1763 });
1764 }
1765
1766 #[test]
1767 fn relation_find_each_batch_processes_all_rows() {
1768 run_seeded_relation_test(|| {
1769 let names = find_each_names(
1770 Relation::<TestUser>::new().order("id", OrderDirection::Asc),
1771 2,
1772 )
1773 .expect("each query should succeed");
1774
1775 assert_eq!(names, vec!["Alice", "Bob", "Carol"]);
1776 });
1777 }
1778
1779 #[test]
1780 fn relation_find_in_batches_yields_no_batches_for_empty_table() {
1781 run_empty_relation_test(|| {
1782 let batches = find_batch_names(
1783 Relation::<TestUser>::new().order("id", OrderDirection::Asc),
1784 2,
1785 )
1786 .expect("batch query should succeed");
1787
1788 assert!(batches.is_empty());
1789 });
1790 }
1791
1792 #[test]
1793 fn relation_find_in_batches_respects_existing_limit_and_offset_scope() {
1794 run_seeded_relation_test(|| {
1795 let batches = find_batch_names(
1796 Relation::<TestUser>::new()
1797 .order("id", OrderDirection::Asc)
1798 .offset(1)
1799 .limit(2),
1800 1,
1801 )
1802 .expect("batch query should succeed");
1803
1804 assert_eq!(
1805 batches,
1806 vec![vec!["Bob".to_owned()], vec!["Carol".to_owned()]],
1807 );
1808 });
1809 }
1810
1811 #[test]
1812 fn relation_find_in_batches_rejects_zero_batch_size() {
1813 run_seeded_relation_test(|| {
1814 let error = find_batch_names(
1815 Relation::<TestUser>::new().order("id", OrderDirection::Asc),
1816 0,
1817 )
1818 .expect_err("zero batch size should fail");
1819
1820 assert!(matches!(error, crate::RecordError::Invalid(_)));
1821 });
1822 }
1823
1824 #[test]
1825 fn relation_count_returns_total_for_unscoped_relation() {
1826 run_seeded_relation_test(|| {
1827 assert_eq!(
1828 count_relation(Relation::<TestUser>::new()).expect("count should succeed"),
1829 3
1830 );
1831 });
1832 }
1833
1834 #[test]
1835 fn relation_count_returns_matches_for_scoped_relation() {
1836 run_seeded_relation_test(|| {
1837 assert_eq!(
1838 count_relation(
1839 Relation::<TestUser>::new()
1840 .r#where(HashMap::from([("name".to_owned(), json!("Alice"))])),
1841 )
1842 .expect("count should succeed"),
1843 1,
1844 );
1845 });
1846 }
1847
1848 #[test]
1849 fn relation_count_returns_zero_for_empty_scope() {
1850 run_seeded_relation_test(|| {
1851 assert_eq!(
1852 count_relation(
1853 Relation::<TestUser>::new()
1854 .r#where(HashMap::from([("name".to_owned(), json!("Nobody"))])),
1855 )
1856 .expect("count should succeed"),
1857 0,
1858 );
1859 });
1860 }
1861
1862 #[test]
1863 fn relation_count_respects_limit_and_offset_scope() {
1864 run_seeded_relation_test(|| {
1865 assert_eq!(
1866 count_relation(
1867 Relation::<TestUser>::new()
1868 .order("id", OrderDirection::Asc)
1869 .offset(1)
1870 .limit(1),
1871 )
1872 .expect("count should succeed"),
1873 1,
1874 );
1875 });
1876 }
1877
1878 #[test]
1879 fn relation_grouped_count_counts_groups() {
1880 run_seeded_relation_test(|| {
1881 insert_user("Bob", "bobby@example.com");
1882
1883 assert_eq!(
1884 count_relation(Relation::<TestUser>::new().group("name"))
1885 .expect("count should succeed"),
1886 3,
1887 );
1888 });
1889 }
1890
1891 #[test]
1892 fn relation_first_returns_first_row_for_ordered_relation() {
1893 run_seeded_relation_test(|| {
1894 let user =
1895 first_relation(Relation::<TestUser>::new().order("id", OrderDirection::Desc))
1896 .expect("query should succeed")
1897 .expect("row should exist");
1898
1899 assert_eq!(user.name, "Carol");
1900 });
1901 }
1902
1903 #[test]
1904 fn relation_first_respects_offset_and_limit() {
1905 run_seeded_relation_test(|| {
1906 let user = first_relation(
1907 Relation::<TestUser>::new()
1908 .order("id", OrderDirection::Asc)
1909 .offset(1)
1910 .limit(1),
1911 )
1912 .expect("query should succeed")
1913 .expect("row should exist");
1914
1915 assert_eq!(user.name, "Bob");
1916 });
1917 }
1918
1919 #[test]
1920 fn relation_first_returns_none_for_empty_scope() {
1921 run_seeded_relation_test(|| {
1922 let user = first_relation(
1923 Relation::<TestUser>::new()
1924 .r#where(HashMap::from([("name".to_owned(), json!("Nobody"))])),
1925 )
1926 .expect("query should succeed");
1927
1928 assert!(user.is_none());
1929 });
1930 }
1931
1932 #[test]
1933 fn relation_last_returns_last_row_for_ordered_relation() {
1934 run_seeded_relation_test(|| {
1935 let user = last_relation(Relation::<TestUser>::new().order("id", OrderDirection::Asc))
1936 .expect("query should succeed")
1937 .expect("row should exist");
1938
1939 assert_eq!(user.name, "Carol");
1940 });
1941 }
1942
1943 #[test]
1944 fn relation_last_respects_offset_and_limit() {
1945 run_seeded_relation_test(|| {
1946 let user = last_relation(
1947 Relation::<TestUser>::new()
1948 .order("id", OrderDirection::Asc)
1949 .offset(1)
1950 .limit(2),
1951 )
1952 .expect("query should succeed")
1953 .expect("row should exist");
1954
1955 assert_eq!(user.name, "Carol");
1956 });
1957 }
1958
1959 #[test]
1960 fn relation_last_returns_none_for_empty_scope() {
1961 run_seeded_relation_test(|| {
1962 let user = last_relation(
1963 Relation::<TestUser>::new()
1964 .r#where(HashMap::from([("name".to_owned(), json!("Nobody"))])),
1965 )
1966 .expect("query should succeed");
1967
1968 assert!(user.is_none());
1969 });
1970 }
1971
1972 #[test]
1973 fn relation_exists_returns_true_for_matching_scope() {
1974 run_seeded_relation_test(|| {
1975 assert!(
1976 exists_relation(
1977 Relation::<TestUser>::new()
1978 .r#where(HashMap::from([("name".to_owned(), json!("Bob"))])),
1979 )
1980 .expect("exists should succeed"),
1981 );
1982 });
1983 }
1984
1985 #[test]
1986 fn relation_exists_returns_false_for_empty_scope() {
1987 run_seeded_relation_test(|| {
1988 assert!(
1989 !exists_relation(
1990 Relation::<TestUser>::new()
1991 .r#where(HashMap::from([("name".to_owned(), json!("Nobody"))])),
1992 )
1993 .expect("exists should succeed"),
1994 );
1995 });
1996 }
1997
1998 #[test]
1999 fn relation_explain_returns_non_empty_plan_for_simple_query() {
2000 run_seeded_relation_test(|| {
2001 let plan =
2002 explain_relation(Relation::<TestUser>::new()).expect("explain should succeed");
2003
2004 assert!(!plan.trim().is_empty());
2005 });
2006 }
2007
2008 #[test]
2009 fn relation_explain_returns_non_empty_plan_for_filtered_query() {
2010 run_seeded_relation_test(|| {
2011 let plan = explain_relation(
2012 Relation::<TestUser>::new()
2013 .r#where(HashMap::from([("name".to_owned(), json!("Bob"))])),
2014 )
2015 .expect("explain should succeed");
2016
2017 assert!(!plan.trim().is_empty());
2018 });
2019 }
2020
2021 #[test]
2022 fn relation_aggregation_sum_returns_total_for_id_column() {
2023 run_seeded_relation_test(|| {
2024 assert_eq!(
2025 sum_relation(Relation::<TestUser>::new(), "id").expect("sum should succeed"),
2026 6.0,
2027 );
2028 });
2029 }
2030
2031 #[test]
2032 fn relation_aggregation_average_returns_mean_for_id_column() {
2033 run_seeded_relation_test(|| {
2034 assert_eq!(
2035 average_relation(Relation::<TestUser>::new(), "id")
2036 .expect("average should succeed"),
2037 2.0,
2038 );
2039 });
2040 }
2041
2042 #[test]
2043 fn relation_aggregation_minimum_returns_first_id() {
2044 run_seeded_relation_test(|| {
2045 assert_eq!(
2046 minimum_relation(
2047 Relation::<TestUser>::new().order("id", OrderDirection::Asc),
2048 "id"
2049 )
2050 .expect("minimum should succeed"),
2051 Some(json!(1)),
2052 );
2053 });
2054 }
2055
2056 #[test]
2057 fn relation_aggregation_maximum_returns_last_id() {
2058 run_seeded_relation_test(|| {
2059 assert_eq!(
2060 maximum_relation(
2061 Relation::<TestUser>::new().order("id", OrderDirection::Asc),
2062 "id"
2063 )
2064 .expect("maximum should succeed"),
2065 Some(json!(3)),
2066 );
2067 });
2068 }
2069
2070 #[test]
2071 fn relation_aggregation_pluck_returns_names() {
2072 run_seeded_relation_test(|| {
2073 assert_eq!(
2074 pluck_relation(
2075 Relation::<TestUser>::new().order("id", OrderDirection::Asc),
2076 "name"
2077 )
2078 .expect("pluck should succeed"),
2079 vec![json!("Alice"), json!("Bob"), json!("Carol")],
2080 );
2081 });
2082 }
2083
2084 #[test]
2085 fn relation_aggregation_pick_returns_first_name() {
2086 run_seeded_relation_test(|| {
2087 assert_eq!(
2088 pick_relation(
2089 Relation::<TestUser>::new().order("id", OrderDirection::Asc),
2090 "name"
2091 )
2092 .expect("pick should succeed"),
2093 Some(json!("Alice")),
2094 );
2095 });
2096 }
2097
2098 #[test]
2099 fn relation_aggregation_ids_returns_primary_keys() {
2100 run_seeded_relation_test(|| {
2101 assert_eq!(
2102 relation_ids(Relation::<TestUser>::new().order("id", OrderDirection::Asc))
2103 .expect("ids should succeed"),
2104 vec![1, 2, 3],
2105 );
2106 });
2107 }
2108
2109 #[test]
2110 fn relation_sole_returns_matching_row_for_single_result_scope() {
2111 run_seeded_relation_test(|| {
2112 let user = sole_relation(
2113 Relation::<TestUser>::new()
2114 .r#where(HashMap::from([("name".to_owned(), json!("Alice"))])),
2115 )
2116 .expect("sole should succeed");
2117
2118 assert_eq!(user.email, "alice@example.com");
2119 });
2120 }
2121
2122 #[test]
2123 fn relation_sole_returns_not_found_for_empty_scope() {
2124 run_seeded_relation_test(|| {
2125 let error = sole_relation(
2126 Relation::<TestUser>::new()
2127 .r#where(HashMap::from([("name".to_owned(), json!("Nobody"))])),
2128 )
2129 .expect_err("empty scopes should fail");
2130
2131 assert!(matches!(error, crate::RecordError::NotFound));
2132 });
2133 }
2134
2135 #[test]
2136 fn relation_sole_returns_exceeded_for_multiple_rows() {
2137 run_seeded_relation_test(|| {
2138 let error = sole_relation(Relation::<TestUser>::new())
2139 .expect_err("multiple rows should fail sole");
2140
2141 assert!(matches!(error, crate::RecordError::SoleRecordExceeded));
2142 });
2143 }
2144
2145 #[test]
2146 fn relation_pluck_columns_returns_requested_values() {
2147 run_seeded_relation_test(|| {
2148 assert_eq!(
2149 pluck_columns_relation(
2150 Relation::<TestUser>::new().order("id", OrderDirection::Asc),
2151 &["id", "name"],
2152 )
2153 .expect("pluck_columns should succeed"),
2154 vec![
2155 vec![json!(1), json!("Alice")],
2156 vec![json!(2), json!("Bob")],
2157 vec![json!(3), json!("Carol")],
2158 ],
2159 );
2160 });
2161 }
2162
2163 #[test]
2164 fn relation_pluck_columns_returns_empty_for_empty_relation() {
2165 run_empty_relation_test(|| {
2166 assert!(
2167 pluck_columns_relation(Relation::<TestUser>::new(), &["id", "name"])
2168 .expect("pluck_columns should succeed")
2169 .is_empty()
2170 );
2171 });
2172 }
2173
2174 #[test]
2175 fn relation_select_columns_alias_populates_requested_columns() {
2176 run_seeded_relation_test(|| {
2177 let users = load_relation(
2178 Relation::<TestUser>::new()
2179 .select_columns(&["name"])
2180 .order("id", OrderDirection::Asc),
2181 )
2182 .expect("relation should load");
2183
2184 assert_eq!(users.len(), 3);
2185 assert_eq!(users[0].name, "Alice");
2186 assert_eq!(users[0].state, RecordState::Persisted);
2187 });
2188 }
2189
2190 #[test]
2191 fn relation_pluck_returns_default_for_unselected_column() {
2192 run_seeded_relation_test(|| {
2193 assert_eq!(
2194 pluck_relation(
2195 Relation::<TestUser>::new()
2196 .select_columns(&["name"])
2197 .order("id", OrderDirection::Asc),
2198 "id",
2199 )
2200 .expect("pluck should succeed"),
2201 vec![json!(0), json!(0), json!(0)],
2202 );
2203 });
2204 }
2205
2206 #[test]
2207 fn relation_pick_returns_default_for_unselected_column() {
2208 run_seeded_relation_test(|| {
2209 assert_eq!(
2210 pick_relation(
2211 Relation::<TestUser>::new()
2212 .select_columns(&["name"])
2213 .order("id", OrderDirection::Asc),
2214 "id",
2215 )
2216 .expect("pick should succeed"),
2217 Some(json!(0)),
2218 );
2219 });
2220 }
2221
2222 #[test]
2223 fn relation_group_count_groups_rows_by_key() {
2224 run_seeded_relation_test(|| {
2225 insert_user("Bob", "bobby@example.com");
2226
2227 let grouped = group_count_relation(Relation::<TestUser>::new().group("name"))
2228 .expect("group_count should succeed");
2229
2230 assert_eq!(grouped.get(&json!("Alice")), Some(&json!(1)));
2231 assert_eq!(grouped.get(&json!("Bob")), Some(&json!(2)));
2232 assert_eq!(grouped.get(&json!("Carol")), Some(&json!(1)));
2233 });
2234 }
2235
2236 #[test]
2237 fn relation_group_count_returns_empty_map_for_empty_scope() {
2238 run_seeded_relation_test(|| {
2239 let grouped = group_count_relation(
2240 Relation::<TestUser>::new()
2241 .group("name")
2242 .r#where(HashMap::from([("name".to_owned(), json!("Nobody"))])),
2243 )
2244 .expect("group_count should succeed");
2245
2246 assert!(grouped.is_empty());
2247 });
2248 }
2249
2250 #[test]
2251 fn relation_group_count_applies_having_filters() {
2252 run_seeded_relation_test(|| {
2253 insert_user("Bob", "bobby@example.com");
2254
2255 let grouped = group_count_relation(
2256 Relation::<TestUser>::new()
2257 .group("name")
2258 .having("COUNT(*) > 1"),
2259 )
2260 .expect("group_count should succeed");
2261
2262 assert_eq!(grouped.len(), 1);
2263 assert_eq!(grouped.get(&json!("Bob")), Some(&json!(2)));
2264 });
2265 }
2266
2267 #[test]
2268 fn relation_group_sum_returns_grouped_totals() {
2269 run_seeded_relation_test(|| {
2270 insert_user("Bob", "bobby@example.com");
2271
2272 let grouped = group_sum_relation(Relation::<TestUser>::new().group("name"), "id")
2273 .expect("group_sum should succeed");
2274
2275 assert_eq!(grouped.get(&json!("Alice")), Some(&json!(1)));
2276 assert_eq!(grouped.get(&json!("Bob")), Some(&json!(6)));
2277 assert_eq!(grouped.get(&json!("Carol")), Some(&json!(3)));
2278 });
2279 }
2280
2281 #[test]
2282 fn relation_group_average_returns_grouped_means() {
2283 run_seeded_relation_test(|| {
2284 insert_user("Bob", "bobby@example.com");
2285
2286 let grouped = group_average_relation(Relation::<TestUser>::new().group("name"), "id")
2287 .expect("group_average should succeed");
2288
2289 assert_eq!(grouped.get(&json!("Alice")), Some(&json!(1.0)));
2290 assert_eq!(grouped.get(&json!("Bob")), Some(&json!(3.0)));
2291 assert_eq!(grouped.get(&json!("Carol")), Some(&json!(3.0)));
2292 });
2293 }
2294
2295 #[test]
2296 fn relation_group_minimum_returns_grouped_extremes() {
2297 run_seeded_relation_test(|| {
2298 insert_user("Bob", "bobby@example.com");
2299
2300 let grouped = group_minimum_relation(Relation::<TestUser>::new().group("name"), "id")
2301 .expect("group_minimum should succeed");
2302
2303 assert_eq!(grouped.get(&json!("Alice")), Some(&json!(1)));
2304 assert_eq!(grouped.get(&json!("Bob")), Some(&json!(2)));
2305 assert_eq!(grouped.get(&json!("Carol")), Some(&json!(3)));
2306 });
2307 }
2308
2309 #[test]
2310 fn relation_group_maximum_returns_grouped_extremes() {
2311 run_seeded_relation_test(|| {
2312 insert_user("Bob", "bobby@example.com");
2313
2314 let grouped = group_maximum_relation(Relation::<TestUser>::new().group("name"), "id")
2315 .expect("group_maximum should succeed");
2316
2317 assert_eq!(grouped.get(&json!("Alice")), Some(&json!(1)));
2318 assert_eq!(grouped.get(&json!("Bob")), Some(&json!(4)));
2319 assert_eq!(grouped.get(&json!("Carol")), Some(&json!(3)));
2320 });
2321 }
2322
2323 #[test]
2324 fn relation_rewhere_replaces_existing_scope() {
2325 run_seeded_relation_test(|| {
2326 assert_eq!(
2327 relation_names(
2328 Relation::<TestUser>::new()
2329 .r#where(HashMap::from([("name".to_owned(), json!("Alice"))]))
2330 .rewhere(HashMap::from([("name".to_owned(), json!("Bob"))]))
2331 .order("id", OrderDirection::Asc),
2332 ),
2333 vec!["Bob"],
2334 );
2335 });
2336 }
2337
2338 #[test]
2339 fn relation_or_where_combines_scopes() {
2340 run_seeded_relation_test(|| {
2341 assert_eq!(
2342 relation_names(
2343 Relation::<TestUser>::new()
2344 .r#where(HashMap::from([("name".to_owned(), json!("Alice"))]))
2345 .or_where(HashMap::from([("name".to_owned(), json!("Bob"))]))
2346 .order("id", OrderDirection::Asc),
2347 ),
2348 vec!["Alice", "Bob"],
2349 );
2350 });
2351 }
2352
2353 #[test]
2354 fn relation_or_where_then_not_where_applies_negation_to_each_branch() {
2355 run_seeded_relation_test(|| {
2356 assert_eq!(
2357 relation_names(
2358 Relation::<TestUser>::new()
2359 .r#where(HashMap::from([("name".to_owned(), json!("Alice"))]))
2360 .or_where(HashMap::from([("name".to_owned(), json!("Bob"))]))
2361 .not_where(HashMap::from([(
2362 "email".to_owned(),
2363 json!("bob@example.com"),
2364 )]))
2365 .order("id", OrderDirection::Asc),
2366 ),
2367 vec!["Alice"],
2368 );
2369 });
2370 }
2371
2372 #[test]
2373 fn relation_not_where_excludes_matching_rows() {
2374 run_seeded_relation_test(|| {
2375 assert_eq!(
2376 relation_names(
2377 Relation::<TestUser>::new()
2378 .not_where(HashMap::from([("name".to_owned(), json!("Bob"))]))
2379 .order("id", OrderDirection::Asc),
2380 ),
2381 vec!["Alice", "Carol"],
2382 );
2383 });
2384 }
2385
2386 #[test]
2387 fn relation_distinct_count_counts_unique_selected_rows() {
2388 run_seeded_relation_test(|| {
2389 insert_user("Bob", "bobby@example.com");
2390
2391 assert_eq!(
2392 count_relation(
2393 Relation::<TestUser>::new()
2394 .select_columns(&["name"])
2395 .distinct(),
2396 )
2397 .expect("count should succeed"),
2398 3,
2399 );
2400 });
2401 }
2402
2403 #[test]
2404 fn relation_aggregation_empty_relation_returns_zero_sum_and_empty_pluck() {
2405 run_empty_relation_test(|| {
2406 assert_eq!(
2407 sum_relation(Relation::<TestUser>::new(), "id").expect("sum should succeed"),
2408 0.0,
2409 );
2410 assert!(
2411 pluck_relation(Relation::<TestUser>::new(), "name")
2412 .expect("pluck should succeed")
2413 .is_empty()
2414 );
2415 });
2416 }
2417
2418 #[test]
2419 fn default_relation_starts_empty_and_unscoped() {
2420 let relation = Relation::<TestUser>::default();
2421
2422 assert_eq!(relation.where_groups.len(), 1);
2423 assert!(relation.where_groups[0].is_empty());
2424 assert!(relation.order_by.is_empty());
2425 assert!(relation.group_columns.is_empty());
2426 assert!(relation.having_conditions.is_empty());
2427 assert!(!relation.is_distinct);
2428 assert!(relation.select_columns.is_empty());
2429 assert!(relation.join_associations.is_empty());
2430 assert!(relation.included_associations.is_empty());
2431 assert!(relation.limit_val.is_none());
2432 assert!(relation.offset_val.is_none());
2433 }
2434
2435 #[test]
2436 fn where_accumulates_conditions() {
2437 let relation = Relation::<TestUser>::new()
2438 .r#where(HashMap::from([("name".to_owned(), json!("Alice"))]))
2439 .r#where(HashMap::from([(
2440 "email".to_owned(),
2441 json!("alice@example.com"),
2442 )]));
2443
2444 assert_eq!(relation.where_groups.len(), 1);
2445 assert_eq!(relation.where_groups[0].positive.len(), 2);
2446 assert!(
2447 relation.where_groups[0]
2448 .positive
2449 .iter()
2450 .any(|(column, value)| column == "name" && value == &json!("Alice"))
2451 );
2452 assert!(
2453 relation.where_groups[0]
2454 .positive
2455 .iter()
2456 .any(|(column, value)| column == "email" && value == &json!("alice@example.com"))
2457 );
2458 }
2459
2460 #[test]
2461 fn not_accumulates_negated_conditions() {
2462 let relation = Relation::<TestUser>::new()
2463 .not(HashMap::from([("name".to_owned(), json!("Alice"))]))
2464 .not(HashMap::from([(
2465 "email".to_owned(),
2466 json!("alice@example.com"),
2467 )]));
2468
2469 assert_eq!(relation.where_groups.len(), 1);
2470 assert_eq!(relation.where_groups[0].negative.len(), 2);
2471 assert!(
2472 relation.where_groups[0]
2473 .negative
2474 .iter()
2475 .any(|(column, value)| column == "name" && value == &json!("Alice"))
2476 );
2477 assert!(
2478 relation.where_groups[0]
2479 .negative
2480 .iter()
2481 .any(|(column, value)| column == "email" && value == &json!("alice@example.com"))
2482 );
2483 }
2484}