Skip to main content

valence_backend_surreal/
bootstrap.rs

1//! Embedded Surreal router bootstrap — register logical names on a [`DatabaseRouter`].
2
3use std::collections::BTreeSet;
4use std::sync::Arc;
5
6use valence_core::backend::DatabaseBackend;
7use valence_core::error::Result;
8use valence_core::router::DatabaseRouter;
9use valence_core::router_key::router_key;
10
11use crate::embedded::{SDb, SurrealEmbeddedBackend, ENGINE_ID};
12
13#[cfg(feature = "inventory")]
14use crate::inventory::collect_distinct_embedded_surreal_logical_names;
15
16/// Options for embedded logical name registration.
17#[derive(Clone, Copy, Debug, Default)]
18pub struct RegisterEmbeddedLogicalNamesOptions {
19    /// When set, also register each logical name under this alternate engine id (host migration shim).
20    pub register_alias_engine_id: Option<&'static str>,
21}
22
23fn wrap_backend(db: SDb) -> Arc<dyn DatabaseBackend> {
24    let backend: Arc<dyn DatabaseBackend> = Arc::new(SurrealEmbeddedBackend::new(db));
25    #[cfg(feature = "instrumentation")]
26    {
27        valence_core::wrap_backend(backend)
28    }
29    #[cfg(not(feature = "instrumentation"))]
30    {
31        backend
32    }
33}
34
35fn register_backend_for_logical(
36    router: &mut DatabaseRouter,
37    logical_name: &str,
38    backend: Arc<dyn DatabaseBackend>,
39    options: RegisterEmbeddedLogicalNamesOptions,
40) {
41    let key = router_key(logical_name, ENGINE_ID);
42    router.register(key, Arc::clone(&backend));
43    if let Some(alias) = options.register_alias_engine_id {
44        let alias_key = router_key(logical_name, alias);
45        router.register(alias_key, backend);
46    }
47}
48
49/// Register one embedded Surreal handle under each logical name.
50pub fn register_embedded_logical_names(
51    router: &mut DatabaseRouter,
52    db: SDb,
53    logical_names: &[&str],
54    options: RegisterEmbeddedLogicalNamesOptions,
55) {
56    let backend = wrap_backend(db);
57    for &name in logical_names {
58        register_backend_for_logical(router, name, Arc::clone(&backend), options);
59    }
60}
61
62/// Register `db` under every distinct logical name in `groups` (deduplicated).
63pub fn register_embedded_logical_names_slices(
64    router: &mut DatabaseRouter,
65    db: SDb,
66    groups: &[&[&str]],
67    options: RegisterEmbeddedLogicalNamesOptions,
68) {
69    let mut seen = BTreeSet::<&str>::new();
70    for group in groups {
71        for &name in *group {
72            seen.insert(name);
73        }
74    }
75    let backend = wrap_backend(db);
76    for name in seen {
77        register_backend_for_logical(router, name, Arc::clone(&backend), options);
78    }
79}
80
81/// Register a distinct embedded Surreal handle under a single logical name.
82pub fn register_embedded_logical_handle(
83    router: &mut DatabaseRouter,
84    logical_name: &str,
85    db: SDb,
86    options: RegisterEmbeddedLogicalNamesOptions,
87) {
88    register_embedded_logical_handles(router, &[(logical_name, db)], options);
89}
90
91/// Register distinct embedded Surreal handles — one backend per `(logical_name, db)` pair.
92pub fn register_embedded_logical_handles(
93    router: &mut DatabaseRouter,
94    handles: &[(&str, SDb)],
95    options: RegisterEmbeddedLogicalNamesOptions,
96) {
97    let mut seen = BTreeSet::<&str>::new();
98    for &(name, ref db) in handles {
99        if !seen.insert(name) {
100            continue;
101        }
102        let backend = wrap_backend(db.clone());
103        register_backend_for_logical(router, name, backend, options);
104    }
105}
106
107#[cfg(feature = "inventory")]
108/// Register embedded logical names discovered from schema inventory.
109pub fn register_embedded_logical_names_from_inventory(
110    router: &mut DatabaseRouter,
111    db: SDb,
112    options: RegisterEmbeddedLogicalNamesOptions,
113) {
114    let names = collect_distinct_embedded_surreal_logical_names();
115    let refs: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
116    register_embedded_logical_names_slices(router, db, &[refs.as_slice()], options);
117}
118
119/// Build a router with embedded Surreal registered for each logical name.
120pub fn shared_router_with_embedded_logical_names(
121    db: SDb,
122    logical_names: &[&str],
123    options: RegisterEmbeddedLogicalNamesOptions,
124) -> Arc<DatabaseRouter> {
125    let mut router = DatabaseRouter::new();
126    register_embedded_logical_names(&mut router, db, logical_names, options);
127    Arc::new(router)
128}
129
130/// Bootstrap an embedded router from explicit logical names.
131///
132/// # Examples
133///
134/// ```
135/// # async fn example() -> valence_core::Result<()> {
136/// use valence_backend_surreal::{
137///     bootstrap_embedded_router, connect_embedded_at_path, EmbeddedEngine,
138///     RegisterEmbeddedLogicalNamesOptions, ENGINE_ID,
139/// };
140/// use valence_core::router_key;
141///
142/// let db = connect_embedded_at_path(EmbeddedEngine::Mem, "", "demo", "demo").await?;
143/// let router = bootstrap_embedded_router(
144///     db,
145///     &["default", "billing"],
146///     RegisterEmbeddedLogicalNamesOptions::default(),
147/// )?;
148/// assert!(router.resolve(&router_key("billing", ENGINE_ID)).is_ok());
149/// # Ok(())
150/// # }
151/// ```
152pub fn bootstrap_embedded_router(
153    db: SDb,
154    logical_names: &[&str],
155    options: RegisterEmbeddedLogicalNamesOptions,
156) -> Result<Arc<DatabaseRouter>> {
157    Ok(shared_router_with_embedded_logical_names(
158        db,
159        logical_names,
160        options,
161    ))
162}
163
164#[cfg(feature = "inventory")]
165/// Bootstrap an embedded router using schema inventory discovery.
166pub fn bootstrap_embedded_router_from_inventory(
167    db: SDb,
168    options: RegisterEmbeddedLogicalNamesOptions,
169) -> Result<Arc<DatabaseRouter>> {
170    let mut router = DatabaseRouter::new();
171    register_embedded_logical_names_from_inventory(&mut router, db, options);
172    Ok(Arc::new(router))
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use surrealdb::engine::local::Mem;
179
180    async fn mem_db() -> SDb {
181        let db = SDb::init();
182        db.connect::<Mem>(()).await.unwrap();
183        db.use_ns("test").use_db("test").await.unwrap();
184        db
185    }
186
187    #[tokio::test]
188    async fn register_multiple_logical_names_shares_backend() {
189        let db = mem_db().await;
190        let mut router = DatabaseRouter::new();
191        register_embedded_logical_names(
192            &mut router,
193            db,
194            &["default", "billing"],
195            RegisterEmbeddedLogicalNamesOptions::default(),
196        );
197        assert_eq!(router.len().unwrap(), 2);
198        let k1 = router_key("default", ENGINE_ID);
199        let k2 = router_key("billing", ENGINE_ID);
200        assert!(router.resolve(&k1).is_ok());
201        assert!(router.resolve(&k2).is_ok());
202    }
203
204    #[tokio::test]
205    async fn alias_engine_registers_second_key() {
206        let db = mem_db().await;
207        let mut router = DatabaseRouter::new();
208        register_embedded_logical_names(
209            &mut router,
210            db,
211            &["default"],
212            RegisterEmbeddedLogicalNamesOptions {
213                register_alias_engine_id: Some("legacy_surreal"),
214            },
215        );
216        assert_eq!(router.len().unwrap(), 2);
217    }
218}