nodedb_columnar/mutation/
write.rs1use nodedb_types::surrogate::Surrogate;
6use nodedb_types::value::Value;
7
8use crate::error::ColumnarError;
9use crate::pk_index::{RowLocation, encode_pk};
10use crate::wal_record::{ColumnarWalRecord, encode_row_for_wal};
11
12use super::engine::{MutationEngine, MutationResult};
13
14impl MutationEngine {
15 pub fn insert(&mut self, values: &[Value]) -> Result<MutationResult, ColumnarError> {
32 self.upsert_row(values, None)
33 }
34
35 pub fn insert_with_surrogate(
41 &mut self,
42 values: &[Value],
43 surrogate: Surrogate,
44 ) -> Result<MutationResult, ColumnarError> {
45 self.upsert_row(values, Some(surrogate))
46 }
47
48 fn upsert_row(
55 &mut self,
56 values: &[Value],
57 surrogate: Option<Surrogate>,
58 ) -> Result<MutationResult, ColumnarError> {
59 let pk_bytes = self.extract_pk_bytes(values)?;
60 let prior = if self.schema.is_bitemporal() {
61 None
62 } else {
63 self.pk_index.get(&pk_bytes).copied()
64 };
65 let mut wal_records = Vec::with_capacity(2);
66 self.commit_row(
67 values,
68 pk_bytes,
69 surrogate,
70 prior.as_slice(),
71 &mut wal_records,
72 )?;
73 Ok(MutationResult { wal_records })
74 }
75
76 pub(super) fn commit_row(
85 &mut self,
86 values: &[Value],
87 pk_bytes: Vec<u8>,
88 surrogate: Option<Surrogate>,
89 replaced: &[RowLocation],
90 wal_records: &mut Vec<ColumnarWalRecord>,
91 ) -> Result<(), ColumnarError> {
92 let row_data = encode_row_for_wal(values)?;
93 self.memtable.append_row(values)?;
94
95 for prior in replaced {
96 self.delete_bitmaps
97 .entry(prior.segment_id)
98 .or_default()
99 .mark_deleted(prior.row_index);
100 wal_records.push(ColumnarWalRecord::DeleteRows {
101 collection: self.collection.clone(),
102 segment_id: prior.segment_id,
103 row_indices: vec![prior.row_index],
104 });
105 }
106 wal_records.push(ColumnarWalRecord::InsertRow {
107 collection: self.collection.clone(),
108 row_data,
109 });
110
111 let location = RowLocation {
112 segment_id: self.memtable_segment_id,
113 row_index: self.memtable_row_counter,
114 };
115 self.pk_index.upsert(pk_bytes, location);
116 self.memtable_surrogates.push(surrogate);
117 self.memtable_row_counter += 1;
118 Ok(())
119 }
120
121 pub fn insert_if_absent(&mut self, values: &[Value]) -> Result<MutationResult, ColumnarError> {
128 let pk_bytes = self.extract_pk_bytes(values)?;
129 if self.pk_index.contains(&pk_bytes) {
130 return Ok(MutationResult {
131 wal_records: Vec::new(),
132 });
133 }
134 let mut wal_records = Vec::with_capacity(1);
135 self.commit_row(values, pk_bytes, None, &[], &mut wal_records)?;
136 Ok(MutationResult { wal_records })
137 }
138
139 pub fn lookup_memtable_row_by_pk(
151 &self,
152 pk_bytes: &[u8],
153 ) -> Result<Option<Vec<Value>>, ColumnarError> {
154 let Some(loc) = self.pk_index.get(pk_bytes).copied() else {
155 return Ok(None);
156 };
157 if loc.segment_id != self.memtable_segment_id {
158 return Ok(None);
159 }
160 self.memtable
161 .get_row(loc.row_index as usize)
162 .map_err(|e| self.memtable_read_fault(e))
163 }
164
165 pub fn delete(&mut self, pk_value: &Value) -> Result<MutationResult, ColumnarError> {
170 let pk_bytes = encode_pk(pk_value);
171
172 let location = self
173 .pk_index
174 .get(&pk_bytes)
175 .copied()
176 .ok_or(ColumnarError::PrimaryKeyNotFound)?;
177
178 let wal = ColumnarWalRecord::DeleteRows {
180 collection: self.collection.clone(),
181 segment_id: location.segment_id,
182 row_indices: vec![location.row_index],
183 };
184
185 let bitmap = self.delete_bitmaps.entry(location.segment_id).or_default();
187 bitmap.mark_deleted(location.row_index);
188
189 self.pk_index.remove(&pk_bytes);
191
192 Ok(MutationResult {
193 wal_records: vec![wal],
194 })
195 }
196
197 pub fn update(
215 &mut self,
216 old_pk: &Value,
217 new_values: &[Value],
218 flushed_surrogate: Option<Surrogate>,
219 ) -> Result<MutationResult, ColumnarError> {
220 let old_pk_bytes = encode_pk(old_pk);
221 let old_location = self
222 .pk_index
223 .get(&old_pk_bytes)
224 .copied()
225 .ok_or(ColumnarError::PrimaryKeyNotFound)?;
226 let surrogate = if old_location.segment_id == self.memtable_segment_id {
227 self.memtable_surrogates
228 .get(old_location.row_index as usize)
229 .copied()
230 .flatten()
231 } else {
232 flushed_surrogate
233 };
234
235 let new_pk_bytes = self.extract_pk_bytes(new_values)?;
236 let pk_changed = new_pk_bytes != old_pk_bytes;
237
238 let mut replaced = Vec::with_capacity(2);
242 replaced.push(old_location);
243 if pk_changed
244 && !self.schema.is_bitemporal()
245 && let Some(prior) = self.pk_index.get(&new_pk_bytes).copied()
246 {
247 replaced.push(prior);
248 }
249
250 let mut wal_records = Vec::with_capacity(replaced.len() + 1);
251 self.commit_row(
252 new_values,
253 new_pk_bytes,
254 surrogate,
255 &replaced,
256 &mut wal_records,
257 )?;
258 if pk_changed {
259 self.pk_index.remove(&old_pk_bytes);
260 }
261
262 Ok(MutationResult { wal_records })
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use nodedb_types::columnar::{ColumnDef, ColumnType, ColumnarSchema};
269
270 use super::*;
271
272 fn schema() -> ColumnarSchema {
273 ColumnarSchema::new(vec![
274 ColumnDef::required("id", ColumnType::Int64).with_primary_key(),
275 ColumnDef::required("name", ColumnType::String),
276 ColumnDef::nullable("score", ColumnType::Float64),
277 ])
278 .expect("valid")
279 }
280
281 fn row(id: i64, name: &str, score: f64) -> Vec<Value> {
282 vec![
283 Value::Integer(id),
284 Value::String(name.into()),
285 Value::Float(score),
286 ]
287 }
288
289 fn live_rows(engine: &MutationEngine) -> Vec<Vec<Value>> {
290 engine
291 .scan_memtable_rows()
292 .collect::<Result<_, _>>()
293 .expect("read")
294 }
295
296 #[test]
297 fn rejected_upsert_keeps_prior_row() {
298 let mut engine = MutationEngine::new("t".into(), schema());
299 engine.insert(&row(1, "a", 0.5)).expect("insert");
300
301 let err = engine
302 .insert(&[Value::Integer(1), Value::Null, Value::Null])
303 .unwrap_err();
304 assert!(matches!(err, ColumnarError::NullViolation(_)));
305
306 assert_eq!(live_rows(&engine), vec![row(1, "a", 0.5)]);
307 assert_eq!(engine.memtable().row_count(), 1);
308 let loc = engine
309 .pk_index()
310 .get(&encode_pk(&Value::Integer(1)))
311 .copied();
312 assert_eq!(loc.map(|l| l.row_index), Some(0));
313 assert!(
314 engine
315 .delete_bitmap(engine.memtable_segment_id())
316 .is_none_or(|bm| !bm.is_deleted(0))
317 );
318 }
319
320 #[test]
321 fn rejected_upsert_with_surrogate_keeps_prior_row() {
322 let mut engine = MutationEngine::new("t".into(), schema());
323 engine
324 .insert_with_surrogate(&row(1, "a", 0.5), Surrogate(7))
325 .expect("insert");
326
327 let err = engine
328 .insert_with_surrogate(
329 &[
330 Value::Integer(1),
331 Value::String("b".into()),
332 Value::Bool(true),
333 ],
334 Surrogate(8),
335 )
336 .unwrap_err();
337 assert!(matches!(err, ColumnarError::TypeMismatch { .. }));
338
339 assert_eq!(live_rows(&engine), vec![row(1, "a", 0.5)]);
340 assert_eq!(engine.memtable_surrogates(), &[Some(Surrogate(7))]);
341 }
342
343 #[test]
344 fn rejected_insert_if_absent_leaves_engine_unchanged() {
345 let mut engine = MutationEngine::new("t".into(), schema());
346 let err = engine
347 .insert_if_absent(&[Value::Integer(1), Value::Null, Value::Null])
348 .unwrap_err();
349 assert!(matches!(err, ColumnarError::NullViolation(_)));
350 assert!(engine.pk_index().is_empty());
351 assert_eq!(engine.memtable().row_count(), 0);
352
353 engine
354 .insert_if_absent(&row(1, "a", 0.5))
355 .expect("insert after rejected row");
356 assert_eq!(live_rows(&engine), vec![row(1, "a", 0.5)]);
357 }
358
359 #[test]
360 fn rejected_update_keeps_old_row() {
361 let mut engine = MutationEngine::new("t".into(), schema());
362 engine.insert(&row(1, "a", 0.5)).expect("insert");
363
364 let err = engine
365 .update(
366 &Value::Integer(1),
367 &[Value::Integer(1), Value::Null, Value::Null],
368 None,
369 )
370 .unwrap_err();
371 assert!(matches!(err, ColumnarError::NullViolation(_)));
372
373 assert_eq!(live_rows(&engine), vec![row(1, "a", 0.5)]);
374 assert!(engine.pk_index().contains(&encode_pk(&Value::Integer(1))));
375 }
376
377 #[test]
378 fn update_to_new_pk_rebinds_index() {
379 let mut engine = MutationEngine::new("t".into(), schema());
380 engine.insert(&row(1, "a", 0.5)).expect("insert");
381
382 let result = engine
383 .update(&Value::Integer(1), &row(2, "b", 0.25), None)
384 .expect("update");
385 assert_eq!(result.wal_records.len(), 2);
386
387 assert_eq!(live_rows(&engine), vec![row(2, "b", 0.25)]);
388 assert!(!engine.pk_index().contains(&encode_pk(&Value::Integer(1))));
389 assert!(engine.pk_index().contains(&encode_pk(&Value::Integer(2))));
390 }
391}