1use std::collections::HashMap;
4use std::sync::Arc;
5
6use super::id::{LogId, LogIdGenerator};
7use crate::storage::unified::entity::{EntityData, EntityId, EntityKind, RowData, UnifiedEntity};
8use crate::storage::unified::store::{StoreError, UnifiedStore};
9use reddb_types::Value;
10
11#[derive(Debug, Clone, Default)]
13pub enum LogRetention {
14 Days(u64),
16 MaxEntries(u64),
18 MaxBytes(u64),
20 #[default]
22 Forever,
23}
24
25#[derive(Debug, Clone)]
27pub struct LogCollectionConfig {
28 pub name: String,
29 pub columns: Vec<String>,
30 pub retention: LogRetention,
31 pub batch_size: usize,
32}
33
34impl LogCollectionConfig {
35 pub fn new(name: &str) -> Self {
36 Self {
37 name: name.to_string(),
38 columns: Vec::new(),
39 retention: LogRetention::Forever,
40 batch_size: 64,
41 }
42 }
43}
44
45#[derive(Debug, Clone)]
47pub struct LogEntry {
48 pub id: LogId,
49 pub fields: HashMap<String, Value>,
50}
51
52fn partial_top_k<T: Clone>(
60 items: &mut Vec<T>,
61 k: usize,
62 cmp: impl Fn(&T, &T) -> std::cmp::Ordering,
63) {
64 let n = items.len();
65 if k == 0 {
66 items.clear();
67 return;
68 }
69 if n <= k.saturating_mul(2) {
70 items.sort_by(|a, b| cmp(a, b));
71 items.truncate(k);
72 return;
73 }
74 let mut idxs: Vec<usize> = (0..n).collect();
75 idxs.select_nth_unstable_by(k - 1, |&a, &b| {
76 cmp(&items[a], &items[b]).then_with(|| a.cmp(&b))
77 });
78 idxs.truncate(k);
79 idxs.sort_by(|&a, &b| cmp(&items[a], &items[b]).then_with(|| a.cmp(&b)));
80 let orig = std::mem::take(items);
81 *items = idxs.into_iter().map(|i| orig[i].clone()).collect();
82}
83
84pub struct LogCollection {
86 config: LogCollectionConfig,
87 id_gen: LogIdGenerator,
88 store: Arc<UnifiedStore>,
89 write_buffer: std::sync::Mutex<Vec<UnifiedEntity>>,
90}
91
92impl LogCollection {
93 pub fn new(store: Arc<UnifiedStore>, config: LogCollectionConfig) -> Self {
94 let _ = store.get_or_create_collection(&config.name);
95
96 let id_gen = LogIdGenerator::new();
98 if let Some(manager) = store.get_collection(&config.name) {
99 let mut max_id = 0u64;
100 manager.for_each_entity(|entity| {
101 if let Some(row) = entity.data.as_row() {
102 if let Some(Value::UnsignedInteger(id)) = row.get_field("id") {
103 if *id > max_id {
104 max_id = *id;
105 }
106 }
107 }
108 true
109 });
110 if max_id > 0 {
111 id_gen.restore(max_id);
112 }
113 }
114
115 Self {
116 config,
117 id_gen,
118 store,
119 write_buffer: std::sync::Mutex::new(Vec::new()),
120 }
121 }
122
123 pub fn append(&self, fields: HashMap<String, Value>) -> LogId {
125 let (id, entity) = self.make_entry(fields);
126 let batch_size = self.config.batch_size;
127 let should_flush = {
128 let mut buf = self.write_buffer.lock().unwrap_or_else(|e| e.into_inner());
129 buf.push(entity);
130 buf.len() >= batch_size
131 };
132
133 if should_flush {
134 self.flush_buffer();
135 }
136
137 id
138 }
139
140 pub(crate) fn append_checked(
143 &self,
144 fields: HashMap<String, Value>,
145 ) -> Result<LogId, StoreError> {
146 let (id, entity) = self.make_entry(fields);
147 self.store.insert_auto(&self.config.name, entity)?;
148 Ok(id)
149 }
150
151 fn make_entry(&self, fields: HashMap<String, Value>) -> (LogId, UnifiedEntity) {
152 let id = self.id_gen.next();
153
154 let mut named = HashMap::with_capacity(fields.len() + 1);
155 named.insert("id".to_string(), Value::UnsignedInteger(id.raw()));
156 for (k, v) in fields {
157 named.insert(k, v);
158 }
159
160 let entity = UnifiedEntity::new(
161 EntityId::new(0),
162 EntityKind::TableRow {
163 table: Arc::from(self.config.name.as_str()),
164 row_id: 0,
165 },
166 EntityData::Row(RowData {
167 columns: Vec::new(),
168 named: Some(named),
169 schema: None,
170 }),
171 );
172
173 (id, entity)
174 }
175
176 pub fn append_fields(&self, fields: Vec<(&str, Value)>) -> LogId {
178 let map: HashMap<String, Value> = fields
179 .into_iter()
180 .map(|(k, v)| (k.to_string(), v))
181 .collect();
182 self.append(map)
183 }
184
185 pub fn flush_buffer(&self) {
187 let entities = {
188 let mut buf = self.write_buffer.lock().unwrap_or_else(|e| e.into_inner());
189 std::mem::take(&mut *buf)
190 };
191
192 if entities.is_empty() {
193 return;
194 }
195
196 for entity in entities {
197 let _ = self.store.insert_auto(&self.config.name, entity);
198 }
199 }
200
201 pub fn recent(&self, limit: usize) -> Vec<LogEntry> {
203 self.flush_buffer();
204
205 let manager = match self.store.get_collection(&self.config.name) {
206 Some(m) => m,
207 None => return Vec::new(),
208 };
209
210 use std::cmp::Reverse;
213 use std::collections::BinaryHeap;
214
215 let mut heap: BinaryHeap<Reverse<(u64, crate::storage::unified::entity::EntityId)>> =
216 BinaryHeap::with_capacity(limit + 1);
217
218 manager.for_each_entity(|entity| {
219 if let Some(row) = entity.data.as_row() {
220 let id_val = row
221 .get_field("id")
222 .and_then(|v| match v {
223 Value::UnsignedInteger(n) => Some(*n),
224 _ => None,
225 })
226 .unwrap_or(0);
227
228 if heap.len() < limit {
229 heap.push(Reverse((id_val, entity.id)));
230 } else if let Some(&Reverse((min_id, _))) = heap.peek() {
231 if id_val > min_id {
232 heap.pop();
233 heap.push(Reverse((id_val, entity.id)));
234 }
235 }
236 }
237 true
238 });
239
240 let mut top_ids: Vec<(u64, crate::storage::unified::entity::EntityId)> = heap
242 .into_vec()
243 .into_iter()
244 .map(|Reverse(pair)| pair)
245 .collect();
246 top_ids.sort_by_key(|b| std::cmp::Reverse(b.0)); top_ids
249 .into_iter()
250 .filter_map(|(log_id, entity_id)| {
251 let entity = manager.get(entity_id)?;
252 let row = entity.data.as_row()?;
253 let mut fields = HashMap::new();
254 for (key, value) in row.iter_fields() {
255 if key != "id" {
256 fields.insert(key.to_string(), value.clone());
257 }
258 }
259 Some(LogEntry {
260 id: LogId(log_id),
261 fields,
262 })
263 })
264 .collect()
265 }
266
267 pub fn range(&self, from_id: LogId, to_id: LogId, limit: usize) -> Vec<LogEntry> {
269 self.flush_buffer();
270
271 let manager = match self.store.get_collection(&self.config.name) {
272 Some(m) => m,
273 None => return Vec::new(),
274 };
275
276 let mut entries = Vec::new();
277 manager.for_each_entity(|entity| {
278 if let Some(row) = entity.data.as_row() {
279 let id_val = row
280 .get_field("id")
281 .and_then(|v| match v {
282 Value::UnsignedInteger(n) => Some(*n),
283 _ => None,
284 })
285 .unwrap_or(0);
286
287 if id_val >= from_id.raw() && id_val <= to_id.raw() {
288 let mut fields = HashMap::new();
289 for (key, value) in row.iter_fields() {
290 if key != "id" {
291 fields.insert(key.to_string(), value.clone());
292 }
293 }
294 entries.push(LogEntry {
295 id: LogId(id_val),
296 fields,
297 });
298 }
299 }
300 true
301 });
302
303 partial_top_k(&mut entries, limit, |a, b| a.id.cmp(&b.id));
304 entries
305 }
306
307 pub fn apply_retention(&self) -> u64 {
309 match &self.config.retention {
310 LogRetention::Forever => 0,
311 LogRetention::Days(days) => {
312 let cutoff_ms = std::time::SystemTime::now()
313 .duration_since(std::time::UNIX_EPOCH)
314 .unwrap_or_default()
315 .as_millis() as u64
316 - days * 86_400_000;
317 let cutoff_id = LogId::from_ms(cutoff_ms);
318 self.delete_before(cutoff_id)
319 }
320 LogRetention::MaxEntries(max) => {
321 let manager = match self.store.get_collection(&self.config.name) {
322 Some(m) => m,
323 None => return 0,
324 };
325
326 let mut ids: Vec<(u64, EntityId)> = Vec::new();
327 manager.for_each_entity(|entity| {
328 if let Some(row) = entity.data.as_row() {
329 if let Some(Value::UnsignedInteger(log_id)) = row.get_field("id") {
330 ids.push((*log_id, entity.id));
331 }
332 }
333 true
334 });
335
336 if ids.len() as u64 <= *max {
337 return 0;
338 }
339
340 ids.sort_by_key(|(log_id, _)| *log_id);
341 let to_delete = ids.len() as u64 - max;
342 let mut deleted = 0u64;
343 for (_, entity_id) in ids.iter().take(to_delete as usize) {
344 if self
345 .store
346 .delete(&self.config.name, *entity_id)
347 .unwrap_or(false)
348 {
349 deleted += 1;
350 }
351 }
352 deleted
353 }
354 LogRetention::MaxBytes(max_bytes) => {
355 let manager = match self.store.get_collection(&self.config.name) {
356 Some(m) => m,
357 None => return 0,
358 };
359
360 let mut entries: Vec<(u64, EntityId, u64)> = Vec::new();
362 manager.for_each_entity(|entity| {
363 if let Some(row) = entity.data.as_row() {
364 let log_id = row
365 .get_field("id")
366 .and_then(|v| match v {
367 Value::UnsignedInteger(n) => Some(*n),
368 _ => None,
369 })
370 .unwrap_or(0);
371
372 let mut size = 8u64; for (key, value) in row.iter_fields() {
375 size += key.len() as u64 + estimate_value_size(value);
376 }
377 entries.push((log_id, entity.id, size));
378 }
379 true
380 });
381
382 entries.sort_by_key(|(log_id, _, _)| *log_id);
383
384 let total_size: u64 = entries.iter().map(|(_, _, s)| s).sum();
385 if total_size <= *max_bytes {
386 return 0;
387 }
388
389 let mut to_free = total_size - max_bytes;
391 let mut deleted = 0u64;
392 for (_, entity_id, size) in &entries {
393 if to_free == 0 {
394 break;
395 }
396 if self
397 .store
398 .delete(&self.config.name, *entity_id)
399 .unwrap_or(false)
400 {
401 deleted += 1;
402 to_free = to_free.saturating_sub(*size);
403 }
404 }
405 deleted
406 }
407 }
408 }
409
410 fn delete_before(&self, cutoff: LogId) -> u64 {
411 let manager = match self.store.get_collection(&self.config.name) {
412 Some(m) => m,
413 None => return 0,
414 };
415
416 let mut to_delete = Vec::new();
417 manager.for_each_entity(|entity| {
418 if let Some(row) = entity.data.as_row() {
419 if let Some(Value::UnsignedInteger(log_id)) = row.get_field("id") {
420 if *log_id < cutoff.raw() {
421 to_delete.push(entity.id);
422 }
423 }
424 }
425 true
426 });
427
428 let mut deleted = 0u64;
429 for entity_id in to_delete {
430 if self
431 .store
432 .delete(&self.config.name, entity_id)
433 .unwrap_or(false)
434 {
435 deleted += 1;
436 }
437 }
438 deleted
439 }
440
441 pub fn len(&self) -> usize {
443 self.flush_buffer();
444 self.store
445 .get_collection(&self.config.name)
446 .map(|m| m.stats().total_entities)
447 .unwrap_or(0)
448 }
449
450 pub fn config(&self) -> &LogCollectionConfig {
452 &self.config
453 }
454}
455
456fn estimate_value_size(value: &Value) -> u64 {
457 match value {
458 Value::Null => 1,
459 Value::Boolean(_) => 1,
460 Value::Integer(_) | Value::UnsignedInteger(_) | Value::Float(_) => 8,
461 Value::Text(s) => s.len() as u64,
462 Value::Blob(b) => b.len() as u64,
463 Value::Vector(v) => v.len() as u64 * 4,
464 Value::Array(a) => a.iter().map(estimate_value_size).sum::<u64>() + 8,
465 _ => 16, }
467}
468
469impl Drop for LogCollection {
470 fn drop(&mut self) {
471 self.flush_buffer();
472 }
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478
479 fn test_store() -> Arc<UnifiedStore> {
480 Arc::new(UnifiedStore::new())
481 }
482
483 #[test]
484 fn test_append_and_query() {
485 let store = test_store();
486 let log = LogCollection::new(store, LogCollectionConfig::new("test_log"));
487
488 let id1 = log.append_fields(vec![
489 ("level", Value::text("info")),
490 ("message", Value::text("hello")),
491 ]);
492 let id2 = log.append_fields(vec![
493 ("level", Value::text("error")),
494 ("message", Value::text("oops")),
495 ]);
496
497 assert!(id2.raw() > id1.raw());
498
499 let recent = log.recent(10);
500 assert_eq!(recent.len(), 2);
501 assert_eq!(recent[0].id, id2); assert_eq!(recent[1].id, id1);
503 }
504
505 #[test]
506 fn test_retention_max_entries() {
507 let store = test_store();
508 let mut config = LogCollectionConfig::new("retention_test");
509 config.retention = LogRetention::MaxEntries(3);
510 config.batch_size = 1;
511
512 let log = LogCollection::new(store, config);
513
514 for i in 0..5 {
515 log.append_fields(vec![("seq", Value::Integer(i))]);
516 }
517
518 assert_eq!(log.len(), 5);
519 let deleted = log.apply_retention();
520 assert_eq!(deleted, 2);
521 assert_eq!(log.len(), 3);
522 }
523
524 #[test]
525 fn test_retention_max_bytes() {
526 let store = test_store();
527 let mut config = LogCollectionConfig::new("bytes_retention_test");
528 config.retention = LogRetention::MaxBytes(200);
529 config.batch_size = 1;
530
531 let log = LogCollection::new(store, config);
532
533 for i in 0..10 {
535 log.append_fields(vec![("msg", Value::text(format!("entry-{}", i)))]);
536 }
537
538 let before = log.len();
539 assert_eq!(before, 10);
540
541 let deleted = log.apply_retention();
542 assert!(
543 deleted > 0,
544 "should delete some entries to fit under 200 bytes"
545 );
546 assert!(log.len() < 10, "should have fewer entries after retention");
547 }
548
549 #[test]
550 fn test_batch_buffering() {
551 let store = test_store();
552 let mut config = LogCollectionConfig::new("batch_test");
553 config.batch_size = 4;
554
555 let log = LogCollection::new(store.clone(), config);
556
557 for _ in 0..3 {
559 log.append_fields(vec![("msg", Value::text("buffered"))]);
560 }
561
562 let entries = log.recent(10);
565 assert_eq!(entries.len(), 3);
566 }
567
568 #[test]
569 fn test_id_is_time_ordered() {
570 let store = test_store();
571 let log = LogCollection::new(store, LogCollectionConfig::new("time_test"));
572
573 let ids: Vec<LogId> = (0..100)
574 .map(|i| log.append_fields(vec![("i", Value::Integer(i))]))
575 .collect();
576
577 for i in 1..ids.len() {
578 assert!(ids[i].raw() > ids[i - 1].raw());
579 assert!(ids[i].timestamp_us() >= ids[i - 1].timestamp_us());
580 }
581 }
582}