prax_query/operations/
view.rs

1//! View operations for querying database views (read-only).
2
3use 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/// A query operation that finds multiple records from a view.
12///
13/// Views are read-only, so only SELECT operations are supported.
14///
15/// # Example
16///
17/// ```rust,ignore
18/// let stats = client
19///     .user_stats()
20///     .find_many()
21///     .r#where(user_stats::post_count::gte(10))
22///     .order_by(user_stats::post_count::desc())
23///     .take(100)
24///     .exec()
25///     .await?;
26/// ```
27#[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    /// Create a new ViewFindMany operation.
40    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    /// Add a filter condition.
53    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    /// Set the order by clause.
60    pub fn order_by(mut self, order: impl Into<OrderBy>) -> Self {
61        self.order_by = order.into();
62        self
63    }
64
65    /// Skip a number of records.
66    pub fn skip(mut self, n: u64) -> Self {
67        self.pagination = self.pagination.skip(n);
68        self
69    }
70
71    /// Take a limited number of records.
72    pub fn take(mut self, n: u64) -> Self {
73        self.pagination = self.pagination.take(n);
74        self
75    }
76
77    /// Select specific fields.
78    pub fn select(mut self, select: impl Into<Select>) -> Self {
79        self.select = select.into();
80        self
81    }
82
83    /// Make the query distinct.
84    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    /// Set cursor for cursor-based pagination.
90    pub fn cursor(mut self, cursor: crate::pagination::Cursor) -> Self {
91        self.pagination = self.pagination.cursor(cursor);
92        self
93    }
94
95    /// Build the SQL query.
96    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        // SELECT clause
102        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        // FROM clause - use the view name
111        sql.push_str(" FROM ");
112        sql.push_str(V::DB_VIEW_NAME);
113
114        // WHERE clause
115        if !self.filter.is_none() {
116            sql.push_str(" WHERE ");
117            sql.push_str(&where_sql);
118        }
119
120        // ORDER BY clause
121        if !self.order_by.is_empty() {
122            sql.push_str(" ORDER BY ");
123            sql.push_str(&self.order_by.to_sql());
124        }
125
126        // LIMIT/OFFSET clause
127        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    /// Execute the query.
137    ///
138    /// Note: This requires the query engine to support view queries.
139    /// For now, we use raw query execution.
140    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        // Views return the same data structure - use raw query
146        // The actual implementation would depend on the QueryEngine
147        let _ = sql;
148        Ok(Vec::new()) // Placeholder - actual implementation in database-specific crates
149    }
150}
151
152/// A query operation that finds a single record from a view.
153pub struct ViewFindFirstOperation<E: QueryEngine, V: View> {
154    inner: ViewFindManyOperation<E, V>,
155}
156
157impl<E: QueryEngine, V: View> ViewFindFirstOperation<E, V> {
158    /// Create a new ViewFindFirst operation.
159    pub fn new(engine: E) -> Self {
160        Self {
161            inner: ViewFindManyOperation::new(engine).take(1),
162        }
163    }
164
165    /// Add a filter condition.
166    pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
167        self.inner = self.inner.r#where(filter);
168        self
169    }
170
171    /// Set the order by clause.
172    pub fn order_by(mut self, order: impl Into<OrderBy>) -> Self {
173        self.inner = self.inner.order_by(order);
174        self
175    }
176
177    /// Build the SQL query.
178    pub fn build_sql(&self) -> (String, Vec<crate::filter::FilterValue>) {
179        self.inner.build_sql()
180    }
181
182    /// Execute the query and return the first result.
183    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
192/// A count operation for views.
193pub 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    /// Create a new ViewCount operation.
201    pub fn new(engine: E) -> Self {
202        Self {
203            engine,
204            filter: Filter::None,
205            _view: PhantomData,
206        }
207    }
208
209    /// Add a filter condition.
210    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    /// Build the SQL query.
216    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    /// Execute the count query.
230    pub async fn exec(self) -> QueryResult<u64> {
231        let (sql, params) = self.build_sql();
232        self.engine.count(&sql, params).await
233    }
234}
235
236/// A refresh operation for materialized views.
237///
238/// This operation refreshes the data in a materialized view.
239/// For PostgreSQL, this executes `REFRESH MATERIALIZED VIEW`.
240///
241/// # Example
242///
243/// ```rust,ignore
244/// // Refresh a materialized view
245/// client
246///     .user_stats()
247///     .refresh()
248///     .exec()
249///     .await?;
250///
251/// // Refresh concurrently (allows reads during refresh)
252/// client
253///     .user_stats()
254///     .refresh()
255///     .concurrently()
256///     .exec()
257///     .await?;
258/// ```
259pub 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    /// Create a new refresh operation.
267    pub fn new(engine: E) -> Self {
268        Self {
269            engine,
270            concurrently: false,
271            _view: PhantomData,
272        }
273    }
274
275    /// Refresh concurrently (allows reads during refresh).
276    ///
277    /// Note: Concurrent refresh requires a unique index on the view.
278    /// Not all databases support concurrent refresh.
279    pub fn concurrently(mut self) -> Self {
280        self.concurrently = V::SUPPORTS_CONCURRENT_REFRESH;
281        self
282    }
283
284    /// Execute the refresh operation.
285    pub async fn exec(self) -> QueryResult<()> {
286        self.engine
287            .refresh_materialized_view(V::DB_VIEW_NAME, self.concurrently)
288            .await
289    }
290}
291
292/// The view query builder that provides access to view query operations.
293///
294/// Views are read-only, so only SELECT operations are available.
295/// No create, update, or delete operations are provided.
296pub struct ViewQueryBuilder<E: QueryEngine, V: View> {
297    engine: E,
298    _view: PhantomData<V>,
299}
300
301impl<E: QueryEngine, V: View> ViewQueryBuilder<E, V> {
302    /// Create a new view query builder.
303    pub fn new(engine: E) -> Self {
304        Self {
305            engine,
306            _view: PhantomData,
307        }
308    }
309
310    /// Start a find_many query on the view.
311    pub fn find_many(&self) -> ViewFindManyOperation<E, V> {
312        ViewFindManyOperation::new(self.engine.clone())
313    }
314
315    /// Start a find_first query on the view.
316    pub fn find_first(&self) -> ViewFindFirstOperation<E, V> {
317        ViewFindFirstOperation::new(self.engine.clone())
318    }
319
320    /// Start a count query on the view.
321    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    /// Start a refresh operation for a materialized view.
328    ///
329    /// This is only available for materialized views.
330    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
344/// A view accessor that provides query operations for a specific view.
345///
346/// This is typically generated by the proc-macro for each view.
347pub trait ViewAccessor<E: QueryEngine>: Send + Sync {
348    /// The view type.
349    type View: View;
350
351    /// Get the query engine.
352    fn engine(&self) -> &E;
353
354    /// Start a find_many query.
355    fn find_many(&self) -> ViewFindManyOperation<E, Self::View>;
356
357    /// Start a find_first query.
358    fn find_first(&self) -> ViewFindFirstOperation<E, Self::View>;
359
360    /// Count records in the view.
361    fn count(&self) -> ViewCountOperation<E, Self::View>;
362}
363
364/// A materialized view accessor with refresh capabilities.
365pub trait MaterializedViewAccessor<E: QueryEngine>: ViewAccessor<E>
366where
367    Self::View: MaterializedView,
368{
369    /// Refresh the materialized view.
370    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    // ========== ViewFindManyOperation Tests ==========
484
485    #[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    // ========== ViewFindFirstOperation Tests ==========
541
542    #[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    // ========== ViewCountOperation Tests ==========
564
565    #[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); // Mock returns 42
593    }
594
595    // ========== RefreshMaterializedViewOperation Tests ==========
596
597    #[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    // ========== ViewQueryBuilder Tests ==========
624
625    #[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        // Just verify we can call refresh on materialized views
657    }
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    // ========== View Trait Tests ==========
666
667    #[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