Skip to main content

prax_query/
traits.rs

1//! Core traits for the query builder.
2
3use std::future::Future;
4use std::pin::Pin;
5
6use crate::error::QueryResult;
7use crate::filter::Filter;
8
9/// A model that can be queried.
10pub trait Model: Sized + Send + Sync {
11    /// The name of the model (used for table name).
12    const MODEL_NAME: &'static str;
13
14    /// The name of the database table.
15    const TABLE_NAME: &'static str;
16
17    /// The primary key column name(s).
18    const PRIMARY_KEY: &'static [&'static str];
19
20    /// All column names for this model.
21    const COLUMNS: &'static [&'static str];
22
23    /// Metadata for fields annotated with `#[prax(generated = "expr")]`.
24    ///
25    /// Each entry is `(field_name, expression, stored)` where `stored` is
26    /// `true` for a physically-stored generated column and `false` for a
27    /// virtual (computed-on-read) column.
28    ///
29    /// Models without any generated fields default to an empty slice.
30    const GENERATED_FIELDS: &'static [(&'static str, &'static str, bool)] = &[];
31
32    /// Metadata for fields annotated with aggregate directives
33    /// (`#[prax(count(rel))]`, `#[prax(sum(rel.field))]`, etc.).
34    ///
35    /// Each entry is `(field_name, kind_str, relation, field)` where
36    /// `kind_str` is one of `"count"`, `"sum"`, `"avg"`, `"min"`, `"max"`,
37    /// `relation` is the relation name, and `field` is the target field
38    /// (always `None` for `count`).
39    ///
40    /// Models without any aggregate fields default to an empty slice.
41    const AGGREGATE_FIELDS: &'static [(
42        &'static str,
43        &'static str,
44        &'static str,
45        Option<&'static str>,
46    )] = &[];
47}
48
49/// Runtime access to a model's primary key and column values.
50///
51/// Used by relation loaders (parent → child FK bucketing) and by upsert
52/// to build the conflict-row lookup. Implemented by codegen for every
53/// `#[derive(Model)]` struct and every `prax_schema!`-generated model.
54///
55/// Both methods return a [`crate::filter::FilterValue`] that mirrors
56/// exactly what the matching `From<T>` impl on the binding side would
57/// produce, so a PK value extracted here is a drop-in replacement for
58/// the same value produced by an equivalent type-checked filter.
59pub trait ModelWithPk: Model {
60    /// Primary-key value for this row.
61    ///
62    /// Single-column PKs return the appropriate scalar variant.
63    /// Composite PKs collapse to [`crate::filter::FilterValue::List`]
64    /// in the same declaration order as [`Model::PRIMARY_KEY`].
65    fn pk_value(&self) -> crate::filter::FilterValue;
66
67    /// Look up a column by its SQL name.
68    ///
69    /// Returns `None` for column names not present in [`Model::COLUMNS`].
70    /// The relation executor uses this to extract foreign-key values
71    /// from a fetched parent row without knowing the concrete FK type.
72    fn get_column_value(&self, column: &str) -> Option<crate::filter::FilterValue>;
73}
74
75/// A database view that can be queried (read-only).
76///
77/// Views are similar to models but only support read operations.
78/// They cannot be inserted into, updated, or deleted from directly.
79pub trait View: Sized + Send + Sync {
80    /// The name of the view.
81    const VIEW_NAME: &'static str;
82
83    /// The name of the database view.
84    const DB_VIEW_NAME: &'static str;
85
86    /// All column names for this view.
87    const COLUMNS: &'static [&'static str];
88
89    /// Whether this is a materialized view.
90    const IS_MATERIALIZED: bool;
91}
92
93/// A materialized view that supports refresh operations.
94pub trait MaterializedView: View {
95    /// Whether concurrent refresh is supported.
96    const SUPPORTS_CONCURRENT_REFRESH: bool = true;
97}
98
99/// A type that can be converted into a filter.
100pub trait IntoFilter {
101    /// Convert this type into a filter.
102    fn into_filter(self) -> Filter;
103}
104
105impl IntoFilter for Filter {
106    fn into_filter(self) -> Filter {
107        self
108    }
109}
110
111impl<F: FnOnce() -> Filter> IntoFilter for F {
112    fn into_filter(self) -> Filter {
113        self()
114    }
115}
116
117/// A query that can be executed.
118pub trait Executable {
119    /// The output type of the query.
120    type Output;
121
122    /// Execute the query and return the result.
123    fn exec(self) -> impl Future<Output = QueryResult<Self::Output>> + Send;
124}
125
126/// A boxed future for async operations.
127pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
128
129/// The query engine abstraction.
130///
131/// This trait defines how queries are executed against a database.
132/// Different implementations can be provided for different databases
133/// (PostgreSQL, MySQL, SQLite, etc.).
134pub trait QueryEngine: Send + Sync + Clone + 'static {
135    /// The SQL dialect this engine targets.
136    ///
137    /// Drivers that emit SQL (Postgres, MySQL, SQLite, MSSQL) override this
138    /// to return their matching dialect so the shared `Operation` builders
139    /// emit dialect-appropriate placeholders, `RETURNING` clauses, identifier
140    /// quoting, and upsert syntax.
141    ///
142    /// The default returns `&crate::dialect::NotSql`, the inert dialect
143    /// whose methods all panic if called. Non-SQL engines (MongoDB,
144    /// document stores) can leave the default in place — their own
145    /// operations never call SQL builders, so the panicking dialect is
146    /// never invoked. If you implement `QueryEngine` for a SQL backend
147    /// and forget to override this method, every attempt to build SQL
148    /// through your engine will panic, which is the intended loud-failure
149    /// mode.
150    fn dialect(&self) -> &dyn crate::dialect::SqlDialect {
151        &crate::dialect::NotSql
152    }
153
154    /// Execute a SELECT query and return rows.
155    fn query_many<T: Model + crate::row::FromRow + Send + 'static>(
156        &self,
157        sql: &str,
158        params: Vec<crate::filter::FilterValue>,
159    ) -> BoxFuture<'_, QueryResult<Vec<T>>>;
160
161    /// Execute a SELECT query expecting one result.
162    fn query_one<T: Model + crate::row::FromRow + Send + 'static>(
163        &self,
164        sql: &str,
165        params: Vec<crate::filter::FilterValue>,
166    ) -> BoxFuture<'_, QueryResult<T>>;
167
168    /// Execute a SELECT query expecting zero or one result.
169    fn query_optional<T: Model + crate::row::FromRow + Send + 'static>(
170        &self,
171        sql: &str,
172        params: Vec<crate::filter::FilterValue>,
173    ) -> BoxFuture<'_, QueryResult<Option<T>>>;
174
175    /// Execute an INSERT query and return the created row.
176    fn execute_insert<T: Model + crate::row::FromRow + Send + 'static>(
177        &self,
178        sql: &str,
179        params: Vec<crate::filter::FilterValue>,
180    ) -> BoxFuture<'_, QueryResult<T>>;
181
182    /// Execute an UPDATE query and return affected rows.
183    fn execute_update<T: Model + crate::row::FromRow + Send + 'static>(
184        &self,
185        sql: &str,
186        params: Vec<crate::filter::FilterValue>,
187    ) -> BoxFuture<'_, QueryResult<Vec<T>>>;
188
189    /// Execute a DELETE query and return affected rows count.
190    fn execute_delete(
191        &self,
192        sql: &str,
193        params: Vec<crate::filter::FilterValue>,
194    ) -> BoxFuture<'_, QueryResult<u64>>;
195
196    /// Execute a raw SQL query.
197    fn execute_raw(
198        &self,
199        sql: &str,
200        params: Vec<crate::filter::FilterValue>,
201    ) -> BoxFuture<'_, QueryResult<u64>>;
202
203    /// Get a count of records.
204    fn count(
205        &self,
206        sql: &str,
207        params: Vec<crate::filter::FilterValue>,
208    ) -> BoxFuture<'_, QueryResult<u64>>;
209
210    /// Execute an aggregate query (COUNT/SUM/AVG/MIN/MAX/GROUP BY) and
211    /// return one map of column name → [`crate::filter::FilterValue`]
212    /// per result row.
213    ///
214    /// Used by [`crate::operations::AggregateOperation`] and
215    /// [`crate::operations::GroupByOperation`] because aggregate result
216    /// sets don't fit a single `Model` schema: their columns are
217    /// dialect-chosen aliases (`_count`, `_sum_views`, …) whose types
218    /// depend on the aggregate function, and group-by queries also
219    /// include the grouped columns themselves. Returning untyped
220    /// column-value maps lets the aggregate builders adapt the shape
221    /// without every driver needing to generate a fresh `FromRow` impl
222    /// per query.
223    ///
224    /// The default returns
225    /// [`crate::error::QueryError::unsupported`], so non-SQL engines
226    /// (MongoDB, document stores) that never build aggregate queries
227    /// through the SQL operation builders don't have to implement this.
228    /// SQL engines must override.
229    fn aggregate_query(
230        &self,
231        sql: &str,
232        params: Vec<crate::filter::FilterValue>,
233    ) -> BoxFuture<
234        '_,
235        QueryResult<Vec<std::collections::HashMap<String, crate::filter::FilterValue>>>,
236    > {
237        let _ = (sql, params);
238        Box::pin(async {
239            Err(crate::error::QueryError::unsupported(
240                "aggregate_query is not implemented for this engine",
241            ))
242        })
243    }
244
245    /// Refresh a materialized view.
246    ///
247    /// For PostgreSQL, this executes `REFRESH MATERIALIZED VIEW`.
248    /// Materialized-view refresh is implemented for PostgreSQL today;
249    /// an MSSQL indexed-view rebuild is not implemented (engines that
250    /// don't override this return `unsupported`).
251    /// For databases that don't support materialized views, this returns an error.
252    fn refresh_materialized_view(
253        &self,
254        view_name: &str,
255        concurrently: bool,
256    ) -> BoxFuture<'_, QueryResult<()>> {
257        let view_name = view_name.to_string();
258        Box::pin(async move {
259            let _ = (view_name, concurrently);
260            Err(crate::error::QueryError::unsupported(
261                "Materialized view refresh is not supported by this database",
262            ))
263        })
264    }
265
266    /// Whether this engine is currently executing inside an open
267    /// transaction.
268    ///
269    /// The nested-write executor (phase 5) checks this to decide
270    /// whether to issue its own `BEGIN`/`COMMIT` around a write plan
271    /// or inline into a transaction the caller already started.
272    ///
273    /// Default returns `false`. Driver engines that wrap a
274    /// driver-native transaction object override and return `true`.
275    fn in_transaction(&self) -> bool {
276        false
277    }
278
279    /// Run the closure inside a transaction.
280    ///
281    /// Drivers that support real transactions override this to issue
282    /// `BEGIN` / `COMMIT` / `ROLLBACK` and route every query emitted
283    /// by the closure through the same underlying transaction. The
284    /// default below simply hands the closure a clone of the current
285    /// engine and executes it inline — it has **no transactional
286    /// semantics** on its own, so drivers that care about atomicity
287    /// must override. The default exists so non-SQL backends
288    /// (MongoDB, document stores) don't have to stub a method they
289    /// don't care about.
290    ///
291    /// The `Self: Clone` bound lets the default clone the engine into
292    /// the closure; every concrete `QueryEngine` already needs `Clone`
293    /// for [`ModelAccessor`] routing, so it's free in practice.
294    fn transaction<'a, R, Fut, F>(&'a self, f: F) -> BoxFuture<'a, QueryResult<R>>
295    where
296        F: FnOnce(Self) -> Fut + Send + 'a,
297        Fut: Future<Output = QueryResult<R>> + Send + 'a,
298        R: Send + 'a,
299        Self: Clone,
300    {
301        let me = self.clone();
302        Box::pin(async move { f(me).await })
303    }
304}
305
306/// Query engine extension for view operations.
307pub trait ViewQueryEngine: QueryEngine {
308    /// Query rows from a view.
309    fn query_view_many<V: View + Send + 'static>(
310        &self,
311        sql: &str,
312        params: Vec<crate::filter::FilterValue>,
313    ) -> BoxFuture<'_, QueryResult<Vec<V>>>;
314
315    /// Query a single row from a view.
316    fn query_view_optional<V: View + Send + 'static>(
317        &self,
318        sql: &str,
319        params: Vec<crate::filter::FilterValue>,
320    ) -> BoxFuture<'_, QueryResult<Option<V>>>;
321
322    /// Count rows in a view.
323    fn count_view(
324        &self,
325        sql: &str,
326        params: Vec<crate::filter::FilterValue>,
327    ) -> BoxFuture<'_, QueryResult<u64>> {
328        self.count(sql, params)
329    }
330}
331
332/// A model accessor that provides query operations.
333///
334/// This is typically generated by the proc-macro for each model.
335pub trait ModelAccessor<E: QueryEngine>: Send + Sync {
336    /// The model type.
337    type Model: Model;
338
339    /// Get the query engine.
340    fn engine(&self) -> &E;
341
342    /// Start a find_many query.
343    fn find_many(&self) -> crate::operations::FindManyOperation<E, Self::Model>;
344
345    /// Start a find_unique query.
346    fn find_unique(&self) -> crate::operations::FindUniqueOperation<E, Self::Model>;
347
348    /// Start a find_first query.
349    fn find_first(&self) -> crate::operations::FindFirstOperation<E, Self::Model>;
350
351    /// Start a create operation.
352    fn create(
353        &self,
354        data: <Self::Model as CreateData>::Data,
355    ) -> crate::operations::CreateOperation<E, Self::Model>
356    where
357        Self::Model: CreateData;
358
359    /// Start an update operation.
360    fn update(&self) -> crate::operations::UpdateOperation<E, Self::Model>;
361
362    /// Start a batch create operation.
363    ///
364    /// Default impl constructs a fresh `CreateManyOperation` from a
365    /// clone of the engine. `QueryEngine` already requires `Clone`,
366    /// so the bound is satisfied unconditionally. Override only if a
367    /// generated accessor needs to pre-configure the batch
368    /// (e.g. setting columns from schema defaults).
369    fn create_many(&self) -> crate::operations::CreateManyOperation<E, Self::Model> {
370        crate::operations::CreateManyOperation::new(self.engine().clone())
371    }
372
373    /// Start a batch update operation.
374    ///
375    /// Default impl constructs a fresh `UpdateManyOperation` from a
376    /// clone of the engine.
377    fn update_many(&self) -> crate::operations::UpdateManyOperation<E, Self::Model> {
378        crate::operations::UpdateManyOperation::new(self.engine().clone())
379    }
380
381    /// Start a delete operation.
382    fn delete(&self) -> crate::operations::DeleteOperation<E, Self::Model>;
383
384    /// Start an upsert operation.
385    fn upsert(
386        &self,
387        create: <Self::Model as CreateData>::Data,
388        update: <Self::Model as UpdateData>::Data,
389    ) -> crate::operations::UpsertOperation<E, Self::Model>
390    where
391        Self::Model: CreateData + UpdateData;
392
393    /// Count records matching a filter.
394    fn count(&self) -> crate::operations::CountOperation<E, Self::Model>;
395}
396
397/// Data for creating a new record.
398pub trait CreateData: Model {
399    /// The type that holds create data.
400    type Data: Send + Sync;
401}
402
403/// Data for updating an existing record.
404pub trait UpdateData: Model {
405    /// The type that holds update data.
406    type Data: Send + Sync;
407}
408
409/// Data for upserting a record.
410pub trait UpsertData: CreateData + UpdateData {}
411
412impl<T: CreateData + UpdateData> UpsertData for T {}
413
414/// Trait for models that support eager loading of relations.
415pub trait WithRelations: Model {
416    /// The type of include specification.
417    type Include;
418
419    /// The type of select specification.
420    type Select;
421}
422
423/// Routes a relation-include request to the right executor call.
424///
425/// Every `#[derive(Model)]` (and `prax_schema!`-generated model) emits
426/// an impl of this trait. Models with no relations get a trivial impl
427/// that errors on any unknown relation name; models with relations
428/// dispatch each name to [`crate::relations::executor::load_has_many`]
429/// and splice the results onto the parent slice.
430///
431/// Implementing this as a model-side trait — rather than carrying a
432/// `Vec<Box<dyn Loader>>` on the find-operation builder — keeps the
433/// executor fully monomorphic and lets `include(...)` remain a simple
434/// `String`-keyed lookup against the model's match arms.
435pub trait ModelRelationLoader<E: QueryEngine>: Sized {
436    /// Load every relation named by `spec` onto the `parents` slice.
437    ///
438    /// The slice is mutated in place — each parent's relation field is
439    /// set to the bucketed child collection. Models with no relations
440    /// return an `internal` [`crate::error::QueryError`] for any name.
441    fn load_relation<'a>(
442        engine: &'a E,
443        parents: &'a mut [Self],
444        spec: &'a crate::relations::IncludeSpec,
445    ) -> BoxFuture<'a, crate::error::QueryResult<()>>;
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451
452    struct TestModel;
453
454    impl Model for TestModel {
455        const MODEL_NAME: &'static str = "TestModel";
456        const TABLE_NAME: &'static str = "test_models";
457        const PRIMARY_KEY: &'static [&'static str] = &["id"];
458        const COLUMNS: &'static [&'static str] = &["id", "name", "email"];
459    }
460
461    impl crate::row::FromRow for TestModel {
462        fn from_row(_row: &impl crate::row::RowRef) -> Result<Self, crate::row::RowError> {
463            Ok(TestModel)
464        }
465    }
466
467    #[test]
468    fn test_model_trait() {
469        assert_eq!(TestModel::MODEL_NAME, "TestModel");
470        assert_eq!(TestModel::TABLE_NAME, "test_models");
471        assert_eq!(TestModel::PRIMARY_KEY, &["id"]);
472    }
473
474    #[test]
475    fn test_into_filter() {
476        let filter = Filter::Equals("id".into(), crate::filter::FilterValue::Int(1));
477        let converted = filter.clone().into_filter();
478        assert_eq!(converted, filter);
479    }
480
481    #[test]
482    fn default_in_transaction_returns_false() {
483        #[derive(Clone)]
484        struct E;
485
486        impl QueryEngine for E {
487            fn query_many<T: Model + crate::row::FromRow + Send + 'static>(
488                &self,
489                _sql: &str,
490                _params: Vec<crate::filter::FilterValue>,
491            ) -> BoxFuture<'_, QueryResult<Vec<T>>> {
492                Box::pin(async { Ok(Vec::new()) })
493            }
494            fn query_one<T: Model + crate::row::FromRow + Send + 'static>(
495                &self,
496                _sql: &str,
497                _params: Vec<crate::filter::FilterValue>,
498            ) -> BoxFuture<'_, QueryResult<T>> {
499                Box::pin(async { Err(crate::error::QueryError::not_found("t")) })
500            }
501            fn query_optional<T: Model + crate::row::FromRow + Send + 'static>(
502                &self,
503                _sql: &str,
504                _params: Vec<crate::filter::FilterValue>,
505            ) -> BoxFuture<'_, QueryResult<Option<T>>> {
506                Box::pin(async { Ok(None) })
507            }
508            fn execute_insert<T: Model + crate::row::FromRow + Send + 'static>(
509                &self,
510                _sql: &str,
511                _params: Vec<crate::filter::FilterValue>,
512            ) -> BoxFuture<'_, QueryResult<T>> {
513                Box::pin(async { Err(crate::error::QueryError::not_found("t")) })
514            }
515            fn execute_update<T: Model + crate::row::FromRow + Send + 'static>(
516                &self,
517                _sql: &str,
518                _params: Vec<crate::filter::FilterValue>,
519            ) -> BoxFuture<'_, QueryResult<Vec<T>>> {
520                Box::pin(async { Ok(Vec::new()) })
521            }
522            fn execute_delete(
523                &self,
524                _sql: &str,
525                _params: Vec<crate::filter::FilterValue>,
526            ) -> BoxFuture<'_, QueryResult<u64>> {
527                Box::pin(async { Ok(0) })
528            }
529            fn execute_raw(
530                &self,
531                _sql: &str,
532                _params: Vec<crate::filter::FilterValue>,
533            ) -> BoxFuture<'_, QueryResult<u64>> {
534                Box::pin(async { Ok(0) })
535            }
536            fn count(
537                &self,
538                _sql: &str,
539                _params: Vec<crate::filter::FilterValue>,
540            ) -> BoxFuture<'_, QueryResult<u64>> {
541                Box::pin(async { Ok(0) })
542            }
543        }
544
545        let e = E;
546        assert!(!e.in_transaction());
547    }
548
549    #[test]
550    #[should_panic(expected = "NotSql dialect does not emit SQL")]
551    fn query_engine_dialect_defaults_to_not_sql() {
552        // A minimal QueryEngine impl that doesn't override dialect() should
553        // inherit the NotSql default so external implementors aren't forced
554        // to add a method they don't care about.
555        use crate::filter::FilterValue;
556
557        #[derive(Clone)]
558        struct DefaultEngine;
559
560        impl QueryEngine for DefaultEngine {
561            fn query_many<T: Model + crate::row::FromRow + Send + 'static>(
562                &self,
563                _sql: &str,
564                _params: Vec<FilterValue>,
565            ) -> BoxFuture<'_, QueryResult<Vec<T>>> {
566                Box::pin(async { Ok(Vec::new()) })
567            }
568
569            fn query_one<T: Model + crate::row::FromRow + Send + 'static>(
570                &self,
571                _sql: &str,
572                _params: Vec<FilterValue>,
573            ) -> BoxFuture<'_, QueryResult<T>> {
574                Box::pin(async { Err(crate::error::QueryError::not_found("test")) })
575            }
576
577            fn query_optional<T: Model + crate::row::FromRow + Send + 'static>(
578                &self,
579                _sql: &str,
580                _params: Vec<FilterValue>,
581            ) -> BoxFuture<'_, QueryResult<Option<T>>> {
582                Box::pin(async { Ok(None) })
583            }
584
585            fn execute_insert<T: Model + crate::row::FromRow + Send + 'static>(
586                &self,
587                _sql: &str,
588                _params: Vec<FilterValue>,
589            ) -> BoxFuture<'_, QueryResult<T>> {
590                Box::pin(async { Err(crate::error::QueryError::not_found("test")) })
591            }
592
593            fn execute_update<T: Model + crate::row::FromRow + Send + 'static>(
594                &self,
595                _sql: &str,
596                _params: Vec<FilterValue>,
597            ) -> BoxFuture<'_, QueryResult<Vec<T>>> {
598                Box::pin(async { Ok(Vec::new()) })
599            }
600
601            fn execute_delete(
602                &self,
603                _sql: &str,
604                _params: Vec<FilterValue>,
605            ) -> BoxFuture<'_, QueryResult<u64>> {
606                Box::pin(async { Ok(0) })
607            }
608
609            fn execute_raw(
610                &self,
611                _sql: &str,
612                _params: Vec<FilterValue>,
613            ) -> BoxFuture<'_, QueryResult<u64>> {
614                Box::pin(async { Ok(0) })
615            }
616
617            fn count(
618                &self,
619                _sql: &str,
620                _params: Vec<FilterValue>,
621            ) -> BoxFuture<'_, QueryResult<u64>> {
622                Box::pin(async { Ok(0) })
623            }
624
625            // Note: dialect() is NOT overridden - we're testing the default
626        }
627
628        let e = DefaultEngine;
629        // If the default ever regresses back to a SQL-emitting dialect, this
630        // test will fail because placeholder() won't panic.
631        let _ = e.dialect().placeholder(1);
632    }
633}