nodedb_lite/engine/columnar/store/
compact.rs1use super::segments::{
6 columnar_err_to_lite, load_segment_bytes, remove_segment_bytes, store_segment_bytes,
7};
8use super::state::ColumnarEngine;
9use crate::error::LiteError;
10use crate::runtime::now_millis_i64;
11use crate::storage::engine::StorageEngine;
12use nodedb_columnar::delete_bitmap::DeleteBitmap;
13use nodedb_types::Namespace;
14use nodedb_types::columnar::{ColumnarProfile, ColumnarSchema};
15use std::collections::{HashMap, HashSet};
16
17impl<S: StorageEngine> ColumnarEngine<S> {
18 pub async fn try_compact_collection(&self, collection: &str) -> Result<bool, LiteError> {
20 let state_arc = self.lookup(collection)?;
21
22 struct Snapshot {
24 schema: ColumnarSchema,
25 profile_tag: u8,
26 to_compact: Vec<u32>,
27 bitmaps: HashMap<u32, DeleteBitmap>,
28 }
29
30 let snap = {
31 let s = Self::lock_state(&state_arc)?;
32 let mut to_compact = Vec::new();
33 for seg_meta in &s.segments {
34 if seg_meta.fully_deleted_at_ms.is_some() {
36 continue;
37 }
38 if let Some(bitmap) = s.mutation.delete_bitmap(seg_meta.segment_id as u64)
39 && bitmap.should_compact(seg_meta.row_count, 0.2)
40 {
41 to_compact.push(seg_meta.segment_id);
42 }
43 }
44 if to_compact.is_empty() {
45 return Ok(false);
46 }
47 let schema = s.mutation.schema().clone();
48 let profile_tag = match &s.profile {
49 ColumnarProfile::Plain => 0,
50 ColumnarProfile::Timeseries { .. } => 1,
51 ColumnarProfile::Spatial { .. } => 2,
52 };
53 let mut bitmaps = HashMap::new();
54 for &seg_id in &to_compact {
55 if let Some(b) = s.mutation.delete_bitmap(seg_id as u64) {
56 bitmaps.insert(seg_id, b.clone());
57 }
58 }
59 Snapshot {
60 schema,
61 profile_tag,
62 to_compact,
63 bitmaps,
64 }
65 };
66
67 for seg_id in &snap.to_compact {
68 let seg_bytes = match load_segment_bytes(&*self.storage, collection, *seg_id).await? {
69 Some(b) => b,
70 None => continue,
71 };
72
73 let empty_bitmap = DeleteBitmap::new();
74 let bitmap = snap.bitmaps.get(seg_id).unwrap_or(&empty_bitmap);
75
76 let result = nodedb_columnar::compaction::compact_segment(
77 &seg_bytes,
78 bitmap,
79 &snap.schema,
80 snap.profile_tag,
81 &self.memory,
82 None,
83 )
84 .map_err(columnar_err_to_lite)?;
85
86 if let Some(new_seg_bytes) = result.segment {
87 store_segment_bytes(&*self.storage, collection, *seg_id, &new_seg_bytes).await?;
88
89 {
92 let mut s = Self::lock_state(&state_arc)?;
93 if let Some(meta) = s.segments.iter_mut().find(|m| m.segment_id == *seg_id) {
94 meta.row_count = result.live_rows as u64;
95 }
96 }
97
98 let del_key = format!("{collection}:del:{seg_id}");
99 self.storage
100 .delete(Namespace::Columnar, del_key.as_bytes())
101 .await?;
102 } else {
103 let is_bitemporal = {
108 let s = Self::lock_state(&state_arc)?;
109 s.bitemporal
110 };
111
112 remove_segment_bytes(&*self.storage, collection, *seg_id).await?;
113 let del_key = format!("{collection}:del:{seg_id}");
114 self.storage
115 .delete(Namespace::Columnar, del_key.as_bytes())
116 .await?;
117
118 {
119 let mut s = Self::lock_state(&state_arc)?;
120 if is_bitemporal {
121 if let Some(meta) = s.segments.iter_mut().find(|m| m.segment_id == *seg_id)
123 {
124 meta.row_count = 0;
125 meta.fully_deleted_at_ms = Some(now_millis_i64());
126 }
127 } else {
128 s.segments.retain(|m| m.segment_id != *seg_id);
129 }
130 }
131 }
132 }
133
134 let meta_bytes = {
136 let s = Self::lock_state(&state_arc)?;
137 zerompk::to_msgpack_vec(&s.segments).map_err(|e| LiteError::Serialization {
138 detail: e.to_string(),
139 })?
140 };
141 let meta_key = format!("{collection}:meta");
142 self.storage
143 .put(Namespace::Columnar, meta_key.as_bytes(), &meta_bytes)
144 .await?;
145
146 Ok(true)
147 }
148
149 pub async fn purge_bitemporal_before(
155 &self,
156 collection: &str,
157 cutoff_ms: i64,
158 ) -> Result<u64, LiteError> {
159 let state_arc = self.lookup(collection)?;
160
161 let (is_bitemporal, to_purge): (bool, Vec<u32>) = {
162 let s = Self::lock_state(&state_arc)?;
163 let purge: Vec<u32> = s
164 .segments
165 .iter()
166 .filter(|m| m.fully_deleted_at_ms.is_some_and(|t| t < cutoff_ms))
167 .map(|m| m.segment_id)
168 .collect();
169 (s.bitemporal, purge)
170 };
171
172 if !is_bitemporal {
173 return Ok(0);
174 }
175
176 if to_purge.is_empty() {
177 return Ok(0);
178 }
179
180 let purged_ids: HashSet<u32> = to_purge.iter().copied().collect();
181
182 {
184 let mut s = Self::lock_state(&state_arc)?;
185 s.segments.retain(|m| !purged_ids.contains(&m.segment_id));
186 }
187
188 let meta_bytes = {
190 let s = Self::lock_state(&state_arc)?;
191 zerompk::to_msgpack_vec(&s.segments).map_err(|e| LiteError::Serialization {
192 detail: e.to_string(),
193 })?
194 };
195 let meta_key = format!("{collection}:meta");
196 self.storage
197 .put(Namespace::Columnar, meta_key.as_bytes(), &meta_bytes)
198 .await?;
199
200 Ok(to_purge.len() as u64)
201 }
202}