Skip to main content

reddb_server/log/
store.rs

1//! Append-only log collection backed by UnifiedStore.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use super::id::{LogId, LogIdGenerator};
7use crate::storage::schema::Value;
8use crate::storage::unified::entity::{EntityData, EntityId, EntityKind, RowData, UnifiedEntity};
9use crate::storage::unified::store::UnifiedStore;
10
11/// Retention policy for log collections.
12#[derive(Debug, Clone, Default)]
13pub enum LogRetention {
14    /// Keep entries for N days, then auto-delete.
15    Days(u64),
16    /// Keep at most N entries (oldest evicted first).
17    MaxEntries(u64),
18    /// Keep total size under N bytes (oldest evicted first).
19    MaxBytes(u64),
20    /// Keep forever (no automatic cleanup).
21    #[default]
22    Forever,
23}
24
25/// Configuration for a log collection.
26#[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/// A single log entry with timestamp-based ID and user fields.
46#[derive(Debug, Clone)]
47pub struct LogEntry {
48    pub id: LogId,
49    pub fields: HashMap<String, Value>,
50}
51
52/// Keep the top `k` items under `cmp` without a full sort when it pays off.
53/// Output is **bit-identical** to `items.sort_by(cmp); items.truncate(k)` for
54/// any input: the small-`n` branch is exactly that stable sort, and the
55/// quickselect branch appends the original index as the final tie-break so the
56/// unstable partition/sort reproduces the stable sort's first `k` elements,
57/// preserving `cmp`'s own tie-breaks. Mirrors
58/// `join_filter::ordering::top_k_records_by_order_by_with_db`.
59fn 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
84/// Append-only log collection.
85pub 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        // Restore ID generator from highest existing entry
97        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    /// Append a single log entry. Returns the assigned ID.
124    pub fn append(&self, fields: HashMap<String, Value>) -> LogId {
125        let id = self.id_gen.next();
126
127        let mut named = HashMap::with_capacity(fields.len() + 1);
128        named.insert("id".to_string(), Value::UnsignedInteger(id.raw()));
129        for (k, v) in fields {
130            named.insert(k, v);
131        }
132
133        let entity = UnifiedEntity::new(
134            EntityId::new(0),
135            EntityKind::TableRow {
136                table: Arc::from(self.config.name.as_str()),
137                row_id: 0,
138            },
139            EntityData::Row(RowData {
140                columns: Vec::new(),
141                named: Some(named),
142                schema: None,
143            }),
144        );
145
146        let batch_size = self.config.batch_size;
147        let should_flush = {
148            let mut buf = self.write_buffer.lock().unwrap_or_else(|e| e.into_inner());
149            buf.push(entity);
150            buf.len() >= batch_size
151        };
152
153        if should_flush {
154            self.flush_buffer();
155        }
156
157        id
158    }
159
160    /// Append a log entry from (key, value) pairs.
161    pub fn append_fields(&self, fields: Vec<(&str, Value)>) -> LogId {
162        let map: HashMap<String, Value> = fields
163            .into_iter()
164            .map(|(k, v)| (k.to_string(), v))
165            .collect();
166        self.append(map)
167    }
168
169    /// Flush the write buffer to storage.
170    pub fn flush_buffer(&self) {
171        let entities = {
172            let mut buf = self.write_buffer.lock().unwrap_or_else(|e| e.into_inner());
173            std::mem::take(&mut *buf)
174        };
175
176        if entities.is_empty() {
177            return;
178        }
179
180        for entity in entities {
181            let _ = self.store.insert_auto(&self.config.name, entity);
182        }
183    }
184
185    /// Query recent entries (newest first).
186    pub fn recent(&self, limit: usize) -> Vec<LogEntry> {
187        self.flush_buffer();
188
189        let manager = match self.store.get_collection(&self.config.name) {
190            Some(m) => m,
191            None => return Vec::new(),
192        };
193
194        // Phase 1: collect top-k log IDs + entity IDs using a bounded min-heap.
195        // Only stores (log_id, entity_id) — no field cloning until phase 2.
196        use std::cmp::Reverse;
197        use std::collections::BinaryHeap;
198
199        let mut heap: BinaryHeap<Reverse<(u64, crate::storage::unified::entity::EntityId)>> =
200            BinaryHeap::with_capacity(limit + 1);
201
202        manager.for_each_entity(|entity| {
203            if let Some(row) = entity.data.as_row() {
204                let id_val = row
205                    .get_field("id")
206                    .and_then(|v| match v {
207                        Value::UnsignedInteger(n) => Some(*n),
208                        _ => None,
209                    })
210                    .unwrap_or(0);
211
212                if heap.len() < limit {
213                    heap.push(Reverse((id_val, entity.id)));
214                } else if let Some(&Reverse((min_id, _))) = heap.peek() {
215                    if id_val > min_id {
216                        heap.pop();
217                        heap.push(Reverse((id_val, entity.id)));
218                    }
219                }
220            }
221            true
222        });
223
224        // Phase 2: fetch full entities only for top-k (avoids cloning all fields)
225        let mut top_ids: Vec<(u64, crate::storage::unified::entity::EntityId)> = heap
226            .into_vec()
227            .into_iter()
228            .map(|Reverse(pair)| pair)
229            .collect();
230        top_ids.sort_by_key(|b| std::cmp::Reverse(b.0)); // newest first
231
232        top_ids
233            .into_iter()
234            .filter_map(|(log_id, entity_id)| {
235                let entity = manager.get(entity_id)?;
236                let row = entity.data.as_row()?;
237                let mut fields = HashMap::new();
238                for (key, value) in row.iter_fields() {
239                    if key != "id" {
240                        fields.insert(key.to_string(), value.clone());
241                    }
242                }
243                Some(LogEntry {
244                    id: LogId(log_id),
245                    fields,
246                })
247            })
248            .collect()
249    }
250
251    /// Query entries within a time range (by ID boundaries).
252    pub fn range(&self, from_id: LogId, to_id: LogId, limit: usize) -> Vec<LogEntry> {
253        self.flush_buffer();
254
255        let manager = match self.store.get_collection(&self.config.name) {
256            Some(m) => m,
257            None => return Vec::new(),
258        };
259
260        let mut entries = Vec::new();
261        manager.for_each_entity(|entity| {
262            if let Some(row) = entity.data.as_row() {
263                let id_val = row
264                    .get_field("id")
265                    .and_then(|v| match v {
266                        Value::UnsignedInteger(n) => Some(*n),
267                        _ => None,
268                    })
269                    .unwrap_or(0);
270
271                if id_val >= from_id.raw() && id_val <= to_id.raw() {
272                    let mut fields = HashMap::new();
273                    for (key, value) in row.iter_fields() {
274                        if key != "id" {
275                            fields.insert(key.to_string(), value.clone());
276                        }
277                    }
278                    entries.push(LogEntry {
279                        id: LogId(id_val),
280                        fields,
281                    });
282                }
283            }
284            true
285        });
286
287        partial_top_k(&mut entries, limit, |a, b| a.id.cmp(&b.id));
288        entries
289    }
290
291    /// Apply retention policy: delete entries older than the threshold.
292    pub fn apply_retention(&self) -> u64 {
293        match &self.config.retention {
294            LogRetention::Forever => 0,
295            LogRetention::Days(days) => {
296                let cutoff_ms = std::time::SystemTime::now()
297                    .duration_since(std::time::UNIX_EPOCH)
298                    .unwrap_or_default()
299                    .as_millis() as u64
300                    - days * 86_400_000;
301                let cutoff_id = LogId::from_ms(cutoff_ms);
302                self.delete_before(cutoff_id)
303            }
304            LogRetention::MaxEntries(max) => {
305                let manager = match self.store.get_collection(&self.config.name) {
306                    Some(m) => m,
307                    None => return 0,
308                };
309
310                let mut ids: Vec<(u64, EntityId)> = Vec::new();
311                manager.for_each_entity(|entity| {
312                    if let Some(row) = entity.data.as_row() {
313                        if let Some(Value::UnsignedInteger(log_id)) = row.get_field("id") {
314                            ids.push((*log_id, entity.id));
315                        }
316                    }
317                    true
318                });
319
320                if ids.len() as u64 <= *max {
321                    return 0;
322                }
323
324                ids.sort_by_key(|(log_id, _)| *log_id);
325                let to_delete = ids.len() as u64 - max;
326                let mut deleted = 0u64;
327                for (_, entity_id) in ids.iter().take(to_delete as usize) {
328                    if self
329                        .store
330                        .delete(&self.config.name, *entity_id)
331                        .unwrap_or(false)
332                    {
333                        deleted += 1;
334                    }
335                }
336                deleted
337            }
338            LogRetention::MaxBytes(max_bytes) => {
339                let manager = match self.store.get_collection(&self.config.name) {
340                    Some(m) => m,
341                    None => return 0,
342                };
343
344                // Collect (log_id, entity_id, approx_size) sorted by time
345                let mut entries: Vec<(u64, EntityId, u64)> = Vec::new();
346                manager.for_each_entity(|entity| {
347                    if let Some(row) = entity.data.as_row() {
348                        let log_id = row
349                            .get_field("id")
350                            .and_then(|v| match v {
351                                Value::UnsignedInteger(n) => Some(*n),
352                                _ => None,
353                            })
354                            .unwrap_or(0);
355
356                        // Approximate entry size: 8 bytes per field + value sizes
357                        let mut size = 8u64; // id field
358                        for (key, value) in row.iter_fields() {
359                            size += key.len() as u64 + estimate_value_size(value);
360                        }
361                        entries.push((log_id, entity.id, size));
362                    }
363                    true
364                });
365
366                entries.sort_by_key(|(log_id, _, _)| *log_id);
367
368                let total_size: u64 = entries.iter().map(|(_, _, s)| s).sum();
369                if total_size <= *max_bytes {
370                    return 0;
371                }
372
373                // Delete oldest entries until under budget
374                let mut to_free = total_size - max_bytes;
375                let mut deleted = 0u64;
376                for (_, entity_id, size) in &entries {
377                    if to_free == 0 {
378                        break;
379                    }
380                    if self
381                        .store
382                        .delete(&self.config.name, *entity_id)
383                        .unwrap_or(false)
384                    {
385                        deleted += 1;
386                        to_free = to_free.saturating_sub(*size);
387                    }
388                }
389                deleted
390            }
391        }
392    }
393
394    fn delete_before(&self, cutoff: LogId) -> u64 {
395        let manager = match self.store.get_collection(&self.config.name) {
396            Some(m) => m,
397            None => return 0,
398        };
399
400        let mut to_delete = Vec::new();
401        manager.for_each_entity(|entity| {
402            if let Some(row) = entity.data.as_row() {
403                if let Some(Value::UnsignedInteger(log_id)) = row.get_field("id") {
404                    if *log_id < cutoff.raw() {
405                        to_delete.push(entity.id);
406                    }
407                }
408            }
409            true
410        });
411
412        let mut deleted = 0u64;
413        for entity_id in to_delete {
414            if self
415                .store
416                .delete(&self.config.name, entity_id)
417                .unwrap_or(false)
418            {
419                deleted += 1;
420            }
421        }
422        deleted
423    }
424
425    /// Total number of entries.
426    pub fn len(&self) -> usize {
427        self.flush_buffer();
428        self.store
429            .get_collection(&self.config.name)
430            .map(|m| m.stats().total_entities)
431            .unwrap_or(0)
432    }
433
434    /// Config reference.
435    pub fn config(&self) -> &LogCollectionConfig {
436        &self.config
437    }
438}
439
440fn estimate_value_size(value: &Value) -> u64 {
441    match value {
442        Value::Null => 1,
443        Value::Boolean(_) => 1,
444        Value::Integer(_) | Value::UnsignedInteger(_) | Value::Float(_) => 8,
445        Value::Text(s) => s.len() as u64,
446        Value::Blob(b) => b.len() as u64,
447        Value::Vector(v) => v.len() as u64 * 4,
448        Value::Array(a) => a.iter().map(estimate_value_size).sum::<u64>() + 8,
449        _ => 16, // conservative default for other types
450    }
451}
452
453impl Drop for LogCollection {
454    fn drop(&mut self) {
455        self.flush_buffer();
456    }
457}
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462
463    fn test_store() -> Arc<UnifiedStore> {
464        Arc::new(UnifiedStore::new())
465    }
466
467    #[test]
468    fn test_append_and_query() {
469        let store = test_store();
470        let log = LogCollection::new(store, LogCollectionConfig::new("test_log"));
471
472        let id1 = log.append_fields(vec![
473            ("level", Value::text("info")),
474            ("message", Value::text("hello")),
475        ]);
476        let id2 = log.append_fields(vec![
477            ("level", Value::text("error")),
478            ("message", Value::text("oops")),
479        ]);
480
481        assert!(id2.raw() > id1.raw());
482
483        let recent = log.recent(10);
484        assert_eq!(recent.len(), 2);
485        assert_eq!(recent[0].id, id2); // newest first
486        assert_eq!(recent[1].id, id1);
487    }
488
489    #[test]
490    fn test_retention_max_entries() {
491        let store = test_store();
492        let mut config = LogCollectionConfig::new("retention_test");
493        config.retention = LogRetention::MaxEntries(3);
494        config.batch_size = 1;
495
496        let log = LogCollection::new(store, config);
497
498        for i in 0..5 {
499            log.append_fields(vec![("seq", Value::Integer(i))]);
500        }
501
502        assert_eq!(log.len(), 5);
503        let deleted = log.apply_retention();
504        assert_eq!(deleted, 2);
505        assert_eq!(log.len(), 3);
506    }
507
508    #[test]
509    fn test_retention_max_bytes() {
510        let store = test_store();
511        let mut config = LogCollectionConfig::new("bytes_retention_test");
512        config.retention = LogRetention::MaxBytes(200);
513        config.batch_size = 1;
514
515        let log = LogCollection::new(store, config);
516
517        // Insert entries with known sizes (~30-50 bytes each)
518        for i in 0..10 {
519            log.append_fields(vec![("msg", Value::text(format!("entry-{}", i)))]);
520        }
521
522        let before = log.len();
523        assert_eq!(before, 10);
524
525        let deleted = log.apply_retention();
526        assert!(
527            deleted > 0,
528            "should delete some entries to fit under 200 bytes"
529        );
530        assert!(log.len() < 10, "should have fewer entries after retention");
531    }
532
533    #[test]
534    fn test_batch_buffering() {
535        let store = test_store();
536        let mut config = LogCollectionConfig::new("batch_test");
537        config.batch_size = 4;
538
539        let log = LogCollection::new(store.clone(), config);
540
541        // Insert 3 — should stay in buffer (batch_size = 4)
542        for _ in 0..3 {
543            log.append_fields(vec![("msg", Value::text("buffered"))]);
544        }
545
546        // Buffer not flushed yet — store might be empty
547        // But recent() flushes first
548        let entries = log.recent(10);
549        assert_eq!(entries.len(), 3);
550    }
551
552    #[test]
553    fn test_id_is_time_ordered() {
554        let store = test_store();
555        let log = LogCollection::new(store, LogCollectionConfig::new("time_test"));
556
557        let ids: Vec<LogId> = (0..100)
558            .map(|i| log.append_fields(vec![("i", Value::Integer(i))]))
559            .collect();
560
561        for i in 1..ids.len() {
562            assert!(ids[i].raw() > ids[i - 1].raw());
563            assert!(ids[i].timestamp_us() >= ids[i - 1].timestamp_us());
564        }
565    }
566}