Skip to main content

nodedb_columnar/mutation/
write.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Write-path mutations: insert, insert_if_absent, delete, update.
4
5use 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    /// Insert a row with upsert-on-duplicate semantics. Returns WAL
16    /// records to persist.
17    ///
18    /// Validates schema. If the PK already exists, the prior row is
19    /// tombstoned via the segment's delete bitmap (a single positional
20    /// delete) before the new row is appended to the memtable. The PK
21    /// index is rebound to the new row location. This matches the
22    /// ClickHouse / Iceberg "sparse PK + positional delete" model and
23    /// keeps `SELECT WHERE pk = X` linearizable on one row without a
24    /// read-time merge pass.
25    ///
26    /// Callers that want strict INSERT (error on duplicate) should check
27    /// `pk_index().contains()` themselves before calling; callers that
28    /// want `ON CONFLICT DO NOTHING` semantics should use
29    /// [`Self::insert_if_absent`].
30    pub fn insert(&mut self, values: &[Value]) -> Result<MutationResult, ColumnarError> {
31        let pk_bytes = self.extract_pk_bytes(values)?;
32        let mut wal_records = Vec::with_capacity(2);
33
34        // Bitemporal collections preserve every version of a PK: each
35        // write appends a new row with a distinct `_ts_system` stamp and
36        // the prior row stays visible to `AS OF` queries. Skipping the
37        // upsert-tombstone here keeps compaction lossless without
38        // needing a separate "version-aware" delete bitmap. The PK
39        // index is still rebound below so current-state reads see the
40        // latest version.
41        let bitemporal = self.schema.is_bitemporal();
42
43        // If a prior row exists for this PK, tombstone it in place so
44        // subsequent scans skip the stale row. The PK index is rebound
45        // below to the freshly-appended row.
46        if !bitemporal && let Some(prior) = self.pk_index.get(&pk_bytes).copied() {
47            let bitmap = self.delete_bitmaps.entry(prior.segment_id).or_default();
48            bitmap.mark_deleted(prior.row_index);
49            wal_records.push(ColumnarWalRecord::DeleteRows {
50                collection: self.collection.clone(),
51                segment_id: prior.segment_id,
52                row_indices: vec![prior.row_index],
53            });
54        }
55
56        let row_data = encode_row_for_wal(values)?;
57        wal_records.push(ColumnarWalRecord::InsertRow {
58            collection: self.collection.clone(),
59            row_data,
60        });
61
62        self.memtable.append_row(values)?;
63        let location = RowLocation {
64            segment_id: self.memtable_segment_id,
65            row_index: self.memtable_row_counter,
66        };
67        self.pk_index.upsert(pk_bytes, location);
68        self.memtable_surrogates.push(None);
69        self.memtable_row_counter += 1;
70
71        Ok(MutationResult { wal_records })
72    }
73
74    /// Insert with a stable cross-engine surrogate identity.
75    ///
76    /// Identical to [`Self::insert`] but also records `surrogate` in the
77    /// per-row side-table so scan prefilters can perform bitmap membership
78    /// checks without a separate lookup pass.
79    pub fn insert_with_surrogate(
80        &mut self,
81        values: &[Value],
82        surrogate: Surrogate,
83    ) -> Result<MutationResult, ColumnarError> {
84        let pk_bytes = self.extract_pk_bytes(values)?;
85        let mut wal_records = Vec::with_capacity(2);
86
87        let bitemporal = self.schema.is_bitemporal();
88        if !bitemporal && let Some(prior) = self.pk_index.get(&pk_bytes).copied() {
89            let bitmap = self.delete_bitmaps.entry(prior.segment_id).or_default();
90            bitmap.mark_deleted(prior.row_index);
91            wal_records.push(ColumnarWalRecord::DeleteRows {
92                collection: self.collection.clone(),
93                segment_id: prior.segment_id,
94                row_indices: vec![prior.row_index],
95            });
96        }
97
98        let row_data = encode_row_for_wal(values)?;
99        wal_records.push(ColumnarWalRecord::InsertRow {
100            collection: self.collection.clone(),
101            row_data,
102        });
103
104        self.memtable.append_row(values)?;
105        let location = RowLocation {
106            segment_id: self.memtable_segment_id,
107            row_index: self.memtable_row_counter,
108        };
109        self.pk_index.upsert(pk_bytes, location);
110        self.memtable_surrogates.push(Some(surrogate));
111        self.memtable_row_counter += 1;
112
113        Ok(MutationResult { wal_records })
114    }
115
116    /// `INSERT ... ON CONFLICT DO NOTHING` semantics: append only if the
117    /// PK is absent; silently skip on duplicate.
118    ///
119    /// Returns `Ok(MutationResult { wal_records })` with an empty vector
120    /// when the row was skipped, so callers that batch WAL appends can
121    /// detect no-ops by checking `wal_records.is_empty()`.
122    pub fn insert_if_absent(&mut self, values: &[Value]) -> Result<MutationResult, ColumnarError> {
123        let pk_bytes = self.extract_pk_bytes(values)?;
124        if self.pk_index.contains(&pk_bytes) {
125            return Ok(MutationResult {
126                wal_records: Vec::new(),
127            });
128        }
129
130        let row_data = encode_row_for_wal(values)?;
131        let wal = ColumnarWalRecord::InsertRow {
132            collection: self.collection.clone(),
133            row_data,
134        };
135        self.memtable.append_row(values)?;
136        let location = RowLocation {
137            segment_id: self.memtable_segment_id,
138            row_index: self.memtable_row_counter,
139        };
140        self.pk_index.upsert(pk_bytes, location);
141        self.memtable_surrogates.push(None);
142        self.memtable_row_counter += 1;
143
144        Ok(MutationResult {
145            wal_records: vec![wal],
146        })
147    }
148
149    /// Look up the current row for a PK in the memtable, if present.
150    ///
151    /// Returns `None` if the PK is not in the index, or if the PK points
152    /// to a flushed segment (callers needing cross-segment lookup must
153    /// go through a segment reader separately). This is the fast path
154    /// used by `ON CONFLICT DO UPDATE` to read the would-be-merged row
155    /// when the duplicate hits the memtable — the common case under
156    /// back-to-back inserts.
157    pub fn lookup_memtable_row_by_pk(&self, pk_bytes: &[u8]) -> Option<Vec<Value>> {
158        let loc = self.pk_index.get(pk_bytes).copied()?;
159        if loc.segment_id != self.memtable_segment_id {
160            return None;
161        }
162        self.memtable.get_row(loc.row_index as usize)
163    }
164
165    /// Delete a row by PK value. Returns WAL record to persist.
166    ///
167    /// Looks up PK in the index to find the segment + row, then marks
168    /// the row in the segment's delete bitmap.
169    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        // Generate WAL record BEFORE applying.
179        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        // Mark in delete bitmap.
186        let bitmap = self.delete_bitmaps.entry(location.segment_id).or_default();
187        bitmap.mark_deleted(location.row_index);
188
189        // Remove from PK index.
190        self.pk_index.remove(&pk_bytes);
191
192        Ok(MutationResult {
193            wal_records: vec![wal],
194        })
195    }
196
197    /// Update a row by PK: DELETE old + INSERT new.
198    ///
199    /// `updates` maps column names to new values. Columns not in the map
200    /// retain their existing values from the old row.
201    ///
202    /// Returns WAL records for both the delete and the insert.
203    ///
204    /// NOTE: The caller must provide the full old row values for the re-insert.
205    /// This method takes the complete new row (already merged with old values).
206    pub fn update(
207        &mut self,
208        old_pk: &Value,
209        new_values: &[Value],
210    ) -> Result<MutationResult, ColumnarError> {
211        // Delete the old row.
212        let delete_result = self.delete(old_pk)?;
213
214        // Insert the new row.
215        let insert_result = self.insert(new_values)?;
216
217        // Combine WAL records.
218        let mut wal_records = delete_result.wal_records;
219        wal_records.extend(insert_result.wal_records);
220
221        Ok(MutationResult { wal_records })
222    }
223}