1use crate::{CoreError, DeleteResult, Identity, KeyPrefix, RecordKey, Result, Store};
11
12#[derive(Clone, Debug, Default, PartialEq, Eq)]
14#[must_use = "a ResetReport may list conflicted keys that were not deleted"]
15pub struct ResetReport {
16 pub deleted: Vec<RecordKey>,
19 pub conflicts: Vec<RecordKey>,
22}
23
24pub async fn reset(store: &dyn Store, prefix: &KeyPrefix) -> Result<ResetReport> {
37 reset_as(store, prefix, None).await
38}
39
40pub async fn reset_as(
50 store: &dyn Store,
51 prefix: &KeyPrefix,
52 author: Option<Identity>,
53) -> Result<ResetReport> {
54 if prefix.namespace.is_none() {
55 return Err(CoreError::Backend("reset requires a namespace".into()));
56 }
57 let mut report = ResetReport::default();
58 for key in store.list(prefix).await? {
59 if !prefix.matches(&key) {
62 continue;
63 }
64 let Some(current) = store.get(&key).await? else {
65 continue;
66 };
67 match store
68 .delete_as(&key, Some(current.revision), author.clone())
69 .await?
70 {
71 DeleteResult::Deleted => report.deleted.push(key),
72 DeleteResult::Conflict(_) => report.conflicts.push(key),
73 }
74 }
75 Ok(report)
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use crate::memstore::MemStore;
82 use crate::test_support::{Hook, HookedStore, rec, seed};
83 use std::sync::atomic::AtomicBool;
84
85 const CLOCK: i64 = 1_000;
86
87 fn ns(namespace: &str) -> KeyPrefix {
88 KeyPrefix {
89 namespace: Some(namespace.into()),
90 collection: None,
91 }
92 }
93
94 #[tokio::test]
95 async fn refuses_a_prefix_without_a_namespace() {
96 let store = MemStore::new().with_clock(CLOCK);
97 seed(&store, rec("ns", "col", "a", "x")).await;
98
99 let err = reset(
100 &store,
101 &KeyPrefix {
102 namespace: None,
103 collection: Some("col".into()),
104 },
105 )
106 .await
107 .unwrap_err();
108
109 assert!(
110 matches!(&err, CoreError::Backend(m) if m == "reset requires a namespace"),
111 "got {err:?}"
112 );
113 assert!(store.raw_snapshot().values().all(|r| !r.is_tombstone()));
115 }
116
117 #[tokio::test]
118 async fn tombstones_every_live_record_in_the_namespace_only() {
119 let store = MemStore::new().with_clock(CLOCK);
120 seed(&store, rec("ns", "a", "1", "x")).await;
121 seed(&store, rec("ns", "b", "2", "y")).await;
122 seed(&store, rec("other", "a", "3", "z")).await;
123
124 let report = reset(&store, &ns("ns")).await.unwrap();
125
126 assert_eq!(
127 report.deleted,
128 vec![
129 RecordKey::new("ns", "a", "1"),
130 RecordKey::new("ns", "b", "2")
131 ]
132 );
133 assert!(report.conflicts.is_empty());
134 for key in &report.deleted {
135 assert_eq!(store.get(key).await.unwrap(), None, "{key} hidden");
136 let raw = store.get_raw(key).await.unwrap().expect("tombstone kept");
137 assert!(raw.is_tombstone());
138 assert_eq!(raw.deleted_at, Some(CLOCK));
139 }
140 let other = RecordKey::new("other", "a", "3");
141 assert!(
142 store.get(&other).await.unwrap().is_some(),
143 "sibling namespace untouched"
144 );
145 }
146
147 #[tokio::test]
148 async fn collection_scope_leaves_sibling_collections_alone() {
149 let store = MemStore::new().with_clock(CLOCK);
150 seed(&store, rec("ns", "col", "1", "x")).await;
151 seed(&store, rec("ns", "sibling", "2", "y")).await;
152
153 let report = reset(
154 &store,
155 &KeyPrefix {
156 namespace: Some("ns".into()),
157 collection: Some("col".into()),
158 },
159 )
160 .await
161 .unwrap();
162
163 assert_eq!(report.deleted, vec![RecordKey::new("ns", "col", "1")]);
164 assert!(report.conflicts.is_empty());
165 assert!(
166 store
167 .get(&RecordKey::new("ns", "sibling", "2"))
168 .await
169 .unwrap()
170 .is_some()
171 );
172 }
173
174 #[tokio::test]
175 async fn already_deleted_keys_are_skipped_and_their_chain_does_not_advance() {
176 let store = MemStore::new().with_clock(CLOCK);
177 let gone = RecordKey::new("ns", "col", "gone");
178 seed(&store, rec("ns", "col", "gone", "x")).await;
179 seed(&store, rec("ns", "col", "live", "y")).await;
180 assert_eq!(
181 store.delete(&gone, None).await.unwrap(),
182 DeleteResult::Deleted
183 );
184 let before = store.get_raw(&gone).await.unwrap().unwrap().revision;
185
186 let report = reset(&store, &ns("ns")).await.unwrap();
187
188 assert_eq!(report.deleted, vec![RecordKey::new("ns", "col", "live")]);
189 assert_eq!(
190 store.get_raw(&gone).await.unwrap().unwrap().revision,
191 before
192 );
193 }
194
195 #[tokio::test]
196 async fn concurrent_edit_is_a_conflict_and_rerunning_is_idempotent() {
197 let raced = RecordKey::new("ns", "col", "b");
198 let store = HookedStore {
199 inner: MemStore::new().with_clock(CLOCK),
200 hook: Hook::EditBeforeDelete(raced.clone()),
201 fired: AtomicBool::new(false),
202 };
203 for id in ["a", "b", "c"] {
204 seed(&store, rec("ns", "col", id, id)).await;
205 }
206
207 let first = reset(&store, &ns("ns")).await.unwrap();
209 assert_eq!(
210 first.deleted,
211 vec![
212 RecordKey::new("ns", "col", "a"),
213 RecordKey::new("ns", "col", "c")
214 ]
215 );
216 assert_eq!(first.conflicts, vec![raced.clone()]);
217 let survivor = store.get(&raced).await.unwrap().expect("edit survives");
218 assert_eq!(survivor.body.bytes(), b"edited concurrently");
219
220 let second = reset(&store, &ns("ns")).await.unwrap();
222 assert_eq!(second.deleted, vec![raced.clone()]);
223 assert!(second.conflicts.is_empty());
224 assert_eq!(store.get(&raced).await.unwrap(), None);
225
226 let third = reset(&store, &ns("ns")).await.unwrap();
228 assert_eq!(third, ResetReport::default());
229 }
230
231 #[tokio::test]
232 async fn reset_as_stamps_the_deleter() {
233 let store = MemStore::new().with_clock(CLOCK);
234 let key = RecordKey::new("ns", "col", "a");
235 seed(&store, rec("ns", "col", "a", "x")).await;
236
237 let report = reset_as(&store, &ns("ns"), Some(Identity::new("x")))
238 .await
239 .unwrap();
240
241 assert_eq!(report.deleted, vec![key.clone()]);
242 let raw = store.get_raw(&key).await.unwrap().expect("tombstone kept");
243 assert!(raw.is_tombstone());
244 assert_eq!(raw.meta.author, Identity::new("x"));
245 }
246
247 #[tokio::test]
248 async fn a_key_that_vanishes_between_list_and_get_is_skipped() {
249 let vanished = RecordKey::new("ns", "col", "b");
250 let store = HookedStore {
251 inner: MemStore::new().with_clock(CLOCK),
252 hook: Hook::VanishBeforeGet(vanished),
253 fired: AtomicBool::new(false),
254 };
255 seed(&store, rec("ns", "col", "a", "x")).await;
256 seed(&store, rec("ns", "col", "b", "y")).await;
257
258 let report = reset(&store, &ns("ns")).await.unwrap();
259
260 assert_eq!(report.deleted, vec![RecordKey::new("ns", "col", "a")]);
261 assert!(report.conflicts.is_empty());
262 }
263}