use std::collections::HashMap;
use nodedb_types::surrogate::Surrogate;
use nodedb_types::value::Value;
use crate::error::ColumnarError;
use crate::pk_index::RowLocation;
use super::engine::{MutationEngine, MutationResult};
pub struct BatchRow<'a> {
pub values: &'a [Value],
pub surrogate: Option<Surrogate>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BatchConflict {
Upsert,
Skip,
}
struct PriorBinding {
location: Option<RowLocation>,
tombstoned: bool,
}
impl MutationEngine {
pub fn insert_batch<'a>(
&mut self,
rows: impl IntoIterator<Item = BatchRow<'a>>,
conflict: BatchConflict,
) -> Result<Vec<MutationResult>, ColumnarError> {
let start = self.memtable.row_count();
let mut priors: HashMap<Vec<u8>, PriorBinding> = HashMap::new();
let mut results = Vec::new();
for row in rows {
match self.insert_batch_row(&row, conflict, &mut priors) {
Ok(result) => results.push(result),
Err(e) => {
self.undo_batch(start, priors);
return Err(e);
}
}
}
Ok(results)
}
fn insert_batch_row(
&mut self,
row: &BatchRow<'_>,
conflict: BatchConflict,
priors: &mut HashMap<Vec<u8>, PriorBinding>,
) -> Result<MutationResult, ColumnarError> {
let pk_bytes = self.extract_pk_bytes(row.values)?;
let prior = self.pk_index.get(&pk_bytes).copied();
if conflict == BatchConflict::Skip && prior.is_some() {
return Ok(MutationResult {
wal_records: Vec::new(),
});
}
let replaced = if self.schema.is_bitemporal() {
None
} else {
prior
};
let mut wal_records = Vec::with_capacity(2);
let key = pk_bytes.clone();
self.commit_row(
row.values,
pk_bytes,
row.surrogate,
replaced.as_slice(),
&mut wal_records,
)?;
priors.entry(key).or_insert(PriorBinding {
location: prior,
tombstoned: replaced.is_some(),
});
Ok(MutationResult { wal_records })
}
fn undo_batch(&mut self, start: usize, priors: HashMap<Vec<u8>, PriorBinding>) {
for (pk_bytes, prior) in priors {
match prior.location {
Some(location) => {
if prior.tombstoned
&& let Some(bm) = self.delete_bitmaps.get_mut(&location.segment_id)
{
bm.unmark_deleted(location.row_index);
}
self.pk_index.upsert(pk_bytes, location);
}
None => {
self.pk_index.remove(&pk_bytes);
}
}
}
self.cut_memtable_to(start);
}
}
#[cfg(test)]
mod tests {
use nodedb_types::columnar::{ColumnDef, ColumnType, ColumnarSchema};
use super::*;
use crate::pk_index::encode_pk;
fn schema() -> ColumnarSchema {
ColumnarSchema::new(vec![
ColumnDef::required("id", ColumnType::Int64).with_primary_key(),
ColumnDef::required("name", ColumnType::String),
])
.expect("valid")
}
fn row(id: i64, name: &str) -> Vec<Value> {
vec![Value::Integer(id), Value::String(name.into())]
}
fn batch(rows: &[Vec<Value>]) -> Vec<BatchRow<'_>> {
rows.iter()
.map(|values| BatchRow {
values,
surrogate: None,
})
.collect()
}
#[test]
fn failed_batch_applies_nothing() {
let mut engine = MutationEngine::new("t".into(), schema());
engine.insert(&row(1, "a")).expect("insert");
let rows = vec![
row(1, "a2"),
row(2, "b"),
row(2, "b2"),
vec![Value::Integer(3), Value::Null],
];
let err = engine
.insert_batch(batch(&rows), BatchConflict::Upsert)
.unwrap_err();
assert!(matches!(err, ColumnarError::NullViolation(_)));
let live: Vec<Vec<Value>> = engine
.scan_memtable_rows()
.collect::<Result<_, _>>()
.expect("read");
assert_eq!(live, vec![row(1, "a")]);
assert_eq!(engine.memtable().row_count(), 1);
assert_eq!(engine.memtable_surrogates().len(), 1);
assert_eq!(engine.pk_index().len(), 1);
let loc = engine
.pk_index()
.get(&encode_pk(&Value::Integer(1)))
.copied();
assert_eq!(loc.map(|l| l.row_index), Some(0));
engine.insert(&row(4, "d")).expect("insert after undo");
engine.insert(&row(5, "e")).expect("insert after undo");
let live: Vec<Vec<Value>> = engine
.scan_memtable_rows()
.collect::<Result<_, _>>()
.expect("read");
assert_eq!(live, vec![row(1, "a"), row(4, "d"), row(5, "e")]);
}
#[test]
fn batch_upserts_and_skips_like_single_rows() {
let mut engine = MutationEngine::new("t".into(), schema());
engine.insert(&row(1, "a")).expect("insert");
let rows = vec![row(1, "a2"), row(2, "b")];
let results = engine
.insert_batch(batch(&rows), BatchConflict::Upsert)
.expect("batch");
assert_eq!(results.len(), 2);
let live: Vec<Vec<Value>> = engine
.scan_memtable_rows()
.collect::<Result<_, _>>()
.expect("read");
assert_eq!(live, vec![row(1, "a2"), row(2, "b")]);
let rows = vec![row(2, "b2"), row(3, "c")];
let results = engine
.insert_batch(batch(&rows), BatchConflict::Skip)
.expect("batch");
assert!(results[0].wal_records.is_empty());
assert_eq!(results[1].wal_records.len(), 1);
let live: Vec<Vec<Value>> = engine
.scan_memtable_rows()
.collect::<Result<_, _>>()
.expect("read");
assert_eq!(live, vec![row(1, "a2"), row(2, "b"), row(3, "c")]);
}
}