1use super::*;
2
3#[derive(Clone)]
9pub struct StoreDatabase {
10 database: Database,
11}
12
13impl StoreDatabase {
14 #[doc(hidden)]
15 pub fn from_database(database: Database) -> Self {
16 Self { database }
17 }
18
19 #[doc(hidden)]
20 pub fn subscribe_committed_changes(
21 &self,
22 ) -> tokio::sync::broadcast::Receiver<std::sync::Arc<crate::CommittedChanges>> {
23 self.database.subscribe_committed_changes()
24 }
25
26 pub(super) async fn call_store<F, R>(&self, operation: F) -> Result<R, DbError>
27 where
28 F: for<'session> FnOnce(&mut StoreSession<'session>) -> Result<R, DbError> + Send + 'static,
29 R: Send + 'static,
30 {
31 self.database.call_store(operation).await
32 }
33
34 pub(super) async fn call_database<F, R>(&self, operation: F) -> Result<R, DbError>
35 where
36 F: for<'session> FnOnce(
37 &mut crate::database_session::DatabaseSession<'session>,
38 ) -> Result<R, DbError>
39 + Send
40 + 'static,
41 R: Send + 'static,
42 {
43 self.database.call_database(operation).await
44 }
45
46 pub async fn read<F, R, E>(&self, read: F) -> Result<Result<R, E>, DbError>
47 where
48 F: for<'connection> FnOnce(SqlReadContext<'connection>) -> Result<R, E> + Send + 'static,
49 R: Send + 'static,
50 E: Send + 'static,
51 {
52 self.database.read_store(read).await
53 }
54
55 pub fn schema_version(&self) -> u32 {
56 self.database.store_schema_version()
57 }
58
59 #[cfg(any(test, feature = "test-utils"))]
60 pub fn assert_owns_payload_directory_for_test(
61 &self,
62 store_dir: &coven_foundation::store_dir::StoreDir,
63 ) {
64 self.database
65 .assert_owns_payload_directory_for_test(store_dir);
66 }
67
68 pub fn sync_routing_hash(&self) -> coven_protocol::store_commit::ObjectHash {
69 self.database.store_sync_routing_hash()
70 }
71
72 pub fn has_synced_tables(&self) -> bool {
73 self.database.store_has_synced_tables()
74 }
75
76 pub fn blob_transition_root(&self, table_name: &str) -> crate::BlobTransitionRoot {
77 self.database.store_blob_transition_root(table_name)
78 }
79
80 pub fn transfer_limits(&self) -> coven_protocol::blob::TransferLimits {
81 self.database.store_transfer_limits()
82 }
83
84 pub fn set_transfer_limits(&self, limits: coven_protocol::blob::TransferLimits) {
87 self.database.set_store_transfer_limits(limits)
88 }
89
90 pub fn blob_tombstone_grace(&self) -> chrono::Duration {
91 self.database.store_blob_tombstone_grace()
92 }
93
94 pub fn has_scoped_graph(&self) -> bool {
95 self.database.store_has_scoped_graph()
96 }
97
98 pub fn stamp(&self) -> String {
99 self.database.store_stamp()
100 }
101
102 pub async fn persist_hlc_high_water(&self) -> Result<(), DbError> {
103 self.set_protocol_state(
104 coven_protocol::hlc::HIGHWATER_STATE_KEY,
105 &self.database.store_hlc_high_water(),
106 )
107 .await
108 }
109
110 pub fn blob_ref_from_change(
111 &self,
112 change: &coven_foundation::changeset::RowChange,
113 ) -> Result<Option<coven_protocol::blob::BlobRef>, crate::BlobDeclError> {
114 self.database.store_blob_ref_from_change(change)
115 }
116
117 pub fn validate_local_blob_cleanup_changes(
118 &self,
119 old_changes: &[coven_foundation::changeset::RowChange],
120 new_changes: &[coven_foundation::changeset::RowChange],
121 ) -> Result<(), crate::BlobDeclError> {
122 self.database
123 .validate_store_local_blob_cleanup_changes(old_changes, new_changes)
124 }
125
126 pub fn receive_wall_ms(&self) -> u64 {
127 self.database.store_receive_wall_ms()
128 }
129
130 pub fn new_store_write_id(&self) -> coven_protocol::write::WriteId {
131 coven_protocol::write::WriteId::from_generated(self.database.new_store_id())
132 }
133
134 pub async fn get_protocol_state(&self, key: &str) -> Result<Option<String>, DbError> {
135 let key = key.to_string();
136 self.call_store(move |session| session.protocol_state(&key))
137 .await
138 }
139
140 pub async fn set_protocol_state(&self, key: &str, value: &str) -> Result<(), DbError> {
141 let key = key.to_string();
142 let value = value.to_string();
143 self.call_store(move |session| session.set_protocol_state(&key, &value))
144 .await
145 }
146
147 pub async fn get_cache_budget(&self, namespace: &str) -> Result<Option<u64>, DbError> {
148 let key = cache_budget_state_key(namespace);
149 match self.get_protocol_state(&key).await? {
150 Some(raw) => raw.parse::<u64>().map(Some).map_err(|error| {
151 DbError::context(
152 format!("cache budget for {namespace:?} in protocol_state is not a byte count"),
153 error,
154 )
155 }),
156 None => Ok(None),
157 }
158 }
159
160 #[doc(hidden)]
161 pub async fn set_cache_budget(&self, namespace: &str, max_bytes: u64) -> Result<(), DbError> {
162 let key = cache_budget_state_key(namespace);
163 self.set_protocol_state(&key, &max_bytes.to_string()).await
164 }
165
166 pub async fn write_status(
167 &self,
168 write_id: &coven_protocol::write::WriteId,
169 ) -> Result<coven_protocol::write::WriteStatus, DbError> {
170 let write_id = write_id.clone();
171 self.call_store(move |session| session.write_status(&write_id))
172 .await
173 }
174
175 pub async fn store_current_publication(
176 &self,
177 ) -> Result<crate::ObservedStorePublication, DbError> {
178 self.call_store(|session| session.store_current_publication())
179 .await
180 }
181
182 pub fn notify_write_status(
183 &self,
184 write_id: coven_protocol::write::WriteId,
185 status: coven_protocol::write::WriteStatus,
186 ) {
187 self.database.notify_store_write_status(write_id, status);
188 }
189
190 pub(super) fn subscribe_store_write_status(
191 &self,
192 write_id: coven_protocol::write::WriteId,
193 current: coven_protocol::write::WriteStatus,
194 ) -> tokio::sync::watch::Receiver<coven_protocol::write::WriteStatus> {
195 self.database
196 .subscribe_store_write_status(write_id, current)
197 }
198
199 pub async fn membership_load_permit(&self) -> MembershipLoadPermit {
200 self.database.membership_load_permit().await
201 }
202
203 pub async fn membership_mutation_permit(&self) -> MembershipMutationPermit {
204 self.database.membership_mutation_permit().await
205 }
206
207 pub async fn store_creation_permit(&self) -> StoreCreationPermit {
208 self.database.store_creation_permit().await
209 }
210
211 pub async fn device_exclusion_permit(&self) -> DeviceExclusionPermit {
212 self.database.device_exclusion_permit().await
213 }
214
215 pub async fn author_own_stream(&self) -> OwnStreamAuthorship {
223 self.database.author_own_store_stream().await
224 }
225
226 pub async fn blob_upload_drain_permit(&self) -> BlobUploadDrainPermit {
233 self.database.blob_upload_drain_permit().await
234 }
235
236 pub async fn snapshot_publication_permit(&self) -> SnapshotPublicationPermit {
237 self.database.snapshot_publication_permit().await
238 }
239
240 pub(super) async fn local_blob_cleanup_permit(&self) -> LocalBlobCleanupPermit {
241 self.database.local_blob_cleanup_permit().await
242 }
243
244 pub(super) async fn apply_local_blob_cleanup_intent(
245 &self,
246 intent: &crate::local_blob_cleanup_intents::LocalBlobCleanupIntent,
247 ) -> Result<(), DbError> {
248 self.database.apply_local_blob_cleanup_intent(intent).await
249 }
250
251 pub(super) async fn stage_host_write_blobs<E>(
252 &self,
253 blobs: Vec<super::NewBlob>,
254 ) -> Result<super::StagedBlobBatch, crate::HostWriteError<E>> {
255 self.database.stage_host_write_blobs(blobs).await
256 }
257
258 pub async fn begin_store_creation_attempt(
259 &self,
260 initialized: coven_protocol::store_creation::StoreCreationAttempt,
261 ) -> Result<coven_protocol::store_creation::StoreCreationAttempt, DbError> {
262 let value = serde_json::to_string(&initialized)
263 .map_err(|error| DbError::context("serialize Store creation attempt", error))?;
264 self.call_store(move |session| session.begin_store_creation_attempt(&value))
265 .await
266 }
267
268 pub async fn load_store_creation_attempt(
269 &self,
270 ) -> Result<Option<coven_protocol::store_creation::StoreCreationAttempt>, DbError> {
271 self.call_store(|session| session.load_store_creation_attempt())
272 .await
273 }
274
275 pub async fn advance_store_creation_attempt(
276 &self,
277 previous: coven_protocol::store_creation::StoreCreationAttempt,
278 next: coven_protocol::store_creation::StoreCreationAttempt,
279 ) -> Result<(), DbError> {
280 let previous = serde_json::to_string(&previous)
281 .map_err(|error| DbError::context("serialize Store creation predecessor", error))?;
282 let next = serde_json::to_string(&next)
283 .map_err(|error| DbError::context("serialize Store creation successor", error))?;
284 self.call_store(move |session| session.advance_store_creation_attempt(&previous, &next))
285 .await
286 }
287
288 pub(super) async fn sync_store_parent_dir(
289 &self,
290 path: &std::path::Path,
291 ) -> Result<(), coven_foundation::atomic_file::FileError> {
292 self.database.sync_store_parent_dir(path).await
293 }
294
295 #[cfg(any(test, feature = "test-utils"))]
296 pub fn new(database: &Database) -> Self {
297 Self::from_database(database.clone())
298 }
299
300 #[cfg(any(test, feature = "test-utils"))]
301 pub fn arm_test_pause(
302 &self,
303 point: crate::DatabaseTestPoint,
304 ) -> (
305 std::sync::Arc<tokio::sync::Notify>,
306 std::sync::Arc<tokio::sync::Notify>,
307 ) {
308 self.database.arm_test_pause(point)
309 }
310
311 #[cfg(any(test, feature = "test-utils"))]
312 pub async fn set_invalid_cache_budget_for_test(
313 &self,
314 namespace: &str,
315 value: &str,
316 ) -> Result<(), DbError> {
317 let key = cache_budget_state_key(namespace);
318 self.set_protocol_state(&key, value).await
319 }
320
321 #[cfg(any(test, feature = "test-utils"))]
322 pub async fn reach_test_point(&self, point: crate::DatabaseTestPoint) {
323 self.database.reach_store_test_point(point).await;
324 }
325
326 #[cfg(any(test, feature = "test-utils"))]
327 pub async fn required_store_root_hash(
328 &self,
329 ) -> Result<coven_protocol::store_commit::ObjectHash, DbError> {
330 self.call_store(|session| Ok(session.required_root_authority()?.store_root_hash))
331 .await
332 }
333
334 #[cfg(any(test, feature = "test-utils"))]
335 pub async fn scoped_snapshot_counts_for_test(&self) -> Result<(i64, i64, i64), DbError> {
336 self.call_store(|session| session.scoped_snapshot_counts())
337 .await
338 }
339
340 #[cfg(any(test, feature = "test-utils"))]
341 pub async fn migrated_scoped_snapshot_facts_for_test(
342 &self,
343 ) -> Result<(i64, i64, String), DbError> {
344 self.call_store(|session| session.migrated_scoped_snapshot_facts())
345 .await
346 }
347
348 #[cfg(any(test, feature = "test-utils"))]
349 pub async fn generation_zero_replay_baseline_for_test(
350 &self,
351 ) -> Result<crate::RetainedReplayBaseline, DbError> {
352 self.call_store(|session| session.generation_zero_replay_baseline())
353 .await
354 }
355
356 #[cfg(any(test, feature = "test-utils"))]
357 pub async fn replace_generation_zero_replay_authority_for_test(
358 &self,
359 authority_bytes: Vec<u8>,
360 ) -> Result<(), DbError> {
361 self.call_store(move |session| {
362 session.replace_generation_zero_replay_authority(&authority_bytes)
363 })
364 .await
365 }
366
367 #[cfg(any(test, feature = "test-utils"))]
368 pub async fn circle_bootstrap_coverage_ref(
369 &self,
370 circle_id: coven_protocol::circle::CircleId,
371 ) -> Result<Option<coven_protocol::circle::CircleBootstrapCoverageRef>, DbError> {
372 self.call_store(move |session| session.circle_bootstrap_coverage_ref(circle_id))
373 .await
374 }
375
376 #[cfg(any(test, feature = "test-utils"))]
377 pub async fn circle_bootstrap_replay_inputs(
378 &self,
379 ) -> Result<
380 Vec<(
381 StoreBatchCommitRef,
382 coven_protocol::circle_activation::VerifiedCircleImage,
383 )>,
384 DbError,
385 > {
386 self.call_store(|session| session.circle_bootstrap_replay_inputs())
387 .await
388 }
389
390 #[cfg(any(test, feature = "test-utils"))]
391 pub async fn circle_control_activation_count_for_test(
392 &self,
393 circle_id: coven_protocol::circle::CircleId,
394 ) -> Result<i64, DbError> {
395 self.call_store(move |session| session.circle_control_activation_count(circle_id))
396 .await
397 }
398}
399
400impl coven_foundation::id_provider::IdProvider for StoreDatabase {
401 fn new_id(&self) -> String {
402 self.database.new_store_id()
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409 use coven_protocol::blob::{TransferLimits, BLOB_TOMBSTONE_GRACE};
410 use std::{collections::BTreeSet, sync::Arc};
411
412 #[tokio::test]
413 async fn read_only_store_reads_leave_writer_payload_cleanup_owed() {
414 let directory = tempfile::tempdir().expect("temp dir");
415 let path = directory.path().join("read-only-store.sqlite");
416 let writer = StoreDatabase::from_database(
417 Database::open(
418 &path,
419 Vec::new(),
420 BLOB_TOMBSTONE_GRACE,
421 TransferLimits::one_at_a_time(),
422 "writer".to_string(),
423 Arc::new(coven_foundation::clock::SystemClock),
424 crate::CovenMigrationPolicy::ApplyPending,
425 &[],
426 )
427 .expect("open writer"),
428 );
429
430 writer
431 .call_database(|session| {
432 session.run_test_sql(|database| {
433 let hash = database.install_payload(b"pending cleanup")?;
434 database.set_payload_owner_claims("owner", &BTreeSet::from([hash]))?;
435 database.set_payload_owner_claims("owner", &BTreeSet::new())
436 })
437 })
438 .await
439 .expect("create pending payload cleanup");
440
441 let reader = StoreDatabase::from_database(
442 Database::open_read_only(
443 &path,
444 Vec::new(),
445 BLOB_TOMBSTONE_GRACE,
446 TransferLimits::one_at_a_time(),
447 "writer".to_string(),
448 Arc::new(coven_foundation::clock::SystemClock),
449 &[],
450 )
451 .expect("open reader"),
452 );
453
454 let value = reader
455 .read(|database| database.query_row("SELECT 1", [], |row| row.get::<_, i64>(0)))
456 .await
457 .expect("run read-only Store operation")
458 .expect("read value");
459 assert_eq!(value, 1);
460
461 let (tracked_value, _) = StoreReads::open(&path)
462 .expect("open application readers")
463 .read_tracked(|database| database.query_row("SELECT 2", [], |row| row.get::<_, i64>(0)))
464 .await
465 .expect("run tracked read-only Store operation");
466 assert_eq!(tracked_value.expect("read tracked value"), 2);
467
468 let cleanup_count: i64 = writer
469 .call_database(|session| {
470 session.run_test_sql(|database| {
471 database
472 .query_row("SELECT COUNT(*) FROM payload_cleanup", [], |row| row.get(0))
473 .map_err(DbError::from)
474 })
475 })
476 .await
477 .expect("count pending payload cleanup");
478 assert_eq!(cleanup_count, 1);
479 }
480}