1use std::marker::PhantomData;
4
5use crate::error::QueryResult;
6use crate::filter::Filter;
7use crate::pagination::Pagination;
8use crate::traits::{MaterializedView, QueryEngine, View};
9use crate::types::{OrderBy, Select};
10
11#[allow(dead_code)]
28pub struct ViewFindManyOperation<E: QueryEngine, V: View> {
29 engine: E,
30 filter: Filter,
31 order_by: OrderBy,
32 pagination: Pagination,
33 select: Select,
34 distinct: Option<Vec<String>>,
35 _view: PhantomData<V>,
36}
37
38impl<E: QueryEngine, V: View> ViewFindManyOperation<E, V> {
39 pub fn new(engine: E) -> Self {
41 Self {
42 engine,
43 filter: Filter::None,
44 order_by: OrderBy::none(),
45 pagination: Pagination::new(),
46 select: Select::All,
47 distinct: None,
48 _view: PhantomData,
49 }
50 }
51
52 pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
54 let new_filter = filter.into();
55 self.filter = self.filter.and_then(new_filter);
56 self
57 }
58
59 pub fn order_by(mut self, order: impl Into<OrderBy>) -> Self {
61 self.order_by = order.into();
62 self
63 }
64
65 pub fn skip(mut self, n: u64) -> Self {
67 self.pagination = self.pagination.skip(n);
68 self
69 }
70
71 pub fn take(mut self, n: u64) -> Self {
73 self.pagination = self.pagination.take(n);
74 self
75 }
76
77 pub fn select(mut self, select: impl Into<Select>) -> Self {
79 self.select = select.into();
80 self
81 }
82
83 pub fn distinct(mut self, columns: impl IntoIterator<Item = impl Into<String>>) -> Self {
85 self.distinct = Some(columns.into_iter().map(Into::into).collect());
86 self
87 }
88
89 pub fn cursor(mut self, cursor: crate::pagination::Cursor) -> Self {
91 self.pagination = self.pagination.cursor(cursor);
92 self
93 }
94
95 pub fn build_sql(&self) -> (String, Vec<crate::filter::FilterValue>) {
97 let (where_sql, params) = self.filter.to_sql(0);
98
99 let mut sql = String::new();
100
101 sql.push_str("SELECT ");
103 if let Some(ref cols) = self.distinct {
104 sql.push_str("DISTINCT ON (");
105 sql.push_str(&cols.join(", "));
106 sql.push_str(") ");
107 }
108 sql.push_str(&self.select.to_sql());
109
110 sql.push_str(" FROM ");
112 sql.push_str(V::DB_VIEW_NAME);
113
114 if !self.filter.is_none() {
116 sql.push_str(" WHERE ");
117 sql.push_str(&where_sql);
118 }
119
120 if !self.order_by.is_empty() {
122 sql.push_str(" ORDER BY ");
123 sql.push_str(&self.order_by.to_sql());
124 }
125
126 let pagination_sql = self.pagination.to_sql();
128 if !pagination_sql.is_empty() {
129 sql.push(' ');
130 sql.push_str(&pagination_sql);
131 }
132
133 (sql, params)
134 }
135
136 pub async fn exec(self) -> QueryResult<Vec<V>>
141 where
142 V: Send + 'static + serde::de::DeserializeOwned,
143 {
144 let (sql, _params) = self.build_sql();
145 let _ = sql;
148 Ok(Vec::new()) }
150}
151
152pub struct ViewFindFirstOperation<E: QueryEngine, V: View> {
154 inner: ViewFindManyOperation<E, V>,
155}
156
157impl<E: QueryEngine, V: View> ViewFindFirstOperation<E, V> {
158 pub fn new(engine: E) -> Self {
160 Self {
161 inner: ViewFindManyOperation::new(engine).take(1),
162 }
163 }
164
165 pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
167 self.inner = self.inner.r#where(filter);
168 self
169 }
170
171 pub fn order_by(mut self, order: impl Into<OrderBy>) -> Self {
173 self.inner = self.inner.order_by(order);
174 self
175 }
176
177 pub fn build_sql(&self) -> (String, Vec<crate::filter::FilterValue>) {
179 self.inner.build_sql()
180 }
181
182 pub async fn exec(self) -> QueryResult<Option<V>>
184 where
185 V: Send + 'static + serde::de::DeserializeOwned,
186 {
187 let results = self.inner.exec().await?;
188 Ok(results.into_iter().next())
189 }
190}
191
192pub struct ViewCountOperation<E: QueryEngine, V: View> {
194 engine: E,
195 filter: Filter,
196 _view: PhantomData<V>,
197}
198
199impl<E: QueryEngine, V: View> ViewCountOperation<E, V> {
200 pub fn new(engine: E) -> Self {
202 Self {
203 engine,
204 filter: Filter::None,
205 _view: PhantomData,
206 }
207 }
208
209 pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
211 self.filter = self.filter.and_then(filter.into());
212 self
213 }
214
215 pub fn build_sql(&self) -> (String, Vec<crate::filter::FilterValue>) {
217 let (where_sql, params) = self.filter.to_sql(0);
218
219 let mut sql = format!("SELECT COUNT(*) FROM {}", V::DB_VIEW_NAME);
220
221 if !self.filter.is_none() {
222 sql.push_str(" WHERE ");
223 sql.push_str(&where_sql);
224 }
225
226 (sql, params)
227 }
228
229 pub async fn exec(self) -> QueryResult<u64> {
231 let (sql, params) = self.build_sql();
232 self.engine.count(&sql, params).await
233 }
234}
235
236pub struct RefreshMaterializedViewOperation<E: QueryEngine, V: MaterializedView> {
260 engine: E,
261 concurrently: bool,
262 _view: PhantomData<V>,
263}
264
265impl<E: QueryEngine, V: MaterializedView> RefreshMaterializedViewOperation<E, V> {
266 pub fn new(engine: E) -> Self {
268 Self {
269 engine,
270 concurrently: false,
271 _view: PhantomData,
272 }
273 }
274
275 pub fn concurrently(mut self) -> Self {
280 self.concurrently = V::SUPPORTS_CONCURRENT_REFRESH;
281 self
282 }
283
284 pub async fn exec(self) -> QueryResult<()> {
286 self.engine
287 .refresh_materialized_view(V::DB_VIEW_NAME, self.concurrently)
288 .await
289 }
290}
291
292pub struct ViewQueryBuilder<E: QueryEngine, V: View> {
297 engine: E,
298 _view: PhantomData<V>,
299}
300
301impl<E: QueryEngine, V: View> ViewQueryBuilder<E, V> {
302 pub fn new(engine: E) -> Self {
304 Self {
305 engine,
306 _view: PhantomData,
307 }
308 }
309
310 pub fn find_many(&self) -> ViewFindManyOperation<E, V> {
312 ViewFindManyOperation::new(self.engine.clone())
313 }
314
315 pub fn find_first(&self) -> ViewFindFirstOperation<E, V> {
317 ViewFindFirstOperation::new(self.engine.clone())
318 }
319
320 pub fn count(&self) -> ViewCountOperation<E, V> {
322 ViewCountOperation::new(self.engine.clone())
323 }
324}
325
326impl<E: QueryEngine, V: MaterializedView> ViewQueryBuilder<E, V> {
327 pub fn refresh(&self) -> RefreshMaterializedViewOperation<E, V> {
331 RefreshMaterializedViewOperation::new(self.engine.clone())
332 }
333}
334
335impl<E: QueryEngine, V: View> Clone for ViewQueryBuilder<E, V> {
336 fn clone(&self) -> Self {
337 Self {
338 engine: self.engine.clone(),
339 _view: PhantomData,
340 }
341 }
342}
343
344pub trait ViewAccessor<E: QueryEngine>: Send + Sync {
348 type View: View;
350
351 fn engine(&self) -> &E;
353
354 fn find_many(&self) -> ViewFindManyOperation<E, Self::View>;
356
357 fn find_first(&self) -> ViewFindFirstOperation<E, Self::View>;
359
360 fn count(&self) -> ViewCountOperation<E, Self::View>;
362}
363
364pub trait MaterializedViewAccessor<E: QueryEngine>: ViewAccessor<E>
366where
367 Self::View: MaterializedView,
368{
369 fn refresh(&self) -> RefreshMaterializedViewOperation<E, Self::View>;
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376 use crate::error::QueryError;
377 use crate::filter::FilterValue;
378 use crate::traits::BoxFuture;
379
380 struct TestView;
381
382 impl View for TestView {
383 const VIEW_NAME: &'static str = "TestView";
384 const DB_VIEW_NAME: &'static str = "test_view";
385 const COLUMNS: &'static [&'static str] = &["id", "user_id", "post_count"];
386 const IS_MATERIALIZED: bool = false;
387 }
388
389 struct TestMaterializedView;
390
391 impl View for TestMaterializedView {
392 const VIEW_NAME: &'static str = "TestMaterializedView";
393 const DB_VIEW_NAME: &'static str = "test_materialized_view";
394 const COLUMNS: &'static [&'static str] = &["id", "stats"];
395 const IS_MATERIALIZED: bool = true;
396 }
397
398 impl MaterializedView for TestMaterializedView {
399 const SUPPORTS_CONCURRENT_REFRESH: bool = true;
400 }
401
402 #[derive(Clone)]
403 struct MockEngine;
404
405 impl QueryEngine for MockEngine {
406 fn query_many<T: crate::traits::Model + Send + 'static>(
407 &self,
408 _sql: &str,
409 _params: Vec<FilterValue>,
410 ) -> BoxFuture<'_, QueryResult<Vec<T>>> {
411 Box::pin(async { Ok(Vec::new()) })
412 }
413
414 fn query_one<T: crate::traits::Model + Send + 'static>(
415 &self,
416 _sql: &str,
417 _params: Vec<FilterValue>,
418 ) -> BoxFuture<'_, QueryResult<T>> {
419 Box::pin(async { Err(QueryError::not_found("test")) })
420 }
421
422 fn query_optional<T: crate::traits::Model + Send + 'static>(
423 &self,
424 _sql: &str,
425 _params: Vec<FilterValue>,
426 ) -> BoxFuture<'_, QueryResult<Option<T>>> {
427 Box::pin(async { Ok(None) })
428 }
429
430 fn execute_insert<T: crate::traits::Model + Send + 'static>(
431 &self,
432 _sql: &str,
433 _params: Vec<FilterValue>,
434 ) -> BoxFuture<'_, QueryResult<T>> {
435 Box::pin(async { Err(QueryError::not_found("test")) })
436 }
437
438 fn execute_update<T: crate::traits::Model + Send + 'static>(
439 &self,
440 _sql: &str,
441 _params: Vec<FilterValue>,
442 ) -> BoxFuture<'_, QueryResult<Vec<T>>> {
443 Box::pin(async { Ok(Vec::new()) })
444 }
445
446 fn execute_delete(
447 &self,
448 _sql: &str,
449 _params: Vec<FilterValue>,
450 ) -> BoxFuture<'_, QueryResult<u64>> {
451 Box::pin(async { Ok(0) })
452 }
453
454 fn execute_raw(
455 &self,
456 _sql: &str,
457 _params: Vec<FilterValue>,
458 ) -> BoxFuture<'_, QueryResult<u64>> {
459 Box::pin(async { Ok(0) })
460 }
461
462 fn count(
463 &self,
464 _sql: &str,
465 _params: Vec<FilterValue>,
466 ) -> BoxFuture<'_, QueryResult<u64>> {
467 Box::pin(async { Ok(42) })
468 }
469
470 fn refresh_materialized_view(
471 &self,
472 view_name: &str,
473 concurrently: bool,
474 ) -> BoxFuture<'_, QueryResult<()>> {
475 let view_name = view_name.to_string();
476 Box::pin(async move {
477 let _ = (view_name, concurrently);
478 Ok(())
479 })
480 }
481 }
482
483 #[test]
486 fn test_view_find_many_basic() {
487 let op = ViewFindManyOperation::<MockEngine, TestView>::new(MockEngine);
488 let (sql, params) = op.build_sql();
489
490 assert_eq!(sql, "SELECT * FROM test_view");
491 assert!(params.is_empty());
492 }
493
494 #[test]
495 fn test_view_find_many_with_filter() {
496 let op = ViewFindManyOperation::<MockEngine, TestView>::new(MockEngine)
497 .r#where(Filter::Gte("post_count".into(), FilterValue::Int(10)));
498
499 let (sql, params) = op.build_sql();
500
501 assert!(sql.contains("WHERE"));
502 assert!(sql.contains("post_count"));
503 assert_eq!(params.len(), 1);
504 }
505
506 #[test]
507 fn test_view_find_many_with_pagination() {
508 let op = ViewFindManyOperation::<MockEngine, TestView>::new(MockEngine)
509 .skip(10)
510 .take(20);
511
512 let (sql, _) = op.build_sql();
513
514 assert!(sql.contains("LIMIT 20"));
515 assert!(sql.contains("OFFSET 10"));
516 }
517
518 #[test]
519 fn test_view_find_many_with_order() {
520 use crate::types::OrderByField;
521
522 let op = ViewFindManyOperation::<MockEngine, TestView>::new(MockEngine)
523 .order_by(OrderByField::desc("post_count"));
524
525 let (sql, _) = op.build_sql();
526
527 assert!(sql.contains("ORDER BY post_count DESC"));
528 }
529
530 #[test]
531 fn test_view_find_many_with_distinct() {
532 let op =
533 ViewFindManyOperation::<MockEngine, TestView>::new(MockEngine).distinct(["user_id"]);
534
535 let (sql, _) = op.build_sql();
536
537 assert!(sql.contains("DISTINCT ON (user_id)"));
538 }
539
540 #[test]
543 fn test_view_find_first_has_limit_1() {
544 let op = ViewFindFirstOperation::<MockEngine, TestView>::new(MockEngine);
545 let (sql, _) = op.build_sql();
546
547 assert!(sql.contains("LIMIT 1"));
548 }
549
550 #[test]
551 fn test_view_find_first_with_filter() {
552 let op = ViewFindFirstOperation::<MockEngine, TestView>::new(MockEngine)
553 .r#where(Filter::Equals("user_id".into(), FilterValue::Int(1)));
554
555 let (sql, params) = op.build_sql();
556
557 assert!(sql.contains("WHERE"));
558 assert!(sql.contains("user_id"));
559 assert!(sql.contains("LIMIT 1"));
560 assert_eq!(params.len(), 1);
561 }
562
563 #[test]
566 fn test_view_count_basic() {
567 let op = ViewCountOperation::<MockEngine, TestView>::new(MockEngine);
568 let (sql, params) = op.build_sql();
569
570 assert_eq!(sql, "SELECT COUNT(*) FROM test_view");
571 assert!(params.is_empty());
572 }
573
574 #[test]
575 fn test_view_count_with_filter() {
576 let op = ViewCountOperation::<MockEngine, TestView>::new(MockEngine)
577 .r#where(Filter::Gte("post_count".into(), FilterValue::Int(5)));
578
579 let (sql, params) = op.build_sql();
580
581 assert!(sql.contains("WHERE"));
582 assert!(sql.contains("post_count"));
583 assert_eq!(params.len(), 1);
584 }
585
586 #[tokio::test]
587 async fn test_view_count_exec() {
588 let op = ViewCountOperation::<MockEngine, TestView>::new(MockEngine);
589 let result = op.exec().await;
590
591 assert!(result.is_ok());
592 assert_eq!(result.unwrap(), 42); }
594
595 #[test]
598 fn test_refresh_materialized_view_default() {
599 let op =
600 RefreshMaterializedViewOperation::<MockEngine, TestMaterializedView>::new(MockEngine);
601
602 assert!(!op.concurrently);
603 }
604
605 #[test]
606 fn test_refresh_materialized_view_concurrently() {
607 let op =
608 RefreshMaterializedViewOperation::<MockEngine, TestMaterializedView>::new(MockEngine)
609 .concurrently();
610
611 assert!(op.concurrently);
612 }
613
614 #[tokio::test]
615 async fn test_refresh_materialized_view_exec() {
616 let op =
617 RefreshMaterializedViewOperation::<MockEngine, TestMaterializedView>::new(MockEngine);
618 let result = op.exec().await;
619
620 assert!(result.is_ok());
621 }
622
623 #[test]
626 fn test_view_query_builder_find_many() {
627 let builder = ViewQueryBuilder::<MockEngine, TestView>::new(MockEngine);
628 let op = builder.find_many();
629 let (sql, _) = op.build_sql();
630
631 assert!(sql.contains("SELECT * FROM test_view"));
632 }
633
634 #[test]
635 fn test_view_query_builder_find_first() {
636 let builder = ViewQueryBuilder::<MockEngine, TestView>::new(MockEngine);
637 let op = builder.find_first();
638 let (sql, _) = op.build_sql();
639
640 assert!(sql.contains("LIMIT 1"));
641 }
642
643 #[test]
644 fn test_view_query_builder_count() {
645 let builder = ViewQueryBuilder::<MockEngine, TestView>::new(MockEngine);
646 let op = builder.count();
647 let (sql, _) = op.build_sql();
648
649 assert!(sql.contains("COUNT(*)"));
650 }
651
652 #[test]
653 fn test_materialized_view_query_builder_refresh() {
654 let builder = ViewQueryBuilder::<MockEngine, TestMaterializedView>::new(MockEngine);
655 let _op = builder.refresh();
656 }
658
659 #[test]
660 fn test_view_query_builder_clone() {
661 let builder = ViewQueryBuilder::<MockEngine, TestView>::new(MockEngine);
662 let _cloned = builder.clone();
663 }
664
665 #[test]
668 fn test_view_trait_constants() {
669 assert_eq!(TestView::VIEW_NAME, "TestView");
670 assert_eq!(TestView::DB_VIEW_NAME, "test_view");
671 assert_eq!(
672 TestView::COLUMNS,
673 &["id", "user_id", "post_count"]
674 );
675 assert!(!TestView::IS_MATERIALIZED);
676 }
677
678 #[test]
679 fn test_materialized_view_trait_constants() {
680 assert_eq!(TestMaterializedView::VIEW_NAME, "TestMaterializedView");
681 assert!(TestMaterializedView::IS_MATERIALIZED);
682 assert!(TestMaterializedView::SUPPORTS_CONCURRENT_REFRESH);
683 }
684}
685
686