nodedb_columnar/mutation/
batch.rs1use std::collections::HashMap;
6
7use nodedb_types::surrogate::Surrogate;
8use nodedb_types::value::Value;
9
10use crate::error::ColumnarError;
11use crate::pk_index::RowLocation;
12
13use super::engine::{MutationEngine, MutationResult};
14
15pub struct BatchRow<'a> {
17 pub values: &'a [Value],
18 pub surrogate: Option<Surrogate>,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum BatchConflict {
24 Upsert,
26 Skip,
28}
29
30struct PriorBinding {
32 location: Option<RowLocation>,
33 tombstoned: bool,
35}
36
37impl MutationEngine {
38 pub fn insert_batch<'a>(
48 &mut self,
49 rows: impl IntoIterator<Item = BatchRow<'a>>,
50 conflict: BatchConflict,
51 ) -> Result<Vec<MutationResult>, ColumnarError> {
52 let start = self.memtable.row_count();
53 let mut priors: HashMap<Vec<u8>, PriorBinding> = HashMap::new();
54 let mut results = Vec::new();
55 for row in rows {
56 match self.insert_batch_row(&row, conflict, &mut priors) {
57 Ok(result) => results.push(result),
58 Err(e) => {
59 self.undo_batch(start, priors);
60 return Err(e);
61 }
62 }
63 }
64 Ok(results)
65 }
66
67 fn insert_batch_row(
69 &mut self,
70 row: &BatchRow<'_>,
71 conflict: BatchConflict,
72 priors: &mut HashMap<Vec<u8>, PriorBinding>,
73 ) -> Result<MutationResult, ColumnarError> {
74 let pk_bytes = self.extract_pk_bytes(row.values)?;
75 let prior = self.pk_index.get(&pk_bytes).copied();
76 if conflict == BatchConflict::Skip && prior.is_some() {
77 return Ok(MutationResult {
78 wal_records: Vec::new(),
79 });
80 }
81 let replaced = if self.schema.is_bitemporal() {
82 None
83 } else {
84 prior
85 };
86
87 let mut wal_records = Vec::with_capacity(2);
88 let key = pk_bytes.clone();
89 self.commit_row(
90 row.values,
91 pk_bytes,
92 row.surrogate,
93 replaced.as_slice(),
94 &mut wal_records,
95 )?;
96 priors.entry(key).or_insert(PriorBinding {
97 location: prior,
98 tombstoned: replaced.is_some(),
99 });
100 Ok(MutationResult { wal_records })
101 }
102
103 fn undo_batch(&mut self, start: usize, priors: HashMap<Vec<u8>, PriorBinding>) {
105 for (pk_bytes, prior) in priors {
106 match prior.location {
107 Some(location) => {
108 if prior.tombstoned
109 && let Some(bm) = self.delete_bitmaps.get_mut(&location.segment_id)
110 {
111 bm.unmark_deleted(location.row_index);
112 }
113 self.pk_index.upsert(pk_bytes, location);
114 }
115 None => {
116 self.pk_index.remove(&pk_bytes);
117 }
118 }
119 }
120 self.cut_memtable_to(start);
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use nodedb_types::columnar::{ColumnDef, ColumnType, ColumnarSchema};
127
128 use super::*;
129 use crate::pk_index::encode_pk;
130
131 fn schema() -> ColumnarSchema {
132 ColumnarSchema::new(vec![
133 ColumnDef::required("id", ColumnType::Int64).with_primary_key(),
134 ColumnDef::required("name", ColumnType::String),
135 ])
136 .expect("valid")
137 }
138
139 fn row(id: i64, name: &str) -> Vec<Value> {
140 vec![Value::Integer(id), Value::String(name.into())]
141 }
142
143 fn batch(rows: &[Vec<Value>]) -> Vec<BatchRow<'_>> {
144 rows.iter()
145 .map(|values| BatchRow {
146 values,
147 surrogate: None,
148 })
149 .collect()
150 }
151
152 #[test]
153 fn failed_batch_applies_nothing() {
154 let mut engine = MutationEngine::new("t".into(), schema());
155 engine.insert(&row(1, "a")).expect("insert");
156
157 let rows = vec![
158 row(1, "a2"),
159 row(2, "b"),
160 row(2, "b2"),
161 vec![Value::Integer(3), Value::Null],
162 ];
163 let err = engine
164 .insert_batch(batch(&rows), BatchConflict::Upsert)
165 .unwrap_err();
166 assert!(matches!(err, ColumnarError::NullViolation(_)));
167
168 let live: Vec<Vec<Value>> = engine
169 .scan_memtable_rows()
170 .collect::<Result<_, _>>()
171 .expect("read");
172 assert_eq!(live, vec![row(1, "a")]);
173 assert_eq!(engine.memtable().row_count(), 1);
174 assert_eq!(engine.memtable_surrogates().len(), 1);
175 assert_eq!(engine.pk_index().len(), 1);
176 let loc = engine
177 .pk_index()
178 .get(&encode_pk(&Value::Integer(1)))
179 .copied();
180 assert_eq!(loc.map(|l| l.row_index), Some(0));
181
182 engine.insert(&row(4, "d")).expect("insert after undo");
184 engine.insert(&row(5, "e")).expect("insert after undo");
185 let live: Vec<Vec<Value>> = engine
186 .scan_memtable_rows()
187 .collect::<Result<_, _>>()
188 .expect("read");
189 assert_eq!(live, vec![row(1, "a"), row(4, "d"), row(5, "e")]);
190 }
191
192 #[test]
193 fn batch_upserts_and_skips_like_single_rows() {
194 let mut engine = MutationEngine::new("t".into(), schema());
195 engine.insert(&row(1, "a")).expect("insert");
196
197 let rows = vec![row(1, "a2"), row(2, "b")];
198 let results = engine
199 .insert_batch(batch(&rows), BatchConflict::Upsert)
200 .expect("batch");
201 assert_eq!(results.len(), 2);
202 let live: Vec<Vec<Value>> = engine
203 .scan_memtable_rows()
204 .collect::<Result<_, _>>()
205 .expect("read");
206 assert_eq!(live, vec![row(1, "a2"), row(2, "b")]);
207
208 let rows = vec![row(2, "b2"), row(3, "c")];
209 let results = engine
210 .insert_batch(batch(&rows), BatchConflict::Skip)
211 .expect("batch");
212 assert!(results[0].wal_records.is_empty());
213 assert_eq!(results[1].wal_records.len(), 1);
214 let live: Vec<Vec<Value>> = engine
215 .scan_memtable_rows()
216 .collect::<Result<_, _>>()
217 .expect("read");
218 assert_eq!(live, vec![row(1, "a2"), row(2, "b"), row(3, "c")]);
219 }
220}