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}