1use 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#[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 = 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 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 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 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 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 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)); 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 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 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 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 let mut size = 8u64; 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 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 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 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, }
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); 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 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 for _ in 0..3 {
543 log.append_fields(vec![("msg", Value::text("buffered"))]);
544 }
545
546 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}