Skip to main content

rainier_orm/
route.rs

1//! Shard routing — the per-query decision of *which physical database* a
2//! generic operation targets.
3//!
4//! Routing is derived in the [`repo`](crate::repo)/[`query`](crate::query)
5//! layer from an entity's shard metadata and passed to the executor as a
6//! [`ShardRoute`]. Single-database executors ignore it (the default trait
7//! methods), so the *same* service code runs against one database or many —
8//! the consumer never threads a shard key, and switching a sharded backend for
9//! a single one (e.g. D1 → MySQL) is a config change, not a code change.
10//!
11//! The routing key is a **shard-encoded id**: the physical shard number lives
12//! in the high bits of a `u64` id ([`ShardCodec`]), so any id self-describes
13//! its shard and routing needs no directory round-trip.
14
15use sea_query::Value;
16
17/// Where a single operation should run.
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum ShardRoute {
20    /// A non-sharded (global) entity, or a single-database backend: no routing.
21    Global,
22    /// Route by this shard-key value — a shard-encoded id the executor decodes
23    /// to a physical shard.
24    Key(u64),
25}
26
27/// Packs a physical shard number into the high bits of a `u64` id, so any id
28/// (primary key or a shard-encoded foreign key) self-describes its shard.
29///
30/// With `shard_bits = 12`, ids carry a 12-bit shard (up to 4096 shards) and a
31/// 52-bit per-shard sequence — ample for app-generated ids. Auto-increment is
32/// not used for sharded entities (it can't carry the shard); the app composes
33/// the id with [`compose`](Self::compose).
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub struct ShardCodec {
36    shard_bits: u32,
37}
38
39impl ShardCodec {
40    /// A sensible default: 12 shard bits (up to 4096 shards) + 52 sequence bits.
41    /// Surfaces that don't need a custom layout can share this so their id
42    /// schemes agree (and stay portable between a single-DB and a fleet).
43    pub const DEFAULT: ShardCodec = ShardCodec::new(12);
44
45    pub const fn new(shard_bits: u32) -> Self {
46        Self { shard_bits }
47    }
48    /// Bits available for the per-shard sequence.
49    pub const fn seq_bits(&self) -> u32 {
50        64 - self.shard_bits
51    }
52    /// The shard number encoded in `id`.
53    pub const fn shard_of(&self, id: u64) -> u32 {
54        (id >> self.seq_bits()) as u32
55    }
56    /// The per-shard local part of `id`.
57    pub const fn local_of(&self, id: u64) -> u64 {
58        id & ((1u64 << self.seq_bits()) - 1)
59    }
60    /// Compose a full id from a shard number and a per-shard local sequence.
61    pub const fn compose(&self, shard: u32, local: u64) -> u64 {
62        ((shard as u64) << self.seq_bits()) | (local & ((1u64 << self.seq_bits()) - 1))
63    }
64}
65
66/// Stable 64-bit hash (FNV-1a) — deterministic across processes/builds, unlike
67/// `std`'s randomized hasher, so a string shard key always routes the same way.
68pub fn stable_hash(bytes: &[u8]) -> u64 {
69    let mut h = 0xcbf2_9ce4_8422_2325u64;
70    for &b in bytes {
71        h ^= b as u64;
72        h = h.wrapping_mul(0x0000_0100_0000_01b3);
73    }
74    h
75}
76
77/// Reduce a bound shard-key value to a `u64` routing key: a numeric id is taken
78/// as-is (the high bits hold the shard under [`ShardCodec`]); a string (e.g. a
79/// hashed-email directory key) is `stable_hash`ed. The locator then maps the
80/// routing key to a physical shard.
81pub(crate) fn routing_key(v: &Value) -> Option<u64> {
82    use sea_query::Value as V;
83    Some(match v {
84        V::TinyInt(Some(n)) => *n as u64,
85        V::SmallInt(Some(n)) => *n as u64,
86        V::Int(Some(n)) => *n as u64,
87        V::BigInt(Some(n)) => *n as u64,
88        V::TinyUnsigned(Some(n)) => *n as u64,
89        V::SmallUnsigned(Some(n)) => *n as u64,
90        V::Unsigned(Some(n)) => *n as u64,
91        V::BigUnsigned(Some(n)) => *n,
92        V::String(Some(s)) => stable_hash(s.as_bytes()),
93        V::Char(Some(c)) => stable_hash(c.encode_utf8(&mut [0u8; 4]).as_bytes()),
94        _ => return None,
95    })
96}
97
98/// The route for constraining `column` to `value` on entity `E`: `Key` when
99/// `column` is one of `E`'s shard-encoded columns, else `Global`.
100pub(crate) fn route_for<E: crate::Entity>(column: &str, value: &Value) -> ShardRoute {
101    if E::shard_columns().contains(&column) {
102        if let Some(k) = routing_key(value) {
103            return ShardRoute::Key(k);
104        }
105    }
106    ShardRoute::Global
107}