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 pub async fn query_raw<T>(&self, sql: Sql) -> QueryResult<Vec<T>>
78 where
79 T: Model + FromRow + Send + 'static,
80 {
81 let (s, p) = sql.build();
82 self.engine.query_many::<T>(&s, p).await
83 }
84
85 /// Execute a raw statement that doesn't return rows.
86 ///
87 /// Use this for `INSERT` / `UPDATE` / `DELETE` / DDL when the typed
88 /// Client API doesn't model what you need. Returns the
89 /// driver-reported affected-row count.
90 ///
91 /// ```rust,ignore
92 /// use prax_query::raw::Sql;
93 ///
94 /// let n = client
95 /// .execute_raw(
96 /// Sql::new("UPDATE users SET verified = TRUE WHERE id = ")
97 /// .bind(user_id),
98 /// )
99 /// .await?;
100 /// assert_eq!(n, 1);
101 /// ```
102 pub async fn execute_raw(&self, sql: Sql) -> QueryResult<u64> {
103 let (s, p) = sql.build();
104 self.engine.execute_raw(&s, p).await
105 }
106
107 /// Run `f` inside a single database transaction.
108 ///
109 /// The closure receives a fresh `PraxClient<E>` whose engine is
110 /// bound to the in-flight transaction — every typed operation
111 /// emitted through that client (`tx.user().create()...`,
112 /// `tx.query_raw(...)`, etc.) routes through the same `BEGIN`
113 /// block. Returning `Ok(r)` commits and yields `Ok(r)` back;
114 /// returning `Err(e)` rolls back and surfaces `e` unchanged.
115 /// Panics bubble through and leave the connection to drop, which
116 /// aborts the transaction on the server.
117 ///
118 /// ```rust,ignore
119 /// use prax_query::error::QueryResult;
120 ///
121 /// let created = client
122 /// .transaction(|tx| async move {
123 /// let u = tx.user().create()
124 /// .set("email", "alice@example.com")
125 /// .exec().await?;
126 /// tx.post().create()
127 /// .set("author_id", u.id)
128 /// .set("title", "hello")
129 /// .exec().await?;
130 /// QueryResult::Ok(u)
131 /// })
132 /// .await?;
133 /// ```
134 ///
135 /// Nested `transaction()` calls on the same engine return
136 /// `QueryError::internal(...)` until dialect-aware SAVEPOINT
137 /// support lands.
138 pub async fn transaction<R, Fut, F>(&self, f: F) -> QueryResult<R>
139 where
140 F: FnOnce(PraxClient<E>) -> Fut + Send + 'static,
141 Fut: std::future::Future<Output = QueryResult<R>> + Send + 'static,
142 R: Send + 'static,
143 {
144 self.engine
145 .transaction(move |tx_engine| async move { f(PraxClient::new(tx_engine)).await })
146 .await
147 }
148}
149
150/// Attach per-model accessors to `PraxClient<E>`.
151///
152/// Each identifier must name a model declared via `#[derive(Model)]` or
153/// `prax_schema!`. For each `Foo` the macro emits a sealed extension
154/// trait `PraxClientExt` with `fn foo(&self) -> foo::Client<E>` and
155/// implements it for `PraxClient<E>`.
156///
157/// The extension-trait detour exists because Rust's orphan rule bans
158/// downstream crates from writing inherent `impl` blocks for types they
159/// do not own — callers use `PraxClient` from `prax_orm`, so they must
160/// go through a trait. The `PraxClientExt` name is fixed; the trait is
161/// brought into scope at the call site by the macro.
162#[macro_export]
163macro_rules! client {
164 ($($model:ident),+ $(,)?) => {
165 /// Generated per-application extension trait on `PraxClient<E>`.
166 /// Calls like `client.user()` / `client.post()` dispatch through
167 /// this trait.
168 pub trait PraxClientExt<E: $crate::__prelude::QueryEngine> {
169 $( $crate::__client_accessor_trait!($model); )+
170 }
171
172 impl<E: $crate::__prelude::QueryEngine> PraxClientExt<E>
173 for $crate::PraxClient<E>
174 {
175 $( $crate::__client_accessor_impl!($model); )+
176 }
177 };
178}
179
180#[doc(hidden)]
181#[macro_export]
182macro_rules! __client_accessor_trait {
183 ($model:ident) => {
184 $crate::__paste::paste! {
185 fn [<$model:snake>](&self) -> [<$model:snake>]::Client<E>;
186 }
187 };
188}
189
190#[doc(hidden)]
191#[macro_export]
192macro_rules! __client_accessor_impl {
193 ($model:ident) => {
194 $crate::__paste::paste! {
195 fn [<$model:snake>](&self) -> [<$model:snake>]::Client<E> {
196 [<$model:snake>]::Client::new(self.engine().clone())
197 }
198 }
199 };
200}
201
202// `pastey` is a drop-in fork of the now-archived `paste` crate; its
203// `paste!` macro lives at the same name. Re-export under the existing
204// `__paste` symbol so the `client!`-macro expansions in this and
205// downstream crates keep compiling without a source change.
206#[doc(hidden)]
207pub use ::pastey as __paste;
208
209/// Re-exports used by the `client!` macro expansion. Keeps callers from
210/// needing to import `prax_query::traits::QueryEngine` themselves.
211#[doc(hidden)]
212pub mod __prelude {
213 pub use prax_query::traits::QueryEngine;
214}