prax_orm/client.rs
1//! Top-level Prax client grouping per-model accessors.
2//!
3//! A `PraxClient<E>` owns a `QueryEngine` and routes operations to the
4//! per-model `Client<E>` values emitted by `#[derive(Model)]` or
5//! `prax_schema!`. The `prax::client!(Foo, Bar, ...)` declarative macro
6//! attaches one accessor per model to `PraxClient`:
7//!
8//! ```rust,ignore
9//! use prax_orm::{client, Model, PraxClient};
10//!
11//! #[derive(Model)]
12//! #[prax(table = "users")]
13//! struct User { #[prax(id, auto)] id: i32, email: String }
14//!
15//! #[derive(Model)]
16//! #[prax(table = "posts")]
17//! struct Post { #[prax(id, auto)] id: i32, title: String }
18//!
19//! // Declares `trait PraxClientExt` with `user()`/`post()` accessors
20//! // and implements it for `PraxClient<E>`. Call site has the trait in
21//! // scope automatically because the macro emits it right there.
22//! client!(User, Post);
23//!
24//! # async fn go<E: prax_query::traits::QueryEngine>(engine: E) {
25//! let prax = PraxClient::new(engine);
26//! let _ = prax.user().find_many();
27//! let _ = prax.post().find_many();
28//! # }
29//! ```
30
31use prax_query::error::QueryResult;
32use prax_query::raw::Sql;
33use prax_query::row::FromRow;
34use prax_query::traits::{Model, QueryEngine};
35
36/// Top-level client grouping every model's per-model `Client<E>`.
37#[derive(Clone)]
38pub struct PraxClient<E: QueryEngine> {
39 engine: E,
40}
41
42impl<E: QueryEngine> PraxClient<E> {
43 /// Create a new top-level client wrapping the given engine.
44 pub fn new(engine: E) -> Self {
45 Self { engine }
46 }
47
48 /// Borrow the underlying engine. Accessor macros clone it per call.
49 pub fn engine(&self) -> &E {
50 &self.engine
51 }
52
53 /// Execute a typed raw SQL query, decoding each returned row as `T`.
54 ///
55 /// The typed Client API covers the common cases, but every ORM
56 /// eventually hits something it doesn't yet model — window functions,
57 /// vendor-specific extensions, recursive CTEs, bespoke aggregates.
58 /// `query_raw` is the escape hatch: build a parameterized
59 /// [`prax_query::raw::Sql`] and route the result through the same
60 /// [`FromRow`] bridge the derived models use, so the returned
61 /// records stay typed.
62 ///
63 /// `T` must implement both [`Model`] (so the driver can associate
64 /// the query with a table) and [`FromRow`] (so each row can be
65 /// decoded). Both are provided automatically by `#[derive(Model)]`.
66 ///
67 /// ```rust,ignore
68 /// use prax_query::raw::Sql;
69 ///
70 /// let users: Vec<User> = client
71 /// .query_raw(
72 /// Sql::new("SELECT id, email FROM users WHERE email = ")
73 /// .bind("alice@example.com"),
74 /// )
75 /// .await?;
76 /// ```
77 // QueryError is the workspace-wide error type; boxing it is a public API
78 // decision outside this function's scope.
79 #[allow(clippy::result_large_err)]
80 pub async fn query_raw<T>(&self, sql: Sql) -> QueryResult<Vec<T>>
81 where
82 T: Model + FromRow + Send + 'static,
83 {
84 let (s, p) = sql.build();
85 self.engine.query_many::<T>(&s, p).await
86 }
87
88 /// Execute a raw statement that doesn't return rows.
89 ///
90 /// Use this for `INSERT` / `UPDATE` / `DELETE` / DDL when the typed
91 /// Client API doesn't model what you need. Returns the
92 /// driver-reported affected-row count.
93 ///
94 /// ```rust,ignore
95 /// use prax_query::raw::Sql;
96 ///
97 /// let n = client
98 /// .execute_raw(
99 /// Sql::new("UPDATE users SET verified = TRUE WHERE id = ")
100 /// .bind(user_id),
101 /// )
102 /// .await?;
103 /// assert_eq!(n, 1);
104 /// ```
105 #[allow(clippy::result_large_err)]
106 pub async fn execute_raw(&self, sql: Sql) -> QueryResult<u64> {
107 let (s, p) = sql.build();
108 self.engine.execute_raw(&s, p).await
109 }
110
111 /// Run `f` inside a single database transaction.
112 ///
113 /// The closure receives a fresh `PraxClient<E>` whose engine is
114 /// bound to the in-flight transaction — every typed operation
115 /// emitted through that client (`tx.user().create()...`,
116 /// `tx.query_raw(...)`, etc.) routes through the same `BEGIN`
117 /// block. Returning `Ok(r)` commits and yields `Ok(r)` back;
118 /// returning `Err(e)` rolls back and surfaces `e` unchanged.
119 /// Panics bubble through and leave the connection to drop, which
120 /// aborts the transaction on the server.
121 ///
122 /// ```rust,ignore
123 /// use prax_query::error::QueryResult;
124 ///
125 /// let created = client
126 /// .transaction(|tx| async move {
127 /// let u = tx.user().create()
128 /// .set("email", "alice@example.com")
129 /// .exec().await?;
130 /// tx.post().create()
131 /// .set("author_id", u.id)
132 /// .set("title", "hello")
133 /// .exec().await?;
134 /// QueryResult::Ok(u)
135 /// })
136 /// .await?;
137 /// ```
138 ///
139 /// Nested `transaction()` calls on the same engine return
140 /// `QueryError::internal(...)` until dialect-aware SAVEPOINT
141 /// support lands.
142 #[allow(clippy::result_large_err)]
143 pub async fn transaction<R, Fut, F>(&self, f: F) -> QueryResult<R>
144 where
145 F: FnOnce(PraxClient<E>) -> Fut + Send + 'static,
146 Fut: std::future::Future<Output = QueryResult<R>> + Send + 'static,
147 R: Send + 'static,
148 {
149 self.engine
150 .transaction(move |tx_engine| async move { f(PraxClient::new(tx_engine)).await })
151 .await
152 }
153}
154
155/// Attach per-model accessors to `PraxClient<E>`.
156///
157/// Each identifier must name a model declared via `#[derive(Model)]` or
158/// `prax_schema!`. For each `Foo` the macro emits a sealed extension
159/// trait `PraxClientExt` with `fn foo(&self) -> foo::Client<E>` and
160/// implements it for `PraxClient<E>`.
161///
162/// The extension-trait detour exists because Rust's orphan rule bans
163/// downstream crates from writing inherent `impl` blocks for types they
164/// do not own — callers use `PraxClient` from `prax_orm`, so they must
165/// go through a trait. The `PraxClientExt` name is fixed; the trait is
166/// brought into scope at the call site by the macro.
167#[macro_export]
168macro_rules! client {
169 ($($model:ident),+ $(,)?) => {
170 /// Generated per-application extension trait on `PraxClient<E>`.
171 /// Calls like `client.user()` / `client.post()` dispatch through
172 /// this trait.
173 pub trait PraxClientExt<E: $crate::__prelude::QueryEngine> {
174 $( $crate::__client_accessor_trait!($model); )+
175 }
176
177 impl<E: $crate::__prelude::QueryEngine> PraxClientExt<E>
178 for $crate::PraxClient<E>
179 {
180 $( $crate::__client_accessor_impl!($model); )+
181 }
182 };
183}
184
185#[doc(hidden)]
186#[macro_export]
187macro_rules! __client_accessor_trait {
188 ($model:ident) => {
189 $crate::__paste::paste! {
190 fn [<$model:snake>](&self) -> [<$model:snake>]::Client<E>;
191 }
192 };
193}
194
195#[doc(hidden)]
196#[macro_export]
197macro_rules! __client_accessor_impl {
198 ($model:ident) => {
199 $crate::__paste::paste! {
200 fn [<$model:snake>](&self) -> [<$model:snake>]::Client<E> {
201 [<$model:snake>]::Client::new(self.engine().clone())
202 }
203 }
204 };
205}
206
207// `pastey` is a drop-in fork of the now-archived `paste` crate; its
208// `paste!` macro lives at the same name. Re-export under the existing
209// `__paste` symbol so the `client!`-macro expansions in this and
210// downstream crates keep compiling without a source change.
211#[doc(hidden)]
212pub use ::pastey as __paste;
213
214/// Re-exports used by the `client!` macro expansion. Keeps callers from
215/// needing to import `prax_query::traits::QueryEngine` themselves.
216#[doc(hidden)]
217pub mod __prelude {
218 pub use prax_query::traits::QueryEngine;
219}