Skip to main content

linkrs_storage/edge/csr_with_properties/
mapping.rs

1use super::{CsrWithProperties, RowVisibility, UNMAPPED_ROW};
2use linkrs_core::types::{EdgeId, Timestamp, INVALID_EDGE_ID};
3use linkrs_core::{StorageError, StorageResult, Value};
4use std::sync::Arc;
5
6impl CsrWithProperties {
7    fn segment_of(slot: usize) -> (usize, usize) {
8        debug_assert_eq!(Self::EDGE_MAP_SEGMENT_ROWS, 1024);
9        (
10            slot / Self::EDGE_MAP_SEGMENT_ROWS,
11            slot % Self::EDGE_MAP_SEGMENT_ROWS,
12        )
13    }
14
15    /// Row mapped to `edge_id`, or `None` for unmapped ids.
16    ///
17    /// Edge ids are table-allocated dense values; untouched segments hold no
18    /// allocation, so this is one segment lookup plus one indexed read.
19    pub(crate) fn mapped_row(&self, edge_id: EdgeId) -> Option<usize> {
20        let (seg, off) = Self::segment_of(edge_id.0 as usize);
21        let pos = self.edge_map_segments.get(seg)?.as_ref()?[off];
22        (pos != UNMAPPED_ROW).then_some(pos as usize)
23    }
24
25    /// Record the `edge_id` to `row_idx` mapping, allocating the segment on demand.
26    ///
27    /// Rejects the unassignable gap sentinel explicitly: gap slots never
28    /// own property rows.
29    pub(crate) fn map_insert(&mut self, edge_id: EdgeId, row_idx: usize) -> StorageResult<()> {
30        if edge_id == INVALID_EDGE_ID {
31            return Err(StorageError::invalid_operation(
32                "unassignable edge id owns no property row",
33            ));
34        }
35        let slot = edge_id.0 as usize;
36        let (seg, off) = Self::segment_of(slot);
37        if seg >= self.edge_map_segments.len() {
38            self.edge_map_segments.resize_with(seg + 1, || None);
39        }
40        let segment = self.edge_map_segments[seg]
41            .get_or_insert_with(|| Box::new([UNMAPPED_ROW; Self::EDGE_MAP_SEGMENT_ROWS]));
42        if segment[off] == UNMAPPED_ROW {
43            self.edge_map_len += 1;
44        }
45        segment[off] = row_idx as u32;
46        Ok(())
47    }
48
49    /// Drop the mapping for `edge_id`, returning its former row.
50    ///
51    /// Edge identifiers never recycle: removal only clears the slot and
52    /// releases empty segments, never returns the id to an allocator. Loads
53    /// rebuild the map in a single pass from persisted rows.
54    pub(crate) fn map_remove(&mut self, edge_id: EdgeId) -> Option<usize> {
55        let slot = edge_id.0 as usize;
56        let (seg, off) = Self::segment_of(slot);
57        let segment = self.edge_map_segments.get_mut(seg)?.as_mut()?;
58        if segment[off] == UNMAPPED_ROW {
59            return None;
60        }
61        let former = segment[off] as usize;
62        segment[off] = UNMAPPED_ROW;
63        self.edge_map_len = self.edge_map_len.saturating_sub(1);
64        if segment.iter().all(|pos| *pos == UNMAPPED_ROW) {
65            self.edge_map_segments[seg] = None;
66        }
67        self.truncate_empty_tail_segments();
68        Some(former)
69    }
70
71    /// Release trailing empty segments so churned id ranges never pin memory.
72    fn truncate_empty_tail_segments(&mut self) {
73        while self
74            .edge_map_segments
75            .last()
76            .is_some_and(|seg| seg.is_none())
77        {
78            self.edge_map_segments.pop();
79        }
80    }
81
82    pub(crate) fn ensure_row_aux_len(&mut self, len: usize) {
83        if self.row_to_edge.len() < len {
84            self.row_to_edge.resize(len, None);
85        }
86    }
87
88    /// Allocate a new row and populate it with the given values.
89    /// Returns the row index (0-based).
90    pub(crate) fn allocate_row(
91        &mut self,
92        values: &[(Arc<str>, Value)],
93        create_ts: Timestamp,
94    ) -> StorageResult<usize> {
95        // Resolve names through the schema index once per call; the hot
96        // write path passes pre-resolved positions straight to
97        // `allocate_row_at` instead. Unknown names keep the historical
98        // behavior of falling back to the column default.
99        let mut positioned: Vec<(usize, Value)> = Vec::with_capacity(values.len());
100        for (name, value) in values {
101            if let Some(&idx) = self.column_index.get(&**name) {
102                positioned.push((idx, value.clone()));
103            }
104        }
105        self.allocate_row_at(&positioned, create_ts)
106    }
107
108    /// Allocate a new row from pre-resolved `(column position, value)` pairs.
109    ///
110    /// Positions come from the schema index held by the caller, so this
111    /// performs no name lookup or string comparison per edge. Columns with
112    /// no provided value take their default. An out-of-range position is a
113    /// caller bug and fails loudly instead of writing the wrong column.
114    /// The creation stamp must come from the timestamp allocator: zero and
115    /// the two top sentinels are reserved and rejected here. Row lifecycle
116    /// follows the explicit virgin/live/deleted state machine.
117    pub(crate) fn allocate_row_at(
118        &mut self,
119        positioned: &[(usize, Value)],
120        create_ts: Timestamp,
121    ) -> StorageResult<usize> {
122        if !linkrs_core::types::is_allocatable_timestamp(create_ts) {
123            return Err(StorageError::invalid_input(format!(
124                "property row carries reserved creation timestamp {}",
125                create_ts
126            )));
127        }
128        for (idx, _) in positioned {
129            if *idx >= self.property_schema.len() {
130                return Err(StorageError::column_not_found(format!(
131                    "property column position out of range: {}",
132                    idx
133                )));
134            }
135        }
136        let row_idx = if let Some(free_off) = self.free_list.pop() {
137            let idx = free_off as usize;
138            if idx >= self.visibility.len() {
139                self.visibility.resize(idx + 1, RowVisibility::new(0));
140            }
141            self.visibility[idx] = RowVisibility::new(create_ts);
142            self.row_count += 1;
143            idx
144        } else {
145            let idx = self.visibility.len();
146            self.visibility.push(RowVisibility::new(create_ts));
147            self.row_count += 1;
148            idx
149        };
150        self.ensure_row_aux_len(self.visibility.len());
151        self.row_to_edge[row_idx] = None;
152        // Extend column data buffer for the new row without generating
153        // a spurious [0, create_ts) version chain entry. We do this by
154        // writing directly to the column's internal buffer and setting
155        // the correct visibility timestamp.
156        let mut provided: Vec<Option<&Value>> = vec![None; self.property_schema.len()];
157        for (idx, value) in positioned {
158            // First occurrence wins, matching the historical name-matched
159            // behavior where each column took its first matching value.
160            if provided[*idx].is_none() {
161                provided[*idx] = Some(value);
162            }
163        }
164        for (i, slot) in provided.into_iter().enumerate() {
165            let col = &mut self.property_columns[i];
166            match slot {
167                Some(v) => {
168                    // Value provided: versioned write with the given value
169                    col.set_versioned(row_idx, Some(v), create_ts)?;
170                }
171                None => {
172                    // No value provided: use default value if available, otherwise None
173                    let default_val = self.property_schema[i].default_value.clone();
174                    col.set_with_timestamp(row_idx, default_val.as_ref(), create_ts)?;
175                }
176            }
177            self.mark_column_dirty_at(i);
178        }
179        Ok(row_idx)
180    }
181
182    /// Release a row back to the free list without leaving an orphan.
183    ///
184    /// Clears the visibility stamp so the slot is skipped by reads and GC
185    /// scans, drops the edge mapping via the reverse index, and queues the
186    /// slot for reuse. Reused slots are fully overwritten by `allocate_row`.
187    /// Idempotent via the explicit state machine: releasing an already-free
188    /// or out-of-range row is a no-op, and virgin slots never enter the free
189    /// list twice so no separate membership set is needed.
190    /// Slots that were never used are never admitted to the free list.
191    pub fn release_row(&mut self, row_idx: usize) {
192        if row_idx >= self.visibility.len() {
193            return;
194        }
195        if self.visibility[row_idx].is_virgin() {
196            if let Some(slot) = self.row_to_edge.get_mut(row_idx) {
197                if let Some(edge_id) = slot.take() {
198                    self.map_remove(edge_id);
199                }
200            }
201            return;
202        }
203        self.visibility[row_idx].create_ts = 0;
204        self.visibility[row_idx].delete_ts = None;
205        self.row_count = self.row_count.saturating_sub(1);
206        if let Some(slot) = self.row_to_edge.get_mut(row_idx) {
207            if let Some(edge_id) = slot.take() {
208                self.map_remove(edge_id);
209            }
210        }
211        // Released rows are virgin by construction, so a second release
212        // takes the virgin path above: slots never enter the free list twice
213        // and no membership set is needed.
214        self.free_list.push(row_idx as u32);
215    }
216
217    /// Get the row index for an edge.
218    pub fn get_row_for_edge(&self, edge_id: EdgeId) -> Option<usize> {
219        self.mapped_row(edge_id)
220    }
221
222    /// Remove edge-to-row mapping and return the row index.
223    pub fn remove_edge_mapping(&mut self, edge_id: EdgeId) -> Option<usize> {
224        if self.inline {
225            return None;
226        }
227        let pos = self.map_remove(edge_id)?;
228        if let Some(slot) = self.row_to_edge.get_mut(pos) {
229            if *slot == Some(edge_id) {
230                *slot = None;
231            }
232        }
233        Some(pos)
234    }
235
236    /// Whether the store holds a row mapping for `edge_id`.
237    pub fn contains_edge(&self, edge_id: EdgeId) -> bool {
238        self.mapped_row(edge_id).is_some()
239    }
240
241    /// Iterate over all edge->row mappings in slot order (for compaction).
242    pub fn edge_mappings(&self) -> impl Iterator<Item = (EdgeId, u32)> + '_ {
243        self.edge_map_segments
244            .iter()
245            .enumerate()
246            .filter_map(|(seg, segment)| {
247                segment.as_ref().map(|values| {
248                    let base = seg * Self::EDGE_MAP_SEGMENT_ROWS;
249                    values.iter().enumerate().filter_map(move |(off, row)| {
250                        (*row != UNMAPPED_ROW).then_some((EdgeId((base + off) as u64), *row))
251                    })
252                })
253            })
254            .flatten()
255    }
256
257    pub fn edge_ids(&self) -> impl Iterator<Item = EdgeId> + '_ {
258        self.edge_mappings().map(|(edge_id, _)| edge_id)
259    }
260
261    pub fn row_count(&self) -> usize {
262        self.row_count
263    }
264
265    /// Segment granularity for sparse map scans. One segment covers this
266    /// many edge ids; empty segments are skipped by segment-aware scans so
267    /// sparse id ranges never pay a full dense walk.
268    pub const EDGE_MAP_SEGMENT_ROWS: usize = 1024;
269
270    /// Ids of segments holding at least one live mapping.
271    pub fn nonempty_map_segments(&self) -> Vec<usize> {
272        self.edge_map_segments
273            .iter()
274            .enumerate()
275            .filter_map(|(seg, segment)| {
276                segment
277                    .as_ref()
278                    .and_then(|values| values.iter().any(|row| *row != UNMAPPED_ROW).then_some(seg))
279            })
280            .collect()
281    }
282
283    /// Mappings within one segment, for segment-skipping scans.
284    pub fn edge_mappings_in_segment(
285        &self,
286        segment: usize,
287    ) -> impl Iterator<Item = (EdgeId, u32)> + '_ {
288        let base = segment * Self::EDGE_MAP_SEGMENT_ROWS;
289        self.edge_map_segments
290            .get(segment)
291            .and_then(|segment| segment.as_ref())
292            .map(|values| {
293                values.iter().enumerate().filter_map(move |(off, row)| {
294                    (*row != UNMAPPED_ROW).then_some((EdgeId((base + off) as u64), *row))
295                })
296            })
297            .into_iter()
298            .flatten()
299    }
300
301    /// Sparse map memory in bytes, for observability.
302    pub fn edge_map_memory_bytes(&self) -> usize {
303        self.edge_map_segments
304            .iter()
305            .filter(|segment| segment.is_some())
306            .count()
307            * Self::EDGE_MAP_SEGMENT_ROWS
308            * std::mem::size_of::<u32>()
309    }
310}