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}