1use rusqlite::Connection;
2
3mod discard;
4
5use super::{
6 MergeMaterializationTransaction, StoreDatabase, StoreSession, StoreTransactionOutcome,
7 VerifiedStoreTransaction,
8};
9use crate::{
10 candidate_graph_exact_objects, circle_operation_ids_in_phase_on, load_circle_operation_on,
11 load_remote_object_on, persist_prepared_remote_object_on, update_remote_object_on,
12 ActiveStorePublication, ActiveStorePublicationOwner, DbError, PreparedCircleOperationRow,
13 VerifiedMergeMaterialization,
14};
15use coven_protocol::circle::{CircleOperationId, CircleOperationState};
16use coven_protocol::circle_activation::VerifiedCircleActivations;
17use coven_protocol::circle_journal::{CircleOperationJournal, CircleOperationProgress};
18use coven_protocol::objects::PreparedExactObject;
19use coven_protocol::remote_object::remote_object_id;
20use coven_protocol::store_commit::{
21 commit_semantic_prefix, VerifiedStoreBatchCommit, VerifiedStoreDeviceOperations,
22};
23
24pub type PreparedCircleObjects = std::collections::BTreeMap<String, PreparedExactObject>;
28
29fn persist_circle_operation_objects_on(
30 conn: &Connection,
31 store_dir: &coven_foundation::store_dir::StoreDir,
32 remotes: &[coven_protocol::remote_object::ClosedRemoteObject],
33 prepared_objects: &PreparedCircleObjects,
34 owner: &coven_protocol::store_commit::StoreBatchCommitRef,
35 domain: &str,
36) -> Result<(), DbError> {
37 let mut installed = std::collections::BTreeSet::new();
38 for remote in remotes {
39 persist_prepared_remote_object_on(conn, store_dir, remote, owner, domain)?;
40 installed.extend(remote.payload_bytes().keys().copied());
41 }
42 for object in prepared_objects.values() {
43 let expected = object.reference().stored_hash();
44 if !installed.insert(expected) {
45 continue;
46 }
47 let actual =
48 crate::payload_store::write_payload_blocking(conn, store_dir, object.stored_bytes())
49 .map_err(|error| DbError::context(format!("install {domain} payload"), error))?;
50 if actual != expected {
51 return Err(DbError::Message(format!(
52 "{domain} payload installed as {actual}, referenced as {expected}"
53 )));
54 }
55 }
56 Ok(())
57}
58
59impl StoreSession<'_> {
60 fn activate_circle_operation(
61 &mut self,
62 journal: CircleOperationJournal,
63 verified: VerifiedCircleActivations,
64 accepted_transition: crate::StoreCommitPublicationOutcome,
65 ) -> Result<Option<crate::OwnedVerifiedMergeMaterialization>, DbError> {
66 self.verified_store_transaction(move |transaction| {
67 let materialization =
68 transaction.activate_circle_operation(journal, verified, accepted_transition)?;
69 Ok(StoreTransactionOutcome::Commit(materialization))
70 })
71 }
72
73 fn insert_circle_operation(
74 &mut self,
75 journal: CircleOperationJournal,
76 prepared_objects: PreparedCircleObjects,
77 ) -> Result<(), DbError> {
78 let remotes = journal
79 .closed_remote_objects(&prepared_objects)
80 .map_err(DbError::from)?;
81 let owner = journal.operation().commit_ref().clone();
82 let row = PreparedCircleOperationRow::from_journal(&journal)?;
83 let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
84 claim_circle_publication_on(&tx, &journal)?;
85 persist_circle_operation_objects_on(
86 &tx,
87 self.store_dir,
88 &remotes,
89 &prepared_objects,
90 &owner,
91 "Circle candidate graph",
92 )?;
93 claim_operation_payloads_on(&tx, &journal.operation_id, journal.operation())?;
94 insert_circle_operation_row_on(&tx, &row)?;
95 tx.commit().map_err(DbError::from)
96 }
97
98 fn insert_circle_operation_superseding(
99 &mut self,
100 journal: CircleOperationJournal,
101 superseded: CircleOperationId,
102 prepared_objects: PreparedCircleObjects,
103 ) -> Result<(), DbError> {
104 let remotes = journal
105 .closed_remote_objects(&prepared_objects)
106 .map_err(DbError::from)?;
107 let owner = journal.operation().commit_ref().clone();
108 let row = PreparedCircleOperationRow::from_journal(&journal)?;
109 let superseded = superseded.as_str().to_string();
110 let circle_id = row.circle_id.clone();
111 let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
112 let discarded = load_circle_operation_on(&tx, &superseded)?.ok_or_else(|| {
113 DbError::Message("superseded Circle operation is absent from its slot".to_string())
114 })?;
115 if discarded.circle_id.to_string() != circle_id {
116 return Err(DbError::Message(
117 "superseded Circle operation belongs to another circle".to_string(),
118 ));
119 }
120 release_operation_payloads_on(&tx, &discarded.operation_id)?;
121 let removed = tx
122 .execute(
123 "DELETE FROM circle_operations WHERE operation_id = ?1 AND circle_id = ?2",
124 rusqlite::params![superseded, circle_id],
125 )
126 .map_err(DbError::from)?;
127 if removed != 1 {
128 return Err(DbError::Message(
129 "superseded Circle operation is absent from its slot".to_string(),
130 ));
131 }
132 claim_circle_publication_on(&tx, &journal)?;
133 persist_circle_operation_objects_on(
134 &tx,
135 self.store_dir,
136 &remotes,
137 &prepared_objects,
138 &owner,
139 "Circle candidate graph",
140 )?;
141 claim_operation_payloads_on(&tx, &journal.operation_id, journal.operation())?;
142 insert_circle_operation_row_on(&tx, &row)?;
143 tx.commit().map_err(DbError::from)
144 }
145
146 fn circle_operation(
147 &mut self,
148 operation_id: String,
149 ) -> Result<Option<CircleOperationJournal>, DbError> {
150 load_circle_operation_on(self.conn, &operation_id)
151 }
152
153 fn circle_operation_step(
154 &mut self,
155 operation_id: String,
156 step: String,
157 ) -> Result<PreparedExactObject, DbError> {
158 let journal = load_circle_operation_on(self.conn, &operation_id)?.ok_or_else(|| {
159 DbError::Message(format!(
160 "Circle operation {operation_id} disappeared before opening step {step:?}"
161 ))
162 })?;
163 let object = journal
164 .operation()
165 .prepared_objects
166 .get(&step)
167 .ok_or_else(|| {
168 DbError::Message(format!(
169 "Circle operation {operation_id} has no payload for step {step:?}"
170 ))
171 })?;
172 let stored_bytes =
173 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir)
174 .payload(object.stored_hash())
175 .map_err(DbError::from)?;
176 PreparedExactObject::new(object.clone(), stored_bytes)
177 .map_err(|error| DbError::context(format!("Circle operation step {step:?}"), error))
178 }
179
180 fn oldest_pending_circle_operation(
181 &mut self,
182 ) -> Result<Option<CircleOperationJournal>, DbError> {
183 let conn = self.conn;
184 let Some(operation_id) = circle_operation_ids_in_phase_on(conn, |progress| {
185 matches!(
186 progress,
187 CircleOperationProgress::Ready | CircleOperationProgress::Finalizing
188 )
189 })?
190 .into_iter()
191 .next() else {
192 return Ok(None);
193 };
194 load_circle_operation_on(conn, &operation_id)
195 }
196
197 fn waiting_circle_operations(&mut self) -> Result<Vec<CircleOperationJournal>, DbError> {
198 let conn = self.conn;
199 let waiting = circle_operation_ids_in_phase_on(conn, |progress| {
200 matches!(progress, CircleOperationProgress::WaitingForCloseResponses)
201 })?;
202 waiting
203 .iter()
204 .map(|operation_id| {
205 load_circle_operation_on(conn, operation_id)?.ok_or_else(|| {
206 DbError::Message(format!(
207 "Circle operation {operation_id} disappeared while being listed"
208 ))
209 })
210 })
211 .collect()
212 }
213
214 fn complete_circle_operation_upload_step(
215 &mut self,
216 operation_id: String,
217 step: String,
218 ) -> Result<(), DbError> {
219 let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
220 let journal = load_circle_operation_on(&tx, &operation_id)?.ok_or_else(|| {
221 DbError::Message(format!(
222 "Circle operation {operation_id} disappeared before its upload step"
223 ))
224 })?;
225 let object = journal
226 .operation()
227 .prepared_objects
228 .get(&step)
229 .ok_or_else(|| {
230 DbError::Message(format!(
231 "Circle upload step {step:?} names no object of operation {operation_id}"
232 ))
233 })?
234 .clone();
235 let candidate_owned = journal.candidate_owned_objects().map_err(DbError::from)?;
236 tx.execute(
237 "INSERT OR IGNORE INTO circle_operation_uploads (operation_id, step)
238 VALUES (?1, ?2)",
239 rusqlite::params![operation_id, step],
240 )
241 .map_err(DbError::from)?;
242 if candidate_owned.contains(&object) {
243 mark_uploaded_object_on(&tx, remote_object_id(&object))?;
244 }
245 tx.commit().map_err(DbError::from)
246 }
247
248 fn begin_circle_operation_finalization(
249 &mut self,
250 journal: CircleOperationJournal,
251 prepared_objects: PreparedCircleObjects,
252 ) -> Result<(), DbError> {
253 if !matches!(journal.state(), CircleOperationState::Finalizing) {
254 return Err(DbError::Message(
255 "Circle finalization journal is not in finalizing state".to_string(),
256 ));
257 }
258 let remotes = journal
259 .closed_remote_objects(&prepared_objects)
260 .map_err(DbError::from)?;
261 let owner = journal.operation().commit_ref().clone();
262 let row = PreparedCircleOperationRow::from_journal(&journal)?;
263 let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
264 let durable =
265 load_circle_operation_on(&tx, journal.operation_id.as_str())?.ok_or_else(|| {
266 DbError::Message(format!(
267 "Circle operation {} disappeared before finalization",
268 journal.operation_id
269 ))
270 })?;
271 if !matches!(
272 durable.state(),
273 CircleOperationState::WaitingForCloseResponses
274 ) || durable.circle_id != journal.circle_id
275 || durable.intent != journal.intent
276 {
277 return Err(DbError::Message(format!(
278 "Circle operation {} changed before finalization",
279 journal.operation_id
280 )));
281 }
282 claim_circle_publication_on(&tx, &journal)?;
283 persist_circle_operation_objects_on(
284 &tx,
285 self.store_dir,
286 &remotes,
287 &prepared_objects,
288 &owner,
289 "Circle close-finalization candidate graph",
290 )?;
291 claim_operation_payloads_on(&tx, &journal.operation_id, journal.operation())?;
292 tx.execute(
293 "DELETE FROM circle_operation_uploads WHERE operation_id = ?1",
294 [&row.operation_id],
295 )
296 .map_err(DbError::from)?;
297 let updated = tx
298 .execute(
299 "UPDATE circle_operations SET prepared = ?3, phase = ?4
300 WHERE operation_id = ?1 AND circle_id = ?2",
301 rusqlite::params![row.operation_id, row.circle_id, row.prepared, row.phase],
302 )
303 .map_err(DbError::from)?;
304 if updated != 1 {
305 return Err(DbError::Message(
306 "Circle operation disappeared during finalization".to_string(),
307 ));
308 }
309 tx.commit().map_err(DbError::from)
310 }
311
312 #[cfg(any(test, feature = "test-utils"))]
313 fn substitute_circle_operation_for_test(
314 &mut self,
315 journal: CircleOperationJournal,
316 ) -> Result<(), DbError> {
317 let row = PreparedCircleOperationRow::from_journal(&journal)?;
318 let updated = self
319 .conn
320 .execute(
321 "UPDATE circle_operations SET prepared = ?3
322 WHERE operation_id = ?1 AND circle_id = ?2",
323 rusqlite::params![row.operation_id, row.circle_id, row.prepared],
324 )
325 .map_err(DbError::from)?;
326 if updated != 1 {
327 return Err(DbError::Message(format!(
328 "Circle operation {} is absent from its slot",
329 row.operation_id
330 )));
331 }
332 Ok(())
333 }
334
335 fn block_circle_operation(
336 &mut self,
337 operation_id: String,
338 block: coven_protocol::circle::CircleOperationBlock,
339 ) -> Result<(), DbError> {
340 let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
341 let mut journal = load_circle_operation_on(&tx, &operation_id)?.ok_or_else(|| {
342 DbError::Message(format!("circle operation {operation_id} is absent"))
343 })?;
344 journal.block(block).map_err(DbError::from)?;
345 update_circle_operation_phase_on(&tx, &journal)?;
346 tx.commit().map_err(DbError::from)
347 }
348
349 fn unblock_circle_operation(&mut self, operation_id: String) -> Result<(), DbError> {
350 let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
351 let mut journal = load_circle_operation_on(&tx, &operation_id)?.ok_or_else(|| {
352 DbError::Message(format!("circle operation {operation_id} is absent"))
353 })?;
354 journal.unblock().map_err(DbError::from)?;
355 update_circle_operation_phase_on(&tx, &journal)?;
356 tx.commit().map_err(DbError::from)
357 }
358}
359
360impl VerifiedStoreTransaction<'_, '_, '_, '_> {
361 fn activate_circle_operation(
362 &mut self,
363 journal: CircleOperationJournal,
364 verified: VerifiedCircleActivations,
365 accepted_transition: crate::StoreCommitPublicationOutcome,
366 ) -> Result<Option<crate::OwnedVerifiedMergeMaterialization>, DbError> {
367 let authority = &mut *self.authority;
368 let gates = self.gates;
369 let tx = self.store.transaction;
370 journal.validate_identity().map_err(DbError::from)?;
371 let durable =
372 load_circle_operation_on(tx, journal.operation_id.as_str())?.ok_or_else(|| {
373 DbError::Message(format!(
374 "circle operation {} disappeared during activation",
375 journal.operation_id
376 ))
377 })?;
378 if durable != journal {
379 return Err(DbError::Message(format!(
380 "circle operation {} changed before activation",
381 journal.operation_id
382 )));
383 }
384 if !journal.is_publishable() {
385 return Err(DbError::Message(
386 "blocked circle operation cannot activate".to_string(),
387 ));
388 }
389 let operation = journal.operation();
390 operation
391 .store_commit
392 .validate_closed_shape()
393 .map_err(DbError::from)?;
394 let creation = &operation.creation;
395 let resolved_roster = creation.resolved_roster();
396 if !creation.control.verify() || !creation.metadata.verify() || !resolved_roster.verify() {
397 return Err(DbError::Message(
398 "circle operation contains invalid signed objects".to_string(),
399 ));
400 }
401 let unverified_commit = operation.commit();
402 let root = authority.root().clone();
403 let author =
404 super::verified_store_authority::VerifiedRegistrationLookup::activated_registration_on(
405 authority,
406 crate::store::store_session::StoreRecords::new(
407 self.store.transaction,
408 self.store.store_dir,
409 ),
410 &root,
411 &unverified_commit.author_registration,
412 )?;
413 let [activation] = verified.circles() else {
414 return Err(DbError::Message(
415 "local Circle publication must carry one common-verifier result".to_string(),
416 ));
417 };
418 let verify_commit = || {
419 let commit = VerifiedStoreBatchCommit::parse(
420 &unverified_commit.to_bytes(),
421 root.store_root_hash,
422 operation.commit_ref(),
423 &author,
424 )
425 .map_err(|error| DbError::context("verify circle Store commit", error))?;
426 if operation.commit_ref().object.slot().logical_key()
427 != commit_semantic_prefix(
428 commit.candidate_family(),
429 &operation.commit_ref().coord.stream_id.to_string(),
430 commit.seq(),
431 commit.commit_hash(),
432 ) + ".json"
433 {
434 return Err(DbError::Message(
435 "circle commit exact object occupies a different semantic slot".to_string(),
436 ));
437 }
438 let [control_ref] = commit.circle_controls() else {
439 return Err(DbError::Message(
440 "circle creation Store commit is not an exact control-only batch".to_string(),
441 ));
442 };
443 let expected_ref = creation.control_ref(
444 control_ref.objects().clone(),
445 Some(control_ref.head_object().clone()),
446 );
447 if control_ref != &expected_ref
448 || !commit.operations().is_some_and(
449 coven_protocol::store_commit::StoreCommitOperations::is_circle_control_activation_only,
450 )
451 {
452 return Err(DbError::Message(
453 "circle creation Store commit is not an exact control-only batch"
454 .to_string(),
455 ));
456 }
457 if activation.reference != *control_ref
458 || activation.circle_id != creation.circle_id
459 || activation.control != creation.control
460 || verified.stream_activations().activating_commit() != operation.commit_ref()
461 || verified.stream_activations().as_slice() != commit.stream_activations()
462 {
463 return Err(DbError::Message(
464 "common-verifier Circle result differs from the durable signed operation"
465 .to_string(),
466 ));
467 }
468 Ok(commit)
469 };
470 let (commit, activation, retained, materialize) = {
471 let commit = verify_commit()?;
472 let accepted_transition =
473 accepted_transition.resolve_installed_on(self.store, &commit)?;
474 let materialize = accepted_transition.requires_materialization();
475 if materialize {
476 super::clock_floor::observe_circle_metadata(
477 &mut self.clock_floor,
478 verified.circles(),
479 crate::IncomingTimestampPolicy::LocallyAuthored,
480 )?;
481 }
482 let acceptance = accepted_transition.install_on(self.store, &commit)?;
483 let retained = if materialize {
484 let device_operations = VerifiedStoreDeviceOperations::without_exclusions(&commit)
485 .map_err(DbError::from)?;
486 let materialization = VerifiedMergeMaterialization::verify(
487 &root,
488 &commit,
489 &[],
490 &device_operations,
491 &verified,
492 &acceptance,
493 &operation.store_commit.history_evidence,
494 None,
495 &[],
496 None,
497 )?;
498 let retained = MergeMaterializationTransaction::from_store(self.store)
499 .record_verified_merge_materialization(authority, materialization)?;
500 authority.insert_verified(retained.clone())?;
501 Some(retained)
502 } else {
503 None
504 };
505 (commit, activation.clone(), retained, materialize)
506 };
507 let mut object_ids = candidate_graph_exact_objects(&commit)?
508 .iter()
509 .map(remote_object_id)
510 .collect::<Vec<_>>();
511 object_ids.extend(operation.bootstrap_blobs()?.into_keys());
512 object_ids.push(remote_object_id(&operation.commit_ref().object));
513 let store_transaction = MergeMaterializationTransaction::from_store(self.store);
514 store_transaction
515 .activate_store_operation_remote_objects(operation.commit_ref(), &object_ids)?;
516 if materialize {
517 store_transaction.record_verified_circle_activations(&commit, &[activation])?;
518 }
519 let active_owner =
520 ActiveStorePublicationOwner::CircleOperation(journal.operation_id.clone());
521 let active_candidate = operation.commit_ref().clone();
522 if store_transaction.circle_current_state_is_deleted(creation.circle_id)? {
527 crate::prune_ineligible_scoped_rows(
528 tx,
529 gates,
530 &std::collections::BTreeSet::from([creation.circle_id]),
531 )
532 .map_err(DbError::from)?;
533 }
534 if !journal.is_finalizing()
535 && matches!(
536 journal.intent,
537 coven_protocol::circle_journal::CircleOperationIntent::RemoveMember { .. }
538 )
539 {
540 let mut waiting = journal;
541 waiting.wait_for_close_responses().map_err(DbError::from)?;
542 update_circle_operation_phase_on(tx, &waiting)?;
543 } else {
544 release_operation_payloads_on(tx, &journal.operation_id)?;
545 let deleted = tx
546 .execute(
547 "DELETE FROM circle_operations WHERE operation_id = ?1 AND circle_id = ?2",
548 rusqlite::params![
549 journal.operation_id.as_str(),
550 creation.circle_id.to_string()
551 ],
552 )
553 .map_err(DbError::from)?;
554 if deleted != 1 {
555 return Err(DbError::Message(
556 "circle operation disappeared during activation".to_string(),
557 ));
558 }
559 }
560 super::active_store_publication::clear_active_store_commit_for_owner_on(
561 tx,
562 &active_owner,
563 &active_candidate,
564 )?;
565 Ok(retained)
566 }
567}
568
569impl StoreDatabase {
570 pub async fn insert_circle_operation(
571 &self,
572 journal: CircleOperationJournal,
573 prepared_objects: PreparedCircleObjects,
574 ) -> Result<(), DbError> {
575 self.call_store(move |session| session.insert_circle_operation(journal, prepared_objects))
576 .await
577 }
578
579 pub async fn insert_circle_operation_superseding(
585 &self,
586 journal: CircleOperationJournal,
587 superseded: CircleOperationId,
588 prepared_objects: PreparedCircleObjects,
589 ) -> Result<(), DbError> {
590 self.call_store(move |session| {
591 session.insert_circle_operation_superseding(journal, superseded, prepared_objects)
592 })
593 .await
594 }
595
596 pub async fn circle_operation(
597 &self,
598 operation_id: &CircleOperationId,
599 ) -> Result<Option<CircleOperationJournal>, DbError> {
600 let operation_id = operation_id.as_str().to_string();
601 self.call_store(move |session| session.circle_operation(operation_id))
602 .await
603 }
604
605 pub async fn circle_operation_step(
606 &self,
607 operation_id: &CircleOperationId,
608 step: &str,
609 ) -> Result<PreparedExactObject, DbError> {
610 let operation_id = operation_id.as_str().to_string();
611 let step = step.to_string();
612 self.call_store(move |session| session.circle_operation_step(operation_id, step))
613 .await
614 }
615
616 pub async fn oldest_pending_circle_operation(
617 &self,
618 ) -> Result<Option<CircleOperationJournal>, DbError> {
619 self.call_store(|session| session.oldest_pending_circle_operation())
620 .await
621 }
622
623 pub async fn waiting_circle_operations(&self) -> Result<Vec<CircleOperationJournal>, DbError> {
624 self.call_store(|session| session.waiting_circle_operations())
625 .await
626 }
627
628 pub async fn complete_circle_operation_upload_step(
641 &self,
642 operation_id: &CircleOperationId,
643 step: &str,
644 ) -> Result<(), DbError> {
645 let operation_id = operation_id.as_str().to_string();
646 let step = step.to_string();
647 self.call_store(move |session| {
648 session.complete_circle_operation_upload_step(operation_id, step)
649 })
650 .await
651 }
652
653 pub async fn begin_circle_operation_finalization(
661 &self,
662 journal: CircleOperationJournal,
663 prepared_objects: PreparedCircleObjects,
664 ) -> Result<(), DbError> {
665 self.call_store(move |session| {
666 session.begin_circle_operation_finalization(journal, prepared_objects)
667 })
668 .await
669 }
670
671 #[cfg(any(test, feature = "test-utils"))]
679 pub async fn substitute_circle_operation_for_test(
680 &self,
681 journal: CircleOperationJournal,
682 ) -> Result<(), DbError> {
683 self.call_store(move |session| session.substitute_circle_operation_for_test(journal))
684 .await
685 }
686
687 pub async fn block_circle_operation(
688 &self,
689 operation_id: &CircleOperationId,
690 block: coven_protocol::circle::CircleOperationBlock,
691 ) -> Result<(), DbError> {
692 let operation_id = operation_id.as_str().to_string();
693 self.call_store(move |session| session.block_circle_operation(operation_id, block))
694 .await
695 }
696
697 pub async fn unblock_circle_operation(
698 &self,
699 operation_id: &CircleOperationId,
700 ) -> Result<(), DbError> {
701 let operation_id = operation_id.as_str().to_string();
702 self.call_store(move |session| session.unblock_circle_operation(operation_id))
703 .await
704 }
705
706 pub async fn activate_circle_operation(
707 &self,
708 journal: CircleOperationJournal,
709 verified: VerifiedCircleActivations,
710 accepted_transition: crate::StoreCommitPublicationOutcome,
711 ) -> Result<Option<crate::OwnedVerifiedMergeMaterialization>, DbError> {
712 self.call_store(move |session| {
713 session.activate_circle_operation(journal, verified, accepted_transition)
714 })
715 .await
716 }
717}
718
719fn claim_circle_publication_on(
720 conn: &Connection,
721 journal: &CircleOperationJournal,
722) -> Result<(), DbError> {
723 if !journal.is_publishable() {
724 return Err(DbError::Message(format!(
725 "Circle operation {} cannot reserve Store publication from its current state",
726 journal.operation_id
727 )));
728 }
729 let owner = ActiveStorePublicationOwner::CircleOperation(journal.operation_id.clone());
730 let active = ActiveStorePublication::for_commit(owner, &journal.operation().store_commit)?;
731 match super::active_store_publication::claim_active_store_publication_on(conn, &active)? {
732 super::active_store_publication::ActiveStorePublicationClaim::Acquired => Ok(()),
733 super::active_store_publication::ActiveStorePublicationClaim::AlreadyOwned => {
734 Err(DbError::Message(format!(
735 "Circle operation {} already owns Store publication without its journal transition",
736 journal.operation_id
737 )))
738 }
739 super::active_store_publication::ActiveStorePublicationClaim::Occupied(owner) => {
740 Err(DbError::Message(format!(
741 "another local Store operation owns publication: {owner:?}"
742 )))
743 }
744 }
745}
746
747fn insert_circle_operation_row_on(
748 conn: &Connection,
749 row: &PreparedCircleOperationRow,
750) -> Result<(), DbError> {
751 conn.execute(
752 "INSERT INTO circle_operations (operation_id, circle_id, prepared, phase)
753 VALUES (?1, ?2, ?3, ?4)",
754 rusqlite::params![row.operation_id, row.circle_id, row.prepared, row.phase],
755 )
756 .map_err(DbError::from)
757 .map(|_| ())
758}
759
760pub(crate) fn update_circle_operation_phase_on(
763 conn: &Connection,
764 journal: &CircleOperationJournal,
765) -> Result<(), DbError> {
766 let updated = conn
767 .execute(
768 "UPDATE circle_operations SET phase = ?3
769 WHERE operation_id = ?1 AND circle_id = ?2",
770 rusqlite::params![
771 journal.operation_id.as_str(),
772 journal.circle_id.to_string(),
773 crate::circle_operation_phase_json(&journal.progress)?
774 ],
775 )
776 .map_err(DbError::from)?;
777 if updated != 1 {
778 return Err(DbError::Message(format!(
779 "circle operation {} disappeared during publication",
780 journal.operation_id
781 )));
782 }
783 Ok(())
784}
785
786pub(crate) fn claim_operation_payloads_on(
797 conn: &Connection,
798 operation_id: &CircleOperationId,
799 operation: &coven_protocol::circle_journal::PreparedCircleOperation,
800) -> Result<(), DbError> {
801 crate::payload_store::set_payload_owner_claims_on(
802 conn,
803 &crate::payload_store::circle_operation_owner_key(operation_id.as_str()),
804 &operation
805 .prepared_objects
806 .values()
807 .map(coven_protocol::objects::ExactObjectRef::stored_hash)
808 .collect(),
809 )
810}
811
812pub(crate) fn release_operation_payloads_on(
817 conn: &Connection,
818 operation_id: &CircleOperationId,
819) -> Result<(), DbError> {
820 crate::payload_store::release_payload_owner_on(
821 conn,
822 &crate::payload_store::circle_operation_owner_key(operation_id.as_str()),
823 )
824}
825
826fn mark_uploaded_object_on(
832 conn: &Connection,
833 object_id: coven_protocol::store_commit::ObjectHash,
834) -> Result<(), DbError> {
835 let current = load_remote_object_on(conn, object_id)?;
836 let mut uploaded = current.clone();
837 uploaded
838 .mark_uploaded_verified()
839 .map_err(|error| DbError::context(format!("mark {object_id} uploaded"), error))?;
840 if uploaded == current {
841 return Ok(());
842 }
843 update_remote_object_on(conn, object_id, &uploaded)
844}