1use super::super::csr_shared::{
2 can_revert_delete, decide_slot_delete, decode_endpoint_pair, is_reclaimable_cold,
3 DeleteSlotOutcome,
4};
5use super::super::{ColdStamps, EdgeId, HotNbr, Nbr, RowEdgeBatch, Timestamp, VertexId};
6use super::core::REUSE_HINT_UNKNOWN;
7use super::overflow::OVERFLOW_REPACK_CHUNKS_PER_VERTEX;
8use super::MutableCsr;
9use linkrs_core::{StorageError, StorageResult};
10use std::collections::HashSet;
11
12const TOMBSTONE_REUSE_SCAN_BOUND: usize = 64;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum EdgePosition {
34 Primary { slot: u32 },
35 Overflow { chunk: u32, slot: u32 },
36}
37
38impl MutableCsr {
39 fn append_overflow(&mut self, src_vid: u32, nbr: Nbr, live_hint: usize) {
40 let chunk_edges = self.effective_chunk_edges(live_hint);
41 let (chunk_count, added) = self.overflow_chunks.push_to_row(src_vid, nbr, chunk_edges);
44 if let Some(new_cap) = added {
45 self.add_capacity(new_cap);
46 self.overflow_chunk_allocs += 1;
47 }
48 if nbr.delete_ts == Timestamp::MAX {
49 let position = self
54 .overflow_chunks
55 .get(src_vid)
56 .map(|chunks| {
57 let chunk = chunks.len().saturating_sub(1);
58 let slot = chunks
59 .last()
60 .map(|tail| tail.len().saturating_sub(1))
61 .unwrap_or(0);
62 EdgePosition::Overflow {
63 chunk: chunk as u32,
64 slot: slot as u32,
65 }
66 })
67 .unwrap_or(EdgePosition::Overflow {
68 chunk: chunk_count.saturating_sub(1) as u32,
69 slot: 0,
70 });
71 self.track_live_insert(src_vid, nbr.endpoint, nbr.rank, position);
72 }
73 if chunk_count > OVERFLOW_REPACK_CHUNKS_PER_VERTEX {
81 let mut noop = |_id: EdgeId, _ts: Timestamp| {};
82 self.compact_overflow_for_vertex(src_vid, Timestamp::MAX, &mut noop);
83 self.repack_count += 1;
84 }
85 }
86
87 pub fn insert_edge(
89 &mut self,
90 src_vid: u32,
91 dst: VertexId,
92 edge_id: EdgeId,
93 ts: Timestamp,
94 ) -> StorageResult<()> {
95 let (decoded_endpoint, decoded_rank) = decode_endpoint_pair(dst).ok_or_else(|| {
96 StorageError::invalid_input(format!("Malformed edge endpoint key: {}", dst))
97 })?;
98
99 let src_idx = src_vid as usize;
100
101 if src_idx >= self.vertex_capacity() {
102 self.ensure_vertex_capacity(src_idx + 1);
103 }
104
105 if self.rows.primary_capacities[src_idx] == 0 {
107 self.allocate_primary_block(src_idx);
108 }
109
110 let live = if let Some(set) = self.live_sets.get(&src_vid) {
116 if set.contains(&(decoded_endpoint, decoded_rank)) {
117 return Err(StorageError::edge_already_exists(format!(
118 "{} -> {:?}",
119 src_vid, dst
120 )));
121 }
122 set.len()
123 } else {
124 let (present, live) = self.row_live_scan(src_vid, decoded_endpoint, decoded_rank);
125 if present {
126 return Err(StorageError::edge_already_exists(format!(
127 "{} -> {:?}",
128 src_vid, dst
129 )));
130 }
131 live
132 };
133
134 let nbr_with_ts = Nbr::with_create_ts(decoded_endpoint, decoded_rank, edge_id, ts);
135
136 let degree = self.rows.degrees[src_idx] as usize;
141 if degree < self.rows.primary_capacities[src_idx] as usize {
142 let base = self.rows.adj_offsets[src_idx] as usize;
143 self.set_slot(base + degree, nbr_with_ts);
144 self.mark_primary_unsorted(src_idx);
145 self.rows.degrees[src_idx] += 1;
146 self.live_counts[src_idx] += 1;
147 self.track_live_insert(
148 src_vid,
149 decoded_endpoint,
150 decoded_rank,
151 EdgePosition::Primary {
152 slot: degree as u32,
153 },
154 );
155 self.edge_count += 1;
156 return Ok(());
157 }
158
159 if self.tombstone_reuse_cutoff != Timestamp::MAX {
168 let base = self.rows.adj_offsets[src_idx] as usize;
169 let cutoff = self.tombstone_reuse_cutoff;
170 if let Some(&hint) = self.reuse_hint.get(src_idx) {
171 if hint != REUSE_HINT_UNKNOWN {
172 let slot = hint as usize;
173 if slot < degree {
174 let reclaimable = self
175 .cold_at(base + slot)
176 .is_some_and(|cold| is_reclaimable_cold(&cold, cutoff));
177 if reclaimable {
178 self.tombstone_reuse_hint_hits += 1;
179 self.set_slot(base + slot, nbr_with_ts);
180 self.mark_primary_unsorted(src_idx);
181 self.invalidate_reuse_hint(src_idx);
182 self.live_counts[src_idx] += 1;
183 self.tombstone_counts[src_idx] =
184 self.tombstone_counts[src_idx].saturating_sub(1);
185 self.track_live_insert(
186 src_vid,
187 decoded_endpoint,
188 decoded_rank,
189 EdgePosition::Primary { slot: slot as u32 },
190 );
191 self.edge_count += 1;
192 self.tombstone_reuse_count += 1;
193 return Ok(());
194 }
195 }
196 self.tombstone_reuse_hint_misses += 1;
197 self.invalidate_reuse_hint(src_idx);
198 }
199 }
200 let bound = degree.min(TOMBSTONE_REUSE_SCAN_BOUND);
201 for i in 0..bound {
202 let reclaimable = self
203 .cold_at(base + i)
204 .is_some_and(|cold| is_reclaimable_cold(&cold, cutoff));
205 if reclaimable {
206 self.set_slot(base + i, nbr_with_ts);
207 self.mark_primary_unsorted(src_idx);
208 self.invalidate_reuse_hint(src_idx);
209 self.live_counts[src_idx] += 1;
210 self.tombstone_counts[src_idx] =
211 self.tombstone_counts[src_idx].saturating_sub(1);
212 self.track_live_insert(
213 src_vid,
214 decoded_endpoint,
215 decoded_rank,
216 EdgePosition::Primary { slot: i as u32 },
217 );
218 self.edge_count += 1;
219 self.tombstone_reuse_count += 1;
220 return Ok(());
221 }
222 }
223 }
224
225 self.append_overflow(src_vid, nbr_with_ts, live.saturating_add(1));
226 self.live_counts[src_idx] += 1;
227 self.edge_count += 1;
228 Ok(())
229 }
230
231 fn scan_overflow_for_edge_id(&self, src_vid: u32, edge_id: EdgeId) -> Option<(usize, usize)> {
237 let chunks = self.overflow_chunks.get(src_vid)?;
238 for (chunk_idx, chunk) in chunks.iter().enumerate() {
239 for (edge_idx, hot) in chunk.hot_slice().iter().enumerate() {
240 if hot.edge_id == edge_id {
241 return Some((chunk_idx, edge_idx));
242 }
243 }
244 }
245 None
246 }
247
248 pub fn delete_edge(
256 &mut self,
257 src_vid: u32,
258 edge_id: EdgeId,
259 ts: Timestamp,
260 ) -> StorageResult<bool> {
261 let src_idx = src_vid as usize;
262 if src_idx >= self.vertex_capacity() {
263 return Ok(false);
264 }
265
266 let found = {
269 let (hot, cold) = self.primary_pair(src_idx);
270 hot.iter()
271 .zip(cold.iter())
272 .enumerate()
273 .find_map(|(i, (h, c))| {
274 (h.edge_id == edge_id).then(|| (i, Nbr::from_parts(*h, *c)))
275 })
276 };
277 if let Some((i, probe)) = found {
278 match decide_slot_delete(&probe, edge_id, ts)? {
279 DeleteSlotOutcome::AlreadyStamped => {
280 return Ok(false);
281 }
282 DeleteSlotOutcome::Stamped => {}
283 }
284 let (start, _) = self.primary_window(src_idx);
285 self.cold_list[start + i].delete_ts = ts;
286 self.edge_count -= 1;
287 self.live_counts[src_idx] = self.live_counts[src_idx].saturating_sub(1);
288 self.tombstone_counts[src_idx] += 1;
289 self.note_primary_tombstone(src_idx, i);
290 self.track_live_remove(src_vid, probe.endpoint, probe.rank);
291 return Ok(true);
292 }
293
294 if let Some((chunk_idx, edge_idx)) = self.scan_overflow_for_edge_id(src_vid, edge_id) {
296 let probe = {
298 let chunks = self.overflow_chunks.get(src_vid).unwrap();
299 chunks[chunk_idx].slot_at(edge_idx).unwrap()
300 };
301 if let Some(chunks) = self.overflow_chunks.get_mut(src_vid) {
302 match decide_slot_delete(&probe, edge_id, ts)? {
303 DeleteSlotOutcome::AlreadyStamped => {
304 return Ok(false);
305 }
306 DeleteSlotOutcome::Stamped => {}
307 }
308 chunks[chunk_idx].cold_at_mut(edge_idx).unwrap().delete_ts = ts;
309 self.edge_count -= 1;
310 self.live_counts[src_idx] = self.live_counts[src_idx].saturating_sub(1);
311 self.tombstone_counts[src_idx] += 1;
312 self.track_live_remove(src_vid, probe.endpoint, probe.rank);
313 return Ok(true);
314 }
315 }
316
317 Ok(false)
318 }
319
320 pub fn delete_edge_by_dst_reporting(
329 &mut self,
330 src_vid: u32,
331 dst: VertexId,
332 ts: Timestamp,
333 on_deleted: &mut dyn FnMut(EdgeId),
334 ) -> usize {
335 let mut noop_pos = |edge_id: EdgeId, _position: EdgePosition| {
336 on_deleted(edge_id);
337 };
338 self.delete_edge_by_dst_reporting_positioned(src_vid, dst, ts, &mut noop_pos)
339 }
340
341 pub fn delete_edge_by_dst_reporting_positioned(
348 &mut self,
349 src_vid: u32,
350 dst: VertexId,
351 ts: Timestamp,
352 on_deleted: &mut dyn FnMut(EdgeId, EdgePosition),
353 ) -> usize {
354 let Some((decoded_endpoint, decoded_rank)) = decode_endpoint_pair(dst) else {
355 return 0;
356 };
357 let src_idx = src_vid as usize;
358 if src_idx >= self.vertex_capacity() {
359 return 0;
360 }
361
362 let mut deleted = 0usize;
363
364 let (start, end) = self.primary_window(src_idx);
370 let hot = &self.hot_list[start..end];
371 let cold = &mut self.cold_list[start..end];
372 let mut first_primary_tombstone: Option<usize> = None;
373 for (i, (h, c)) in hot.iter().zip(cold.iter_mut()).enumerate() {
374 if h.endpoint == decoded_endpoint && h.rank == decoded_rank && c.is_live() {
375 c.delete_ts = ts;
376 self.edge_count -= 1;
377 self.live_counts[src_idx] = self.live_counts[src_idx].saturating_sub(1);
378 self.tombstone_counts[src_idx] += 1;
379 on_deleted(h.edge_id, EdgePosition::Primary { slot: i as u32 });
380 if first_primary_tombstone.is_none_or(|first| i < first) {
381 first_primary_tombstone = Some(i);
382 }
383 deleted += 1;
384 }
385 }
386
387 if let Some(chunks) = self.overflow_chunks.get_mut(src_vid) {
389 for (chunk_idx, chunk) in chunks.iter_mut().enumerate() {
390 for slot_idx in 0..chunk.len() {
391 let hot = chunk.hot_at(slot_idx).unwrap();
392 let cold = chunk.cold_at(slot_idx).unwrap();
393 if hot.endpoint == decoded_endpoint
394 && hot.rank == decoded_rank
395 && cold.is_live()
396 {
397 chunk.cold_at_mut(slot_idx).unwrap().delete_ts = ts;
398 self.edge_count -= 1;
399 self.live_counts[src_idx] = self.live_counts[src_idx].saturating_sub(1);
400 self.tombstone_counts[src_idx] += 1;
401 on_deleted(
402 hot.edge_id,
403 EdgePosition::Overflow {
404 chunk: chunk_idx as u32,
405 slot: slot_idx as u32,
406 },
407 );
408 deleted += 1;
409 }
410 }
411 }
412 }
413 if deleted > 0 {
414 if let Some(slot) = first_primary_tombstone {
415 self.note_primary_tombstone(src_idx, slot);
416 }
417 self.track_live_remove(src_vid, decoded_endpoint, decoded_rank);
418 }
419
420 deleted
421 }
422
423 pub fn delete_edge_by_dst(&mut self, src_vid: u32, dst: VertexId, ts: Timestamp) -> usize {
428 let mut noop = |_: EdgeId| {};
429 self.delete_edge_by_dst_reporting(src_vid, dst, ts, &mut noop)
430 }
431
432 pub fn delete_edge_by_offset(
433 &mut self,
434 src_vid: u32,
435 offset: i32,
436 ts: Timestamp,
437 ) -> StorageResult<bool> {
438 if offset < 0 {
439 return Ok(false);
440 }
441 let src_idx = src_vid as usize;
442 if src_idx >= self.vertex_capacity() || self.rows.primary_capacities[src_idx] == 0 {
443 return Ok(false);
444 }
445 if offset as usize >= self.rows.degrees[src_idx] as usize {
446 return Ok(false);
447 }
448 let idx = self.rows.adj_offsets[src_idx] as usize + offset as usize;
449 if idx >= self.hot_list.len() {
450 return Ok(false);
451 }
452 let probe = Nbr::from_parts(self.hot_list[idx], self.cold_list[idx]);
453 match decide_slot_delete(&probe, probe.edge_id, ts)? {
458 DeleteSlotOutcome::AlreadyStamped => {
459 return Ok(false);
460 }
461 DeleteSlotOutcome::Stamped => {}
462 }
463 self.cold_list[idx].delete_ts = ts;
464 self.edge_count -= 1;
465 self.live_counts[src_idx] = self.live_counts[src_idx].saturating_sub(1);
466 self.tombstone_counts[src_idx] += 1;
467 self.note_primary_tombstone(src_idx, offset as usize);
468 self.track_live_remove(src_vid, probe.endpoint, probe.rank);
469 Ok(true)
470 }
471
472 pub fn revert_delete_by_offset(&mut self, src_vid: u32, offset: i32, ts: Timestamp) -> bool {
478 if offset < 0 {
479 return false;
480 }
481 let src_idx = src_vid as usize;
482 if src_idx >= self.vertex_capacity() || self.rows.primary_capacities[src_idx] == 0 {
483 return false;
484 }
485 if offset as usize >= self.rows.degrees[src_idx] as usize {
486 return false;
487 }
488
489 let base_offset = self.rows.adj_offsets[src_idx] as usize;
490 let idx = base_offset + offset as usize;
491
492 if idx >= self.hot_list.len() {
493 return false;
494 }
495
496 let probe = Nbr::from_parts(self.hot_list[idx], self.cold_list[idx]);
497 if can_revert_delete(&probe, ts) {
500 self.cold_list[idx].delete_ts = Timestamp::MAX;
501 self.edge_count += 1;
502 self.live_counts[src_idx] += 1;
503 self.tombstone_counts[src_idx] = self.tombstone_counts[src_idx].saturating_sub(1);
504 self.invalidate_reuse_hint(src_idx);
505 self.track_live_insert(
506 src_vid,
507 probe.endpoint,
508 probe.rank,
509 EdgePosition::Primary {
510 slot: offset as u32,
511 },
512 );
513 return true;
514 }
515 false
516 }
517
518 pub fn rollback_insert(&mut self, src_vid: u32, edge_id: EdgeId) -> bool {
531 let src_idx = src_vid as usize;
532 if src_idx >= self.vertex_capacity() {
533 return false;
534 }
535
536 let found = {
539 let (hot, cold) = self.primary_pair(src_idx);
540 hot.iter()
541 .zip(cold.iter())
542 .enumerate()
543 .find_map(|(i, (h, c))| {
544 (h.edge_id == edge_id).then(|| (i, Nbr::from_parts(*h, *c)))
545 })
546 };
547 if let Some((i, probe)) = found {
548 let (start, _) = self.primary_window(src_idx);
549 let degree = self.rows.degrees[src_idx] as usize;
550 let was_live = probe.delete_ts == Timestamp::MAX;
551 self.hot_list
553 .copy_within(start + i + 1..start + degree, start + i);
554 self.cold_list
555 .copy_within(start + i + 1..start + degree, start + i);
556 self.rows.degrees[src_idx] -= 1;
557 let tail = start + self.rows.degrees[src_idx] as usize;
560 self.hot_list[tail] = HotNbr::dead_gap();
561 self.cold_list[tail] = ColdStamps::dead_gap();
562 self.invalidate_reuse_hint(src_idx);
563 if was_live {
564 self.live_counts[src_idx] = self.live_counts[src_idx].saturating_sub(1);
565 if self.live_sets.get(&src_vid).is_some() {
569 self.rebuild_live_set_for_vertex(src_vid);
570 }
571 self.edge_count -= 1;
572 } else {
573 self.tombstone_counts[src_idx] = self.tombstone_counts[src_idx].saturating_sub(1);
574 }
575 return true;
576 }
577
578 if let Some((chunk_idx, edge_idx)) = self.scan_overflow_for_edge_id(src_vid, edge_id) {
580 let probe = {
582 let chunks = self.overflow_chunks.get(src_vid).unwrap();
583 chunks[chunk_idx].slot_at(edge_idx).unwrap()
584 };
585 let was_live = probe.delete_ts == Timestamp::MAX;
586 let detached = if let Some(chunks) = self.overflow_chunks.get_mut(src_vid) {
590 chunks[chunk_idx].remove(edge_idx);
591 if chunks[chunk_idx].is_empty() {
593 let removed = chunks.remove(chunk_idx);
594 Some((removed.capacity(), chunks.is_empty()))
595 } else {
596 None
597 }
598 } else {
599 return false;
600 };
601 if let Some((freed, emptied)) = detached {
602 self.sub_capacity(freed);
603 if emptied {
604 if was_live {
605 self.edge_count -= 1;
606 self.live_counts[src_idx] = self.live_counts[src_idx].saturating_sub(1);
607 } else {
608 self.tombstone_counts[src_idx] =
609 self.tombstone_counts[src_idx].saturating_sub(1);
610 }
611 self.overflow_chunks.remove(src_vid);
612 self.rebuild_live_set_for_vertex(src_vid);
613 return true;
614 }
615 }
616 if was_live {
617 if self.live_sets.get(&src_vid).is_some() {
621 self.rebuild_live_set_for_vertex(src_vid);
622 }
623 self.edge_count -= 1;
624 self.live_counts[src_idx] = self.live_counts[src_idx].saturating_sub(1);
625 } else {
626 self.tombstone_counts[src_idx] = self.tombstone_counts[src_idx].saturating_sub(1);
627 }
628 return true;
629 }
630
631 false
632 }
633
634 pub fn revert_delete_by_edge_id(
640 &mut self,
641 src_vid: u32,
642 edge_id: EdgeId,
643 ts: Timestamp,
644 ) -> bool {
645 let src_idx = src_vid as usize;
646 if src_idx >= self.vertex_capacity() {
647 return false;
648 }
649
650 let found = {
652 let (hot, cold) = self.primary_pair(src_idx);
653 hot.iter()
654 .zip(cold.iter())
655 .enumerate()
656 .find_map(|(i, (h, c))| {
657 let probe = Nbr::from_parts(*h, *c);
658 (probe.edge_id == edge_id && can_revert_delete(&probe, ts))
659 .then_some((i, probe))
660 })
661 };
662 if let Some((i, probe)) = found {
663 let (start, _) = self.primary_window(src_idx);
664 self.cold_list[start + i].delete_ts = Timestamp::MAX;
665 self.edge_count += 1;
666 self.invalidate_reuse_hint(src_idx);
667 self.track_live_insert(
668 src_vid,
669 probe.endpoint,
670 probe.rank,
671 EdgePosition::Primary { slot: i as u32 },
672 );
673 return true;
674 }
675
676 if let Some((chunk_idx, edge_idx)) = self.scan_overflow_for_edge_id(src_vid, edge_id) {
678 let probe = {
679 let chunks = self.overflow_chunks.get(src_vid).unwrap();
680 chunks[chunk_idx].slot_at(edge_idx).unwrap()
681 };
682 if let Some(chunks) = self.overflow_chunks.get_mut(src_vid) {
683 if can_revert_delete(&probe, ts) {
684 chunks[chunk_idx].cold_at_mut(edge_idx).unwrap().delete_ts = Timestamp::MAX;
685 self.edge_count += 1;
686 self.track_live_insert(
687 src_vid,
688 probe.endpoint,
689 probe.rank,
690 EdgePosition::Overflow {
691 chunk: chunk_idx as u32,
692 slot: edge_idx as u32,
693 },
694 );
695 return true;
696 }
697 }
698 }
699
700 false
701 }
702
703 pub fn reserve_for_batch(&mut self, counts: &[(u32, usize)]) {
720 if counts.is_empty() {
721 return;
722 }
723 let mut aggregated: Vec<(u32, usize)> = Vec::with_capacity(counts.len());
727 {
728 let mut push = |src: u32, incoming: usize| {
729 if let Some(last) = aggregated.last_mut() {
730 if last.0 == src {
731 last.1 = last.1.saturating_add(incoming);
732 return;
733 }
734 }
735 aggregated.push((src, incoming));
736 };
737 if counts.windows(2).all(|w| w[0].0 <= w[1].0) {
738 for &(src, incoming) in counts {
739 push(src, incoming);
740 }
741 } else {
742 let mut sorted = counts.to_vec();
743 sorted.sort_unstable_by_key(|(src, _)| *src);
744 for (src, incoming) in sorted {
745 push(src, incoming);
746 }
747 }
748 }
749 for (src_vid, _) in &aggregated {
751 let src_idx = *src_vid as usize;
752 if src_idx >= self.vertex_capacity() {
753 self.ensure_vertex_capacity(src_idx + 1);
754 }
755 if self.rows.primary_capacities[src_idx] == 0 {
756 self.allocate_primary_block(src_idx);
757 }
758 }
759 let mut new_caps = self.rows.primary_capacities.clone();
761 let mut total_extra = 0usize;
762 for (src_vid, incoming) in &aggregated {
763 let src_idx = *src_vid as usize;
764 let live = self.live_key_count(*src_vid);
765 let want = Self::sized_row_capacity(live.saturating_add(*incoming));
766 let have = new_caps[src_idx] as usize;
767 if want > have {
768 total_extra += want - have;
769 new_caps[src_idx] = want as u32;
770 }
771 }
772 if total_extra == 0 {
773 return;
774 }
775 let rows = self.vertex_capacity();
776 if aggregated.len().saturating_mul(8) < rows.max(1) {
781 for (src_vid, incoming) in &aggregated {
782 let src_idx = *src_vid as usize;
783 let live = self.live_key_count(*src_vid);
784 let want = Self::sized_row_capacity(live.saturating_add(*incoming));
785 let have = self.rows.primary_capacities[src_idx] as usize;
786 if want <= have {
787 continue;
788 }
789 let degree = self.rows.degrees[src_idx] as usize;
790 let old_base = self.rows.adj_offsets[src_idx] as usize;
791 if old_base.saturating_add(have) == self.hot_list.len() {
792 let grow = want - have;
793 self.hot_list
794 .resize(self.hot_list.len() + grow, HotNbr::dead_gap());
795 self.cold_list
796 .resize(self.cold_list.len() + grow, ColdStamps::dead_gap());
797 self.rows.primary_capacities[src_idx] = want as u32;
798 self.add_capacity(grow);
799 } else {
800 let new_base = self.hot_list.len();
801 self.hot_list.resize(new_base + want, HotNbr::dead_gap());
802 self.cold_list
803 .resize(new_base + want, ColdStamps::dead_gap());
804 if degree > 0 {
805 self.hot_list
806 .copy_within(old_base..old_base + degree, new_base);
807 self.cold_list
808 .copy_within(old_base..old_base + degree, new_base);
809 }
810 self.rows.adj_offsets[src_idx] = new_base as u32;
811 self.rows.primary_capacities[src_idx] = want as u32;
812 self.add_capacity(want);
813 for slot in old_base..old_base + have {
818 self.hot_list[slot] = HotNbr::dead_gap();
819 self.cold_list[slot] = ColdStamps::dead_gap();
820 }
821 }
822 }
823 return;
824 }
825 let total: usize = new_caps.iter().map(|&c| c as usize).sum();
826 let mut new_hot = Vec::with_capacity(total);
827 let mut new_cold = Vec::with_capacity(total);
828 let mut new_offsets = vec![0u32; rows];
829 for vid in 0..rows {
830 new_offsets[vid] = new_hot.len() as u32;
831 let base = self.rows.adj_offsets[vid] as usize;
832 let cap = self.rows.primary_capacities[vid] as usize;
833 let want = new_caps[vid] as usize;
834 if cap > 0 {
835 new_hot.extend_from_slice(&self.hot_list[base..base + cap]);
836 new_cold.extend_from_slice(&self.cold_list[base..base + cap]);
837 }
838 if want > cap {
839 new_hot.resize(new_hot.len() + (want - cap), HotNbr::dead_gap());
840 new_cold.resize(new_cold.len() + (want - cap), ColdStamps::dead_gap());
841 }
842 }
843 self.hot_list = new_hot;
844 self.cold_list = new_cold;
845 self.rows.adj_offsets = new_offsets;
846 self.rows.primary_capacities = new_caps;
847 self.add_capacity(total_extra);
848 self.reset_reuse_hints();
849 }
850
851 pub fn batch_put_edges(
860 &mut self,
861 groups: &[RowEdgeBatch],
862 check_duplicates: bool,
863 ) -> StorageResult<usize> {
864 if check_duplicates {
865 let mut keys: Vec<(u32, i64)> = Vec::new();
870 let mut seen: HashSet<(u32, i64)> = HashSet::new();
871 for (src_vid, batch) in groups {
872 keys.clear();
873 keys.extend(
874 batch
875 .iter()
876 .map(|(endpoint, rank, _, _)| (*endpoint, *rank)),
877 );
878 if !keys.windows(2).all(|w| w[0] <= w[1]) {
881 keys.sort_unstable();
882 }
883 for window in keys.windows(2) {
884 if window[0] == window[1] {
885 return Err(StorageError::edge_already_exists(format!(
886 "duplicate key in bulk batch for vertex {}",
887 src_vid
888 )));
889 }
890 }
891 seen.clear();
898 seen.reserve(batch.len());
899 self.visit_physical(*src_vid, |nbr| {
900 if nbr.delete_ts == Timestamp::MAX {
901 seen.insert((nbr.endpoint, nbr.rank));
902 }
903 true
904 });
905 for (endpoint, rank, _, _) in batch {
906 if !seen.insert((*endpoint, *rank)) {
907 return Err(StorageError::edge_already_exists(format!(
908 "{} -> ({}, {})",
909 src_vid, endpoint, rank
910 )));
911 }
912 }
913 }
914 }
915 let counts: Vec<(u32, usize)> = groups
916 .iter()
917 .map(|(src, batch)| (*src, batch.len()))
918 .collect();
919 self.reserve_for_batch(&counts);
920 let mut inserted = 0usize;
921 for (src_vid, batch) in groups {
922 let src_idx = *src_vid as usize;
923 let mut live = self.live_key_count(*src_vid);
926 for (endpoint, rank, edge_id, create_ts) in batch {
927 let nbr = Nbr::with_create_ts(*endpoint, *rank, *edge_id, *create_ts);
928 live += 1;
931 let degree = self.rows.degrees[src_idx] as usize;
932 let cap = self.rows.primary_capacities[src_idx] as usize;
933 if degree < cap {
934 let base = self.rows.adj_offsets[src_idx] as usize;
935 self.set_slot(base + degree, nbr);
936 self.mark_primary_unsorted(src_idx);
937 self.rows.degrees[src_idx] += 1;
938 } else {
939 self.append_overflow(*src_vid, nbr, live);
940 }
941 self.edge_count += 1;
942 inserted += 1;
943 }
944 self.rebuild_live_set_for_vertex(*src_vid);
945 }
946 Ok(inserted)
947 }
948
949 pub fn delete_edge_at_position(
955 &mut self,
956 src_vid: u32,
957 position: EdgePosition,
958 expected: EdgeId,
959 ts: Timestamp,
960 ) -> StorageResult<bool> {
961 match position {
962 EdgePosition::Primary { slot } => {
963 let src_idx = src_vid as usize;
964 if src_idx >= self.vertex_capacity() {
965 return Ok(false);
966 }
967 if slot as usize >= self.rows.degrees[src_idx] as usize {
968 return Ok(false);
969 }
970 let idx = self.rows.adj_offsets[src_idx] as usize + slot as usize;
971 let probe = match (self.hot_list.get(idx), self.cold_list.get(idx)) {
972 (Some(hot), Some(cold)) => Nbr::from_parts(*hot, *cold),
973 _ => return Ok(false),
974 };
975 if probe.edge_id != expected {
976 return Ok(false);
977 }
978 match decide_slot_delete(&probe, expected, ts)? {
979 DeleteSlotOutcome::AlreadyStamped => return Ok(false),
980 DeleteSlotOutcome::Stamped => {}
981 }
982 self.cold_list[idx].delete_ts = ts;
983 self.edge_count -= 1;
984 self.note_primary_tombstone(src_idx, slot as usize);
985 self.track_live_remove(src_vid, probe.endpoint, probe.rank);
986 Ok(true)
987 }
988 EdgePosition::Overflow { chunk, slot } => {
989 let Some(chunks) = self.overflow_chunks.get_mut(src_vid) else {
990 return Ok(false);
991 };
992 let Some(chunk) = chunks.get_mut(chunk as usize) else {
993 return Ok(false);
994 };
995 let probe = match (chunk.hot_at(slot as usize), chunk.cold_at(slot as usize)) {
996 (Some(hot), Some(cold)) => Nbr::from_parts(hot, cold),
997 _ => return Ok(false),
998 };
999 if probe.edge_id != expected {
1000 return Ok(false);
1001 }
1002 match decide_slot_delete(&probe, expected, ts)? {
1003 DeleteSlotOutcome::AlreadyStamped => return Ok(false),
1004 DeleteSlotOutcome::Stamped => {}
1005 }
1006 chunk.cold_at_mut(slot as usize).unwrap().delete_ts = ts;
1007 self.edge_count -= 1;
1008 self.track_live_remove(src_vid, probe.endpoint, probe.rank);
1009 Ok(true)
1010 }
1011 }
1012 }
1013
1014 pub fn revert_delete_at_position(
1018 &mut self,
1019 src_vid: u32,
1020 position: EdgePosition,
1021 expected: EdgeId,
1022 ts: Timestamp,
1023 ) -> bool {
1024 match position {
1025 EdgePosition::Primary { slot } => {
1026 let src_idx = src_vid as usize;
1027 if src_idx >= self.vertex_capacity() {
1028 return false;
1029 }
1030 if slot as usize >= self.rows.degrees[src_idx] as usize {
1031 return false;
1032 }
1033 let idx = self.rows.adj_offsets[src_idx] as usize + slot as usize;
1034 let probe = match (self.hot_list.get(idx), self.cold_list.get(idx)) {
1035 (Some(hot), Some(cold)) => Nbr::from_parts(*hot, *cold),
1036 _ => return false,
1037 };
1038 if probe.edge_id != expected || !can_revert_delete(&probe, ts) {
1039 return false;
1040 }
1041 self.cold_list[idx].delete_ts = Timestamp::MAX;
1042 self.edge_count += 1;
1043 self.invalidate_reuse_hint(src_idx);
1044 self.track_live_insert(
1045 src_vid,
1046 probe.endpoint,
1047 probe.rank,
1048 EdgePosition::Primary { slot },
1049 );
1050 true
1051 }
1052 EdgePosition::Overflow { chunk, slot } => {
1053 let (chunk_idx, slot_idx) = (chunk, slot);
1054 let Some(chunks) = self.overflow_chunks.get_mut(src_vid) else {
1055 return false;
1056 };
1057 let Some(chunk) = chunks.get_mut(chunk_idx as usize) else {
1058 return false;
1059 };
1060 let probe = match (
1061 chunk.hot_at(slot_idx as usize),
1062 chunk.cold_at(slot_idx as usize),
1063 ) {
1064 (Some(hot), Some(cold)) => Nbr::from_parts(hot, cold),
1065 _ => return false,
1066 };
1067 if probe.edge_id != expected || !can_revert_delete(&probe, ts) {
1068 return false;
1069 }
1070 chunk.cold_at_mut(slot_idx as usize).unwrap().delete_ts = Timestamp::MAX;
1071 self.edge_count += 1;
1072 self.track_live_insert(
1073 src_vid,
1074 probe.endpoint,
1075 probe.rank,
1076 EdgePosition::Overflow {
1077 chunk: chunk_idx,
1078 slot: slot_idx,
1079 },
1080 );
1081 true
1082 }
1083 }
1084 }
1085}