1use std::future::Future;
128
129pub use quokka_macros::{
130 repository_execute, repository_execute as execute, repository_query_all,
131 repository_query_all as query_all, repository_query_one, repository_query_one as query_one,
132 repository_query_optional, repository_query_optional as query_optional,
133};
134use sqlx::{query::Query, query_builder::Separated, QueryBuilder};
135
136use crate::{
137 state::{Database, FromState, ProvideState},
138 Error, Result,
139};
140
141#[derive(Clone, Debug)]
145pub struct PaginatedSearch {
146 database: Database,
147}
148
149#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
150pub struct PaginatedResult<E> {
151 pub page: u64,
152 pub total_pages: u64,
153 pub total_items: u64,
154 pub per_page: u64,
155 pub next_page: Option<u64>,
156 pub previous_page: Option<u64>,
157 pub items: Vec<E>,
158}
159
160pub trait SearchCriteria: Send {
161 fn extend_query_builder(&self, qb: &mut QueryBuilder<'_, sqlx::Postgres>);
162}
163
164pub struct AndCriteria {
166 pub criteria: Vec<Box<dyn SearchCriteria>>,
167}
168
169pub struct OrCriteria {
171 pub criteria: Vec<Box<dyn SearchCriteria>>,
172}
173
174pub struct EqCriteria {
176 pub column: &'static str,
177 pub value: Box<dyn Bindable<sqlx::Postgres>>,
178}
179
180pub struct NeCriteria {
182 pub column: &'static str,
183 pub value: Box<dyn Bindable<sqlx::Postgres>>,
184}
185
186pub struct LikeCriteria {
188 pub column: &'static str,
189 pub value: Box<dyn Bindable<sqlx::Postgres>>,
190}
191
192pub struct GtCriteria {
194 pub column: &'static str,
195 pub value: Box<dyn Bindable<sqlx::Postgres>>,
196}
197
198pub struct GteCriteria {
200 pub column: &'static str,
201 pub value: Box<dyn Bindable<sqlx::Postgres>>,
202}
203
204pub struct LtCriteria {
206 pub column: &'static str,
207 pub value: Box<dyn Bindable<sqlx::Postgres>>,
208}
209
210pub struct LteCriteria {
212 pub column: &'static str,
213 pub value: Box<dyn Bindable<sqlx::Postgres>>,
214}
215
216pub struct InCriteria {
218 pub column: &'static str,
219 pub values: Vec<Box<dyn Bindable<sqlx::Postgres>>>,
220}
221
222pub struct NotNullCriteria {
224 pub column: &'static str,
225}
226
227pub struct IsNullCriteria {
229 pub column: &'static str,
230}
231
232impl PaginatedSearch {
233 #[tracing::instrument(skip(self, search, pagination))]
237 pub async fn search<
238 E: for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>,
239 S: SearchCriteria + Send,
240 >(
241 &self,
242 table_name: &'static str,
243 search: impl Into<Option<S>>,
244 pagination: impl Into<Option<PaginationQuery>>,
245 ) -> Result<PaginatedResult<E>> {
246 let search: Option<S> = search.into();
247 let pagination: Option<PaginationQuery> = pagination.into();
248 let table_name = table_name
249 .chars()
250 .filter(|char| char.is_alphanumeric() || char.eq(&'_'))
251 .collect::<String>();
252
253 let mut data_query =
254 QueryBuilder::<'_, sqlx::Postgres>::new(format!("SELECT * FROM \"{table_name}\" "));
255
256 let mut count_query = QueryBuilder::<'_, sqlx::Postgres>::new(format!(
257 "SELECT COUNT(*) FROM \"{table_name}\" "
258 ));
259
260 if let Some(search) = search {
261 data_query.push(" WHERE ");
262 count_query.push(" WHERE ");
263
264 search.extend_query_builder(&mut data_query);
265 search.extend_query_builder(&mut count_query);
266 }
267
268 if let Some(order) = pagination.as_ref().and_then(|value| value.order.as_ref()) {
269 let order_column = order
270 .0
271 .chars()
272 .skip_while(|char| !char.is_alphabetic())
273 .filter(|char| char.is_alphanumeric() || '_'.eq(char))
274 .collect::<String>();
275
276 data_query.push(format!(" ORDER BY {} {} ", order_column, order.1));
277 }
278
279 if let Some(page_size) = pagination
280 .as_ref()
281 .and_then(|value| value.page_size.as_ref())
282 {
283 data_query.push(format!(" LIMIT {} ", page_size));
284 }
285
286 if let (Some(page), Some(page_size)) = (
287 pagination.as_ref().and_then(|value| value.page.as_ref()),
288 pagination
289 .as_ref()
290 .and_then(|value| value.page_size.as_ref()),
291 ) {
292 data_query.push(format!(" OFFSET {} ", page * page_size));
293 }
294
295 let data = async {
296 data_query
297 .build()
298 .fetch_all(self.database.ro())
299 .await
300 .map_err(Error::wrap_error("Unable to search for entity", 500))
301 };
302
303 let pagination_info = async {
304 count_query
305 .build()
306 .fetch_one(self.database.ro())
307 .await
308 .map_err(Error::wrap_error("Unable to count entities", 500))
309 };
310
311 let (data, pagination_info) = futures::future::try_join(data, pagination_info).await?;
312
313 let items = data
314 .iter()
315 .map(E::from_row)
316 .collect::<std::result::Result<Vec<_>, sqlx::Error>>()
317 .map_err(Error::wrap_error("Unable to map entity", 500))?;
318
319 use sqlx::Row;
320
321 let page = pagination
322 .as_ref()
323 .and_then(|value| value.page)
324 .unwrap_or_default();
325 let per_page = pagination
326 .as_ref()
327 .and_then(|value| value.page_size)
328 .unwrap_or_default();
329
330 let total_items = pagination_info
331 .try_get::<i64, _>(0)
332 .map_err(Error::wrap_error("Unable to get entity count", 500))?
333 as u64;
334
335 let total_pages = if per_page == 0 {
336 1
337 } else {
338 total_items.div_ceil(per_page)
339 };
340
341 Ok(PaginatedResult {
342 total_items,
343 total_pages,
344 next_page: if total_pages == 0 {
345 None
346 } else if page < (total_pages - 1) {
347 Some(page + 1)
348 } else {
349 None
350 },
351 previous_page: if page > 0 { Some(page - 1) } else { None },
352 page,
353 items,
354 per_page,
355 })
356 }
357}
358
359#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
360pub enum PaginationOrder {
361 #[default]
362 Ascending,
363 Descending,
364}
365
366#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
370pub struct PaginationQuery {
371 pub page: Option<u64>,
372 #[serde(alias = "per_page")]
373 pub page_size: Option<u64>,
374 #[serde(default)]
375 pub order: Option<(String, PaginationOrder)>,
376}
377
378pub trait BaseRepository {
384 type Entity: for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>
386 + serde::de::DeserializeOwned
387 + serde::Serialize;
388
389 type PkType: for<'r> sqlx::Type<sqlx::Postgres>;
391
392 fn get_entities(
403 &self,
404 page: i32,
405 page_size: i32,
406 order_by: &'static str,
407 direction: PaginationOrder,
408 ) -> impl Future<Output = Result<Vec<Self::Entity>>> + Send;
409
410 fn get_entity(&self, pk: Self::PkType) -> impl Future<Output = Result<Self::Entity>> + Send;
414
415 fn update_entity(&self, entity: Self::Entity) -> impl Future<Output = Result<u64>> + Send;
419
420 fn create_entity(
424 &self,
425 entity: Self::Entity,
426 ) -> impl Future<Output = Result<Self::Entity>> + Send;
427
428 fn delete_entity(&self, pk: Self::PkType) -> impl Future<Output = Result<u64>> + Send;
433}
434
435pub trait Bindable<DB: sqlx::Database>: Send + Sync {
437 fn bind_to_query_builder<'q>(&self, qb: &mut QueryBuilder<'q, DB>)
438 where
439 Self: 'q;
440
441 fn bind_to_query<'q>(
442 &self,
443 query: Query<'q, DB, sqlx::postgres::PgArguments>,
444 ) -> Query<'q, DB, sqlx::postgres::PgArguments>
445 where
446 Self: 'q;
447
448 fn bind_to_separated<'qb, 'args>(
449 &self,
450 separated: &mut Separated<'qb, 'args, DB, &'static str>,
451 ) where
452 Self: 'qb,
453 Self: 'args;
454}
455
456impl<
457 T: for<'e> sqlx::encode::Encode<'e, sqlx::Postgres>
458 + sqlx::Type<sqlx::Postgres>
459 + Send
460 + Sync
461 + Clone,
462 > Bindable<sqlx::Postgres> for T
463{
464 fn bind_to_query_builder<'q>(&self, qb: &mut QueryBuilder<'q, sqlx::Postgres>)
465 where
466 Self: 'q,
467 {
468 qb.push_bind(self.clone());
469 }
470
471 fn bind_to_query<'q>(
472 &self,
473 query: Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
474 ) -> Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>
475 where
476 Self: 'q,
477 {
478 query.bind(self.clone())
479 }
480
481 fn bind_to_separated<'qb, 'args>(
482 &self,
483 separated: &mut Separated<'qb, 'args, sqlx::Postgres, &'static str>,
484 ) where
485 Self: 'qb,
486 Self: 'args,
487 {
488 separated.push_bind(self.clone());
489 }
490}
491
492impl std::fmt::Display for PaginationOrder {
493 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
494 match self {
495 PaginationOrder::Ascending => f.write_str("ASC"),
496 PaginationOrder::Descending => f.write_str("DESC"),
497 }
498 }
499}
500
501impl<S> FromState<S> for PaginatedSearch
502where
503 S: ProvideState<Database>,
504{
505 fn from_state(state: &S) -> Self {
506 Self {
507 database: ProvideState::<Database>::provide(state),
508 }
509 }
510}
511
512impl Default for AndCriteria {
513 fn default() -> Self {
514 Self::new()
515 }
516}
517
518impl AndCriteria {
519 pub fn new() -> Self {
520 Self {
521 criteria: Vec::new(),
522 }
523 }
524
525 pub fn and(mut self, criteria: impl SearchCriteria + 'static) -> Self {
526 self.criteria.push(Box::new(criteria));
527
528 self
529 }
530}
531
532impl Default for OrCriteria {
533 fn default() -> Self {
534 Self::new()
535 }
536}
537
538impl OrCriteria {
539 pub fn new() -> Self {
540 Self {
541 criteria: Vec::new(),
542 }
543 }
544
545 pub fn or(mut self, criteria: impl SearchCriteria + 'static) -> Self {
546 self.criteria.push(Box::new(criteria));
547
548 self
549 }
550}
551
552impl EqCriteria {
553 pub fn new(column: &'static str, value: impl Bindable<sqlx::Postgres> + 'static) -> Self {
554 Self {
555 column,
556 value: Box::new(value),
557 }
558 }
559}
560
561impl NeCriteria {
562 pub fn new(column: &'static str, value: impl Bindable<sqlx::Postgres> + 'static) -> Self {
563 Self {
564 column,
565 value: Box::new(value),
566 }
567 }
568}
569
570impl LikeCriteria {
571 pub fn new(column: &'static str, value: impl Bindable<sqlx::Postgres> + 'static) -> Self {
572 Self {
573 column,
574 value: Box::new(value),
575 }
576 }
577}
578
579impl GtCriteria {
580 pub fn new(column: &'static str, value: impl Bindable<sqlx::Postgres> + 'static) -> Self {
581 Self {
582 column,
583 value: Box::new(value),
584 }
585 }
586}
587
588impl GteCriteria {
589 pub fn new(column: &'static str, value: impl Bindable<sqlx::Postgres> + 'static) -> Self {
590 Self {
591 column,
592 value: Box::new(value),
593 }
594 }
595}
596
597impl LtCriteria {
598 pub fn new(column: &'static str, value: impl Bindable<sqlx::Postgres> + 'static) -> Self {
599 Self {
600 column,
601 value: Box::new(value),
602 }
603 }
604}
605
606impl LteCriteria {
607 pub fn new(column: &'static str, value: impl Bindable<sqlx::Postgres> + 'static) -> Self {
608 Self {
609 column,
610 value: Box::new(value),
611 }
612 }
613}
614
615impl NotNullCriteria {
616 pub fn new(column: &'static str) -> Self {
617 Self { column }
618 }
619}
620
621impl IsNullCriteria {
622 pub fn new(column: &'static str) -> Self {
623 Self { column }
624 }
625}
626
627impl InCriteria {
628 pub fn new<I: Bindable<sqlx::Postgres> + 'static>(
629 column: &'static str,
630 values: impl IntoIterator<Item = I>,
631 ) -> Self {
632 Self {
633 column,
634 values: values
635 .into_iter()
636 .map(|value| Box::new(value) as Box<dyn Bindable<sqlx::Postgres>>)
637 .collect::<Vec<_>>(),
638 }
639 }
640}
641
642impl SearchCriteria for AndCriteria {
643 fn extend_query_builder(&self, qb: &mut QueryBuilder<'_, sqlx::Postgres>) {
644 let mut peekable = self.criteria.iter().peekable();
645 qb.push(" ( ");
646 while let Some(criteria) = peekable.next() {
647 criteria.extend_query_builder(qb);
648
649 if peekable.peek().is_some() {
650 qb.push(" AND ");
651 }
652 }
653 qb.push(" ) ");
654 }
655}
656
657impl SearchCriteria for OrCriteria {
658 fn extend_query_builder(&self, qb: &mut QueryBuilder<'_, sqlx::Postgres>) {
659 let mut peekable = self.criteria.iter().peekable();
660 qb.push(" ( ");
661 while let Some(criteria) = peekable.next() {
662 criteria.extend_query_builder(qb);
663
664 if peekable.peek().is_some() {
665 qb.push(" OR ");
666 }
667 }
668 qb.push(" ) ");
669 }
670}
671
672impl SearchCriteria for EqCriteria {
673 fn extend_query_builder(&self, qb: &mut QueryBuilder<'_, sqlx::Postgres>) {
674 self.value
675 .bind_to_query_builder(qb.push(self.column).push(" = "));
676 }
677}
678
679impl SearchCriteria for NeCriteria {
680 fn extend_query_builder(&self, qb: &mut QueryBuilder<'_, sqlx::Postgres>) {
681 self.value
682 .bind_to_query_builder(qb.push(self.column).push(" != "));
683 }
684}
685
686impl SearchCriteria for LikeCriteria {
687 fn extend_query_builder(&self, qb: &mut QueryBuilder<'_, sqlx::Postgres>) {
688 self.value
689 .bind_to_query_builder(qb.push(self.column).push(" LIKE "));
690 }
691}
692
693impl SearchCriteria for GtCriteria {
694 fn extend_query_builder(&self, qb: &mut QueryBuilder<'_, sqlx::Postgres>) {
695 self.value
696 .bind_to_query_builder(qb.push(self.column).push(" > "));
697 }
698}
699
700impl SearchCriteria for GteCriteria {
701 fn extend_query_builder(&self, qb: &mut QueryBuilder<'_, sqlx::Postgres>) {
702 self.value
703 .bind_to_query_builder(qb.push(self.column).push(" >= "));
704 }
705}
706
707impl SearchCriteria for LtCriteria {
708 fn extend_query_builder(&self, qb: &mut QueryBuilder<'_, sqlx::Postgres>) {
709 self.value
710 .bind_to_query_builder(qb.push(self.column).push(" < "));
711 }
712}
713
714impl SearchCriteria for LteCriteria {
715 fn extend_query_builder(&self, qb: &mut QueryBuilder<'_, sqlx::Postgres>) {
716 self.value
717 .bind_to_query_builder(qb.push(self.column).push(" <= "));
718 }
719}
720
721impl SearchCriteria for NotNullCriteria {
722 fn extend_query_builder(&self, qb: &mut QueryBuilder<'_, sqlx::Postgres>) {
723 qb.push(self.column).push(" IS NOT NULL ");
724 }
725}
726
727impl SearchCriteria for IsNullCriteria {
728 fn extend_query_builder(&self, qb: &mut QueryBuilder<'_, sqlx::Postgres>) {
729 qb.push(self.column).push(" IS NULL ");
730 }
731}
732
733impl SearchCriteria for InCriteria {
734 fn extend_query_builder(&self, qb: &mut QueryBuilder<'_, sqlx::Postgres>) {
735 qb.push(self.column)
736 .push(" IN ")
737 .push_tuples(&self.values, |mut builder, value| {
738 value.bind_to_separated(&mut builder);
739 });
740 }
741}
742
743impl ProvideState<Database> for sqlx::PgPool {
744 fn provide(&self) -> Database {
745 Database::from(self.clone())
746 }
747}
748
749#[cfg(test)]
750mod tests {
751 use sqlx::PgPool;
752
753 use crate::{
754 helper::database::{
755 AndCriteria, EqCriteria, GtCriteria, GteCriteria, InCriteria, IsNullCriteria,
756 LikeCriteria, LtCriteria, LteCriteria, NeCriteria, NotNullCriteria, OrCriteria,
757 PaginatedSearch, PaginationQuery,
758 },
759 state::FromState,
760 Result,
761 };
762
763 #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, sqlx::FromRow)]
764 pub struct Article {
765 pub id: i32,
766 pub created_at: time::OffsetDateTime,
767 pub updated_at: time::OffsetDateTime,
768 pub title: String,
769 pub content: String,
770 }
771
772 #[sqlx::test(
773 fixtures(path = "../../fixtures", scripts("articles")),
774 migrations = "./migrations_test"
775 )]
776 async fn test_plain_search(db: PgPool) -> Result<()> {
777 let search = PaginatedSearch::from_state(&db);
778 let articles = search
779 .search::<Article, EqCriteria>("article", None, None)
780 .await?;
781
782 assert_eq!(articles.items.len(), 5);
783 assert_eq!(articles.page, 0);
784 assert_eq!(articles.total_pages, 1);
785 assert_eq!(articles.next_page, None);
786 assert_eq!(articles.previous_page, None);
787
788 Ok(())
789 }
790
791 #[sqlx::test(
792 fixtures(path = "../../fixtures", scripts("articles")),
793 migrations = "./migrations_test"
794 )]
795 async fn test_search_by_id(db: PgPool) -> Result<()> {
796 let search = PaginatedSearch::from_state(&db);
797 let articles = search
798 .search::<Article, EqCriteria>("article", Some(EqCriteria::new("id", 1)), None)
799 .await?;
800
801 assert_eq!(articles.items.len(), 1, "Wrong size");
802 assert_eq!(articles.items.first().unwrap().id, 1, "Wrong id");
803 assert_eq!(articles.page, 0, "Wrong page");
804 assert_eq!(articles.total_pages, 1, "Wrong total pages");
805 assert_eq!(articles.next_page, None, "Wrong next page");
806 assert_eq!(articles.previous_page, None, "Wrong previous page");
807
808 Ok(())
809 }
810
811 #[sqlx::test(
812 fixtures(path = "../../fixtures", scripts("articles")),
813 migrations = "./migrations_test"
814 )]
815 async fn test_search_paged(db: PgPool) -> Result<()> {
816 let search = PaginatedSearch::from_state(&db);
817 let articles = search
818 .search::<Article, EqCriteria>(
819 "article",
820 None,
821 Some(PaginationQuery {
822 page: 2.into(),
823 page_size: 1.into(),
824 ..Default::default()
825 }),
826 )
827 .await?;
828
829 assert_eq!(articles.items.len(), 1);
830 assert_eq!(articles.page, 2);
831 assert_eq!(articles.total_pages, 5);
832 assert_eq!(articles.next_page, Some(3));
833 assert_eq!(articles.previous_page, Some(1));
834
835 Ok(())
836 }
837
838 #[sqlx::test(
839 fixtures(path = "../../fixtures", scripts("articles")),
840 migrations = "./migrations_test"
841 )]
842 async fn test_search_page_min(db: PgPool) -> Result<()> {
843 let search = PaginatedSearch::from_state(&db);
844 let articles = search
845 .search::<Article, EqCriteria>(
846 "article",
847 None,
848 Some(PaginationQuery {
849 page: 0.into(),
850 page_size: 1.into(),
851 ..Default::default()
852 }),
853 )
854 .await?;
855
856 assert_eq!(articles.items.len(), 1);
857 assert_eq!(articles.page, 0);
858 assert_eq!(articles.total_pages, 5);
859 assert_eq!(articles.next_page, Some(1));
860 assert_eq!(articles.previous_page, None);
861
862 Ok(())
863 }
864
865 #[sqlx::test(
866 fixtures(path = "../../fixtures", scripts("articles")),
867 migrations = "./migrations_test"
868 )]
869 async fn test_search_page_max(db: PgPool) -> Result<()> {
870 let search = PaginatedSearch::from_state(&db);
871 let articles = search
872 .search::<Article, EqCriteria>(
873 "article",
874 None,
875 Some(PaginationQuery {
876 page: 4.into(),
877 page_size: 1.into(),
878 ..Default::default()
879 }),
880 )
881 .await?;
882
883 assert_eq!(articles.items.len(), 1);
884 assert_eq!(articles.page, 4);
885 assert_eq!(articles.total_pages, 5);
886 assert_eq!(articles.next_page, None);
887 assert_eq!(articles.previous_page, Some(3));
888
889 Ok(())
890 }
891
892 #[sqlx::test(
893 fixtures(path = "../../fixtures", scripts("articles")),
894 migrations = "./migrations_test"
895 )]
896 async fn test_eq_criteria(db: PgPool) -> Result<()> {
897 let search = PaginatedSearch::from_state(&db);
898 let entities = search
899 .search::<Article, _>("article", EqCriteria::new("id", 3), None)
900 .await?;
901
902 assert_eq!(entities.items.len(), 1);
903 assert_eq!(entities.items.first().unwrap().id, 3);
904
905 Ok(())
906 }
907
908 #[sqlx::test(
909 fixtures(path = "../../fixtures", scripts("articles")),
910 migrations = "./migrations_test"
911 )]
912 async fn test_ne_criteria(db: PgPool) -> Result<()> {
913 let search = PaginatedSearch::from_state(&db);
914 let entities = search
915 .search::<Article, _>("article", NeCriteria::new("id", 3), None)
916 .await?;
917
918 assert_eq!(entities.items.len(), 4);
919 assert_ne!(entities.items.first().unwrap().id, 3);
920
921 Ok(())
922 }
923
924 #[sqlx::test(
925 fixtures(path = "../../fixtures", scripts("articles")),
926 migrations = "./migrations_test"
927 )]
928 async fn test_gt_criteria(db: PgPool) -> Result<()> {
929 let search = PaginatedSearch::from_state(&db);
930 let entities = search
931 .search::<Article, _>("article", GtCriteria::new("id", 3), None)
932 .await?;
933
934 assert_eq!(
935 entities
936 .items
937 .into_iter()
938 .map(|item| item.id)
939 .collect::<Vec<_>>()
940 .as_slice(),
941 &[4, 5]
942 );
943
944 Ok(())
945 }
946
947 #[sqlx::test(
948 fixtures(path = "../../fixtures", scripts("articles")),
949 migrations = "./migrations_test"
950 )]
951 async fn test_gte_criteria(db: PgPool) -> Result<()> {
952 let search = PaginatedSearch::from_state(&db);
953 let entities = search
954 .search::<Article, _>("article", GteCriteria::new("id", 3), None)
955 .await?;
956
957 assert_eq!(
958 entities
959 .items
960 .into_iter()
961 .map(|item| item.id)
962 .collect::<Vec<_>>()
963 .as_slice(),
964 &[3, 4, 5]
965 );
966
967 Ok(())
968 }
969
970 #[sqlx::test(
971 fixtures(path = "../../fixtures", scripts("articles")),
972 migrations = "./migrations_test"
973 )]
974 async fn test_lt_criteria(db: PgPool) -> Result<()> {
975 let search = PaginatedSearch::from_state(&db);
976 let entities = search
977 .search::<Article, _>("article", LtCriteria::new("id", 3), None)
978 .await?;
979
980 assert_eq!(
981 entities
982 .items
983 .into_iter()
984 .map(|item| item.id)
985 .collect::<Vec<_>>()
986 .as_slice(),
987 &[1, 2]
988 );
989
990 Ok(())
991 }
992
993 #[sqlx::test(
994 fixtures(path = "../../fixtures", scripts("articles")),
995 migrations = "./migrations_test"
996 )]
997 async fn test_lte_criteria(db: PgPool) -> Result<()> {
998 let search = PaginatedSearch::from_state(&db);
999 let entities = search
1000 .search::<Article, _>("article", LteCriteria::new("id", 3), None)
1001 .await?;
1002
1003 assert_eq!(
1004 entities
1005 .items
1006 .into_iter()
1007 .map(|item| item.id)
1008 .collect::<Vec<_>>()
1009 .as_slice(),
1010 &[1, 2, 3]
1011 );
1012
1013 Ok(())
1014 }
1015
1016 #[sqlx::test(
1017 fixtures(path = "../../fixtures", scripts("articles")),
1018 migrations = "./migrations_test"
1019 )]
1020 async fn test_like_criteria(db: PgPool) -> Result<()> {
1021 let search = PaginatedSearch::from_state(&db);
1022 let entities = search
1023 .search::<Article, _>("article", LikeCriteria::new("title", "% Article 2"), None)
1024 .await?;
1025
1026 assert_eq!(entities.items.first().unwrap().title, "Test Article 2");
1027
1028 Ok(())
1029 }
1030
1031 #[sqlx::test(
1032 fixtures(path = "../../fixtures", scripts("articles")),
1033 migrations = "./migrations_test"
1034 )]
1035 async fn test_not_null_criteria(db: PgPool) -> Result<()> {
1036 let search = PaginatedSearch::from_state(&db);
1037 let entities = search
1038 .search::<Article, _>("article", NotNullCriteria::new("title"), None)
1039 .await?;
1040
1041 assert_eq!(entities.items.len(), 5);
1042
1043 Ok(())
1044 }
1045
1046 #[sqlx::test(
1047 fixtures(path = "../../fixtures", scripts("articles")),
1048 migrations = "./migrations_test"
1049 )]
1050 async fn test_is_null_criteria(db: PgPool) -> Result<()> {
1051 let search = PaginatedSearch::from_state(&db);
1052 let entities = search
1053 .search::<Article, _>("article", IsNullCriteria::new("title"), None)
1054 .await?;
1055
1056 assert_eq!(entities.items.len(), 0);
1057
1058 Ok(())
1059 }
1060
1061 #[sqlx::test(
1062 fixtures(path = "../../fixtures", scripts("articles")),
1063 migrations = "./migrations_test"
1064 )]
1065 async fn test_in_criteria(db: PgPool) -> Result<()> {
1066 let search = PaginatedSearch::from_state(&db);
1067 let entities = search
1068 .search::<Article, _>("article", InCriteria::new("id", [1, 3, 5]), None)
1069 .await?;
1070
1071 assert_eq!(
1072 entities
1073 .items
1074 .into_iter()
1075 .map(|item| item.id)
1076 .collect::<Vec<_>>()
1077 .as_slice(),
1078 &[1, 3, 5]
1079 );
1080
1081 Ok(())
1082 }
1083
1084 #[sqlx::test(
1085 fixtures(path = "../../fixtures", scripts("articles")),
1086 migrations = "./migrations_test"
1087 )]
1088 async fn test_and_criteria(db: PgPool) -> Result<()> {
1089 let search = PaginatedSearch::from_state(&db);
1090 let entities = search
1091 .search::<Article, _>(
1092 "article",
1093 AndCriteria::new()
1094 .and(LikeCriteria::new("title", "%Article%"))
1095 .and(LikeCriteria::new("title", "%3")),
1096 None,
1097 )
1098 .await?;
1099
1100 assert_eq!(
1101 entities
1102 .items
1103 .into_iter()
1104 .map(|item| item.id)
1105 .collect::<Vec<_>>()
1106 .as_slice(),
1107 &[3]
1108 );
1109
1110 Ok(())
1111 }
1112
1113 #[sqlx::test(
1114 fixtures(path = "../../fixtures", scripts("articles")),
1115 migrations = "./migrations_test"
1116 )]
1117 async fn test_or_criteria(db: PgPool) -> Result<()> {
1118 let search = PaginatedSearch::from_state(&db);
1119 let entities = search
1120 .search::<Article, _>(
1121 "article",
1122 OrCriteria::new()
1123 .or(EqCriteria::new("id", 3))
1124 .or(EqCriteria::new("id", 4)),
1125 None,
1126 )
1127 .await?;
1128
1129 assert_eq!(
1130 entities
1131 .items
1132 .into_iter()
1133 .map(|item| item.id)
1134 .collect::<Vec<_>>()
1135 .as_slice(),
1136 &[3, 4]
1137 );
1138
1139 Ok(())
1140 }
1141}