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>) {
102 let (where_sql, params) = self.filter.to_sql(0, &crate::dialect::Postgres);
103
104 let mut sql = String::new();
105
106 sql.push_str("SELECT ");
108 if let Some(ref cols) = self.distinct {
109 sql.push_str("DISTINCT ON (");
110 sql.push_str(&cols.join(", "));
111 sql.push_str(") ");
112 }
113 sql.push_str(&self.select.to_sql());
114
115 sql.push_str(" FROM ");
117 sql.push_str(V::DB_VIEW_NAME);
118
119 if !self.filter.is_none() {
121 sql.push_str(" WHERE ");
122 sql.push_str(&where_sql);
123 }
124
125 if !self.order_by.is_empty() {
127 sql.push_str(" ORDER BY ");
128 sql.push_str(&self.order_by.to_sql());
129 }
130
131 let pagination_sql = self.pagination.to_sql();
133 if !pagination_sql.is_empty() {
134 sql.push(' ');
135 sql.push_str(&pagination_sql);
136 }
137
138 (sql, params)
139 }
140
141 pub async fn exec(self) -> QueryResult<Vec<V>>
146 where
147 V: Send + 'static + serde::de::DeserializeOwned,
148 {
149 let (sql, _params) = self.build_sql();
150 let _ = sql;
153 Ok(Vec::new()) }
155}
156
157pub struct ViewFindFirstOperation<E: QueryEngine, V: View> {
159 inner: ViewFindManyOperation<E, V>,
160}
161
162impl<E: QueryEngine, V: View> ViewFindFirstOperation<E, V> {
163 pub fn new(engine: E) -> Self {
165 Self {
166 inner: ViewFindManyOperation::new(engine).take(1),
167 }
168 }
169
170 pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
172 self.inner = self.inner.r#where(filter);
173 self
174 }
175
176 pub fn order_by(mut self, order: impl Into<OrderBy>) -> Self {
178 self.inner = self.inner.order_by(order);
179 self
180 }
181
182 pub fn build_sql(&self) -> (String, Vec<crate::filter::FilterValue>) {
184 self.inner.build_sql()
185 }
186
187 pub async fn exec(self) -> QueryResult<Option<V>>
189 where
190 V: Send + 'static + serde::de::DeserializeOwned,
191 {
192 let results = self.inner.exec().await?;
193 Ok(results.into_iter().next())
194 }
195}
196
197pub struct ViewCountOperation<E: QueryEngine, V: View> {
199 engine: E,
200 filter: Filter,
201 _view: PhantomData<V>,
202}
203
204impl<E: QueryEngine, V: View> ViewCountOperation<E, V> {
205 pub fn new(engine: E) -> Self {
207 Self {
208 engine,
209 filter: Filter::None,
210 _view: PhantomData,
211 }
212 }
213
214 pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
216 self.filter = self.filter.and_then(filter.into());
217 self
218 }
219
220 pub fn build_sql(&self) -> (String, Vec<crate::filter::FilterValue>) {
225 let (where_sql, params) = self.filter.to_sql(0, &crate::dialect::Postgres);
226
227 let mut sql = format!("SELECT COUNT(*) FROM {}", V::DB_VIEW_NAME);
228
229 if !self.filter.is_none() {
230 sql.push_str(" WHERE ");
231 sql.push_str(&where_sql);
232 }
233
234 (sql, params)
235 }
236
237 pub async fn exec(self) -> QueryResult<u64> {
239 let (sql, params) = self.build_sql();
240 self.engine.count(&sql, params).await
241 }
242}
243
244pub struct RefreshMaterializedViewOperation<E: QueryEngine, V: MaterializedView> {
268 engine: E,
269 concurrently: bool,
270 _view: PhantomData<V>,
271}
272
273impl<E: QueryEngine, V: MaterializedView> RefreshMaterializedViewOperation<E, V> {
274 pub fn new(engine: E) -> Self {
276 Self {
277 engine,
278 concurrently: false,
279 _view: PhantomData,
280 }
281 }
282
283 pub fn concurrently(mut self) -> Self {
288 self.concurrently = V::SUPPORTS_CONCURRENT_REFRESH;
289 self
290 }
291
292 pub async fn exec(self) -> QueryResult<()> {
294 self.engine
295 .refresh_materialized_view(V::DB_VIEW_NAME, self.concurrently)
296 .await
297 }
298}
299
300pub struct ViewQueryBuilder<E: QueryEngine, V: View> {
305 engine: E,
306 _view: PhantomData<V>,
307}
308
309impl<E: QueryEngine, V: View> ViewQueryBuilder<E, V> {
310 pub fn new(engine: E) -> Self {
312 Self {
313 engine,
314 _view: PhantomData,
315 }
316 }
317
318 pub fn find_many(&self) -> ViewFindManyOperation<E, V> {
320 ViewFindManyOperation::new(self.engine.clone())
321 }
322
323 pub fn find_first(&self) -> ViewFindFirstOperation<E, V> {
325 ViewFindFirstOperation::new(self.engine.clone())
326 }
327
328 pub fn count(&self) -> ViewCountOperation<E, V> {
330 ViewCountOperation::new(self.engine.clone())
331 }
332}
333
334impl<E: QueryEngine, V: MaterializedView> ViewQueryBuilder<E, V> {
335 pub fn refresh(&self) -> RefreshMaterializedViewOperation<E, V> {
339 RefreshMaterializedViewOperation::new(self.engine.clone())
340 }
341}
342
343impl<E: QueryEngine, V: View> Clone for ViewQueryBuilder<E, V> {
344 fn clone(&self) -> Self {
345 Self {
346 engine: self.engine.clone(),
347 _view: PhantomData,
348 }
349 }
350}
351
352pub trait ViewAccessor<E: QueryEngine>: Send + Sync {
356 type View: View;
358
359 fn engine(&self) -> &E;
361
362 fn find_many(&self) -> ViewFindManyOperation<E, Self::View>;
364
365 fn find_first(&self) -> ViewFindFirstOperation<E, Self::View>;
367
368 fn count(&self) -> ViewCountOperation<E, Self::View>;
370}
371
372pub trait MaterializedViewAccessor<E: QueryEngine>: ViewAccessor<E>
374where
375 Self::View: MaterializedView,
376{
377 fn refresh(&self) -> RefreshMaterializedViewOperation<E, Self::View>;
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384 use crate::error::QueryError;
385 use crate::filter::FilterValue;
386 use crate::traits::BoxFuture;
387
388 struct TestView;
389
390 impl View for TestView {
391 const VIEW_NAME: &'static str = "TestView";
392 const DB_VIEW_NAME: &'static str = "test_view";
393 const COLUMNS: &'static [&'static str] = &["id", "user_id", "post_count"];
394 const IS_MATERIALIZED: bool = false;
395 }
396
397 struct TestMaterializedView;
398
399 impl View for TestMaterializedView {
400 const VIEW_NAME: &'static str = "TestMaterializedView";
401 const DB_VIEW_NAME: &'static str = "test_materialized_view";
402 const COLUMNS: &'static [&'static str] = &["id", "stats"];
403 const IS_MATERIALIZED: bool = true;
404 }
405
406 impl MaterializedView for TestMaterializedView {
407 const SUPPORTS_CONCURRENT_REFRESH: bool = true;
408 }
409
410 #[derive(Clone)]
411 struct MockEngine;
412
413 impl QueryEngine for MockEngine {
414 fn dialect(&self) -> &dyn crate::dialect::SqlDialect {
415 &crate::dialect::Postgres
416 }
417
418 fn query_many<T: crate::traits::Model + crate::row::FromRow + Send + 'static>(
419 &self,
420 _sql: &str,
421 _params: Vec<FilterValue>,
422 ) -> BoxFuture<'_, QueryResult<Vec<T>>> {
423 Box::pin(async { Ok(Vec::new()) })
424 }
425
426 fn query_one<T: crate::traits::Model + crate::row::FromRow + Send + 'static>(
427 &self,
428 _sql: &str,
429 _params: Vec<FilterValue>,
430 ) -> BoxFuture<'_, QueryResult<T>> {
431 Box::pin(async { Err(QueryError::not_found("test")) })
432 }
433
434 fn query_optional<T: crate::traits::Model + crate::row::FromRow + Send + 'static>(
435 &self,
436 _sql: &str,
437 _params: Vec<FilterValue>,
438 ) -> BoxFuture<'_, QueryResult<Option<T>>> {
439 Box::pin(async { Ok(None) })
440 }
441
442 fn execute_insert<T: crate::traits::Model + crate::row::FromRow + Send + 'static>(
443 &self,
444 _sql: &str,
445 _params: Vec<FilterValue>,
446 ) -> BoxFuture<'_, QueryResult<T>> {
447 Box::pin(async { Err(QueryError::not_found("test")) })
448 }
449
450 fn execute_update<T: crate::traits::Model + crate::row::FromRow + Send + 'static>(
451 &self,
452 _sql: &str,
453 _params: Vec<FilterValue>,
454 ) -> BoxFuture<'_, QueryResult<Vec<T>>> {
455 Box::pin(async { Ok(Vec::new()) })
456 }
457
458 fn execute_delete(
459 &self,
460 _sql: &str,
461 _params: Vec<FilterValue>,
462 ) -> BoxFuture<'_, QueryResult<u64>> {
463 Box::pin(async { Ok(0) })
464 }
465
466 fn execute_raw(
467 &self,
468 _sql: &str,
469 _params: Vec<FilterValue>,
470 ) -> BoxFuture<'_, QueryResult<u64>> {
471 Box::pin(async { Ok(0) })
472 }
473
474 fn count(&self, _sql: &str, _params: Vec<FilterValue>) -> BoxFuture<'_, QueryResult<u64>> {
475 Box::pin(async { Ok(42) })
476 }
477
478 fn refresh_materialized_view(
479 &self,
480 view_name: &str,
481 concurrently: bool,
482 ) -> BoxFuture<'_, QueryResult<()>> {
483 let view_name = view_name.to_string();
484 Box::pin(async move {
485 let _ = (view_name, concurrently);
486 Ok(())
487 })
488 }
489 }
490
491 #[test]
494 fn test_view_find_many_basic() {
495 let op = ViewFindManyOperation::<MockEngine, TestView>::new(MockEngine);
496 let (sql, params) = op.build_sql();
497
498 assert_eq!(sql, "SELECT * FROM test_view");
499 assert!(params.is_empty());
500 }
501
502 #[test]
503 fn test_view_find_many_with_filter() {
504 let op = ViewFindManyOperation::<MockEngine, TestView>::new(MockEngine)
505 .r#where(Filter::Gte("post_count".into(), FilterValue::Int(10)));
506
507 let (sql, params) = op.build_sql();
508
509 assert!(sql.contains("WHERE"));
510 assert!(sql.contains("post_count"));
511 assert_eq!(params.len(), 1);
512 }
513
514 #[test]
515 fn test_view_find_many_with_pagination() {
516 let op = ViewFindManyOperation::<MockEngine, TestView>::new(MockEngine)
517 .skip(10)
518 .take(20);
519
520 let (sql, _) = op.build_sql();
521
522 assert!(sql.contains("LIMIT 20"));
523 assert!(sql.contains("OFFSET 10"));
524 }
525
526 #[test]
527 fn test_view_find_many_with_order() {
528 use crate::types::OrderByField;
529
530 let op = ViewFindManyOperation::<MockEngine, TestView>::new(MockEngine)
531 .order_by(OrderByField::desc("post_count"));
532
533 let (sql, _) = op.build_sql();
534
535 assert!(sql.contains("ORDER BY post_count DESC"));
536 }
537
538 #[test]
539 fn test_view_find_many_with_distinct() {
540 let op =
541 ViewFindManyOperation::<MockEngine, TestView>::new(MockEngine).distinct(["user_id"]);
542
543 let (sql, _) = op.build_sql();
544
545 assert!(sql.contains("DISTINCT ON (user_id)"));
546 }
547
548 #[test]
551 fn test_view_find_first_has_limit_1() {
552 let op = ViewFindFirstOperation::<MockEngine, TestView>::new(MockEngine);
553 let (sql, _) = op.build_sql();
554
555 assert!(sql.contains("LIMIT 1"));
556 }
557
558 #[test]
559 fn test_view_find_first_with_filter() {
560 let op = ViewFindFirstOperation::<MockEngine, TestView>::new(MockEngine)
561 .r#where(Filter::Equals("user_id".into(), FilterValue::Int(1)));
562
563 let (sql, params) = op.build_sql();
564
565 assert!(sql.contains("WHERE"));
566 assert!(sql.contains("user_id"));
567 assert!(sql.contains("LIMIT 1"));
568 assert_eq!(params.len(), 1);
569 }
570
571 #[test]
574 fn test_view_count_basic() {
575 let op = ViewCountOperation::<MockEngine, TestView>::new(MockEngine);
576 let (sql, params) = op.build_sql();
577
578 assert_eq!(sql, "SELECT COUNT(*) FROM test_view");
579 assert!(params.is_empty());
580 }
581
582 #[test]
583 fn test_view_count_with_filter() {
584 let op = ViewCountOperation::<MockEngine, TestView>::new(MockEngine)
585 .r#where(Filter::Gte("post_count".into(), FilterValue::Int(5)));
586
587 let (sql, params) = op.build_sql();
588
589 assert!(sql.contains("WHERE"));
590 assert!(sql.contains("post_count"));
591 assert_eq!(params.len(), 1);
592 }
593
594 #[tokio::test]
595 async fn test_view_count_exec() {
596 let op = ViewCountOperation::<MockEngine, TestView>::new(MockEngine);
597 let result = op.exec().await;
598
599 assert!(result.is_ok());
600 assert_eq!(result.unwrap(), 42); }
602
603 #[test]
606 fn test_refresh_materialized_view_default() {
607 let op =
608 RefreshMaterializedViewOperation::<MockEngine, TestMaterializedView>::new(MockEngine);
609
610 assert!(!op.concurrently);
611 }
612
613 #[test]
614 fn test_refresh_materialized_view_concurrently() {
615 let op =
616 RefreshMaterializedViewOperation::<MockEngine, TestMaterializedView>::new(MockEngine)
617 .concurrently();
618
619 assert!(op.concurrently);
620 }
621
622 #[tokio::test]
623 async fn test_refresh_materialized_view_exec() {
624 let op =
625 RefreshMaterializedViewOperation::<MockEngine, TestMaterializedView>::new(MockEngine);
626 let result = op.exec().await;
627
628 assert!(result.is_ok());
629 }
630
631 #[test]
634 fn test_view_query_builder_find_many() {
635 let builder = ViewQueryBuilder::<MockEngine, TestView>::new(MockEngine);
636 let op = builder.find_many();
637 let (sql, _) = op.build_sql();
638
639 assert!(sql.contains("SELECT * FROM test_view"));
640 }
641
642 #[test]
643 fn test_view_query_builder_find_first() {
644 let builder = ViewQueryBuilder::<MockEngine, TestView>::new(MockEngine);
645 let op = builder.find_first();
646 let (sql, _) = op.build_sql();
647
648 assert!(sql.contains("LIMIT 1"));
649 }
650
651 #[test]
652 fn test_view_query_builder_count() {
653 let builder = ViewQueryBuilder::<MockEngine, TestView>::new(MockEngine);
654 let op = builder.count();
655 let (sql, _) = op.build_sql();
656
657 assert!(sql.contains("COUNT(*)"));
658 }
659
660 #[test]
661 fn test_materialized_view_query_builder_refresh() {
662 let builder = ViewQueryBuilder::<MockEngine, TestMaterializedView>::new(MockEngine);
663 let _op = builder.refresh();
664 }
666
667 #[test]
668 fn test_view_query_builder_clone() {
669 let builder = ViewQueryBuilder::<MockEngine, TestView>::new(MockEngine);
670 let _cloned = builder.clone();
671 }
672
673 #[test]
676 fn test_view_trait_constants() {
677 assert_eq!(TestView::VIEW_NAME, "TestView");
678 assert_eq!(TestView::DB_VIEW_NAME, "test_view");
679 assert_eq!(TestView::COLUMNS, &["id", "user_id", "post_count"]);
680 assert!(!TestView::IS_MATERIALIZED);
681 }
682
683 #[test]
684 fn test_materialized_view_trait_constants() {
685 assert_eq!(TestMaterializedView::VIEW_NAME, "TestMaterializedView");
686 assert!(TestMaterializedView::IS_MATERIALIZED);
687 assert!(TestMaterializedView::SUPPORTS_CONCURRENT_REFRESH);
688 }
689}