Skip to main content

rustrails_record/
relation.rs

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/// A chainable query builder for [`Record`] types.
34#[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    /// Creates an empty relation.
87    pub fn new() -> Self {
88        Self::default()
89    }
90
91    /// Adds equality conditions to the relation.
92    pub fn r#where(mut self, conditions: HashMap<String, JsonValue>) -> Self {
93        self.add_positive_conditions(conditions);
94        self
95    }
96
97    /// Adds an order clause to the relation.
98    pub fn order(mut self, column: &str, dir: OrderDirection) -> Self {
99        self.order_by.push((column.to_owned(), dir));
100        self
101    }
102
103    /// Groups results by the specified column.
104    pub fn group(mut self, column: &str) -> Self {
105        self.group_columns.push(column.to_owned());
106        self
107    }
108
109    /// Adds a HAVING condition (used after group).
110    pub fn having(mut self, condition: &str) -> Self {
111        self.having_conditions.push(condition.to_owned());
112        self
113    }
114
115    /// Marks the query to return distinct results only.
116    pub fn distinct(mut self) -> Self {
117        self.is_distinct = true;
118        self
119    }
120
121    /// Restricts which columns are selected.
122    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    /// Backwards-compatible alias for [`Self::select_columns`].
129    pub fn select(self, columns: &[&str]) -> Self {
130        self.select_columns(columns)
131    }
132
133    /// Stores a join declaration (implementation pending full association support).
134    pub fn joins(mut self, association: &str) -> Self {
135        self.join_associations.push(association.to_owned());
136        self
137    }
138
139    /// Stores an eager-load declaration (implementation pending).
140    pub fn includes(mut self, association: &str) -> Self {
141        self.included_associations.push(association.to_owned());
142        self
143    }
144
145    /// Replaces the current ordering.
146    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    /// Replaces the current column selection.
153    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    /// Replaces the current where scope.
161    pub fn rewhere(mut self, conditions: HashMap<String, JsonValue>) -> Self {
162        self.where_groups = vec![ConditionGroup::from_positive(conditions)];
163        self
164    }
165
166    /// Limits the number of returned rows.
167    pub fn limit(mut self, n: u64) -> Self {
168        self.limit_val = Some(n);
169        self
170    }
171
172    /// Skips the first `n` rows.
173    pub fn offset(mut self, n: u64) -> Self {
174        self.offset_val = Some(n);
175        self
176    }
177
178    /// Adds negated equality conditions to the relation.
179    pub fn not_where(mut self, conditions: HashMap<String, JsonValue>) -> Self {
180        self.add_negative_conditions(conditions);
181        self
182    }
183
184    /// Backwards-compatible alias for [`Self::not_where`].
185    pub fn not(self, conditions: HashMap<String, JsonValue>) -> Self {
186        self.not_where(conditions)
187    }
188
189    /// Combines an additional where scope with `OR` semantics.
190    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    /// Executes the relation and returns all matching records.
233    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    /// Synchronous wrapper for [`Self::load`].
249    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    /// Iterates over records in batches, calling the closure for each record.
257    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    /// Synchronous wrapper for [`Self::find_each`].
276    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    /// Iterates over matching records in batches, calling the closure for each batch.
285    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    /// Synchronous wrapper for [`Self::find_in_batches`].
341    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    /// Executes the relation and returns the first matching record.
350    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    /// Synchronous wrapper for [`Self::first`].
363    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    /// Executes the relation and returns the last matching record within the current scope.
371    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    /// Synchronous wrapper for [`Self::last`].
380    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    /// Counts matching rows.
388    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    /// Synchronous wrapper for [`Self::count`].
401    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    /// Returns `true` when the relation matches at least one row.
410    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    /// Synchronous wrapper for [`Self::exists`].
418    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    /// Returns the only matching record.
426    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    /// Synchronous wrapper for [`Self::sole`].
439    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    /// Returns the database query plan as a string.
447    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    /// Synchronous wrapper for [`Self::explain`].
475    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    /// Returns the sum of the specified column.
483    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    /// Synchronous wrapper for [`Self::sum`].
497    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    /// Returns the average of the specified column.
506    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    /// Synchronous wrapper for [`Self::average`].
529    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    /// Returns the minimum value of the specified column.
538    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    /// Synchronous wrapper for [`Self::minimum`].
551    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    /// Returns the maximum value of the specified column.
560    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    /// Synchronous wrapper for [`Self::maximum`].
573    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    /// Returns grouped counts for the first configured group column.
582    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    /// Returns grouped sums for the first configured group column.
599    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    /// Returns grouped averages for the first configured group column.
617    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    /// Returns grouped minimum values for the first configured group column.
635    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    /// Returns grouped maximum values for the first configured group column.
658    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    /// Plucks the values of the specified columns into row vectors.
682    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    /// Synchronous wrapper for [`Self::pluck_columns`].
710    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    /// Plucks the values of the specified column into a [`Vec`].
719    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    /// Synchronous wrapper for [`Self::pluck`].
736    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    /// Returns the first value of the specified column.
745    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    /// Synchronous wrapper for [`Self::pick`].
762    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    /// Returns all primary key values.
771    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    /// Synchronous wrapper for [`Self::ids`].
784    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}