1pub(crate) use crate::blob_records::load_activated_registration_on;
12pub use crate::blob_records::remote_audience_to_db;
13pub(crate) use crate::circle_snapshot_records::load_outbound_circle_snapshot_on;
14pub(crate) use crate::circle_snapshot_records::load_published_circle_snapshot_on;
15pub use crate::cloud_outbox_records::{
16 outbox_identity, row_to_outbox_entry, CloudOutboxRecords, OutboxIdentity,
17};
18use crate::connection_io::scan_max_updated_at;
19use crate::connection_io::seed_from;
20pub(crate) use crate::local_state::{
21 delete_protocol_state_on, get_protocol_state_on, required_protocol_state_on,
22 set_protocol_state_on,
23};
24use crate::local_store_identity::pin_host_device_id_on;
25use crate::local_store_identity::validate_host_device_id_on;
26pub(crate) use crate::remote_object_records::begin_remote_candidate_nonactivation_on;
27pub(crate) use crate::remote_object_records::begin_remote_candidate_nonactivation_with_verified_head_on;
28pub use crate::remote_object_records::candidate_graph_exact_objects;
29pub(crate) use crate::remote_object_records::finish_remote_candidate_nonactivation_on;
30pub(crate) use crate::remote_object_records::index_retained_replay_owner_on;
31pub(crate) use crate::remote_object_records::load_protocol_inert_object_on;
32pub(crate) use crate::remote_object_records::load_remote_object_on;
33pub(crate) use crate::remote_object_records::mark_remote_object_uploaded_on;
34pub(crate) use crate::remote_object_records::mark_reusable_retained_authority_uploaded_on;
35pub(crate) use crate::remote_object_records::persist_exact_remote_object_on;
36pub(crate) use crate::remote_object_records::persist_prepared_remote_object_on;
37pub(crate) use crate::remote_object_records::record_reclaimed_store_package_on;
38pub(crate) use crate::remote_object_records::reopen_remote_object_on;
39pub(crate) use crate::remote_object_records::replace_prepared_merge_head_remote_on;
40pub(crate) use crate::remote_object_records::update_remote_object_on;
41pub(crate) use crate::remote_object_records::{
42 validate_prepared_blob_on, validate_prepared_package_on, validate_remote_object_on,
43};
44use crate::snapshot_objects::validate_snapshot_object_owners_on;
45pub(crate) use crate::snapshot_objects::{
46 install_snapshot_blob_plan_on, install_snapshot_blob_plans_on, persist_snapshot_image_on,
47 validate_snapshot_blob_plans_on,
48};
49pub use crate::snapshot_objects::{
50 snapshot_generation_as_i64, validate_snapshot_author, validate_snapshot_image,
51 verify_snapshot_blob_spools,
52};
53pub(crate) use crate::snapshot_records::load_outbound_store_snapshot_on;
54pub(crate) use crate::snapshot_records::load_published_store_snapshot_on;
55pub(crate) use crate::snapshot_records::load_published_store_snapshots_on;
56pub use crate::store_ack_records::store_snapshot_first_slot;
57pub(crate) use crate::store_ack_records::{
58 finish_outbound_store_ack_on, load_published_store_ack_on, verify_next_local_store_ack_on,
59};
60pub(crate) use crate::store_authority_records::install_store_founder_state_on;
61pub(crate) use crate::store_reclaim_records::{
62 clear_store_reclaim_operation_stuck_on, insert_store_reclaim_operation_on,
63 load_store_reclaim_operation_on, mark_store_reclaim_operation_stuck_on,
64 update_store_reclaim_operation_on,
65};
66pub use crate::store_reclaim_records::{
67 parse_store_reclaim_operation, store_reclaim_journal_error,
68};
69use std::collections::{BTreeMap, BTreeSet};
70use std::path::{Path, PathBuf};
71use std::sync::Arc;
72
73use coven_keys::encryption::EncryptionService;
74use coven_protocol::audience_package::{AudiencePackage, RowBlobLocatorBinding};
75use coven_protocol::blob::locator::{BlobLocator, RemoteAudience, StoredBlobRef};
76use coven_protocol::blob::{BlobRef, RowBlobAuthority, RowBlobRef};
77use coven_protocol::circle::Audience;
78use coven_protocol::hlc::{Hlc, Timestamp, HIGHWATER_STATE_KEY, MAX_FUTURE_SKEW_MS};
79use coven_protocol::membership::{
80 AuthorHead, MembershipEntry, MembershipEntryRef, MembershipHeadRef,
81};
82use coven_protocol::objects::{ExactObjectRef, PreparedExactObject};
83use coven_protocol::remote_object::{
84 remote_object_id, CandidateExclusiveObjectDomain, RemoteObjectRecord, RetainedReplayOwner,
85 SharedLiveSetObjectDomain,
86};
87use coven_protocol::store_commit::{
88 ack_slot_prefix, ObjectHash, ResolvedStoreDeviceState, SnapshotImageRef, SnapshotMeta,
89 StoreAck, StoreAckRef, StoreBatchCommit, StoreBatchCommitRef, StoreCommitCoord,
90 StoreDeviceHead, StoreDeviceRegistration, StoreDeviceRegistrationRef, StoreProtocolRoot,
91 StoreSnapshotRef,
92};
93use coven_protocol::synced_schema::SyncedTable;
94use coven_protocol::write::{WriteId, WriteStatus};
95use rusqlite::{Connection, OptionalExtension};
96
97pub use rusqlite;
98
99mod blob_bindings;
100pub(crate) use blob_bindings::{
101 install_pulled_merge_membership_activations_on, install_pulled_package_activation_on,
102};
103mod blob_declarations;
104mod blob_records;
105mod changeset;
106mod changeset_identity;
107mod circle_operation_records;
108mod cloud_outbox_records;
109mod connection_io;
110mod coven_migration;
111mod coven_schema;
112mod coven_schema_definitions;
113mod database_connection;
114pub(crate) use connection_io::capture_changeset;
115#[cfg(any(test, feature = "test-utils"))]
116pub(crate) use coven_migration::COVEN_SCHEMA_VERSION_STATE_KEY;
117pub(crate) use coven_migration::{
118 initialize_coven_schema_version, run_coven_migrations_in_transaction,
119 run_uninitialized_snapshot_coven_migrations_in_transaction, validate_coven_schema_for_reader,
120};
121pub use coven_migration::{CovenMigrationError, CovenMigrationPolicy};
122#[cfg(test)]
123pub(crate) use coven_schema::all_table_names;
124pub(crate) use coven_schema::{
125 apply_coven_routing_schema, apply_coven_schema, live_coven_schema_manifest, user_table_names,
126};
127pub use coven_schema::{
128 expected_coven_schema_manifest, is_reserved_table_name, CovenSchemaManifest,
129};
130mod circle_snapshot_records;
131mod database_open;
132mod database_runtime;
133mod database_session;
134mod external_blob_records;
135mod gate;
136mod live_query;
137mod local_state;
138mod local_store_identity;
139mod make_remote;
140mod migration;
141mod operation_models;
142mod prepared_audience_objects;
143mod prepared_external_blob;
144mod remote_object_records;
145mod routing_contract;
146mod schema_contract;
147mod schema_introspection;
148mod snapshot_objects;
149mod snapshot_records;
150pub mod store;
151mod store_ack_records;
152mod store_authority_records;
153mod store_coordinates;
154mod store_reclaim_records;
155#[cfg(any(test, feature = "test-utils"))]
156mod test_sql;
157#[cfg(any(test, feature = "test-utils"))]
158pub mod test_support;
159#[cfg(any(test, feature = "test-utils"))]
160mod test_transaction;
161#[cfg(any(test, feature = "test-utils"))]
162pub use coven_schema::DatabaseTestTable;
163#[cfg(any(test, feature = "test-utils"))]
164pub(crate) use test_sql::DatabaseTestSql;
165#[cfg(any(test, feature = "test-utils"))]
166pub use test_support::synthetic_store;
167#[cfg(any(test, feature = "test-utils"))]
168pub use test_support::{
169 DatabaseImageTest, OutboxAttempt, RetainedRegistrationTamper, ScopedRoutingStateForTest,
170};
171#[cfg(any(test, feature = "test-utils"))]
172pub(crate) use test_transaction::DatabaseTestTransaction;
173mod write_lifecycle;
174mod write_models;
175
176#[cfg(any(test, feature = "test-utils"))]
177pub use blob_declarations::{from_tables_call_count, reset_from_tables_call_count};
178pub use blob_declarations::{BlobDeclError, BlobDecls, PublicationBlob};
179pub(crate) use blob_records::{load_prepared_audience_objects_on, previous_row_blob_for_write_on};
180pub use changeset::{
181 value_ref_to_string, walk as walk_changeset, walk_old as walk_old_changeset, ChangesetError,
182};
183pub use changeset_identity::ChangesetIdentityError;
184pub(crate) use circle_operation_records::{
185 circle_operation_ids_in_phase_on, circle_operation_phase_json,
186};
187pub(crate) use circle_operation_records::{
188 circle_operation_uploaded_steps_on, load_circle_operation_on,
189};
190pub use circle_operation_records::{parse_circle_operation_row, PreparedCircleOperationRow};
191pub use coven_protocol::objects::{ExactProtocolObject, PreparedProtocolObject};
192pub(crate) use database_connection::{DatabaseConnection, DatabaseCore};
193use database_open::CovenMetadataOpen;
194pub use database_runtime::Database;
195pub use external_blob_records::ExternalBlob;
196use external_blob_records::ExternalBlobRecords;
197pub(crate) use gate::query_truth;
198pub(crate) use gate::{
199 active_circle_control, align_inbound_scoped_root_audiences, audience_moves,
200 capture_routing_changes, filter_inbound_circle_changeset, filter_inbound_store_rows,
201 live_row_audience, normalize_inbound_store_changeset, partition_outbound,
202 prune_ineligible_scoped_rows, prune_private_routes_without_rows, retain_snapshot_audience_rows,
203 validate_scoped_foreign_key_audiences, validate_snapshot_routing_state,
204};
205#[cfg(any(test, feature = "test-utils"))]
206pub use gate::{
207 from_tables_call_count as gate_from_tables_call_count,
208 reset_from_tables_call_count as reset_gate_from_tables_call_count,
209};
210pub use gate::{
211 is_routing_table, store_audience_transitions, AudienceMove, AudiencePartition,
212 CircleControlFailure, CirclePartitionControl, CirclePartitionControlError, GateError, Gates,
213 RoutingChanges, StoreAudienceTransitions,
214};
215pub use live_query::{CommittedChanges, QueryDependencies};
216pub(crate) use local_store_identity::local_activated_registration_ref_on;
217pub use migration::supported_version;
218pub(crate) use migration::{ensure_schema_supported, run_migrations_in_transaction};
219pub use migration::{Migration, MigrationContext, MigrationError, MigrationStep};
220pub use operation_models::{
221 DurableCircleSnapshotPublication, DurableDeviceRegistration, DurableMembershipMutation,
222 DurableSnapshotPublication, LocalDeviceRegistrationJournalRow, LocalDeviceRegistrationState,
223 MembershipMutationActivation, OwnerRecoveryPublication, PreparedLocalDeviceRegistrationRow,
224 PreparedSnapshotBlob, PublishedCircleSnapshot, PublishedStoreSnapshot,
225};
226pub use prepared_audience_objects::{
227 validate_prepared_audience_blob_graph, BlobActivation, MakeRemoteIntentState,
228 PreparedAudienceBlob, PreparedAudienceObjects, PreparedAudiencePackage, PreparedRemoteObject,
229 StoredBlobReferenceState,
230};
231pub use prepared_external_blob::{prepare_external_blob, PreparedExternalBlob};
232pub use routing_contract::SyncRoutingContract;
233pub use routing_contract::SyncRoutingContractError;
234use schema_contract::validate_host_synced_tables;
235pub use schema_contract::DurablePreparedProtocolObject;
236pub use schema_contract::{StoreBatchCompletion, StoreBatchLocalCleanup};
237
238#[derive(Debug, Clone, PartialEq, Eq)]
239pub enum MaterializationHold {
240 ForeignKeyDependency,
241 ConstraintConflict(Vec<String>),
242 PrivateSharedConflict {
243 table: String,
244 row_id: String,
245 commit: coven_protocol::store_commit::StoreBatchCommitRef,
246 },
247 InvalidLocalCircleContext {
248 circle_id: coven_protocol::circle::CircleId,
249 },
250}
251
252pub type MaterializationOutcome = coven_protocol::membership::ApplyOutcome<MaterializationHold>;
253
254pub(crate) use schema_introspection::{create_table_sql, foreign_key_edges, table_columns};
255pub use schema_introspection::{
256 quote_ident, rewrite_create_into_schema, CreateTableSchemaError, ForeignKeyEdge,
257 ForeignKeySchemaError,
258};
259pub use store::device_join_journal;
260pub use store::device_join_journal::DeviceJoinJournalError;
261pub(crate) use store::payload_store;
262pub use store::PayloadStoreError;
263pub use store::{
264 activated_merge_membership_remote_objects, DeviceJoinBootstrapActivation,
265 DeviceJoinBootstrapCommit, DeviceJoinBootstrapPlan, DeviceJoinBootstrapRowData,
266 MembershipAuthorityBytes, PreparedMergeMaterialization, PreparedMergeMaterializationPackage,
267 ResolvedDeviceJoinBootstrap, VerifiedAcknowledgedStoreSnapshot,
268 VerifiedReplayBaselineRetirementProof, VerifiedStoreSnapshotAuthority,
269};
270pub use store::{
271 audience_moves_by_row, local_blob_cleanup_intents, AudienceBlobMoveStaging, PostUpload,
272 StagedAudienceBlobRollback,
273};
274pub(crate) use store::{
275 copy_table_with_conflicts, install_circle_bootstrap_image_on,
276 install_circle_bootstrap_remote_objects_on,
277};
278pub use store::{
279 projection_table_names, AdvancedReplayBaseline, BlobTransitionRoot, BlobUploadDrainPermit,
280 BlockedWriteDiscard, CandidateCleanupObject, CircleAckPublicationInput, CreatedSnapshot,
281 DeviceJoinJournalStore, DurableStoreReclaimObject, DurableStoreReclaimOperation,
282 HostWriteBlobTransaction, HostWriteError, HostWriteOperation, IncomingTimestampPolicy,
283 InstalledReplayBaseline, LocalBlobCleanup, MakeRemoteAdmission, MaterializedLocalBlob,
284 MergeCandidateAbandonmentPreparation, ObservedStorePublication, OutboxEntry, OutboxFailure,
285 OutboxFailureKind, OutboxOperation, OutboxUploadState, OwnStreamAuthorship,
286 OwnedVerifiedMergeMaterialization, PreparedCircleObjects, ReclaimCommitActivation,
287 ReclaimedStorePackage, RetainedAudiencePackage, RetainedMergeHistoryCheckpoint,
288 RetainedMergeMaterializationKey, RetainedPackageApplication, RetainedReplayAuthority,
289 RetainedReplayBaseline, RetainedReplayGenesisAuthority, SnapshotBlobAudience, SnapshotBlobFact,
290 SnapshotDatabaseImage, SnapshotImageError, SnapshotImageOperationError,
291 SnapshotPublicationPermit, StoreDatabase, StoreReclaimJournalError, StoreRowWrites,
292 StoreWritePreparation, StuckReclaimOperation, TableSchema, ValidatedChangeset,
293 VerifiedMergeMaterialization, VerifiedMergeMembershipObjects, WinningRow, GENERATION_ZERO,
294};
295#[cfg(any(test, feature = "test-utils"))]
296pub use store::{resolve_and_apply_changeset, ApplyResult};
297#[cfg(any(test, feature = "test-utils"))]
298pub use store::{select_author_exclusion_activation_locator, AuthorExclusionLocatorTamper};
299pub use store::{BlobFileFailure, BlobFileFailures, SqlContext, SqlReadContext, WriteBatch};
300pub use store::{
301 CloudOutboxSnapshot, MakeRemoteProgress, QueuedDelete, QueuedMakeRemote, QueuedUpload,
302 QueuedUploadPhase,
303};
304pub use store_authority_records::DurableFounderMembershipJournal;
305pub(crate) use store_authority_records::{
306 founder_graph_identity, install_store_root_authority_on, load_local_store_founder_graph_on,
307 load_store_root_authority_on,
308};
309pub use store_authority_records::{
310 DurableFounderGraph, DurableFounderMembership, FounderMembershipRefs, StoreOwnerAnchor,
311};
312pub use write_models::{
313 ActivatedStoreAck, AuthorExclusionActivationLocator, BlockedMergeCandidate,
314 CompletePreparedStoreWriteOutcome, InitialStoreMembershipAuthority, MergeAbandonmentState,
315 OutboundStoreAck, OutboundStoreAckActivation, PreparedMergeAbandonmentCandidates,
316 PreparedStoreWrite, PreparedStoreWriteCommit, PreparedStoreWritePartitions, PublishedStoreAck,
317 StoreWriteBase, StoreWriteBlobFact, StoreWriteBlobFacts, StoreWriteBlobMoveDestination,
318 StoreWriteRemoteBlob, StoreWriteRouting, TerminalCandidateAuthority,
319 TerminalCandidateCleanupVerification,
320};
321pub(crate) use write_models::{
322 MergeReplayWrite, MergeReplayWriteEffect, ReplayJournal, SettledStoreWrite, SettledWriteFold,
323};
324
325pub const LOCAL_DEVICE_ID_STATE_KEY: &str = "local_device_id";
326const HOST_DEVICE_ID_STATE_KEY: &str = "host_device_id";
327pub const SYNC_ROUTING_CONTRACT_STATE_KEY: &str = "sync_routing_contract";
328pub const SYNC_ROUTING_HASH_STATE_KEY: &str = "sync_routing_hash";
329pub const COVEN_SCHEMA_MANIFEST_STATE_KEY: &str = "coven_schema_manifest";
330pub const COVEN_INITIALIZED_STATE_KEY: &str = "coven_initialized";
331pub const COVEN_INITIALIZED_STATE_VALUE: &str = "1";
332pub const STORE_DEVICE_GENESIS_STATE_KEY: &str = "store_device_genesis_state";
333const GATE_BASELINE_SCHEMA: &str = "coven_gate_empty";
334const COVEN_CLEANUP_GUARD_PREFIX: &str = "coven_cleanup_guard_";
335
336fn is_coven_cleanup_guard_name(name: &str) -> bool {
337 name.get(..COVEN_CLEANUP_GUARD_PREFIX.len())
338 .is_some_and(|prefix| prefix.eq_ignore_ascii_case(COVEN_CLEANUP_GUARD_PREFIX))
339}
340
341thread_local! {
342 static COVEN_SQL_AUTHORITY_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
354 static HOST_SQL_WRITE_SEEN: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
355}
356
357pub(crate) fn reset_host_sql_write_observation() {
358 HOST_SQL_WRITE_SEEN.with(|seen| seen.set(false));
359}
360
361pub(crate) fn host_sql_write_was_observed() -> bool {
362 HOST_SQL_WRITE_SEEN.with(std::cell::Cell::get)
363}
364
365pub(crate) fn observe_host_sql_write() {
366 HOST_SQL_WRITE_SEEN.with(|seen| seen.set(true));
367}
368
369pub(crate) fn with_coven_sql_authority<R>(f: impl FnOnce() -> R) -> R {
373 struct DepthGuard;
374 impl Drop for DepthGuard {
375 fn drop(&mut self) {
376 COVEN_SQL_AUTHORITY_DEPTH.with(|depth| depth.set(depth.get() - 1));
377 }
378 }
379 COVEN_SQL_AUTHORITY_DEPTH.with(|depth| depth.set(depth.get() + 1));
380 let _guard = DepthGuard;
381 f()
382}
383
384pub(crate) fn authorize_host_sql(
385 context: rusqlite::hooks::AuthContext<'_>,
386) -> rusqlite::hooks::Authorization {
387 use rusqlite::hooks::{AuthAction, Authorization};
388
389 let coven_owned = COVEN_SQL_AUTHORITY_DEPTH.with(|depth| depth.get()) > 0;
390 if !coven_owned
391 && matches!(
392 context.action,
393 AuthAction::Delete { .. } | AuthAction::Insert { .. } | AuthAction::Update { .. }
394 )
395 {
396 HOST_SQL_WRITE_SEEN.with(|seen| seen.set(true));
397 }
398 if coven_owned {
399 return Authorization::Allow;
400 }
401
402 let runs_from_coven_cleanup_guard = context.accessor.is_some_and(is_coven_cleanup_guard_name);
403 let mut accesses_coven_table = match context.action {
404 AuthAction::Delete { table_name }
405 | AuthAction::Insert { table_name }
406 | AuthAction::CreateTable { table_name }
407 | AuthAction::DropTable { table_name }
408 | AuthAction::CreateVtable { table_name, .. }
409 | AuthAction::DropVtable { table_name, .. } => is_reserved_table_name(table_name),
410 AuthAction::Update { table_name, .. }
411 | AuthAction::Read { table_name, .. }
412 | AuthAction::CreateIndex { table_name, .. }
413 | AuthAction::DropIndex { table_name, .. }
414 | AuthAction::CreateTrigger { table_name, .. }
415 | AuthAction::DropTrigger { table_name, .. }
416 | AuthAction::AlterTable { table_name, .. } => is_reserved_table_name(table_name),
417 _ => false,
418 };
419 if runs_from_coven_cleanup_guard {
420 accesses_coven_table = false;
421 }
422 let changes_coven_cleanup_guard = match context.action {
423 AuthAction::CreateTempTrigger { trigger_name, .. }
424 | AuthAction::CreateTrigger { trigger_name, .. }
425 | AuthAction::DropTempTrigger { trigger_name, .. }
426 | AuthAction::DropTrigger { trigger_name, .. } => is_coven_cleanup_guard_name(trigger_name),
427 _ => false,
428 };
429 if accesses_coven_table
430 || changes_coven_cleanup_guard
431 || matches!(
432 context.action,
433 AuthAction::Transaction { .. } | AuthAction::Savepoint { .. }
434 )
435 || context
436 .database_name
437 .is_some_and(|name| name.eq_ignore_ascii_case(GATE_BASELINE_SCHEMA))
438 || matches!(
439 context.action,
440 AuthAction::Detach { database_name }
441 if database_name.eq_ignore_ascii_case(GATE_BASELINE_SCHEMA)
442 )
443 || matches!(
444 context.action,
445 AuthAction::Pragma { pragma_name, .. }
446 if pragma_name.eq_ignore_ascii_case("database_list")
447 )
448 {
449 Authorization::Deny
450 } else {
451 Authorization::Allow
452 }
453}
454
455#[derive(Debug)]
458pub struct StagedBlobRollbackFailure {
459 pub path: PathBuf,
460 pub reason: StagedBlobRollbackReason,
461}
462
463#[derive(Debug, thiserror::Error)]
464pub enum StagedBlobRollbackReason {
465 #[error("staged audience blob disappeared before rollback")]
466 Missing,
467 #[error("{0}")]
468 File(#[from] coven_foundation::atomic_file::FileError),
469}
470
471impl std::fmt::Display for StagedBlobRollbackFailure {
472 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
473 write!(formatter, "{}: {}", self.path.display(), self.reason)
474 }
475}
476
477#[derive(Debug)]
479pub struct StagedBlobRollbackFailures(pub Vec<StagedBlobRollbackFailure>);
480
481impl std::fmt::Display for StagedBlobRollbackFailures {
482 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
483 for (index, failure) in self.0.iter().enumerate() {
484 if index > 0 {
485 formatter.write_str("; ")?;
486 }
487 write!(formatter, "{failure}")?;
488 }
489 Ok(())
490 }
491}
492
493#[derive(Debug, thiserror::Error)]
495pub enum DbError {
496 #[error("database error: {0}")]
497 Message(String),
498 #[error("Store writes depend on state being removed: {writes:?}")]
499 WriteDependencyConflict { writes: Vec<WriteId> },
500 #[error("replay retirement cut is not a canonical application prefix")]
501 ReplayRetirementCutNotPrefix,
502 #[error(
503 "write callback prepared no INSERT, UPDATE, or DELETE statement; pure reads belong on read"
504 )]
505 ReadOnlyWriteTransaction,
506 #[error("{0}")]
507 Sqlite(#[from] rusqlite::Error),
508 #[error("failed to construct the expected Coven schema manifest: {0}")]
509 ExpectedSchema(#[source] &'static rusqlite::Error),
510 #[error("{0}")]
514 Serde(#[from] serde_json::Error),
515 #[error("{0}")]
519 Protocol(#[source] Box<coven_protocol::store_commit::StoreProtocolError>),
520 #[error("{0}")]
521 RemoteObject(#[source] Box<coven_protocol::remote_object::RemoteObjectRecordError>),
522 #[error("{0}")]
523 AudiencePackage(#[source] Box<coven_protocol::audience_package::AudiencePackageError>),
524 #[error("{0}")]
525 ObjectHash(#[from] coven_foundation::object_hash::InvalidObjectHash),
526 #[error("{0}")]
527 BlobLocator(#[from] coven_protocol::blob::locator::BlobLocatorError),
528 #[error("{0}")]
529 CircleId(#[from] coven_protocol::circle::CircleIdError),
530 #[error("{0}")]
531 RowRoutingKey(#[from] coven_protocol::circle::RowRoutingKeyError),
532 #[error("{0}")]
533 CirclePartitionControl(#[from] crate::CirclePartitionControlError),
534 #[error("{0}")]
535 Gate(#[from] crate::gate::GateError),
536 #[error("{0}")]
537 Storage(#[from] coven_protocol::objects::StorageError),
538 #[error("unsafe blob path: {0}")]
539 BlobPath(#[from] coven_foundation::store_dir::PathTokenError),
540 #[error("{0}")]
541 Io(#[from] std::io::Error),
542 #[error("{0}")]
543 File(#[from] coven_foundation::atomic_file::FileError),
544 #[error("{0}")]
545 LocalBlobRemoval(#[from] coven_foundation::store_dir::LocalBlobRemovalError),
546 #[error("{0}")]
547 CachedLocatorRemoval(#[from] coven_foundation::store_dir::CachedLocatorRemovalError),
548 #[error("stored value is out of range: {0}")]
550 IntRange(#[from] std::num::TryFromIntError),
551 #[error("stored value is not an integer: {0}")]
552 ParseInt(#[from] std::num::ParseIntError),
553 #[error("stored value is not UTF-8: {0}")]
554 Utf8(#[from] std::string::FromUtf8Error),
555 #[error("{0}")]
556 Utf8Slice(#[from] std::str::Utf8Error),
557 #[error("{0}")]
558 BlobDecl(#[from] crate::BlobDeclError),
559 #[error("{0}")]
560 ChangesetIdentity(#[from] crate::ChangesetIdentityError),
561 #[error("{0}")]
562 Changeset(#[from] crate::ChangesetError),
563 #[error("{0}")]
564 AuthorStreamId(#[from] coven_protocol::causal_grants::AuthorStreamIdParseError),
565 #[error("{0}")]
566 RowBlobRef(#[from] coven_protocol::blob::RowBlobRefError),
567 #[error("{0}")]
568 WriteRetraction(#[source] Box<coven_protocol::write::WriteRetractionError>),
569 #[error("{0}")]
570 RotationGate(#[from] coven_protocol::objects::RotationGateError),
571 #[error("{0}")]
572 Encryption(#[from] coven_keys::encryption::EncryptionError),
573 #[error("{0}")]
574 SnapshotImage(#[source] Box<crate::store::SnapshotImageError>),
575 #[error("{0}")]
576 PayloadStore(#[source] Box<PayloadStoreError>),
577 #[error("{operation}; payload cleanup failed: {cleanup}")]
578 PayloadCleanupFailed {
579 operation: Box<DbError>,
580 cleanup: Box<DbError>,
581 },
582 #[error("{operation}; committed-change capture failed: {capture}")]
583 ChangeCaptureFailed {
584 operation: Box<DbError>,
585 capture: Box<DbError>,
586 },
587 #[error("{0}")]
588 FromSql(#[from] rusqlite::types::FromSqlError),
589 #[error("{0}")]
590 BlobOpeningAuthority(#[from] coven_protocol::blob::BlobOpeningAuthorityError),
591 #[error("{0}")]
592 OwnerPromotionJournal(
593 #[source] Box<coven_protocol::owner_promotion_journal::OwnerPromotionJournalError>,
594 ),
595 #[error("{0}")]
596 CircleJournal(#[source] Box<coven_protocol::circle_journal::CircleJournalError>),
597 #[error("{0}")]
598 SyncRoutingContract(#[from] SyncRoutingContractError),
599 #[error("{0}")]
600 RowIdentity(#[from] coven_protocol::synced_schema::RowIdentityError),
601 #[error("{0}")]
602 PreparedCommit(#[source] Box<coven_protocol::prepared_commit::PreparedCommitError>),
603 #[error("{0}")]
604 CircleState(#[source] Box<coven_protocol::circle_activation::CircleStateError>),
605 #[error("{0}")]
606 DeviceExclusionJournal(
607 #[source] Box<coven_protocol::device_exclusion_journal::StoreDeviceExclusionJournalError>,
608 ),
609 #[error("{0}")]
610 StoreReclaimJournal(#[source] Box<StoreReclaimJournalError>),
611 #[error("{0}")]
612 CommitNewFile(#[from] coven_foundation::local_file::CommitNewFileError),
613 #[error("audience blob staging: {0}")]
617 AudienceBlobStaging(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
618 #[error("{operation}; audience blob rollback failed: {rollback}")]
622 AudienceBlobRollbackFailed {
623 operation: Box<DbError>,
624 rollback: StagedBlobRollbackFailures,
625 },
626 #[error("staged audience blob rollback failed: {0}")]
627 StagedBlobRollback(StagedBlobRollbackFailures),
628 #[error(
632 "blob move requires materialization for {table}/{row_id}/{column} at {row_stamp}: {reason}"
633 )]
634 BlobMoveRequiresMaterialization {
635 table: String,
636 row_id: String,
637 column: String,
638 row_stamp: String,
639 reason: Box<DbError>,
640 },
641 #[error("outbox entry {entry_id} has unparseable last_attempt_at {value:?}: {source}")]
645 UnparseableOutboxAttemptTime {
646 entry_id: i64,
647 value: String,
648 source: chrono::ParseError,
649 },
650 #[error("{context}: {source}")]
654 Context {
655 context: String,
656 source: Box<DbError>,
657 },
658 #[error("database error: Store protocol root hash is absent")]
659 StoreRootHashMissing,
660 #[error(
665 "device excluded from circle {circle_id} close {close_id} must reset before publishing"
666 )]
667 ExcludedDeviceMustReset {
668 circle_id: coven_protocol::circle::CircleId,
669 close_id: coven_protocol::circle::CircleEpochCloseId,
670 },
671}
672
673impl DbError {
674 pub fn context(context: impl Into<String>, source: impl Into<DbError>) -> DbError {
677 DbError::Context {
678 context: context.into(),
679 source: Box::new(source.into()),
680 }
681 }
682}
683
684macro_rules! boxed_db_error_from {
685 ($source:path, $variant:ident) => {
686 impl From<$source> for DbError {
687 fn from(source: $source) -> Self {
688 Self::$variant(Box::new(source))
689 }
690 }
691 };
692}
693
694boxed_db_error_from!(
695 coven_protocol::owner_promotion_journal::OwnerPromotionJournalError,
696 OwnerPromotionJournal
697);
698boxed_db_error_from!(
699 coven_protocol::device_exclusion_journal::StoreDeviceExclusionJournalError,
700 DeviceExclusionJournal
701);
702boxed_db_error_from!(StoreReclaimJournalError, StoreReclaimJournal);
703boxed_db_error_from!(
704 coven_protocol::remote_object::RemoteObjectRecordError,
705 RemoteObject
706);
707boxed_db_error_from!(coven_protocol::write::WriteRetractionError, WriteRetraction);
708boxed_db_error_from!(crate::store::SnapshotImageError, SnapshotImage);
709boxed_db_error_from!(coven_protocol::store_commit::StoreProtocolError, Protocol);
710boxed_db_error_from!(
711 coven_protocol::audience_package::AudiencePackageError,
712 AudiencePackage
713);
714boxed_db_error_from!(PayloadStoreError, PayloadStore);
715boxed_db_error_from!(
716 coven_protocol::circle_activation::CircleStateError,
717 CircleState
718);
719boxed_db_error_from!(
720 coven_protocol::circle_journal::CircleJournalError,
721 CircleJournal
722);
723boxed_db_error_from!(
724 coven_protocol::prepared_commit::PreparedCommitError,
725 PreparedCommit
726);
727
728#[cfg(test)]
729mod db_error_tests;
730
731pub(crate) fn query_mapped_rows<T, P, F>(
737 conn: &Connection,
738 sql: &str,
739 params: P,
740 mut mapper: F,
741) -> Result<Vec<T>, rusqlite::Error>
742where
743 P: rusqlite::Params,
744 F: FnMut(&rusqlite::Row<'_>) -> rusqlite::Result<T>,
745{
746 let mut statement = conn.prepare_cached(sql)?;
747 let rows = statement.query_map(params, |row| mapper(row))?;
748 let mut mapped = Vec::new();
749 for row in rows {
750 mapped.push(row?);
751 }
752 Ok(mapped)
753}
754
755#[derive(Debug, thiserror::Error)]
761pub enum OpenError {
762 #[error(transparent)]
763 CovenMigration(#[from] CovenMigrationError),
764 #[error(transparent)]
765 Migration(#[from] MigrationError),
766 #[error(transparent)]
767 Db(#[from] DbError),
768}
769
770#[cfg(any(test, feature = "test-utils"))]
772#[doc(hidden)]
773#[derive(Clone, Debug, PartialEq, Eq)]
774pub enum DatabaseTestPoint {
775 LocalBlobCleanupRequested,
776 LocalBlobCleanupAcquired,
777 LocalBlobCleanupBeforeFilesystem {
778 namespace: String,
779 blob_id: String,
780 },
781 LocalBlobCleanupFinished,
782 PullAfterRemoteCommit {
783 device_id: String,
784 seq: u64,
785 },
786 StoreWriteCommitUploaded {
787 write_id: WriteId,
788 },
789 StoreWriteHeadReadBack {
790 write_id: WriteId,
791 },
792 StoreDeviceExclusionCandidateStaged,
793 DeviceJoinAttemptPositionHeld,
797}
798
799#[cfg(any(test, feature = "test-utils"))]
800#[doc(hidden)]
801#[derive(Clone, Copy, Debug, PartialEq, Eq)]
802pub enum MergeMaterializationFailurePoint {
803 SummaryMaterialization,
804 RetractionDeletion,
805 ProjectionReplacement,
806}
807
808#[cfg(any(test, feature = "test-utils"))]
809struct ArmedTestPause<K> {
810 point: K,
811 reached: Arc<tokio::sync::Notify>,
812 resume: Arc<tokio::sync::Notify>,
813}
814
815#[cfg(any(test, feature = "test-utils"))]
816struct TestPauseState<K> {
817 armed: Option<ArmedTestPause<K>>,
818 observers: Vec<tokio::sync::mpsc::UnboundedSender<K>>,
819}
820
821#[cfg(any(test, feature = "test-utils"))]
822struct TestPausePoints<K> {
823 state: std::sync::Mutex<TestPauseState<K>>,
824}
825
826#[cfg(any(test, feature = "test-utils"))]
827impl<K> Default for TestPausePoints<K> {
828 fn default() -> Self {
829 Self {
830 state: std::sync::Mutex::new(TestPauseState {
831 armed: None,
832 observers: Vec::new(),
833 }),
834 }
835 }
836}
837
838#[cfg(any(test, feature = "test-utils"))]
839impl<K: Clone + PartialEq> TestPausePoints<K> {
840 fn arm(&self, point: K) -> (Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>) {
841 let reached = Arc::new(tokio::sync::Notify::new());
842 let resume = Arc::new(tokio::sync::Notify::new());
843 let prior = self
844 .state
845 .lock()
846 .expect("database test pause mutex poisoned")
847 .armed
848 .replace(ArmedTestPause {
849 point,
850 reached: reached.clone(),
851 resume: resume.clone(),
852 });
853 assert!(prior.is_none(), "database test pause already armed");
854 (reached, resume)
855 }
856
857 fn observe(&self) -> tokio::sync::mpsc::UnboundedReceiver<K> {
858 let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
859 self.state
860 .lock()
861 .expect("database test pause mutex poisoned")
862 .observers
863 .push(sender);
864 receiver
865 }
866
867 async fn reach(&self, point: K) {
868 let pause = {
869 let mut state = self
870 .state
871 .lock()
872 .expect("database test pause mutex poisoned");
873 state
874 .observers
875 .retain(|observer| observer.send(point.clone()).is_ok());
876 if state
877 .armed
878 .as_ref()
879 .is_some_and(|pause| pause.point == point)
880 {
881 state.armed.take()
882 } else {
883 None
884 }
885 };
886 if let Some(pause) = pause {
887 pause.reached.notify_one();
888 pause.resume.notified().await;
889 }
890 }
891}
892
893pub struct StagedCircleInstall {
897 pub activation_commit: StoreBatchCommitRef,
898 pub image: coven_protocol::circle_activation::VerifiedCircleImage,
899}
900
901enum CircleRestoreSelection {
902 Pending,
903 Selected(Vec<StagedCircleInstall>),
904}
905
906pub struct VerifiedSnapshotBootstrapInstall {
907 snapshot: PublishedStoreSnapshot,
908 store_root: coven_protocol::objects::VerifiedObject<StoreProtocolRoot>,
909 founder: coven_protocol::objects::VerifiedObject<StoreDeviceRegistration>,
910 authority: coven_protocol::store_commit::RetainedReplaySnapshotAuthority,
911 membership: InitialStoreMembershipAuthority,
912 routing_key: Option<coven_protocol::circle::RowRoutingKey>,
913 circle_selection: CircleRestoreSelection,
914 #[cfg(any(test, feature = "test-utils"))]
918 fail_circle_install: bool,
919}
920
921impl VerifiedSnapshotBootstrapInstall {
922 pub fn new(
923 snapshot: PublishedStoreSnapshot,
924 store_root: coven_protocol::objects::VerifiedObject<StoreProtocolRoot>,
925 founder: coven_protocol::objects::VerifiedObject<StoreDeviceRegistration>,
926 authority: crate::VerifiedStoreSnapshotAuthority,
927 membership: InitialStoreMembershipAuthority,
928 routing_encryption: Option<&EncryptionService>,
929 ) -> Result<Self, DbError> {
930 if store_root.value.to_bytes() != store_root.bytes
931 || store_root.value.object_hash() != store_root.semantic_hash
932 {
933 return Err(DbError::Message(
934 "bootstrap Store root differs from its verified object".to_string(),
935 ));
936 }
937 let root = coven_protocol::store_commit::StoreRootRef {
938 store_root_id: store_root.value.descriptor.store_root_id(),
939 store_root_hash: store_root.semantic_hash,
940 object: store_root.object.clone(),
941 };
942 let founder_reference =
943 StoreDeviceRegistrationRef::from_registration(&founder.value, founder.object.clone());
944 if founder.semantic_hash != founder_reference.registration_hash {
945 return Err(DbError::Message(
946 "bootstrap founder semantic hash differs from its exact registration".to_string(),
947 ));
948 }
949 let authority = authority.into_authority();
950 authority.validate()?;
951 if authority.store_root != root
952 || authority.founder_registration != founder_reference
953 || authority.snapshot != snapshot.reference
954 || authority.metadata != snapshot.meta
955 || snapshot.meta.successor.next_slot != snapshot.successor_slot
956 {
957 return Err(DbError::Message(
958 "bootstrap snapshot differs from its verified authority authority".to_string(),
959 ));
960 }
961 let routing_key = routing_encryption
962 .map(|encryption| {
963 coven_protocol::circle::derive_row_routing_key(encryption, root.store_root_hash)
964 .map_err(|error| DbError::context("derive bootstrap row-routing key", error))
965 })
966 .transpose()?;
967 Ok(Self {
968 snapshot,
969 store_root,
970 founder,
971 authority,
972 membership,
973 routing_key,
974 circle_selection: CircleRestoreSelection::Pending,
975 #[cfg(any(test, feature = "test-utils"))]
976 fail_circle_install: false,
977 })
978 }
979
980 pub fn with_circle_installs(mut self, circle_installs: Vec<StagedCircleInstall>) -> Self {
985 self.circle_selection = CircleRestoreSelection::Selected(circle_installs);
986 self
987 }
988
989 #[cfg(any(test, feature = "test-utils"))]
993 pub fn fail_circle_install_for_test(mut self) -> Self {
994 self.fail_circle_install = true;
995 self
996 }
997}
998
999#[cfg(test)]
1000mod tests;