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}