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_published_blob_drop_intents_for_restoration_on(
455 conn: &rusqlite::Connection,
456 blobs: &[coven_protocol::blob::BlobRef],
457 can_restore: impl Fn(&PublishedBlobDropIntent, bool) -> Result<bool, DbError>,
458) -> Result<Vec<PublishedBlobDropIntent>, DbError> {
459 let blobs = blobs
460 .iter()
461 .map(|blob| (blob.namespace.as_str(), blob.id.as_str()))
462 .collect::<std::collections::BTreeSet<_>>();
463 let mut taken = Vec::new();
464 for (namespace, blob_id) in blobs {
465 let intents = {
466 let mut statement = conn
467 .prepare(
468 "SELECT seq, namespace, blob_id, size, plaintext_hash, locator_hash, disposition,
469 (
470 EXISTS (
471 SELECT 1 FROM store_write_blob_leases
472 WHERE namespace = ?1 AND blob_id = ?2
473 ) OR EXISTS (
474 SELECT 1 FROM retained_replay_blob_leases
475 WHERE namespace = ?1 AND blob_id = ?2
476 )
477 )
478 FROM published_blob_drop_intents
479 WHERE namespace = ?1 AND blob_id = ?2
480 ORDER BY seq, locator_hash",
481 )
482 .map_err(DbError::from)?;
483 let intents = statement
484 .query_map((namespace, blob_id), |row| {
485 Ok((
486 row_to_published_blob_drop_intent(row)?,
487 row.get::<_, bool>(7)?,
488 ))
489 })
490 .map_err(DbError::from)?
491 .collect::<Result<Vec<_>, _>>()
492 .map_err(DbError::from)?;
493 intents
494 };
495 for (intent, leased) in intents {
496 if !can_restore(&intent, leased)? {
497 continue;
498 }
499 let removed = crate::with_coven_sql_authority(|| {
500 conn.execute(
501 "DELETE FROM published_blob_drop_intents
502 WHERE seq = ?1 AND namespace = ?2 AND blob_id = ?3 AND locator_hash = ?4",
503 rusqlite::params![
504 i64::try_from(intent.seq).map_err(|_| DbError::Message(format!(
505 "published blob drop sequence {} exceeds SQLite integer range",
506 intent.seq
507 )))?,
508 intent.drop.namespace,
509 intent.drop.id,
510 intent.drop.locator_hash.to_string(),
511 ],
512 )
513 .map_err(DbError::from)
514 })?;
515 if removed != 1 {
516 return Err(DbError::Message(format!(
517 "published blob drop intent changed while restoring {namespace}/{blob_id}"
518 )));
519 }
520 taken.push(intent);
521 }
522 }
523 Ok(taken)
524}
525
526pub(super) fn reinsert_published_blob_drop_intent_on(
527 conn: &rusqlite::Connection,
528 intent: &PublishedBlobDropIntent,
529) -> Result<(), DbError> {
530 let inserted = crate::with_coven_sql_authority(|| {
531 conn.execute(
532 "INSERT INTO published_blob_drop_intents
533 (seq, namespace, blob_id, size, plaintext_hash, locator_hash, disposition)
534 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
535 rusqlite::params![
536 i64::try_from(intent.seq).map_err(|_| DbError::Message(format!(
537 "published blob drop sequence {} exceeds SQLite integer range",
538 intent.seq
539 )))?,
540 intent.drop.namespace,
541 intent.drop.id,
542 i64::try_from(intent.drop.size).map_err(|_| DbError::Message(format!(
543 "published blob drop size {} exceeds SQLite integer range",
544 intent.drop.size
545 )))?,
546 intent.drop.plaintext_hash.to_string(),
547 intent.drop.locator_hash.to_string(),
548 intent.drop.disposition.as_db(),
549 ],
550 )
551 .map_err(DbError::from)
552 })?;
553 if inserted != 1 {
554 return Err(DbError::Message(
555 "published blob drop intent was not restored".to_string(),
556 ));
557 }
558 Ok(())
559}
560
561impl StoreDatabase {
562 #[doc(hidden)]
563 pub async fn cloud_outbox_snapshot(&self) -> Result<CloudOutboxSnapshot, DbError> {
564 self.call_store(|session| session.cloud_outbox_snapshot())
565 .await
566 }
567
568 #[doc(hidden)]
569 pub async fn queued_uploads(&self) -> Result<Vec<QueuedUpload>, DbError> {
570 self.queued_upload_rows(None).await
571 }
572
573 #[doc(hidden)]
574 pub async fn queued_uploads_for_root(
575 &self,
576 root_table: &str,
577 root_id: &str,
578 ) -> Result<Vec<QueuedUpload>, DbError> {
579 self.queued_upload_rows(Some((root_table.to_string(), root_id.to_string())))
580 .await
581 }
582
583 async fn queued_upload_rows(
584 &self,
585 root: Option<(String, String)>,
586 ) -> Result<Vec<QueuedUpload>, DbError> {
587 self.call_store(move |session| session.queued_upload_rows(root))
588 .await
589 }
590
591 #[doc(hidden)]
592 pub async fn queued_deletes(&self) -> Result<Vec<QueuedDelete>, DbError> {
593 self.call_store(|session| session.queued_deletes()).await
594 }
595
596 pub async fn pending_blob_deletes(&self) -> Result<Vec<OutboxEntry>, DbError> {
597 self.pending_outbox("delete").await
598 }
599
600 async fn pending_outbox(&self, operation: &'static str) -> Result<Vec<OutboxEntry>, DbError> {
601 self.call_store(move |session| session.pending_outbox(operation))
602 .await
603 }
604
605 pub async fn remove_blob_delete(&self, entry: &OutboxEntry) -> Result<(), DbError> {
606 let OutboxOperation::Delete { stored } = &entry.operation else {
607 return Err(DbError::Message(
608 "blob delete dequeue requires a delete outbox entry".to_string(),
609 ));
610 };
611 let id = entry.id;
612 let stored = serde_json::to_string(stored)
613 .map_err(|error| DbError::context("serialize stored blob ref", error))?;
614 self.call_store(move |session| session.remove_blob_delete(id, stored))
615 .await
616 }
617
618 pub async fn published_blob_drop_intents(
619 &self,
620 max_seq: u64,
621 ) -> Result<Vec<PublishedBlobDropIntent>, DbError> {
622 self.call_store(move |session| session.published_blob_drop_intents(max_seq))
623 .await
624 }
625
626 pub async fn clear_published_blob_drop_intent(
627 &self,
628 intent: &PublishedBlobDropIntent,
629 ) -> Result<(), DbError> {
630 let seq = intent.seq;
631 let namespace = intent.drop.namespace.clone();
632 let id = intent.drop.id.clone();
633 let locator_hash = intent.drop.locator_hash.to_string();
634 self.call_store(move |session| {
635 session.clear_published_blob_drop_intent(seq, namespace, id, locator_hash)
636 })
637 .await
638 }
639
640 pub async fn pending_blob_uploads(&self) -> Result<Vec<OutboxEntry>, DbError> {
641 self.pending_outbox("upload").await
642 }
643
644 pub async fn mark_blob_upload_prepared(
645 &self,
646 entry: &OutboxEntry,
647 authority: coven_protocol::audience_package::PackageAudience,
648 stored: coven_protocol::blob::locator::StoredBlobRef,
649 spool_path: std::path::PathBuf,
650 ) -> Result<(), DbError> {
651 let OutboxOperation::Upload { row, state, .. } = &entry.operation else {
652 return Err(DbError::Message(
653 "only an upload outbox entry can own a prepared blob".to_string(),
654 ));
655 };
656 if state != &OutboxUploadState::Pending {
657 return Err(DbError::Message(
658 "blob upload is already prepared".to_string(),
659 ));
660 }
661 let locator = stored.locator();
662 if !coven_protocol::blob::locator_describes_row(
663 locator,
664 row.blob(),
665 row.plaintext_size(),
666 row.plaintext_hash(),
667 ) {
668 return Err(DbError::Message(
669 "prepared blob differs from its exact Local row version".to_string(),
670 ));
671 }
672 if locator.audience() != authority.remote_audience() {
673 return Err(DbError::Message(
674 "prepared blob audience differs from its package authority".to_string(),
675 ));
676 }
677 let prepared = OutboxUploadState::Prepared {
678 authority,
679 stored,
680 spool_path,
681 };
682 let prepared_json = serde_json::to_string(&prepared)
683 .map_err(|error| DbError::context("serialize prepared blob upload", error))?;
684 let pending_json = serde_json::to_string(&OutboxUploadState::Pending)
685 .map_err(|error| DbError::context("serialize pending blob upload", error))?;
686 self.swap_blob_upload_state(
687 entry.id,
688 row,
689 pending_json,
690 prepared_json,
691 "prepared-object handoff",
692 )
693 .await
694 }
695
696 pub async fn mark_blob_upload_created(&self, entry: &OutboxEntry) -> Result<(), DbError> {
697 let OutboxOperation::Upload { row, state, .. } = &entry.operation else {
698 return Err(DbError::Message(
699 "only a prepared upload outbox entry can record cloud creation".to_string(),
700 ));
701 };
702 let OutboxUploadState::Prepared {
703 authority,
704 stored,
705 spool_path,
706 } = state
707 else {
708 return Err(DbError::Message(
709 "cloud creation requires a prepared upload object".to_string(),
710 ));
711 };
712 let created_json = serde_json::to_string(&OutboxUploadState::Created {
713 authority: authority.clone(),
714 stored: stored.clone(),
715 spool_path: spool_path.clone(),
716 })
717 .map_err(|error| DbError::context("serialize created blob upload", error))?;
718 let prepared_json = serde_json::to_string(state)
719 .map_err(|error| DbError::context("serialize prepared blob upload identity", error))?;
720 self.swap_blob_upload_state(
721 entry.id,
722 row,
723 prepared_json,
724 created_json,
725 "cloud-created handoff",
726 )
727 .await
728 }
729
730 pub async fn record_outbox_failure(
731 &self,
732 entry: &OutboxEntry,
733 failure: OutboxFailure,
734 attempted_at: &str,
735 ) -> Result<(), DbError> {
736 let entry = entry.clone();
737 let attempted_at = attempted_at.to_string();
738 self.call_store(move |session| session.record_outbox_failure(entry, failure, attempted_at))
739 .await
740 }
741
742 async fn swap_blob_upload_state(
743 &self,
744 id: i64,
745 row: &coven_protocol::blob::RowBlobRef,
746 from: String,
747 to: String,
748 context: &'static str,
749 ) -> Result<(), DbError> {
750 let table = row.table().to_string();
751 let row_id = row.row_id().to_string();
752 let column = row.column().to_string();
753 let row_stamp = row.row_stamp().to_string();
754 self.call_store(move |session| {
755 session.swap_blob_upload_state(id, table, row_id, column, row_stamp, from, to, context)
756 })
757 .await
758 }
759
760 pub async fn reset_outbox_backoff(&self) -> Result<(), DbError> {
761 self.call_store(|session| session.reset_outbox_backoff())
762 .await
763 }
764
765 pub async fn make_remote_intent_state(
766 &self,
767 root_table: &str,
768 root_id: &str,
769 ) -> Result<Option<MakeRemoteIntentState>, DbError> {
770 let root_table = root_table.to_string();
771 let root_id = root_id.to_string();
772 self.call_store(move |session| session.make_remote_intent_state(root_table, root_id))
773 .await
774 }
775
776 pub async fn make_remote_progress(
777 &self,
778 root_table: &str,
779 root_id: &str,
780 ) -> Result<Option<crate::MakeRemoteProgress>, DbError> {
781 Ok(self
782 .make_remote_intent_state(root_table, root_id)
783 .await?
784 .map(|state| match state {
785 MakeRemoteIntentState::Uploading => MakeRemoteProgress::Uploading,
786 MakeRemoteIntentState::Cancelling => MakeRemoteProgress::Cancelling,
787 MakeRemoteIntentState::Publishing(_) => MakeRemoteProgress::Publishing,
788 }))
789 }
790
791 pub async fn finish_cancelled_blob_upload(&self, entry: &OutboxEntry) -> Result<bool, DbError> {
792 let entry = entry.clone();
793 self.call_store(move |session| session.finish_cancelled_blob_upload(entry))
794 .await
795 }
796}
797
798fn row_to_queued_upload(row: &rusqlite::Row<'_>) -> rusqlite::Result<QueuedUpload> {
799 let invalid = |index: usize, source: Box<dyn std::error::Error + Send + Sync>| {
800 rusqlite::Error::FromSqlConversionFailure(index, rusqlite::types::Type::Text, source)
801 };
802 let encoded: String = row.get(0)?;
803 let reference: coven_protocol::blob::RowBlobRef =
804 serde_json::from_str(&encoded).map_err(|error| invalid(0, Box::new(error)))?;
805 let state_json: String = row.get(5)?;
806 let state: OutboxUploadState =
807 serde_json::from_str(&state_json).map_err(|error| invalid(5, Box::new(error)))?;
808 let (phase, provider_bytes_total) = match &state {
809 OutboxUploadState::Pending => (QueuedUploadPhase::Pending, None),
810 OutboxUploadState::Prepared { stored, .. } => (
811 QueuedUploadPhase::Prepared,
812 Some(stored.object().stored_size()),
813 ),
814 OutboxUploadState::Created { stored, .. } => (
815 QueuedUploadPhase::Created,
816 Some(stored.object().stored_size()),
817 ),
818 };
819 let attempt_count: i64 = row.get(6)?;
820 let last_failure = row
821 .get::<_, Option<String>>(7)?
822 .map(|encoded| serde_json::from_str(&encoded).map_err(|error| invalid(7, Box::new(error))))
823 .transpose()?;
824 Ok(QueuedUpload {
825 blob: reference,
826 root_table: row.get(1)?,
827 root_id: row.get(2)?,
828 root_label: row.get(3)?,
829 retain_pinned: row.get(4)?,
830 phase,
831 provider_bytes_total,
832 attempt_count: u64::try_from(attempt_count).map_err(|error| invalid(6, Box::new(error)))?,
833 last_failure,
834 created_at: row.get(8)?,
835 last_attempt_at: row.get(9)?,
836 })
837}
838
839fn row_to_published_blob_drop_intent(
840 row: &rusqlite::Row<'_>,
841) -> rusqlite::Result<PublishedBlobDropIntent> {
842 let size: Option<i64> = row.get(3)?;
843 let size = size.ok_or_else(|| {
844 rusqlite::Error::FromSqlConversionFailure(
845 3,
846 rusqlite::types::Type::Integer,
847 Box::new(std::io::Error::new(
848 std::io::ErrorKind::InvalidData,
849 "published blob drop intent is missing size",
850 )),
851 )
852 })?;
853 if size < 0 {
854 return Err(rusqlite::Error::FromSqlConversionFailure(
855 3,
856 rusqlite::types::Type::Integer,
857 Box::new(std::io::Error::new(
858 std::io::ErrorKind::InvalidData,
859 format!("published blob drop intent has negative size {size}"),
860 )),
861 ));
862 }
863 let plaintext_hash = row.get::<_, String>(4)?.parse().map_err(|error| {
864 rusqlite::Error::FromSqlConversionFailure(4, rusqlite::types::Type::Text, Box::new(error))
865 })?;
866 let locator_hash = row.get::<_, String>(5)?.parse().map_err(|error| {
867 rusqlite::Error::FromSqlConversionFailure(5, rusqlite::types::Type::Text, Box::new(error))
868 })?;
869 let disposition_raw: String = row.get(6)?;
870 let disposition = coven_protocol::blob::DeferredLocalBlobDisposition::from_db(&disposition_raw)
871 .map_err(|message| {
872 rusqlite::Error::FromSqlConversionFailure(
873 6,
874 rusqlite::types::Type::Text,
875 Box::new(std::io::Error::new(
876 std::io::ErrorKind::InvalidData,
877 message,
878 )),
879 )
880 })?;
881 Ok(PublishedBlobDropIntent {
882 seq: row.get::<_, i64>(0)? as u64,
883 drop: coven_protocol::blob::DeferredLocalBlobDrop {
884 namespace: row.get(1)?,
885 id: row.get(2)?,
886 size: size as u64,
887 plaintext_hash,
888 locator_hash,
889 disposition,
890 },
891 })
892}
893
894fn row_to_queued_delete(row: &rusqlite::Row<'_>) -> rusqlite::Result<QueuedDelete> {
895 let invalid = |index: usize, source: Box<dyn std::error::Error + Send + Sync>| {
896 rusqlite::Error::FromSqlConversionFailure(index, rusqlite::types::Type::Text, source)
897 };
898 let encoded: String = row.get(0)?;
899 let stored: coven_protocol::blob::locator::StoredBlobRef =
900 serde_json::from_str(&encoded).map_err(|error| invalid(0, Box::new(error)))?;
901 let attempt_count: i64 = row.get(1)?;
902 let last_error = row
903 .get::<_, Option<String>>(2)?
904 .map(|encoded| {
905 serde_json::from_str::<OutboxFailure>(&encoded)
906 .map(|failure| failure.message)
907 .map_err(|error| invalid(2, Box::new(error)))
908 })
909 .transpose()?;
910 Ok(QueuedDelete {
911 namespace: stored.locator().namespace().to_string(),
912 blob_id: stored.locator().blob_id().to_string(),
913 attempt_count: u64::try_from(attempt_count).map_err(|error| invalid(1, Box::new(error)))?,
914 last_error,
915 created_at: row.get(3)?,
916 last_attempt_at: row.get(4)?,
917 })
918}