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