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    /// For MSSQL, this rebuilds the indexed view.
249    /// For databases that don't support materialized views, this returns an error.
250    fn refresh_materialized_view(
251        &self,
252        view_name: &str,
253        concurrently: bool,
254    ) -> BoxFuture<'_, QueryResult<()>> {
255        let view_name = view_name.to_string();
256        Box::pin(async move {
257            let _ = (view_name, concurrently);
258            Err(crate::error::QueryError::unsupported(
259                "Materialized view refresh is not supported by this database",
260            ))
261        })
262    }
263
264    /// Whether this engine is currently executing inside an open
265    /// transaction.
266    ///
267    /// The nested-write executor (phase 5) checks this to decide
268    /// whether to issue its own `BEGIN`/`COMMIT` around a write plan
269    /// or inline into a transaction the caller already started.
270    ///
271    /// Default returns `false`. Driver engines that wrap a
272    /// driver-native transaction object override and return `true`.
273    fn in_transaction(&self) -> bool {
274        false
275    }
276
277    /// Run the closure inside a transaction.
278    ///
279    /// Drivers that support real transactions override this to issue
280    /// `BEGIN` / `COMMIT` / `ROLLBACK` and route every query emitted
281    /// by the closure through the same underlying transaction. The
282    /// default below simply hands the closure a clone of the current
283    /// engine and executes it inline — it has **no transactional
284    /// semantics** on its own, so drivers that care about atomicity
285    /// must override. The default exists so non-SQL backends
286    /// (MongoDB, document stores) don't have to stub a method they
287    /// don't care about.
288    ///
289    /// The `Self: Clone` bound lets the default clone the engine into
290    /// the closure; every concrete `QueryEngine` already needs `Clone`
291    /// for [`ModelAccessor`] routing, so it's free in practice.
292    fn transaction<'a, R, Fut, F>(&'a self, f: F) -> BoxFuture<'a, QueryResult<R>>
293    where
294        F: FnOnce(Self) -> Fut + Send + 'a,
295        Fut: Future<Output = QueryResult<R>> + Send + 'a,
296        R: Send + 'a,
297        Self: Clone,
298    {
299        let me = self.clone();
300        Box::pin(async move { f(me).await })
301    }
302}
303
304/// Query engine extension for view operations.
305pub trait ViewQueryEngine: QueryEngine {
306    /// Query rows from a view.
307    fn query_view_many<V: View + Send + 'static>(
308        &self,
309        sql: &str,
310        params: Vec<crate::filter::FilterValue>,
311    ) -> BoxFuture<'_, QueryResult<Vec<V>>>;
312
313    /// Query a single row from a view.
314    fn query_view_optional<V: View + Send + 'static>(
315        &self,
316        sql: &str,
317        params: Vec<crate::filter::FilterValue>,
318    ) -> BoxFuture<'_, QueryResult<Option<V>>>;
319
320    /// Count rows in a view.
321    fn count_view(
322        &self,
323        sql: &str,
324        params: Vec<crate::filter::FilterValue>,
325    ) -> BoxFuture<'_, QueryResult<u64>> {
326        self.count(sql, params)
327    }
328}
329
330/// A model accessor that provides query operations.
331///
332/// This is typically generated by the proc-macro for each model.
333pub trait ModelAccessor<E: QueryEngine>: Send + Sync {
334    /// The model type.
335    type Model: Model;
336
337    /// Get the query engine.
338    fn engine(&self) -> &E;
339
340    /// Start a find_many query.
341    fn find_many(&self) -> crate::operations::FindManyOperation<E, Self::Model>;
342
343    /// Start a find_unique query.
344    fn find_unique(&self) -> crate::operations::FindUniqueOperation<E, Self::Model>;
345
346    /// Start a find_first query.
347    fn find_first(&self) -> crate::operations::FindFirstOperation<E, Self::Model>;
348
349    /// Start a create operation.
350    fn create(
351        &self,
352        data: <Self::Model as CreateData>::Data,
353    ) -> crate::operations::CreateOperation<E, Self::Model>
354    where
355        Self::Model: CreateData;
356
357    /// Start an update operation.
358    fn update(&self) -> crate::operations::UpdateOperation<E, Self::Model>;
359
360    /// Start a batch create operation.
361    ///
362    /// Default impl constructs a fresh `CreateManyOperation` from a
363    /// clone of the engine. `QueryEngine` already requires `Clone`,
364    /// so the bound is satisfied unconditionally. Override only if a
365    /// generated accessor needs to pre-configure the batch
366    /// (e.g. setting columns from schema defaults).
367    fn create_many(&self) -> crate::operations::CreateManyOperation<E, Self::Model> {
368        crate::operations::CreateManyOperation::new(self.engine().clone())
369    }
370
371    /// Start a batch update operation.
372    ///
373    /// Default impl constructs a fresh `UpdateManyOperation` from a
374    /// clone of the engine.
375    fn update_many(&self) -> crate::operations::UpdateManyOperation<E, Self::Model> {
376        crate::operations::UpdateManyOperation::new(self.engine().clone())
377    }
378
379    /// Start a delete operation.
380    fn delete(&self) -> crate::operations::DeleteOperation<E, Self::Model>;
381
382    /// Start an upsert operation.
383    fn upsert(
384        &self,
385        create: <Self::Model as CreateData>::Data,
386        update: <Self::Model as UpdateData>::Data,
387    ) -> crate::operations::UpsertOperation<E, Self::Model>
388    where
389        Self::Model: CreateData + UpdateData;
390
391    /// Count records matching a filter.
392    fn count(&self) -> crate::operations::CountOperation<E, Self::Model>;
393}
394
395/// Data for creating a new record.
396pub trait CreateData: Model {
397    /// The type that holds create data.
398    type Data: Send + Sync;
399}
400
401/// Data for updating an existing record.
402pub trait UpdateData: Model {
403    /// The type that holds update data.
404    type Data: Send + Sync;
405}
406
407/// Data for upserting a record.
408pub trait UpsertData: CreateData + UpdateData {}
409
410impl<T: CreateData + UpdateData> UpsertData for T {}
411
412/// Trait for models that support eager loading of relations.
413pub trait WithRelations: Model {
414    /// The type of include specification.
415    type Include;
416
417    /// The type of select specification.
418    type Select;
419}
420
421/// Routes a relation-include request to the right executor call.
422///
423/// Every `#[derive(Model)]` (and `prax_schema!`-generated model) emits
424/// an impl of this trait. Models with no relations get a trivial impl
425/// that errors on any unknown relation name; models with relations
426/// dispatch each name to [`crate::relations::executor::load_has_many`]
427/// and splice the results onto the parent slice.
428///
429/// Implementing this as a model-side trait — rather than carrying a
430/// `Vec<Box<dyn Loader>>` on the find-operation builder — keeps the
431/// executor fully monomorphic and lets `include(...)` remain a simple
432/// `String`-keyed lookup against the model's match arms.
433pub trait ModelRelationLoader<E: QueryEngine>: Sized {
434    /// Load every relation named by `spec` onto the `parents` slice.
435    ///
436    /// The slice is mutated in place — each parent's relation field is
437    /// set to the bucketed child collection. Models with no relations
438    /// return an `internal` [`crate::error::QueryError`] for any name.
439    fn load_relation<'a>(
440        engine: &'a E,
441        parents: &'a mut [Self],
442        spec: &'a crate::relations::IncludeSpec,
443    ) -> BoxFuture<'a, crate::error::QueryResult<()>>;
444}
445
446#[cfg(test)]
447mod tests {
448    use super::*;
449
450    struct TestModel;
451
452    impl Model for TestModel {
453        const MODEL_NAME: &'static str = "TestModel";
454        const TABLE_NAME: &'static str = "test_models";
455        const PRIMARY_KEY: &'static [&'static str] = &["id"];
456        const COLUMNS: &'static [&'static str] = &["id", "name", "email"];
457    }
458
459    impl crate::row::FromRow for TestModel {
460        fn from_row(_row: &impl crate::row::RowRef) -> Result<Self, crate::row::RowError> {
461            Ok(TestModel)
462        }
463    }
464
465    #[test]
466    fn test_model_trait() {
467        assert_eq!(TestModel::MODEL_NAME, "TestModel");
468        assert_eq!(TestModel::TABLE_NAME, "test_models");
469        assert_eq!(TestModel::PRIMARY_KEY, &["id"]);
470    }
471
472    #[test]
473    fn test_into_filter() {
474        let filter = Filter::Equals("id".into(), crate::filter::FilterValue::Int(1));
475        let converted = filter.clone().into_filter();
476        assert_eq!(converted, filter);
477    }
478
479    #[test]
480    fn default_in_transaction_returns_false() {
481        #[derive(Clone)]
482        struct E;
483
484        impl QueryEngine for E {
485            fn query_many<T: Model + crate::row::FromRow + Send + 'static>(
486                &self,
487                _sql: &str,
488                _params: Vec<crate::filter::FilterValue>,
489            ) -> BoxFuture<'_, QueryResult<Vec<T>>> {
490                Box::pin(async { Ok(Vec::new()) })
491            }
492            fn query_one<T: Model + crate::row::FromRow + Send + 'static>(
493                &self,
494                _sql: &str,
495                _params: Vec<crate::filter::FilterValue>,
496            ) -> BoxFuture<'_, QueryResult<T>> {
497                Box::pin(async { Err(crate::error::QueryError::not_found("t")) })
498            }
499            fn query_optional<T: Model + crate::row::FromRow + Send + 'static>(
500                &self,
501                _sql: &str,
502                _params: Vec<crate::filter::FilterValue>,
503            ) -> BoxFuture<'_, QueryResult<Option<T>>> {
504                Box::pin(async { Ok(None) })
505            }
506            fn execute_insert<T: Model + crate::row::FromRow + Send + 'static>(
507                &self,
508                _sql: &str,
509                _params: Vec<crate::filter::FilterValue>,
510            ) -> BoxFuture<'_, QueryResult<T>> {
511                Box::pin(async { Err(crate::error::QueryError::not_found("t")) })
512            }
513            fn execute_update<T: Model + crate::row::FromRow + Send + 'static>(
514                &self,
515                _sql: &str,
516                _params: Vec<crate::filter::FilterValue>,
517            ) -> BoxFuture<'_, QueryResult<Vec<T>>> {
518                Box::pin(async { Ok(Vec::new()) })
519            }
520            fn execute_delete(
521                &self,
522                _sql: &str,
523                _params: Vec<crate::filter::FilterValue>,
524            ) -> BoxFuture<'_, QueryResult<u64>> {
525                Box::pin(async { Ok(0) })
526            }
527            fn execute_raw(
528                &self,
529                _sql: &str,
530                _params: Vec<crate::filter::FilterValue>,
531            ) -> BoxFuture<'_, QueryResult<u64>> {
532                Box::pin(async { Ok(0) })
533            }
534            fn count(
535                &self,
536                _sql: &str,
537                _params: Vec<crate::filter::FilterValue>,
538            ) -> BoxFuture<'_, QueryResult<u64>> {
539                Box::pin(async { Ok(0) })
540            }
541        }
542
543        let e = E;
544        assert!(!e.in_transaction());
545    }
546
547    #[test]
548    #[should_panic(expected = "NotSql dialect does not emit SQL")]
549    fn query_engine_dialect_defaults_to_not_sql() {
550        // A minimal QueryEngine impl that doesn't override dialect() should
551        // inherit the NotSql default so external implementors aren't forced
552        // to add a method they don't care about.
553        use crate::filter::FilterValue;
554
555        #[derive(Clone)]
556        struct DefaultEngine;
557
558        impl QueryEngine for DefaultEngine {
559            fn query_many<T: Model + crate::row::FromRow + Send + 'static>(
560                &self,
561                _sql: &str,
562                _params: Vec<FilterValue>,
563            ) -> BoxFuture<'_, QueryResult<Vec<T>>> {
564                Box::pin(async { Ok(Vec::new()) })
565            }
566
567            fn query_one<T: Model + crate::row::FromRow + Send + 'static>(
568                &self,
569                _sql: &str,
570                _params: Vec<FilterValue>,
571            ) -> BoxFuture<'_, QueryResult<T>> {
572                Box::pin(async { Err(crate::error::QueryError::not_found("test")) })
573            }
574
575            fn query_optional<T: Model + crate::row::FromRow + Send + 'static>(
576                &self,
577                _sql: &str,
578                _params: Vec<FilterValue>,
579            ) -> BoxFuture<'_, QueryResult<Option<T>>> {
580                Box::pin(async { Ok(None) })
581            }
582
583            fn execute_insert<T: Model + crate::row::FromRow + Send + 'static>(
584                &self,
585                _sql: &str,
586                _params: Vec<FilterValue>,
587            ) -> BoxFuture<'_, QueryResult<T>> {
588                Box::pin(async { Err(crate::error::QueryError::not_found("test")) })
589            }
590
591            fn execute_update<T: Model + crate::row::FromRow + Send + 'static>(
592                &self,
593                _sql: &str,
594                _params: Vec<FilterValue>,
595            ) -> BoxFuture<'_, QueryResult<Vec<T>>> {
596                Box::pin(async { Ok(Vec::new()) })
597            }
598
599            fn execute_delete(
600                &self,
601                _sql: &str,
602                _params: Vec<FilterValue>,
603            ) -> BoxFuture<'_, QueryResult<u64>> {
604                Box::pin(async { Ok(0) })
605            }
606
607            fn execute_raw(
608                &self,
609                _sql: &str,
610                _params: Vec<FilterValue>,
611            ) -> BoxFuture<'_, QueryResult<u64>> {
612                Box::pin(async { Ok(0) })
613            }
614
615            fn count(
616                &self,
617                _sql: &str,
618                _params: Vec<FilterValue>,
619            ) -> BoxFuture<'_, QueryResult<u64>> {
620                Box::pin(async { Ok(0) })
621            }
622
623            // Note: dialect() is NOT overridden - we're testing the default
624        }
625
626        let e = DefaultEngine;
627        // If the default ever regresses back to a SQL-emitting dialect, this
628        // test will fail because placeholder() won't panic.
629        let _ = e.dialect().placeholder(1);
630    }
631}