1use std::collections::HashMap;
6
7use nodedb_types::columnar::ColumnarSchema;
8use nodedb_types::surrogate::Surrogate;
9use nodedb_types::value::Value;
10
11use crate::delete_bitmap::DeleteBitmap;
12use crate::error::ColumnarError;
13use crate::memtable::ColumnarMemtable;
14use crate::pk_index::{PkIndex, encode_pk};
15use crate::wal_record::ColumnarWalRecord;
16
17pub struct MutationEngine {
22 pub(super) collection: String,
23 pub(super) schema: ColumnarSchema,
24 pub(super) memtable: ColumnarMemtable,
25 pub(super) pk_index: PkIndex,
26 pub(super) delete_bitmaps: HashMap<u64, DeleteBitmap>,
28 pub(super) pk_col_indices: Vec<usize>,
30 pub(super) next_segment_id: u64,
32 pub(super) memtable_segment_id: u64,
35 pub(super) memtable_row_counter: u32,
37 pub(super) memtable_surrogates: Vec<Option<Surrogate>>,
43}
44
45#[derive(Debug)]
47pub struct MutationResult {
48 pub wal_records: Vec<ColumnarWalRecord>,
50}
51
52impl MutationEngine {
53 pub fn new(collection: String, schema: ColumnarSchema) -> Self {
55 Self::with_flush_threshold(collection, schema, crate::memtable::DEFAULT_FLUSH_THRESHOLD)
56 }
57
58 pub fn with_flush_threshold(
64 collection: String,
65 schema: ColumnarSchema,
66 flush_threshold: usize,
67 ) -> Self {
68 let pk_col_indices: Vec<usize> = schema
69 .columns
70 .iter()
71 .enumerate()
72 .filter(|(_, c)| c.primary_key)
73 .map(|(i, _)| i)
74 .collect();
75
76 let memtable = ColumnarMemtable::with_threshold(&schema, flush_threshold.max(1));
77 let memtable_segment_id = 0;
79
80 Self {
81 collection,
82 schema,
83 memtable,
84 pk_index: PkIndex::new(),
85 delete_bitmaps: HashMap::new(),
86 pk_col_indices,
87 next_segment_id: 1,
88 memtable_segment_id,
89 memtable_row_counter: 0,
90 memtable_surrogates: Vec::new(),
91 }
92 }
93
94 pub fn memtable(&self) -> &ColumnarMemtable {
98 &self.memtable
99 }
100
101 pub fn memtable_mut(&mut self) -> &mut ColumnarMemtable {
103 &mut self.memtable
104 }
105
106 pub fn pk_index(&self) -> &PkIndex {
108 &self.pk_index
109 }
110
111 pub fn pk_index_mut(&mut self) -> &mut PkIndex {
113 &mut self.pk_index
114 }
115
116 pub fn delete_bitmap(&self, segment_id: u64) -> Option<&DeleteBitmap> {
118 self.delete_bitmaps.get(&segment_id)
119 }
120
121 pub fn delete_bitmap_mut(&mut self, segment_id: u64) -> &mut DeleteBitmap {
126 self.delete_bitmaps.entry(segment_id).or_default()
127 }
128
129 pub fn memtable_segment_id(&self) -> u64 {
131 self.memtable_segment_id
132 }
133
134 pub fn pk_col_indices(&self) -> &[usize] {
136 &self.pk_col_indices
137 }
138
139 pub fn delete_bitmaps(&self) -> &HashMap<u64, DeleteBitmap> {
141 &self.delete_bitmaps
142 }
143
144 pub fn collection(&self) -> &str {
146 &self.collection
147 }
148
149 pub fn schema(&self) -> &ColumnarSchema {
151 &self.schema
152 }
153
154 pub fn should_flush(&self) -> bool {
156 self.memtable.should_flush()
157 }
158
159 pub fn memtable_surrogates(&self) -> &[Option<Surrogate>] {
164 &self.memtable_surrogates
165 }
166
167 pub fn scan_memtable_rows(
173 &self,
174 ) -> impl Iterator<Item = Result<Vec<Value>, ColumnarError>> + '_ {
175 let deletes = self.delete_bitmaps.get(&self.memtable_segment_id);
176 self.memtable
177 .iter_rows()
178 .enumerate()
179 .filter_map(move |(row_idx, row)| {
180 if deletes.is_some_and(|bm| bm.is_deleted(row_idx as u32)) {
181 None
182 } else {
183 Some(row.map_err(|e| self.memtable_read_fault(e)))
184 }
185 })
186 }
187
188 pub fn scan_memtable_rows_with_surrogates(
195 &self,
196 ) -> impl Iterator<Item = Result<(Option<Surrogate>, Vec<Value>), ColumnarError>> + '_ {
197 let deletes = self.delete_bitmaps.get(&self.memtable_segment_id);
198 let surrogates = &self.memtable_surrogates;
199 self.memtable
200 .iter_rows()
201 .enumerate()
202 .filter_map(move |(row_idx, row)| {
203 if deletes.is_some_and(|bm| bm.is_deleted(row_idx as u32)) {
204 return None;
205 }
206 let surrogate = surrogates.get(row_idx).copied().flatten();
207 Some(
208 row.map(|row| (surrogate, row))
209 .map_err(|e| self.memtable_read_fault(e)),
210 )
211 })
212 }
213
214 pub fn get_memtable_row(&self, row_idx: usize) -> Result<Option<Vec<Value>>, ColumnarError> {
219 if self
220 .delete_bitmaps
221 .get(&self.memtable_segment_id)
222 .is_some_and(|bm| bm.is_deleted(row_idx as u32))
223 {
224 return Ok(None);
225 }
226 self.memtable
227 .get_row(row_idx)
228 .map_err(|e| self.memtable_read_fault(e))
229 }
230
231 pub(super) fn memtable_read_fault(&self, err: ColumnarError) -> ColumnarError {
236 crate::diag::memtable_cell_corrupt(&err, &self.collection);
237 err
238 }
239
240 pub fn rollback_memtable_inserts(
251 &mut self,
252 row_count_before: usize,
253 inserted_pks: &[Vec<u8>],
254 displaced: &[(Vec<u8>, crate::pk_index::RowLocation)],
255 ) {
256 for pk in inserted_pks {
258 self.pk_index.remove(pk);
259 }
260 for (pk, prior_location) in displaced {
262 self.pk_index.upsert(pk.clone(), *prior_location);
263 if let Some(bm) = self.delete_bitmaps.get_mut(&prior_location.segment_id) {
265 bm.unmark_deleted(prior_location.row_index);
266 }
267 }
268 self.cut_memtable_to(row_count_before);
270 }
271
272 pub(super) fn cut_memtable_to(&mut self, row_count: usize) {
279 if let Some(bm) = self.delete_bitmaps.get_mut(&self.memtable_segment_id) {
280 bm.unmark_from(row_count as u32);
281 }
282 self.memtable.truncate_to(row_count);
283 self.memtable_surrogates.truncate(row_count);
284 self.memtable_row_counter = row_count as u32;
285 }
286
287 pub fn restore_deleted_rows(&mut self, rows: &[(Vec<u8>, crate::pk_index::RowLocation)]) {
297 for (pk_bytes, location) in rows {
298 if let Some(bm) = self.delete_bitmaps.get_mut(&location.segment_id) {
299 bm.unmark_deleted(location.row_index);
300 }
301 self.pk_index.upsert(pk_bytes.clone(), *location);
302 }
303 }
304
305 pub fn next_segment_id(&self) -> u64 {
309 self.next_segment_id
310 }
311
312 pub fn encode_pk_from_row(&self, values: &[Value]) -> Result<Vec<u8>, ColumnarError> {
315 self.extract_pk_bytes(values)
316 }
317
318 pub(super) fn extract_pk_bytes(&self, values: &[Value]) -> Result<Vec<u8>, ColumnarError> {
322 if values.len() != self.schema.columns.len() {
323 return Err(ColumnarError::SchemaMismatch {
324 expected: self.schema.columns.len(),
325 got: values.len(),
326 });
327 }
328
329 if self.pk_col_indices.len() == 1 {
330 Ok(encode_pk(&values[self.pk_col_indices[0]]))
331 } else {
332 let pk_values: Vec<&Value> = self.pk_col_indices.iter().map(|&i| &values[i]).collect();
333 Ok(crate::pk_index::encode_composite_pk(&pk_values))
334 }
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use nodedb_types::columnar::{ColumnDef, ColumnType, ColumnarSchema};
341 use nodedb_types::surrogate_bitmap::SurrogateBitmap;
342
343 use super::*;
344
345 fn minimal_schema() -> ColumnarSchema {
346 ColumnarSchema {
347 columns: vec![ColumnDef::required("id", ColumnType::Int64).with_primary_key()],
348 version: 1,
349 }
350 }
351
352 #[test]
353 fn segment_id_allocator_returns_err_at_u64_max() {
354 let mut engine = MutationEngine::new("test".to_string(), minimal_schema());
355 engine.next_segment_id = u64::MAX;
356 let result = engine.on_memtable_flushed(u64::MAX - 1);
357 assert!(
358 matches!(result, Err(ColumnarError::SegmentIdExhausted)),
359 "expected SegmentIdExhausted, got: {result:?}"
360 );
361 }
362
363 #[test]
364 fn with_flush_threshold_triggers_should_flush_at_threshold() {
365 use nodedb_types::value::Value;
366
367 let schema = minimal_schema();
368 let mut engine = MutationEngine::with_flush_threshold("col".to_string(), schema, 2);
370 assert!(!engine.should_flush(), "empty engine should not need flush");
371
372 engine.insert(&[Value::Integer(1)]).expect("insert row 1");
373 assert!(!engine.should_flush(), "1 row below threshold of 2");
374
375 engine.insert(&[Value::Integer(2)]).expect("insert row 2");
376 assert!(
377 engine.should_flush(),
378 "2 rows at threshold of 2 must trigger flush"
379 );
380 }
381
382 #[test]
383 fn with_flush_threshold_zero_is_clamped_to_one() {
384 use nodedb_types::value::Value;
385
386 let schema = minimal_schema();
387 let mut engine = MutationEngine::with_flush_threshold("col".to_string(), schema, 0);
388 engine.insert(&[Value::Integer(1)]).expect("insert");
389 assert!(
391 engine.should_flush(),
392 "clamped-to-1 threshold: 1 row must flush"
393 );
394 }
395
396 fn test_schema() -> ColumnarSchema {
397 ColumnarSchema::new(vec![
398 ColumnDef::required("id", ColumnType::Int64).with_primary_key(),
399 ColumnDef::required("name", ColumnType::String),
400 ColumnDef::nullable("score", ColumnType::Float64),
401 ])
402 .expect("valid")
403 }
404
405 #[test]
406 fn insert_and_pk_check() {
407 let mut engine = MutationEngine::new("test".into(), test_schema());
408
409 let result = engine
410 .insert(&[
411 Value::Integer(1),
412 Value::String("Alice".into()),
413 Value::Float(0.75),
414 ])
415 .expect("insert");
416
417 assert_eq!(result.wal_records.len(), 1);
418 assert!(matches!(
419 &result.wal_records[0],
420 ColumnarWalRecord::InsertRow { .. }
421 ));
422
423 assert_eq!(engine.pk_index().len(), 1);
424 assert_eq!(engine.memtable().row_count(), 1);
425 }
426
427 #[test]
428 fn delete_by_pk() {
429 let mut engine = MutationEngine::new("test".into(), test_schema());
430
431 engine
432 .insert(&[
433 Value::Integer(1),
434 Value::String("Alice".into()),
435 Value::Null,
436 ])
437 .expect("insert");
438
439 let result = engine.delete(&Value::Integer(1)).expect("delete");
440 assert_eq!(result.wal_records.len(), 1);
441 assert!(matches!(
442 &result.wal_records[0],
443 ColumnarWalRecord::DeleteRows { .. }
444 ));
445
446 assert!(engine.pk_index().is_empty());
448 }
449
450 #[test]
451 fn delete_nonexistent_pk() {
452 let mut engine = MutationEngine::new("test".into(), test_schema());
453
454 let err = engine.delete(&Value::Integer(999));
455 assert!(matches!(err, Err(ColumnarError::PrimaryKeyNotFound)));
456 }
457
458 #[test]
459 fn update_row() {
460 let mut engine = MutationEngine::new("test".into(), test_schema());
461
462 engine
463 .insert(&[
464 Value::Integer(1),
465 Value::String("Alice".into()),
466 Value::Float(0.5),
467 ])
468 .expect("insert");
469
470 let result = engine
472 .update(
473 &Value::Integer(1),
474 &[
475 Value::Integer(1),
476 Value::String("Alice Updated".into()),
477 Value::Float(0.75),
478 ],
479 None,
480 )
481 .expect("update");
482
483 assert_eq!(result.wal_records.len(), 2);
485 assert!(matches!(
486 &result.wal_records[0],
487 ColumnarWalRecord::DeleteRows { .. }
488 ));
489 assert!(matches!(
490 &result.wal_records[1],
491 ColumnarWalRecord::InsertRow { .. }
492 ));
493
494 assert_eq!(engine.pk_index().len(), 1);
496 assert_eq!(engine.memtable().row_count(), 2);
498 }
499
500 #[test]
501 fn memtable_flush_remaps_pk() {
502 let mut engine = MutationEngine::new("test".into(), test_schema());
503
504 for i in 0..5 {
505 engine
506 .insert(&[
507 Value::Integer(i),
508 Value::String(format!("u{i}")),
509 Value::Null,
510 ])
511 .expect("insert");
512 }
513
514 let result = engine.on_memtable_flushed(1).expect("flush");
516 assert_eq!(result.wal_records.len(), 1);
517 assert!(matches!(
518 &result.wal_records[0],
519 ColumnarWalRecord::MemtableFlushed {
520 segment_id: 1,
521 row_count: 5,
522 ..
523 }
524 ));
525
526 let pk = encode_pk(&Value::Integer(3));
528 let loc = engine.pk_index().get(&pk).expect("pk exists");
529 assert_eq!(loc.segment_id, 1);
530 assert_eq!(loc.row_index, 3);
531 }
532
533 #[test]
534 fn multiple_inserts_and_deletes() {
535 let mut engine = MutationEngine::new("test".into(), test_schema());
536
537 for i in 0..10 {
538 engine
539 .insert(&[
540 Value::Integer(i),
541 Value::String(format!("u{i}")),
542 Value::Null,
543 ])
544 .expect("insert");
545 }
546
547 for i in (1..10).step_by(2) {
549 engine.delete(&Value::Integer(i)).expect("delete");
550 }
551
552 assert_eq!(engine.pk_index().len(), 5); }
554
555 #[test]
556 fn prefilter_row_boundary_membership() {
557 let mut engine = MutationEngine::new("col".into(), test_schema());
561
562 let surrogates = [10u32, 20, 30, 40, 50];
563 for (i, &s) in surrogates.iter().enumerate() {
564 engine
565 .insert_with_surrogate(
566 &[
567 Value::Integer(i as i64),
568 Value::String(format!("r{i}")),
569 Value::Null,
570 ],
571 Surrogate(s),
572 )
573 .expect("insert_with_surrogate");
574 }
575
576 assert_eq!(engine.memtable_surrogates().len(), 5);
577
578 let bitmap = SurrogateBitmap::from_iter([Surrogate(20), Surrogate(40)]);
579
580 let surrogates_slice = engine.memtable_surrogates();
583 let (mt_min, mt_max) = surrogates_slice
584 .iter()
585 .flatten()
586 .fold((u32::MAX, u32::MIN), |(lo, hi), s| {
587 (lo.min(s.0), hi.max(s.0))
588 });
589 assert_eq!(mt_min, 10);
590 assert_eq!(mt_max, 50);
591 let bm_min = bitmap.0.min().unwrap();
592 let bm_max = bitmap.0.max().unwrap();
593 assert!(!(bm_max < mt_min || bm_min > mt_max));
595
596 let passing: Vec<_> = engine
598 .scan_memtable_rows_with_surrogates()
599 .map(|r| r.expect("read"))
600 .filter(|(sur, _)| sur.is_some_and(|s| bitmap.contains(s)))
601 .collect();
602 assert_eq!(passing.len(), 2);
603 assert!(passing[0].0 == Some(Surrogate(20)));
605 assert!(passing[1].0 == Some(Surrogate(40)));
606 }
607
608 #[test]
609 fn prefilter_block_boundary_skip() {
610 let mut engine = MutationEngine::new("col".into(), test_schema());
614
615 for (i, s) in [100u32, 200, 300].iter().enumerate() {
616 engine
617 .insert_with_surrogate(
618 &[
619 Value::Integer(i as i64),
620 Value::String(format!("r{i}")),
621 Value::Null,
622 ],
623 Surrogate(*s),
624 )
625 .expect("insert_with_surrogate");
626 }
627
628 let surrogates_slice = engine.memtable_surrogates();
629 let (mt_min, mt_max) = surrogates_slice
630 .iter()
631 .flatten()
632 .fold((u32::MAX, u32::MIN), |(lo, hi), s| {
633 (lo.min(s.0), hi.max(s.0))
634 });
635 assert_eq!(mt_min, 100);
636 assert_eq!(mt_max, 300);
637
638 let bitmap = SurrogateBitmap::from_iter([Surrogate(10), Surrogate(20)]);
639 let bm_min = bitmap.0.min().unwrap();
640 let bm_max = bitmap.0.max().unwrap();
641
642 assert!(bm_max < mt_min || bm_min > mt_max);
644 }
645
646 #[test]
647 fn insert_without_surrogate_stores_none() {
648 let mut engine = MutationEngine::new("col".into(), test_schema());
649 engine
650 .insert(&[Value::Integer(1), Value::String("x".into()), Value::Null])
651 .expect("insert");
652 assert_eq!(engine.memtable_surrogates(), &[None]);
653 }
654
655 #[test]
656 fn an_update_keeps_the_old_rows_surrogate() {
657 let mut engine = MutationEngine::new("col".into(), test_schema());
658 engine
659 .insert_with_surrogate(
660 &[Value::Integer(1), Value::String("x".into()), Value::Null],
661 Surrogate(42),
662 )
663 .expect("insert");
664 engine
665 .update(
666 &Value::Integer(1),
667 &[Value::Integer(2), Value::String("y".into()), Value::Null],
668 Some(Surrogate(7)),
669 )
670 .expect("update");
671 let live: Vec<Option<Surrogate>> = engine
672 .scan_memtable_rows_with_surrogates()
673 .map(|r| r.expect("read").0)
674 .collect();
675 assert_eq!(
676 live,
677 vec![Some(Surrogate(42))],
678 "the replacement row carries the identity the memtable row had"
679 );
680 }
681
682 #[test]
683 fn an_update_of_a_flushed_row_carries_the_segment_surrogate() {
684 let mut engine = MutationEngine::new("col".into(), test_schema());
685 engine
686 .insert_with_surrogate(
687 &[Value::Integer(1), Value::String("x".into()), Value::Null],
688 Surrogate(42),
689 )
690 .expect("insert");
691 let segment_id = engine.next_segment_id();
692 let _drained = engine.memtable_mut().drain_optimized();
693 engine.on_memtable_flushed(segment_id).expect("flush");
694 engine
695 .update(
696 &Value::Integer(1),
697 &[Value::Integer(1), Value::String("y".into()), Value::Null],
698 Some(Surrogate(42)),
699 )
700 .expect("update");
701 let live: Vec<Option<Surrogate>> = engine
702 .scan_memtable_rows_with_surrogates()
703 .map(|r| r.expect("read").0)
704 .collect();
705 assert_eq!(
706 live,
707 vec![Some(Surrogate(42))],
708 "the replacement row carries the identity the flushed row had"
709 );
710 }
711
712 #[test]
713 fn flush_clears_surrogate_table() {
714 let mut engine = MutationEngine::new("col".into(), test_schema());
715 engine
716 .insert_with_surrogate(
717 &[Value::Integer(1), Value::String("x".into()), Value::Null],
718 Surrogate(42),
719 )
720 .expect("insert");
721 assert_eq!(engine.memtable_surrogates().len(), 1);
722 engine.on_memtable_flushed(1).expect("flush");
723 assert!(engine.memtable_surrogates().is_empty());
724 }
725
726 #[test]
730 fn mutations_only_emit_replayable_wal_records() {
731 let mut engine = MutationEngine::new("test".into(), test_schema());
732
733 let mut records = Vec::new();
734 for i in 0..10 {
735 records.extend(
736 engine
737 .insert(&[
738 Value::Integer(i),
739 Value::String(format!("u{i}")),
740 Value::Null,
741 ])
742 .expect("insert")
743 .wal_records,
744 );
745 }
746 records.extend(engine.on_memtable_flushed(1).expect("flush").wal_records);
747 for i in 0..3 {
748 records.extend(
749 engine
750 .delete(&Value::Integer(i))
751 .expect("delete")
752 .wal_records,
753 );
754 }
755
756 assert!(!records.is_empty());
757 for rec in &records {
758 assert!(
759 matches!(
760 rec,
761 ColumnarWalRecord::InsertRow { .. }
762 | ColumnarWalRecord::DeleteRows { .. }
763 | ColumnarWalRecord::MemtableFlushed { .. }
764 ),
765 "unexpected WAL record shape: {rec:?}"
766 );
767 }
768 }
769
770 #[test]
772 fn segment_id_allocator_returns_err_when_both_counters_are_exhausted() {
773 let mut engine = MutationEngine::new("test".into(), test_schema());
774
775 engine.next_segment_id = u64::MAX;
776 engine.memtable_segment_id = u64::MAX;
777
778 let err = engine
779 .on_memtable_flushed(u64::MAX)
780 .expect_err("should be exhausted");
781 assert!(
782 matches!(err, ColumnarError::SegmentIdExhausted),
783 "expected SegmentIdExhausted, got {err:?}"
784 );
785 }
786}