prax-query 0.9.6

Type-safe query builder for the Prax ORM
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
//! Core traits for the query builder.

use std::future::Future;
use std::pin::Pin;

use crate::error::QueryResult;
use crate::filter::Filter;

/// A model that can be queried.
pub trait Model: Sized + Send + Sync {
    /// The name of the model (used for table name).
    const MODEL_NAME: &'static str;

    /// The name of the database table.
    const TABLE_NAME: &'static str;

    /// The primary key column name(s).
    const PRIMARY_KEY: &'static [&'static str];

    /// All column names for this model.
    const COLUMNS: &'static [&'static str];
}

/// Runtime access to a model's primary key and column values.
///
/// Used by relation loaders (parent → child FK bucketing) and by upsert
/// to build the conflict-row lookup. Implemented by codegen for every
/// `#[derive(Model)]` struct and every `prax_schema!`-generated model.
///
/// Both methods return a [`crate::filter::FilterValue`] that mirrors
/// exactly what the matching `From<T>` impl on the binding side would
/// produce, so a PK value extracted here is a drop-in replacement for
/// the same value produced by an equivalent type-checked filter.
pub trait ModelWithPk: Model {
    /// Primary-key value for this row.
    ///
    /// Single-column PKs return the appropriate scalar variant.
    /// Composite PKs collapse to [`crate::filter::FilterValue::List`]
    /// in the same declaration order as [`Model::PRIMARY_KEY`].
    fn pk_value(&self) -> crate::filter::FilterValue;

    /// Look up a column by its SQL name.
    ///
    /// Returns `None` for column names not present in [`Model::COLUMNS`].
    /// The relation executor uses this to extract foreign-key values
    /// from a fetched parent row without knowing the concrete FK type.
    fn get_column_value(&self, column: &str) -> Option<crate::filter::FilterValue>;
}

/// A database view that can be queried (read-only).
///
/// Views are similar to models but only support read operations.
/// They cannot be inserted into, updated, or deleted from directly.
pub trait View: Sized + Send + Sync {
    /// The name of the view.
    const VIEW_NAME: &'static str;

    /// The name of the database view.
    const DB_VIEW_NAME: &'static str;

    /// All column names for this view.
    const COLUMNS: &'static [&'static str];

    /// Whether this is a materialized view.
    const IS_MATERIALIZED: bool;
}

/// A materialized view that supports refresh operations.
pub trait MaterializedView: View {
    /// Whether concurrent refresh is supported.
    const SUPPORTS_CONCURRENT_REFRESH: bool = true;
}

/// A type that can be converted into a filter.
pub trait IntoFilter {
    /// Convert this type into a filter.
    fn into_filter(self) -> Filter;
}

impl IntoFilter for Filter {
    fn into_filter(self) -> Filter {
        self
    }
}

impl<F: FnOnce() -> Filter> IntoFilter for F {
    fn into_filter(self) -> Filter {
        self()
    }
}

/// A query that can be executed.
pub trait Executable {
    /// The output type of the query.
    type Output;

    /// Execute the query and return the result.
    fn exec(self) -> impl Future<Output = QueryResult<Self::Output>> + Send;
}

/// A boxed future for async operations.
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;

/// The query engine abstraction.
///
/// This trait defines how queries are executed against a database.
/// Different implementations can be provided for different databases
/// (PostgreSQL, MySQL, SQLite, etc.).
pub trait QueryEngine: Send + Sync + Clone + 'static {
    /// The SQL dialect this engine targets.
    ///
    /// Drivers that emit SQL (Postgres, MySQL, SQLite, MSSQL) override this
    /// to return their matching dialect so the shared `Operation` builders
    /// emit dialect-appropriate placeholders, `RETURNING` clauses, identifier
    /// quoting, and upsert syntax.
    ///
    /// The default returns `&crate::dialect::NotSql`, the inert dialect
    /// whose methods all panic if called. Non-SQL engines (MongoDB,
    /// document stores) can leave the default in place — their own
    /// operations never call SQL builders, so the panicking dialect is
    /// never invoked. If you implement `QueryEngine` for a SQL backend
    /// and forget to override this method, every attempt to build SQL
    /// through your engine will panic, which is the intended loud-failure
    /// mode.
    fn dialect(&self) -> &dyn crate::dialect::SqlDialect {
        &crate::dialect::NotSql
    }

    /// Execute a SELECT query and return rows.
    fn query_many<T: Model + crate::row::FromRow + Send + 'static>(
        &self,
        sql: &str,
        params: Vec<crate::filter::FilterValue>,
    ) -> BoxFuture<'_, QueryResult<Vec<T>>>;

    /// Execute a SELECT query expecting one result.
    fn query_one<T: Model + crate::row::FromRow + Send + 'static>(
        &self,
        sql: &str,
        params: Vec<crate::filter::FilterValue>,
    ) -> BoxFuture<'_, QueryResult<T>>;

    /// Execute a SELECT query expecting zero or one result.
    fn query_optional<T: Model + crate::row::FromRow + Send + 'static>(
        &self,
        sql: &str,
        params: Vec<crate::filter::FilterValue>,
    ) -> BoxFuture<'_, QueryResult<Option<T>>>;

    /// Execute an INSERT query and return the created row.
    fn execute_insert<T: Model + crate::row::FromRow + Send + 'static>(
        &self,
        sql: &str,
        params: Vec<crate::filter::FilterValue>,
    ) -> BoxFuture<'_, QueryResult<T>>;

    /// Execute an UPDATE query and return affected rows.
    fn execute_update<T: Model + crate::row::FromRow + Send + 'static>(
        &self,
        sql: &str,
        params: Vec<crate::filter::FilterValue>,
    ) -> BoxFuture<'_, QueryResult<Vec<T>>>;

    /// Execute a DELETE query and return affected rows count.
    fn execute_delete(
        &self,
        sql: &str,
        params: Vec<crate::filter::FilterValue>,
    ) -> BoxFuture<'_, QueryResult<u64>>;

    /// Execute a raw SQL query.
    fn execute_raw(
        &self,
        sql: &str,
        params: Vec<crate::filter::FilterValue>,
    ) -> BoxFuture<'_, QueryResult<u64>>;

    /// Get a count of records.
    fn count(
        &self,
        sql: &str,
        params: Vec<crate::filter::FilterValue>,
    ) -> BoxFuture<'_, QueryResult<u64>>;

    /// Execute an aggregate query (COUNT/SUM/AVG/MIN/MAX/GROUP BY) and
    /// return one map of column name → [`crate::filter::FilterValue`]
    /// per result row.
    ///
    /// Used by [`crate::operations::AggregateOperation`] and
    /// [`crate::operations::GroupByOperation`] because aggregate result
    /// sets don't fit a single `Model` schema: their columns are
    /// dialect-chosen aliases (`_count`, `_sum_views`, …) whose types
    /// depend on the aggregate function, and group-by queries also
    /// include the grouped columns themselves. Returning untyped
    /// column-value maps lets the aggregate builders adapt the shape
    /// without every driver needing to generate a fresh `FromRow` impl
    /// per query.
    ///
    /// The default returns
    /// [`crate::error::QueryError::unsupported`], so non-SQL engines
    /// (MongoDB, document stores) that never build aggregate queries
    /// through the SQL operation builders don't have to implement this.
    /// SQL engines must override.
    fn aggregate_query(
        &self,
        sql: &str,
        params: Vec<crate::filter::FilterValue>,
    ) -> BoxFuture<
        '_,
        QueryResult<Vec<std::collections::HashMap<String, crate::filter::FilterValue>>>,
    > {
        let _ = (sql, params);
        Box::pin(async {
            Err(crate::error::QueryError::unsupported(
                "aggregate_query is not implemented for this engine",
            ))
        })
    }

    /// Refresh a materialized view.
    ///
    /// For PostgreSQL, this executes `REFRESH MATERIALIZED VIEW`.
    /// For MSSQL, this rebuilds the indexed view.
    /// For databases that don't support materialized views, this returns an error.
    fn refresh_materialized_view(
        &self,
        view_name: &str,
        concurrently: bool,
    ) -> BoxFuture<'_, QueryResult<()>> {
        let view_name = view_name.to_string();
        Box::pin(async move {
            let _ = (view_name, concurrently);
            Err(crate::error::QueryError::unsupported(
                "Materialized view refresh is not supported by this database",
            ))
        })
    }

    /// Run the closure inside a transaction.
    ///
    /// Drivers that support real transactions override this to issue
    /// `BEGIN` / `COMMIT` / `ROLLBACK` and route every query emitted
    /// by the closure through the same underlying transaction. The
    /// default below simply hands the closure a clone of the current
    /// engine and executes it inline — it has **no transactional
    /// semantics** on its own, so drivers that care about atomicity
    /// must override. The default exists so non-SQL backends
    /// (MongoDB, document stores) don't have to stub a method they
    /// don't care about.
    ///
    /// The `Self: Clone` bound lets the default clone the engine into
    /// the closure; every concrete `QueryEngine` already needs `Clone`
    /// for [`ModelAccessor`] routing, so it's free in practice.
    fn transaction<'a, R, Fut, F>(&'a self, f: F) -> BoxFuture<'a, QueryResult<R>>
    where
        F: FnOnce(Self) -> Fut + Send + 'a,
        Fut: Future<Output = QueryResult<R>> + Send + 'a,
        R: Send + 'a,
        Self: Clone,
    {
        let me = self.clone();
        Box::pin(async move { f(me).await })
    }
}

/// Query engine extension for view operations.
pub trait ViewQueryEngine: QueryEngine {
    /// Query rows from a view.
    fn query_view_many<V: View + Send + 'static>(
        &self,
        sql: &str,
        params: Vec<crate::filter::FilterValue>,
    ) -> BoxFuture<'_, QueryResult<Vec<V>>>;

    /// Query a single row from a view.
    fn query_view_optional<V: View + Send + 'static>(
        &self,
        sql: &str,
        params: Vec<crate::filter::FilterValue>,
    ) -> BoxFuture<'_, QueryResult<Option<V>>>;

    /// Count rows in a view.
    fn count_view(
        &self,
        sql: &str,
        params: Vec<crate::filter::FilterValue>,
    ) -> BoxFuture<'_, QueryResult<u64>> {
        self.count(sql, params)
    }
}

/// A model accessor that provides query operations.
///
/// This is typically generated by the proc-macro for each model.
pub trait ModelAccessor<E: QueryEngine>: Send + Sync {
    /// The model type.
    type Model: Model;

    /// Get the query engine.
    fn engine(&self) -> &E;

    /// Start a find_many query.
    fn find_many(&self) -> crate::operations::FindManyOperation<E, Self::Model>;

    /// Start a find_unique query.
    fn find_unique(&self) -> crate::operations::FindUniqueOperation<E, Self::Model>;

    /// Start a find_first query.
    fn find_first(&self) -> crate::operations::FindFirstOperation<E, Self::Model>;

    /// Start a create operation.
    fn create(
        &self,
        data: <Self::Model as CreateData>::Data,
    ) -> crate::operations::CreateOperation<E, Self::Model>
    where
        Self::Model: CreateData;

    /// Start an update operation.
    fn update(&self) -> crate::operations::UpdateOperation<E, Self::Model>;

    /// Start a delete operation.
    fn delete(&self) -> crate::operations::DeleteOperation<E, Self::Model>;

    /// Start an upsert operation.
    fn upsert(
        &self,
        create: <Self::Model as CreateData>::Data,
        update: <Self::Model as UpdateData>::Data,
    ) -> crate::operations::UpsertOperation<E, Self::Model>
    where
        Self::Model: CreateData + UpdateData;

    /// Count records matching a filter.
    fn count(&self) -> crate::operations::CountOperation<E, Self::Model>;
}

/// Data for creating a new record.
pub trait CreateData: Model {
    /// The type that holds create data.
    type Data: Send + Sync;
}

/// Data for updating an existing record.
pub trait UpdateData: Model {
    /// The type that holds update data.
    type Data: Send + Sync;
}

/// Data for upserting a record.
pub trait UpsertData: CreateData + UpdateData {}

impl<T: CreateData + UpdateData> UpsertData for T {}

/// Trait for models that support eager loading of relations.
pub trait WithRelations: Model {
    /// The type of include specification.
    type Include;

    /// The type of select specification.
    type Select;
}

/// Routes a relation-include request to the right executor call.
///
/// Every `#[derive(Model)]` (and `prax_schema!`-generated model) emits
/// an impl of this trait. Models with no relations get a trivial impl
/// that errors on any unknown relation name; models with relations
/// dispatch each name to [`crate::relations::executor::load_has_many`]
/// and splice the results onto the parent slice.
///
/// Implementing this as a model-side trait — rather than carrying a
/// `Vec<Box<dyn Loader>>` on the find-operation builder — keeps the
/// executor fully monomorphic and lets `include(...)` remain a simple
/// `String`-keyed lookup against the model's match arms.
pub trait ModelRelationLoader<E: QueryEngine>: Sized {
    /// Load every relation named by `spec` onto the `parents` slice.
    ///
    /// The slice is mutated in place — each parent's relation field is
    /// set to the bucketed child collection. Models with no relations
    /// return an `internal` [`crate::error::QueryError`] for any name.
    fn load_relation<'a>(
        engine: &'a E,
        parents: &'a mut [Self],
        spec: &'a crate::relations::IncludeSpec,
    ) -> BoxFuture<'a, crate::error::QueryResult<()>>;
}

#[cfg(test)]
mod tests {
    use super::*;

    struct TestModel;

    impl Model for TestModel {
        const MODEL_NAME: &'static str = "TestModel";
        const TABLE_NAME: &'static str = "test_models";
        const PRIMARY_KEY: &'static [&'static str] = &["id"];
        const COLUMNS: &'static [&'static str] = &["id", "name", "email"];
    }

    impl crate::row::FromRow for TestModel {
        fn from_row(_row: &impl crate::row::RowRef) -> Result<Self, crate::row::RowError> {
            Ok(TestModel)
        }
    }

    #[test]
    fn test_model_trait() {
        assert_eq!(TestModel::MODEL_NAME, "TestModel");
        assert_eq!(TestModel::TABLE_NAME, "test_models");
        assert_eq!(TestModel::PRIMARY_KEY, &["id"]);
    }

    #[test]
    fn test_into_filter() {
        let filter = Filter::Equals("id".into(), crate::filter::FilterValue::Int(1));
        let converted = filter.clone().into_filter();
        assert_eq!(converted, filter);
    }

    #[test]
    #[should_panic(expected = "NotSql dialect does not emit SQL")]
    fn query_engine_dialect_defaults_to_not_sql() {
        // A minimal QueryEngine impl that doesn't override dialect() should
        // inherit the NotSql default so external implementors aren't forced
        // to add a method they don't care about.
        use crate::filter::FilterValue;

        #[derive(Clone)]
        struct DefaultEngine;

        impl QueryEngine for DefaultEngine {
            fn query_many<T: Model + crate::row::FromRow + Send + 'static>(
                &self,
                _sql: &str,
                _params: Vec<FilterValue>,
            ) -> BoxFuture<'_, QueryResult<Vec<T>>> {
                Box::pin(async { Ok(Vec::new()) })
            }

            fn query_one<T: Model + crate::row::FromRow + Send + 'static>(
                &self,
                _sql: &str,
                _params: Vec<FilterValue>,
            ) -> BoxFuture<'_, QueryResult<T>> {
                Box::pin(async { Err(crate::error::QueryError::not_found("test")) })
            }

            fn query_optional<T: Model + crate::row::FromRow + Send + 'static>(
                &self,
                _sql: &str,
                _params: Vec<FilterValue>,
            ) -> BoxFuture<'_, QueryResult<Option<T>>> {
                Box::pin(async { Ok(None) })
            }

            fn execute_insert<T: Model + crate::row::FromRow + Send + 'static>(
                &self,
                _sql: &str,
                _params: Vec<FilterValue>,
            ) -> BoxFuture<'_, QueryResult<T>> {
                Box::pin(async { Err(crate::error::QueryError::not_found("test")) })
            }

            fn execute_update<T: Model + crate::row::FromRow + Send + 'static>(
                &self,
                _sql: &str,
                _params: Vec<FilterValue>,
            ) -> BoxFuture<'_, QueryResult<Vec<T>>> {
                Box::pin(async { Ok(Vec::new()) })
            }

            fn execute_delete(
                &self,
                _sql: &str,
                _params: Vec<FilterValue>,
            ) -> BoxFuture<'_, QueryResult<u64>> {
                Box::pin(async { Ok(0) })
            }

            fn execute_raw(
                &self,
                _sql: &str,
                _params: Vec<FilterValue>,
            ) -> BoxFuture<'_, QueryResult<u64>> {
                Box::pin(async { Ok(0) })
            }

            fn count(
                &self,
                _sql: &str,
                _params: Vec<FilterValue>,
            ) -> BoxFuture<'_, QueryResult<u64>> {
                Box::pin(async { Ok(0) })
            }

            // Note: dialect() is NOT overridden - we're testing the default
        }

        let e = DefaultEngine;
        // If the default ever regresses back to a SQL-emitting dialect, this
        // test will fail because placeholder() won't panic.
        let _ = e.dialect().placeholder(1);
    }
}