Skip to main content

rainier_orm/
executor.rs

1//! The [`Executor`] port — the one trait a backend implements to be usable.
2//!
3//! Everything above it (the repository, the schema builder, the derive) is
4//! backend-agnostic; everything below it is backend-specific. An executor
5//! reports its [`Dialect`] (so the layers above render the right SQL) and runs
6//! two primitives: read rows, or run a write and report its outcome. That is
7//! the entire surface a new backend must satisfy — sea-orm, D1-over-HTTP, and
8//! a D1 native binding each implement just this.
9
10use crate::{Dialect, Result, Row, ShardRoute};
11use sea_query::Value;
12
13/// The result of a write statement.
14#[derive(Debug, Clone, Copy, Default)]
15pub struct ExecOutcome {
16    /// Rows inserted/updated/deleted.
17    pub rows_affected: u64,
18    /// The autogenerated key from an `INSERT`, where the backend reports one
19    /// (`LAST_INSERT_ID()` / `last_insert_rowid()` / D1 `meta.last_row_id`).
20    /// `0` when there was none.
21    pub last_insert_id: i64,
22}
23
24/// A database backend. The single integration point: implement this and the
25/// generic CRUD layer works against your store unchanged.
26///
27/// `async fn` in trait (stable since Rust 1.75) keeps this `dyn`-free and
28/// wasm-safe; callers hold a concrete executor and monomorphise.
29///
30/// The trait deliberately does **not** require `Send + Sync`. A multi-threaded
31/// server's concrete executors (`SeaOrmExecutor`, `D1Executor<reqwest-transport>`)
32/// *are* `Send + Sync`, so state that holds them is too — the bound is provided
33/// by the concrete type, not demanded by the trait. Leaving it off lets a
34/// single-threaded Cloudflare Worker implement the trait over its `!Send`
35/// native D1 binding, so the exact same generic services run there.
36#[allow(async_fn_in_trait)]
37pub trait Executor {
38    /// The SQL dialect this backend speaks, so callers render matching SQL.
39    fn dialect(&self) -> Dialect;
40
41    /// The **shard family** this connector routes within, or `None` for a
42    /// single, unsharded database.
43    ///
44    /// This is the explicit "am I sharded, and as what?" marker: a `Some(name)`
45    /// connector uses sharded reasoning (ids are shard-encoded; operations
46    /// route within the named family), while `None` is a plain one-database
47    /// connector. Callers and migrations can branch on it without knowing the
48    /// concrete executor — and an entity's [`shard_columns`](crate::Entity::shard_columns) should match
49    /// the connector it runs on. Defaults to `None`.
50    fn shard_family(&self) -> Option<&str> {
51        None
52    }
53
54    /// `true` when this connector is sharded — sugar for
55    /// `shard_family().is_some()`.
56    fn is_sharded(&self) -> bool {
57        self.shard_family().is_some()
58    }
59
60    /// Mint a shard-encoded primary key for a row whose shard-key value is
61    /// `shard_key` (e.g. the owning `user_id`). `repo::insert` calls this when
62    /// an entity's primary key is itself a shard key, is *not* auto-increment,
63    /// and is unset — so id assignment for the sharded data tier happens **here,
64    /// in the connector**, transparently, not in the service. The default is
65    /// `None` (no allocation: the caller's id or the DB's auto-increment stands)
66    /// so single, unsharded executors are unaffected.
67    fn allocate_id(&self, _shard_key: u64) -> Option<u64> {
68        None
69    }
70
71    /// Run a query and return every row. Parameters are bound positionally in
72    /// the order sea-query emitted them.
73    async fn fetch_all(&self, sql: &str, params: Vec<Value>) -> Result<Vec<Box<dyn Row>>>;
74
75    /// Run a write and report its [`ExecOutcome`].
76    async fn execute(&self, sql: &str, params: Vec<Value>) -> Result<ExecOutcome>;
77
78    /// Run a query against the physical database the [`ShardRoute`] selects.
79    ///
80    /// **This is the sharding transparency seam.** The `repo`/`query` layer
81    /// always calls the routed variants; a single-database executor inherits
82    /// the default below — which *ignores* the route and runs on its one
83    /// connection — so it needs no change and sharding stays invisible above.
84    /// A sharding executor overrides this to dispatch to the right shard.
85    async fn fetch_all_routed(
86        &self,
87        _route: ShardRoute,
88        sql: &str,
89        params: Vec<Value>,
90    ) -> Result<Vec<Box<dyn Row>>> {
91        self.fetch_all(sql, params).await
92    }
93
94    /// Routed counterpart of [`execute`](Self::execute). Defaults to ignoring
95    /// the route — see [`fetch_all_routed`](Self::fetch_all_routed).
96    async fn execute_routed(
97        &self,
98        _route: ShardRoute,
99        sql: &str,
100        params: Vec<Value>,
101    ) -> Result<ExecOutcome> {
102        self.execute(sql, params).await
103    }
104}