Skip to main content

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    ///
97    /// View operations do not yet accept a dialect. They emit Postgres
98    /// placeholders and `DISTINCT ON`; behaviour against other backends is
99    /// undefined until view ops inherit the dialect-threaded build_sql shape
100    /// that `FindManyOperation` and friends use.
101    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        // SELECT clause
107        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        // FROM clause - use the view name
116        sql.push_str(" FROM ");
117        sql.push_str(V::DB_VIEW_NAME);
118
119        // WHERE clause
120        if !self.filter.is_none() {
121            sql.push_str(" WHERE ");
122            sql.push_str(&where_sql);
123        }
124
125        // ORDER BY clause
126        if !self.order_by.is_empty() {
127            sql.push_str(" ORDER BY ");
128            sql.push_str(&self.order_by.to_sql());
129        }
130
131        // LIMIT/OFFSET clause
132        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    /// Execute the query.
142    ///
143    /// Note: This requires the query engine to support view queries.
144    /// For now, we use raw query execution.
145    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        // Views return the same data structure - use raw query
151        // The actual implementation would depend on the QueryEngine
152        let _ = sql;
153        Ok(Vec::new()) // Placeholder - actual implementation in database-specific crates
154    }
155}
156
157/// A query operation that finds a single record from a view.
158pub struct ViewFindFirstOperation<E: QueryEngine, V: View> {
159    inner: ViewFindManyOperation<E, V>,
160}
161
162impl<E: QueryEngine, V: View> ViewFindFirstOperation<E, V> {
163    /// Create a new ViewFindFirst operation.
164    pub fn new(engine: E) -> Self {
165        Self {
166            inner: ViewFindManyOperation::new(engine).take(1),
167        }
168    }
169
170    /// Add a filter condition.
171    pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
172        self.inner = self.inner.r#where(filter);
173        self
174    }
175
176    /// Set the order by clause.
177    pub fn order_by(mut self, order: impl Into<OrderBy>) -> Self {
178        self.inner = self.inner.order_by(order);
179        self
180    }
181
182    /// Build the SQL query.
183    pub fn build_sql(&self) -> (String, Vec<crate::filter::FilterValue>) {
184        self.inner.build_sql()
185    }
186
187    /// Execute the query and return the first result.
188    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
197/// A count operation for views.
198pub 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    /// Create a new ViewCount operation.
206    pub fn new(engine: E) -> Self {
207        Self {
208            engine,
209            filter: Filter::None,
210            _view: PhantomData,
211        }
212    }
213
214    /// Add a filter condition.
215    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    /// Build the SQL query.
221    ///
222    /// Same caveat as `ViewFindManyOperation::build_sql`: view ops still emit
223    /// Postgres placeholders unconditionally.
224    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    /// Execute the count query.
238    pub async fn exec(self) -> QueryResult<u64> {
239        let (sql, params) = self.build_sql();
240        self.engine.count(&sql, params).await
241    }
242}
243
244/// A refresh operation for materialized views.
245///
246/// This operation refreshes the data in a materialized view.
247/// For PostgreSQL, this executes `REFRESH MATERIALIZED VIEW`.
248///
249/// # Example
250///
251/// ```rust,ignore
252/// // Refresh a materialized view
253/// client
254///     .user_stats()
255///     .refresh()
256///     .exec()
257///     .await?;
258///
259/// // Refresh concurrently (allows reads during refresh)
260/// client
261///     .user_stats()
262///     .refresh()
263///     .concurrently()
264///     .exec()
265///     .await?;
266/// ```
267pub 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    /// Create a new refresh operation.
275    pub fn new(engine: E) -> Self {
276        Self {
277            engine,
278            concurrently: false,
279            _view: PhantomData,
280        }
281    }
282
283    /// Refresh concurrently (allows reads during refresh).
284    ///
285    /// Note: Concurrent refresh requires a unique index on the view.
286    /// Not all databases support concurrent refresh.
287    pub fn concurrently(mut self) -> Self {
288        self.concurrently = V::SUPPORTS_CONCURRENT_REFRESH;
289        self
290    }
291
292    /// Execute the refresh operation.
293    pub async fn exec(self) -> QueryResult<()> {
294        self.engine
295            .refresh_materialized_view(V::DB_VIEW_NAME, self.concurrently)
296            .await
297    }
298}
299
300/// The view query builder that provides access to view query operations.
301///
302/// Views are read-only, so only SELECT operations are available.
303/// No create, update, or delete operations are provided.
304pub struct ViewQueryBuilder<E: QueryEngine, V: View> {
305    engine: E,
306    _view: PhantomData<V>,
307}
308
309impl<E: QueryEngine, V: View> ViewQueryBuilder<E, V> {
310    /// Create a new view query builder.
311    pub fn new(engine: E) -> Self {
312        Self {
313            engine,
314            _view: PhantomData,
315        }
316    }
317
318    /// Start a find_many query on the view.
319    pub fn find_many(&self) -> ViewFindManyOperation<E, V> {
320        ViewFindManyOperation::new(self.engine.clone())
321    }
322
323    /// Start a find_first query on the view.
324    pub fn find_first(&self) -> ViewFindFirstOperation<E, V> {
325        ViewFindFirstOperation::new(self.engine.clone())
326    }
327
328    /// Start a count query on the view.
329    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    /// Start a refresh operation for a materialized view.
336    ///
337    /// This is only available for materialized views.
338    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
352/// A view accessor that provides query operations for a specific view.
353///
354/// This is typically generated by the proc-macro for each view.
355pub trait ViewAccessor<E: QueryEngine>: Send + Sync {
356    /// The view type.
357    type View: View;
358
359    /// Get the query engine.
360    fn engine(&self) -> &E;
361
362    /// Start a find_many query.
363    fn find_many(&self) -> ViewFindManyOperation<E, Self::View>;
364
365    /// Start a find_first query.
366    fn find_first(&self) -> ViewFindFirstOperation<E, Self::View>;
367
368    /// Count records in the view.
369    fn count(&self) -> ViewCountOperation<E, Self::View>;
370}
371
372/// A materialized view accessor with refresh capabilities.
373pub trait MaterializedViewAccessor<E: QueryEngine>: ViewAccessor<E>
374where
375    Self::View: MaterializedView,
376{
377    /// Refresh the materialized view.
378    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    // ========== ViewFindManyOperation Tests ==========
492
493    #[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    // ========== ViewFindFirstOperation Tests ==========
549
550    #[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    // ========== ViewCountOperation Tests ==========
572
573    #[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); // Mock returns 42
601    }
602
603    // ========== RefreshMaterializedViewOperation Tests ==========
604
605    #[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    // ========== ViewQueryBuilder Tests ==========
632
633    #[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        // Just verify we can call refresh on materialized views
665    }
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    // ========== View Trait Tests ==========
674
675    #[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}