1use super::*;
2use crate::{MakeRemoteIntentState, OutboxIdentity};
3
4#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
5#[serde(rename_all = "snake_case", deny_unknown_fields)]
6pub struct OutboxFailure {
7 pub message: String,
8 pub kind: OutboxFailureKind,
9}
10
11impl OutboxFailure {
12 pub fn other(message: impl Into<String>) -> Self {
13 Self {
14 message: message.into(),
15 kind: OutboxFailureKind::Other,
16 }
17 }
18
19 pub fn source_unavailable(path: std::path::PathBuf, message: impl Into<String>) -> Self {
20 Self {
21 message: message.into(),
22 kind: OutboxFailureKind::SourceUnavailable { path },
23 }
24 }
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
28#[serde(rename_all = "snake_case", deny_unknown_fields)]
29pub enum OutboxFailureKind {
30 Other,
31 SourceUnavailable { path: std::path::PathBuf },
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct OutboxEntry {
36 pub id: i64,
37 pub attempt_count: i64,
38 pub last_attempt_at: Option<String>,
39 pub operation: OutboxOperation,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum OutboxOperation {
44 Upload {
45 root_table: String,
46 root_id: String,
47 row: coven_protocol::blob::RowBlobRef,
48 source_path: std::path::PathBuf,
49 retain_pinned: bool,
50 state: OutboxUploadState,
51 },
52 Delete {
53 stored: coven_protocol::blob::locator::StoredBlobRef,
54 },
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
58#[serde(rename_all = "snake_case", deny_unknown_fields)]
59pub enum OutboxUploadState {
60 Pending,
61 Prepared {
62 authority: coven_protocol::audience_package::PackageAudience,
63 stored: coven_protocol::blob::locator::StoredBlobRef,
64 spool_path: std::path::PathBuf,
65 },
66 Created {
67 authority: coven_protocol::audience_package::PackageAudience,
68 stored: coven_protocol::blob::locator::StoredBlobRef,
69 spool_path: std::path::PathBuf,
70 },
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum MakeRemoteProgress {
78 Uploading,
80 Cancelling,
82 Publishing,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum QueuedUploadPhase {
88 Pending,
89 Prepared,
90 Created,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct QueuedMakeRemote {
95 pub root_table: String,
96 pub root_id: String,
97 pub root_label: String,
102 pub retain_pinned: bool,
103 pub progress: MakeRemoteProgress,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct CloudOutboxSnapshot {
108 pub uploads: Vec<QueuedUpload>,
109 pub deletes: Vec<QueuedDelete>,
110 pub make_remotes: Vec<QueuedMakeRemote>,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct QueuedDelete {
120 pub namespace: String,
122 pub blob_id: String,
124 pub attempt_count: u64,
126 pub last_error: Option<String>,
128 pub created_at: String,
130 pub last_attempt_at: Option<String>,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct QueuedUpload {
144 pub blob: coven_protocol::blob::RowBlobRef,
147 pub root_table: String,
150 pub root_id: String,
151 pub root_label: String,
156 pub retain_pinned: bool,
159 pub phase: QueuedUploadPhase,
163 pub provider_bytes_total: Option<u64>,
166 pub attempt_count: u64,
168 pub last_failure: Option<OutboxFailure>,
170 pub created_at: String,
172 pub last_attempt_at: Option<String>,
174}
175
176#[derive(Clone)]
177pub struct PublishedBlobDropIntent {
178 pub seq: u64,
179 pub drop: coven_protocol::blob::DeferredLocalBlobDrop,
180}
181
182impl StoreSession<'_> {
183 fn queued_upload_rows(
184 &mut self,
185 root: Option<(String, String)>,
186 ) -> Result<Vec<QueuedUpload>, DbError> {
187 const COLUMNS: &str = "SELECT row_ref, root_table, root_id, root_label, retain_pinned,
188 upload_state, attempt_count, last_error, created_at, last_attempt_at
189 FROM cloud_outbox WHERE operation = 'upload'";
190 let (sql, parameters): (String, Vec<String>) = match root {
191 Some((root_table, root_id)) => (
192 format!("{COLUMNS} AND root_table = ?1 AND root_id = ?2 ORDER BY id"),
193 vec![root_table, root_id],
194 ),
195 None => (format!("{COLUMNS} ORDER BY id"), Vec::new()),
196 };
197 let mut statement = self.conn.prepare(&sql).map_err(DbError::from)?;
198 let uploads = statement
199 .query_map(rusqlite::params_from_iter(parameters), row_to_queued_upload)
200 .map_err(DbError::from)?
201 .collect::<Result<Vec<_>, _>>()
202 .map_err(DbError::from)?;
203 Ok(uploads)
204 }
205
206 fn queued_deletes(&mut self) -> Result<Vec<QueuedDelete>, DbError> {
207 let mut statement = self
208 .conn
209 .prepare(
210 "SELECT stored_ref, attempt_count, last_error, created_at, last_attempt_at
211 FROM cloud_outbox WHERE operation = 'delete' ORDER BY id",
212 )
213 .map_err(DbError::from)?;
214 let deletes = statement
215 .query_map([], row_to_queued_delete)
216 .map_err(DbError::from)?
217 .collect::<Result<Vec<_>, _>>()
218 .map_err(DbError::from)?;
219 Ok(deletes)
220 }
221
222 fn queued_make_remotes(&mut self) -> Result<Vec<QueuedMakeRemote>, DbError> {
223 let mut statement = self
224 .conn
225 .prepare(
226 "SELECT root_table, root_id, root_label, retain_pinned, state
227 FROM blob_make_remote_intents ORDER BY root_table, root_id",
228 )
229 .map_err(DbError::from)?;
230 let make_remotes = statement
231 .query_map([], |row| {
232 let state: String = row.get(4)?;
233 let progress = match state.as_str() {
234 "uploading" => MakeRemoteProgress::Uploading,
235 "cancelling" => MakeRemoteProgress::Cancelling,
236 "publishing" => MakeRemoteProgress::Publishing,
237 _ => {
238 return Err(rusqlite::Error::FromSqlConversionFailure(
239 4,
240 rusqlite::types::Type::Text,
241 Box::new(std::io::Error::other(format!(
242 "invalid make_remote state {state:?}"
243 ))),
244 ))
245 }
246 };
247 Ok(QueuedMakeRemote {
248 root_table: row.get(0)?,
249 root_id: row.get(1)?,
250 root_label: row.get(2)?,
251 retain_pinned: row.get(3)?,
252 progress,
253 })
254 })
255 .map_err(DbError::from)?
256 .collect::<Result<Vec<_>, _>>()
257 .map_err(DbError::from)?;
258 Ok(make_remotes)
259 }
260
261 fn cloud_outbox_snapshot(&mut self) -> Result<CloudOutboxSnapshot, DbError> {
262 Ok(CloudOutboxSnapshot {
263 uploads: self.queued_upload_rows(None)?,
264 deletes: self.queued_deletes()?,
265 make_remotes: self.queued_make_remotes()?,
266 })
267 }
268
269 fn pending_outbox(&mut self, operation: &'static str) -> Result<Vec<OutboxEntry>, DbError> {
270 let mut statement = self
271 .conn
272 .prepare(
273 "SELECT id, operation, row_ref, stored_ref, source_path, retain_pinned,
274 upload_state, attempt_count, last_attempt_at, root_table, root_id
275 FROM cloud_outbox WHERE operation = ?1 ORDER BY id",
276 )
277 .map_err(DbError::from)?;
278 let entries = statement
279 .query_map([operation], crate::row_to_outbox_entry)
280 .map_err(DbError::from)?
281 .collect::<Result<Vec<_>, _>>()
282 .map_err(DbError::from)?;
283 Ok(entries)
284 }
285
286 fn remove_blob_delete(&mut self, id: i64, stored: String) -> Result<(), DbError> {
287 let removed = self
288 .conn
289 .execute(
290 "DELETE FROM cloud_outbox
291 WHERE id = ?1 AND operation = 'delete' AND stored_ref = ?2",
292 rusqlite::params![id, stored],
293 )
294 .map_err(DbError::from)?;
295 if removed != 1 {
296 return Err(DbError::Message(
297 "blob delete outbox entry changed before exact dequeue".to_string(),
298 ));
299 }
300 Ok(())
301 }
302
303 fn published_blob_drop_intents(
304 &mut self,
305 max_seq: u64,
306 ) -> Result<Vec<PublishedBlobDropIntent>, DbError> {
307 let mut statement = self
308 .conn
309 .prepare(
310 "SELECT seq, namespace, blob_id, size, plaintext_hash, locator_hash, disposition
311 FROM published_blob_drop_intents
312 WHERE seq <= ?1
313 AND NOT EXISTS (
314 SELECT 1 FROM store_write_blob_leases lease
315 WHERE lease.namespace = published_blob_drop_intents.namespace
316 AND lease.blob_id = published_blob_drop_intents.blob_id
317 )
318 AND NOT EXISTS (
319 SELECT 1 FROM retained_replay_blob_leases baseline
320 WHERE baseline.namespace = published_blob_drop_intents.namespace
321 AND baseline.blob_id = published_blob_drop_intents.blob_id
322 )
323 ORDER BY seq, namespace, blob_id, locator_hash",
324 )
325 .map_err(DbError::from)?;
326 let intents = statement
327 .query_map([max_seq as i64], row_to_published_blob_drop_intent)
328 .map_err(DbError::from)?
329 .collect::<Result<Vec<_>, _>>()
330 .map_err(DbError::from)?;
331 Ok(intents)
332 }
333
334 fn clear_published_blob_drop_intent(
335 &mut self,
336 seq: u64,
337 namespace: String,
338 id: String,
339 locator_hash: String,
340 ) -> Result<(), DbError> {
341 self.conn
342 .execute(
343 "DELETE FROM published_blob_drop_intents
344 WHERE seq = ?1 AND namespace = ?2 AND blob_id = ?3 AND locator_hash = ?4",
345 rusqlite::params![seq as i64, namespace, id, locator_hash],
346 )
347 .map(|_| ())
348 .map_err(DbError::from)
349 }
350
351 fn record_outbox_failure(
352 &mut self,
353 entry: OutboxEntry,
354 failure: OutboxFailure,
355 attempted_at: String,
356 ) -> Result<(), DbError> {
357 let identity = crate::outbox_identity(&entry.operation)?;
358 let encoded = serde_json::to_string(&failure)
359 .map_err(|error| DbError::context("serialize outbox failure", error))?;
360 let updated = match identity {
361 OutboxIdentity::Upload {
362 table,
363 row_id,
364 column,
365 row_stamp,
366 } => self.conn.execute(
367 "UPDATE cloud_outbox SET attempt_count = attempt_count + 1,
368 last_error = ?1, last_attempt_at = ?2
369 WHERE id = ?3 AND operation = 'upload' AND table_name = ?4
370 AND row_id = ?5 AND column_name = ?6 AND row_stamp = ?7",
371 rusqlite::params![
372 encoded,
373 attempted_at,
374 entry.id,
375 table,
376 row_id,
377 column,
378 row_stamp
379 ],
380 ),
381 OutboxIdentity::Stored { operation, stored } => self.conn.execute(
382 "UPDATE cloud_outbox SET attempt_count = attempt_count + 1,
383 last_error = ?1, last_attempt_at = ?2
384 WHERE id = ?3 AND operation = ?4 AND stored_ref = ?5",
385 rusqlite::params![encoded, attempted_at, entry.id, operation, stored],
386 ),
387 }
388 .map_err(DbError::from)?;
389 if updated != 1 {
390 return Err(DbError::Message(
391 "cloud outbox entry changed before failure recording".to_string(),
392 ));
393 }
394 Ok(())
395 }
396
397 #[allow(clippy::too_many_arguments)]
398 fn swap_blob_upload_state(
399 &mut self,
400 id: i64,
401 table: String,
402 row_id: String,
403 column: String,
404 row_stamp: String,
405 from: String,
406 to: String,
407 context: &'static str,
408 ) -> Result<(), DbError> {
409 let updated = self
410 .conn
411 .execute(
412 "UPDATE cloud_outbox SET upload_state = ?1, last_error = NULL
413 WHERE id = ?2 AND operation = 'upload' AND table_name = ?3
414 AND row_id = ?4 AND column_name = ?5 AND row_stamp = ?6
415 AND upload_state = ?7",
416 rusqlite::params![to, id, table, row_id, column, row_stamp, from],
417 )
418 .map_err(DbError::from)?;
419 if updated != 1 {
420 return Err(DbError::Message(format!(
421 "upload outbox entry changed before {context}"
422 )));
423 }
424 Ok(())
425 }
426
427 fn reset_outbox_backoff(&mut self) -> Result<(), DbError> {
428 self.conn
429 .execute(
430 "UPDATE cloud_outbox SET last_attempt_at = NULL WHERE attempt_count > 0",
431 [],
432 )
433 .map(|_| ())
434 .map_err(DbError::from)
435 }
436
437 fn make_remote_intent_state(
438 &mut self,
439 root_table: String,
440 root_id: String,
441 ) -> Result<Option<MakeRemoteIntentState>, DbError> {
442 Database::make_remote_intent_state(self.conn, &root_table, &root_id)
443 }
444
445 fn finish_cancelled_blob_upload(&mut self, entry: OutboxEntry) -> Result<bool, DbError> {
446 let transaction = self.conn.unchecked_transaction().map_err(DbError::from)?;
447 let finished =
448 crate::CloudOutboxRecords::new(&transaction).finish_cancelled_upload(&entry)?;
449 transaction.commit().map_err(DbError::from)?;
450 Ok(finished)
451 }
452}
453
454pub(super) fn take_leased_published_blob_drop_intents_for_restoration_on(
455 conn: &rusqlite::Connection,
456 blobs: &[coven_protocol::blob::BlobRef],
457) -> Result<Vec<PublishedBlobDropIntent>, DbError> {
458 let blobs = blobs
459 .iter()
460 .map(|blob| (blob.namespace.as_str(), blob.id.as_str()))
461 .collect::<std::collections::BTreeSet<_>>();
462 let mut taken = Vec::new();
463 for (namespace, blob_id) in blobs {
464 let intents = {
465 let mut statement = conn
466 .prepare(
467 "SELECT seq, namespace, blob_id, size, plaintext_hash, locator_hash, disposition
468 FROM published_blob_drop_intents
469 WHERE namespace = ?1 AND blob_id = ?2
470 AND (
471 EXISTS (
472 SELECT 1 FROM store_write_blob_leases
473 WHERE namespace = ?1 AND blob_id = ?2
474 ) OR EXISTS (
475 SELECT 1 FROM retained_replay_blob_leases
476 WHERE namespace = ?1 AND blob_id = ?2
477 )
478 )
479 ORDER BY seq, locator_hash",
480 )
481 .map_err(DbError::from)?;
482 let intents = statement
483 .query_map((namespace, blob_id), row_to_published_blob_drop_intent)
484 .map_err(DbError::from)?
485 .collect::<Result<Vec<_>, _>>()
486 .map_err(DbError::from)?;
487 intents
488 };
489 for intent in intents {
490 let removed = crate::with_coven_sql_authority(|| {
491 conn.execute(
492 "DELETE FROM published_blob_drop_intents
493 WHERE seq = ?1 AND namespace = ?2 AND blob_id = ?3 AND locator_hash = ?4",
494 rusqlite::params![
495 i64::try_from(intent.seq).map_err(|_| DbError::Message(format!(
496 "published blob drop sequence {} exceeds SQLite integer range",
497 intent.seq
498 )))?,
499 intent.drop.namespace,
500 intent.drop.id,
501 intent.drop.locator_hash.to_string(),
502 ],
503 )
504 .map_err(DbError::from)
505 })?;
506 if removed != 1 {
507 return Err(DbError::Message(format!(
508 "published blob drop intent changed while restoring {namespace}/{blob_id}"
509 )));
510 }
511 taken.push(intent);
512 }
513 }
514 Ok(taken)
515}
516
517pub(super) fn reinsert_published_blob_drop_intent_on(
518 conn: &rusqlite::Connection,
519 intent: &PublishedBlobDropIntent,
520) -> Result<(), DbError> {
521 let inserted = crate::with_coven_sql_authority(|| {
522 conn.execute(
523 "INSERT INTO published_blob_drop_intents
524 (seq, namespace, blob_id, size, plaintext_hash, locator_hash, disposition)
525 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
526 rusqlite::params![
527 i64::try_from(intent.seq).map_err(|_| DbError::Message(format!(
528 "published blob drop sequence {} exceeds SQLite integer range",
529 intent.seq
530 )))?,
531 intent.drop.namespace,
532 intent.drop.id,
533 i64::try_from(intent.drop.size).map_err(|_| DbError::Message(format!(
534 "published blob drop size {} exceeds SQLite integer range",
535 intent.drop.size
536 )))?,
537 intent.drop.plaintext_hash.to_string(),
538 intent.drop.locator_hash.to_string(),
539 intent.drop.disposition.as_db(),
540 ],
541 )
542 .map_err(DbError::from)
543 })?;
544 if inserted != 1 {
545 return Err(DbError::Message(
546 "published blob drop intent was not restored".to_string(),
547 ));
548 }
549 Ok(())
550}
551
552impl StoreDatabase {
553 #[doc(hidden)]
554 pub async fn cloud_outbox_snapshot(&self) -> Result<CloudOutboxSnapshot, DbError> {
555 self.call_store(|session| session.cloud_outbox_snapshot())
556 .await
557 }
558
559 #[doc(hidden)]
560 pub async fn queued_uploads(&self) -> Result<Vec<QueuedUpload>, DbError> {
561 self.queued_upload_rows(None).await
562 }
563
564 #[doc(hidden)]
565 pub async fn queued_uploads_for_root(
566 &self,
567 root_table: &str,
568 root_id: &str,
569 ) -> Result<Vec<QueuedUpload>, DbError> {
570 self.queued_upload_rows(Some((root_table.to_string(), root_id.to_string())))
571 .await
572 }
573
574 async fn queued_upload_rows(
575 &self,
576 root: Option<(String, String)>,
577 ) -> Result<Vec<QueuedUpload>, DbError> {
578 self.call_store(move |session| session.queued_upload_rows(root))
579 .await
580 }
581
582 #[doc(hidden)]
583 pub async fn queued_deletes(&self) -> Result<Vec<QueuedDelete>, DbError> {
584 self.call_store(|session| session.queued_deletes()).await
585 }
586
587 pub async fn pending_blob_deletes(&self) -> Result<Vec<OutboxEntry>, DbError> {
588 self.pending_outbox("delete").await
589 }
590
591 async fn pending_outbox(&self, operation: &'static str) -> Result<Vec<OutboxEntry>, DbError> {
592 self.call_store(move |session| session.pending_outbox(operation))
593 .await
594 }
595
596 pub async fn remove_blob_delete(&self, entry: &OutboxEntry) -> Result<(), DbError> {
597 let OutboxOperation::Delete { stored } = &entry.operation else {
598 return Err(DbError::Message(
599 "blob delete dequeue requires a delete outbox entry".to_string(),
600 ));
601 };
602 let id = entry.id;
603 let stored = serde_json::to_string(stored)
604 .map_err(|error| DbError::context("serialize stored blob ref", error))?;
605 self.call_store(move |session| session.remove_blob_delete(id, stored))
606 .await
607 }
608
609 pub async fn published_blob_drop_intents(
610 &self,
611 max_seq: u64,
612 ) -> Result<Vec<PublishedBlobDropIntent>, DbError> {
613 self.call_store(move |session| session.published_blob_drop_intents(max_seq))
614 .await
615 }
616
617 pub async fn clear_published_blob_drop_intent(
618 &self,
619 intent: &PublishedBlobDropIntent,
620 ) -> Result<(), DbError> {
621 let seq = intent.seq;
622 let namespace = intent.drop.namespace.clone();
623 let id = intent.drop.id.clone();
624 let locator_hash = intent.drop.locator_hash.to_string();
625 self.call_store(move |session| {
626 session.clear_published_blob_drop_intent(seq, namespace, id, locator_hash)
627 })
628 .await
629 }
630
631 pub async fn pending_blob_uploads(&self) -> Result<Vec<OutboxEntry>, DbError> {
632 self.pending_outbox("upload").await
633 }
634
635 pub async fn mark_blob_upload_prepared(
636 &self,
637 entry: &OutboxEntry,
638 authority: coven_protocol::audience_package::PackageAudience,
639 stored: coven_protocol::blob::locator::StoredBlobRef,
640 spool_path: std::path::PathBuf,
641 ) -> Result<(), DbError> {
642 let OutboxOperation::Upload { row, state, .. } = &entry.operation else {
643 return Err(DbError::Message(
644 "only an upload outbox entry can own a prepared blob".to_string(),
645 ));
646 };
647 if state != &OutboxUploadState::Pending {
648 return Err(DbError::Message(
649 "blob upload is already prepared".to_string(),
650 ));
651 }
652 let locator = stored.locator();
653 if !coven_protocol::blob::locator_describes_row(
654 locator,
655 row.blob(),
656 row.plaintext_size(),
657 row.plaintext_hash(),
658 ) {
659 return Err(DbError::Message(
660 "prepared blob differs from its exact Local row version".to_string(),
661 ));
662 }
663 if locator.audience() != authority.remote_audience() {
664 return Err(DbError::Message(
665 "prepared blob audience differs from its package authority".to_string(),
666 ));
667 }
668 let prepared = OutboxUploadState::Prepared {
669 authority,
670 stored,
671 spool_path,
672 };
673 let prepared_json = serde_json::to_string(&prepared)
674 .map_err(|error| DbError::context("serialize prepared blob upload", error))?;
675 let pending_json = serde_json::to_string(&OutboxUploadState::Pending)
676 .map_err(|error| DbError::context("serialize pending blob upload", error))?;
677 self.swap_blob_upload_state(
678 entry.id,
679 row,
680 pending_json,
681 prepared_json,
682 "prepared-object handoff",
683 )
684 .await
685 }
686
687 pub async fn mark_blob_upload_created(&self, entry: &OutboxEntry) -> Result<(), DbError> {
688 let OutboxOperation::Upload { row, state, .. } = &entry.operation else {
689 return Err(DbError::Message(
690 "only a prepared upload outbox entry can record cloud creation".to_string(),
691 ));
692 };
693 let OutboxUploadState::Prepared {
694 authority,
695 stored,
696 spool_path,
697 } = state
698 else {
699 return Err(DbError::Message(
700 "cloud creation requires a prepared upload object".to_string(),
701 ));
702 };
703 let created_json = serde_json::to_string(&OutboxUploadState::Created {
704 authority: authority.clone(),
705 stored: stored.clone(),
706 spool_path: spool_path.clone(),
707 })
708 .map_err(|error| DbError::context("serialize created blob upload", error))?;
709 let prepared_json = serde_json::to_string(state)
710 .map_err(|error| DbError::context("serialize prepared blob upload identity", error))?;
711 self.swap_blob_upload_state(
712 entry.id,
713 row,
714 prepared_json,
715 created_json,
716 "cloud-created handoff",
717 )
718 .await
719 }
720
721 pub async fn record_outbox_failure(
722 &self,
723 entry: &OutboxEntry,
724 failure: OutboxFailure,
725 attempted_at: &str,
726 ) -> Result<(), DbError> {
727 let entry = entry.clone();
728 let attempted_at = attempted_at.to_string();
729 self.call_store(move |session| session.record_outbox_failure(entry, failure, attempted_at))
730 .await
731 }
732
733 async fn swap_blob_upload_state(
734 &self,
735 id: i64,
736 row: &coven_protocol::blob::RowBlobRef,
737 from: String,
738 to: String,
739 context: &'static str,
740 ) -> Result<(), DbError> {
741 let table = row.table().to_string();
742 let row_id = row.row_id().to_string();
743 let column = row.column().to_string();
744 let row_stamp = row.row_stamp().to_string();
745 self.call_store(move |session| {
746 session.swap_blob_upload_state(id, table, row_id, column, row_stamp, from, to, context)
747 })
748 .await
749 }
750
751 pub async fn reset_outbox_backoff(&self) -> Result<(), DbError> {
752 self.call_store(|session| session.reset_outbox_backoff())
753 .await
754 }
755
756 pub async fn make_remote_intent_state(
757 &self,
758 root_table: &str,
759 root_id: &str,
760 ) -> Result<Option<MakeRemoteIntentState>, DbError> {
761 let root_table = root_table.to_string();
762 let root_id = root_id.to_string();
763 self.call_store(move |session| session.make_remote_intent_state(root_table, root_id))
764 .await
765 }
766
767 pub async fn make_remote_progress(
768 &self,
769 root_table: &str,
770 root_id: &str,
771 ) -> Result<Option<crate::MakeRemoteProgress>, DbError> {
772 Ok(self
773 .make_remote_intent_state(root_table, root_id)
774 .await?
775 .map(|state| match state {
776 MakeRemoteIntentState::Uploading => MakeRemoteProgress::Uploading,
777 MakeRemoteIntentState::Cancelling => MakeRemoteProgress::Cancelling,
778 MakeRemoteIntentState::Publishing(_) => MakeRemoteProgress::Publishing,
779 }))
780 }
781
782 pub async fn finish_cancelled_blob_upload(&self, entry: &OutboxEntry) -> Result<bool, DbError> {
783 let entry = entry.clone();
784 self.call_store(move |session| session.finish_cancelled_blob_upload(entry))
785 .await
786 }
787}
788
789fn row_to_queued_upload(row: &rusqlite::Row<'_>) -> rusqlite::Result<QueuedUpload> {
790 let invalid = |index: usize, source: Box<dyn std::error::Error + Send + Sync>| {
791 rusqlite::Error::FromSqlConversionFailure(index, rusqlite::types::Type::Text, source)
792 };
793 let encoded: String = row.get(0)?;
794 let reference: coven_protocol::blob::RowBlobRef =
795 serde_json::from_str(&encoded).map_err(|error| invalid(0, Box::new(error)))?;
796 let state_json: String = row.get(5)?;
797 let state: OutboxUploadState =
798 serde_json::from_str(&state_json).map_err(|error| invalid(5, Box::new(error)))?;
799 let (phase, provider_bytes_total) = match &state {
800 OutboxUploadState::Pending => (QueuedUploadPhase::Pending, None),
801 OutboxUploadState::Prepared { stored, .. } => (
802 QueuedUploadPhase::Prepared,
803 Some(stored.object().stored_size()),
804 ),
805 OutboxUploadState::Created { stored, .. } => (
806 QueuedUploadPhase::Created,
807 Some(stored.object().stored_size()),
808 ),
809 };
810 let attempt_count: i64 = row.get(6)?;
811 let last_failure = row
812 .get::<_, Option<String>>(7)?
813 .map(|encoded| serde_json::from_str(&encoded).map_err(|error| invalid(7, Box::new(error))))
814 .transpose()?;
815 Ok(QueuedUpload {
816 blob: reference,
817 root_table: row.get(1)?,
818 root_id: row.get(2)?,
819 root_label: row.get(3)?,
820 retain_pinned: row.get(4)?,
821 phase,
822 provider_bytes_total,
823 attempt_count: u64::try_from(attempt_count).map_err(|error| invalid(6, Box::new(error)))?,
824 last_failure,
825 created_at: row.get(8)?,
826 last_attempt_at: row.get(9)?,
827 })
828}
829
830fn row_to_published_blob_drop_intent(
831 row: &rusqlite::Row<'_>,
832) -> rusqlite::Result<PublishedBlobDropIntent> {
833 let size: Option<i64> = row.get(3)?;
834 let size = size.ok_or_else(|| {
835 rusqlite::Error::FromSqlConversionFailure(
836 3,
837 rusqlite::types::Type::Integer,
838 Box::new(std::io::Error::new(
839 std::io::ErrorKind::InvalidData,
840 "published blob drop intent is missing size",
841 )),
842 )
843 })?;
844 if size < 0 {
845 return Err(rusqlite::Error::FromSqlConversionFailure(
846 3,
847 rusqlite::types::Type::Integer,
848 Box::new(std::io::Error::new(
849 std::io::ErrorKind::InvalidData,
850 format!("published blob drop intent has negative size {size}"),
851 )),
852 ));
853 }
854 let plaintext_hash = row.get::<_, String>(4)?.parse().map_err(|error| {
855 rusqlite::Error::FromSqlConversionFailure(4, rusqlite::types::Type::Text, Box::new(error))
856 })?;
857 let locator_hash = row.get::<_, String>(5)?.parse().map_err(|error| {
858 rusqlite::Error::FromSqlConversionFailure(5, rusqlite::types::Type::Text, Box::new(error))
859 })?;
860 let disposition_raw: String = row.get(6)?;
861 let disposition = coven_protocol::blob::DeferredLocalBlobDisposition::from_db(&disposition_raw)
862 .map_err(|message| {
863 rusqlite::Error::FromSqlConversionFailure(
864 6,
865 rusqlite::types::Type::Text,
866 Box::new(std::io::Error::new(
867 std::io::ErrorKind::InvalidData,
868 message,
869 )),
870 )
871 })?;
872 Ok(PublishedBlobDropIntent {
873 seq: row.get::<_, i64>(0)? as u64,
874 drop: coven_protocol::blob::DeferredLocalBlobDrop {
875 namespace: row.get(1)?,
876 id: row.get(2)?,
877 size: size as u64,
878 plaintext_hash,
879 locator_hash,
880 disposition,
881 },
882 })
883}
884
885fn row_to_queued_delete(row: &rusqlite::Row<'_>) -> rusqlite::Result<QueuedDelete> {
886 let invalid = |index: usize, source: Box<dyn std::error::Error + Send + Sync>| {
887 rusqlite::Error::FromSqlConversionFailure(index, rusqlite::types::Type::Text, source)
888 };
889 let encoded: String = row.get(0)?;
890 let stored: coven_protocol::blob::locator::StoredBlobRef =
891 serde_json::from_str(&encoded).map_err(|error| invalid(0, Box::new(error)))?;
892 let attempt_count: i64 = row.get(1)?;
893 let last_error = row
894 .get::<_, Option<String>>(2)?
895 .map(|encoded| {
896 serde_json::from_str::<OutboxFailure>(&encoded)
897 .map(|failure| failure.message)
898 .map_err(|error| invalid(2, Box::new(error)))
899 })
900 .transpose()?;
901 Ok(QueuedDelete {
902 namespace: stored.locator().namespace().to_string(),
903 blob_id: stored.locator().blob_id().to_string(),
904 attempt_count: u64::try_from(attempt_count).map_err(|error| invalid(1, Box::new(error)))?,
905 last_error,
906 created_at: row.get(3)?,
907 last_attempt_at: row.get(4)?,
908 })
909}