Skip to main content

rainier_database/
lib.rs

1//! # rainier-database
2//!
3//! Rainier's data layer, built on **[Rainier ORM]** — the multi-dialect DBAL
4//! where one `#[derive(Entity)]` serves SQLite, Cloudflare D1, MySQL and
5//! Postgres, and where the stack of hand-written per-engine repositories
6//! collapses to zero.
7//!
8//! This crate adds the four things a framework needs on top of that:
9//!
10//! | | |
11//! |---|---|
12//! | [`Connection`] / [`Database`] | a `dyn`-safe port over Rainier ORM's `Executor`, so a backend can live in the IoC container |
13//! | [`Model`] | an entity the framework manages, with lifecycle **hooks** |
14//! | [`Repository`] / [`EntityRepository`] | a contract to depend on, implemented once for every model |
15//! | [`Criteria`] / [`Paginated`] | composable scopes and paging |
16//!
17//! ```ignore
18//! use rainier_orm::{repo, Entity, SeaOrmExecutor, PoolConfig};
19//! use rainier_database::{bind_executor, Criteria, Database, EntityRepository, Model, Repository};
20//!
21//! #[derive(Entity, Clone)]
22//! #[orm(table = "posts")]
23//! struct Post {
24//!     #[orm(pk, auto_increment)] id: u64,
25//!     title: String,
26//!     published: bool,
27//! }
28//! impl Model for Post {}
29//!
30//! bind_executor!(SeaOrmExecutor);
31//!
32//! # async fn run() -> rainier_support::Result<()> {
33//! let executor = SeaOrmExecutor::connect("sqlite::memory:", &PoolConfig::default()).await?;
34//! let db = Database::new(executor);
35//! let posts = EntityRepository::<Post>::new(db.clone());
36//!
37//! // Through the repository contract…
38//! let page = posts
39//!     .paginate_matching(Criteria::new().where_eq("published", true), 1, 20)
40//!     .await?;
41//!
42//! // …or straight through Rainier ORM, since `Database` is an `Executor`.
43//! let newest: Option<Post> = repo::query::<Post>().order_by_desc("id").first(&db).await?;
44//! # Ok(()) }
45//! ```
46//!
47//! ## The `Executor` problem, and how it is solved
48//!
49//! Rainier ORM's `Executor` uses `async fn` in trait and carries no `Send + Sync`
50//! bound — deliberately, so the same code runs in a single-threaded Cloudflare
51//! Worker over a `!Send` D1 binding. That makes it unusable as `dyn Executor`,
52//! which is exactly what a container needs to store.
53//!
54//! [`Database`] resolves this by holding an `Arc<dyn Connection>` (Rainier's
55//! `dyn`-safe mirror) and **re-implementing `Executor` on top of it**. The
56//! whole ORM surface therefore works against a `Database` unchanged — no
57//! wrapper per operation, and no gap between what Rainier ORM can do and what
58//! Rainier exposes. Register a concrete backend with
59//! [`bind_executor!`].
60//!
61//! [Rainier ORM]: https://github.com/safewords/rainier-framework/tree/main/crates/rainier-orm
62
63#![deny(missing_docs)]
64#![deny(rustdoc::broken_intra_doc_links)]
65
66pub mod connection;
67pub mod criteria;
68pub mod factory;
69pub mod migrator;
70pub mod model;
71pub mod pagination;
72pub mod raw;
73pub mod relation;
74pub mod repository;
75pub mod row;
76pub mod statement;
77pub mod testing;
78
79pub use connection::{Connection, Database};
80pub use criteria::{Constraint, Criteria};
81pub use factory::{Factory, HasFactory};
82pub use migrator::{Down, Migration, Migrator, Step};
83pub use model::{Created, Creating, Deleted, Deleting, Model, Updated, Updating};
84pub use pagination::Paginated;
85pub use raw::RawQuery;
86pub use relation::{
87    BelongsTo, BelongsToMany, HasMany, HasOne, PivotQuery, Related, RelatedCounts, Relation,
88    RelationKey,
89};
90pub use repository::{EntityRepository, Repository};
91pub use row::{Cell, ColumnRequest, OwnedRow};
92pub use statement::Prepared;
93
94// The ORM, re-exported so an application depends on one Rainier ORM version and
95// gets the derive, the query builder and the migrator without naming it twice.
96pub use rainier_orm;
97pub use rainier_orm::{
98    ddl, migrate, repo, schema, Action, Blueprint, ColumnKind, Dialect, Entity, Executor, IndexDef,
99    Query, TableChanges,
100};
101
102/// What [`bind_executor!`] expands to. Not a stable API.
103#[doc(hidden)]
104pub mod reexport {
105    pub use crate::row::{ColumnRequest, OwnedRow};
106    pub use rainier_orm::sea_query::Value;
107    pub use rainier_orm::{Dialect, ExecOutcome, Executor, Row, ShardRoute};
108    pub use rainier_support::{BoxFuture, Error, LocalBoxFuture, Result};
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use crate::testing::{fake_database, MemoryConnection};
115
116    #[derive(rainier_orm::Entity, Clone, Debug, PartialEq)]
117    #[orm(table = "users")]
118    struct User {
119        #[orm(pk, auto_increment)]
120        id: u64,
121        #[orm(unique)]
122        email: String,
123    }
124
125    impl Model for User {}
126
127    #[tokio::test]
128    async fn the_orm_surface_is_reachable_through_the_executor_impl() {
129        // Implementing `Executor` for `Database` keeps Rainier ORM's own API
130        // available: `repo::` and the query builder take it with no adapter.
131        // These futures are `Send` — see the assertion in
132        // `the_orms_own_futures_are_send_once_upstream_scopes_its_statements`
133        // below — so this path is usable from a request handler too.
134        let (db, connection) = fake_database(
135            MemoryConnection::new(Dialect::Sqlite)
136                .returning([OwnedRow::new().with("id", 1_u64).with("email", "a@b.c")]),
137        );
138
139        let found: Option<User> = repo::find_one_by(&db, "email", "a@b.c").await.unwrap();
140        assert_eq!(found.unwrap().email, "a@b.c");
141
142        let sql = connection.last_statement().unwrap();
143        assert!(sql.contains("users"), "{sql}");
144        assert!(sql.contains("email"), "{sql}");
145    }
146
147    #[tokio::test]
148    async fn the_query_builder_works_against_a_database() {
149        let (db, connection) = fake_database(MemoryConnection::new(Dialect::Sqlite));
150
151        let _: Vec<User> = repo::query::<User>()
152            .where_like("email", "%@example.com")
153            .order_by_desc("id")
154            .limit(5)
155            .all(&db)
156            .await
157            .unwrap();
158
159        let sql = connection.last_statement().unwrap();
160        assert!(sql.contains("LIKE"), "{sql}");
161        assert!(sql.contains("ORDER BY"), "{sql}");
162    }
163
164    #[tokio::test]
165    async fn the_repository_path_covers_the_same_ground_and_stays_send() {
166        let (db, connection) = fake_database(
167            MemoryConnection::new(Dialect::Sqlite)
168                .returning([OwnedRow::new().with("id", 1_u64).with("email", "a@b.c")]),
169        );
170        let users = EntityRepository::<User>::new(db);
171
172        let found = users.first_by("email", "a@b.c".into()).await.unwrap();
173        assert_eq!(found.unwrap().email, "a@b.c");
174        assert!(connection.last_statement().unwrap().contains("users"));
175    }
176
177    #[test]
178    fn the_orms_own_futures_are_send_once_upstream_scopes_its_statements() {
179        // The property `rainier-database::statement` exists to work around.
180        // While Rainier ORM holds a `sea_query` statement across its awaits
181        // these futures are `!Send` and this does not compile; once it does,
182        // the request path could call `repo::` directly and that module could
183        // be retired.
184        //
185        // Kept as a live check rather than a comment so the answer is never
186        // stale: it tracks whichever Rainier ORM the build resolves.
187        fn assert_send<T: Send>(_: T) {}
188
189        let (db, _) = fake_database(MemoryConnection::new(Dialect::Sqlite));
190        assert_send(async move {
191            let found: rainier_orm::Result<Option<User>> =
192                repo::find_one_by(&db, "email", "a@b.c").await;
193            let listed: rainier_orm::Result<Vec<User>> =
194                repo::query::<User>().where_eq("id", 1_i64).all(&db).await;
195            (found.is_ok(), listed.is_ok())
196        });
197    }
198
199    #[tokio::test]
200    async fn schema_ddl_renders_for_the_connections_dialect() {
201        let ddl = schema::create_table_ddl::<User>(Dialect::Sqlite);
202        assert!(ddl.contains("users"), "{ddl}");
203        assert!(ddl.contains("email"), "{ddl}");
204    }
205}