Skip to main content

linkrs_storage/edge/csr_with_properties/
persistence.rs

1use super::{CsrWithProperties, RowVisibility, FSST_MAX_SYMBOLS};
2use linkrs_core::types::{EdgeId, INVALID_EDGE_ID};
3use linkrs_core::{StorageError, StorageResult, Value};
4use std::collections::HashSet;
5
6impl CsrWithProperties {
7    pub fn dump(&self) -> Vec<u8> {
8        // Current-value snapshot plus per-column identity, encoding choice
9        // and statistics. Row version chains stay memory-only by design:
10        // load restores plain values holding the latest value with the row
11        // creation stamp, then re-applies the recorded encodings.
12        let mut buf = Vec::new();
13        buf.extend_from_slice(&(self.visibility.len() as u32).to_le_bytes());
14        for vis in &self.visibility {
15            buf.extend_from_slice(&vis.create_ts.to_le_bytes());
16            if let Some(del) = vis.delete_ts {
17                buf.push(1);
18                buf.extend_from_slice(&del.to_le_bytes());
19            } else {
20                buf.push(0);
21            }
22        }
23        buf.extend_from_slice(&(self.row_count as u32).to_le_bytes());
24        buf.extend_from_slice(&(self.edge_map_len as u32).to_le_bytes());
25        for (edge_id, pos) in self.edge_mappings() {
26            buf.extend_from_slice(&(edge_id.0).to_le_bytes());
27            buf.extend_from_slice(&pos.to_le_bytes());
28        }
29        buf.extend_from_slice(&(self.free_list.len() as u32).to_le_bytes());
30        for &off in &self.free_list {
31            buf.extend_from_slice(&off.to_le_bytes());
32        }
33        // Serialize current column values (without version history) keyed by
34        // column name, each carrying its stable identifier, its encoding
35        // choice and its last refreshed statistics.
36        buf.extend_from_slice(&(self.property_columns.len() as u32).to_le_bytes());
37        for (idx, col) in self.property_columns.iter().enumerate() {
38            // The schema entry is the single name authority: the loader keys
39            // the payload back through `column_index`, which is built from it.
40            let schema = &self.property_schema[idx];
41            buf.extend_from_slice(&(schema.name.len() as u32).to_le_bytes());
42            buf.extend_from_slice(schema.name.as_bytes());
43            buf.extend_from_slice(&schema.prop_id.to_le_bytes());
44            buf.push(col.encoding_type().to_u8());
45            let rows = self.visibility.len();
46            buf.extend_from_slice(&(rows as u32).to_le_bytes());
47            buf.reserve(rows.saturating_mul(16));
48            let mut cell_scratch: Vec<u8> = Vec::with_capacity(64);
49            for row_idx in 0..rows {
50                let val = col.get(row_idx);
51                if let Some(v) = val {
52                    buf.push(1);
53                    cell_scratch.clear();
54                    let taken = std::mem::take(&mut cell_scratch);
55                    match postcard::to_extend(&v, taken) {
56                        Ok(encoded) => {
57                            buf.extend_from_slice(&(encoded.len() as u32).to_le_bytes());
58                            buf.extend_from_slice(&encoded);
59                            cell_scratch = encoded;
60                        }
61                        Err(_) => {
62                            buf.extend_from_slice(&0u32.to_le_bytes());
63                            cell_scratch = Vec::with_capacity(64);
64                        }
65                    }
66                } else {
67                    buf.push(0);
68                }
69            }
70            match col.stats() {
71                Some(stats) => {
72                    let mut stats_buf = Vec::new();
73                    if stats.serialize_meta(&mut stats_buf).is_ok() {
74                        buf.push(1);
75                        buf.extend_from_slice(&(stats_buf.len() as u32).to_le_bytes());
76                        buf.extend_from_slice(&stats_buf);
77                    } else {
78                        buf.push(0);
79                    }
80                }
81                None => buf.push(0),
82            }
83        }
84        buf
85    }
86
87    pub fn load(&mut self, data: &[u8]) -> StorageResult<()> {
88        fn need(data: &[u8], offset: usize, len: usize, what: &str) -> StorageResult<()> {
89            if data.len().saturating_sub(offset) < len {
90                return Err(StorageError::deserialize_error(format!(
91                    "properties payload too short for {}",
92                    what
93                )));
94            }
95            Ok(())
96        }
97        if data.is_empty() {
98            return Err(StorageError::deserialize_error(
99                "properties payload is empty",
100            ));
101        }
102        let mut offset = 0usize;
103        need(data, offset, 4, "visibility length")?;
104        let vis_len = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
105        offset += 4;
106        self.visibility.clear();
107        self.visibility.reserve(vis_len);
108        for _ in 0..vis_len {
109            need(data, offset, 8, "row creation stamp")?;
110            let create = u64::from_le_bytes(data[offset..offset + 8].try_into().unwrap());
111            offset += 8;
112            need(data, offset, 1, "row deletion flag")?;
113            let has_del = data[offset];
114            offset += 1;
115            let del = if has_del == 1 {
116                need(data, offset, 8, "row deletion stamp")?;
117                let d = u64::from_le_bytes(data[offset..offset + 8].try_into().unwrap());
118                offset += 8;
119                Some(d)
120            } else if has_del == 0 {
121                None
122            } else {
123                return Err(StorageError::deserialize_error(format!(
124                    "invalid row deletion flag: {}",
125                    has_del
126                )));
127            };
128            // Free slots persist as zero creation with no deletion mark;
129            // any deletion-marked row must carry an allocator stamp. Zero is
130            // reserved and marks virgin rows in the explicit state machine.
131            if del.is_some() && !linkrs_core::types::is_allocatable_timestamp(create) {
132                return Err(StorageError::deserialize_error(format!(
133                    "property row carries reserved creation stamp {} with a deletion mark",
134                    create
135                )));
136            }
137            self.visibility.push(RowVisibility {
138                create_ts: create,
139                delete_ts: del,
140            });
141        }
142        need(data, offset, 4, "row count")?;
143        self.row_count = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
144        offset += 4;
145        need(data, offset, 4, "edge map length")?;
146        let map_len = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
147        offset += 4;
148        self.edge_map_segments.clear();
149        self.edge_map_len = 0;
150        // Wire format is unchanged (entry count plus id/row pairs); the sparse
151        // map is rebuilt from the pairs. Rows must land inside the restored
152        // visibility window, otherwise the payload is rejected.
153        let vis_len = self.visibility.len();
154        for _ in 0..map_len {
155            need(data, offset, 12, "edge map entry")?;
156            let eid = u64::from_le_bytes(data[offset..offset + 8].try_into().unwrap());
157            offset += 8;
158            let pos = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
159            offset += 4;
160            if eid == INVALID_EDGE_ID.0 || (pos as usize) >= vis_len {
161                return Err(StorageError::deserialize_error(
162                    "edge map entry outside the restored row window",
163                ));
164            }
165            if self.mapped_row(EdgeId(eid)).is_some() {
166                return Err(StorageError::deserialize_error(
167                    "duplicate edge map entry in properties payload",
168                ));
169            }
170            self.map_insert(EdgeId(eid), pos as usize)?;
171        }
172        // Mapped rows own live property state, so a zero creation stamp
173        // here means the payload confuses a free slot with a live row.
174        for (edge_id, pos) in self.edge_mappings() {
175            let owned = self
176                .visibility
177                .get(pos as usize)
178                .map(|vis| linkrs_core::types::is_allocatable_timestamp(vis.create_ts))
179                .unwrap_or(false);
180            if !owned {
181                return Err(StorageError::deserialize_error(format!(
182                    "property mapping for edge {:?} lands on a free row",
183                    edge_id
184                )));
185            }
186        }
187        need(data, offset, 4, "free list length")?;
188        let free_len = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
189        offset += 4;
190        self.free_list.clear();
191        for _ in 0..free_len {
192            need(data, offset, 4, "free list entry")?;
193            let off = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
194            offset += 4;
195            self.free_list.push(off);
196        }
197        need(data, offset, 4, "column count")?;
198        {
199            let col_count =
200                u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
201            offset += 4;
202            let mut seen_names = HashSet::new();
203            let mut seen_ids = HashSet::new();
204            for _ in 0..col_count {
205                need(data, offset, 4, "column name length")?;
206                let name_len =
207                    u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
208                offset += 4;
209                need(data, offset, name_len, "column name")?;
210                let name = String::from_utf8_lossy(&data[offset..offset + name_len]).to_string();
211                offset += name_len;
212                if !seen_names.insert(name.clone()) {
213                    return Err(StorageError::deserialize_error(format!(
214                        "duplicate column in properties payload: {}",
215                        name
216                    )));
217                }
218                need(data, offset, 4, "column identifier")?;
219                let prop_id = i32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
220                offset += 4;
221                if !seen_ids.insert(prop_id) {
222                    return Err(StorageError::deserialize_error(format!(
223                        "duplicate column identifier in properties payload: {}",
224                        prop_id
225                    )));
226                }
227                need(data, offset, 1, "column encoding")?;
228                let encoding_tag = data[offset];
229                offset += 1;
230                if encoding_tag > 6 {
231                    return Err(StorageError::deserialize_error(format!(
232                        "unknown column encoding tag: {}",
233                        encoding_tag
234                    )));
235                }
236                let encoding = crate::encoding::EncodingType::from_u8(encoding_tag);
237                need(data, offset, 4, "column row count")?;
238                let rows =
239                    u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
240                offset += 4;
241                if let Some(&col_idx) = self.column_index.get(name.as_str()) {
242                    self.property_schema[col_idx].prop_id = prop_id;
243                    let col = &mut self.property_columns[col_idx];
244                    col.col_id = prop_id;
245                    if col.len() < rows {
246                        col.resize(rows);
247                    }
248                    for row_idx in 0..rows {
249                        need(data, offset, 1, "column cell flag")?;
250                        let has = data[offset];
251                        offset += 1;
252                        if has == 1 {
253                            need(data, offset, 4, "column cell length")?;
254                            let vlen =
255                                u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap())
256                                    as usize;
257                            offset += 4;
258                            need(data, offset, vlen, "column cell value")?;
259                            let vbytes = &data[offset..offset + vlen];
260                            offset += vlen;
261                            if let Ok(val) = postcard::from_bytes::<Value>(vbytes) {
262                                let _ = col.set(row_idx, Some(&val));
263                            }
264                        } else if has == 0 {
265                            let _ = col.set(row_idx, None);
266                        } else {
267                            return Err(StorageError::deserialize_error(format!(
268                                "invalid column cell flag: {}",
269                                has
270                            )));
271                        }
272                    }
273                    need(data, offset, 1, "column stats flag")?;
274                    let has_stats = data[offset];
275                    offset += 1;
276                    if has_stats == 1 {
277                        need(data, offset, 4, "column stats length")?;
278                        let stats_len =
279                            u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap())
280                                as usize;
281                        offset += 4;
282                        need(data, offset, stats_len, "column stats")?;
283                        let stats_bytes = &data[offset..offset + stats_len];
284                        offset += stats_len;
285                        let mut cursor = stats_bytes;
286                        let stats =
287                            crate::column_stats::ColumnStats::deserialize_meta(&mut cursor)?;
288                        if !cursor.is_empty() {
289                            return Err(StorageError::deserialize_error(
290                                "unexpected trailing data in column stats".to_string(),
291                            ));
292                        }
293                        if encoding != crate::encoding::EncodingType::None {
294                            self.apply_encoding_to_column(&name, encoding, FSST_MAX_SYMBOLS)?;
295                        }
296                        let col = &mut self.property_columns[col_idx];
297                        col.set_stats(stats);
298                    } else if has_stats == 0 {
299                        if encoding != crate::encoding::EncodingType::None {
300                            self.apply_encoding_to_column(&name, encoding, FSST_MAX_SYMBOLS)?;
301                        }
302                    } else {
303                        return Err(StorageError::deserialize_error(format!(
304                            "invalid column stats flag: {}",
305                            has_stats
306                        )));
307                    }
308                } else {
309                    for _ in 0..rows {
310                        need(data, offset, 1, "unknown column cell flag")?;
311                        let has = data[offset];
312                        offset += 1;
313                        if has == 1 {
314                            need(data, offset, 4, "unknown column cell length")?;
315                            let vlen =
316                                u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap())
317                                    as usize;
318                            offset += 4;
319                            need(data, offset, vlen, "unknown column cell value")?;
320                            offset += vlen;
321                        } else if has != 0 {
322                            return Err(StorageError::deserialize_error(format!(
323                                "invalid unknown column cell flag: {}",
324                                has
325                            )));
326                        }
327                    }
328                    // Unpublished columns keep the abort-on-reload contract:
329                    // skip their cells, encoding tag and statistics alike.
330                    need(data, offset, 1, "unknown column stats flag")?;
331                    let unknown_stats = data[offset];
332                    offset += 1;
333                    if unknown_stats == 1 {
334                        need(data, offset, 4, "unknown column stats length")?;
335                        let stats_len =
336                            u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap())
337                                as usize;
338                        offset += 4;
339                        need(data, offset, stats_len, "unknown column stats")?;
340                        offset += stats_len;
341                    } else if unknown_stats != 0 {
342                        return Err(StorageError::deserialize_error(format!(
343                            "invalid unknown column stats flag: {}",
344                            unknown_stats
345                        )));
346                    }
347                }
348            }
349        }
350        if offset != data.len() {
351            return Err(StorageError::deserialize_error(
352                "unexpected trailing data in properties payload".to_string(),
353            ));
354        }
355        self.rebuild_aux_indexes();
356        self.dirty_columns.clear();
357        self.recompute_history_floor();
358        Ok(())
359    }
360
361    /// Recompute the attribute time-travel floor after any load or merge.
362    ///
363    /// Dropped version chains start a new history epoch at the maximum
364    /// creation stamp; strict reads below it fail instead of serving the
365    /// current value as history. Zero on empty stores, disabling the fence.
366    pub(crate) fn recompute_history_floor(&mut self) {
367        self.history_floor = self
368            .visibility
369            .iter()
370            .map(|vis| vis.create_ts)
371            .filter(|ts| linkrs_core::types::is_allocatable_timestamp(*ts))
372            .max()
373            .unwrap_or(0);
374    }
375
376    fn rebuild_aux_indexes(&mut self) {
377        self.row_to_edge.clear();
378        self.row_to_edge.resize(self.visibility.len(), None);
379        let mappings: Vec<(EdgeId, u32)> = self.edge_mappings().collect();
380        for (edge_id, pos) in mappings {
381            let idx = pos as usize;
382            if idx < self.row_to_edge.len() {
383                self.row_to_edge[idx] = Some(edge_id);
384            }
385        }
386        self.rebuild_schema_indexes();
387        let max_id = self
388            .property_schema
389            .iter()
390            .map(|s| s.prop_id)
391            .max()
392            .unwrap_or(-1);
393        self.next_prop_id = max_id
394            .saturating_add(1)
395            .max(self.property_schema.len() as i32);
396    }
397}