Skip to main content

valence_backend_mongodb/
backend.rs

1//! MongoDB wire [`DatabaseBackend`] using the official Rust driver.
2
3use std::collections::HashSet;
4use std::sync::Arc;
5
6use futures::TryStreamExt;
7use mongodb::bson::{doc, Document};
8use mongodb::options::IndexOptions;
9use mongodb::{Client, Collection, IndexModel};
10use serde_json::{Map, Value};
11
12use valence_core::{
13    BackendCapabilities, CompiledQuery, Database, DatabaseBackend, DatabaseFromEngine, Error,
14    KnownEngines, RecordId, Result,
15};
16
17use crate::config::MongoConfig;
18
19/// Stable engine slug for router keys (`mongodb:logical_name`).
20pub const ENGINE_ID: &str = KnownEngines::MONGODB;
21
22/// Schema evaluator const for `database:` routing.
23pub const PRIMARY: DatabaseFromEngine = Database::from_engine("primary", ENGINE_ID);
24
25const EDGES_COLLECTION: &str = "valence_edges";
26
27/// MongoDB-backed [`DatabaseBackend`] storing JSON documents per collection.
28///
29/// # Examples
30///
31/// ```ignore
32/// use std::sync::Arc;
33/// use valence::{
34///     valence_schema, Database, DatabaseFromEngine, FieldType, MongoBackend, Valence,
35///     MONGODB_ENGINE_ID,
36/// };
37///
38/// const COUNTER_DB: DatabaseFromEngine =
39///     Database::from_engine("default", MONGODB_ENGINE_ID);
40///
41/// valence_schema! {
42///     Counter {
43///         table: "counter",
44///         version: "0.1.0",
45///         database: COUNTER_DB,
46///         fields: [
47///             id: { r#type: FieldType::String, primary_key: true, required: true },
48///             value: { r#type: FieldType::Integer, required: true },
49///         ],
50///     }
51/// }
52///
53/// // Reads VALENCE_MONGODB_URI and optional VALENCE_MONGODB_DATABASE.
54/// let backend = MongoBackend::from_env().await?;
55/// let valence = Valence::builder()
56///     .add_backend("default", Arc::new(backend))
57///     .build()?;
58/// assert_eq!(
59///     valence.backend_for_table("counter")?.engine_id(),
60///     MONGODB_ENGINE_ID
61/// );
62/// # Ok::<(), valence::Error>(())
63/// ```
64#[derive(Clone)]
65pub struct MongoBackend {
66    client: Client,
67    database: String,
68    unique_fields: Arc<tokio::sync::RwLock<HashSet<(String, String)>>>,
69}
70
71impl std::fmt::Debug for MongoBackend {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        f.debug_struct("MongoBackend")
74            .field("database", &self.database)
75            .finish_non_exhaustive()
76    }
77}
78
79impl MongoBackend {
80    /// Start a builder for explicit host wiring.
81    pub fn builder() -> crate::config::MongoBackendBuilder {
82        crate::config::MongoBackendBuilder::new()
83    }
84
85    /// Connect using env defaults via builder (shorthand).
86    pub async fn from_env() -> Result<Self> {
87        Self::builder().from_env_defaults().build().await
88    }
89
90    /// Connect to MongoDB at `uri` using database `database`.
91    pub async fn connect(uri: &str, database: &str) -> Result<Self> {
92        Self::builder().uri(uri).database(database).build().await
93    }
94
95    /// Connect using explicit config.
96    pub async fn connect_with_config(config: MongoConfig) -> Result<Self> {
97        let client = Client::with_uri_str(&config.uri)
98            .await
99            .map_err(|e| Error::Database(e.to_string()))?;
100        let backend = Self {
101            client,
102            database: config.database,
103            unique_fields: Arc::new(tokio::sync::RwLock::new(HashSet::new())),
104        };
105        backend.ensure_edges_collection().await?;
106        Ok(backend)
107    }
108
109    fn assert_safe_table(table: &str) -> Result<()> {
110        if table.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
111            Ok(())
112        } else {
113            Err(Error::Validation(format!("unsafe table name: {table}")))
114        }
115    }
116
117    fn collection(&self, table: &str) -> Collection<Document> {
118        self.client.database(&self.database).collection(table)
119    }
120
121    async fn ensure_edges_collection(&self) -> Result<()> {
122        let coll = self.collection(EDGES_COLLECTION);
123        let index = IndexModel::builder()
124            .keys(doc! {
125                "from_table": 1,
126                "from_id": 1,
127                "edge_type": 1,
128                "to_table": 1,
129                "to_id": 1,
130            })
131            .options(IndexOptions::builder().unique(true).build())
132            .build();
133        coll.create_index(index)
134            .await
135            .map_err(|e| Error::Database(e.to_string()))?;
136        Ok(())
137    }
138
139    async fn unique_fields_for(&self, table: &str) -> Vec<String> {
140        self.unique_fields
141            .read()
142            .await
143            .iter()
144            .filter(|(t, _)| t == table)
145            .map(|(_, f)| f.clone())
146            .collect()
147    }
148
149    async fn check_unique_fields(
150        &self,
151        table: &str,
152        record: &Value,
153        exclude_id: Option<&str>,
154    ) -> Result<()> {
155        for field in self.unique_fields_for(table).await {
156            let Some(value) = record.get(&field).and_then(|v| v.as_str()) else {
157                continue;
158            };
159            if let Some(exclude) = exclude_id {
160                if let Ok(Some(row)) = self.get_record(table, exclude).await {
161                    if row.get(&field).and_then(|v| v.as_str()) == Some(value) {
162                        continue;
163                    }
164                }
165            }
166            let coll = self.collection(table);
167            let filter = doc! { field.as_str(): value };
168            if let Some(existing) = coll
169                .find_one(filter)
170                .await
171                .map_err(|e| Error::Database(e.to_string()))?
172            {
173                let existing_id = existing.get_str("_id").unwrap_or("");
174                if exclude_id != Some(existing_id) {
175                    return Err(Error::Database(format!(
176                        "duplicate unique index value for {table}.{field}"
177                    )));
178                }
179            }
180        }
181        Ok(())
182    }
183
184    fn value_to_bson(value: &Value) -> mongodb::bson::Bson {
185        serde_json::from_value(value.clone()).unwrap_or(mongodb::bson::Bson::Null)
186    }
187
188    fn doc_to_row(table: &str, id: &str, doc: Document) -> Value {
189        let mut map = Map::new();
190        for (k, v) in doc {
191            if k == "_id" {
192                continue;
193            }
194            map.insert(k, bson_to_json(v));
195        }
196        row_from_body(table, id, Value::Object(map))
197    }
198
199    async fn rows_for_table(&self, table: &str, limit: Option<usize>) -> Result<Vec<Value>> {
200        Self::assert_safe_table(table)?;
201        let coll = self.collection(table);
202        let mut cursor = coll
203            .find(doc! {})
204            .await
205            .map_err(|e| Error::Database(e.to_string()))?;
206        let mut rows = Vec::new();
207        while let Some(doc) = cursor
208            .try_next()
209            .await
210            .map_err(|e| Error::Database(e.to_string()))?
211        {
212            let id = doc.get_str("_id").unwrap_or("").to_string();
213            rows.push(Self::doc_to_row(table, &id, doc));
214            if limit.is_some_and(|n| rows.len() >= n) {
215                break;
216            }
217        }
218        Ok(rows)
219    }
220}
221
222#[async_trait::async_trait]
223impl DatabaseBackend for MongoBackend {
224    fn engine_id(&self) -> &'static str {
225        ENGINE_ID
226    }
227
228    fn capabilities(&self) -> BackendCapabilities {
229        BackendCapabilities {
230            supports_merge: true,
231            supports_graph_edges: true,
232            telemetry_label: "mongodb",
233        }
234    }
235
236    async fn execute_compiled_query(&self, compiled: &CompiledQuery) -> Result<Vec<Value>> {
237        let q = compiled.query_string.trim();
238        if let Ok(descriptor) = serde_json::from_str::<Value>(q) {
239            if let Some(collection) = descriptor.get("collection").and_then(|v| v.as_str()) {
240                let limit = descriptor
241                    .get("limit")
242                    .and_then(|v| v.as_u64())
243                    .map(|n| n as usize);
244                let mut rows = self.rows_for_table(collection, limit).await?;
245                rows = valence_core::query::apply_equality_where(rows, compiled);
246                rows = valence_core::query::apply_order_limit_offset(rows, &compiled.query_string);
247                return Ok(rows);
248            }
249        }
250
251        let upper = q.to_uppercase();
252        if !upper.starts_with("SELECT ") {
253            return Ok(vec![]);
254        }
255        let from_idx = upper
256            .find(" FROM ")
257            .ok_or_else(|| Error::Internal("missing FROM in select".into()))?;
258        let table = q[from_idx + 6..]
259            .split_whitespace()
260            .next()
261            .unwrap_or("")
262            .trim();
263        if table.is_empty() {
264            return Ok(vec![]);
265        }
266        // Load all candidates; WHERE / ORDER / LIMIT applied in-process (parity with mem).
267        let mut rows = self.rows_for_table(table, None).await?;
268        rows = valence_core::query::apply_equality_where(rows, compiled);
269        rows = valence_core::query::apply_order_limit_offset(rows, &compiled.query_string);
270        if upper.contains("SELECT ID") && !upper.contains("BODY") {
271            // Match mem: IdOnlyRecord deserializes `{ "id": ... }`, not bare strings.
272            return Ok(rows
273                .iter()
274                .filter_map(|r| {
275                    r.get("id")
276                        .and_then(|id| id.get("id").and_then(|x| x.as_str()))
277                        .or_else(|| r.get("id").and_then(|id| id.as_str()))
278                        .map(|id| serde_json::json!({ "id": id }))
279                })
280                .collect());
281        }
282        Ok(rows)
283    }
284
285    async fn ensure_schemaless_table(&self, _table: &str) -> Result<()> {
286        Ok(())
287    }
288
289    async fn get_record(&self, table: &str, id: &str) -> Result<Option<Value>> {
290        Self::assert_safe_table(table)?;
291        let coll = self.collection(table);
292        let doc = coll
293            .find_one(doc! { "_id": id })
294            .await
295            .map_err(|e| Error::Database(e.to_string()))?;
296        Ok(doc.map(|d| Self::doc_to_row(table, id, d)))
297    }
298
299    async fn create_record(&self, table: &str, content: Value) -> Result<Value> {
300        Self::assert_safe_table(table)?;
301        self.check_unique_fields(table, &content, None).await?;
302        let id = storage_id(&content).unwrap_or_else(uuid_simple);
303        let mut record = content;
304        if let Some(obj) = record.as_object_mut() {
305            let has_string_id = obj.get("id").and_then(|v| v.as_str()).is_some();
306            if !has_string_id {
307                obj.insert("id".into(), record_id_json(table, &id));
308            }
309        }
310        let mut doc = body_document(&record);
311        doc.insert("_id", id.clone());
312        let coll = self.collection(table);
313        coll.insert_one(doc).await.map_err(map_duplicate_key)?;
314        Ok(record)
315    }
316
317    async fn update_record(&self, table: &str, id: &str, content: Value) -> Result<Value> {
318        if self.get_record(table, id).await?.is_none() {
319            return Err(Error::NotFound(format!("{table}:{id}")));
320        }
321        self.check_unique_fields(table, &content, Some(id)).await?;
322        let mut record = content;
323        if let Some(obj) = record.as_object_mut() {
324            obj.insert("id".into(), record_id_json(table, id));
325        }
326        let doc = body_document(&record);
327        let coll = self.collection(table);
328        coll.replace_one(doc! { "_id": id }, doc)
329            .await
330            .map_err(map_duplicate_key)?;
331        Ok(record)
332    }
333
334    async fn merge_record(&self, table: &str, id: &str, patch: Value) -> Result<Value> {
335        let existing = self
336            .get_record(table, id)
337            .await?
338            .unwrap_or_else(|| row_from_body(table, id, Value::Object(Map::new())));
339        let mut merged = existing;
340        if let (Some(base), Some(patch_obj)) = (merged.as_object_mut(), patch.as_object()) {
341            for (k, v) in patch_obj {
342                base.insert(k.clone(), v.clone());
343            }
344        }
345        self.check_unique_fields(table, &merged, Some(id)).await?;
346        let doc = body_document(&merged);
347        let coll = self.collection(table);
348        coll.replace_one(doc! { "_id": id }, doc)
349            .await
350            .map_err(|e| {
351                if e.to_string().contains("duplicate key") {
352                    Error::Database(format!("duplicate unique index value for {table}"))
353                } else {
354                    Error::Database(e.to_string())
355                }
356            })?;
357        Ok(merged)
358    }
359
360    async fn upsert_record(&self, table: &str, id: &str, content: Value) -> Result<Value> {
361        if self.get_record(table, id).await?.is_some() {
362            self.update_record(table, id, content).await
363        } else {
364            let mut record = content;
365            if let Some(obj) = record.as_object_mut() {
366                obj.insert("id".into(), record_id_json(table, id));
367            }
368            self.create_record(table, record).await
369        }
370    }
371
372    async fn delete_record(&self, table: &str, id: &str) -> Result<()> {
373        let coll = self.collection(table);
374        coll.delete_one(doc! { "_id": id })
375            .await
376            .map_err(|e| Error::Database(e.to_string()))?;
377        Ok(())
378    }
379
380    async fn relate_edge(&self, from: &RecordId, edge_table: &str, to: &RecordId) -> Result<()> {
381        let coll = self.collection(EDGES_COLLECTION);
382        coll.insert_one(doc! {
383            "from_table": from.table(),
384            "from_id": from.id(),
385            "edge_type": edge_table,
386            "to_table": to.table(),
387            "to_id": to.id(),
388        })
389        .await
390        .map_err(|e| Error::Database(e.to_string()))?;
391        Ok(())
392    }
393
394    async fn unrelate_edge(&self, from: &RecordId, edge_table: &str, to: &RecordId) -> Result<()> {
395        let coll = self.collection(EDGES_COLLECTION);
396        coll.delete_one(doc! {
397            "from_table": from.table(),
398            "from_id": from.id(),
399            "edge_type": edge_table,
400            "to_table": to.table(),
401            "to_id": to.id(),
402        })
403        .await
404        .map_err(|e| Error::Database(e.to_string()))?;
405        Ok(())
406    }
407
408    async fn get_edge_targets(&self, from: &RecordId, edge_table: &str) -> Result<Vec<RecordId>> {
409        let coll = self.collection(EDGES_COLLECTION);
410        let mut cursor = coll
411            .find(doc! {
412                "from_table": from.table(),
413                "from_id": from.id(),
414                "edge_type": edge_table,
415            })
416            .await
417            .map_err(|e| Error::Database(e.to_string()))?;
418        let mut out = Vec::new();
419        while let Some(edge) = cursor
420            .try_next()
421            .await
422            .map_err(|e| Error::Database(e.to_string()))?
423        {
424            let to_table = edge.get_str("to_table").unwrap_or("").to_string();
425            let to_id = edge.get_str("to_id").unwrap_or("").to_string();
426            out.push(RecordId::new(to_table, to_id));
427        }
428        Ok(out)
429    }
430
431    async fn define_unique_index(&self, table: &str, field: &str) -> Result<()> {
432        Self::assert_safe_table(table)?;
433        self.unique_fields
434            .write()
435            .await
436            .insert((table.to_string(), field.to_string()));
437        let coll = self.collection(table);
438        let index = IndexModel::builder()
439            .keys(doc! { field: 1 })
440            .options(IndexOptions::builder().unique(true).sparse(true).build())
441            .build();
442        coll.create_index(index)
443            .await
444            .map_err(|e| Error::Database(e.to_string()))?;
445        Ok(())
446    }
447}
448
449fn map_duplicate_key(e: mongodb::error::Error) -> Error {
450    if e.to_string().contains("duplicate key") {
451        Error::Database("duplicate unique index value".into())
452    } else {
453        Error::Database(e.to_string())
454    }
455}
456
457fn body_document(record: &Value) -> Document {
458    let mut doc = Document::new();
459    if let Some(obj) = record.as_object() {
460        for (k, v) in obj {
461            if k == "id" {
462                continue;
463            }
464            doc.insert(k.clone(), MongoBackend::value_to_bson(v));
465        }
466    }
467    doc
468}
469
470fn bson_to_json(bson: mongodb::bson::Bson) -> Value {
471    serde_json::to_value(bson).unwrap_or(Value::Null)
472}
473
474fn row_from_body(table: &str, id: &str, body: Value) -> Value {
475    let mut obj = body.as_object().cloned().unwrap_or_default();
476    obj.insert("id".into(), record_id_json(table, id));
477    Value::Object(obj)
478}
479
480fn record_id_json(table: &str, id: &str) -> Value {
481    serde_json::json!({
482        "table": table,
483        "id": id,
484    })
485}
486
487fn storage_id(content: &Value) -> Option<String> {
488    content.get("id").and_then(|v| {
489        v.get("id")
490            .and_then(|x| x.as_str())
491            .map(str::to_string)
492            .or_else(|| v.as_str().map(str::to_string))
493    })
494}
495
496fn uuid_simple() -> String {
497    uuid::Uuid::new_v4().to_string()
498}