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    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}