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::unified::entity::{EntityData, EntityId, EntityKind, RowData, UnifiedEntity};
8use crate::storage::unified::store::{StoreError, UnifiedStore};
9use reddb_types::Value;
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, 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    /// Write one entry before returning its ID. Used by the per-request HTTP
141    /// log collection, which has no previously buffered entries.
142    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    /// Append a log entry from (key, value) pairs.
177    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    /// Flush the write buffer to storage.
186    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    /// Query recent entries (newest first).
202    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        // Phase 1: collect top-k log IDs + entity IDs using a bounded min-heap.
211        // Only stores (log_id, entity_id) — no field cloning until phase 2.
212        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        // Phase 2: fetch full entities only for top-k (avoids cloning all fields)
241        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)); // newest first
247
248        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    /// Query entries within a time range (by ID boundaries).
268    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    /// Apply retention policy: delete entries older than the threshold.
308    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                // Collect (log_id, entity_id, approx_size) sorted by time
361                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                        // Approximate entry size: 8 bytes per field + value sizes
373                        let mut size = 8u64; // id field
374                        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                // Delete oldest entries until under budget
390                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    /// Total number of entries.
442    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    /// Config reference.
451    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, // conservative default for other types
466    }
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); // newest first
502        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        // Insert entries with known sizes (~30-50 bytes each)
534        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        // Insert 3 — should stay in buffer (batch_size = 4)
558        for _ in 0..3 {
559            log.append_fields(vec![("msg", Value::text("buffered"))]);
560        }
561
562        // Buffer not flushed yet — store might be empty
563        // But recent() flushes first
564        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}