nodedb_lite/nodedb/collection/
bulk.rs1use std::collections::HashMap;
7
8use nodedb_types::error::{NodeDbError, NodeDbResult};
9use nodedb_types::value::Value;
10
11use super::super::convert::value_to_loro;
12use super::super::{LockExt, NodeDbLite};
13use crate::storage::engine::StorageEngine;
14
15impl<S: StorageEngine> NodeDbLite<S> {
16 pub fn bulk_update(
24 &self,
25 collection: &str,
26 filters: &[nodedb_query::ScanFilter],
27 updates: &HashMap<String, Value>,
28 ) -> NodeDbResult<u64> {
29 let guard = self
30 .fts_state
31 .try_admit_mutation("bulk_update")
32 .map_err(NodeDbError::from)?;
33 let result = (|| {
34 let mut crdt = self.crdt.lock_or_recover();
36 let ids = crdt.list_ids(collection);
37
38 let mut matching_ids = Vec::new();
39 for id in ids {
40 if let Some(loro_val) = crdt.read(collection, &id) {
41 let doc = crate::nodedb::convert::loro_value_to_document(&id, &loro_val);
42 let json = serde_json::to_value(&doc.fields).unwrap_or_default();
43 let msgpack = nodedb_types::json_msgpack::json_to_msgpack_or_empty(&json);
44 if filters.is_empty()
45 || nodedb_query::ScanFilter::all_match_binary(filters, &msgpack)
46 .map_err(|_| NodeDbError::division_by_zero())?
47 {
48 matching_ids.push(id);
49 }
50 }
51 }
52
53 let update_fields: Vec<(&str, loro::LoroValue)> = updates
54 .iter()
55 .map(|(k, v)| (k.as_str(), value_to_loro(v)))
56 .collect();
57
58 let mut count = 0u64;
59 for id in &matching_ids {
60 crdt.set_fields(collection, id, &update_fields)
62 .map_err(NodeDbError::from)?;
63 count += 1;
64 }
65 drop(crdt);
66
67 for id in &matching_ids {
69 let crdt = self.crdt.lock_or_recover();
70 if let Some(loro_val) = crdt.read(collection, id) {
71 let doc = crate::nodedb::convert::loro_value_to_document(id, &loro_val);
72 drop(crdt);
73 self.index_document_text(collection, id, &doc.fields)?;
74 self.index_document_sparse(collection, id, &doc.fields);
75 }
76 }
77
78 Ok(count)
79 })();
80 guard.finish(result)
81 }
82
83 pub fn bulk_delete(
88 &self,
89 collection: &str,
90 filters: &[nodedb_query::ScanFilter],
91 ) -> NodeDbResult<u64> {
92 let guard = self
93 .fts_state
94 .try_admit_mutation("bulk_delete")
95 .map_err(NodeDbError::from)?;
96 let result = (|| {
97 let mut crdt = self.crdt.lock_or_recover();
98 let ids = crdt.list_ids(collection);
99
100 let mut matching_ids = Vec::new();
101 for id in ids {
102 if let Some(loro_val) = crdt.read(collection, &id) {
103 let doc = crate::nodedb::convert::loro_value_to_document(&id, &loro_val);
104 let json = serde_json::to_value(&doc.fields).unwrap_or_default();
105 let msgpack = nodedb_types::json_msgpack::json_to_msgpack_or_empty(&json);
106 if filters.is_empty()
107 || nodedb_query::ScanFilter::all_match_binary(filters, &msgpack)
108 .map_err(|_| NodeDbError::division_by_zero())?
109 {
110 matching_ids.push(id);
111 }
112 }
113 }
114
115 let mut count = 0u64;
116 for id in &matching_ids {
117 crdt.delete(collection, id).map_err(NodeDbError::storage)?;
118 count += 1;
119 }
120 drop(crdt);
121
122 for id in &matching_ids {
123 self.remove_document_text(collection, id)?;
124 self.remove_document_sparse(collection, id);
125 }
126
127 Ok(count)
128 })();
129 guard.finish(result)
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use crate::nodedb::LockExt;
136 use crate::{LiteConfig, NodeDbLite, PagedbStorageMem};
137 use nodedb_types::value::Value;
138 use std::collections::HashMap;
139
140 #[tokio::test]
141 async fn busy_bulk_update_preserves_source_and_checkpoint_trust() {
142 let storage = PagedbStorageMem::open_in_memory().await.unwrap();
143 let db = NodeDbLite::open_with_config(
144 storage,
145 LiteConfig {
146 auto_flush_ms: 0,
147 sync_enabled: false,
148 ..LiteConfig::default()
149 },
150 )
151 .await
152 .unwrap();
153 db.crdt
154 .lock_or_recover()
155 .upsert("docs", "a", &[("value", loro::LoroValue::I64(1))])
156 .unwrap();
157 db.fts_state.mark_checkpoint_trusted();
158 let permit = db.fts_state.admit_exclusive().await;
159 let updates = HashMap::from([("value".into(), Value::Integer(2))]);
160 let result = db.bulk_update("docs", &[], &updates);
161 assert!(result.is_err());
162 assert_eq!(
163 db.crdt.lock_or_recover().read_field("docs", "a", "value"),
164 Some(loro::LoroValue::I64(1))
165 );
166 assert!(db.fts_state.checkpoint_trusted());
167 drop(permit);
168 assert_eq!(db.bulk_update("docs", &[], &updates).unwrap(), 1);
169 }
170}