Skip to main content

nodedb_columnar/mutation/
batch.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! All-or-nothing multi-row insert.
4
5use 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
15/// One row of a batch insert.
16pub struct BatchRow<'a> {
17    pub values: &'a [Value],
18    pub surrogate: Option<Surrogate>,
19}
20
21/// What a batch insert does with a row whose primary key is already bound.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum BatchConflict {
24    /// Replace the bound row, as [`MutationEngine::insert`] does.
25    Upsert,
26    /// Skip the row, as [`MutationEngine::insert_if_absent`] does.
27    Skip,
28}
29
30/// The PK binding a batch found before it first wrote that PK.
31struct PriorBinding {
32    location: Option<RowLocation>,
33    /// Whether the batch tombstoned `location`.
34    tombstoned: bool,
35}
36
37impl MutationEngine {
38    /// Insert every row of `rows`, or none of them.
39    ///
40    /// Each row follows the single-row rules for `conflict`. When a row
41    /// fails, the rows this call already wrote are undone: the memtable is
42    /// cut back, their PK bindings are restored, the rows they replaced are
43    /// live again, and the error is returned.
44    ///
45    /// Returns one `MutationResult` per row, in order. A skipped row has an
46    /// empty `wal_records`.
47    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    /// Write one batch row and record the PK binding it replaced.
68    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    /// Undo the rows a failed batch wrote from memtable row `start` on.
104    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        // An index cut by the undo starts live when a new row lands on it.
183        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}