Skip to main content

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}