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