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