1use super::*;
2
3#[derive(Clone)]
7pub struct Database {
8 connection: DatabaseConnection,
9}
10
11fn store_dir_of(path: &Path) -> coven_foundation::store_dir::StoreDir {
17 if path == Path::new(":memory:") {
18 return coven_foundation::store_dir::StoreDir::new_ephemeral(
19 std::env::temp_dir().join(format!("coven-in-memory-store-{}", uuid::Uuid::new_v4())),
20 );
21 }
22 let parent = path
23 .parent()
24 .filter(|parent| !parent.as_os_str().is_empty())
25 .unwrap_or_else(|| Path::new("."));
26 coven_foundation::store_dir::StoreDir::new(parent)
27}
28
29impl Database {
30 pub(crate) async fn prepare_received_snapshot_circles(
31 &self,
32 selection: crate::StagedCircleRestore,
33 receiver_wall_ms: u64,
34 ) -> Result<(), DbError> {
35 self.connection
36 .prepare_received_snapshot_circles(selection, receiver_wall_ms)
37 .await
38 }
39
40 pub(crate) async fn prepare_snapshot_database(
41 &self,
42 plaintext: Vec<u8>,
43 install: VerifiedSnapshotBootstrapInstall,
44 ) -> Result<Database, OpenError> {
45 let connection = self
46 .connection
47 .prepare_snapshot_database(plaintext, install)
48 .await?;
49 Ok(Self { connection })
50 }
51
52 pub(crate) async fn into_prepared_snapshot(
53 self,
54 ) -> Result<crate::PreparedStoreSnapshot, DbError> {
55 self.connection.into_prepared_snapshot().await
56 }
57
58 pub(crate) async fn discard_snapshot_preparation(self) -> Result<(), DbError> {
59 self.connection.discard_snapshot_preparation().await
60 }
61
62 pub(crate) fn from_core(core: DatabaseCore, thread_name: &str) -> Result<Self, DbError> {
63 Ok(Self {
64 connection: DatabaseConnection::start(core, thread_name)?,
65 })
66 }
67
68 pub(crate) fn call_database<F, R>(
69 &self,
70 operation: F,
71 ) -> impl std::future::Future<Output = Result<R, DbError>> + Send + '_
72 where
73 F: for<'session> FnOnce(
74 &mut crate::database_session::DatabaseSession<'session>,
75 ) -> Result<R, DbError>
76 + Send
77 + 'static,
78 R: Send + 'static,
79 {
80 self.connection.call_database(operation)
81 }
82
83 pub(crate) fn call_store<F, R>(
84 &self,
85 operation: F,
86 ) -> impl std::future::Future<Output = Result<R, DbError>> + Send + '_
87 where
88 F: for<'session> FnOnce(&mut crate::store::StoreSession<'session>) -> Result<R, DbError>
89 + Send
90 + 'static,
91 R: Send + 'static,
92 {
93 self.connection.call_store(operation)
94 }
95
96 pub(crate) fn read_store<F, R, E>(
97 &self,
98 read: F,
99 ) -> impl std::future::Future<Output = Result<Result<R, E>, DbError>> + Send + '_
100 where
101 F: for<'connection> FnOnce(crate::store::SqlReadContext<'connection>) -> Result<R, E>
102 + Send
103 + 'static,
104 R: Send + 'static,
105 E: Send + 'static,
106 {
107 self.connection.read_store(read)
108 }
109
110 pub(crate) fn store_schema_version(&self) -> u32 {
111 self.connection.store_schema_version()
112 }
113
114 pub(crate) fn store_sync_routing_hash(&self) -> ObjectHash {
115 self.connection.store_sync_routing_hash()
116 }
117
118 pub(crate) fn store_has_synced_tables(&self) -> bool {
119 self.connection.store_has_synced_tables()
120 }
121
122 pub(crate) fn store_blob_transition_root(&self, table_name: &str) -> BlobTransitionRoot {
123 self.connection.store_blob_transition_root(table_name)
124 }
125
126 pub(crate) fn store_transfer_limits(&self) -> coven_protocol::blob::TransferLimits {
127 self.connection.store_transfer_limits()
128 }
129
130 pub(crate) fn set_store_transfer_limits(&self, limits: coven_protocol::blob::TransferLimits) {
131 self.connection.set_store_transfer_limits(limits)
132 }
133
134 pub(crate) fn store_blob_tombstone_grace(&self) -> chrono::Duration {
135 self.connection.store_blob_tombstone_grace()
136 }
137
138 pub(crate) fn store_has_scoped_graph(&self) -> bool {
139 self.connection.store_has_scoped_graph()
140 }
141
142 pub(crate) fn store_stamp(&self) -> String {
143 self.connection.store_stamp()
144 }
145
146 pub(crate) fn store_hlc_high_water(&self) -> String {
147 self.connection.store_hlc_high_water()
148 }
149
150 pub(crate) fn store_blob_ref_from_change(
151 &self,
152 change: &coven_foundation::changeset::RowChange,
153 ) -> Result<Option<coven_protocol::blob::BlobRef>, BlobDeclError> {
154 self.connection.store_blob_ref_from_change(change)
155 }
156
157 pub(crate) fn validate_store_local_blob_cleanup_changes(
158 &self,
159 old_changes: &[coven_foundation::changeset::RowChange],
160 new_changes: &[coven_foundation::changeset::RowChange],
161 ) -> Result<(), BlobDeclError> {
162 self.connection
163 .validate_store_local_blob_cleanup_changes(old_changes, new_changes)
164 }
165
166 pub(crate) fn store_receive_wall_ms(&self) -> u64 {
167 self.connection.store_receive_wall_ms()
168 }
169
170 #[cfg(any(test, feature = "test-utils"))]
171 pub(crate) fn assert_owns_payload_directory_for_test(
172 &self,
173 store_dir: &coven_foundation::store_dir::StoreDir,
174 ) {
175 self.connection
176 .assert_owns_payload_directory_for_test(store_dir);
177 }
178
179 pub(crate) fn new_store_id(&self) -> String {
180 self.connection.new_store_id()
181 }
182
183 pub(crate) fn notify_store_write_status(&self, write_id: WriteId, status: WriteStatus) {
184 self.connection.notify_store_write_status(write_id, status);
185 }
186
187 pub(crate) fn subscribe_store_write_status(
188 &self,
189 write_id: WriteId,
190 current: WriteStatus,
191 ) -> tokio::sync::watch::Receiver<WriteStatus> {
192 self.connection
193 .subscribe_store_write_status(write_id, current)
194 }
195
196 pub(crate) fn subscribe_committed_changes(
197 &self,
198 ) -> tokio::sync::broadcast::Receiver<Arc<crate::CommittedChanges>> {
199 self.connection.subscribe_committed_changes()
200 }
201
202 pub(crate) async fn membership_load_permit(&self) -> crate::store::MembershipLoadPermit {
203 self.connection.membership_load_permit().await
204 }
205
206 pub(crate) async fn membership_mutation_permit(
207 &self,
208 ) -> crate::store::MembershipMutationPermit {
209 self.connection.membership_mutation_permit().await
210 }
211
212 pub(crate) async fn store_creation_permit(&self) -> crate::store::StoreCreationPermit {
213 self.connection.store_creation_permit().await
214 }
215
216 pub(crate) async fn device_exclusion_permit(&self) -> crate::store::DeviceExclusionPermit {
217 self.connection.device_exclusion_permit().await
218 }
219
220 pub(crate) async fn author_own_store_stream(&self) -> tokio::sync::OwnedMutexGuard<()> {
221 self.connection.author_own_store_stream().await
222 }
223
224 pub(crate) async fn blob_upload_drain_permit(&self) -> crate::store::BlobUploadDrainPermit {
225 self.connection.blob_upload_drain_permit().await
226 }
227
228 pub(crate) async fn snapshot_publication_permit(
229 &self,
230 ) -> crate::store::SnapshotPublicationPermit {
231 self.connection.snapshot_publication_permit().await
232 }
233
234 pub(crate) async fn local_blob_cleanup_permit(&self) -> crate::store::LocalBlobCleanupPermit {
235 self.connection.local_blob_cleanup_permit().await
236 }
237
238 pub(crate) async fn apply_local_blob_cleanup_intent(
239 &self,
240 intent: &crate::local_blob_cleanup_intents::LocalBlobCleanupIntent,
241 ) -> Result<(), DbError> {
242 self.connection
243 .apply_local_blob_cleanup_intent(intent)
244 .await
245 }
246
247 pub(crate) async fn stage_host_write_blobs<E>(
248 &self,
249 blobs: Vec<crate::store::NewBlob>,
250 ) -> Result<crate::store::StagedBlobBatch, crate::HostWriteError<E>> {
251 self.connection.stage_host_write_blobs(blobs).await
252 }
253
254 pub(crate) async fn sync_store_parent_dir(
255 &self,
256 path: &Path,
257 ) -> Result<(), coven_foundation::atomic_file::FileError> {
258 self.connection.sync_store_parent_dir(path).await
259 }
260
261 #[cfg(any(test, feature = "test-utils"))]
262 pub(crate) async fn reach_store_test_point(&self, point: DatabaseTestPoint) {
263 self.connection.reach_store_test_point(point).await;
264 }
265
266 pub fn open(
275 path: &Path,
276 synced_tables: Vec<SyncedTable>,
277 blob_tombstone_grace: chrono::Duration,
278 transfer_limits: coven_protocol::blob::TransferLimits,
279 device_id: String,
280 clock: coven_foundation::clock::ClockRef,
281 coven_migration_policy: CovenMigrationPolicy,
282 migrations: &[Migration],
283 ) -> Result<Database, OpenError> {
284 let hlc = Hlc::try_new(device_id, clock).map_err(|e| DbError::context("device_id", e))?;
285 Self::open_with_hlc_and_coven_metadata(
286 path,
287 synced_tables,
288 blob_tombstone_grace,
289 transfer_limits,
290 Arc::new(hlc),
291 coven_migration_policy,
292 migrations,
293 CovenMetadataOpen::Detect,
294 )
295 }
296
297 pub fn open_initialized_store(
298 path: &Path,
299 install: &VerifiedSnapshotBootstrapInstall,
300 synced_tables: Vec<SyncedTable>,
301 blob_tombstone_grace: chrono::Duration,
302 transfer_limits: coven_protocol::blob::TransferLimits,
303 device_id: String,
304 clock: coven_foundation::clock::ClockRef,
305 coven_migration_policy: CovenMigrationPolicy,
306 migrations: &[Migration],
307 ) -> Result<Database, OpenError> {
308 if ObjectHash::digest(&std::fs::read(path).map_err(DbError::from)?)
309 != install.snapshot.meta.image.image_hash
310 {
311 return Err(DbError::Message(
312 "snapshot database image differs from its authenticated plaintext hash".into(),
313 )
314 .into());
315 }
316 let hlc = Hlc::try_new(device_id, clock).map_err(|e| DbError::context("device_id", e))?;
317 Self::open_with_hlc_and_coven_metadata(
318 path,
319 synced_tables,
320 blob_tombstone_grace,
321 transfer_limits,
322 Arc::new(hlc),
323 coven_migration_policy,
324 migrations,
325 CovenMetadataOpen::VerifiedSnapshot(install),
326 )
327 }
328
329 fn open_with_hlc_and_coven_metadata(
330 path: &Path,
331 synced_tables: Vec<SyncedTable>,
332 blob_tombstone_grace: chrono::Duration,
333 transfer_limits: coven_protocol::blob::TransferLimits,
334 hlc: Arc<Hlc>,
335 coven_migration_policy: CovenMigrationPolicy,
336 migrations: &[Migration],
337 metadata_open: CovenMetadataOpen<'_>,
338 ) -> Result<Database, OpenError> {
339 let store_dir = store_dir_of(path);
340 Self::open_with_hlc_and_coven_metadata_in_store_dir(
341 path,
342 store_dir,
343 crate::connection_io::ConnectionDurability::Full,
344 synced_tables,
345 blob_tombstone_grace,
346 transfer_limits,
347 hlc,
348 coven_migration_policy,
349 migrations,
350 metadata_open,
351 )
352 }
353
354 fn open_with_hlc_and_coven_metadata_in_store_dir(
355 path: &Path,
356 store_dir: coven_foundation::store_dir::StoreDir,
357 connection_durability: crate::connection_io::ConnectionDurability,
358 synced_tables: Vec<SyncedTable>,
359 blob_tombstone_grace: chrono::Duration,
360 transfer_limits: coven_protocol::blob::TransferLimits,
361 hlc: Arc<Hlc>,
362 coven_migration_policy: CovenMigrationPolicy,
363 migrations: &[Migration],
364 metadata_open: CovenMetadataOpen<'_>,
365 ) -> Result<Database, OpenError> {
366 let core = DatabaseCore::open(
367 path,
368 store_dir,
369 connection_durability,
370 synced_tables,
371 blob_tombstone_grace,
372 transfer_limits,
373 hlc,
374 coven_migration_policy,
375 migrations,
376 metadata_open,
377 )?;
378
379 Self::from_core(core, "coven-db").map_err(OpenError::from)
380 }
381
382 #[cfg(any(test, feature = "test-utils"))]
383 pub fn open_in_store_dir_for_test(
384 path: &Path,
385 store_dir: coven_foundation::store_dir::StoreDir,
386 synced_tables: Vec<SyncedTable>,
387 blob_tombstone_grace: chrono::Duration,
388 transfer_limits: coven_protocol::blob::TransferLimits,
389 device_id: String,
390 clock: coven_foundation::clock::ClockRef,
391 coven_migration_policy: CovenMigrationPolicy,
392 migrations: &[Migration],
393 ) -> Result<Database, OpenError> {
394 let hlc = Hlc::try_new(device_id, clock).map_err(|e| DbError::context("device_id", e))?;
395 Self::open_with_hlc_in_store_dir_for_test(
396 path,
397 store_dir,
398 synced_tables,
399 blob_tombstone_grace,
400 transfer_limits,
401 Arc::new(hlc),
402 coven_migration_policy,
403 migrations,
404 )
405 }
406
407 #[cfg(any(test, feature = "test-utils"))]
408 pub fn open_with_hlc_in_store_dir_for_test(
409 path: &Path,
410 store_dir: coven_foundation::store_dir::StoreDir,
411 synced_tables: Vec<SyncedTable>,
412 blob_tombstone_grace: chrono::Duration,
413 transfer_limits: coven_protocol::blob::TransferLimits,
414 hlc: Arc<Hlc>,
415 coven_migration_policy: CovenMigrationPolicy,
416 migrations: &[Migration],
417 ) -> Result<Database, OpenError> {
418 Self::open_with_hlc_and_coven_metadata_in_store_dir(
419 path,
420 store_dir,
421 crate::connection_io::ConnectionDurability::Disabled,
422 synced_tables,
423 blob_tombstone_grace,
424 transfer_limits,
425 hlc,
426 coven_migration_policy,
427 migrations,
428 CovenMetadataOpen::Detect,
429 )
430 }
431
432 pub fn open_read_only(
448 path: &Path,
449 synced_tables: Vec<SyncedTable>,
450 blob_tombstone_grace: chrono::Duration,
451 transfer_limits: coven_protocol::blob::TransferLimits,
452 device_id: String,
453 clock: coven_foundation::clock::ClockRef,
454 migrations: &[Migration],
455 ) -> Result<Database, OpenError> {
456 let hlc = Hlc::try_new(device_id, clock).map_err(|e| DbError::context("device_id", e))?;
457 let store_dir = store_dir_of(path);
458 let core = DatabaseCore::open_read_only(
459 path,
460 store_dir,
461 synced_tables,
462 blob_tombstone_grace,
463 transfer_limits,
464 Arc::new(hlc),
465 migrations,
466 )?;
467 Self::from_core(core, "coven-db-ro").map_err(OpenError::from)
468 }
469
470 #[cfg(any(test, feature = "test-utils"))]
471 #[doc(hidden)]
472 pub fn arm_test_pause(
473 &self,
474 point: DatabaseTestPoint,
475 ) -> (Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>) {
476 self.connection.arm_test_pause(point)
477 }
478
479 #[cfg(any(test, feature = "test-utils"))]
480 #[doc(hidden)]
481 pub fn observe_test_points(&self) -> tokio::sync::mpsc::UnboundedReceiver<DatabaseTestPoint> {
482 self.connection.observe_test_points()
483 }
484
485 #[cfg(any(test, feature = "test-utils"))]
486 #[doc(hidden)]
487 pub fn fail_next_merge_materialization_at(&self, point: MergeMaterializationFailurePoint) {
488 self.connection.fail_next_merge_materialization_at(point);
489 }
490
491 #[cfg(any(test, feature = "test-utils"))]
498 pub fn open_with_hlc(
499 path: &Path,
500 synced_tables: Vec<SyncedTable>,
501 blob_tombstone_grace: chrono::Duration,
502 transfer_limits: coven_protocol::blob::TransferLimits,
503 hlc: Arc<Hlc>,
504 coven_migration_policy: CovenMigrationPolicy,
505 migrations: &[Migration],
506 ) -> Result<Database, OpenError> {
507 Self::open_with_hlc_and_coven_metadata(
508 path,
509 synced_tables,
510 blob_tombstone_grace,
511 transfer_limits,
512 hlc,
513 coven_migration_policy,
514 migrations,
515 CovenMetadataOpen::Detect,
516 )
517 }
518
519 #[cfg(any(test, feature = "test-utils"))]
520 pub fn schema_version(&self) -> u32 {
521 self.connection.store_schema_version()
522 }
523
524 #[cfg(any(test, feature = "test-utils"))]
525 pub fn synced_tables_for_test(&self) -> Vec<SyncedTable> {
526 self.connection.store_synced_tables()
527 }
528
529 #[cfg(any(test, feature = "test-utils"))]
530 pub async fn replace_with_database_image_for_test(
531 &self,
532 image: Vec<u8>,
533 ) -> Result<(), DbError> {
534 self.connection
535 .replace_with_database_image_for_test(image)
536 .await
537 }
538
539 #[cfg(any(test, feature = "test-utils"))]
540 pub async fn database_image_for_test(&self) -> Result<Vec<u8>, DbError> {
541 self.connection.database_image_for_test().await
542 }
543
544 #[cfg(any(test, feature = "test-utils"))]
545 pub fn sync_routing_hash(&self) -> ObjectHash {
546 self.connection.store_sync_routing_hash()
547 }
548
549 #[cfg(any(test, feature = "test-utils"))]
554 pub fn receive_wall_ms(&self) -> u64 {
555 self.connection.store_receive_wall_ms()
556 }
557}
558
559#[cfg(test)]
560mod tests {
561 use super::*;
562
563 #[test]
564 fn a_relative_database_uses_the_working_directory_as_its_store_directory() {
565 assert_eq!(
566 store_dir_of(Path::new("store.sqlite")).as_ref(),
567 Path::new(".")
568 );
569 }
570}