rainier-orm 2.0.0

Rainier's multi-dialect ORM: one #[derive(Entity)] across SQLite, MySQL, Postgres and D1.
Documentation
//! The [`Executor`] port — the one trait a backend implements to be usable.
//!
//! Everything above it (the repository, the schema builder, the derive) is
//! backend-agnostic; everything below it is backend-specific. An executor
//! reports its [`Dialect`] (so the layers above render the right SQL) and runs
//! two primitives: read rows, or run a write and report its outcome. That is
//! the entire surface a new backend must satisfy — sea-orm, D1-over-HTTP, and
//! a D1 native binding each implement just this.

use crate::{Dialect, Result, Row, ShardRoute};
use sea_query::Value;

/// The result of a write statement.
#[derive(Debug, Clone, Copy, Default)]
pub struct ExecOutcome {
    /// Rows inserted/updated/deleted.
    pub rows_affected: u64,
    /// The autogenerated key from an `INSERT`, where the backend reports one
    /// (`LAST_INSERT_ID()` / `last_insert_rowid()` / D1 `meta.last_row_id`).
    /// `0` when there was none.
    pub last_insert_id: i64,
}

/// A database backend. The single integration point: implement this and the
/// generic CRUD layer works against your store unchanged.
///
/// `async fn` in trait (stable since Rust 1.75) keeps this `dyn`-free and
/// wasm-safe; callers hold a concrete executor and monomorphise.
///
/// The trait deliberately does **not** require `Send + Sync`. A multi-threaded
/// server's concrete executors (`SeaOrmExecutor`, `D1Executor<reqwest-transport>`)
/// *are* `Send + Sync`, so state that holds them is too — the bound is provided
/// by the concrete type, not demanded by the trait. Leaving it off lets a
/// single-threaded Cloudflare Worker implement the trait over its `!Send`
/// native D1 binding, so the exact same generic services run there.
#[allow(async_fn_in_trait)]
pub trait Executor {
    /// The SQL dialect this backend speaks, so callers render matching SQL.
    fn dialect(&self) -> Dialect;

    /// The **shard family** this connector routes within, or `None` for a
    /// single, unsharded database.
    ///
    /// This is the explicit "am I sharded, and as what?" marker: a `Some(name)`
    /// connector uses sharded reasoning (ids are shard-encoded; operations
    /// route within the named family), while `None` is a plain one-database
    /// connector. Callers and migrations can branch on it without knowing the
    /// concrete executor — and an entity's [`shard_columns`](crate::Entity::shard_columns) should match
    /// the connector it runs on. Defaults to `None`.
    fn shard_family(&self) -> Option<&str> {
        None
    }

    /// `true` when this connector is sharded — sugar for
    /// `shard_family().is_some()`.
    fn is_sharded(&self) -> bool {
        self.shard_family().is_some()
    }

    /// Mint a shard-encoded primary key for a row whose shard-key value is
    /// `shard_key` (e.g. the owning `user_id`). `repo::insert` calls this when
    /// an entity's primary key is itself a shard key, is *not* auto-increment,
    /// and is unset — so id assignment for the sharded data tier happens **here,
    /// in the connector**, transparently, not in the service. The default is
    /// `None` (no allocation: the caller's id or the DB's auto-increment stands)
    /// so single, unsharded executors are unaffected.
    fn allocate_id(&self, _shard_key: u64) -> Option<u64> {
        None
    }

    /// Run a query and return every row. Parameters are bound positionally in
    /// the order sea-query emitted them.
    async fn fetch_all(&self, sql: &str, params: Vec<Value>) -> Result<Vec<Box<dyn Row>>>;

    /// Run a write and report its [`ExecOutcome`].
    async fn execute(&self, sql: &str, params: Vec<Value>) -> Result<ExecOutcome>;

    /// Run a query against the physical database the [`ShardRoute`] selects.
    ///
    /// **This is the sharding transparency seam.** The `repo`/`query` layer
    /// always calls the routed variants; a single-database executor inherits
    /// the default below — which *ignores* the route and runs on its one
    /// connection — so it needs no change and sharding stays invisible above.
    /// A sharding executor overrides this to dispatch to the right shard.
    async fn fetch_all_routed(
        &self,
        _route: ShardRoute,
        sql: &str,
        params: Vec<Value>,
    ) -> Result<Vec<Box<dyn Row>>> {
        self.fetch_all(sql, params).await
    }

    /// Routed counterpart of [`execute`](Self::execute). Defaults to ignoring
    /// the route — see [`fetch_all_routed`](Self::fetch_all_routed).
    async fn execute_routed(
        &self,
        _route: ShardRoute,
        sql: &str,
        params: Vec<Value>,
    ) -> Result<ExecOutcome> {
        self.execute(sql, params).await
    }
}