1use super::*;
2
3#[derive(Clone)]
9pub struct StoreDatabase {
10 database: Database,
11}
12
13impl StoreDatabase {
14 pub async fn prepare_snapshot_database(
18 &self,
19 plaintext: Vec<u8>,
20 install: crate::VerifiedSnapshotBootstrapInstall,
21 ) -> Result<Self, crate::OpenError> {
22 self.database
23 .prepare_snapshot_database(plaintext, install)
24 .await
25 .map(Self::from_database)
26 }
27
28 pub async fn into_prepared_snapshot(self) -> Result<crate::PreparedStoreSnapshot, DbError> {
31 self.database.into_prepared_snapshot().await
32 }
33
34 pub async fn discard_snapshot_preparation(self) -> Result<(), DbError> {
36 self.database.discard_snapshot_preparation().await
37 }
38
39 pub async fn prepare_received_snapshot_circles(
41 &self,
42 selection: crate::StagedCircleRestore,
43 receiver_wall_ms: u64,
44 ) -> Result<(), DbError> {
45 self.database
46 .prepare_received_snapshot_circles(selection, receiver_wall_ms)
47 .await
48 }
49
50 pub async fn install_received_snapshot(
53 &self,
54 prepared: crate::PreparedStoreSnapshot,
55 expected: crate::StorePublicationBoundary,
56 materializations: Vec<crate::PreparedMergeMaterialization>,
57 accepted: crate::AcceptedStorePublicationInterval,
58 snapshots: Vec<crate::VerifiedStoreSnapshotAuthority>,
59 membership: coven_protocol::membership::LocalStoreMembership,
60 routing_encryption: Option<coven_keys::encryption::EncryptionService>,
61 routing_key: Option<coven_protocol::circle::RowRoutingKey>,
62 receiver_wall_ms: u64,
63 ) -> Result<
64 (
65 crate::MaterializationOutcome,
66 Vec<coven_protocol::store_commit::StoreBatchCommitRef>,
67 ),
68 DbError,
69 > {
70 #[cfg(any(test, feature = "test-utils"))]
71 self.reach_test_point(crate::DatabaseTestPoint::ReceivedSnapshotInstallRequested)
72 .await;
73 let snapshots = snapshots
74 .into_iter()
75 .map(crate::VerifiedStoreSnapshotAuthority::into_authority)
76 .collect();
77 let (applied, installed) = self
78 .call_store(move |receiver| {
79 prepared.install_on(
80 receiver,
81 expected,
82 materializations,
83 accepted,
84 snapshots,
85 membership,
86 routing_encryption.as_ref(),
87 routing_key,
88 receiver_wall_ms,
89 )
90 })
91 .await?;
92 for (write_id, status) in applied.write_status_notifications {
93 self.notify_write_status(write_id, status);
94 }
95 Ok((applied.outcome, installed))
96 }
97
98 #[doc(hidden)]
99 pub fn from_database(database: Database) -> Self {
100 Self { database }
101 }
102
103 #[doc(hidden)]
104 pub fn subscribe_committed_changes(
105 &self,
106 ) -> tokio::sync::broadcast::Receiver<std::sync::Arc<crate::CommittedChanges>> {
107 self.database.subscribe_committed_changes()
108 }
109
110 pub(super) fn call_store<F, R>(
111 &self,
112 operation: F,
113 ) -> impl std::future::Future<Output = Result<R, DbError>> + Send + '_
114 where
115 F: for<'session> FnOnce(&mut StoreSession<'session>) -> Result<R, DbError> + Send + 'static,
116 R: Send + 'static,
117 {
118 self.database.call_store(operation)
119 }
120
121 pub(super) fn call_database<F, R>(
122 &self,
123 operation: F,
124 ) -> impl std::future::Future<Output = Result<R, DbError>> + Send + '_
125 where
126 F: for<'session> FnOnce(
127 &mut crate::database_session::DatabaseSession<'session>,
128 ) -> Result<R, DbError>
129 + Send
130 + 'static,
131 R: Send + 'static,
132 {
133 self.database.call_database(operation)
134 }
135
136 pub fn read<F, R, E>(
137 &self,
138 read: F,
139 ) -> impl std::future::Future<Output = Result<Result<R, E>, DbError>> + Send + '_
140 where
141 F: for<'connection> FnOnce(SqlReadContext<'connection>) -> Result<R, E> + Send + 'static,
142 R: Send + 'static,
143 E: Send + 'static,
144 {
145 self.database.read_store(read)
146 }
147
148 pub fn schema_version(&self) -> u32 {
149 self.database.store_schema_version()
150 }
151
152 #[cfg(any(test, feature = "test-utils"))]
153 pub fn synced_tables_for_test(&self) -> Vec<coven_protocol::synced_schema::SyncedTable> {
154 self.database.synced_tables_for_test()
155 }
156
157 #[cfg(any(test, feature = "test-utils"))]
158 pub async fn replace_with_database_image_for_test(
159 &self,
160 image: Vec<u8>,
161 ) -> Result<(), DbError> {
162 self.database
163 .replace_with_database_image_for_test(image)
164 .await
165 }
166
167 #[cfg(any(test, feature = "test-utils"))]
168 pub async fn database_image_for_test(&self) -> Result<Vec<u8>, DbError> {
169 self.database.database_image_for_test().await
170 }
171
172 #[cfg(any(test, feature = "test-utils"))]
173 pub fn assert_owns_payload_directory_for_test(
174 &self,
175 store_dir: &coven_foundation::store_dir::StoreDir,
176 ) {
177 self.database
178 .assert_owns_payload_directory_for_test(store_dir);
179 }
180
181 pub fn sync_routing_hash(&self) -> coven_protocol::store_commit::ObjectHash {
182 self.database.store_sync_routing_hash()
183 }
184
185 pub fn has_synced_tables(&self) -> bool {
186 self.database.store_has_synced_tables()
187 }
188
189 pub fn blob_transition_root(&self, table_name: &str) -> crate::BlobTransitionRoot {
190 self.database.store_blob_transition_root(table_name)
191 }
192
193 pub fn transfer_limits(&self) -> coven_protocol::blob::TransferLimits {
194 self.database.store_transfer_limits()
195 }
196
197 pub fn set_transfer_limits(&self, limits: coven_protocol::blob::TransferLimits) {
200 self.database.set_store_transfer_limits(limits)
201 }
202
203 pub fn blob_tombstone_grace(&self) -> chrono::Duration {
204 self.database.store_blob_tombstone_grace()
205 }
206
207 pub fn has_scoped_graph(&self) -> bool {
208 self.database.store_has_scoped_graph()
209 }
210
211 pub fn stamp(&self) -> String {
212 self.database.store_stamp()
213 }
214
215 pub async fn persist_hlc_high_water(&self) -> Result<(), DbError> {
216 let floor = self.database.store_hlc_high_water();
217 self.call_store(move |session| session.persist_clock_floor(&floor))
218 .await
219 }
220
221 pub fn blob_ref_from_change(
222 &self,
223 change: &coven_foundation::changeset::RowChange,
224 ) -> Result<Option<coven_protocol::blob::BlobRef>, crate::BlobDeclError> {
225 self.database.store_blob_ref_from_change(change)
226 }
227
228 pub fn validate_local_blob_cleanup_changes(
229 &self,
230 old_changes: &[coven_foundation::changeset::RowChange],
231 new_changes: &[coven_foundation::changeset::RowChange],
232 ) -> Result<(), crate::BlobDeclError> {
233 self.database
234 .validate_store_local_blob_cleanup_changes(old_changes, new_changes)
235 }
236
237 pub fn receive_wall_ms(&self) -> u64 {
238 self.database.store_receive_wall_ms()
239 }
240
241 pub fn new_store_write_id(&self) -> coven_protocol::write::WriteId {
242 coven_protocol::write::WriteId::from_generated(self.database.new_store_id())
243 }
244
245 pub async fn get_protocol_state(&self, key: &str) -> Result<Option<String>, DbError> {
246 let key = key.to_string();
247 self.call_store(move |session| session.protocol_state(&key))
248 .await
249 }
250
251 pub async fn set_protocol_state(&self, key: &str, value: &str) -> Result<(), DbError> {
252 let key = key.to_string();
253 let value = value.to_string();
254 self.call_store(move |session| session.set_protocol_state(&key, &value))
255 .await
256 }
257
258 pub async fn get_cache_budget(&self, namespace: &str) -> Result<Option<u64>, DbError> {
259 let key = cache_budget_state_key(namespace);
260 match self.get_protocol_state(&key).await? {
261 Some(raw) => raw.parse::<u64>().map(Some).map_err(|error| {
262 DbError::context(
263 format!("cache budget for {namespace:?} in protocol_state is not a byte count"),
264 error,
265 )
266 }),
267 None => Ok(None),
268 }
269 }
270
271 #[doc(hidden)]
272 pub async fn set_cache_budget(&self, namespace: &str, max_bytes: u64) -> Result<(), DbError> {
273 let key = cache_budget_state_key(namespace);
274 self.set_protocol_state(&key, &max_bytes.to_string()).await
275 }
276
277 pub async fn write_status(
278 &self,
279 write_id: &coven_protocol::write::WriteId,
280 ) -> Result<coven_protocol::write::WriteStatus, DbError> {
281 let write_id = write_id.clone();
282 self.call_store(move |session| session.write_status(&write_id))
283 .await
284 }
285
286 pub async fn store_current_publication(
287 &self,
288 ) -> Result<crate::StorePublicationBoundary, DbError> {
289 self.call_store(|session| session.store_current_publication())
290 .await
291 }
292
293 pub async fn store_publication_entries(
294 &self,
295 ) -> Result<
296 Vec<
297 coven_protocol::objects::ExactProtocolObject<
298 coven_protocol::store_commit::StorePublicationEntry,
299 >,
300 >,
301 DbError,
302 > {
303 self.call_store(|session| session.store_publication_entries())
304 .await
305 }
306
307 pub fn notify_write_status(
308 &self,
309 write_id: coven_protocol::write::WriteId,
310 status: coven_protocol::write::WriteStatus,
311 ) {
312 self.database.notify_store_write_status(write_id, status);
313 }
314
315 pub(super) fn subscribe_store_write_status(
316 &self,
317 write_id: coven_protocol::write::WriteId,
318 current: coven_protocol::write::WriteStatus,
319 ) -> tokio::sync::watch::Receiver<coven_protocol::write::WriteStatus> {
320 self.database
321 .subscribe_store_write_status(write_id, current)
322 }
323
324 pub async fn membership_load_permit(&self) -> MembershipLoadPermit {
325 self.database.membership_load_permit().await
326 }
327
328 pub async fn membership_mutation_permit(&self) -> MembershipMutationPermit {
329 self.database.membership_mutation_permit().await
330 }
331
332 pub async fn store_creation_permit(&self) -> StoreCreationPermit {
333 self.database.store_creation_permit().await
334 }
335
336 pub async fn device_exclusion_permit(&self) -> DeviceExclusionPermit {
337 self.database.device_exclusion_permit().await
338 }
339
340 pub async fn author_own_stream(&self) -> OwnStreamAuthorship {
348 OwnStreamAuthorship {
349 _guard: self.database.author_own_store_stream().await,
350 database: self.clone(),
351 }
352 }
353
354 pub async fn blob_upload_drain_permit(&self) -> BlobUploadDrainPermit {
361 self.database.blob_upload_drain_permit().await
362 }
363
364 pub async fn snapshot_publication_permit(&self) -> SnapshotPublicationPermit {
365 self.database.snapshot_publication_permit().await
366 }
367
368 pub(super) async fn local_blob_cleanup_permit(&self) -> LocalBlobCleanupPermit {
369 self.database.local_blob_cleanup_permit().await
370 }
371
372 pub(super) async fn apply_local_blob_cleanup_intent(
373 &self,
374 intent: &crate::local_blob_cleanup_intents::LocalBlobCleanupIntent,
375 ) -> Result<(), DbError> {
376 self.database.apply_local_blob_cleanup_intent(intent).await
377 }
378
379 pub(super) async fn stage_host_write_blobs<E>(
380 &self,
381 blobs: Vec<super::NewBlob>,
382 ) -> Result<super::StagedBlobBatch, crate::HostWriteError<E>> {
383 self.database.stage_host_write_blobs(blobs).await
384 }
385
386 pub async fn begin_store_creation_attempt(
387 &self,
388 initialized: coven_protocol::store_creation::StoreCreationAttempt,
389 ) -> Result<coven_protocol::store_creation::StoreCreationAttempt, DbError> {
390 let value = serde_json::to_string(&initialized)
391 .map_err(|error| DbError::context("serialize Store creation attempt", error))?;
392 self.call_store(move |session| session.begin_store_creation_attempt(&value))
393 .await
394 }
395
396 pub async fn load_store_creation_attempt(
397 &self,
398 ) -> Result<Option<coven_protocol::store_creation::StoreCreationAttempt>, DbError> {
399 self.call_store(|session| session.load_store_creation_attempt())
400 .await
401 }
402
403 pub async fn advance_store_creation_attempt(
404 &self,
405 previous: coven_protocol::store_creation::StoreCreationAttempt,
406 next: coven_protocol::store_creation::StoreCreationAttempt,
407 ) -> Result<(), DbError> {
408 let previous = serde_json::to_string(&previous)
409 .map_err(|error| DbError::context("serialize Store creation predecessor", error))?;
410 let next = serde_json::to_string(&next)
411 .map_err(|error| DbError::context("serialize Store creation successor", error))?;
412 self.call_store(move |session| session.advance_store_creation_attempt(&previous, &next))
413 .await
414 }
415
416 pub(super) async fn sync_store_parent_dir(
417 &self,
418 path: &std::path::Path,
419 ) -> Result<(), coven_foundation::atomic_file::FileError> {
420 self.database.sync_store_parent_dir(path).await
421 }
422
423 #[cfg(any(test, feature = "test-utils"))]
424 pub fn new(database: &Database) -> Self {
425 Self::from_database(database.clone())
426 }
427
428 #[cfg(any(test, feature = "test-utils"))]
429 pub fn arm_test_pause(
430 &self,
431 point: crate::DatabaseTestPoint,
432 ) -> (
433 std::sync::Arc<tokio::sync::Notify>,
434 std::sync::Arc<tokio::sync::Notify>,
435 ) {
436 self.database.arm_test_pause(point)
437 }
438
439 #[cfg(any(test, feature = "test-utils"))]
440 pub async fn set_invalid_cache_budget_for_test(
441 &self,
442 namespace: &str,
443 value: &str,
444 ) -> Result<(), DbError> {
445 let key = cache_budget_state_key(namespace);
446 self.set_protocol_state(&key, value).await
447 }
448
449 #[cfg(any(test, feature = "test-utils"))]
450 pub async fn reach_test_point(&self, point: crate::DatabaseTestPoint) {
451 self.database.reach_store_test_point(point).await;
452 }
453
454 #[cfg(any(test, feature = "test-utils"))]
455 pub async fn required_store_root_hash(
456 &self,
457 ) -> Result<coven_protocol::store_commit::ObjectHash, DbError> {
458 self.call_store(|session| Ok(session.required_root_authority()?.store_root_hash))
459 .await
460 }
461
462 #[cfg(any(test, feature = "test-utils"))]
463 pub async fn scoped_snapshot_counts_for_test(&self) -> Result<(i64, i64, i64), DbError> {
464 self.call_store(|session| session.scoped_snapshot_counts())
465 .await
466 }
467
468 #[cfg(any(test, feature = "test-utils"))]
469 pub async fn migrated_scoped_snapshot_facts_for_test(
470 &self,
471 ) -> Result<(i64, i64, String), DbError> {
472 self.call_store(|session| session.migrated_scoped_snapshot_facts())
473 .await
474 }
475
476 #[cfg(any(test, feature = "test-utils"))]
477 pub async fn replay_baseline_for_test(&self) -> Result<crate::RetainedReplayBaseline, DbError> {
478 self.call_store(|session| session.load_replay_baseline())
479 .await
480 }
481
482 #[cfg(any(test, feature = "test-utils"))]
483 pub async fn replace_replay_authority_for_test(
484 &self,
485 authority_bytes: Vec<u8>,
486 ) -> Result<(), DbError> {
487 self.call_store(move |session| session.replace_replay_authority(&authority_bytes))
488 .await
489 }
490
491 #[cfg(any(test, feature = "test-utils"))]
492 pub async fn circle_bootstrap_coverage_ref(
493 &self,
494 circle_id: coven_protocol::circle::CircleId,
495 ) -> Result<Option<coven_protocol::circle::CircleBootstrapCoverageRef>, DbError> {
496 self.call_store(move |session| session.circle_bootstrap_coverage_ref(circle_id))
497 .await
498 }
499
500 #[cfg(any(test, feature = "test-utils"))]
501 pub async fn circle_bootstrap_replay_inputs(
502 &self,
503 ) -> Result<
504 Vec<(
505 StoreBatchCommitRef,
506 coven_protocol::circle_activation::VerifiedCircleImage,
507 )>,
508 DbError,
509 > {
510 self.call_store(|session| session.circle_bootstrap_replay_inputs())
511 .await
512 }
513
514 #[cfg(any(test, feature = "test-utils"))]
515 pub async fn circle_control_activation_count_for_test(
516 &self,
517 circle_id: coven_protocol::circle::CircleId,
518 ) -> Result<i64, DbError> {
519 self.call_store(move |session| session.circle_control_activation_count(circle_id))
520 .await
521 }
522}
523
524impl coven_foundation::id_provider::IdProvider for StoreDatabase {
525 fn new_id(&self) -> String {
526 self.database.new_store_id()
527 }
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533 use coven_protocol::blob::{TransferLimits, BLOB_TOMBSTONE_GRACE};
534 use std::{collections::BTreeSet, sync::Arc};
535
536 #[tokio::test]
537 async fn store_calls_dispatch_in_poll_order_and_ignore_unpolled_calls() {
538 let store = StoreDatabase::from_database(crate::tests::fixtures::open_outbox_database(
539 "poll-order",
540 ));
541 let unpolled = store.call_store(|session| session.set_protocol_state("dropped", "written"));
542 drop(unpolled);
543 assert_eq!(store.get_protocol_state("dropped").await.unwrap(), None);
544
545 let first = store.call_store(|session| session.set_protocol_state("order", "first"));
546 let second = store.call_store(|session| session.set_protocol_state("order", "second"));
547 second.await.unwrap();
548 first.await.unwrap();
549 assert_eq!(
550 store.get_protocol_state("order").await.unwrap().as_deref(),
551 Some("first")
552 );
553 }
554
555 #[tokio::test]
556 async fn cancelling_a_dispatched_store_call_preserves_its_commit() {
557 let store = StoreDatabase::from_database(crate::tests::fixtures::open_outbox_database(
558 "cancel-dispatched",
559 ));
560 let (started_tx, started_rx) = tokio::sync::oneshot::channel();
561 let (release_tx, release_rx) = std::sync::mpsc::channel();
562 let mut call = Box::pin(store.call_store(move |session| {
563 started_tx.send(()).expect("signal dispatched operation");
564 release_rx.recv().expect("release dispatched operation");
565 session.set_protocol_state("cancelled", "committed")
566 }));
567 tokio::select! {
568 result = started_rx => result.expect("operation started"),
569 result = &mut call => panic!("operation completed before release: {result:?}"),
570 }
571 drop(call);
572 release_tx.send(()).expect("release database worker");
573 assert_eq!(
574 store
575 .get_protocol_state("cancelled")
576 .await
577 .unwrap()
578 .as_deref(),
579 Some("committed")
580 );
581 }
582
583 #[tokio::test]
584 async fn read_only_store_reads_leave_writer_payload_cleanup_owed() {
585 let directory = tempfile::tempdir().expect("temp dir");
586 let path = directory.path().join("read-only-store.sqlite");
587 let writer = StoreDatabase::from_database(
588 Database::open(
589 &path,
590 Vec::new(),
591 BLOB_TOMBSTONE_GRACE,
592 TransferLimits::one_at_a_time(),
593 "writer".to_string(),
594 Arc::new(coven_foundation::clock::SystemClock),
595 crate::CovenMigrationPolicy::ApplyPending,
596 &[],
597 )
598 .expect("open writer"),
599 );
600
601 writer
602 .call_database(|session| {
603 session.run_test_sql(|database| {
604 let hash = database.install_payload(b"pending cleanup")?;
605 database.set_payload_owner_claims("owner", &BTreeSet::from([hash]))?;
606 database.set_payload_owner_claims("owner", &BTreeSet::new())
607 })
608 })
609 .await
610 .expect("create pending payload cleanup");
611
612 let reader = StoreDatabase::from_database(
613 Database::open_read_only(
614 &path,
615 Vec::new(),
616 BLOB_TOMBSTONE_GRACE,
617 TransferLimits::one_at_a_time(),
618 "writer".to_string(),
619 Arc::new(coven_foundation::clock::SystemClock),
620 &[],
621 )
622 .expect("open reader"),
623 );
624
625 let value = reader
626 .read(|database| database.query_row("SELECT 1", [], |row| row.get::<_, i64>(0)))
627 .await
628 .expect("run read-only Store operation")
629 .expect("read value");
630 assert_eq!(value, 1);
631
632 let (tracked_value, _) = StoreReads::open(&path)
633 .expect("open application readers")
634 .read_tracked(|database| database.query_row("SELECT 2", [], |row| row.get::<_, i64>(0)))
635 .await
636 .expect("run tracked read-only Store operation");
637 assert_eq!(tracked_value.expect("read tracked value"), 2);
638
639 let cleanup_count: i64 = writer
640 .call_database(|session| {
641 session.run_test_sql(|database| {
642 database
643 .query_row("SELECT COUNT(*) FROM payload_cleanup", [], |row| row.get(0))
644 .map_err(DbError::from)
645 })
646 })
647 .await
648 .expect("count pending payload cleanup");
649 assert_eq!(cleanup_count, 1);
650 }
651}