nodedb_types/dropped_collection.rs
1//! `DroppedCollection` — row shape for `_system.dropped_collections`.
2//!
3//! Returned by `NodeDb::list_dropped_collections`. Each row describes
4//! one soft-deleted collection within its retention window (the
5//! `StoredCollection` redb row is still present with `is_active =
6//! false`). Once the retention window elapses, the sweeper hard-deletes
7//! the row and it disappears from this list.
8
9use serde::{Deserialize, Serialize};
10
11/// A soft-deleted collection awaiting either `UNDROP` or retention-driven
12/// hard deletion.
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct DroppedCollection {
15 /// Tenant that owns this collection.
16 pub tenant_id: u32,
17 /// Collection name (unique per tenant while soft-deleted).
18 pub name: String,
19 /// Preserved owner at the time of `DROP` — the user who can
20 /// `UNDROP` without superuser/tenant_admin elevation. Empty
21 /// string if the owner row could not be resolved at the time
22 /// of the catalog read.
23 pub owner: String,
24 /// Engine / collection-type slug resolved from
25 /// `StoredCollection.collection_type.as_str()`. One of
26 /// `"document"`, `"strict"`, `"columnar"`, `"timeseries"`,
27 /// `"columnar:spatial"`, `"kv"`. Drives operator dashboards that
28 /// need to group pending-purge rows by engine.
29 pub engine_type: String,
30 /// Wall-clock nanoseconds when `DROP COLLECTION` was committed
31 /// (from `stored.modification_hlc.wall_ns`).
32 pub deactivated_at_ns: u64,
33 /// Wall-clock nanoseconds at which the retention window elapses
34 /// and the sweeper will hard-delete this row. Derived from
35 /// `deactivated_at_ns + retention_window`.
36 pub retention_expires_at_ns: u64,
37}
38
39impl DroppedCollection {
40 /// Whether the retention window has already elapsed as of `now_ns`.
41 /// Rows with `is_expired(now_ns) == true` are candidates for the
42 /// next sweeper pass; they may appear in the list briefly before
43 /// the sweeper runs.
44 pub fn is_expired(&self, now_ns: u64) -> bool {
45 now_ns >= self.retention_expires_at_ns
46 }
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52
53 #[test]
54 fn is_expired_strictly_on_or_past_window() {
55 let d = DroppedCollection {
56 tenant_id: 1,
57 name: "orders".into(),
58 owner: "admin".into(),
59 engine_type: "document".into(),
60 deactivated_at_ns: 100,
61 retention_expires_at_ns: 200,
62 };
63 assert!(!d.is_expired(199));
64 assert!(d.is_expired(200));
65 assert!(d.is_expired(300));
66 }
67
68 #[test]
69 fn serde_roundtrip() {
70 let d = DroppedCollection {
71 tenant_id: 7,
72 name: "test".into(),
73 owner: "alice".into(),
74 engine_type: "timeseries".into(),
75 deactivated_at_ns: 1_700_000_000_000_000_000,
76 retention_expires_at_ns: 1_700_604_800_000_000_000,
77 };
78 let json = serde_json::to_string(&d).unwrap();
79 let back: DroppedCollection = serde_json::from_str(&json).unwrap();
80 assert_eq!(d, back);
81 }
82}