nodedb_lite/query/document_ops/writes/
batch.rs1use std::collections::HashSet;
3
4use super::super::is_strict;
5use super::super::reads::msgpack_bytes_to_crdt_fields;
6use super::super::write_helpers::affected;
7use super::super::write_helpers::{decode_strict_fields, fields_to_values, strict_schema};
8use crate::engine::crdt::{CrdtRowOp, CrdtRowWrite};
9use crate::error::LiteError;
10use crate::query::engine::LiteQueryEngine;
11use crate::query::text_index::{index_strict_rows, reindex_documents};
12use crate::storage::engine::StorageEngine;
13use nodedb_types::result::QueryResult;
14use nodedb_types::value::Value;
15
16pub async fn batch_insert<S: StorageEngine>(
23 engine: &LiteQueryEngine<S>,
24 collection: &str,
25 documents: &[(String, Vec<u8>)],
26 if_absent: bool,
27) -> Result<QueryResult, LiteError> {
28 batch_insert_coordinated(engine, None, collection, documents, if_absent).await
29}
30
31pub(crate) async fn batch_insert_coordinated<S: StorageEngine>(
32 engine: &LiteQueryEngine<S>,
33 permit: Option<&crate::engine::fts::coordinator::TextMutationPermit>,
34 collection: &str,
35 documents: &[(String, Vec<u8>)],
36 if_absent: bool,
37) -> Result<QueryResult, LiteError> {
38 if let Some(permit) = permit {
39 return batch_insert_admitted(engine, permit, collection, documents, if_absent).await;
40 }
41 let guard = engine.fts_state.admit_mutation().await;
42 let result =
43 batch_insert_admitted(engine, guard.permit(), collection, documents, if_absent).await;
44 guard.finish(result)
45}
46
47pub(crate) async fn batch_insert_admitted<S: StorageEngine>(
48 engine: &LiteQueryEngine<S>,
49 permit: &crate::engine::fts::coordinator::TextMutationPermit,
50 collection: &str,
51 documents: &[(String, Vec<u8>)],
52 if_absent: bool,
53) -> Result<QueryResult, LiteError> {
54 let _permit = permit;
55 if is_strict(engine, collection) {
56 let schema = strict_schema(engine, collection)?;
57 let mut seen: HashSet<&str> = HashSet::with_capacity(documents.len());
58 let mut rows: Vec<Vec<Value>> = Vec::with_capacity(documents.len());
59 for (doc_id, value_bytes) in documents {
60 if if_absent {
61 let repeated = !seen.insert(doc_id.as_str());
62 if repeated
63 || engine
64 .strict
65 .get(collection, &Value::String(doc_id.clone()))
66 .await?
67 .is_some()
68 {
69 continue;
70 }
71 }
72 let fields = decode_strict_fields(value_bytes)?;
73 rows.push(fields_to_values(&fields, &schema.columns));
74 }
75 let affected_n = rows.len() as u64;
76 if !rows.is_empty() {
77 engine.strict.insert_rows(collection, &rows).await?;
80 index_strict_rows(engine, collection, rows.iter().map(Vec::as_slice))?;
81 }
82 Ok(affected(affected_n, "INSERT"))
83 } else {
84 let mut decoded: Vec<(&str, Vec<(String, loro::LoroValue)>)> =
85 Vec::with_capacity(documents.len());
86 for (doc_id, value_bytes) in documents {
87 let crdt_fields = msgpack_bytes_to_crdt_fields(value_bytes)?;
88 decoded.push((doc_id.as_str(), crdt_fields));
89 }
90 let mut crdt = engine.crdt.lock().map_err(|_| LiteError::LockPoisoned)?;
91 let mut seen: HashSet<&str> = HashSet::with_capacity(decoded.len());
93 let mut kept: Vec<&(&str, Vec<(String, loro::LoroValue)>)> =
94 Vec::with_capacity(decoded.len());
95 for entry in &decoded {
96 let doc_id = entry.0;
97 let taken = !seen.insert(doc_id) || crdt.exists(collection, doc_id);
98 if !taken {
99 kept.push(entry);
100 } else if !if_absent {
101 return Err(LiteError::BadRequest {
102 detail: format!(
103 "duplicate key value violates unique constraint on '{collection}' (id = '{doc_id}')"
104 ),
105 });
106 }
107 }
108 let affected_n = kept.len() as u64;
109 let slices: Vec<Vec<(&str, loro::LoroValue)>> = kept
110 .iter()
111 .map(|(_, fields)| {
112 fields
113 .iter()
114 .map(|(k, v)| (k.as_str(), v.clone()))
115 .collect()
116 })
117 .collect();
118 let rows: Vec<CrdtRowOp<'_>> = kept
119 .iter()
120 .zip(&slices)
121 .map(|((doc_id, _), f)| (CrdtRowWrite::Upsert, collection, *doc_id, f.as_slice()))
122 .collect();
123 crdt.check_unique_writes(&rows)?;
125 for &(_, _, doc_id, fields) in &rows {
126 crdt.upsert_deferred(collection, doc_id, fields)?;
127 }
128 crdt.flush_deltas().map_err(|e| LiteError::Storage {
129 detail: e.to_string(),
130 })?;
131 drop(crdt);
132 reindex_documents(engine, collection, kept.iter().map(|(doc_id, _)| *doc_id))?;
133 Ok(affected(affected_n, "INSERT"))
134 }
135}