1use std::collections::BTreeSet;
2
3use crate::{persist_exact_remote_object_on, DbError};
4
5use super::{StoreDatabase, StoreSession};
6
7impl StoreSession<'_> {
8 fn begin_owner_promotion_journal(
9 &self,
10 journal_key: &str,
11 target_key: &str,
12 value: &str,
13 ) -> Result<coven_protocol::owner_promotion_journal::OwnerPromotionJournal, DbError> {
14 let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
15 tx.execute(
16 "INSERT OR IGNORE INTO protocol_state (key, value) VALUES (?1, ?2)",
17 (journal_key, value),
18 )
19 .map_err(DbError::from)?;
20 tx.execute(
21 "INSERT OR IGNORE INTO protocol_state (key, value) VALUES (?1, ?2)",
22 (target_key, value),
23 )
24 .map_err(DbError::from)?;
25 let by_id = crate::required_protocol_state_on(&tx, journal_key)?;
26 let by_target = crate::required_protocol_state_on(&tx, target_key)?;
27 if by_id != by_target {
28 return Err(DbError::Message(
29 "Owner-promotion id and target journals disagree".to_string(),
30 ));
31 }
32 tx.commit().map_err(DbError::from)?;
33 serde_json::from_str(&by_id)
34 .map_err(|error| DbError::context("parse begun Owner-promotion journal", error))
35 }
36
37 fn begin_owner_promotion_acceptance_journal(
38 &self,
39 journal_key: &str,
40 value: &str,
41 ) -> Result<coven_protocol::owner_promotion_journal::OwnerPromotionJournal, DbError> {
42 self.conn
43 .execute(
44 "INSERT OR IGNORE INTO protocol_state (key, value) VALUES (?1, ?2)",
45 (journal_key, value),
46 )
47 .map_err(DbError::from)?;
48 let actual = crate::required_protocol_state_on(self.conn, journal_key)?;
49 if actual != value {
50 return Err(DbError::Message(
51 "Owner-promotion id is already bound to different candidate acceptance".to_string(),
52 ));
53 }
54 serde_json::from_str(&actual).map_err(|error| {
55 DbError::context("parse begun Owner-promotion candidate acceptance", error)
56 })
57 }
58
59 fn advance_owner_promotion_journal(
60 &self,
61 transition: coven_protocol::owner_promotion_journal::OwnerPromotionJournalTransition,
62 ) -> Result<(), DbError> {
63 let (journal_key, target_key, previous_value, next_value, remote_objects) =
64 transition.into_values();
65 let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
66 crate::store::store_session::StoreTransaction::new(&tx, self.store_dir)
67 .advance_owner_promotion_journal(
68 journal_key,
69 target_key,
70 previous_value,
71 next_value,
72 remote_objects,
73 )?;
74 tx.commit().map_err(DbError::from)
75 }
76
77 fn end_nonactivated_owner_promotion_candidate(
78 &self,
79 transition: coven_protocol::owner_promotion_journal::OwnerPromotionJournalTransition,
80 candidate: coven_protocol::store_commit::StoreBatchCommitRef,
81 objects: Vec<coven_protocol::objects::ExactObjectRef>,
82 nonactivation: coven_protocol::remote_object::CandidateNonactivation,
83 ) -> Result<Vec<super::candidate_records::CandidateCleanupObject>, DbError> {
84 let (journal_key, target_key, previous_value, next_value, remote_objects) =
85 transition.into_values();
86 let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
87 let cleanup = super::candidate_records::begin_candidate_nonactivation_targets_on(
88 &tx,
89 &candidate,
90 &objects,
91 &nonactivation,
92 )?;
93 crate::store::store_session::StoreTransaction::new(&tx, self.store_dir)
94 .advance_owner_promotion_journal(
95 journal_key,
96 target_key,
97 previous_value,
98 next_value,
99 remote_objects,
100 )?;
101 tx.commit().map_err(DbError::from)?;
102 Ok(cleanup)
103 }
104
105 fn owner_promotion_candidate_cleanup_targets(
106 &self,
107 candidate: &coven_protocol::store_commit::StoreBatchCommitRef,
108 objects: &[coven_protocol::objects::ExactObjectRef],
109 ) -> Result<Vec<super::candidate_records::CandidateCleanupObject>, DbError> {
110 super::candidate_records::candidate_cleanup_targets_on(self.conn, candidate, objects)
111 }
112
113 fn replace_failed_owner_promotion_journal(
114 &self,
115 replacement: coven_protocol::owner_promotion_journal::OwnerPromotionJournal,
116 target_key: String,
117 replacement_key: String,
118 previous_value: String,
119 replacement_value: String,
120 ) -> Result<coven_protocol::owner_promotion_journal::OwnerPromotionJournal, DbError> {
121 let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
122 let inserted = tx
123 .execute(
124 "INSERT OR IGNORE INTO protocol_state (key, value) VALUES (?1, ?2)",
125 (&replacement_key, &replacement_value),
126 )
127 .map_err(DbError::from)?;
128 if inserted != 1 {
129 return Err(DbError::Message(
130 "fresh Owner-promotion retry identity is already present".to_string(),
131 ));
132 }
133 let replaced = tx
134 .execute(
135 "UPDATE protocol_state SET value = ?1 WHERE key = ?2 AND value = ?3",
136 (&replacement_value, &target_key, &previous_value),
137 )
138 .map_err(DbError::from)?;
139 if replaced != 1 {
140 return Err(DbError::Message(
141 "Owner-promotion retry lost its exact failed target attempt".to_string(),
142 ));
143 }
144 tx.commit().map_err(DbError::from)?;
145 Ok(replacement)
146 }
147}
148
149impl StoreDatabase {
150 pub async fn load_owner_promotion_journal(
151 &self,
152 promotion_id: coven_protocol::store_commit::OwnerPromotionId,
153 ) -> Result<Option<coven_protocol::owner_promotion_journal::OwnerPromotionJournal>, DbError>
154 {
155 let key = format!("owner_promotion/{promotion_id}");
156 self.call_store(move |session| {
157 session
158 .protocol_state(&key)?
159 .map(|value| {
160 let journal: coven_protocol::owner_promotion_journal::OwnerPromotionJournal =
161 serde_json::from_str(&value).map_err(|error| {
162 DbError::context("parse Owner-promotion journal", error)
163 })?;
164 journal.validate_id(promotion_id).map_err(DbError::from)?;
165 Ok(journal)
166 })
167 .transpose()
168 })
169 .await
170 }
171
172 pub async fn load_owner_promotion_target(
173 &self,
174 key: String,
175 ) -> Result<Option<coven_protocol::owner_promotion_journal::OwnerPromotionJournal>, DbError>
176 {
177 self.call_store(move |session| {
178 let value = session.protocol_state(&key)?;
179 let Some(value) = value else {
180 return Ok(None);
181 };
182 let journal: coven_protocol::owner_promotion_journal::OwnerPromotionJournal =
183 serde_json::from_str(&value).map_err(|error| {
184 DbError::context("parse Owner-promotion target journal", error)
185 })?;
186 journal.validate_target_key(&key).map_err(DbError::from)?;
187 let journal_key = format!("owner_promotion/{}", journal.promotion_id());
188 let by_id = session.protocol_state(&journal_key)?;
189 if by_id.as_deref() != Some(value.as_str()) {
190 return Err(DbError::Message(
191 "Owner-promotion target and id journals disagree".to_string(),
192 ));
193 }
194 Ok(Some(journal))
195 })
196 .await
197 }
198
199 pub async fn begin_owner_promotion_journal(
200 &self,
201 target_key: String,
202 journal: coven_protocol::owner_promotion_journal::OwnerPromotionJournal,
203 ) -> Result<coven_protocol::owner_promotion_journal::OwnerPromotionJournal, DbError> {
204 journal.validate_begin().map_err(DbError::from)?;
205 if journal.target_state_key().map_err(DbError::from)? != target_key {
206 return Err(DbError::Message(
207 "Owner-promotion target index differs from its journal target".to_string(),
208 ));
209 }
210 let journal_key = format!("owner_promotion/{}", journal.promotion_id());
211 let value = serde_json::to_string(&journal)
212 .map_err(|error| DbError::context("serialize Owner-promotion journal", error))?;
213 self.call_store(move |session| {
214 session.begin_owner_promotion_journal(&journal_key, &target_key, &value)
215 })
216 .await
217 }
218
219 pub async fn begin_owner_promotion_acceptance_journal(
220 &self,
221 journal: coven_protocol::owner_promotion_journal::OwnerPromotionJournal,
222 ) -> Result<coven_protocol::owner_promotion_journal::OwnerPromotionJournal, DbError> {
223 journal.validate_acceptance_begin().map_err(DbError::from)?;
224 let journal_key = format!("owner_promotion/{}", journal.promotion_id());
225 let value = serde_json::to_string(&journal).map_err(|error| {
226 DbError::context("serialize Owner-promotion candidate acceptance", error)
227 })?;
228 self.call_store(move |session| {
229 session.begin_owner_promotion_acceptance_journal(&journal_key, &value)
230 })
231 .await
232 }
233
234 pub async fn advance_owner_promotion_journal(
235 &self,
236 transition: coven_protocol::owner_promotion_journal::OwnerPromotionJournalTransition,
237 ) -> Result<(), DbError> {
238 self.call_store(move |session| session.advance_owner_promotion_journal(transition))
239 .await
240 }
241
242 pub async fn end_nonactivated_owner_promotion_candidate(
250 &self,
251 transition: coven_protocol::owner_promotion_journal::OwnerPromotionJournalTransition,
252 candidate: coven_protocol::store_commit::StoreBatchCommitRef,
253 objects: Vec<coven_protocol::objects::ExactObjectRef>,
254 nonactivation: coven_protocol::remote_object::VerifiedCandidateNonactivation,
255 ) -> Result<Vec<super::candidate_records::CandidateCleanupObject>, DbError> {
256 if nonactivation.candidate_reference().map_err(DbError::from)? != candidate {
257 return Err(DbError::Message(
258 "verified nonactivation names another Owner-promotion candidate".to_string(),
259 ));
260 }
261 let nonactivation = nonactivation.into_durable();
262 self.call_store(move |session| {
263 session.end_nonactivated_owner_promotion_candidate(
264 transition,
265 candidate,
266 objects,
267 nonactivation,
268 )
269 })
270 .await
271 }
272
273 pub async fn owner_promotion_candidate_cleanup_targets(
278 &self,
279 candidate: coven_protocol::store_commit::StoreBatchCommitRef,
280 objects: Vec<coven_protocol::objects::ExactObjectRef>,
281 ) -> Result<Vec<super::candidate_records::CandidateCleanupObject>, DbError> {
282 self.call_store(move |session| {
283 session.owner_promotion_candidate_cleanup_targets(&candidate, &objects)
284 })
285 .await
286 }
287
288 pub async fn replace_failed_owner_promotion_journal(
289 &self,
290 previous: coven_protocol::owner_promotion_journal::OwnerPromotionJournal,
291 replacement: coven_protocol::owner_promotion_journal::OwnerPromotionJournal,
292 ) -> Result<coven_protocol::owner_promotion_journal::OwnerPromotionJournal, DbError> {
293 previous
294 .validate_failed_attempt_replacement(&replacement)
295 .map_err(DbError::from)?;
296 let target_key = previous.target_state_key().map_err(DbError::from)?;
297 if replacement.target_state_key().map_err(DbError::from)? != target_key {
298 return Err(DbError::Message(
299 "Owner-promotion retry target differs from its failed attempt".to_string(),
300 ));
301 }
302 let replacement_key = format!("owner_promotion/{}", replacement.promotion_id());
303 let previous_value = serde_json::to_string(&previous)
304 .map_err(|error| DbError::context("serialize failed Owner-promotion journal", error))?;
305 let replacement_value = serde_json::to_string(&replacement).map_err(|error| {
306 DbError::context("serialize replacement Owner-promotion journal", error)
307 })?;
308 self.call_store(move |session| {
309 session.replace_failed_owner_promotion_journal(
310 replacement,
311 target_key,
312 replacement_key,
313 previous_value,
314 replacement_value,
315 )
316 })
317 .await
318 }
319}
320
321#[allow(clippy::too_many_arguments)]
322pub(super) fn advance_owner_promotion_journal_on(
323 tx: &rusqlite::Transaction<'_>,
324 store_dir: &coven_foundation::store_dir::StoreDir,
325 journal_key: String,
326 target_key: String,
327 previous_value: String,
328 next_value: String,
329 remote_objects: Vec<coven_protocol::remote_object::ClosedRemoteObject>,
330) -> Result<(), DbError> {
331 let mut object_ids = BTreeSet::new();
332 for remote in &remote_objects {
333 if !object_ids.insert(remote.object_id()) {
334 return Err(DbError::Message(
335 "Owner-promotion journal repeats a remote object".to_string(),
336 ));
337 }
338 persist_exact_remote_object_on(tx, store_dir, remote, "Owner-promotion candidate object")?;
339 }
340 let by_id = tx
341 .execute(
342 "UPDATE protocol_state SET value = ?1 WHERE key = ?2 AND value = ?3",
343 (&next_value, &journal_key, &previous_value),
344 )
345 .map_err(DbError::from)?;
346 let by_target = tx
347 .execute(
348 "UPDATE protocol_state SET value = ?1 WHERE key = ?2 AND value = ?3",
349 (&next_value, &target_key, &previous_value),
350 )
351 .map_err(DbError::from)?;
352 if by_id != 1 || by_target != 1 {
353 return Err(DbError::Message(
354 "Owner-promotion journal advance lost its exact predecessor".to_string(),
355 ));
356 }
357 Ok(())
358}