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