Skip to main content

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}