Skip to main content

coven_core/
database.rs

1//! The owned SQLite connection.
2//!
3//! coven owns one `rusqlite::Connection` together with the sync bookkeeping
4//! beside it. Every database access — the host's app SQL, coven's bookkeeping,
5//! changeset capture and apply — runs against that one connection, so access is
6//! serialized.
7//!
8//! Hosts open coven with [`crate::Coven::builder`] and run app SQL through
9//! [`crate::CovenHandle::sql`] or [`crate::CovenHandle::write`].
10
11use std::collections::{BTreeMap, BTreeSet, HashMap};
12use std::path::{Path, PathBuf};
13use std::sync::Arc;
14
15use rusqlite::{Connection, OptionalExtension};
16use tracing::error;
17use tracing::warn;
18
19use crate::blob::decl::BlobDecls;
20use crate::blob::locator::{BlobLocator, RemoteAudience, StoredBlobRef};
21use crate::blob::{BlobRef, Provenance, RowBlobAuthority, RowBlobRef};
22use crate::db::{
23    apply_coven_schema, expected_coven_schema_manifest, is_reserved_table_name,
24    live_coven_schema_manifest, CovenSchemaManifest, ExternalBlob, OutboxEntry, OutboxOperation,
25    OutboxUploadState,
26};
27use crate::encryption::EncryptionService;
28use crate::migration::{run_migrations_in_transaction, Migration, MigrationError};
29use crate::sync::audience_package::{AudiencePackage, RowBlobLocatorBinding};
30use crate::sync::circle::Audience;
31use crate::sync::gate::{self, Gates};
32use crate::sync::hlc::{Hlc, Timestamp, UpdatedAtStamper, HIGHWATER_STATE_KEY, MAX_FUTURE_SKEW_MS};
33use crate::sync::membership::{
34    AuthorHead, AuthorStreamId, MembershipEntry, MembershipEntryRef, MembershipHeadRef,
35    SerialAuthorizationState, SerialMembershipState,
36};
37use crate::sync::provider::ProviderAdminState;
38use crate::sync::remote_object::{
39    remote_object_id, CandidateExclusiveObjectDomain, RemoteObjectRecord, SharedLiveSetObjectDomain,
40};
41use crate::sync::routing_contract::SyncRoutingContract;
42use crate::sync::session::{quote_ident, SyncedTable};
43use crate::sync::storage::{ExactObjectRef, PreparedExactObject, VersionToken, VersionedObject};
44use crate::sync::store_commit::{
45    ack_slot_prefix, commit_semantic_prefix, snapshot_image_semantic_prefix, snapshot_slot_prefix,
46    CommitFrontier, ObjectHash, ResolvedStoreDeviceState, SnapshotImageRef, SnapshotMeta, StoreAck,
47    StoreAckRef, StoreBatchCommit, StoreBatchCommitRef, StoreCommitCoord, StoreDeviceHead,
48    StoreDeviceRegistration, StoreDeviceRegistrationRef, StoreDeviceStateRef, StoreProtocolRoot,
49    StoreSerialHead, StoreSerialHeadState, StoreSerialPredecessor, StoreSnapshotRef,
50    StreamActivationId, SERIAL_STREAM_ID,
51};
52use crate::write::{
53    AffectedRow, PendingBranch, PendingBranchId, PendingWrite, PublishedPosition, WriteId,
54    WriteReceipt, WriteResolution, WriteStatus,
55};
56use crate::WritePolicy;
57
58pub const LOCAL_DEVICE_ID_STATE_KEY: &str = "local_device_id";
59const HOST_DEVICE_ID_STATE_KEY: &str = "host_device_id";
60pub const WRITE_POLICY_STATE_KEY: &str = "write_policy";
61const SYNC_ROUTING_CONTRACT_STATE_KEY: &str = "sync_routing_contract";
62pub const SYNC_ROUTING_HASH_STATE_KEY: &str = "sync_routing_hash";
63const COVEN_SCHEMA_MANIFEST_STATE_KEY: &str = "coven_schema_manifest";
64const COVEN_INITIALIZED_STATE_KEY: &str = "coven_initialized";
65const COVEN_INITIALIZED_STATE_VALUE: &str = "1";
66pub const SERIAL_MEMBERSHIP_STATE_KEY: &str = "serial_membership_state";
67pub const SERIAL_KEY_GENERATION_STATE_KEY: &str = "serial_key_generation";
68pub const SERIAL_PROVIDER_ADMIN_STATE_KEY: &str = "serial_provider_admin_state";
69const SERIAL_CANDIDATE_ABANDONMENT_STATE_KEY: &str = "serial_candidate_abandonment";
70const STORE_DEVICE_GENESIS_STATE_KEY: &str = "store_device_genesis_state";
71const GATE_BASELINE_SCHEMA: &str = "coven_gate_empty";
72
73fn load_store_device_genesis_state_on(
74    conn: &Connection,
75) -> Result<ResolvedStoreDeviceState, DbError> {
76    let raw: String = conn
77        .query_row(
78            "SELECT value FROM protocol_state WHERE key = ?1",
79            [STORE_DEVICE_GENESIS_STATE_KEY],
80            |row| row.get(0),
81        )
82        .map_err(DbError::from)?;
83    serde_json::from_str(&raw)
84        .map_err(|error| DbError::Message(format!("parse Store device genesis state: {error}")))
85}
86
87fn store_serial_predecessor_on(
88    conn: &Connection,
89    commit: Option<&StoreBatchCommitRef>,
90) -> Result<StoreSerialPredecessor, DbError> {
91    if let Some(commit) = commit {
92        return Ok(StoreSerialPredecessor::Commit(commit.clone()));
93    }
94    let root = required_store_root_authority_on(conn)?;
95    let genesis = load_store_device_genesis_state_on(conn)?;
96    let mut devices = genesis.devices.values();
97    let Some(founder) = devices.next() else {
98        return Err(DbError::Message(
99            "Serial genesis must contain exactly one founder registration".to_string(),
100        ));
101    };
102    if devices.next().is_some() {
103        return Err(DbError::Message(
104            "Serial genesis must contain exactly one founder registration".to_string(),
105        ));
106    }
107    Ok(StoreSerialPredecessor::Genesis {
108        root,
109        founder_registration: founder.registration.clone(),
110    })
111}
112
113fn load_store_device_snapshot_on(
114    conn: &Connection,
115    reference: &StoreBatchCommitRef,
116) -> Result<ResolvedStoreDeviceState, DbError> {
117    let exact = serde_json::to_string(reference)
118        .map_err(|error| DbError::Message(format!("serialize Store commit ref: {error}")))?;
119    let raw: String = conn
120        .query_row(
121            "SELECT state FROM store_device_state_snapshots WHERE commit_ref = ?1",
122            [exact],
123            |row| row.get(0),
124        )
125        .map_err(DbError::from)?;
126    serde_json::from_str(&raw)
127        .map_err(|error| DbError::Message(format!("parse Store device state snapshot: {error}")))
128}
129
130fn load_declared_store_device_state_on(
131    conn: &Connection,
132    reference: &StoreDeviceStateRef,
133) -> Result<ResolvedStoreDeviceState, DbError> {
134    let state = match reference {
135        StoreDeviceStateRef::MergeConcurrent { frontier, .. } => {
136            let CommitFrontier::MergeConcurrent(frontier) = frontier else {
137                return Err(DbError::Message(
138                    "Merge device state carries a Serial frontier".into(),
139                ));
140            };
141            if frontier.is_empty() {
142                load_store_device_genesis_state_on(conn)?
143            } else {
144                ResolvedStoreDeviceState::merge(
145                    frontier
146                        .values()
147                        .map(|commit| load_store_device_snapshot_on(conn, commit))
148                        .collect::<Result<Vec<_>, _>>()?,
149                )
150                .map_err(|error| DbError::Message(error.to_string()))?
151            }
152        }
153        StoreDeviceStateRef::Serial { position, .. } => match position {
154            StoreSerialPredecessor::Genesis { .. } => load_store_device_genesis_state_on(conn)?,
155            StoreSerialPredecessor::Commit(commit) => load_store_device_snapshot_on(conn, commit)?,
156        },
157    };
158    if state.state_hash != reference.state_hash() || state.recovery != reference.recovery() {
159        return Err(DbError::Message(
160            "declared Store device state differs from its exact predecessor snapshots".into(),
161        ));
162    }
163    Ok(state)
164}
165
166fn authorize_host_sql(context: rusqlite::hooks::AuthContext<'_>) -> rusqlite::hooks::Authorization {
167    use rusqlite::hooks::{AuthAction, Authorization};
168
169    if context
170        .database_name
171        .is_some_and(|name| name.eq_ignore_ascii_case(GATE_BASELINE_SCHEMA))
172        || matches!(
173            context.action,
174            AuthAction::Detach { database_name }
175                if database_name.eq_ignore_ascii_case(GATE_BASELINE_SCHEMA)
176        )
177        || matches!(
178            context.action,
179            AuthAction::Pragma { pragma_name, .. }
180                if pragma_name.eq_ignore_ascii_case("database_list")
181        )
182    {
183        Authorization::Deny
184    } else {
185        Authorization::Allow
186    }
187}
188
189/// An error from the owned database.
190#[derive(Debug, thiserror::Error)]
191pub enum DbError {
192    #[error("database error: {0}")]
193    Message(String),
194    #[error("database error: Store protocol root hash is absent")]
195    StoreRootHashMissing,
196}
197
198impl DbError {
199    pub(crate) fn into_message(self) -> String {
200        match self {
201            Self::Message(message) => message,
202            Self::StoreRootHashMissing => "Store protocol root hash is absent".to_string(),
203        }
204    }
205
206    fn missing_store_root_hash() -> Self {
207        Self::StoreRootHashMissing
208    }
209}
210
211impl From<rusqlite::Error> for DbError {
212    fn from(e: rusqlite::Error) -> Self {
213        DbError::Message(e.to_string())
214    }
215}
216
217/// Why opening the database failed. Splits a migration-ladder failure from every
218/// other open-time database error so the [`MigrationError`] a host acts on —
219/// [`MigrationError::SchemaTooNew`], whose remedy is "update the app" — stays
220/// matchable at the open boundary instead of being flattened into a
221/// [`DbError`] string.
222#[derive(Debug, thiserror::Error)]
223pub enum OpenError {
224    #[error(transparent)]
225    Migration(#[from] MigrationError),
226    #[error(transparent)]
227    Db(#[from] DbError),
228}
229
230#[derive(Clone)]
231struct DatabaseState {
232    write_policy: WritePolicy,
233    hlc: Arc<Hlc>,
234    synced_tables: Arc<Vec<SyncedTable>>,
235    schema_version: u32,
236    sync_routing_hash: ObjectHash,
237    gates: Arc<Gates>,
238    blob_decls: Arc<BlobDecls>,
239    /// The host's blob-tombstone convergence window, read by the tombstone GC to
240    /// age each tombstone's `deleted_at`. Host config carried here alongside
241    /// `synced_tables` so the sync layer reads it from the one owner rather than
242    /// threading a separately-passed copy that could diverge.
243    blob_tombstone_grace: chrono::Duration,
244    /// How many blob transfers each transfer loop runs at once, read by the upload
245    /// drain and the pin loop (both hold `&Database`). Open-time host config carried
246    /// here for the same single-owner reason as `blob_tombstone_grace`.
247    transfer_limits: crate::blob::TransferLimits,
248    /// Serializes complete membership-chain loads that share this database, so a
249    /// load cannot return an older chain after another load commits a newer floor.
250    membership_load: Arc<tokio::sync::Mutex<()>>,
251    /// Serializes construction and execution of the one local membership mutation
252    /// whose exact signed bytes are held in `outbound_membership_mutation`.
253    membership_mutation: Arc<tokio::sync::Mutex<()>>,
254    /// Serializes staging and publication of the one exact snapshot generation
255    /// held in `outbound_store_snapshot`.
256    snapshot_publication: Arc<tokio::sync::Mutex<()>>,
257    /// Serializes the full durable-intent to filesystem-deletion to intent-removal
258    /// operation across every clone of this database.
259    local_blob_cleanup: Arc<tokio::sync::Mutex<()>>,
260    ids: crate::id_provider::IdRef,
261    write_statuses:
262        Arc<std::sync::Mutex<HashMap<WriteId, tokio::sync::watch::Sender<WriteStatus>>>>,
263    #[cfg(any(test, feature = "test-utils"))]
264    test_pause_points: Arc<TestPausePoints<DatabaseTestPoint>>,
265}
266
267/// Test-only checkpoints reached by database operations whose ordering matters.
268#[cfg(any(test, feature = "test-utils"))]
269#[doc(hidden)]
270#[derive(Clone, Debug, PartialEq, Eq)]
271pub enum DatabaseTestPoint {
272    LocalBlobCleanupRequested,
273    LocalBlobCleanupAcquired,
274    LocalBlobCleanupBeforeFilesystem { namespace: String, blob_id: String },
275    LocalBlobCleanupFinished,
276    PullAfterRemoteCommit { device_id: String, seq: u64 },
277}
278
279#[cfg(any(test, feature = "test-utils"))]
280struct ArmedTestPause<K> {
281    point: K,
282    reached: Arc<tokio::sync::Notify>,
283    resume: Arc<tokio::sync::Notify>,
284}
285
286#[cfg(any(test, feature = "test-utils"))]
287struct TestPauseState<K> {
288    armed: Option<ArmedTestPause<K>>,
289    observers: Vec<tokio::sync::mpsc::UnboundedSender<K>>,
290}
291
292#[cfg(any(test, feature = "test-utils"))]
293struct TestPausePoints<K> {
294    state: std::sync::Mutex<TestPauseState<K>>,
295}
296
297#[cfg(any(test, feature = "test-utils"))]
298impl<K> Default for TestPausePoints<K> {
299    fn default() -> Self {
300        Self {
301            state: std::sync::Mutex::new(TestPauseState {
302                armed: None,
303                observers: Vec::new(),
304            }),
305        }
306    }
307}
308
309#[cfg(any(test, feature = "test-utils"))]
310impl<K: Clone + PartialEq> TestPausePoints<K> {
311    fn arm(&self, point: K) -> (Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>) {
312        let reached = Arc::new(tokio::sync::Notify::new());
313        let resume = Arc::new(tokio::sync::Notify::new());
314        let prior = self
315            .state
316            .lock()
317            .expect("database test pause mutex poisoned")
318            .armed
319            .replace(ArmedTestPause {
320                point,
321                reached: reached.clone(),
322                resume: resume.clone(),
323            });
324        assert!(prior.is_none(), "database test pause already armed");
325        (reached, resume)
326    }
327
328    fn observe(&self) -> tokio::sync::mpsc::UnboundedReceiver<K> {
329        let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
330        self.state
331            .lock()
332            .expect("database test pause mutex poisoned")
333            .observers
334            .push(sender);
335        receiver
336    }
337
338    async fn reach(&self, point: K) {
339        let pause = {
340            let mut state = self
341                .state
342                .lock()
343                .expect("database test pause mutex poisoned");
344            state
345                .observers
346                .retain(|observer| observer.send(point.clone()).is_ok());
347            if state
348                .armed
349                .as_ref()
350                .is_some_and(|pause| pause.point == point)
351            {
352                state.armed.take()
353            } else {
354                None
355            }
356        };
357        if let Some(pause) = pause {
358            pause.reached.notify_one();
359            pause.resume.notified().await;
360        }
361    }
362}
363
364/// The owned SQLite connection and the sync bookkeeping resolved beside it at
365/// open. One connection thread owns this for the connection's whole life; every
366/// database access runs against it, so access is serialized. Changeset capture is
367/// per-transaction — [`Database::run_store_write_transaction_on`] attaches a
368/// session for the span of one host write and drains it into the existing write records —
369/// so no capture state lives on the core between calls.
370///
371/// `DatabaseCore` holds only `Send` fields (a `rusqlite::Connection`, which is
372/// `Send`, plus `Arc`s and a `u32`), so it is `Send` by construction — the
373/// connection thread receives it by value across a single `thread::spawn` with no
374/// manual `unsafe impl`.
375struct DatabaseCore {
376    conn: Connection,
377    hlc: Arc<Hlc>,
378    synced_tables: Arc<Vec<SyncedTable>>,
379    schema_version: u32,
380    sync_routing_hash: ObjectHash,
381    gates: Arc<Gates>,
382    blob_decls: Arc<BlobDecls>,
383    blob_tombstone_grace: chrono::Duration,
384    transfer_limits: crate::blob::TransferLimits,
385    write_policy: WritePolicy,
386}
387
388#[derive(Clone, Copy)]
389enum CovenMetadataOpen {
390    Detect,
391    InitializeVerifiedSnapshot,
392}
393
394fn serialized_write_policy(write_policy: WritePolicy) -> Result<String, DbError> {
395    serde_json::to_string(&write_policy)
396        .map_err(|error| DbError::Message(format!("serialize Store write policy: {error}")))
397}
398
399fn initialize_write_policy(conn: &Connection, write_policy: WritePolicy) -> Result<(), DbError> {
400    let serialized = serialized_write_policy(write_policy)?;
401    conn.execute(
402        "INSERT INTO protocol_state (key, value) VALUES (?1, ?2)",
403        (WRITE_POLICY_STATE_KEY, serialized),
404    )
405    .map_err(DbError::from)?;
406    Ok(())
407}
408
409fn protocol_state_exists(conn: &Connection) -> Result<bool, DbError> {
410    conn.query_row(
411        "SELECT EXISTS(\
412             SELECT 1 FROM main.sqlite_schema \
413             WHERE type = 'table' AND name = 'protocol_state'\
414         )",
415        [],
416        |row| row.get(0),
417    )
418    .map_err(DbError::from)
419}
420
421fn has_coven_initialization_marker(conn: &Connection) -> Result<bool, DbError> {
422    if !protocol_state_exists(conn)? {
423        return Ok(false);
424    }
425    let marker = conn
426        .query_row(
427            "SELECT value FROM protocol_state WHERE key = ?1",
428            [COVEN_INITIALIZED_STATE_KEY],
429            |row| row.get::<_, String>(0),
430        )
431        .optional()
432        .map_err(DbError::from)?;
433    match marker.as_deref() {
434        None => Ok(false),
435        Some(COVEN_INITIALIZED_STATE_VALUE) => Ok(true),
436        Some(value) => Err(DbError::Message(format!(
437            "Store database has invalid Coven initialization marker {value:?}"
438        ))),
439    }
440}
441
442fn initialize_coven_metadata_on(
443    conn: &Connection,
444    write_policy: WritePolicy,
445    sync_routing_contract: &SyncRoutingContract,
446    install_routing_schema: bool,
447) -> Result<(), DbError> {
448    apply_coven_schema(conn).map_err(DbError::from)?;
449    if install_routing_schema {
450        crate::db::apply_coven_routing_schema(conn).map_err(DbError::from)?;
451    }
452    let schema_manifest = validate_live_coven_schema(conn, install_routing_schema)?;
453    initialize_write_policy(conn, write_policy)?;
454    let contract_json =
455        String::from_utf8(sync_routing_contract.bytes().to_vec()).map_err(|error| {
456            DbError::Message(format!("encode sync-routing contract metadata: {error}"))
457        })?;
458    conn.execute(
459        "INSERT INTO protocol_state (key, value) VALUES (?1, ?2)",
460        (SYNC_ROUTING_CONTRACT_STATE_KEY, contract_json),
461    )
462    .map_err(DbError::from)?;
463    conn.execute(
464        "INSERT INTO protocol_state (key, value) VALUES (?1, ?2)",
465        (COVEN_SCHEMA_MANIFEST_STATE_KEY, schema_manifest),
466    )
467    .map_err(DbError::from)?;
468    conn.execute(
469        "INSERT INTO protocol_state (key, value) VALUES (?1, ?2)",
470        (
471            SYNC_ROUTING_HASH_STATE_KEY,
472            sync_routing_contract.hash().to_string(),
473        ),
474    )
475    .map_err(DbError::from)?;
476    conn.execute(
477        "INSERT INTO protocol_state (key, value) VALUES (?1, ?2)",
478        (COVEN_INITIALIZED_STATE_KEY, COVEN_INITIALIZED_STATE_VALUE),
479    )
480    .map(|_| ())
481    .map_err(DbError::from)
482}
483
484fn validate_live_coven_schema(conn: &Connection, include_routing: bool) -> Result<String, DbError> {
485    let expected = expected_coven_schema_manifest(include_routing).map_err(DbError::from)?;
486    let actual = live_coven_schema_manifest(conn).map_err(DbError::from)?;
487    if actual != expected {
488        return Err(DbError::Message(format!(
489            "Coven schema does not match the current table, index, constraint, primary-key, STRICT, and WITHOUT ROWID declarations: expected {expected:?}, found {actual:?}"
490        )));
491    }
492    serde_json::to_string(&expected)
493        .map_err(|error| DbError::Message(format!("serialize Coven schema manifest: {error}")))
494}
495
496fn validate_initialized_coven_schema(
497    conn: &Connection,
498    include_routing: bool,
499) -> Result<(), DbError> {
500    let expected = expected_coven_schema_manifest(include_routing).map_err(DbError::from)?;
501    let stored_json = conn
502        .query_row(
503            "SELECT value FROM protocol_state WHERE key = ?1",
504            [COVEN_SCHEMA_MANIFEST_STATE_KEY],
505            |row| row.get::<_, String>(0),
506        )
507        .optional()
508        .map_err(DbError::from)?
509        .ok_or_else(|| {
510            DbError::Message(
511                "Store database is missing required Coven schema manifest metadata".to_string(),
512            )
513        })?;
514    let stored: CovenSchemaManifest = serde_json::from_str(&stored_json).map_err(|error| {
515        DbError::Message(format!("Store Coven schema manifest is invalid: {error}"))
516    })?;
517    if stored != expected {
518        return Err(DbError::Message(format!(
519            "Store Coven schema manifest does not match the current schema: stored {stored:?}, current {expected:?}"
520        )));
521    }
522    validate_live_coven_schema(conn, include_routing)?;
523    Ok(())
524}
525
526fn load_coven_metadata(
527    conn: &Connection,
528    write_policy: WritePolicy,
529) -> Result<SyncRoutingContract, DbError> {
530    if !has_coven_initialization_marker(conn)? {
531        return Err(DbError::Message(
532            "Store database is missing required Coven initialization metadata".to_string(),
533        ));
534    }
535    validate_write_policy(conn, write_policy)?;
536    let contract_bytes = conn
537        .query_row(
538            "SELECT value FROM protocol_state WHERE key = ?1",
539            [SYNC_ROUTING_CONTRACT_STATE_KEY],
540            |row| row.get::<_, String>(0),
541        )
542        .optional()
543        .map_err(DbError::from)?
544        .ok_or_else(|| {
545            DbError::Message(
546                "Store database is missing required sync_routing_contract metadata".to_string(),
547            )
548        })?;
549    let contract = SyncRoutingContract::from_bytes(contract_bytes.as_bytes()).map_err(|error| {
550        DbError::Message(format!("Store sync-routing contract is invalid: {error}"))
551    })?;
552    let stored_hash = conn
553        .query_row(
554            "SELECT value FROM protocol_state WHERE key = ?1",
555            [SYNC_ROUTING_HASH_STATE_KEY],
556            |row| row.get::<_, String>(0),
557        )
558        .optional()
559        .map_err(DbError::from)?
560        .ok_or_else(|| {
561            DbError::Message(
562                "Store database is missing required sync_routing_hash metadata".to_string(),
563            )
564        })?;
565    let stored_hash: ObjectHash = stored_hash.parse().map_err(|error| {
566        DbError::Message(format!(
567            "Store sync_routing_hash metadata is invalid: {error}"
568        ))
569    })?;
570    if stored_hash != contract.hash() {
571        return Err(DbError::Message(format!(
572            "Store sync-routing contract hashes to {}, but metadata records {stored_hash}",
573            contract.hash(),
574        )));
575    }
576    Ok(contract)
577}
578
579fn validate_sync_routing_contract(
580    pinned: &SyncRoutingContract,
581    resolved: &SyncRoutingContract,
582) -> Result<(), DbError> {
583    if pinned.bytes() != resolved.bytes() || pinned.hash() != resolved.hash() {
584        return Err(DbError::Message(format!(
585            "Store sync-routing hash is {}, but open resolved {}",
586            pinned.hash(),
587            resolved.hash(),
588        )));
589    }
590    Ok(())
591}
592
593fn validate_write_policy(conn: &Connection, requested: WritePolicy) -> Result<(), DbError> {
594    let stored = conn
595        .query_row(
596            "SELECT value FROM protocol_state WHERE key = ?1",
597            [WRITE_POLICY_STATE_KEY],
598            |row| row.get::<_, String>(0),
599        )
600        .optional()
601        .map_err(DbError::from)?
602        .ok_or_else(|| {
603            DbError::Message("Store database is missing required write_policy metadata".to_string())
604        })?;
605    let stored: WritePolicy = serde_json::from_str(&stored).map_err(|error| {
606        DbError::Message(format!("Store write_policy metadata is invalid: {error}"))
607    })?;
608    if stored != requested {
609        return Err(DbError::Message(format!(
610            "Store write policy is {stored:?}, but open requested {requested:?}"
611        )));
612    }
613    Ok(())
614}
615
616impl DatabaseCore {
617    fn open(
618        path: &Path,
619        synced_tables: Vec<SyncedTable>,
620        blob_tombstone_grace: chrono::Duration,
621        transfer_limits: crate::blob::TransferLimits,
622        write_policy: WritePolicy,
623        hlc: Arc<Hlc>,
624        migrations: &[Migration],
625        metadata_open: CovenMetadataOpen,
626    ) -> Result<(Self, DatabaseState, UpdatedAtStamper), OpenError> {
627        let mut conn = open_connection(path)?;
628        conn.pragma_update(None, "foreign_keys", "ON")
629            .map_err(DbError::from)?;
630
631        let initialized = match metadata_open {
632            CovenMetadataOpen::InitializeVerifiedSnapshot => false,
633            CovenMetadataOpen::Detect => {
634                let initialized = has_coven_initialization_marker(&conn)?;
635                if !initialized
636                    && !live_coven_schema_manifest(&conn)
637                        .map_err(DbError::from)?
638                        .is_empty()
639                {
640                    return Err(DbError::Message(
641                        "Store database contains Coven schema objects without the required initialization marker"
642                            .to_string(),
643                    )
644                    .into());
645                }
646                initialized
647            }
648        };
649        let pinned_routing_contract = initialized
650            .then(|| load_coven_metadata(&conn, write_policy))
651            .transpose()?;
652        if let Some(pinned) = &pinned_routing_contract {
653            validate_initialized_coven_schema(
654                &conn,
655                write_policy == WritePolicy::MergeConcurrent && pinned.has_scoped_graph(),
656            )?;
657        }
658        let (schema_version, sync_routing_contract, gates, blob_decls) = {
659            let tx = conn.transaction().map_err(DbError::from)?;
660            let outcome = (|| -> Result<_, OpenError> {
661                let schema_version = run_migrations_in_transaction(&tx, migrations)?;
662
663                // The host ladder and routing validation share this transaction.
664                // A pending migration that changes confidentiality topology cannot
665                // leave either its DDL or `user_version` advance committed.
666                validate_host_synced_tables(&tx, &synced_tables)?;
667                let resolved = SyncRoutingContract::from_connection(&tx, &synced_tables)
668                    .map_err(|error| DbError::Message(error.to_string()))?;
669                if let Some(pinned) = &pinned_routing_contract {
670                    validate_sync_routing_contract(pinned, &resolved)?;
671                    validate_initialized_coven_schema(
672                        &tx,
673                        write_policy == WritePolicy::MergeConcurrent && resolved.has_scoped_graph(),
674                    )?;
675                } else {
676                    initialize_coven_metadata_on(
677                        &tx,
678                        write_policy,
679                        &resolved,
680                        write_policy == WritePolicy::MergeConcurrent && resolved.has_scoped_graph(),
681                    )?;
682                }
683                pin_host_device_id_on(&tx, hlc.device_id(), initialized)?;
684                let gates = Gates::from_tables(&tx, &synced_tables)
685                    .map_err(|error| DbError::Message(error.to_string()))?;
686                let blob_decls = BlobDecls::from_tables(&tx, &synced_tables)
687                    .map_err(|error| DbError::Message(error.to_string()))?;
688                Ok((schema_version, resolved, gates, blob_decls))
689            })();
690            match outcome {
691                Ok(initialized) => {
692                    tx.commit().map_err(DbError::from)?;
693                    initialized
694                }
695                Err(error) => return Err(error),
696            }
697        };
698        let sync_routing_hash = sync_routing_contract.hash();
699        // Seed the register clock so a restart cannot mint a stamp behind a value
700        // already on disk. Floor = max(persisted high-water, max synced-row
701        // `_updated_at`).
702        let persisted = conn
703            .query_row(
704                "SELECT value FROM protocol_state WHERE key = ?1",
705                [HIGHWATER_STATE_KEY],
706                |r| r.get::<_, String>(0),
707            )
708            .optional()
709            .map_err(DbError::from)?;
710        seed_from(&hlc, persisted, "HLC high-water mark in protocol_state")?;
711        let seed_wall_ms = hlc.wall_now_ms();
712        let seed_bound_ms = seed_wall_ms.saturating_add(MAX_FUTURE_SKEW_MS);
713        let on_disk = scan_max_updated_at(&conn, &synced_tables, seed_bound_ms)?;
714        seed_from(&hlc, on_disk, "`_updated_at` in synced tables")?;
715
716        let stamper = UpdatedAtStamper::new(hlc.clone());
717        let synced_tables = Arc::new(synced_tables);
718        let gates = Arc::new(gates);
719        let blob_decls = Arc::new(blob_decls);
720        blob_decls
721            .install_cleanup_guards(&conn)
722            .map_err(|e| DbError::Message(e.to_string()))?;
723        gate::attach_empty_clone(&conn, &gates)
724            .map_err(|error| DbError::Message(format!("install host transaction gate: {error}")))?;
725        let core = DatabaseCore {
726            conn,
727            hlc,
728            synced_tables,
729            schema_version,
730            sync_routing_hash,
731            gates,
732            blob_decls,
733            blob_tombstone_grace,
734            transfer_limits,
735            write_policy,
736        };
737        let state = core.state();
738
739        Ok((core, state, stamper))
740    }
741
742    /// Open the connection at `path` read-only: a `SQLITE_OPEN_READONLY`
743    /// connection resolving the same gate/blob models a writer open resolves, but
744    /// running no migration ladder and no schema/bookkeeping writes. It refuses a
745    /// db a newer binary migrated past this one (the writer's `SchemaTooNew`
746    /// policy), since its models must understand the on-disk schema. Backs
747    /// [`Database::open_read_only`]; see it for why a reader takes no store lock.
748    fn open_read_only(
749        path: &Path,
750        synced_tables: Vec<SyncedTable>,
751        blob_tombstone_grace: chrono::Duration,
752        transfer_limits: crate::blob::TransferLimits,
753        write_policy: WritePolicy,
754        hlc: Arc<Hlc>,
755        migrations: &[Migration],
756    ) -> Result<(Self, DatabaseState), OpenError> {
757        let conn = open_connection_read_only(path)?;
758        // `foreign_keys` is a per-connection runtime setting, not a write to the db
759        // file, so it is allowed on a read-only connection; keeping it on matches the
760        // writer's relational view. A read never inserts, so it enforces nothing new.
761        conn.pragma_update(None, "foreign_keys", "ON")
762            .map_err(DbError::from)?;
763        // Open against the on-disk schema exactly as the writer left it: run no
764        // migration ladder (that writes), but refuse a schema newer than this binary
765        // knows — the same policy `run_migrations` applies — because the gate and blob
766        // models below are resolved against a schema this binary must understand.
767        let schema_version = crate::migration::ensure_schema_supported(&conn, migrations)?;
768
769        // Reads only (PRAGMA table_info): assert the host tables the writer created
770        // still present the synced-table contract, so a wrong schema fails loud at
771        // open rather than mid-read.
772        validate_host_synced_tables(&conn, &synced_tables)?;
773        let pinned_routing_contract = load_coven_metadata(&conn, write_policy)?;
774        validate_initialized_coven_schema(
775            &conn,
776            write_policy == WritePolicy::MergeConcurrent
777                && pinned_routing_contract.has_scoped_graph(),
778        )?;
779        validate_host_device_id_on(&conn, hlc.device_id())?;
780        let sync_routing_contract = SyncRoutingContract::from_connection(&conn, &synced_tables)
781            .map_err(|error| DbError::Message(error.to_string()))?;
782        validate_sync_routing_contract(&pinned_routing_contract, &sync_routing_contract)?;
783        let sync_routing_hash = sync_routing_contract.hash();
784
785        let synced_tables = Arc::new(synced_tables);
786
787        // No register-clock seeding: a reader never mints an `_updated_at`, so it has
788        // no stamp to keep ahead of on-disk values.
789        let gates = Arc::new(
790            Gates::from_tables(&conn, &synced_tables)
791                .map_err(|e| DbError::Message(e.to_string()))?,
792        );
793        let blob_decls = Arc::new(
794            BlobDecls::from_tables(&conn, &synced_tables)
795                .map_err(|e| DbError::Message(e.to_string()))?,
796        );
797        let core = DatabaseCore {
798            conn,
799            hlc,
800            synced_tables,
801            schema_version,
802            sync_routing_hash,
803            gates,
804            blob_decls,
805            blob_tombstone_grace,
806            transfer_limits,
807            write_policy,
808        };
809        let state = core.state();
810        Ok((core, state))
811    }
812
813    fn state(&self) -> DatabaseState {
814        DatabaseState {
815            write_policy: self.write_policy,
816            hlc: self.hlc.clone(),
817            synced_tables: self.synced_tables.clone(),
818            schema_version: self.schema_version,
819            sync_routing_hash: self.sync_routing_hash,
820            gates: self.gates.clone(),
821            blob_decls: self.blob_decls.clone(),
822            blob_tombstone_grace: self.blob_tombstone_grace,
823            transfer_limits: self.transfer_limits,
824            membership_load: Arc::new(tokio::sync::Mutex::new(())),
825            membership_mutation: Arc::new(tokio::sync::Mutex::new(())),
826            snapshot_publication: Arc::new(tokio::sync::Mutex::new(())),
827            local_blob_cleanup: Arc::new(tokio::sync::Mutex::new(())),
828            ids: Arc::new(crate::id_provider::UuidProvider),
829            write_statuses: Arc::new(std::sync::Mutex::new(HashMap::new())),
830            #[cfg(any(test, feature = "test-utils"))]
831            test_pause_points: Arc::new(TestPausePoints::default()),
832        }
833    }
834
835    fn connection(&self) -> &Connection {
836        &self.conn
837    }
838}
839
840/// A unit of work for the connection thread: a caller's closure to run against
841/// the owned core, or the sentinel the last [`Database`] clone sends as it drops
842/// to stop the thread. A `Run` closure carries its own reply channel, so it
843/// returns `()` — [`Database::on_connection_thread`] builds it to capture the
844/// caller's result and send it back.
845enum DbJob {
846    Run(Box<dyn FnOnce(&mut DatabaseCore) + Send>),
847    Stop,
848}
849
850/// The connection thread's send channel and join handle, shared by every
851/// [`Database`] clone through an `Arc`. The last clone to drop shuts the thread
852/// down and joins it, so the connection closes on the thread that owned it before
853/// control returns to the dropper.
854struct ConnectionThread {
855    jobs: tokio::sync::mpsc::UnboundedSender<DbJob>,
856    join: Option<std::thread::JoinHandle<()>>,
857}
858
859impl Drop for ConnectionThread {
860    fn drop(&mut self) {
861        // Reached only when the last `Database` clone drops — no other clone can
862        // still be sending. Queue `Stop` behind whatever jobs are already in
863        // flight so the thread drains them, exits, and drops the core (closing the
864        // connection). A send error means the thread already stopped.
865        let _ = self.jobs.send(DbJob::Stop);
866        let handle = match self.join.take() {
867            Some(handle) => handle,
868            None => return,
869        };
870        if tokio::runtime::Handle::try_current().is_ok() {
871            // A `Database` handle roams freely across async tasks, so this last
872            // drop can land inside a runtime task. Joining here would block that
873            // worker until the queue drains — the very stall this thread exists to
874            // remove. Detach instead: the thread drains its queue, drops the core,
875            // and exits on its own. Every queued job's effect is durable, so a
876            // thread left unjoined loses nothing; only the deterministic close
877            // moves off this task.
878            drop(handle);
879        } else {
880            // Sync context — tests, process teardown — where there is no worker to
881            // stall. Join for a deterministic shutdown: the connection is fully
882            // closed before we return. Jobs run under `catch_unwind`, so the thread
883            // never unwinds from a caller's closure; a join error is a real fault
884            // (a panic in the core's own drop) and is surfaced, not swallowed.
885            if handle.join().is_err() {
886                error!("database connection thread panicked");
887            }
888        }
889    }
890}
891
892/// Own the connection on this thread and run each queued job in send order until
893/// `Stop`. The channel's FIFO is the serialization the `tokio::Mutex` used to
894/// provide, and the connection never leaves this thread.
895fn run_connection_thread(
896    mut core: DatabaseCore,
897    mut jobs: tokio::sync::mpsc::UnboundedReceiver<DbJob>,
898) {
899    while let Some(job) = jobs.blocking_recv() {
900        match job {
901            DbJob::Run(f) => f(&mut core),
902            DbJob::Stop => break,
903        }
904    }
905    // Loop exited: drop `core` here — closing the connection on the thread that
906    // has owned it throughout.
907}
908
909/// A handle to the owned database. Cloneable; every clone sends work to the one
910/// connection thread over the same channel, so access serializes as the channel's
911/// FIFO.
912#[derive(Clone)]
913pub struct Database {
914    thread: Arc<ConnectionThread>,
915    state: DatabaseState,
916}
917
918pub(crate) struct PreparedStoreWrite {
919    pub write_id: WriteId,
920    pub changeset: Vec<u8>,
921    pub partitions: PreparedStoreWritePartitions,
922    pub inverse_changeset: Vec<u8>,
923    pub base: StoreWriteBase,
924    pub blob_facts: StoreWriteBlobFacts,
925}
926
927#[derive(Debug, Clone, PartialEq, Eq)]
928pub(crate) struct PreparedStoreWritePartitions {
929    pub store: Option<gate::AudiencePartition>,
930    pub circles: Vec<gate::AudiencePartition>,
931}
932
933#[derive(Clone, Copy)]
934enum StoreWriteRouting<'a> {
935    Unscoped,
936    MergeScoped(&'a EncryptionService),
937    SerialScoped,
938}
939
940impl PreparedStoreWritePartitions {
941    #[cfg(test)]
942    pub(crate) fn iter(&self) -> impl Iterator<Item = &gate::AudiencePartition> {
943        self.store.iter().chain(self.circles.iter())
944    }
945}
946
947pub(crate) struct SerialStoreBranchPreparationWork {
948    pub branch_id: PendingBranchId,
949    pub base: Option<StoreBatchCommitRef>,
950    pub writes: Vec<PreparedStoreWrite>,
951}
952
953#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
954#[serde(rename_all = "snake_case", deny_unknown_fields)]
955pub(crate) enum StoreWriteBase {
956    MergeConcurrent {
957        dependencies: BTreeMap<String, StoreBatchCommitRef>,
958    },
959    Serial {
960        branch_id: PendingBranchId,
961        base: Option<StoreBatchCommitRef>,
962    },
963}
964
965#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
966#[serde(deny_unknown_fields)]
967pub(crate) struct StoreWriteBlobFacts {
968    pub blobs: Vec<StoreWriteBlobFact>,
969}
970
971#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
972#[serde(deny_unknown_fields)]
973pub(crate) struct StoreWriteBlobFact {
974    pub table: String,
975    pub row_id: String,
976    pub row_stamp: String,
977    pub column: String,
978    pub blob: BlobRef,
979    pub plaintext_size: u64,
980    pub plaintext_hash: ObjectHash,
981    pub external_path: Option<PathBuf>,
982    pub previous: Option<StoreWriteRemoteBlob>,
983}
984
985#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
986#[serde(deny_unknown_fields)]
987pub(crate) struct StoreWriteRemoteBlob {
988    pub authority: crate::sync::audience_package::PackageAudience,
989    pub stored: StoredBlobRef,
990}
991
992impl StoreWriteBlobFact {
993    fn identity_key(&self) -> (String, String, String, String) {
994        (
995            self.table.clone(),
996            self.row_id.clone(),
997            self.column.clone(),
998            self.row_stamp.clone(),
999        )
1000    }
1001}
1002
1003#[derive(Debug, Clone)]
1004pub(crate) struct ExactProtocolObject<T> {
1005    pub value: T,
1006    pub bytes: Vec<u8>,
1007    pub object: ExactObjectRef,
1008    pub prepared: PreparedExactObject,
1009}
1010
1011#[derive(Debug, Clone)]
1012pub(crate) struct CanonicalProtocolObject<T> {
1013    pub value: T,
1014    pub bytes: Vec<u8>,
1015}
1016
1017pub(crate) struct PreparedProtocolObject<T> {
1018    pub value: T,
1019    pub prepared: PreparedExactObject,
1020}
1021
1022#[derive(Debug, Clone)]
1023pub(crate) struct PreparedStoreWriteCommit {
1024    pub audiences: PreparedAudienceObjects,
1025    pub commit: ExactProtocolObject<StoreBatchCommit>,
1026    pub head: ExactProtocolObject<StoreDeviceHead>,
1027}
1028
1029#[derive(Debug, Clone)]
1030pub(crate) struct BlockedMergeCandidate {
1031    pub commit: ExactProtocolObject<StoreBatchCommit>,
1032    pub head: ExactProtocolObject<StoreDeviceHead>,
1033}
1034
1035#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1036pub(crate) enum MergeAbandonmentState {
1037    None,
1038    Prepared,
1039    Accepted,
1040    CandidateWon,
1041    OtherWon,
1042}
1043
1044#[derive(Debug, Clone)]
1045pub(crate) struct PreparedSerialStoreWriteCommit {
1046    pub audiences: PreparedAudienceObjects,
1047    pub commit: ExactProtocolObject<StoreBatchCommit>,
1048}
1049
1050#[derive(Debug, Clone)]
1051pub(crate) struct PreparedSerialStoreBranch {
1052    pub branch_id: PendingBranchId,
1053    pub base: Option<StoreBatchCommitRef>,
1054    pub base_head_bytes: Option<Vec<u8>>,
1055    pub base_head_version: Option<VersionToken>,
1056    pub writes: Vec<PreparedSerialStoreWriteCommit>,
1057    pub head: CanonicalProtocolObject<StoreSerialHead>,
1058}
1059
1060#[derive(Debug, Clone)]
1061pub(crate) struct PreparedSerialCandidateAbandonment {
1062    pub branch_id: PendingBranchId,
1063    pub base: Option<StoreBatchCommitRef>,
1064    pub base_head: VersionedObject,
1065    pub authority: ExactProtocolObject<StoreBatchCommit>,
1066    pub head: CanonicalProtocolObject<StoreSerialHead>,
1067    pub original_head_bytes: Vec<u8>,
1068    durable_state: String,
1069}
1070
1071#[derive(Debug, Clone, PartialEq, Eq)]
1072pub enum SerialBranchDiscardState {
1073    Local,
1074    Abandonment,
1075    Conflict,
1076}
1077
1078#[derive(Debug, Clone)]
1079pub(crate) struct UnresolvedSerialBranch {
1080    pub branch_id: PendingBranchId,
1081    pub base: Option<StoreBatchCommitRef>,
1082    pub conflicted: bool,
1083}
1084
1085#[derive(Debug, Clone)]
1086pub(crate) struct OutboundStoreAck {
1087    pub reference: StoreAckRef,
1088    pub ack: ExactProtocolObject<StoreAck>,
1089}
1090
1091#[derive(Debug, Clone)]
1092pub(crate) struct PublishedStoreAck {
1093    pub reference: StoreAckRef,
1094    pub successor_slot: crate::storage::cloud::ObjectSlot,
1095}
1096
1097#[derive(Debug, Clone)]
1098pub(crate) struct DurableFounderGraph {
1099    pub root: ExactProtocolObject<StoreProtocolRoot>,
1100    pub registration: ExactProtocolObject<StoreDeviceRegistration>,
1101    pub initial_ack: ExactProtocolObject<StoreAck>,
1102    pub initial_ack_ref: StoreAckRef,
1103    pub membership: DurableFounderMembership,
1104    pub registration_state: LocalDeviceRegistrationState,
1105}
1106
1107#[derive(Debug, Clone)]
1108pub(crate) enum DurableFounderMembership {
1109    MergeConcurrent {
1110        entry: ExactProtocolObject<MembershipEntry>,
1111        entry_ref: MembershipEntryRef,
1112        head: ExactProtocolObject<AuthorHead>,
1113        head_ref: MembershipHeadRef,
1114    },
1115    Serial,
1116}
1117
1118#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1119#[serde(rename_all = "snake_case", deny_unknown_fields)]
1120enum DurableFounderMembershipJournal {
1121    MergeConcurrent {
1122        entry_ref: MembershipEntryRef,
1123        entry_bytes: Vec<u8>,
1124        entry_prepared: PreparedExactObject,
1125        head_ref: MembershipHeadRef,
1126        head_bytes: Vec<u8>,
1127        head_prepared: PreparedExactObject,
1128    },
1129    Serial,
1130}
1131
1132impl DurableFounderMembershipJournal {
1133    fn from_graph(graph: &DurableFounderMembership) -> Self {
1134        match graph {
1135            DurableFounderMembership::MergeConcurrent {
1136                entry,
1137                entry_ref,
1138                head,
1139                head_ref,
1140            } => Self::MergeConcurrent {
1141                entry_ref: entry_ref.clone(),
1142                entry_bytes: entry.bytes.clone(),
1143                entry_prepared: entry.prepared.clone(),
1144                head_ref: head_ref.clone(),
1145                head_bytes: head.bytes.clone(),
1146                head_prepared: head.prepared.clone(),
1147            },
1148            DurableFounderMembership::Serial => Self::Serial,
1149        }
1150    }
1151
1152    fn into_graph(self) -> Result<DurableFounderMembership, DbError> {
1153        match self {
1154            Self::MergeConcurrent {
1155                entry_ref,
1156                entry_bytes,
1157                entry_prepared,
1158                head_ref,
1159                head_bytes,
1160                head_prepared,
1161            } => {
1162                let entry_value: MembershipEntry =
1163                    serde_json::from_slice(&entry_bytes).map_err(|error| {
1164                        DbError::Message(format!("local founder membership entry: {error}"))
1165                    })?;
1166                let head_value: AuthorHead =
1167                    serde_json::from_slice(&head_bytes).map_err(|error| {
1168                        DbError::Message(format!("local founder membership head: {error}"))
1169                    })?;
1170                Ok(DurableFounderMembership::MergeConcurrent {
1171                    entry: ExactProtocolObject {
1172                        value: entry_value,
1173                        bytes: entry_bytes,
1174                        object: entry_prepared.reference().clone(),
1175                        prepared: entry_prepared,
1176                    },
1177                    entry_ref,
1178                    head: ExactProtocolObject {
1179                        value: head_value,
1180                        bytes: head_bytes,
1181                        object: head_prepared.reference().clone(),
1182                        prepared: head_prepared,
1183                    },
1184                    head_ref,
1185                })
1186            }
1187            Self::Serial => Ok(DurableFounderMembership::Serial),
1188        }
1189    }
1190}
1191
1192#[derive(Debug, Clone, PartialEq, Eq)]
1193pub(crate) enum FounderMembershipRefs {
1194    MergeConcurrent {
1195        entry: MembershipEntryRef,
1196        head: MembershipHeadRef,
1197    },
1198    Serial,
1199}
1200
1201fn founder_graph_identity(graph: &DurableFounderGraph) -> ObjectHash {
1202    let membership = match &graph.membership {
1203        DurableFounderMembership::MergeConcurrent {
1204            entry,
1205            entry_ref,
1206            head,
1207            head_ref,
1208        } => serde_json::to_vec(&(
1209            entry_ref,
1210            &entry.bytes,
1211            &entry.prepared,
1212            head_ref,
1213            &head.bytes,
1214            &head.prepared,
1215        )),
1216        DurableFounderMembership::Serial => serde_json::to_vec(&"serial"),
1217    }
1218    .expect("founder membership graph serialization cannot fail");
1219    ObjectHash::digest(
1220        &serde_json::to_vec(&(
1221            &graph.root.bytes,
1222            &graph.root.prepared,
1223            &graph.registration.bytes,
1224            &graph.registration.prepared,
1225            &graph.initial_ack_ref,
1226            &graph.initial_ack.bytes,
1227            &graph.initial_ack.prepared,
1228            membership,
1229        ))
1230        .expect("founder graph serialization cannot fail"),
1231    )
1232}
1233
1234fn load_store_root_authority_on(
1235    conn: &Connection,
1236) -> Result<Option<(crate::sync::store_commit::StoreRootRef, StoreProtocolRoot)>, DbError> {
1237    conn.query_row(
1238        "SELECT store_root_hash, store_protocol_root_bytes, store_root_object \
1239         FROM store_protocol_root_authority WHERE singleton = 1",
1240        [],
1241        |row| {
1242            Ok((
1243                row.get::<_, String>(0)?,
1244                row.get::<_, Vec<u8>>(1)?,
1245                row.get::<_, String>(2)?,
1246            ))
1247        },
1248    )
1249    .optional()
1250    .map_err(DbError::from)?
1251    .map(|(hash, bytes, object)| {
1252        let value = StoreProtocolRoot::parse(&bytes)
1253            .map_err(|error| DbError::Message(format!("Store root authority bytes: {error}")))?;
1254        let store_root_hash: ObjectHash = hash.parse().map_err(|error| {
1255            DbError::Message(format!("Store root authority semantic hash: {error}"))
1256        })?;
1257        let object: ExactObjectRef = serde_json::from_str(&object)
1258            .map_err(|error| DbError::Message(format!("Store root authority object: {error}")))?;
1259        if value.object_hash() != store_root_hash {
1260            return Err(DbError::Message(
1261                "Store root authority hash differs from its signed bytes".to_string(),
1262            ));
1263        }
1264        Ok((
1265            crate::sync::store_commit::StoreRootRef {
1266                store_root_id: value.descriptor.store_root_id(),
1267                store_root_hash,
1268                object,
1269            },
1270            value,
1271        ))
1272    })
1273    .transpose()
1274}
1275
1276fn required_store_root_authority_on(
1277    conn: &Connection,
1278) -> Result<crate::sync::store_commit::StoreRootRef, DbError> {
1279    load_store_root_authority_on(conn)?
1280        .map(|(reference, _)| reference)
1281        .ok_or_else(|| DbError::Message("exact Store root authority is absent".to_string()))
1282}
1283
1284fn install_store_root_authority_on(
1285    conn: &Connection,
1286    reference: &crate::sync::store_commit::StoreRootRef,
1287    bytes: &[u8],
1288) -> Result<(), DbError> {
1289    let value = StoreProtocolRoot::parse(bytes)
1290        .map_err(|error| DbError::Message(format!("install Store root authority: {error}")))?;
1291    if value.object_hash() != reference.store_root_hash {
1292        return Err(DbError::Message(
1293            "installed Store root reference differs from its signed bytes".to_string(),
1294        ));
1295    }
1296    let object = serde_json::to_string(&reference.object)
1297        .map_err(|error| DbError::Message(format!("serialize Store root authority: {error}")))?;
1298    let existing = load_store_root_authority_on(conn)?;
1299    if let Some((existing_reference, existing_value)) = existing {
1300        if existing_reference == *reference && existing_value == value {
1301            return Ok(());
1302        }
1303        return Err(DbError::Message(
1304            "database already trusts a different exact Store root".to_string(),
1305        ));
1306    }
1307    conn.execute(
1308        "INSERT INTO store_protocol_root_authority \
1309         (singleton, store_root_hash, store_protocol_root_bytes, store_root_object) \
1310         VALUES (1, ?1, ?2, ?3)",
1311        rusqlite::params![reference.store_root_hash.to_string(), bytes, object],
1312    )
1313    .map(|_| ())
1314    .map_err(DbError::from)
1315}
1316
1317fn install_store_founder_state_on(
1318    conn: &Connection,
1319    root: &crate::sync::store_commit::StoreRootRef,
1320    founder_reference: &StoreDeviceRegistrationRef,
1321    founder: &StoreDeviceRegistration,
1322    founder_bytes: &[u8],
1323    genesis: &ResolvedStoreDeviceState,
1324) -> Result<(), DbError> {
1325    if founder.store_root != *root {
1326        return Err(DbError::Message(
1327            "Store founder registration belongs to another exact root".to_string(),
1328        ));
1329    }
1330    founder_reference
1331        .verify_registration(founder)
1332        .map_err(|error| DbError::Message(error.to_string()))?;
1333    if founder.to_bytes() != founder_bytes {
1334        return Err(DbError::Message(
1335            "Store founder registration differs from its exact bytes".to_string(),
1336        ));
1337    }
1338    let founder_authority = crate::sync::store_commit::StoreDeviceRegistrationActivation::Founder {
1339        root: root.clone(),
1340    };
1341    let founder_values = (
1342        founder_reference.registration_hash.to_string(),
1343        founder.author_pubkey.clone(),
1344        founder.device_signing_pubkey.clone(),
1345        founder_bytes.to_vec(),
1346        serde_json::to_string(founder_reference).map_err(|error| {
1347            DbError::Message(format!("serialize Store founder registration ref: {error}"))
1348        })?,
1349        serde_json::to_string(&founder_authority).map_err(|error| {
1350            DbError::Message(format!("serialize Store founder activation: {error}"))
1351        })?,
1352    );
1353    conn.execute(
1354        "INSERT INTO store_device_registration_activations
1355         (device_id, registration_hash, author_pubkey, device_signing_pubkey,
1356          registration_bytes, registration_object, activation_authority)
1357         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
1358         ON CONFLICT(device_id) DO NOTHING",
1359        rusqlite::params![
1360            founder.device_id.to_string(),
1361            &founder_values.0,
1362            &founder_values.1,
1363            &founder_values.2,
1364            &founder_values.3,
1365            &founder_values.4,
1366            &founder_values.5,
1367        ],
1368    )
1369    .map_err(DbError::from)?;
1370    let stored_founder: (String, String, String, Vec<u8>, String, String) = conn
1371        .query_row(
1372            "SELECT registration_hash, author_pubkey, device_signing_pubkey,
1373                    registration_bytes, registration_object, activation_authority
1374             FROM store_device_registration_activations WHERE device_id = ?1",
1375            [founder.device_id.to_string()],
1376            |row| {
1377                Ok((
1378                    row.get(0)?,
1379                    row.get(1)?,
1380                    row.get(2)?,
1381                    row.get(3)?,
1382                    row.get(4)?,
1383                    row.get(5)?,
1384                ))
1385            },
1386        )
1387        .map_err(DbError::from)?;
1388    if stored_founder != founder_values {
1389        return Err(DbError::Message(
1390            "Store founder activation differs from installed exact authority".to_string(),
1391        ));
1392    }
1393    let genesis = serde_json::to_string(genesis)
1394        .map_err(|error| DbError::Message(format!("serialize Store device genesis: {error}")))?;
1395    conn.execute(
1396        "INSERT OR IGNORE INTO protocol_state (key, value) VALUES (?1, ?2)",
1397        (STORE_DEVICE_GENESIS_STATE_KEY, &genesis),
1398    )
1399    .map_err(DbError::from)?;
1400    let stored_genesis: String = conn
1401        .query_row(
1402            "SELECT value FROM protocol_state WHERE key = ?1",
1403            [STORE_DEVICE_GENESIS_STATE_KEY],
1404            |row| row.get(0),
1405        )
1406        .map_err(DbError::from)?;
1407    if stored_genesis != genesis {
1408        return Err(DbError::Message(
1409            "Store device genesis differs from installed exact authority".to_string(),
1410        ));
1411    }
1412    Ok(())
1413}
1414
1415fn validate_founder_graph(graph: &DurableFounderGraph) -> Result<(), DbError> {
1416    let root = StoreProtocolRoot::parse(&graph.root.bytes)
1417        .map_err(|error| DbError::Message(format!("founder Store root: {error}")))?;
1418    if root != graph.root.value
1419        || root.object_hash() != graph.root.value.object_hash()
1420        || graph.root.object != *graph.root.prepared.reference()
1421    {
1422        return Err(DbError::Message(
1423            "founder Store root differs from its prepared exact object".to_string(),
1424        ));
1425    }
1426    let root_ref = crate::sync::store_commit::StoreRootRef {
1427        store_root_id: root.descriptor.store_root_id(),
1428        store_root_hash: root.object_hash(),
1429        object: graph.root.object.clone(),
1430    };
1431    let registration = StoreDeviceRegistration::parse_at(
1432        &graph.registration.bytes,
1433        &root_ref,
1434        graph.registration.value.device_id,
1435    )
1436    .map_err(|error| DbError::Message(format!("founder Store registration: {error}")))?;
1437    if registration != graph.registration.value
1438        || graph.registration.object != *graph.registration.prepared.reference()
1439        || registration.author_pubkey != root.descriptor.founder_pubkey
1440        || graph.registration.object.slot() != &root.descriptor.founder_registration
1441        || registration.provider != root.descriptor.founder_provider_admin.provider
1442        || !matches!(
1443            registration.origin,
1444            crate::sync::store_commit::StoreDeviceRegistrationOrigin::Founder { .. }
1445        )
1446    {
1447        return Err(DbError::Message(
1448            "founder registration differs from its root or prepared exact object".to_string(),
1449        ));
1450    }
1451    let registration_ref = StoreDeviceRegistrationRef::from_registration(
1452        &registration,
1453        graph.registration.object.clone(),
1454    );
1455    let initial_ack = StoreAck::parse_at(
1456        &graph.initial_ack.bytes,
1457        root_ref.store_root_hash,
1458        &graph.initial_ack_ref,
1459        &registration,
1460    )
1461    .map_err(|error| DbError::Message(format!("founder initial acknowledgement: {error}")))?;
1462    if initial_ack != graph.initial_ack.value
1463        || graph.initial_ack_ref.revision != 1
1464        || graph.initial_ack_ref.object != graph.initial_ack.object
1465        || graph.initial_ack.object != *graph.initial_ack.prepared.reference()
1466        || initial_ack.predecessor.is_some()
1467        || initial_ack.author_registration != registration_ref
1468        || match (&root.descriptor.write_policy, &initial_ack.store_cut) {
1469            (
1470                crate::WritePolicy::MergeConcurrent,
1471                crate::sync::store_commit::StoreHistoryCut::MergeConcurrent(commits),
1472            ) => !commits.is_empty(),
1473            (
1474                crate::WritePolicy::Serial,
1475                crate::sync::store_commit::StoreHistoryCut::Serial(
1476                    crate::sync::store_commit::StoreSerialPredecessor::Genesis {
1477                        root: ack_root,
1478                        founder_registration,
1479                    },
1480                ),
1481            ) => ack_root != &root_ref || founder_registration != &registration_ref,
1482            _ => true,
1483        }
1484    {
1485        return Err(DbError::Message(
1486            "founder initial acknowledgement differs from its exact root graph".to_string(),
1487        ));
1488    }
1489    match (&root.descriptor.membership, &graph.membership) {
1490        (
1491            crate::sync::store_commit::StoreMembershipGenesis::MergeConcurrent { .. },
1492            DurableFounderMembership::MergeConcurrent {
1493                entry,
1494                entry_ref,
1495                head,
1496                head_ref,
1497            },
1498        ) => {
1499            let parsed_entry: MembershipEntry = serde_json::from_slice(&entry.bytes)
1500                .map_err(|error| DbError::Message(format!("founder membership entry: {error}")))?;
1501            if parsed_entry != entry.value
1502                || root
1503                    .descriptor
1504                    .validate_merge_founder_entry(&parsed_entry)
1505                    .is_err()
1506                || entry_ref.coord != parsed_entry.coord()
1507                || entry_ref.object != entry.object
1508                || entry.object != *entry.prepared.reference()
1509            {
1510                return Err(DbError::Message(
1511                    "founder membership entry differs from its root or exact reference".to_string(),
1512                ));
1513            }
1514            let parsed_head: AuthorHead = serde_json::from_slice(&head.bytes)
1515                .map_err(|error| DbError::Message(format!("founder membership head: {error}")))?;
1516            let anchor = parsed_entry.change.membership_anchor().ok_or_else(|| {
1517                DbError::Message("founder entry has no Store membership anchor".to_string())
1518            })?;
1519            let crate::sync::store_commit::GrantStreamAnchor::StoreMembership { first_slot } =
1520                anchor
1521            else {
1522                return Err(DbError::Message(
1523                    "founder membership entry uses a recovery anchor".to_string(),
1524                ));
1525            };
1526            if parsed_head != head.value
1527                || !parsed_head.verify(&registration)
1528                || parsed_head.author_registration != registration_ref
1529                || parsed_head.entry != *entry_ref
1530                || parsed_head.predecessor.is_some()
1531                || parsed_head.entry_coord() != parsed_entry.coord()
1532                || head_ref.coord != parsed_entry.coord()
1533                || head_ref.head_hash != parsed_head.head_hash()
1534                || head_ref.object != head.object
1535                || head.object != *head.prepared.reference()
1536                || head.object.slot() != &first_slot
1537                || parsed_head.successor.activation
1538                    != StreamActivationId::store_membership(
1539                        &root_ref,
1540                        &registration_ref,
1541                        &parsed_entry.author_owner_grant,
1542                        &crate::sync::store_commit::GrantStreamAnchor::StoreMembership {
1543                            first_slot: first_slot.clone(),
1544                        },
1545                    )
1546            {
1547                return Err(DbError::Message(
1548                    "founder membership head differs from its exact root graph".to_string(),
1549                ));
1550            }
1551        }
1552        (
1553            crate::sync::store_commit::StoreMembershipGenesis::Serial,
1554            DurableFounderMembership::Serial,
1555        ) => {}
1556        _ => {
1557            return Err(DbError::Message(
1558                "founder membership graph differs from the root policy".to_string(),
1559            ))
1560        }
1561    }
1562    Ok(())
1563}
1564
1565fn consume_store_creation_probes_on(
1566    conn: &Connection,
1567    graph: &DurableFounderGraph,
1568) -> Result<(), DbError> {
1569    use crate::sync::provider::{
1570        ExactProbeProgress, ProviderProbeJournalRecord, SerialProbeProgress,
1571    };
1572    use crate::sync::store_protocol_root::{
1573        StoreCreationAttempt, StoreCreationProbeIds, STORE_CREATION_ATTEMPT_STATE_KEY,
1574    };
1575
1576    let attempt_json: String = conn
1577        .query_row(
1578            "SELECT value FROM protocol_state WHERE key = ?1",
1579            [STORE_CREATION_ATTEMPT_STATE_KEY],
1580            |row| row.get(0),
1581        )
1582        .map_err(DbError::from)?;
1583    let attempt: StoreCreationAttempt = serde_json::from_str(&attempt_json)
1584        .map_err(|error| DbError::Message(format!("parse Store creation attempt: {error}")))?;
1585    let StoreCreationAttempt::FounderGraphReserved(graph_reservation) = attempt else {
1586        return Err(DbError::Message(
1587            "Store creation attempt has not reserved the complete founder graph".to_string(),
1588        ));
1589    };
1590    let reservation = &graph_reservation.descriptor;
1591    let descriptor = &graph.root.value.descriptor;
1592    let founder = reservation.membership.founder();
1593    let authority = &founder.root.authority;
1594    if authority.creation_id != descriptor.creation_id
1595        || authority.founder_grant != descriptor.founder_grant
1596        || authority.provider_admin_grant != descriptor.founder_provider_admin.grant_id
1597        || authority.binding.store != descriptor.provider
1598        || authority.binding.device != descriptor.founder_provider_admin.provider
1599        || authority.founder_pubkey != descriptor.founder_pubkey
1600        || authority.write_policy != descriptor.write_policy
1601        || authority.schema_version != descriptor.schema_version
1602        || authority.sync_routing_hash != descriptor.sync_routing_hash
1603        || founder.root.root_slot != descriptor.root_slot
1604        || founder.registration_slot != descriptor.founder_registration
1605        || &reservation.recovery_slot != descriptor.founder_recovery.first_slot()
1606        || match (&reservation.membership, &descriptor.membership) {
1607            (
1608                crate::sync::store_protocol_root::MembershipReservation::MergeConcurrent {
1609                    first_slot,
1610                    ..
1611                },
1612                crate::sync::store_commit::StoreMembershipGenesis::MergeConcurrent {
1613                    founder_membership,
1614                },
1615            ) => founder_membership.first_slot() != first_slot,
1616            (
1617                crate::sync::store_protocol_root::MembershipReservation::Serial { .. },
1618                crate::sync::store_commit::StoreMembershipGenesis::Serial,
1619            ) => false,
1620            _ => true,
1621        }
1622    {
1623        return Err(DbError::Message(
1624            "signed Store descriptor differs from its durable creation attempt".to_string(),
1625        ));
1626    }
1627    if graph.registration.value.store_commits != graph_reservation.store_commits
1628        || graph.registration.value.acknowledgements != graph_reservation.acknowledgements
1629        || graph.registration.value.snapshots != graph_reservation.snapshots
1630        || graph.initial_ack.value.last_sync != authority.founder_timestamp
1631        || graph.initial_ack.value.successor.next_slot != graph_reservation.next_ack_slot
1632        || match (&graph.membership, &graph_reservation.membership) {
1633            (
1634                DurableFounderMembership::MergeConcurrent { entry, head, .. },
1635                crate::sync::store_protocol_root::FounderMembershipPublicationReservation::MergeConcurrent {
1636                    next_head_slot,
1637                },
1638            ) => {
1639                entry.value.created_at != authority.founder_timestamp
1640                    || head.value.successor.next_slot != *next_head_slot
1641            }
1642            (
1643                DurableFounderMembership::Serial,
1644                crate::sync::store_protocol_root::FounderMembershipPublicationReservation::Serial,
1645            ) => false,
1646            _ => true,
1647        }
1648    {
1649        return Err(DbError::Message(
1650            "signed founder graph differs from its durable slot reservation".to_string(),
1651        ));
1652    }
1653
1654    let load_probe = |probe_id: crate::sync::provider::ProviderProbeId| {
1655        let key = format!("provider_probe/{}", hex::encode(probe_id.as_bytes()));
1656        let value: String = conn
1657            .query_row(
1658                "SELECT value FROM protocol_state WHERE key = ?1",
1659                [&key],
1660                |row| row.get(0),
1661            )
1662            .map_err(DbError::from)?;
1663        let record = serde_json::from_str(&value)
1664            .map_err(|error| DbError::Message(format!("parse provider probe journal: {error}")))?;
1665        Ok::<_, DbError>((key, record))
1666    };
1667    let (exact_id, serial_id) = match authority.probes {
1668        StoreCreationProbeIds::MergeConcurrent { exact_slots } => (exact_slots, None),
1669        StoreCreationProbeIds::Serial {
1670            exact_slots,
1671            serial_coordination,
1672        } => (exact_slots, Some(serial_coordination)),
1673    };
1674    let (exact_key, exact) = load_probe(exact_id)?;
1675    let ProviderProbeJournalRecord::Exact(exact) = exact else {
1676        return Err(DbError::Message(
1677            "Store creation exact probe id names another probe kind".to_string(),
1678        ));
1679    };
1680    let ExactProbeProgress::ReceiptReady { receipt } = exact.progress else {
1681        return Err(DbError::Message(
1682            "Store creation exact probe has no terminal receipt".to_string(),
1683        ));
1684    };
1685    if receipt != descriptor.founder_provider_admin.capability.exact_slots {
1686        return Err(DbError::Message(
1687            "signed Store descriptor differs from its terminal exact probe".to_string(),
1688        ));
1689    }
1690    let mut consumed_keys = vec![exact_key];
1691    match (
1692        serial_id,
1693        &descriptor
1694            .founder_provider_admin
1695            .capability
1696            .serial_coordination,
1697    ) {
1698        (None, None) => {}
1699        (Some(probe_id), Some(expected)) => {
1700            let (key, serial) = load_probe(probe_id)?;
1701            let ProviderProbeJournalRecord::Serial(serial) = serial else {
1702                return Err(DbError::Message(
1703                    "Store creation serial probe id names another probe kind".to_string(),
1704                ));
1705            };
1706            let SerialProbeProgress::ReceiptReady { receipt } = serial.progress else {
1707                return Err(DbError::Message(
1708                    "Store creation serial probe has no terminal receipt".to_string(),
1709                ));
1710            };
1711            if &receipt != expected {
1712                return Err(DbError::Message(
1713                    "signed Store descriptor differs from its terminal serial probe".to_string(),
1714                ));
1715            }
1716            consumed_keys.push(key);
1717        }
1718        _ => {
1719            return Err(DbError::Message(
1720                "Store creation probe policy differs from the signed descriptor".to_string(),
1721            ))
1722        }
1723    }
1724    consumed_keys.push(STORE_CREATION_ATTEMPT_STATE_KEY.to_string());
1725    for key in consumed_keys {
1726        let deleted = conn
1727            .execute("DELETE FROM protocol_state WHERE key = ?1", [key])
1728            .map_err(DbError::from)?;
1729        if deleted != 1 {
1730            return Err(DbError::Message(
1731                "Store creation journal disappeared during typed consumption".to_string(),
1732            ));
1733        }
1734    }
1735    Ok(())
1736}
1737
1738fn load_local_store_founder_graph_on(
1739    conn: &Connection,
1740) -> Result<Option<DurableFounderGraph>, DbError> {
1741    let owned_rows: i64 = conn
1742        .query_row(
1743            "SELECT EXISTS(SELECT 1 FROM local_store_protocol_root) \
1744                  + EXISTS(SELECT 1 FROM local_store_device_registration) \
1745                  + EXISTS(SELECT 1 FROM local_store_founder_graph)",
1746            [],
1747            |row| row.get(0),
1748        )
1749        .map_err(DbError::from)?;
1750    if owned_rows == 0 {
1751        return Ok(None);
1752    }
1753    if owned_rows != 3 {
1754        return Err(DbError::Message(
1755            "local Store founder graph is only partially durable".to_string(),
1756        ));
1757    }
1758    let raw = conn
1759        .query_row(
1760            "SELECT r.store_root_hash, r.store_protocol_root_bytes, r.prepared_object, \
1761                    d.device_id, d.registration_hash, d.registration_bytes, d.prepared_object, \
1762                    d.initial_ack_ref, d.initial_ack_bytes, d.initial_ack_prepared, d.state, \
1763                    g.membership_graph \
1764             FROM local_store_protocol_root r \
1765             CROSS JOIN local_store_device_registration d \
1766             CROSS JOIN local_store_founder_graph g \
1767             WHERE r.singleton = 1 AND d.singleton = 1 AND g.singleton = 1",
1768            [],
1769            |row| {
1770                Ok((
1771                    row.get::<_, String>(0)?,
1772                    row.get::<_, Vec<u8>>(1)?,
1773                    row.get::<_, String>(2)?,
1774                    row.get::<_, String>(3)?,
1775                    row.get::<_, String>(4)?,
1776                    row.get::<_, Vec<u8>>(5)?,
1777                    row.get::<_, String>(6)?,
1778                    row.get::<_, String>(7)?,
1779                    row.get::<_, Vec<u8>>(8)?,
1780                    row.get::<_, String>(9)?,
1781                    row.get::<_, String>(10)?,
1782                    row.get::<_, String>(11)?,
1783                ))
1784            },
1785        )
1786        .map_err(DbError::from)?;
1787    let (
1788        root_hash,
1789        root_bytes,
1790        root_prepared,
1791        device_id,
1792        registration_hash,
1793        registration_bytes,
1794        registration_prepared,
1795        initial_ack_ref,
1796        initial_ack_bytes,
1797        initial_ack_prepared,
1798        registration_state,
1799        membership_graph,
1800    ) = raw;
1801    let registration_state: LocalDeviceRegistrationState =
1802        serde_json::from_str(&registration_state).map_err(|error| {
1803            DbError::Message(format!("local registration journal state: {error}"))
1804        })?;
1805    let root_value = StoreProtocolRoot::parse(&root_bytes)
1806        .map_err(|error| DbError::Message(format!("local founder Store root: {error}")))?;
1807    let root_prepared: PreparedExactObject = serde_json::from_str(&root_prepared)
1808        .map_err(|error| DbError::Message(format!("local founder Store root object: {error}")))?;
1809    let store_root_hash: ObjectHash = root_hash
1810        .parse()
1811        .map_err(|error| DbError::Message(format!("local founder Store root hash: {error}")))?;
1812    if store_root_hash != root_value.object_hash() {
1813        return Err(DbError::Message(
1814            "local founder Store root hash differs from its bytes".to_string(),
1815        ));
1816    }
1817    let root_ref = crate::sync::store_commit::StoreRootRef {
1818        store_root_id: root_value.descriptor.store_root_id(),
1819        store_root_hash,
1820        object: root_prepared.reference().clone(),
1821    };
1822    let parsed_device_id = device_id
1823        .parse()
1824        .map_err(|error| DbError::Message(format!("local founder device id: {error}")))?;
1825    let registration_value =
1826        StoreDeviceRegistration::parse_at(&registration_bytes, &root_ref, parsed_device_id)
1827            .map_err(|error| {
1828                DbError::Message(format!("local founder Store registration: {error}"))
1829            })?;
1830    let parsed_registration_hash: ObjectHash = registration_hash.parse().map_err(|error| {
1831        DbError::Message(format!("local founder Store registration hash: {error}"))
1832    })?;
1833    if parsed_registration_hash != registration_value.registration_hash() {
1834        return Err(DbError::Message(
1835            "local founder registration hash differs from its bytes".to_string(),
1836        ));
1837    }
1838    let registration_prepared: PreparedExactObject = serde_json::from_str(&registration_prepared)
1839        .map_err(|error| {
1840        DbError::Message(format!("local founder registration object: {error}"))
1841    })?;
1842    let initial_ack_ref: StoreAckRef = serde_json::from_str(&initial_ack_ref)
1843        .map_err(|error| DbError::Message(format!("local founder initial ack ref: {error}")))?;
1844    let initial_ack_value = StoreAck::parse_at(
1845        &initial_ack_bytes,
1846        root_ref.store_root_hash,
1847        &initial_ack_ref,
1848        &registration_value,
1849    )
1850    .map_err(|error| DbError::Message(format!("local founder initial ack: {error}")))?;
1851    let initial_ack_prepared: PreparedExactObject = serde_json::from_str(&initial_ack_prepared)
1852        .map_err(|error| DbError::Message(format!("local founder initial ack object: {error}")))?;
1853    let membership = serde_json::from_str::<DurableFounderMembershipJournal>(&membership_graph)
1854        .map_err(|error| DbError::Message(format!("local founder membership graph: {error}")))?
1855        .into_graph()?;
1856    let graph = DurableFounderGraph {
1857        root: ExactProtocolObject {
1858            value: root_value,
1859            bytes: root_bytes,
1860            object: root_prepared.reference().clone(),
1861            prepared: root_prepared,
1862        },
1863        registration: ExactProtocolObject {
1864            value: registration_value,
1865            bytes: registration_bytes,
1866            object: registration_prepared.reference().clone(),
1867            prepared: registration_prepared,
1868        },
1869        initial_ack: ExactProtocolObject {
1870            value: initial_ack_value,
1871            bytes: initial_ack_bytes,
1872            object: initial_ack_prepared.reference().clone(),
1873            prepared: initial_ack_prepared,
1874        },
1875        initial_ack_ref,
1876        membership,
1877        registration_state,
1878    };
1879    validate_founder_graph(&graph)?;
1880    Ok(Some(graph))
1881}
1882
1883/// The exact commit coordinate that first made a blob locator authoritative.
1884#[derive(Debug, Clone, PartialEq, Eq)]
1885pub(crate) struct BlobActivation {
1886    pub coord: StoreCommitCoord,
1887}
1888
1889#[derive(Debug, Clone, PartialEq, Eq)]
1890pub(crate) struct PreparedAudiencePackage {
1891    remote_object_id: ObjectHash,
1892    package: AudiencePackage,
1893    semantic_bytes: Vec<u8>,
1894    stored_bytes: Vec<u8>,
1895    object: ExactObjectRef,
1896}
1897
1898impl PreparedAudiencePackage {
1899    fn from_remote(remote: RemoteObjectRecord) -> Result<Self, DbError> {
1900        remote
1901            .validate()
1902            .map_err(|error| DbError::Message(format!("prepared remote package: {error}")))?;
1903        let is_package = match &remote {
1904            RemoteObjectRecord::CandidateCommit(_) | RemoteObjectRecord::RetainedAuthority(_) => {
1905                false
1906            }
1907            RemoteObjectRecord::CandidateExclusive(record) => matches!(
1908                record.identity.domain,
1909                CandidateExclusiveObjectDomain::StorePackage
1910                    | CandidateExclusiveObjectDomain::CirclePackage { .. }
1911            ),
1912            RemoteObjectRecord::SharedLiveSet(record) => matches!(
1913                record.identity.domain,
1914                SharedLiveSetObjectDomain::StorePackage | SharedLiveSetObjectDomain::CirclePackage
1915            ),
1916        };
1917        if !is_package {
1918            return Err(DbError::Message(
1919                "prepared package index references a non-package remote object".to_string(),
1920            ));
1921        }
1922        let remote_object_id = remote.object_id();
1923        let semantic_bytes = remote.bytes().canonical_semantic_bytes().to_vec();
1924        let object = remote.object().clone();
1925        let stored_bytes = remote
1926            .bytes()
1927            .stored()
1928            .inline_bytes()
1929            .ok_or_else(|| {
1930                DbError::Message("prepared package remote object is not inline".to_string())
1931            })?
1932            .to_vec();
1933        Self::new(remote_object_id, semantic_bytes, stored_bytes, object)
1934    }
1935
1936    pub(crate) fn new(
1937        remote_object_id: ObjectHash,
1938        semantic_bytes: Vec<u8>,
1939        stored_bytes: Vec<u8>,
1940        object: ExactObjectRef,
1941    ) -> Result<Self, DbError> {
1942        let package = AudiencePackage::parse(&semantic_bytes)
1943            .map_err(|error| DbError::Message(format!("prepared audience package: {error}")))?;
1944        object.verify(&stored_bytes).map_err(|error| {
1945            DbError::Message(format!("prepared audience package stored bytes: {error}"))
1946        })?;
1947        Ok(Self {
1948            remote_object_id,
1949            package,
1950            semantic_bytes,
1951            stored_bytes,
1952            object,
1953        })
1954    }
1955
1956    pub(crate) fn remote_object_id(&self) -> ObjectHash {
1957        self.remote_object_id
1958    }
1959
1960    pub(crate) fn package(&self) -> &AudiencePackage {
1961        &self.package
1962    }
1963
1964    pub(crate) fn semantic_bytes(&self) -> &[u8] {
1965        &self.semantic_bytes
1966    }
1967
1968    pub(crate) fn stored_bytes(&self) -> &[u8] {
1969        &self.stored_bytes
1970    }
1971
1972    pub(crate) fn object(&self) -> &ExactObjectRef {
1973        &self.object
1974    }
1975}
1976
1977#[derive(Debug, Clone, PartialEq, Eq)]
1978pub(crate) struct PreparedAudienceBlob {
1979    remote_object_id: ObjectHash,
1980    audience: RemoteAudience,
1981    blob: StoredBlobRef,
1982    spool_path: Option<PathBuf>,
1983}
1984
1985impl PreparedAudienceBlob {
1986    pub(crate) fn from_remote(
1987        audience: RemoteAudience,
1988        expected_locator_hash: &str,
1989        remote: RemoteObjectRecord,
1990        spool_path: Option<PathBuf>,
1991    ) -> Result<Self, DbError> {
1992        remote
1993            .validate()
1994            .map_err(|error| DbError::Message(format!("prepared remote blob: {error}")))?;
1995        if !matches!(
1996            &remote,
1997            RemoteObjectRecord::SharedLiveSet(record)
1998                if record.identity.domain == SharedLiveSetObjectDomain::StoredBlob
1999        ) {
2000            return Err(DbError::Message(
2001                "prepared blob index references a non-blob remote object".to_string(),
2002            ));
2003        }
2004        let locator = BlobLocator::parse(remote.bytes().canonical_semantic_bytes())
2005            .map_err(|error| DbError::Message(format!("prepared blob locator: {error}")))?;
2006        if locator.locator_hash().to_string() != expected_locator_hash {
2007            return Err(DbError::Message(format!(
2008                "prepared blob locator hashes to {}, indexed as {expected_locator_hash}",
2009                locator.locator_hash()
2010            )));
2011        }
2012        if locator.audience() != audience {
2013            return Err(DbError::Message(format!(
2014                "prepared blob index audience {audience:?} differs from locator audience {:?}",
2015                locator.audience()
2016            )));
2017        }
2018        let requires_upload = matches!(
2019            &remote,
2020            RemoteObjectRecord::SharedLiveSet(record)
2021                if matches!(record.state, crate::sync::remote_object::OwnedObjectState::Prepared { .. })
2022        );
2023        if requires_upload && spool_path.is_none() {
2024            return Err(DbError::Message(
2025                "prepared blob awaiting upload has no local spool".to_string(),
2026            ));
2027        }
2028        if spool_path.as_ref().is_some_and(|path| !path.is_absolute()) {
2029            return Err(DbError::Message(
2030                "prepared blob local spool path is not absolute".to_string(),
2031            ));
2032        }
2033        let blob = StoredBlobRef::new(locator, remote.object().clone())
2034            .map_err(|error| DbError::Message(format!("prepared blob reference: {error}")))?;
2035        Ok(Self {
2036            remote_object_id: remote.object_id(),
2037            audience,
2038            blob,
2039            spool_path,
2040        })
2041    }
2042
2043    pub(crate) fn remote_object_id(&self) -> ObjectHash {
2044        self.remote_object_id
2045    }
2046
2047    pub(crate) fn audience(&self) -> &RemoteAudience {
2048        &self.audience
2049    }
2050
2051    pub(crate) fn blob(&self) -> &StoredBlobRef {
2052        &self.blob
2053    }
2054
2055    pub(crate) fn spool_path(&self) -> Option<&Path> {
2056        self.spool_path.as_deref()
2057    }
2058}
2059
2060#[derive(Debug, Clone)]
2061pub(crate) struct PreparedAudienceObjects {
2062    pub packages: Vec<PreparedAudiencePackage>,
2063    pub blobs: Vec<PreparedAudienceBlob>,
2064}
2065
2066pub(crate) struct PreparedRemoteObject {
2067    pub record: RemoteObjectRecord,
2068    pub spool_path: Option<PathBuf>,
2069}
2070
2071#[derive(Debug, Clone, PartialEq, Eq)]
2072pub(crate) enum MakeRemoteIntentState {
2073    Uploading,
2074    Cancelling,
2075    Publishing(WriteId),
2076}
2077
2078#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2079pub(crate) enum StoredBlobReferenceState {
2080    NotLiveRemote,
2081    LiveRemote,
2082    Unresolved,
2083}
2084
2085fn validate_prepared_audience_blob_graph(
2086    object_ids: &std::collections::BTreeSet<ObjectHash>,
2087    audiences: &PreparedAudienceObjects,
2088) -> Result<(), DbError> {
2089    let mut indexed = std::collections::BTreeSet::new();
2090    for package in &audiences.packages {
2091        if !indexed.insert(package.remote_object_id()) {
2092            return Err(DbError::Message(
2093                "prepared audience objects contain a duplicate package body".to_string(),
2094            ));
2095        }
2096    }
2097    for blob in &audiences.blobs {
2098        if !indexed.insert(blob.remote_object_id()) {
2099            return Err(DbError::Message(
2100                "prepared audience objects contain a duplicate blob body".to_string(),
2101            ));
2102        }
2103    }
2104    if &indexed != object_ids {
2105        return Err(DbError::Message(
2106            "closed remote objects differ from package/blob indexes".to_string(),
2107        ));
2108    }
2109    validate_prepared_audience_blob_bindings(audiences)
2110}
2111
2112fn validate_prepared_audience_blob_bindings(
2113    audiences: &PreparedAudienceObjects,
2114) -> Result<(), DbError> {
2115    for package in &audiences.packages {
2116        let audience = package.package().audience().remote_audience();
2117        for binding in package.package().blob_bindings() {
2118            if !audiences
2119                .blobs
2120                .iter()
2121                .any(|blob| blob.audience() == &audience && blob.blob() == binding.blob())
2122            {
2123                return Err(DbError::Message(
2124                    "prepared package blob binding has no exact blob index".to_string(),
2125                ));
2126            }
2127        }
2128    }
2129    for blob in &audiences.blobs {
2130        if !audiences.packages.iter().any(|package| {
2131            package.package().audience().remote_audience() == *blob.audience()
2132                && package
2133                    .package()
2134                    .blob_bindings()
2135                    .iter()
2136                    .any(|binding| binding.blob() == blob.blob())
2137        }) {
2138            return Err(DbError::Message(
2139                "prepared blob index has no exact package binding".to_string(),
2140            ));
2141        }
2142    }
2143    Ok(())
2144}
2145
2146#[cfg(feature = "invariant-tests")]
2147#[doc(hidden)]
2148pub fn exercise_exact_outbound_blob_graph(
2149    circle: bool,
2150    include_body: bool,
2151    include_locator: bool,
2152    include_binding: bool,
2153) -> Result<(), String> {
2154    use crate::blob::BlobScope;
2155    use crate::storage::cloud::ObjectSlot;
2156    use crate::sync::audience_package::RowBlobLocatorBinding;
2157    use crate::sync::circle::CircleId;
2158    use crate::sync::circle_control::CircleControlCoord;
2159    use crate::sync::store_commit::{CandidateFamilyId, StoreCommitCoord};
2160
2161    let store_root_hash = ObjectHash::digest(b"outbound-graph-store");
2162    let write_id = WriteId::from_generated("outbound-graph-write".to_string());
2163    let coord = StoreCommitCoord::Serial { sequence: 1 };
2164    let candidate_family = CandidateFamilyId::from_hash(ObjectHash::digest(b"outbound-family"));
2165    let remote_audience = if circle {
2166        RemoteAudience::Circle(CircleId::from_bytes([7; 16]))
2167    } else {
2168        RemoteAudience::Store
2169    };
2170    let uploader_bytes = b"outbound graph uploader registration";
2171    let uploader = StoreDeviceRegistrationRef {
2172        device_id: "01"
2173            .repeat(32)
2174            .parse::<crate::sync::store_commit::StoreDeviceId>()
2175            .map_err(|error| error.to_string())?,
2176        registration_hash: ObjectHash::digest(uploader_bytes),
2177        object: ExactObjectRef::new(
2178            ObjectSlot::logical("store-v1/registrations/outbound-graph.json".to_string())
2179                .map_err(|error| error.to_string())?,
2180            uploader_bytes.len() as u64,
2181            ObjectHash::digest(uploader_bytes),
2182        ),
2183    };
2184    let locator = BlobLocator::opaque(
2185        "media".to_string(),
2186        "blob-a".to_string(),
2187        uploader,
2188        remote_audience.clone(),
2189        BlobScope::Master,
2190        crate::KeyFingerprint::from_bytes([3; 8]),
2191        7,
2192        ObjectHash::digest(b"content"),
2193    )
2194    .map_err(|error| error.to_string())?;
2195    let stored_bytes = b"sealed-content".to_vec();
2196    let object = ExactObjectRef::new(
2197        ObjectSlot::logical(locator.semantic_key()).map_err(|error| error.to_string())?,
2198        stored_bytes.len() as u64,
2199        ObjectHash::digest(&stored_bytes),
2200    );
2201    let stored = StoredBlobRef::new(locator, object).map_err(|error| error.to_string())?;
2202    let bindings = if include_binding {
2203        vec![
2204            RowBlobLocatorBinding::new("items", "row-a", "stamp-a", "media_blob", stored.clone())
2205                .map_err(|error| error.to_string())?,
2206        ]
2207    } else {
2208        Vec::new()
2209    };
2210    let package = if let RemoteAudience::Circle(circle_id) = remote_audience {
2211        AudiencePackage::circle(
2212            store_root_hash,
2213            candidate_family,
2214            write_id.clone(),
2215            coord,
2216            1,
2217            circle_id,
2218            CircleControlCoord::Serial {
2219                author_pubkey: "author-a".to_string(),
2220                generation: 1,
2221                control_hash: ObjectHash::digest(b"circle-control"),
2222            },
2223            crate::KeyFingerprint::from_bytes([3; 8]),
2224            b"changeset".to_vec(),
2225            bindings,
2226        )
2227    } else {
2228        AudiencePackage::store(
2229            store_root_hash,
2230            candidate_family,
2231            write_id,
2232            coord,
2233            1,
2234            b"changeset".to_vec(),
2235            bindings,
2236        )
2237    }
2238    .map_err(|error| error.to_string())?;
2239    let package_bytes = package.to_bytes();
2240    let package_object = ExactObjectRef::new(
2241        ObjectSlot::logical("test/package".to_string()).map_err(|error| error.to_string())?,
2242        package_bytes.len() as u64,
2243        ObjectHash::digest(&package_bytes),
2244    );
2245    let package_id = ObjectHash::digest(b"package-record");
2246    let blob_id = ObjectHash::digest(b"blob-record");
2247    let packages = vec![PreparedAudiencePackage::new(
2248        package_id,
2249        package_bytes.clone(),
2250        package_bytes,
2251        package_object,
2252    )
2253    .map_err(|error| error.to_string())?];
2254    let blobs = if include_locator {
2255        vec![PreparedAudienceBlob {
2256            remote_object_id: blob_id,
2257            audience: remote_audience,
2258            blob: stored,
2259            spool_path: Some(PathBuf::from("/outbound-blob.spool")),
2260        }]
2261    } else {
2262        Vec::new()
2263    };
2264    let mut object_ids = std::collections::BTreeSet::from([package_id]);
2265    if include_body {
2266        object_ids.insert(blob_id);
2267    }
2268    validate_prepared_audience_blob_graph(&object_ids, &PreparedAudienceObjects { packages, blobs })
2269        .map_err(|error| error.to_string())
2270}
2271
2272#[derive(Debug, Clone)]
2273pub(crate) struct DurableDeviceRegistration {
2274    pub device_id: crate::sync::store_commit::StoreDeviceId,
2275    pub registration_hash: ObjectHash,
2276    pub registration_bytes: Vec<u8>,
2277    pub prepared: PreparedExactObject,
2278    pub initial_ack_ref: StoreAckRef,
2279    pub initial_ack: ExactProtocolObject<StoreAck>,
2280    pub state: LocalDeviceRegistrationState,
2281}
2282
2283#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2284#[serde(rename_all = "snake_case", deny_unknown_fields)]
2285pub(crate) enum LocalDeviceRegistrationState {
2286    Prepared,
2287    Created,
2288    Activated {
2289        authority: crate::sync::store_commit::StoreDeviceRegistrationActivation,
2290    },
2291    Retired {
2292        authority: crate::sync::store_commit::StoreDeviceRegistrationActivation,
2293        retirement: crate::sync::store_commit::StoreDeviceSelfRetirementRef,
2294    },
2295}
2296
2297type PreparedLocalDeviceRegistrationRow =
2298    (String, String, Vec<u8>, String, String, Vec<u8>, String);
2299type LocalDeviceRegistrationJournalRow = (
2300    String,
2301    String,
2302    Vec<u8>,
2303    String,
2304    String,
2305    Vec<u8>,
2306    String,
2307    String,
2308);
2309
2310impl DurableDeviceRegistration {
2311    pub(crate) fn is_activated(&self) -> bool {
2312        matches!(self.state, LocalDeviceRegistrationState::Activated { .. })
2313    }
2314
2315    pub(crate) fn is_retired(&self) -> bool {
2316        matches!(self.state, LocalDeviceRegistrationState::Retired { .. })
2317    }
2318}
2319
2320#[derive(Debug, Clone)]
2321pub(crate) struct DurableMembershipMutation {
2322    pub intent_hash: ObjectHash,
2323    pub plan_bytes: Vec<u8>,
2324    pub progress_bytes: Vec<u8>,
2325}
2326
2327#[derive(Debug, Clone)]
2328pub(crate) struct DurableSnapshotPublication {
2329    pub reference: StoreSnapshotRef,
2330    pub meta: ExactProtocolObject<SnapshotMeta>,
2331    pub image: ExactProtocolObject<Vec<u8>>,
2332    pub blobs: Vec<PreparedSnapshotBlob>,
2333}
2334
2335#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2336#[serde(deny_unknown_fields)]
2337pub(crate) struct PreparedSnapshotBlob {
2338    pub bindings: Vec<RowBlobLocatorBinding>,
2339    pub authority: crate::sync::audience_package::PackageAudience,
2340    pub remote: RemoteObjectRecord,
2341    pub spool_path: Option<PathBuf>,
2342}
2343
2344#[derive(Debug, Clone)]
2345pub(crate) struct PublishedStoreSnapshot {
2346    pub reference: StoreSnapshotRef,
2347    pub successor_slot: crate::storage::cloud::ObjectSlot,
2348    pub meta: SnapshotMeta,
2349}
2350
2351pub(crate) struct StoreWritePreparation {
2352    pub write_id: WriteId,
2353    pub remote_objects: Vec<RemoteObjectRecord>,
2354    pub audiences: PreparedAudienceObjects,
2355    pub commit: PreparedProtocolObject<StoreBatchCommit>,
2356    pub head: PreparedProtocolObject<StoreDeviceHead>,
2357    pub local_cleanup: StoreBatchLocalCleanup,
2358    pub completion: StoreBatchCompletion,
2359}
2360
2361pub(crate) struct MergeCandidateAbandonmentPreparation {
2362    pub write_id: WriteId,
2363    pub commit: PreparedProtocolObject<StoreBatchCommit>,
2364    pub head: PreparedProtocolObject<StoreDeviceHead>,
2365}
2366
2367pub(crate) struct SerialCandidateAbandonmentPreparation {
2368    pub branch_id: PendingBranchId,
2369    pub candidate: StoreBatchCommitRef,
2370    pub commit: PreparedProtocolObject<StoreBatchCommit>,
2371    pub head: StoreSerialHead,
2372    pub original_head_bytes: Vec<u8>,
2373}
2374
2375pub(crate) struct SerialStoreWritePreparation {
2376    pub branch_id: PendingBranchId,
2377    pub base: Option<StoreBatchCommitRef>,
2378    pub base_head_bytes: Option<Vec<u8>>,
2379    pub base_head_version: Option<VersionToken>,
2380    pub writes: Vec<SerialStoreWritePreparationEntry>,
2381    pub head: StoreSerialHead,
2382}
2383
2384pub(crate) struct SerialStoreWritePreparationEntry {
2385    pub write_id: WriteId,
2386    pub remote_objects: Vec<RemoteObjectRecord>,
2387    pub audiences: PreparedAudienceObjects,
2388    pub commit: PreparedProtocolObject<StoreBatchCommit>,
2389    pub local_cleanup: StoreBatchLocalCleanup,
2390    pub completion: StoreBatchCompletion,
2391}
2392
2393#[derive(Debug, Clone, PartialEq, Eq)]
2394pub(crate) struct CandidateCleanupObject {
2395    pub(crate) object: ExactObjectRef,
2396}
2397
2398#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2399#[serde(rename_all = "snake_case", deny_unknown_fields)]
2400enum PreparedStoreWriteState {
2401    MergeConcurrent {
2402        commit: DurablePreparedProtocolObject,
2403        head: DurablePreparedProtocolObject,
2404        local_cleanup: StoreBatchLocalCleanup,
2405        completion: StoreBatchCompletion,
2406    },
2407    MergeAbandonment {
2408        candidate_commit: DurablePreparedProtocolObject,
2409        candidate_head: DurablePreparedProtocolObject,
2410        authority_commit: DurablePreparedProtocolObject,
2411        authority_head: DurablePreparedProtocolObject,
2412        outcome: MergeAbandonmentOutcome,
2413        local_cleanup: StoreBatchLocalCleanup,
2414        completion: StoreBatchCompletion,
2415    },
2416    SerialPreparing,
2417    Serial {
2418        base_head_bytes: Option<Vec<u8>>,
2419        base_head_version: Option<VersionToken>,
2420        commit: DurablePreparedProtocolObject,
2421        tip_head_bytes: Option<Vec<u8>>,
2422        local_cleanup: StoreBatchLocalCleanup,
2423        completion: StoreBatchCompletion,
2424    },
2425}
2426
2427#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2428#[serde(rename_all = "snake_case", deny_unknown_fields)]
2429enum MergeAbandonmentOutcome {
2430    Prepared,
2431    Accepted {
2432        authority: StoreBatchCommitRef,
2433    },
2434    Lost {
2435        winner_commit: StoreBatchCommitRef,
2436        winner_head: crate::sync::store_commit::StoreDeviceHeadRef,
2437    },
2438}
2439
2440#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2441#[serde(deny_unknown_fields)]
2442struct DurableSerialCandidateAbandonment {
2443    branch_id: PendingBranchId,
2444    base: Option<StoreBatchCommitRef>,
2445    base_head: VersionedObject,
2446    candidate: StoreBatchCommitRef,
2447    commit: DurablePreparedProtocolObject,
2448    head_bytes: Vec<u8>,
2449    original_head_bytes: Vec<u8>,
2450}
2451
2452struct PreparedSerialCandidate {
2453    commit: StoreBatchCommit,
2454    reference: StoreBatchCommitRef,
2455    canonical_signed_bytes: Vec<u8>,
2456}
2457
2458struct PreparedMergeCandidate {
2459    commit: StoreBatchCommit,
2460    reference: StoreBatchCommitRef,
2461    canonical_signed_bytes: Vec<u8>,
2462    head: StoreDeviceHead,
2463    head_prepared: PreparedExactObject,
2464}
2465
2466fn parse_prepared_merge_candidate_on(
2467    conn: &Connection,
2468    prepared: &PreparedStoreWriteState,
2469) -> Result<Option<PreparedMergeCandidate>, DbError> {
2470    let (commit, head) = match prepared {
2471        PreparedStoreWriteState::MergeConcurrent { commit, head, .. } => (commit, head),
2472        PreparedStoreWriteState::MergeAbandonment {
2473            candidate_commit,
2474            candidate_head,
2475            ..
2476        } => (candidate_commit, candidate_head),
2477        PreparedStoreWriteState::SerialPreparing | PreparedStoreWriteState::Serial { .. } => {
2478            return Ok(None);
2479        }
2480    };
2481    parse_prepared_merge_candidate_parts_on(conn, commit, head).map(Some)
2482}
2483
2484fn parse_prepared_merge_candidate_parts_on(
2485    conn: &Connection,
2486    commit: &DurablePreparedProtocolObject,
2487    head: &DurablePreparedProtocolObject,
2488) -> Result<PreparedMergeCandidate, DbError> {
2489    let root = required_store_root_authority_on(conn)?;
2490    let unverified: StoreBatchCommit = serde_json::from_slice(&commit.semantic_bytes)
2491        .map_err(|error| DbError::Message(format!("signed Merge candidate: {error}")))?;
2492    let registration =
2493        load_activated_registration_on(conn, &root, &unverified.author_registration)?;
2494    let coord = StoreCommitCoord::MergeConcurrent {
2495        stream_id: AuthorStreamId::store_announcements(&root, &unverified.author_registration),
2496        sequence: unverified.seq(),
2497    };
2498    let value = StoreBatchCommit::parse_at(
2499        &commit.semantic_bytes,
2500        root.store_root_hash,
2501        &coord,
2502        &registration,
2503    )
2504    .map_err(|error| DbError::Message(format!("verify Merge candidate: {error}")))?;
2505    let reference =
2506        StoreBatchCommitRef::from_commit(&value, coord, commit.prepared.reference().clone())
2507            .map_err(|error| DbError::Message(error.to_string()))?;
2508    let head_value = StoreDeviceHead::parse_at(
2509        &head.semantic_bytes,
2510        root.store_root_hash,
2511        &registration,
2512        &reference,
2513    )
2514    .map_err(|error| DbError::Message(format!("verify Merge candidate head: {error}")))?;
2515    Ok(PreparedMergeCandidate {
2516        commit: value,
2517        reference,
2518        canonical_signed_bytes: commit.semantic_bytes.clone(),
2519        head: head_value,
2520        head_prepared: head.prepared.clone(),
2521    })
2522}
2523
2524fn parse_prepared_merge_publication_on(
2525    conn: &Connection,
2526    prepared: &PreparedStoreWriteState,
2527) -> Result<Option<PreparedMergeCandidate>, DbError> {
2528    match prepared {
2529        PreparedStoreWriteState::MergeConcurrent { commit, head, .. } => {
2530            parse_prepared_merge_candidate_parts_on(conn, commit, head).map(Some)
2531        }
2532        PreparedStoreWriteState::MergeAbandonment {
2533            authority_commit,
2534            authority_head,
2535            ..
2536        } => parse_prepared_merge_candidate_parts_on(conn, authority_commit, authority_head)
2537            .map(Some),
2538        PreparedStoreWriteState::SerialPreparing | PreparedStoreWriteState::Serial { .. } => {
2539            Ok(None)
2540        }
2541    }
2542}
2543
2544fn parse_prepared_serial_candidate(raw: &str) -> Result<Option<PreparedSerialCandidate>, DbError> {
2545    let prepared: PreparedStoreWriteState = serde_json::from_str(raw)
2546        .map_err(|error| DbError::Message(format!("prepared Serial candidate: {error}")))?;
2547    let PreparedStoreWriteState::Serial { commit, .. } = prepared else {
2548        return match prepared {
2549            PreparedStoreWriteState::SerialPreparing => Ok(None),
2550            PreparedStoreWriteState::MergeConcurrent { .. } => Err(DbError::Message(
2551                "MergeConcurrent publication reached Serial candidate state".to_string(),
2552            )),
2553            PreparedStoreWriteState::MergeAbandonment { .. } => Err(DbError::Message(
2554                "Merge abandonment reached Serial candidate state".to_string(),
2555            )),
2556            PreparedStoreWriteState::Serial { .. } => unreachable!(),
2557        };
2558    };
2559    let value: StoreBatchCommit = serde_json::from_slice(&commit.semantic_bytes)
2560        .map_err(|error| DbError::Message(format!("signed Serial candidate: {error}")))?;
2561    let reference = StoreBatchCommitRef::from_commit(
2562        &value,
2563        StoreCommitCoord::Serial {
2564            sequence: value.seq(),
2565        },
2566        commit.prepared.reference().clone(),
2567    )
2568    .map_err(|error| DbError::Message(error.to_string()))?;
2569    Ok(Some(PreparedSerialCandidate {
2570        commit: value,
2571        reference,
2572        canonical_signed_bytes: commit.semantic_bytes,
2573    }))
2574}
2575
2576fn begin_merge_candidate_nonactivation_on(
2577    conn: &Connection,
2578    write_id: &WriteId,
2579    candidate: &PreparedMergeCandidate,
2580    winner_head: crate::sync::store_commit::StoreDeviceHeadRef,
2581    include_indexed_objects: bool,
2582) -> Result<(), DbError> {
2583    let nonactivation = crate::sync::remote_object::CandidateNonactivation {
2584        candidate: crate::sync::store_commit::StoreBatchCommitDeletionTarget {
2585            coord: candidate.reference.coord.clone(),
2586            object: candidate.reference.object.clone(),
2587            canonical_signed_bytes: candidate.canonical_signed_bytes.clone(),
2588        },
2589        proof: crate::sync::remote_object::CandidateNonactivationProof::MergeWinner { winner_head },
2590    };
2591    let mut object_ids = if include_indexed_objects {
2592        let mut statement = conn
2593            .prepare(
2594                "SELECT remote_object_id FROM store_write_packages WHERE write_id = ?1
2595                 UNION
2596                 SELECT remote_object_id FROM store_write_blobs WHERE write_id = ?1
2597                 ORDER BY remote_object_id",
2598            )
2599            .map_err(DbError::from)?;
2600        let object_ids = statement
2601            .query_map([write_id.as_str()], |row| row.get::<_, String>(0))
2602            .map_err(DbError::from)?
2603            .collect::<Result<Vec<_>, _>>()
2604            .map_err(DbError::from)?;
2605        drop(statement);
2606        object_ids
2607    } else {
2608        Vec::new()
2609    };
2610    object_ids.push(remote_object_id(candidate.head_prepared.reference()).to_string());
2611    for encoded in object_ids {
2612        let object_id: ObjectHash = encoded.parse().map_err(|error| {
2613            DbError::Message(format!("Merge conflict remote object id: {error}"))
2614        })?;
2615        let mut remote = load_remote_object_on(conn, object_id)?;
2616        remote
2617            .begin_candidate_nonactivation(nonactivation.clone())
2618            .map_err(|error| {
2619                DbError::Message(format!(
2620                    "record Merge candidate nonactivation for {object_id}: {error}"
2621                ))
2622            })?;
2623        update_remote_object_on(conn, object_id, &remote)?;
2624    }
2625    let commit_object_id = remote_object_id(&candidate.reference.object);
2626    let mut remote = load_remote_object_on(conn, commit_object_id)?;
2627    remote
2628        .begin_candidate_nonactivation(nonactivation)
2629        .map_err(|error| {
2630            DbError::Message(format!(
2631                "record Merge candidate commit nonactivation for {commit_object_id}: {error}"
2632            ))
2633        })?;
2634    update_remote_object_on(conn, commit_object_id, &remote)
2635}
2636
2637fn merge_candidate_cleanup_targets_on(
2638    conn: &Connection,
2639    write_id: &WriteId,
2640    candidate: &PreparedMergeCandidate,
2641    include_indexed_objects: bool,
2642) -> Result<Vec<CandidateCleanupObject>, DbError> {
2643    let commit_remote = load_remote_object_on(conn, remote_object_id(&candidate.reference.object))?;
2644    if !matches!(
2645        &commit_remote,
2646        RemoteObjectRecord::CandidateCommit(record)
2647            if matches!(
2648                &record.state,
2649                crate::sync::remote_object::CandidateCommitState::CleanupPending {
2650                    proof: crate::sync::remote_object::CandidateNonactivationProof::MergeWinner { .. }
2651                } | crate::sync::remote_object::CandidateCommitState::AbsentVerified {
2652                    proof: crate::sync::remote_object::CandidateNonactivationProof::MergeWinner { .. }
2653                }
2654            )
2655    ) {
2656        return Err(DbError::Message(
2657            "Merge candidate has no durable winner proof".to_string(),
2658        ));
2659    }
2660    let mut cleanup = BTreeMap::new();
2661    if include_indexed_objects {
2662        let mut statement = conn
2663            .prepare(
2664                "SELECT remote_object_id FROM store_write_packages WHERE write_id = ?1
2665                 UNION
2666                 SELECT remote_object_id FROM store_write_blobs WHERE write_id = ?1",
2667            )
2668            .map_err(DbError::from)?;
2669        let encoded = statement
2670            .query_map([write_id.as_str()], |row| row.get::<_, String>(0))
2671            .map_err(DbError::from)?
2672            .collect::<Result<Vec<_>, _>>()
2673            .map_err(DbError::from)?;
2674        drop(statement);
2675        for encoded in encoded {
2676            let object_id: ObjectHash = encoded.parse().map_err(|error| {
2677                DbError::Message(format!("Merge cleanup remote object id: {error}"))
2678            })?;
2679            let remote = load_remote_object_on(conn, object_id)?;
2680            if let Some(object) = remote.cleanup_target() {
2681                cleanup.insert(
2682                    object.clone(),
2683                    CandidateCleanupObject {
2684                        object: object.clone(),
2685                    },
2686                );
2687            } else if !remote
2688                .candidate_cleanup_complete(&candidate.reference)
2689                .map_err(|error| DbError::Message(format!("Merge cleanup {object_id}: {error}")))?
2690            {
2691                return Err(DbError::Message(format!(
2692                    "Merge candidate object {object_id} has no cleanup transition"
2693                )));
2694            }
2695        }
2696    }
2697    let head_remote =
2698        load_remote_object_on(conn, remote_object_id(candidate.head_prepared.reference()))?;
2699    if !head_remote
2700        .candidate_cleanup_complete(&candidate.reference)
2701        .map_err(|error| DbError::Message(format!("Merge cleanup head: {error}")))?
2702    {
2703        return Err(DbError::Message(
2704            "Merge candidate head absence is not verified".to_string(),
2705        ));
2706    }
2707    let mut targets = Vec::new();
2708    for object in candidate_manifest_exact_objects(&candidate.commit) {
2709        if let Some(target) = cleanup.remove(object) {
2710            targets.push(target);
2711        }
2712    }
2713    if !cleanup.is_empty() {
2714        return Err(DbError::Message(
2715            "Merge cleanup contains an object outside the signed candidate manifest".to_string(),
2716        ));
2717    }
2718    if let Some(object) = commit_remote.cleanup_target() {
2719        targets.push(CandidateCleanupObject {
2720            object: object.clone(),
2721        });
2722    } else if !commit_remote
2723        .candidate_cleanup_complete(&candidate.reference)
2724        .map_err(|error| DbError::Message(format!("Merge cleanup commit: {error}")))?
2725    {
2726        return Err(DbError::Message(
2727            "Merge candidate commit cleanup is incomplete".to_string(),
2728        ));
2729    }
2730    Ok(targets)
2731}
2732
2733fn remove_cleaned_merge_authority_on(
2734    tx: &rusqlite::Transaction<'_>,
2735    authority: &PreparedMergeCandidate,
2736) -> Result<(), DbError> {
2737    for object in [
2738        authority.reference.object.clone(),
2739        authority.head_prepared.reference().clone(),
2740    ] {
2741        let object_id = remote_object_id(&object);
2742        let remote = load_remote_object_on(tx, object_id)?;
2743        if !remote
2744            .candidate_cleanup_complete(&authority.reference)
2745            .map_err(|error| {
2746                DbError::Message(format!(
2747                    "validate abandoned authority cleanup for {object_id}: {error}"
2748                ))
2749            })?
2750        {
2751            return Err(DbError::Message(
2752                "losing Merge abandonment cleanup is incomplete".to_string(),
2753            ));
2754        }
2755        let removed = tx
2756            .execute(
2757                "DELETE FROM remote_objects WHERE object_id = ?1",
2758                [object_id.to_string()],
2759            )
2760            .map_err(DbError::from)?;
2761        if removed != 1 {
2762            return Err(DbError::Message(format!(
2763                "abandoned authority object {object_id} disappeared during removal"
2764            )));
2765        }
2766    }
2767    Ok(())
2768}
2769
2770#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2771#[serde(deny_unknown_fields)]
2772struct DurablePreparedProtocolObject {
2773    semantic_bytes: Vec<u8>,
2774    prepared: PreparedExactObject,
2775}
2776
2777#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2778#[serde(deny_unknown_fields)]
2779pub(crate) struct StoreBatchLocalCleanup {
2780    pub drops: Vec<crate::sync::service::DeferredLocalBlobDrop>,
2781}
2782
2783#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2784#[serde(deny_unknown_fields)]
2785pub(crate) struct StoreBatchCompletion {}
2786
2787fn validate_host_synced_tables(
2788    conn: &Connection,
2789    synced_tables: &[SyncedTable],
2790) -> Result<(), DbError> {
2791    // Which table owns each declared blob namespace, so a second table claiming an
2792    // already-owned namespace is caught here. A blob's namespace is part of its
2793    // address; two tables sharing one makes `row_for_blob_in_namespace` resolve to
2794    // whichever the hash map iterates first.
2795    let mut namespace_owner: HashMap<&str, &str> = HashMap::new();
2796    let mut table_by_sqlite_name: HashMap<String, &str> = HashMap::new();
2797    for table in synced_tables {
2798        let name = table.name();
2799        if name.is_empty() {
2800            return Err(DbError::Message(
2801                "synced table name must not be empty".to_string(),
2802            ));
2803        }
2804        if is_reserved_table_name(name) {
2805            return Err(DbError::Message(format!(
2806                "synced table {name:?} is reserved by coven"
2807            )));
2808        }
2809        let sqlite_name = name.to_ascii_lowercase();
2810        if let Some(prior) = table_by_sqlite_name.insert(sqlite_name, name) {
2811            return Err(DbError::Message(format!(
2812                "synced tables {prior:?} and {name:?} are declared as the same SQLite table more than once"
2813            )));
2814        }
2815        if let Some(live_name) = canonical_table_name(conn, name)? {
2816            if live_name != name {
2817                return Err(DbError::Message(format!(
2818                    "synced table {name:?} does not use the live schema's exact spelling {live_name:?}"
2819                )));
2820            }
2821        }
2822        validate_synced_table_contract(conn, name)?;
2823        validate_existing_row_identities(conn, table)?;
2824        if let Some(decl) = table.blob() {
2825            let namespace = decl.namespace.as_str();
2826            if let Some(prior) = namespace_owner.insert(namespace, name) {
2827                return Err(DbError::Message(format!(
2828                    "synced tables {prior:?} and {name:?} both declare blob namespace \
2829                     {namespace:?}; a namespace must be owned by exactly one table"
2830                )));
2831            }
2832        }
2833    }
2834    Ok(())
2835}
2836
2837/// Return the live `main`-schema spelling SQLite resolves for `table`.
2838/// SQLite table identifiers compare case-insensitively, while coven dispatches
2839/// changesets by their exact table name, so open requires declarations to use
2840/// this canonical spelling.
2841fn canonical_table_name(conn: &Connection, table: &str) -> Result<Option<String>, DbError> {
2842    conn.query_row(
2843        "SELECT name FROM main.sqlite_schema \
2844         WHERE type = 'table' AND name = ?1 COLLATE NOCASE",
2845        [table],
2846        |row| row.get(0),
2847    )
2848    .optional()
2849    .map_err(DbError::from)
2850}
2851
2852fn validate_existing_row_identities(conn: &Connection, table: &SyncedTable) -> Result<(), DbError> {
2853    if table.row_identity() == crate::sync::session::RowIdentity::SharedKey {
2854        return Ok(());
2855    }
2856    let sql = format!(
2857        "SELECT id FROM {}",
2858        crate::sync::session::quote_ident(table.name())
2859    );
2860    let mut statement = conn.prepare(&sql).map_err(DbError::from)?;
2861    let ids = statement
2862        .query_map([], |row| row.get::<_, String>(0))
2863        .map_err(DbError::from)?;
2864    for id in ids {
2865        let id = id.map_err(DbError::from)?;
2866        crate::sync::session::validate_row_identity(table.name(), table.row_identity(), &id)
2867            .map_err(|error| DbError::Message(error.to_string()))?;
2868    }
2869    Ok(())
2870}
2871
2872/// One column of a table's `PRAGMA table_info`. `position` is the column ordinal
2873/// — the index a session changeset reports for that column, so the pk's position
2874/// is what the by-position apply path reads. `pk` is 0 for a non-key column or its
2875/// 1-based rank within the primary key.
2876struct ColumnInfo {
2877    position: i64,
2878    name: String,
2879    declared_type: String,
2880    not_null: bool,
2881    pk: i64,
2882}
2883
2884/// Enforce the synced-table contract ([`crate::sync::session::SyncedTable`]) on
2885/// `table`'s live schema: the table declared STRICT; a single primary key
2886/// column, named `id`, declared TEXT, at column 0; and an `_updated_at` column
2887/// declared TEXT NOT NULL. A violation is an open error naming the table and the
2888/// requirement it broke, so the integrator learns it on their own device instead
2889/// of a peer's pull failing on the row.
2890fn validate_synced_table_contract(conn: &Connection, table: &str) -> Result<(), DbError> {
2891    match table_is_strict(conn, table)? {
2892        None => {
2893            return Err(DbError::Message(format!(
2894                "synced table {table:?} is declared in `synced_tables` but no migration \
2895                 creates it — add a `CREATE TABLE {table} (...) STRICT` to the schema \
2896                 migrations, or remove the declaration"
2897            )));
2898        }
2899        Some(false) => {
2900            return Err(DbError::Message(format!(
2901                "synced table {table:?} is not declared STRICT; the sync contract assumes typed \
2902                 columns (apply preserves storage classes peer-to-peer, LWW arbitration renders \
2903                 values to strings for comparison), which STRICT enforces at the insert — declare \
2904                 it STRICT: `CREATE TABLE {table} (...) STRICT`"
2905            )));
2906        }
2907        Some(true) => {}
2908    }
2909
2910    let sql = format!(
2911        "PRAGMA table_info({})",
2912        crate::sync::session::quote_ident(table)
2913    );
2914    let mut stmt = conn.prepare(&sql).map_err(DbError::from)?;
2915    let mut columns = Vec::new();
2916    let rows = stmt
2917        .query_map([], |row| {
2918            Ok(ColumnInfo {
2919                position: row.get::<_, i64>(0)?,
2920                name: row.get::<_, String>(1)?,
2921                declared_type: row.get::<_, String>(2)?,
2922                not_null: row.get::<_, i64>(3)? != 0,
2923                pk: row.get::<_, i64>(5)?,
2924            })
2925        })
2926        .map_err(DbError::from)?;
2927    for row in rows {
2928        columns.push(row.map_err(DbError::from)?);
2929    }
2930
2931    let pk_columns: Vec<&ColumnInfo> = columns.iter().filter(|c| c.pk > 0).collect();
2932    let pk = match pk_columns.as_slice() {
2933        [single] => *single,
2934        [] => {
2935            return Err(DbError::Message(format!(
2936                "synced table {table:?} has no primary key; the contract requires a single \
2937                 `id` TEXT primary key at column 0"
2938            )))
2939        }
2940        _ => {
2941            let names: Vec<&str> = pk_columns.iter().map(|c| c.name.as_str()).collect();
2942            return Err(DbError::Message(format!(
2943                "synced table {table:?} has a composite primary key {names:?}; the contract \
2944                 requires a single `id` TEXT primary key at column 0"
2945            )));
2946        }
2947    };
2948    if pk.name != "id" {
2949        return Err(DbError::Message(format!(
2950            "synced table {table:?} primary key is {:?}, not `id`; the contract requires the \
2951             primary key to be the `id` column",
2952            pk.name
2953        )));
2954    }
2955    if pk.position != 0 {
2956        return Err(DbError::Message(format!(
2957            "synced table {table:?} primary key `id` is at column {}, not column 0; the \
2958             contract requires `id` to be the first column",
2959            pk.position
2960        )));
2961    }
2962    if !declared_as_text(&pk.declared_type) {
2963        return Err(DbError::Message(format!(
2964            "synced table {table:?} primary key `id` is declared {:?}, not TEXT; the contract \
2965             requires an `id` TEXT primary key",
2966            pk.declared_type
2967        )));
2968    }
2969
2970    let updated_at = columns
2971        .iter()
2972        .find(|c| c.name == "_updated_at")
2973        .ok_or_else(|| {
2974            DbError::Message(format!(
2975                "synced table {table:?} has no `_updated_at` column; the contract requires \
2976                 `_updated_at TEXT NOT NULL`"
2977            ))
2978        })?;
2979    if !declared_as_text(&updated_at.declared_type) {
2980        return Err(DbError::Message(format!(
2981            "synced table {table:?} column `_updated_at` is declared {:?}, not TEXT; the \
2982             contract requires `_updated_at TEXT NOT NULL`",
2983            updated_at.declared_type
2984        )));
2985    }
2986    if !updated_at.not_null {
2987        return Err(DbError::Message(format!(
2988            "synced table {table:?} column `_updated_at` is nullable; the contract requires \
2989             `_updated_at TEXT NOT NULL`"
2990        )));
2991    }
2992
2993    Ok(())
2994}
2995
2996/// Whether a `PRAGMA table_info` declared type is TEXT, case-insensitively. SQL
2997/// keywords are case-insensitive, so `text` and `TEXT` both satisfy the contract;
2998/// any other declared type (or none) does not.
2999fn declared_as_text(declared_type: &str) -> bool {
3000    declared_type.eq_ignore_ascii_case("TEXT")
3001}
3002
3003/// Whether `table` (in the `main` schema) is declared STRICT, via `PRAGMA
3004/// table_list`'s `strict` column (SQLite 3.37+) — the schema-level flag itself,
3005/// not `sqlite_master.sql` text, which a hand-formatted `CREATE TABLE` could spell
3006/// many ways. `None` means the table doesn't exist in `main` at all — a declared
3007/// synced table no migration created — which the caller reports as its own
3008/// contract error rather than folding into "not STRICT".
3009fn table_is_strict(conn: &Connection, table: &str) -> Result<Option<bool>, DbError> {
3010    let sql = format!(
3011        "PRAGMA table_list({})",
3012        crate::sync::session::quote_ident(table)
3013    );
3014    let mut stmt = conn.prepare(&sql).map_err(DbError::from)?;
3015    let rows = stmt
3016        .query_map([], |row| {
3017            Ok((row.get::<_, String>(0)?, row.get::<_, i64>(5)?))
3018        })
3019        .map_err(DbError::from)?;
3020    for row in rows {
3021        let (schema, strict) = row.map_err(DbError::from)?;
3022        if schema == "main" {
3023            return Ok(Some(strict != 0));
3024        }
3025    }
3026    Ok(None)
3027}
3028
3029struct PreparedCircleOperationRow {
3030    operation_id: String,
3031    circle_id: String,
3032    payload: Vec<u8>,
3033}
3034
3035impl PreparedCircleOperationRow {
3036    fn from_journal(
3037        journal: crate::sync::circle_ops::CircleOperationJournal,
3038    ) -> Result<Self, DbError> {
3039        let operation_id = journal.operation_id.clone();
3040        let circle_id = journal.circle_id().to_string();
3041        let payload = serde_json::to_vec(&journal).map_err(|error| {
3042            DbError::Message(format!("serialize circle operation journal: {error}"))
3043        })?;
3044        Ok(Self {
3045            operation_id,
3046            circle_id,
3047            payload,
3048        })
3049    }
3050}
3051
3052impl Database {
3053    pub(crate) fn id_provider(&self) -> &dyn crate::id_provider::IdProvider {
3054        self.state.ids.as_ref()
3055    }
3056
3057    #[doc(hidden)]
3058    pub fn new_write_id(&self) -> WriteId {
3059        WriteId::from_generated(self.state.ids.new_id())
3060    }
3061
3062    fn notify_write_status_in(
3063        statuses: &Arc<std::sync::Mutex<HashMap<WriteId, tokio::sync::watch::Sender<WriteStatus>>>>,
3064        write_id: &WriteId,
3065        status: WriteStatus,
3066    ) {
3067        let senders = statuses.lock().expect("write status mutex poisoned");
3068        if let Some(sender) = senders.get(write_id) {
3069            sender.send_replace(status);
3070        }
3071    }
3072
3073    fn set_write_status_on(
3074        conn: &Connection,
3075        write_id: &WriteId,
3076        status: &WriteStatus,
3077    ) -> Result<(), DbError> {
3078        let status = serde_json::to_string(status)
3079            .map_err(|error| DbError::Message(format!("serialize write status: {error}")))?;
3080        let updated = conn
3081            .execute(
3082                "UPDATE store_writes SET status = ?2 WHERE write_id = ?1",
3083                rusqlite::params![write_id.as_str(), status],
3084            )
3085            .map_err(DbError::from)?;
3086        if updated != 1 {
3087            return Err(DbError::Message(format!("write {write_id} does not exist")));
3088        }
3089        Ok(())
3090    }
3091
3092    pub(crate) async fn set_write_status(
3093        &self,
3094        write_id: &WriteId,
3095        status: WriteStatus,
3096    ) -> Result<(), DbError> {
3097        let stored_id = write_id.clone();
3098        let stored_status = status.clone();
3099        let statuses = self.state.write_statuses.clone();
3100        self.call(move |conn| {
3101            Self::set_write_status_on(conn, &stored_id, &stored_status)?;
3102            Self::notify_write_status_in(&statuses, &stored_id, stored_status);
3103            Ok(())
3104        })
3105        .await
3106    }
3107
3108    pub async fn write_status(&self, write_id: &WriteId) -> Result<WriteStatus, DbError> {
3109        let write_id = write_id.clone();
3110        self.call(move |conn| {
3111            let raw: String = conn
3112                .query_row(
3113                    "SELECT status FROM store_writes WHERE write_id = ?1",
3114                    [write_id.as_str()],
3115                    |row| row.get(0),
3116                )
3117                .map_err(DbError::from)?;
3118            serde_json::from_str(&raw)
3119                .map_err(|error| DbError::Message(format!("write {write_id} status: {error}")))
3120        })
3121        .await
3122    }
3123
3124    pub async fn pending_writes(&self) -> Result<Vec<PendingWrite>, DbError> {
3125        self.call(move |conn| {
3126            let mut statement = conn
3127                .prepare(
3128                    "SELECT write_id, status, affected_rows FROM store_writes
3129                     WHERE status IN ('\"pending\"', '\"publishing\"')
3130                        OR json_extract(status, '$.blocked') IS NOT NULL
3131                        OR json_extract(status, '$.conflict') IS NOT NULL
3132                     ORDER BY ordinal",
3133                )
3134                .map_err(DbError::from)?;
3135            let rows = statement
3136                .query_map([], |row| {
3137                    Ok((
3138                        row.get::<_, String>(0)?,
3139                        row.get::<_, String>(1)?,
3140                        row.get::<_, String>(2)?,
3141                    ))
3142                })
3143                .map_err(DbError::from)?;
3144            rows.map(|row| {
3145                let (write_id, status, affected_rows) = row.map_err(DbError::from)?;
3146                Ok(PendingWrite {
3147                    write_id: WriteId::from_generated(write_id),
3148                    status: serde_json::from_str(&status).map_err(|error| {
3149                        DbError::Message(format!("pending write status: {error}"))
3150                    })?,
3151                    affected_rows: serde_json::from_str(&affected_rows).map_err(|error| {
3152                        DbError::Message(format!("pending affected rows: {error}"))
3153                    })?,
3154                })
3155            })
3156            .collect()
3157        })
3158        .await
3159    }
3160
3161    /// Writes whose semantic publication fault requires an explicit host action.
3162    pub async fn blocked_writes(&self) -> Result<Vec<PendingWrite>, DbError> {
3163        Ok(self
3164            .pending_writes()
3165            .await?
3166            .into_iter()
3167            .filter(|write| matches!(write.status, WriteStatus::Blocked(_)))
3168            .collect())
3169    }
3170
3171    /// Return one blocked write, or its whole Serial branch, to production
3172    /// publication. The next preparation attempt revalidates every captured fact;
3173    /// another semantic failure records a fresh `Blocked` status.
3174    pub async fn retry_blocked_write(&self, write_id: &WriteId) -> Result<Vec<WriteId>, DbError> {
3175        let write_id = write_id.clone();
3176        let statuses = self.state.write_statuses.clone();
3177        self.call(move |conn| {
3178            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
3179            let (raw_status, raw_base, prepared): (String, String, Option<String>) = tx
3180                .query_row(
3181                    "SELECT status, base, prepared FROM store_writes WHERE write_id = ?1",
3182                    [write_id.as_str()],
3183                    |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
3184                )
3185                .map_err(DbError::from)?;
3186            let status: WriteStatus = serde_json::from_str(&raw_status)
3187                .map_err(|error| DbError::Message(format!("blocked write {write_id} status: {error}")))?;
3188            if !matches!(status, WriteStatus::Blocked(_)) {
3189                return Err(DbError::Message(format!("write {write_id} is not blocked")));
3190            }
3191            let base: StoreWriteBase = serde_json::from_str(&raw_base)
3192                .map_err(|error| DbError::Message(format!("blocked write {write_id} base: {error}")))?;
3193
3194            let mut retried = Vec::new();
3195            match base {
3196                StoreWriteBase::MergeConcurrent { .. } => {
3197                    if let Some(raw_prepared) = prepared.as_deref() {
3198                        let prepared: PreparedStoreWriteState = serde_json::from_str(raw_prepared)
3199                            .map_err(|error| {
3200                                DbError::Message(format!("blocked write {write_id} preparation: {error}"))
3201                            })?;
3202                        if !matches!(prepared, PreparedStoreWriteState::MergeConcurrent { .. }) {
3203                            return Err(DbError::Message(format!(
3204                                "blocked MergeConcurrent write {write_id} has Serial preparation"
3205                            )));
3206                        }
3207                        let candidate = parse_prepared_merge_candidate_on(&tx, &prepared)?
3208                            .expect("matched Merge preparation")
3209                            .reference;
3210                        let remote =
3211                            load_remote_object_on(&tx, remote_object_id(&candidate.object))?;
3212                        if matches!(
3213                            remote,
3214                            RemoteObjectRecord::CandidateCommit(
3215                                crate::sync::remote_object::CandidateCommitRecord {
3216                                    state:
3217                                        crate::sync::remote_object::CandidateCommitState::CleanupPending {
3218                                            proof: crate::sync::remote_object::CandidateNonactivationProof::MergeWinner { .. }
3219                                        }
3220                                        | crate::sync::remote_object::CandidateCommitState::AbsentVerified {
3221                                            proof: crate::sync::remote_object::CandidateNonactivationProof::MergeWinner { .. }
3222                                        },
3223                                    ..
3224                                }
3225                            )
3226                        ) {
3227                            return Err(DbError::Message(format!(
3228                                "Merge write {write_id} has an irreversible winner and cannot be retried"
3229                            )));
3230                        }
3231                    }
3232                    let next = if prepared.is_some() {
3233                        WriteStatus::Publishing
3234                    } else {
3235                        WriteStatus::Pending
3236                    };
3237                    let next_json = serde_json::to_string(&next)
3238                        .map_err(|error| DbError::Message(format!("serialize retry status: {error}")))?;
3239                    let updated = tx
3240                        .execute(
3241                            "UPDATE store_writes SET status = ?2
3242                             WHERE write_id = ?1 AND json_extract(status, '$.blocked') IS NOT NULL",
3243                            rusqlite::params![write_id.as_str(), next_json],
3244                        )
3245                        .map_err(DbError::from)?;
3246                    if updated != 1 {
3247                        return Err(DbError::Message(format!(
3248                            "blocked write {write_id} changed during retry"
3249                        )));
3250                    }
3251                    retried.push((write_id, next));
3252                }
3253                StoreWriteBase::Serial {
3254                    branch_id,
3255                    base: branch_base,
3256                } => {
3257                    if prepared.is_some() {
3258                        return Err(DbError::Message(format!(
3259                            "blocked Serial branch {} retains publication preparation",
3260                            branch_id.first_write_id()
3261                        )));
3262                    }
3263                    let expected_base = StoreWriteBase::Serial {
3264                        branch_id: branch_id.clone(),
3265                        base: branch_base,
3266                    };
3267                    let mut statement = tx
3268                        .prepare(
3269                            "SELECT write_id, status, base, prepared FROM store_writes
3270                             WHERE base = ?1
3271                               AND status != '\"local_only\"'
3272                               AND json_extract(status, '$.published') IS NULL
3273                               AND json_extract(status, '$.resolved') IS NULL
3274                             ORDER BY ordinal",
3275                        )
3276                        .map_err(DbError::from)?;
3277                    let rows = statement
3278                        .query_map([&raw_base], |row| {
3279                            Ok((
3280                                row.get::<_, String>(0)?,
3281                                row.get::<_, String>(1)?,
3282                                row.get::<_, String>(2)?,
3283                                row.get::<_, Option<String>>(3)?,
3284                            ))
3285                        })
3286                        .map_err(DbError::from)?;
3287                    let mut branch_write_ids = Vec::new();
3288                    for row in rows {
3289                        let (stored_id, raw_status, raw_base, prepared) =
3290                            row.map_err(DbError::from)?;
3291                        let stored_base: StoreWriteBase = serde_json::from_str(&raw_base)
3292                            .map_err(|error| DbError::Message(format!("Serial retry base: {error}")))?;
3293                        if stored_base != expected_base {
3294                            return Err(DbError::Message(
3295                                "Serial database contains more than one unresolved branch"
3296                                    .to_string(),
3297                            ));
3298                        }
3299                        let stored_status: WriteStatus = serde_json::from_str(&raw_status)
3300                            .map_err(|error| DbError::Message(format!("Serial retry status: {error}")))?;
3301                        if !matches!(stored_status, WriteStatus::Pending | WriteStatus::Blocked(_))
3302                            || prepared.is_some()
3303                        {
3304                            return Err(DbError::Message(format!(
3305                                "Serial branch write {stored_id} is not pending or blocked without preparation"
3306                            )));
3307                        }
3308                        if matches!(stored_status, WriteStatus::Blocked(_)) {
3309                            branch_write_ids.push(WriteId::from_generated(stored_id));
3310                        }
3311                    }
3312                    drop(statement);
3313                    let pending = serde_json::to_string(&WriteStatus::Pending)
3314                        .map_err(|error| DbError::Message(format!("serialize retry status: {error}")))?;
3315                    for branch_write_id in branch_write_ids {
3316                        let updated = tx
3317                            .execute(
3318                                "UPDATE store_writes SET status = ?2
3319                                 WHERE write_id = ?1
3320                                   AND json_extract(status, '$.blocked') IS NOT NULL
3321                                   AND prepared IS NULL",
3322                                rusqlite::params![branch_write_id.as_str(), &pending],
3323                            )
3324                            .map_err(DbError::from)?;
3325                        if updated != 1 {
3326                            return Err(DbError::Message(format!(
3327                                "blocked Serial write {branch_write_id} changed during retry"
3328                            )));
3329                        }
3330                        retried.push((branch_write_id, WriteStatus::Pending));
3331                    }
3332                }
3333            }
3334            tx.commit().map_err(DbError::from)?;
3335            let retried_ids = retried
3336                .iter()
3337                .map(|(write_id, _)| write_id.clone())
3338                .collect();
3339            for (write_id, status) in retried {
3340                Self::notify_write_status_in(&statuses, &write_id, status);
3341            }
3342            Ok(retried_ids)
3343        })
3344        .await
3345    }
3346
3347    /// Atomically reverse a blocked write and every later unpublished shared
3348    /// write whose working-row state depends on it.
3349    pub async fn discard_blocked_write(&self, write_id: &WriteId) -> Result<Vec<WriteId>, DbError> {
3350        let write_id = write_id.clone();
3351        let synced_tables = self.synced_tables().to_vec();
3352        let statuses = self.state.write_statuses.clone();
3353        self.call(move |conn| {
3354            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
3355            let (raw_status, target_ordinal): (String, i64) = tx
3356                .query_row(
3357                    "SELECT status, ordinal FROM store_writes WHERE write_id = ?1",
3358                    [write_id.as_str()],
3359                    |row| Ok((row.get(0)?, row.get(1)?)),
3360                )
3361                .map_err(DbError::from)?;
3362            let target_status: WriteStatus = serde_json::from_str(&raw_status)
3363                .map_err(|error| DbError::Message(format!("blocked write {write_id} status: {error}")))?;
3364            if !matches!(target_status, WriteStatus::Blocked(_)) {
3365                return Err(DbError::Message(format!("write {write_id} is not blocked")));
3366            }
3367
3368            let mut statement = tx
3369                .prepare(
3370                    "SELECT write_id, status, inverse_changeset FROM store_writes
3371                     WHERE ordinal >= ?1
3372                       AND status != '\"local_only\"'
3373                       AND json_extract(status, '$.published') IS NULL
3374                       AND json_extract(status, '$.resolved') IS NULL
3375                     ORDER BY ordinal",
3376                )
3377                .map_err(DbError::from)?;
3378            let rows = statement
3379                .query_map([target_ordinal], |row| {
3380                    Ok((
3381                        row.get::<_, String>(0)?,
3382                        row.get::<_, String>(1)?,
3383                        row.get::<_, Vec<u8>>(2)?,
3384                    ))
3385                })
3386                .map_err(DbError::from)?;
3387            let mut discarded = Vec::new();
3388            for row in rows {
3389                let (stored_id, raw_status, inverse) = row.map_err(DbError::from)?;
3390                let status: WriteStatus = serde_json::from_str(&raw_status)
3391                    .map_err(|error| DbError::Message(format!("discard write status: {error}")))?;
3392                if !matches!(status, WriteStatus::Pending | WriteStatus::Blocked(_)) {
3393                    return Err(DbError::Message(format!(
3394                        "write {stored_id} after blocked write {write_id} has non-discardable status {status:?}"
3395                    )));
3396                }
3397                discarded.push((WriteId::from_generated(stored_id), inverse));
3398            }
3399            drop(statement);
3400            if discarded.first().map(|(stored_id, _)| stored_id) != Some(&write_id) {
3401                return Err(DbError::Message(format!(
3402                    "blocked write {write_id} is absent from its unpublished suffix"
3403                )));
3404            }
3405            let schema = Arc::new(crate::sync::conflict::TableSchema::from_db(
3406                &tx,
3407                &synced_tables,
3408            )?);
3409            for (_, inverse) in discarded.iter().rev() {
3410                let inverse = crate::sync::apply::ValidatedChangeset::new(
3411                    inverse,
3412                    schema.clone(),
3413                )
3414                .map_err(|error| DbError::Message(format!("invalid blocked-write inverse: {error}")))?;
3415                crate::sync::apply::apply_changeset_strict_on(&tx, inverse)
3416                    .map_err(|error| DbError::Message(format!("reverse blocked-write suffix: {error}")))?;
3417            }
3418            let discarded_ids: Vec<_> = discarded
3419                .into_iter()
3420                .map(|(write_id, _)| write_id)
3421                .collect();
3422            let resolution = WriteResolution::Discarded;
3423            Self::resolve_unpublished_writes_on(&tx, &discarded_ids, &resolution)?;
3424            tx.commit().map_err(DbError::from)?;
3425            let status = WriteStatus::Resolved(resolution);
3426            for discarded_id in &discarded_ids {
3427                Self::notify_write_status_in(&statuses, discarded_id, status.clone());
3428            }
3429            Ok(discarded_ids)
3430        })
3431        .await
3432    }
3433
3434    pub async fn pending_branches(&self) -> Result<Option<PendingBranch>, DbError> {
3435        self.call(|conn| {
3436            let mut statement = conn
3437                .prepare(
3438                    "SELECT write_id, status, affected_rows, base FROM store_writes
3439                     WHERE status IN ('\"pending\"', '\"publishing\"')
3440                        OR json_extract(status, '$.blocked') IS NOT NULL
3441                        OR json_extract(status, '$.conflict') IS NOT NULL
3442                     ORDER BY ordinal",
3443                )
3444                .map_err(DbError::from)?;
3445            let rows = statement
3446                .query_map([], |row| {
3447                    Ok((
3448                        row.get::<_, String>(0)?,
3449                        row.get::<_, String>(1)?,
3450                        row.get::<_, String>(2)?,
3451                        row.get::<_, String>(3)?,
3452                    ))
3453                })
3454                .map_err(DbError::from)?;
3455            let mut records = Vec::new();
3456            for row in rows {
3457                let (write_id, status, affected_rows, base) = row.map_err(DbError::from)?;
3458                records.push((
3459                    PendingWrite {
3460                        write_id: WriteId::from_generated(write_id),
3461                        status: serde_json::from_str(&status).map_err(|error| {
3462                            DbError::Message(format!("pending write status: {error}"))
3463                        })?,
3464                        affected_rows: serde_json::from_str(&affected_rows).map_err(|error| {
3465                            DbError::Message(format!("pending affected rows: {error}"))
3466                        })?,
3467                    },
3468                    serde_json::from_str::<StoreWriteBase>(&base).map_err(|error| {
3469                        DbError::Message(format!("pending write base: {error}"))
3470                    })?,
3471                ));
3472            }
3473            drop(statement);
3474
3475            let mut conflict = None;
3476            let mut conflict_exact_base = None;
3477            for (write, base) in &records {
3478                let WriteStatus::Conflict(candidate) = &write.status else {
3479                    continue;
3480                };
3481                let StoreWriteBase::Serial {
3482                    branch_id,
3483                    base: stored_base,
3484                } = base
3485                else {
3486                    return Err(DbError::Message(
3487                        "MergeConcurrent base carries a Serial conflict".to_string(),
3488                    ));
3489                };
3490                let exact_base = store_serial_predecessor_on(conn, stored_base.as_ref())?;
3491                if branch_id != &candidate.branch_id || exact_base != candidate.base {
3492                    return Err(DbError::Message(
3493                        "Serial conflict status differs from its durable branch base".to_string(),
3494                    ));
3495                }
3496                match &conflict {
3497                    None => {
3498                        conflict = Some(candidate.clone());
3499                        conflict_exact_base = Some(stored_base.clone());
3500                    }
3501                    Some(existing) if existing == candidate => {}
3502                    Some(_) => {
3503                        return Err(DbError::Message(
3504                            "Serial database contains more than one conflict branch".to_string(),
3505                        ))
3506                    }
3507                }
3508            }
3509            let Some(conflict) = conflict else {
3510                return Ok(None);
3511            };
3512            let conflict = *conflict;
3513            let expected_base = StoreWriteBase::Serial {
3514                branch_id: conflict.branch_id.clone(),
3515                base: conflict_exact_base.expect("conflict row carries exact base"),
3516            };
3517            let mut writes = Vec::new();
3518            for (write, base) in records {
3519                if matches!(base, StoreWriteBase::MergeConcurrent { .. }) {
3520                    continue;
3521                }
3522                if base != expected_base {
3523                    return Err(DbError::Message(
3524                        "Serial database contains more than one unresolved branch".to_string(),
3525                    ));
3526                }
3527                if !matches!(
3528                    &write.status,
3529                    WriteStatus::Conflict(_) | WriteStatus::Pending
3530                ) {
3531                    return Err(DbError::Message(format!(
3532                        "conflicted Serial branch write {} has non-resolvable status {:?}",
3533                        write.write_id, write.status
3534                    )));
3535                }
3536                writes.push(write);
3537            }
3538            Ok(Some(PendingBranch {
3539                branch_id: conflict.branch_id,
3540                base: conflict.base,
3541                current: conflict.current,
3542                writes,
3543            }))
3544        })
3545        .await
3546    }
3547
3548    pub async fn serial_branch_discard_state(
3549        &self,
3550        branch_id: &PendingBranchId,
3551    ) -> Result<SerialBranchDiscardState, DbError> {
3552        let branch_id = branch_id.clone();
3553        self.call(move |conn| {
3554            let abandonment: Option<String> = conn
3555                .query_row(
3556                    "SELECT value FROM protocol_state WHERE key = ?1",
3557                    [SERIAL_CANDIDATE_ABANDONMENT_STATE_KEY],
3558                    |row| row.get(0),
3559                )
3560                .optional()
3561                .map_err(DbError::from)?;
3562            if let Some(abandonment) = abandonment {
3563                let abandonment: DurableSerialCandidateAbandonment =
3564                    serde_json::from_str(&abandonment).map_err(|error| {
3565                        DbError::Message(format!("Serial candidate abandonment: {error}"))
3566                    })?;
3567                if abandonment.branch_id != branch_id {
3568                    return Err(DbError::Message(
3569                        "another Serial branch owns candidate abandonment".to_string(),
3570                    ));
3571                }
3572                return Ok(SerialBranchDiscardState::Abandonment);
3573            }
3574            let mut statement = conn
3575                .prepare(
3576                    "SELECT base, status, prepared FROM store_writes
3577                     WHERE status != '\"local_only\"'
3578                       AND json_extract(status, '$.published') IS NULL
3579                       AND json_extract(status, '$.resolved') IS NULL
3580                       AND json_type(base, '$.serial') IS NOT NULL
3581                     ORDER BY ordinal",
3582                )
3583                .map_err(DbError::from)?;
3584            let rows = statement
3585                .query_map([], |row| {
3586                    Ok((
3587                        row.get::<_, String>(0)?,
3588                        row.get::<_, String>(1)?,
3589                        row.get::<_, Option<String>>(2)?,
3590                    ))
3591                })
3592                .map_err(DbError::from)?
3593                .collect::<Result<Vec<_>, _>>()
3594                .map_err(DbError::from)?;
3595            drop(statement);
3596            if rows.is_empty() {
3597                return Err(DbError::Message("Serial branch is absent".to_string()));
3598            }
3599            let mut base = None;
3600            let mut saw_local = false;
3601            let mut saw_prepared = false;
3602            let mut saw_conflict = false;
3603            for (raw_base, raw_status, raw_prepared) in rows {
3604                let StoreWriteBase::Serial {
3605                    branch_id: stored_branch_id,
3606                    base: stored_base,
3607                } = serde_json::from_str(&raw_base).map_err(|error| {
3608                    DbError::Message(format!("Serial discard branch base: {error}"))
3609                })?
3610                else {
3611                    unreachable!("selected Serial base")
3612                };
3613                if stored_branch_id != branch_id {
3614                    return Err(DbError::Message(
3615                        "Serial database contains another unresolved branch".to_string(),
3616                    ));
3617                }
3618                if base.as_ref().is_some_and(|base| base != &stored_base) {
3619                    return Err(DbError::Message(
3620                        "Serial discard branch has inconsistent bases".to_string(),
3621                    ));
3622                }
3623                base.get_or_insert(stored_base);
3624                let status: WriteStatus = serde_json::from_str(&raw_status)
3625                    .map_err(|error| DbError::Message(format!("Serial discard status: {error}")))?;
3626                match status {
3627                    WriteStatus::Pending | WriteStatus::Blocked(_) if raw_prepared.is_none() => {
3628                        saw_local = true;
3629                    }
3630                    WriteStatus::Publishing if raw_prepared.is_some() => saw_prepared = true,
3631                    WriteStatus::Conflict(conflict) if conflict.branch_id == branch_id => {
3632                        saw_conflict = true;
3633                    }
3634                    other => {
3635                        return Err(DbError::Message(format!(
3636                            "Serial branch has non-discardable state {other:?}"
3637                        )));
3638                    }
3639                }
3640            }
3641            match (saw_local, saw_prepared, saw_conflict) {
3642                (true, false, false) => Ok(SerialBranchDiscardState::Local),
3643                (false, true, false) => Ok(SerialBranchDiscardState::Abandonment),
3644                (false, false, true) => {
3645                    base.expect("nonempty branch");
3646                    Ok(SerialBranchDiscardState::Conflict)
3647                }
3648                _ => Err(DbError::Message(
3649                    "Serial branch mixes incompatible discard states".to_string(),
3650                )),
3651            }
3652        })
3653        .await
3654    }
3655
3656    pub async fn discard_local_serial_branch(
3657        &self,
3658        branch_id: PendingBranchId,
3659    ) -> Result<Vec<WriteId>, DbError> {
3660        let synced_tables = self.synced_tables().to_vec();
3661        let statuses = self.state.write_statuses.clone();
3662        self.call(move |conn| {
3663            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
3664            let mut statement = tx
3665                .prepare(
3666                    "SELECT write_id, base, status, prepared, inverse_changeset
3667                     FROM store_writes
3668                     WHERE status != '\"local_only\"'
3669                       AND json_extract(status, '$.published') IS NULL
3670                       AND json_extract(status, '$.resolved') IS NULL
3671                       AND json_type(base, '$.serial') IS NOT NULL
3672                     ORDER BY ordinal",
3673                )
3674                .map_err(DbError::from)?;
3675            let rows = statement
3676                .query_map([], |row| {
3677                    Ok((
3678                        row.get::<_, String>(0)?,
3679                        row.get::<_, String>(1)?,
3680                        row.get::<_, String>(2)?,
3681                        row.get::<_, Option<String>>(3)?,
3682                        row.get::<_, Vec<u8>>(4)?,
3683                    ))
3684                })
3685                .map_err(DbError::from)?
3686                .collect::<Result<Vec<_>, _>>()
3687                .map_err(DbError::from)?;
3688            drop(statement);
3689            if rows.is_empty() {
3690                return Err(DbError::Message("Serial branch is absent".to_string()));
3691            }
3692            let mut branch = Vec::new();
3693            for (write_id, raw_base, raw_status, prepared, inverse) in rows {
3694                let StoreWriteBase::Serial {
3695                    branch_id: stored_branch_id,
3696                    ..
3697                } = serde_json::from_str(&raw_base).map_err(|error| {
3698                    DbError::Message(format!("local Serial discard base: {error}"))
3699                })?
3700                else {
3701                    unreachable!("selected Serial base")
3702                };
3703                let status: WriteStatus = serde_json::from_str(&raw_status).map_err(|error| {
3704                    DbError::Message(format!("local Serial discard status: {error}"))
3705                })?;
3706                if stored_branch_id != branch_id
3707                    || !matches!(status, WriteStatus::Pending | WriteStatus::Blocked(_))
3708                    || prepared.is_some()
3709                {
3710                    return Err(DbError::Message(
3711                        "Serial branch is not locally discardable".to_string(),
3712                    ));
3713                }
3714                branch.push((WriteId::from_generated(write_id), inverse));
3715            }
3716            let schema = Arc::new(crate::sync::conflict::TableSchema::from_db(
3717                &tx,
3718                &synced_tables,
3719            )?);
3720            for (_, inverse) in branch.iter().rev() {
3721                let inverse = crate::sync::apply::ValidatedChangeset::new(inverse, schema.clone())
3722                    .map_err(|error| {
3723                        DbError::Message(format!("invalid Serial inverse: {error}"))
3724                    })?;
3725                crate::sync::apply::apply_changeset_strict_on(&tx, inverse)
3726                    .map_err(|error| DbError::Message(format!("reverse Serial branch: {error}")))?;
3727            }
3728            let write_ids = branch
3729                .into_iter()
3730                .map(|(write_id, _)| write_id)
3731                .collect::<Vec<_>>();
3732            let resolution = WriteResolution::Discarded;
3733            Self::resolve_unpublished_writes_on(&tx, &write_ids, &resolution)?;
3734            tx.commit().map_err(DbError::from)?;
3735            let status = WriteStatus::Resolved(resolution);
3736            for write_id in &write_ids {
3737                Self::notify_write_status_in(&statuses, write_id, status.clone());
3738            }
3739            Ok(write_ids)
3740        })
3741        .await
3742    }
3743
3744    pub async fn conflicted_serial_branch_base(
3745        &self,
3746        branch_id: &PendingBranchId,
3747    ) -> Result<Option<StoreBatchCommitRef>, DbError> {
3748        let branch = self.unresolved_serial_branch().await?.ok_or_else(|| {
3749            DbError::Message(format!(
3750                "Serial branch {} does not exist",
3751                branch_id.first_write_id()
3752            ))
3753        })?;
3754        if &branch.branch_id != branch_id || !branch.conflicted {
3755            return Err(DbError::Message(format!(
3756                "Serial branch {} is not conflicted",
3757                branch_id.first_write_id()
3758            )));
3759        }
3760        Ok(branch.base)
3761    }
3762
3763    pub(crate) async fn unresolved_serial_branch(
3764        &self,
3765    ) -> Result<Option<UnresolvedSerialBranch>, DbError> {
3766        self.call(|conn| {
3767            let mut statement = conn
3768                .prepare(
3769                    "SELECT base, status FROM store_writes
3770                     WHERE status != '\"local_only\"'
3771                       AND json_extract(status, '$.published') IS NULL
3772                       AND json_extract(status, '$.resolved') IS NULL
3773                       AND json_type(base, '$.serial') IS NOT NULL
3774                     ORDER BY ordinal",
3775                )
3776                .map_err(DbError::from)?;
3777            let rows = statement
3778                .query_map([], |row| {
3779                    Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
3780                })
3781                .map_err(DbError::from)?;
3782            let mut branch = None;
3783            for row in rows {
3784                let (raw_base, raw_status) = row.map_err(DbError::from)?;
3785                let StoreWriteBase::Serial { branch_id, base } = serde_json::from_str(&raw_base)
3786                    .map_err(|error| {
3787                        DbError::Message(format!("unresolved Serial base: {error}"))
3788                    })?
3789                else {
3790                    return Err(DbError::Message(
3791                        "MergeConcurrent base reached a Serial branch query".to_string(),
3792                    ));
3793                };
3794                let status: WriteStatus = serde_json::from_str(&raw_status).map_err(|error| {
3795                    DbError::Message(format!("unresolved Serial status: {error}"))
3796                })?;
3797                match &mut branch {
3798                    None => {
3799                        branch = Some(UnresolvedSerialBranch {
3800                            branch_id,
3801                            base,
3802                            conflicted: matches!(status, WriteStatus::Conflict(_)),
3803                        });
3804                    }
3805                    Some(existing) if existing.branch_id == branch_id && existing.base == base => {
3806                        existing.conflicted |= matches!(status, WriteStatus::Conflict(_));
3807                    }
3808                    Some(_) => {
3809                        return Err(DbError::Message(
3810                            "Serial database contains more than one unresolved branch".to_string(),
3811                        ));
3812                    }
3813                }
3814            }
3815            Ok(branch)
3816        })
3817        .await
3818    }
3819
3820    pub async fn subscribe_write_status(
3821        &self,
3822        write_id: &WriteId,
3823    ) -> Result<tokio::sync::watch::Receiver<WriteStatus>, DbError> {
3824        let write_id = write_id.clone();
3825        let statuses = self.state.write_statuses.clone();
3826        self.call(move |conn| {
3827            let raw: String = conn
3828                .query_row(
3829                    "SELECT status FROM store_writes WHERE write_id = ?1",
3830                    [write_id.as_str()],
3831                    |row| row.get(0),
3832                )
3833                .map_err(DbError::from)?;
3834            let current: WriteStatus = serde_json::from_str(&raw)
3835                .map_err(|error| DbError::Message(format!("write {write_id} status: {error}")))?;
3836            let mut senders = statuses.lock().expect("write status mutex poisoned");
3837            let sender = senders
3838                .entry(write_id)
3839                .or_insert_with(|| tokio::sync::watch::channel(current.clone()).0);
3840            sender.send_replace(current);
3841            Ok(sender.subscribe())
3842        })
3843        .await
3844    }
3845
3846    /// Lock the complete membership load transaction for this database handle
3847    /// and every clone that shares its state.
3848    pub(crate) async fn lock_membership_load(&self) -> tokio::sync::OwnedMutexGuard<()> {
3849        self.state.membership_load.clone().lock_owned().await
3850    }
3851
3852    pub(crate) async fn lock_membership_mutation(&self) -> tokio::sync::OwnedMutexGuard<()> {
3853        self.state.membership_mutation.clone().lock_owned().await
3854    }
3855
3856    pub(crate) async fn lock_snapshot_publication(&self) -> tokio::sync::OwnedMutexGuard<()> {
3857        self.state.snapshot_publication.clone().lock_owned().await
3858    }
3859
3860    pub(crate) async fn lock_local_blob_cleanup(&self) -> tokio::sync::OwnedMutexGuard<()> {
3861        self.state.local_blob_cleanup.clone().lock_owned().await
3862    }
3863
3864    #[cfg(any(test, feature = "test-utils"))]
3865    #[doc(hidden)]
3866    pub fn arm_test_pause(
3867        &self,
3868        point: DatabaseTestPoint,
3869    ) -> (Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>) {
3870        self.state.test_pause_points.arm(point)
3871    }
3872
3873    #[cfg(any(test, feature = "test-utils"))]
3874    #[doc(hidden)]
3875    pub fn observe_test_points(&self) -> tokio::sync::mpsc::UnboundedReceiver<DatabaseTestPoint> {
3876        self.state.test_pause_points.observe()
3877    }
3878
3879    #[cfg(any(test, feature = "test-utils"))]
3880    pub(crate) async fn reach_test_point(&self, point: DatabaseTestPoint) {
3881        self.state.test_pause_points.reach(point).await;
3882    }
3883
3884    /// Open and own the connection at `path`.
3885    ///
3886    /// Runs the host migration ladder and validates its final sync-routing
3887    /// contract in one transaction. A fresh database creates Coven metadata in
3888    /// that transaction; an initialized database commits only when the final
3889    /// contract exactly matches its pinned bytes. Then seeds the register clock
3890    /// from on-disk rows. Returns the handle plus the non-optional `_updated_at`
3891    /// stamper the host binds into every synced-row write.
3892    ///
3893    pub fn open(
3894        path: &Path,
3895        synced_tables: Vec<SyncedTable>,
3896        blob_tombstone_grace: chrono::Duration,
3897        transfer_limits: crate::blob::TransferLimits,
3898        write_policy: WritePolicy,
3899        device_id: String,
3900        migrations: &[Migration],
3901    ) -> Result<(Database, UpdatedAtStamper), OpenError> {
3902        let hlc =
3903            Hlc::try_new(device_id).map_err(|e| DbError::Message(format!("device_id {e}")))?;
3904        Self::open_with_hlc_and_coven_metadata(
3905            path,
3906            synced_tables,
3907            blob_tombstone_grace,
3908            transfer_limits,
3909            write_policy,
3910            Arc::new(hlc),
3911            migrations,
3912            CovenMetadataOpen::Detect,
3913        )
3914    }
3915
3916    pub(crate) fn open_initialized_store(
3917        path: &Path,
3918        synced_tables: Vec<SyncedTable>,
3919        blob_tombstone_grace: chrono::Duration,
3920        transfer_limits: crate::blob::TransferLimits,
3921        write_policy: WritePolicy,
3922        device_id: String,
3923        migrations: &[Migration],
3924    ) -> Result<(Database, UpdatedAtStamper), OpenError> {
3925        let hlc =
3926            Hlc::try_new(device_id).map_err(|e| DbError::Message(format!("device_id {e}")))?;
3927        Self::open_with_hlc_and_coven_metadata(
3928            path,
3929            synced_tables,
3930            blob_tombstone_grace,
3931            transfer_limits,
3932            write_policy,
3933            Arc::new(hlc),
3934            migrations,
3935            CovenMetadataOpen::InitializeVerifiedSnapshot,
3936        )
3937    }
3938
3939    /// Open with a caller-supplied register clock instead of a fresh
3940    /// system-wall-clock one. Lets a test inject an [`Hlc`] over a controlled
3941    /// wall clock to exercise the skew/restart-seeding guarantees, sharing the
3942    /// production open path (migration, seed, session) so the test drives the
3943    /// real unit.
3944    ///
3945    #[cfg(any(test, feature = "test-utils"))]
3946    pub(crate) fn open_with_hlc(
3947        path: &Path,
3948        synced_tables: Vec<SyncedTable>,
3949        blob_tombstone_grace: chrono::Duration,
3950        transfer_limits: crate::blob::TransferLimits,
3951        write_policy: WritePolicy,
3952        hlc: Arc<Hlc>,
3953        migrations: &[Migration],
3954    ) -> Result<(Database, UpdatedAtStamper), OpenError> {
3955        Self::open_with_hlc_and_coven_metadata(
3956            path,
3957            synced_tables,
3958            blob_tombstone_grace,
3959            transfer_limits,
3960            write_policy,
3961            hlc,
3962            migrations,
3963            CovenMetadataOpen::Detect,
3964        )
3965    }
3966
3967    fn open_with_hlc_and_coven_metadata(
3968        path: &Path,
3969        synced_tables: Vec<SyncedTable>,
3970        blob_tombstone_grace: chrono::Duration,
3971        transfer_limits: crate::blob::TransferLimits,
3972        write_policy: WritePolicy,
3973        hlc: Arc<Hlc>,
3974        migrations: &[Migration],
3975        metadata_open: CovenMetadataOpen,
3976    ) -> Result<(Database, UpdatedAtStamper), OpenError> {
3977        let (core, state, stamper) = DatabaseCore::open(
3978            path,
3979            synced_tables,
3980            blob_tombstone_grace,
3981            transfer_limits,
3982            write_policy,
3983            hlc,
3984            migrations,
3985            metadata_open,
3986        )?;
3987
3988        let database = {
3989            let (jobs_tx, jobs_rx) = tokio::sync::mpsc::unbounded_channel::<DbJob>();
3990            let join = std::thread::Builder::new()
3991                .name("coven-db".to_string())
3992                .spawn(move || run_connection_thread(core, jobs_rx))
3993                .map_err(|e| DbError::Message(format!("spawn database connection thread: {e}")))?;
3994            Database {
3995                thread: Arc::new(ConnectionThread {
3996                    jobs: jobs_tx,
3997                    join: Some(join),
3998                }),
3999                state,
4000            }
4001        };
4002
4003        Ok((database, stamper))
4004    }
4005
4006    /// Open the store at `path` read-only for a same-store secondary reader
4007    /// (e.g. a separate process reading while another holds the writer open).
4008    ///
4009    /// Distinct from [`Database::open`] in three ways, all so the reader never
4010    /// mutates shared state a concurrent writer owns: the connection is
4011    /// `SQLITE_OPEN_READONLY`; no migration ladder or bookkeeping DDL runs (it
4012    /// opens against the schema the writer left, and refuses one newer than this
4013    /// binary knows — the writer's `SchemaTooNew` policy); and it returns no
4014    /// stamper, because a reader mints no `_updated_at`. Reads are safe across
4015    /// processes because the writer opens the db in WAL mode, so a reader observes
4016    /// committed rows while the writer commits more.
4017    ///
4018    /// The caller takes no store open-lock for a read-only open: the exclusive
4019    /// advisory lock guards against a second *writer*, and a read-only connection
4020    /// cannot write, so multiple readers and one writer coexist under WAL.
4021    pub fn open_read_only(
4022        path: &Path,
4023        synced_tables: Vec<SyncedTable>,
4024        blob_tombstone_grace: chrono::Duration,
4025        transfer_limits: crate::blob::TransferLimits,
4026        write_policy: WritePolicy,
4027        device_id: String,
4028        migrations: &[Migration],
4029    ) -> Result<Database, OpenError> {
4030        let hlc =
4031            Hlc::try_new(device_id).map_err(|e| DbError::Message(format!("device_id {e}")))?;
4032        let (core, state) = DatabaseCore::open_read_only(
4033            path,
4034            synced_tables,
4035            blob_tombstone_grace,
4036            transfer_limits,
4037            write_policy,
4038            Arc::new(hlc),
4039            migrations,
4040        )?;
4041        let (jobs_tx, jobs_rx) = tokio::sync::mpsc::unbounded_channel::<DbJob>();
4042        let join = std::thread::Builder::new()
4043            .name("coven-db-ro".to_string())
4044            .spawn(move || run_connection_thread(core, jobs_rx))
4045            .map_err(|e| DbError::Message(format!("spawn database connection thread: {e}")))?;
4046        Ok(Database {
4047            thread: Arc::new(ConnectionThread {
4048                jobs: jobs_tx,
4049                join: Some(join),
4050            }),
4051            state,
4052        })
4053    }
4054
4055    /// The host's declared synced-table set, the single owner of which tables
4056    /// participate in changeset sync. Each journaled write's capture session
4057    /// attaches exactly these, the register-clock seed scanned these, and the
4058    /// gate/apply operate over these — so the sync layer reads the set from here
4059    /// instead of carrying a separately-passed copy that could silently diverge.
4060    pub fn synced_tables(&self) -> &[SyncedTable] {
4061        &self.state.synced_tables
4062    }
4063
4064    /// The host's blob-tombstone convergence window. The tombstone GC ages each
4065    /// tombstone's `deleted_at` against this to decide when a deleted blob may be
4066    /// erased. Fixed for this handle's life.
4067    pub fn blob_tombstone_grace(&self) -> chrono::Duration {
4068        self.state.blob_tombstone_grace
4069    }
4070
4071    /// How many blob transfers each transfer loop may run at once. Read by the
4072    /// upload drain ([`crate::blob::upload::drain_uploads`]) and the pin loop
4073    /// ([`crate::blob::cache::pin`]). Fixed for this handle's life.
4074    pub fn transfer_limits(&self) -> crate::blob::TransferLimits {
4075        self.state.transfer_limits
4076    }
4077
4078    pub fn write_policy(&self) -> WritePolicy {
4079        self.state.write_policy
4080    }
4081
4082    /// The gate model resolved from the final synced table set and live schema at
4083    /// open. Fixed for this handle's life.
4084    #[doc(hidden)]
4085    pub fn gates(&self) -> Arc<Gates> {
4086        self.state.gates.clone()
4087    }
4088
4089    /// Blob declarations resolved from the final synced table set and live schema at
4090    /// open. Fixed for this handle's life.
4091    #[doc(hidden)]
4092    pub fn blob_decls(&self) -> Arc<BlobDecls> {
4093        self.state.blob_decls.clone()
4094    }
4095
4096    /// The applied synced-schema version — `PRAGMA user_version` after the
4097    /// migration ladder ran at open. This is the single source of the wire
4098    /// `schema_version`: every outgoing changeset is stamped with it, the pull
4099    /// gates compare incoming changesets and the min-floor against it, and the
4100    /// snapshot meta carries it. A device cannot stamp a version it has not
4101    /// migrated to. Cached because migrations run only at open, so the value is
4102    /// fixed for the handle's life.
4103    pub fn schema_version(&self) -> u32 {
4104        self.state.schema_version
4105    }
4106
4107    /// Hash of the declarations and live schema shape that decide row routing
4108    /// and confidentiality for this Store.
4109    pub fn sync_routing_hash(&self) -> ObjectHash {
4110        self.state.sync_routing_hash
4111    }
4112
4113    /// The shared register clock. coven's sync layer records pulled rows as its
4114    /// floor and stamps envelopes off it; it is the same `Arc<Hlc>` the stamper wraps.
4115    pub fn hlc(&self) -> Arc<Hlc> {
4116        self.state.hlc.clone()
4117    }
4118
4119    pub fn stamper(&self) -> UpdatedAtStamper {
4120        UpdatedAtStamper::new(self.state.hlc.clone())
4121    }
4122
4123    /// The receiver's current wall-clock millis, read from this database's
4124    /// register clock. The pull reads it once and passes it down to bound an
4125    /// incoming `_updated_at`'s physical component (a grossly-future stamp must not
4126    /// win last-writer-wins or ratchet the clock).
4127    pub(crate) fn receive_wall_ms(&self) -> u64 {
4128        self.state.hlc.wall_now_ms()
4129    }
4130
4131    /// Run `f` against the connection and await the result.
4132    ///
4133    /// This is how coven runs bookkeeping, gating reads, raw test writes, and
4134    /// apply — anything that needs `&Connection`. Public host writes and coven
4135    /// transitions that mutate synced rows wrap their write in a
4136    /// [`Self::run_internal_store_write_transaction_on`] transaction (still through
4137    /// `call`) so it lands in the pending-changeset journal.
4138    ///
4139    /// Hands `f` to the connection thread and awaits its reply, so the SQL runs
4140    /// off the async executor.
4141    pub async fn call<F, R>(&self, f: F) -> Result<R, DbError>
4142    where
4143        F: FnOnce(&Connection) -> Result<R, DbError> + Send + 'static,
4144        R: Send + 'static,
4145    {
4146        self.on_connection_thread(move |core| f(core.connection()))
4147            .await
4148    }
4149
4150    /// Send `f` to the connection thread, run it against the owned core there, and
4151    /// await its result. A panic in `f` is caught on the connection thread (so it
4152    /// cannot unwind the thread and take the connection with it) and resumed on
4153    /// this task, matching the pre-thread behavior where the closure panicked
4154    /// directly on the caller.
4155    ///
4156    /// Cancellation: once dispatched, `f` runs to completion on the connection
4157    /// thread regardless of whether the caller is still awaiting. If the caller is
4158    /// cancelled between the thread committing and this reply resolving, it never
4159    /// observes the result even though the effect landed — the same "the operation
4160    /// may have committed" contract any network call carries. This is deliberate:
4161    /// the durable database state is the source of truth, and a caller must treat
4162    /// a cancelled call as possibly-committed. Follow-ups that matter beyond that
4163    /// durable state — observer notifications, publish triggers — are not driven
4164    /// off this return value; the sync cycle re-derives them from durable state.
4165    async fn on_connection_thread<F, R>(&self, f: F) -> R
4166    where
4167        F: FnOnce(&mut DatabaseCore) -> R + Send + 'static,
4168        R: Send + 'static,
4169    {
4170        let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
4171        let job = DbJob::Run(Box::new(move |core| {
4172            let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(core)));
4173            // The caller may have been cancelled and dropped `reply_rx`; a failed
4174            // send is that normal outcome, not an error.
4175            let _ = reply_tx.send(outcome);
4176        }));
4177        if self.thread.jobs.send(job).is_err() {
4178            panic!("database connection thread stopped before a call completed");
4179        }
4180        match reply_rx.await {
4181            Ok(Ok(value)) => value,
4182            Ok(Err(panic)) => std::panic::resume_unwind(panic),
4183            Err(_) => {
4184                panic!("database connection thread dropped a call's reply without responding")
4185            }
4186        }
4187    }
4188
4189    fn start_host_change_journal_on<'c>(
4190        conn: &'c Connection,
4191        synced_tables: &[SyncedTable],
4192    ) -> Result<rusqlite::session::Session<'c>, DbError> {
4193        attach_session(conn, synced_tables)
4194    }
4195
4196    fn drain_host_change_journal_on(
4197        session: &mut rusqlite::session::Session<'_>,
4198    ) -> Result<Vec<u8>, DbError> {
4199        capture_changeset(session)
4200    }
4201
4202    fn invert_changeset(changeset: &[u8]) -> Result<Vec<u8>, DbError> {
4203        if changeset.is_empty() {
4204            return Ok(Vec::new());
4205        }
4206        let mut inverse = Vec::new();
4207        rusqlite::session::invert_strm(&mut &changeset[..], &mut inverse).map_err(DbError::from)?;
4208        Ok(inverse)
4209    }
4210
4211    fn run_host_sql_on<R, E>(conn: &Connection, f: impl FnOnce() -> Result<R, E>) -> Result<R, E>
4212    where
4213        E: From<DbError>,
4214    {
4215        conn.authorizer(Some(authorize_host_sql))
4216            .map_err(DbError::from)
4217            .map_err(E::from)?;
4218        let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
4219        if let Err(error) = conn.authorizer(
4220            None::<fn(rusqlite::hooks::AuthContext<'_>) -> rusqlite::hooks::Authorization>,
4221        ) {
4222            panic!("failed to remove host SQL gate-baseline guard: {error}");
4223        }
4224        match outcome {
4225            Ok(result) => result,
4226            Err(panic) => std::panic::resume_unwind(panic),
4227        }
4228    }
4229
4230    fn capture_host_changes_on<R, E>(
4231        conn: &Connection,
4232        synced_tables: &[SyncedTable],
4233        f: impl FnOnce() -> Result<R, E>,
4234    ) -> Result<(R, Vec<u8>), E>
4235    where
4236        E: From<DbError>,
4237    {
4238        let mut journal =
4239            Self::start_host_change_journal_on(conn, synced_tables).map_err(E::from)?;
4240        let value = Self::run_host_sql_on(conn, f)?;
4241        let captured = Self::drain_host_change_journal_on(&mut journal).map_err(E::from)?;
4242        crate::sync::session::validate_changeset_row_identities(&captured, synced_tables)
4243            .map_err(|error| DbError::Message(error.to_string()))
4244            .map_err(E::from)?;
4245        Ok((value, captured))
4246    }
4247
4248    fn capture_store_write_blob_facts_on(
4249        tx: &rusqlite::Transaction<'_>,
4250        changeset: &[u8],
4251        blob_decls: &BlobDecls,
4252    ) -> Result<StoreWriteBlobFacts, DbError> {
4253        let changes = crate::changeset::walk(changeset)
4254            .map_err(|error| DbError::Message(format!("read Store write blobs: {error}")))?;
4255        let mut facts = BTreeMap::new();
4256        for change in changes {
4257            let Some(publication) = blob_decls
4258                .publication_blob_from_change(tx, &change)
4259                .map_err(|error| DbError::Message(format!("capture Store write blob: {error}")))?
4260            else {
4261                continue;
4262            };
4263            let plaintext_hash = publication.plaintext_hash.parse().map_err(|error| {
4264                DbError::Message(format!(
4265                    "capture Store write blob {}/{} plaintext hash: {error}",
4266                    publication.blob.namespace, publication.blob.id
4267                ))
4268            })?;
4269            let external_path = if publication.blob.provenance == Provenance::UserProvided {
4270                tx.query_row(
4271                    "SELECT path FROM local_blob_refs
4272                     WHERE table_name = ?1 AND row_id = ?2 AND column_name = ?3
4273                       AND row_stamp = ?4 AND namespace = ?5 AND blob_id = ?6",
4274                    rusqlite::params![
4275                        publication.table,
4276                        publication.row_id,
4277                        publication.column,
4278                        publication.row_stamp,
4279                        publication.blob.namespace,
4280                        publication.blob.id,
4281                    ],
4282                    |row| row.get::<_, String>(0),
4283                )
4284                .optional()
4285                .map_err(DbError::from)?
4286                .map(PathBuf::from)
4287            } else {
4288                None
4289            };
4290            let previous = previous_row_blob_for_write_on(
4291                tx,
4292                &publication.table,
4293                &publication.row_id,
4294                &publication.row_stamp,
4295                &publication.column,
4296                &publication.blob,
4297                publication.plaintext_size,
4298                plaintext_hash,
4299            )?;
4300            let fact = StoreWriteBlobFact {
4301                table: publication.table,
4302                row_id: publication.row_id,
4303                row_stamp: publication.row_stamp,
4304                column: publication.column,
4305                blob: publication.blob,
4306                plaintext_size: publication.plaintext_size,
4307                plaintext_hash,
4308                external_path,
4309                previous,
4310            };
4311            let key = fact.identity_key();
4312            if let Some(prior) = facts.insert(key.clone(), fact.clone()) {
4313                if prior != fact {
4314                    return Err(DbError::Message(format!(
4315                        "Store write gives row {}/{}/{} at {} conflicting blob facts",
4316                        key.0, key.1, key.2, key.3
4317                    )));
4318                }
4319            }
4320        }
4321        Ok(StoreWriteBlobFacts {
4322            blobs: facts.into_values().collect(),
4323        })
4324    }
4325
4326    fn capture_partition_blob_facts_on(
4327        tx: &rusqlite::Transaction<'_>,
4328        partitions: &[gate::AudiencePartition],
4329        blob_decls: &BlobDecls,
4330    ) -> Result<StoreWriteBlobFacts, DbError> {
4331        let mut facts = BTreeMap::new();
4332        for partition in partitions {
4333            for fact in
4334                Self::capture_store_write_blob_facts_on(tx, &partition.changeset, blob_decls)?.blobs
4335            {
4336                let key = fact.identity_key();
4337                if let Some(prior) = facts.insert(key.clone(), fact.clone()) {
4338                    if prior != fact {
4339                        return Err(DbError::Message(format!(
4340                            "audience partitions give row {}/{}/{} at {} conflicting blob facts",
4341                            key.0, key.1, key.2, key.3
4342                        )));
4343                    }
4344                }
4345            }
4346        }
4347        Ok(StoreWriteBlobFacts {
4348            blobs: facts.into_values().collect(),
4349        })
4350    }
4351
4352    fn partition_captured_write_on(
4353        tx: &rusqlite::Transaction<'_>,
4354        captured: &[u8],
4355        gates: &Gates,
4356        write_policy: WritePolicy,
4357        routing: StoreWriteRouting<'_>,
4358    ) -> Result<Vec<gate::AudiencePartition>, DbError> {
4359        match routing {
4360            StoreWriteRouting::MergeScoped(encryption) => {
4361                let store_root_hash = Self::required_store_root_hash_on(tx)?;
4362                let key = crate::sync::circle::derive_row_routing_key(encryption, store_root_hash)
4363                    .map_err(|error| {
4364                        DbError::Message(format!("derive row routing key: {error}"))
4365                    })?;
4366                let routing_changeset = gate::capture_routing_changes(tx, captured, gates, &key)
4367                    .map_err(|error| {
4368                        DbError::Message(format!("capture scoped routing changes: {error}"))
4369                    })?;
4370                gate::partition_outbound(tx, captured, &routing_changeset, gates, write_policy)
4371                    .map_err(|error| {
4372                        DbError::Message(format!("partition scoped host transaction: {error}"))
4373                    })
4374            }
4375            StoreWriteRouting::SerialScoped => gate::partition_outbound(
4376                tx,
4377                captured,
4378                &gate::RoutingChanges::empty(),
4379                gates,
4380                write_policy,
4381            )
4382            .map_err(|error| {
4383                DbError::Message(format!("partition scoped host transaction: {error}"))
4384            }),
4385            StoreWriteRouting::Unscoped => {
4386                let changeset = gate::gate_outbound(tx, captured, gates)
4387                    .map_err(|error| DbError::Message(format!("gate host transaction: {error}")))?;
4388                Ok((!changeset.is_empty())
4389                    .then_some(gate::AudiencePartition {
4390                        audience: crate::sync::circle::Audience::Store,
4391                        control: None,
4392                        changeset,
4393                    })
4394                    .into_iter()
4395                    .collect())
4396            }
4397        }
4398    }
4399
4400    fn store_write_routing<'a>(
4401        gates: &Gates,
4402        write_policy: WritePolicy,
4403        routing_encryption: Option<&'a EncryptionService>,
4404    ) -> Result<StoreWriteRouting<'a>, DbError> {
4405        if !gates.has_scoped_graph() {
4406            return Ok(StoreWriteRouting::Unscoped);
4407        }
4408        match write_policy {
4409            WritePolicy::MergeConcurrent => routing_encryption
4410                .map(StoreWriteRouting::MergeScoped)
4411                .ok_or_else(|| {
4412                    DbError::Message(
4413                        "Merge scoped write requires the Store generation-1 routing key"
4414                            .to_string(),
4415                    )
4416                }),
4417            WritePolicy::Serial => Ok(StoreWriteRouting::SerialScoped),
4418        }
4419    }
4420
4421    pub(crate) fn validate_store_write_routing(
4422        gates: &Gates,
4423        write_policy: WritePolicy,
4424        routing_encryption: Option<&EncryptionService>,
4425    ) -> Result<(), DbError> {
4426        Self::store_write_routing(gates, write_policy, routing_encryption).map(drop)
4427    }
4428
4429    fn insert_store_write_on(
4430        tx: &rusqlite::Transaction<'_>,
4431        write_id: &WriteId,
4432        partitions: &[gate::AudiencePartition],
4433        inverse_changeset: &[u8],
4434        base: &StoreWriteBase,
4435        blob_facts: &StoreWriteBlobFacts,
4436        rows_changed: u64,
4437    ) -> Result<WriteStatus, DbError> {
4438        let affected_rows = if partitions.is_empty() {
4439            // A tripwire, not a routine event. An empty capture from a transaction
4440            // that also CHANGED NO ROWS is a pure read left on the write path —
4441            // warn so it gets moved to the journal-free read path
4442            // (`CovenHandle::sql_read`). An empty capture
4443            // from a transaction that DID change rows is a device-local-table
4444            // write (those tables aren't in the session) — a supported, routine
4445            // pattern that stays on `sql()` silently. The one case this misses:
4446            // a conditional write to a synced table that no-op'd this cycle (an
4447            // idempotent INSERT OR IGNORE re-run) also changed no rows and warns;
4448            // legitimate but rare, tolerated.
4449            if rows_changed == 0 {
4450                warn!("journaled sql transaction changed nothing; pure reads belong on sql_read");
4451                // Debug builds name the offender: the backtrace runs through the
4452                // host's monomorphized closure, whose symbol carries the call
4453                // site's module path. Captured only when the warn fires.
4454                #[cfg(debug_assertions)]
4455                warn!(
4456                    "zero-change sql transaction backtrace:\n{}",
4457                    std::backtrace::Backtrace::force_capture()
4458                );
4459            }
4460            Vec::new()
4461        } else {
4462            let mut affected = Vec::new();
4463            for partition in partitions {
4464                affected.extend(
4465                    crate::changeset::walk(&partition.changeset)
4466                        .map_err(|error| {
4467                            DbError::Message(format!("read affected write rows: {error}"))
4468                        })?
4469                        .into_iter()
4470                        .filter(|row| !gate::is_routing_table(&row.table))
4471                        .map(|row| {
4472                            let primary_key = row.pk().map(str::to_owned).ok_or_else(|| {
4473                                DbError::Message(format!(
4474                                    "shared write row in {:?} has no primary key",
4475                                    row.table
4476                                ))
4477                            })?;
4478                            Ok(AffectedRow {
4479                                table: row.table,
4480                                primary_key,
4481                            })
4482                        })
4483                        .collect::<Result<Vec<_>, DbError>>()?,
4484                );
4485            }
4486            affected.sort();
4487            affected.dedup();
4488            affected
4489        };
4490        let status = if partitions.is_empty() {
4491            WriteStatus::LocalOnly
4492        } else {
4493            WriteStatus::Pending
4494        };
4495        let base = serde_json::to_string(base)
4496            .map_err(|error| DbError::Message(format!("serialize pending Store base: {error}")))?;
4497        let status_json = serde_json::to_string(&status)
4498            .map_err(|error| DbError::Message(format!("serialize write status: {error}")))?;
4499        let affected_rows = serde_json::to_string(&affected_rows)
4500            .map_err(|error| DbError::Message(format!("serialize affected rows: {error}")))?;
4501        let blob_facts_json = serde_json::to_string(blob_facts).map_err(|error| {
4502            DbError::Message(format!("serialize Store write blob facts: {error}"))
4503        })?;
4504        let store_changeset = partitions
4505            .iter()
4506            .find(|partition| partition.audience == crate::sync::circle::Audience::Store)
4507            .map(|partition| partition.changeset.as_slice())
4508            .unwrap_or_default();
4509        tx.execute(
4510            "INSERT INTO store_writes
4511             (write_id, status, affected_rows, changeset, inverse_changeset, base, blob_facts)
4512             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
4513            rusqlite::params![
4514                write_id.as_str(),
4515                status_json,
4516                affected_rows,
4517                store_changeset,
4518                inverse_changeset,
4519                base,
4520                blob_facts_json,
4521            ],
4522        )
4523        .map_err(DbError::from)?;
4524        for partition in partitions {
4525            let audience = match partition.audience {
4526                crate::sync::circle::Audience::Store => "store".to_string(),
4527                crate::sync::circle::Audience::Local => {
4528                    return Err(DbError::Message(
4529                        "Local audience must not enter the durable outbound journal".to_string(),
4530                    ));
4531                }
4532                crate::sync::circle::Audience::Circle(circle_id) => circle_id.to_string(),
4533            };
4534            let control = partition
4535                .control
4536                .as_ref()
4537                .map(gate::CirclePartitionControl::stored_json);
4538            tx.execute(
4539                "INSERT INTO store_write_partitions
4540                 (write_id, audience, control_coord, changeset)
4541                 VALUES (?1, ?2, ?3, ?4)",
4542                rusqlite::params![write_id.as_str(), audience, control, partition.changeset,],
4543            )
4544            .map_err(DbError::from)?;
4545        }
4546        if status == WriteStatus::Pending {
4547            for fact in &blob_facts.blobs {
4548                if fact.blob.provenance != Provenance::HostProvided {
4549                    continue;
4550                }
4551                tx.execute(
4552                    "INSERT OR IGNORE INTO store_write_blob_leases
4553                     (write_id, namespace, blob_id) VALUES (?1, ?2, ?3)",
4554                    (write_id.as_str(), &fact.blob.namespace, &fact.blob.id),
4555                )
4556                .map_err(DbError::from)?;
4557            }
4558        }
4559        Ok(status)
4560    }
4561
4562    pub fn run_store_write_transaction_on<R, E>(
4563        conn: &Connection,
4564        synced_tables: &[SyncedTable],
4565        gates: &Gates,
4566        blob_decls: &BlobDecls,
4567        write_policy: WritePolicy,
4568        routing_encryption: Option<&EncryptionService>,
4569        write_id: WriteId,
4570        f: impl FnOnce(&rusqlite::Transaction<'_>) -> Result<R, E>,
4571    ) -> Result<WriteReceipt<R>, E>
4572    where
4573        E: From<DbError>,
4574    {
4575        (|| {
4576            let routing = Self::store_write_routing(gates, write_policy, routing_encryption)
4577                .map_err(E::from)?;
4578            let changes_before = conn.total_changes();
4579            let tx = conn
4580                .unchecked_transaction()
4581                .map_err(DbError::from)
4582                .map_err(E::from)?;
4583            let (value, captured) = Self::capture_host_changes_on(&tx, synced_tables, || f(&tx))?;
4584            let partitions =
4585                Self::partition_captured_write_on(&tx, &captured, gates, write_policy, routing)
4586                    .map_err(E::from)?;
4587            let blob_facts = Self::capture_partition_blob_facts_on(&tx, &partitions, blob_decls)
4588                .map_err(E::from)?;
4589            let rows_changed = conn.total_changes().saturating_sub(changes_before);
4590            let local_stream_id = local_merge_stream_id_on(&tx).map_err(E::from)?;
4591            let inverse_changeset = Self::invert_changeset(&captured).map_err(E::from)?;
4592            let base = match write_policy {
4593                WritePolicy::MergeConcurrent => StoreWriteBase::MergeConcurrent {
4594                    dependencies: Self::materialized_frontier_on(&tx, local_stream_id.as_deref())
4595                        .map_err(E::from)?,
4596                },
4597                WritePolicy::Serial => {
4598                    let existing: Option<String> = tx
4599                        .query_row(
4600                            "SELECT base FROM store_writes
4601                             WHERE status != '\"local_only\"'
4602                               AND json_extract(status, '$.published') IS NULL
4603                               AND json_extract(status, '$.resolved') IS NULL
4604                               AND json_type(base, '$.serial') IS NOT NULL
4605                             ORDER BY ordinal LIMIT 1",
4606                            [],
4607                            |row| row.get(0),
4608                        )
4609                        .optional()
4610                        .map_err(DbError::from)
4611                        .map_err(E::from)?;
4612                    match existing {
4613                        Some(existing) => serde_json::from_str(&existing)
4614                            .map_err(|error| {
4615                                DbError::Message(format!("pending serial branch base: {error}"))
4616                            })
4617                            .map_err(E::from)?,
4618                        None => StoreWriteBase::Serial {
4619                            branch_id: PendingBranchId::from_first_write(write_id.clone()),
4620                            base: Self::latest_position_for_device_on(&tx, SERIAL_STREAM_ID)
4621                                .map_err(E::from)?,
4622                        },
4623                    }
4624                }
4625            };
4626            let status = Self::insert_store_write_on(
4627                &tx,
4628                &write_id,
4629                &partitions,
4630                &inverse_changeset,
4631                &base,
4632                &blob_facts,
4633                rows_changed,
4634            )
4635            .map_err(E::from)?;
4636            let pending_branch_id = match (&status, &base) {
4637                (WriteStatus::LocalOnly, _) | (_, StoreWriteBase::MergeConcurrent { .. }) => None,
4638                (_, StoreWriteBase::Serial { branch_id, .. }) => Some(branch_id.clone()),
4639            };
4640            tx.commit().map_err(DbError::from).map_err(E::from)?;
4641            Ok(WriteReceipt {
4642                value,
4643                write_id,
4644                status,
4645                pending_branch_id,
4646            })
4647        })()
4648    }
4649
4650    pub fn run_internal_store_write_transaction_on<R, E>(
4651        conn: &Connection,
4652        synced_tables: &[SyncedTable],
4653        write_policy: WritePolicy,
4654        routing_encryption: Option<&EncryptionService>,
4655        write_id: WriteId,
4656        f: impl FnOnce(&rusqlite::Transaction<'_>) -> Result<R, E>,
4657    ) -> Result<R, E>
4658    where
4659        E: From<DbError>,
4660    {
4661        let gates = Gates::from_tables(conn, synced_tables)
4662            .map_err(|error| DbError::Message(error.to_string()))
4663            .map_err(E::from)?;
4664        let blob_decls = BlobDecls::from_tables(conn, synced_tables)
4665            .map_err(|error| DbError::Message(error.to_string()))
4666            .map_err(E::from)?;
4667        Self::run_store_write_transaction_on(
4668            conn,
4669            synced_tables,
4670            &gates,
4671            &blob_decls,
4672            write_policy,
4673            routing_encryption,
4674            write_id,
4675            f,
4676        )
4677        .map(|receipt| receipt.value)
4678    }
4679
4680    pub(crate) async fn prepare_store_write(&self) -> Result<Option<PreparedStoreWrite>, DbError> {
4681        let write_policy = self.write_policy();
4682        self.call(move |conn| {
4683            let stored = conn
4684                .query_row(
4685                "SELECT write_id, changeset, inverse_changeset, base, blob_facts FROM store_writes
4686                 WHERE status = '\"pending\"'
4687                   AND ordinal = (
4688                       SELECT MIN(ordinal) FROM store_writes
4689                       WHERE status != '\"local_only\"'
4690                         AND json_extract(status, '$.published') IS NULL
4691                         AND json_extract(status, '$.resolved') IS NULL
4692                   )
4693                   AND NOT EXISTS (
4694                       SELECT 1 FROM store_writes WHERE prepared IS NOT NULL
4695                   )
4696                 ORDER BY ordinal LIMIT 1",
4697                [],
4698                |row| {
4699                    Ok((
4700                        row.get::<_, String>(0)?,
4701                        row.get::<_, Vec<u8>>(1)?,
4702                        row.get::<_, Vec<u8>>(2)?,
4703                        row.get::<_, String>(3)?,
4704                        row.get::<_, String>(4)?,
4705                    ))
4706                },
4707            )
4708                .optional()
4709                .map_err(DbError::from)?;
4710            let Some((write_id, changeset, inverse_changeset, base, blob_facts)) = stored else {
4711                return Ok(None);
4712            };
4713            let partitions = Self::prepared_store_write_partitions_on(
4714                conn,
4715                &write_id,
4716                &changeset,
4717                write_policy,
4718            )?;
4719            Ok(Some(PreparedStoreWrite {
4720                write_id: WriteId::from_generated(write_id),
4721                changeset,
4722                partitions,
4723                inverse_changeset,
4724                base: serde_json::from_str(&base)
4725                    .map_err(|error| DbError::Message(format!("pending write base: {error}")))?,
4726                blob_facts: serde_json::from_str(&blob_facts).map_err(|error| {
4727                    DbError::Message(format!("pending write blob facts: {error}"))
4728                })?,
4729            }))
4730        })
4731        .await
4732    }
4733
4734    fn prepared_store_write_partitions_on(
4735        conn: &Connection,
4736        write_id: &str,
4737        stored_store_changeset: &[u8],
4738        write_policy: WritePolicy,
4739    ) -> Result<PreparedStoreWritePartitions, DbError> {
4740        let mut statement = conn
4741            .prepare(
4742                "SELECT audience, control_coord, changeset
4743                 FROM store_write_partitions
4744                 WHERE write_id = ?1
4745                 ORDER BY CASE audience WHEN 'store' THEN 0 ELSE 1 END,
4746                          audience, control_coord",
4747            )
4748            .map_err(DbError::from)?;
4749        let rows = statement
4750            .query_map([write_id], |row| {
4751                Ok((
4752                    row.get::<_, String>(0)?,
4753                    row.get::<_, Option<String>>(1)?,
4754                    row.get::<_, Vec<u8>>(2)?,
4755                ))
4756            })
4757            .map_err(DbError::from)?;
4758        let mut store = None;
4759        let mut circles = Vec::new();
4760        for row in rows {
4761            let (audience, control, changeset) = row.map_err(DbError::from)?;
4762            if audience == "store" {
4763                if control.is_some() {
4764                    return Err(DbError::Message(format!(
4765                        "pending write {write_id} Store partition carries a Circle control"
4766                    )));
4767                }
4768                if store.is_some() {
4769                    return Err(DbError::Message(format!(
4770                        "pending write {write_id} carries more than one Store partition"
4771                    )));
4772                }
4773                store = Some(gate::AudiencePartition {
4774                    audience: crate::sync::circle::Audience::Store,
4775                    control: None,
4776                    changeset,
4777                });
4778                continue;
4779            }
4780            let circle_id = audience
4781                .parse::<crate::sync::circle::CircleId>()
4782                .map_err(|error| {
4783                    DbError::Message(format!(
4784                        "pending write {write_id} has invalid audience {audience:?}: {error}"
4785                    ))
4786                })?;
4787            let control_json = control.ok_or_else(|| {
4788                DbError::Message(format!(
4789                    "pending write {write_id} Circle {circle_id} has no control coordinate"
4790                ))
4791            })?;
4792            let control =
4793                gate::CirclePartitionControl::from_stored_json(control_json).map_err(|error| {
4794                    DbError::Message(format!(
4795                        "pending write {write_id} Circle {circle_id} control coordinate: {error}"
4796                    ))
4797                })?;
4798            let control_policy = match control.coordinate() {
4799                crate::sync::circle::CircleControlCoord::MergeConcurrent { .. } => {
4800                    WritePolicy::MergeConcurrent
4801                }
4802                crate::sync::circle::CircleControlCoord::Serial { .. } => WritePolicy::Serial,
4803            };
4804            if control_policy != write_policy {
4805                return Err(DbError::Message(format!(
4806                    "pending write {write_id} Circle {circle_id} control uses {control_policy:?}, database uses {write_policy:?}"
4807                )));
4808            }
4809            circles.push(gate::AudiencePartition {
4810                audience: crate::sync::circle::Audience::Circle(circle_id),
4811                control: Some(control),
4812                changeset,
4813            });
4814        }
4815        drop(statement);
4816        match &store {
4817            Some(partition) if partition.changeset != stored_store_changeset => {
4818                return Err(DbError::Message(format!(
4819                    "pending write {write_id} Store partition differs from store_writes.changeset"
4820                )));
4821            }
4822            None if !stored_store_changeset.is_empty() => {
4823                return Err(DbError::Message(format!(
4824                    "pending write {write_id} has a store_writes.changeset without a Store partition"
4825                )));
4826            }
4827            Some(_) | None => {}
4828        }
4829        if store.is_none() && circles.is_empty() {
4830            return Err(DbError::Message(format!(
4831                "pending write {write_id} has no durable audience partitions"
4832            )));
4833        }
4834        Ok(PreparedStoreWritePartitions { store, circles })
4835    }
4836
4837    pub(crate) async fn reserve_serial_store_branch(
4838        &self,
4839    ) -> Result<Option<SerialStoreBranchPreparationWork>, DbError> {
4840        if self.write_policy() != WritePolicy::Serial {
4841            return Err(DbError::Message(
4842                "Serial branch reservation requires the Serial write policy".to_string(),
4843            ));
4844        }
4845        let statuses = self.state.write_statuses.clone();
4846        self.call(move |conn| {
4847            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
4848            let mut statement = tx
4849                .prepare(
4850                    "SELECT write_id, changeset, inverse_changeset, base, blob_facts, status, prepared
4851                     FROM store_writes
4852                     WHERE status != '\"local_only\"'
4853                       AND json_extract(status, '$.published') IS NULL
4854                       AND json_extract(status, '$.resolved') IS NULL
4855                     ORDER BY ordinal",
4856                )
4857                .map_err(DbError::from)?;
4858            let rows = statement
4859                .query_map([], |row| {
4860                    Ok((
4861                        row.get::<_, String>(0)?,
4862                        row.get::<_, Vec<u8>>(1)?,
4863                        row.get::<_, Vec<u8>>(2)?,
4864                        row.get::<_, String>(3)?,
4865                        row.get::<_, String>(4)?,
4866                        row.get::<_, String>(5)?,
4867                        row.get::<_, Option<String>>(6)?,
4868                    ))
4869                })
4870                .map_err(DbError::from)?;
4871            let mut records = Vec::new();
4872            for row in rows {
4873                records.push(row.map_err(DbError::from)?);
4874            }
4875            drop(statement);
4876            let Some(first) = records.first() else {
4877                tx.commit().map_err(DbError::from)?;
4878                return Ok(None);
4879            };
4880            let first_base: StoreWriteBase = serde_json::from_str(&first.3)
4881                .map_err(|error| DbError::Message(format!("pending Serial write base: {error}")))?;
4882            let StoreWriteBase::Serial { branch_id, base } = first_base else {
4883                return Err(DbError::Message(
4884                    "MergeConcurrent write exists in a Serial database".to_string(),
4885                ));
4886            };
4887            let preparing = serde_json::to_string(&PreparedStoreWriteState::SerialPreparing)
4888                .map_err(|error| DbError::Message(format!("serialize Serial reservation: {error}")))?;
4889            let publishing = serde_json::to_string(&WriteStatus::Publishing)
4890                .map_err(|error| DbError::Message(format!("serialize write status: {error}")))?;
4891            let mut writes = Vec::new();
4892            let mut newly_reserved = Vec::new();
4893            for (write_id, changeset, inverse_changeset, raw_base, blob_facts, status, prepared) in
4894                records
4895            {
4896                let parsed_base: StoreWriteBase = serde_json::from_str(&raw_base)
4897                    .map_err(|error| DbError::Message(format!("pending Serial write base: {error}")))?;
4898                if parsed_base
4899                    != (StoreWriteBase::Serial {
4900                        branch_id: branch_id.clone(),
4901                        base: base.clone(),
4902                    })
4903                {
4904                    break;
4905                }
4906                match (status.as_str(), prepared.as_deref()) {
4907                    ("\"pending\"", None) => {
4908                        let updated = tx
4909                            .execute(
4910                                "UPDATE store_writes SET status = ?2, prepared = ?3
4911                                 WHERE write_id = ?1 AND status = '\"pending\"' AND prepared IS NULL",
4912                                rusqlite::params![&write_id, &publishing, &preparing],
4913                            )
4914                            .map_err(DbError::from)?;
4915                        if updated != 1 {
4916                            return Err(DbError::Message(format!(
4917                                "Serial write {write_id} lost branch reservation"
4918                            )));
4919                        }
4920                        newly_reserved.push(WriteId::from_generated(write_id.clone()));
4921                    }
4922                    ("\"publishing\"", Some(stored)) if stored == preparing => {}
4923                    ("\"publishing\"", Some(_)) => {
4924                        tx.commit().map_err(DbError::from)?;
4925                        return Ok(None);
4926                    }
4927                    _ => {
4928                        tx.commit().map_err(DbError::from)?;
4929                        return Ok(None);
4930                    }
4931                }
4932                let partitions = Self::prepared_store_write_partitions_on(
4933                    &tx,
4934                    &write_id,
4935                    &changeset,
4936                    WritePolicy::Serial,
4937                )?;
4938                writes.push(PreparedStoreWrite {
4939                    write_id: WriteId::from_generated(write_id),
4940                    changeset,
4941                    partitions,
4942                    inverse_changeset,
4943                    base: parsed_base,
4944                    blob_facts: serde_json::from_str(&blob_facts).map_err(|error| {
4945                        DbError::Message(format!("pending Serial write blob facts: {error}"))
4946                    })?,
4947                });
4948            }
4949            if writes.is_empty() {
4950                return Err(DbError::Message("Serial branch reservation is empty".to_string()));
4951            }
4952            tx.commit().map_err(DbError::from)?;
4953            for write_id in newly_reserved {
4954                Self::notify_write_status_in(&statuses, &write_id, WriteStatus::Publishing);
4955            }
4956            Ok(Some(SerialStoreBranchPreparationWork {
4957                branch_id,
4958                base,
4959                writes,
4960            }))
4961        })
4962        .await
4963    }
4964
4965    #[cfg(test)]
4966    pub(crate) async fn enqueue_store_changeset_for_test(
4967        &self,
4968        changeset: Vec<u8>,
4969    ) -> Result<(), DbError> {
4970        let write_id = self.new_write_id();
4971        let blob_decls = self.blob_decls();
4972        let write_policy = self.write_policy();
4973        self.call(move |conn| {
4974            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
4975            let local_stream_id = local_merge_stream_id_on(&tx)?;
4976            let base = match write_policy {
4977                WritePolicy::MergeConcurrent => StoreWriteBase::MergeConcurrent {
4978                    dependencies: Self::materialized_frontier_on(&tx, local_stream_id.as_deref())?,
4979                },
4980                WritePolicy::Serial => StoreWriteBase::Serial {
4981                    branch_id: PendingBranchId::from_first_write(write_id.clone()),
4982                    base: Self::latest_position_for_device_on(&tx, SERIAL_STREAM_ID)?,
4983                },
4984            };
4985            let inverse_changeset = Self::invert_changeset(&changeset)?;
4986            let partitions = vec![gate::AudiencePartition {
4987                audience: crate::sync::circle::Audience::Store,
4988                control: None,
4989                changeset,
4990            }];
4991            let blob_facts = Self::capture_partition_blob_facts_on(&tx, &partitions, &blob_decls)?;
4992            Self::insert_store_write_on(
4993                &tx,
4994                &write_id,
4995                &partitions,
4996                &inverse_changeset,
4997                &base,
4998                &blob_facts,
4999                1,
5000            )?;
5001            tx.commit().map_err(DbError::from)
5002        })
5003        .await
5004    }
5005
5006    pub(crate) async fn prepare_store_write_commit(
5007        &self,
5008        stage: StoreWritePreparation,
5009    ) -> Result<(), DbError> {
5010        let write_id = stage.write_id.clone();
5011        let statuses = self.state.write_statuses.clone();
5012        self.call(move |conn| {
5013            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
5014            let local_device_id: String = tx.query_row(
5015                "SELECT value FROM protocol_state WHERE key = ?1",
5016                [LOCAL_DEVICE_ID_STATE_KEY],
5017                |row| row.get(0),
5018            ).map_err(DbError::from)?;
5019            let root = required_store_root_authority_on(&tx)?;
5020            let (registration_bytes, registration_object): (Vec<u8>, String) = tx.query_row(
5021                "SELECT registration_bytes, registration_object \
5022                 FROM store_device_registration_activations WHERE device_id = ?1",
5023                [&local_device_id],
5024                |row| Ok((row.get(0)?, row.get(1)?)),
5025            ).map_err(DbError::from)?;
5026            let registration_ref: StoreDeviceRegistrationRef =
5027                serde_json::from_str(&registration_object).map_err(|error| {
5028                    DbError::Message(format!("prepared write exact registration ref: {error}"))
5029                })?;
5030            if registration_ref != stage.commit.value.author_registration
5031                || registration_ref != stage.head.value.author_registration
5032            {
5033                return Err(DbError::Message(
5034                    "prepared Store commit/head author registration differs from local activation"
5035                        .to_string(),
5036                ));
5037            }
5038            let registration = StoreDeviceRegistration::parse_at(
5039                &registration_bytes,
5040                &root,
5041                registration_ref.device_id,
5042            ).map_err(|error| DbError::Message(format!(
5043                "prepared write author registration: {error}"
5044            )))?;
5045            registration_ref.verify_registration(&registration)
5046                .map_err(|error| DbError::Message(error.to_string()))?;
5047            let stream_id = AuthorStreamId::store_announcements(&root, &registration_ref);
5048            let coord = StoreCommitCoord::MergeConcurrent {
5049                stream_id,
5050                sequence: stage.commit.value.seq(),
5051            };
5052            stage.commit.value.verify_at(root.store_root_hash, &coord, &registration)
5053                .map_err(|error| DbError::Message(format!(
5054                    "verify prepared Store commit: {error}"
5055                )))?;
5056            if stage.commit.value.write_id != stage.write_id {
5057                return Err(DbError::Message(
5058                    "prepared write id differs from signed commit".to_string(),
5059                ));
5060            }
5061            let commit_ref = StoreBatchCommitRef::from_commit(
5062                &stage.commit.value,
5063                coord,
5064                stage.commit.prepared.reference().clone(),
5065            ).map_err(|error| DbError::Message(error.to_string()))?;
5066            if stage.head.value.commit != commit_ref {
5067                return Err(DbError::Message(
5068                    "prepared Store head does not activate the exact prepared commit".to_string(),
5069                ));
5070            }
5071            StoreDeviceHead::parse_at(
5072                &stage.head.value.to_bytes(),
5073                root.store_root_hash,
5074                &registration,
5075                &commit_ref,
5076            ).map_err(|error| DbError::Message(format!(
5077                "verify prepared Store head: {error}"
5078            )))?;
5079            let (stored_changeset, stored_base, stored_status, stored_preparation): (
5080                Vec<u8>,
5081                String,
5082                String,
5083                Option<String>,
5084            ) = tx
5085                .query_row(
5086                    "SELECT changeset, base, status, prepared
5087                     FROM store_writes WHERE write_id = ?1",
5088                    [stage.write_id.as_str()],
5089                    |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
5090                )
5091                .map_err(DbError::from)?;
5092            if stored_status != "\"pending\"" || stored_preparation.is_some() {
5093                return Err(DbError::Message(format!(
5094                    "write {} is not an unprepared pending write",
5095                    stage.write_id
5096                )));
5097            }
5098            let partitions = Self::prepared_store_write_partitions_on(
5099                &tx,
5100                stage.write_id.as_str(),
5101                &stored_changeset,
5102                WritePolicy::MergeConcurrent,
5103            )?;
5104            let stored_base: StoreWriteBase =
5105                serde_json::from_str(&stored_base).map_err(|error| {
5106                    DbError::Message(format!("write {} base: {error}", stage.write_id))
5107                })?;
5108            let StoreWriteBase::MergeConcurrent {
5109                dependencies: stored_dependencies,
5110            } = stored_base
5111            else {
5112                return Err(DbError::Message(format!(
5113                    "serial write {} reached MergeConcurrent preparation",
5114                    stage.write_id
5115                )));
5116            };
5117            let stored_dependencies = CommitFrontier::from_refs(
5118                WritePolicy::MergeConcurrent,
5119                stored_dependencies,
5120            ).map_err(|error| DbError::Message(format!(
5121                "stored write dependencies: {error}"
5122            )))?;
5123            if stored_dependencies.merge_commits().map_err(|error| {
5124                DbError::Message(error.to_string())
5125            })? != stage.commit.value.merge_dependencies().map_err(|error| {
5126                DbError::Message(format!("prepared Store commit policy: {error}"))
5127            })? {
5128                return Err(DbError::Message(format!(
5129                    "prepared commit dependencies differ from write {}",
5130                    stage.write_id
5131                )));
5132            }
5133            let another_prepared: Option<String> = tx
5134                .query_row(
5135                    "SELECT write_id FROM store_writes
5136                     WHERE prepared IS NOT NULL AND write_id != ?1
5137                     ORDER BY ordinal LIMIT 1",
5138                    [stage.write_id.as_str()],
5139                    |row| row.get(0),
5140                )
5141                .optional()
5142                .map_err(DbError::from)?;
5143            if let Some(other_write_id) = another_prepared {
5144                return Err(DbError::Message(format!(
5145                    "write {other_write_id} already owns Store publication"
5146                )));
5147            }
5148            let durable_predecessor =
5149                Self::latest_position_for_device_on(&tx, &stream_id.to_string())?;
5150            let expected_seq = durable_predecessor
5151                .as_ref()
5152                .map_or(1, |reference| reference.coord.sequence().saturating_add(1));
5153            if stage.commit.value.seq() != expected_seq
5154                || stage.commit.value.order.predecessor() != durable_predecessor.as_ref()
5155            {
5156                return Err(DbError::Message(format!(
5157                    "outbound Store commit exact predecessor differs from durable {durable_predecessor:?}"
5158                )));
5159            }
5160
5161            Self::persist_closed_write_objects_on(
5162                &tx,
5163                &stage.write_id,
5164                root.store_root_hash,
5165                &commit_ref,
5166                &stage.commit.value,
5167                stage.commit.prepared.stored_bytes(),
5168                &partitions,
5169                &stage.remote_objects,
5170                &stage.audiences,
5171            )?;
5172            let head_ref = crate::sync::store_commit::StoreDeviceHeadRef {
5173                head_hash: stage.head.value.head_hash(),
5174                object: stage.head.prepared.reference().clone(),
5175            };
5176            let head_remote = RemoteObjectRecord::candidate_activated_store_head(
5177                head_ref,
5178                stage.head.value.to_bytes(),
5179                stage.head.prepared.stored_bytes().to_vec(),
5180                commit_ref.clone(),
5181            )
5182            .map_err(|error| DbError::Message(format!("prepared Store head: {error}")))?;
5183            persist_exact_remote_object_on(&tx, &head_remote, "Store head")?;
5184
5185            let prepared = PreparedStoreWriteState::MergeConcurrent {
5186                commit: DurablePreparedProtocolObject {
5187                    semantic_bytes: stage.commit.value.to_bytes(),
5188                    prepared: stage.commit.prepared,
5189                },
5190                head: DurablePreparedProtocolObject {
5191                    semantic_bytes: stage.head.value.to_bytes(),
5192                    prepared: stage.head.prepared,
5193                },
5194                local_cleanup: stage.local_cleanup,
5195                completion: stage.completion,
5196            };
5197            let prepared = serde_json::to_string(&prepared).map_err(|error| {
5198                DbError::Message(format!("serialize prepared Store write: {error}"))
5199            })?;
5200            let status = serde_json::to_string(&WriteStatus::Publishing)
5201                .map_err(|error| DbError::Message(format!("serialize write status: {error}")))?;
5202            let updated = tx
5203                .execute(
5204                    "UPDATE store_writes SET prepared = ?2, status = ?3
5205                     WHERE write_id = ?1 AND prepared IS NULL AND status = '\"pending\"'",
5206                    rusqlite::params![stage.write_id.as_str(), prepared, status],
5207                )
5208                .map_err(DbError::from)?;
5209            if updated != 1 {
5210                return Err(DbError::Message(format!(
5211                    "write {} lost pending preparation ownership",
5212                    stage.write_id
5213                )));
5214            }
5215            tx.commit().map_err(DbError::from)?;
5216            Self::notify_write_status_in(&statuses, &write_id, WriteStatus::Publishing);
5217            Ok(())
5218        })
5219        .await
5220    }
5221
5222    pub(crate) async fn prepare_merge_candidate_abandonment(
5223        &self,
5224        stage: MergeCandidateAbandonmentPreparation,
5225    ) -> Result<(), DbError> {
5226        if self.write_policy() != WritePolicy::MergeConcurrent {
5227            return Err(DbError::Message(
5228                "Merge candidate abandonment requires MergeConcurrent policy".to_string(),
5229            ));
5230        }
5231        let statuses = self.state.write_statuses.clone();
5232        let notified_write_id = stage.write_id.clone();
5233        self.call(move |conn| {
5234            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
5235            let (raw_status, raw_prepared): (String, String) = tx
5236                .query_row(
5237                    "SELECT status, prepared FROM store_writes WHERE write_id = ?1",
5238                    [stage.write_id.as_str()],
5239                    |row| Ok((row.get(0)?, row.get(1)?)),
5240                )
5241                .map_err(DbError::from)?;
5242            let status: WriteStatus = serde_json::from_str(&raw_status)
5243                .map_err(|error| DbError::Message(format!("Merge abandonment status: {error}")))?;
5244            if !matches!(status, WriteStatus::Blocked(_)) {
5245                return Err(DbError::Message(format!(
5246                    "write {} is not blocked",
5247                    stage.write_id
5248                )));
5249            }
5250            let prepared: PreparedStoreWriteState = serde_json::from_str(&raw_prepared)
5251                .map_err(|error| DbError::Message(format!("prepared Merge candidate: {error}")))?;
5252            let PreparedStoreWriteState::MergeConcurrent {
5253                commit: candidate_commit,
5254                head: candidate_head,
5255                local_cleanup,
5256                completion,
5257            } = prepared
5258            else {
5259                return Err(DbError::Message(
5260                    "Merge abandonment requires one prepared candidate".to_string(),
5261                ));
5262            };
5263            let candidate =
5264                parse_prepared_merge_candidate_parts_on(&tx, &candidate_commit, &candidate_head)?;
5265            if candidate.commit.write_id != stage.write_id {
5266                return Err(DbError::Message(
5267                    "prepared Merge candidate differs from its write identity".to_string(),
5268                ));
5269            }
5270            let root = required_store_root_authority_on(&tx)?;
5271            let registration =
5272                load_activated_registration_on(&tx, &root, &candidate.commit.author_registration)?;
5273            stage
5274                .commit
5275                .value
5276                .verify_at(
5277                    root.store_root_hash,
5278                    &candidate.reference.coord,
5279                    &registration,
5280                )
5281                .map_err(|error| {
5282                    DbError::Message(format!("verify Merge abandonment commit: {error}"))
5283                })?;
5284            if stage.commit.value.write_id != stage.write_id
5285                || stage.commit.value.abandoned_candidates()
5286                    != [crate::sync::store_commit::CandidateCleanupManifest {
5287                        candidate: crate::sync::store_commit::StoreBatchCommitDeletionTarget {
5288                            coord: candidate.reference.coord.clone(),
5289                            object: candidate.reference.object.clone(),
5290                            canonical_signed_bytes: candidate.canonical_signed_bytes.clone(),
5291                        },
5292                    }]
5293            {
5294                return Err(DbError::Message(
5295                    "Merge abandonment does not name its exact prepared candidate".to_string(),
5296                ));
5297            }
5298            let authority_ref = StoreBatchCommitRef::from_commit(
5299                &stage.commit.value,
5300                candidate.reference.coord.clone(),
5301                stage.commit.prepared.reference().clone(),
5302            )
5303            .map_err(|error| DbError::Message(error.to_string()))?;
5304            if stage.head.value.commit != authority_ref
5305                || stage.head.prepared.reference().slot()
5306                    != candidate.head_prepared.reference().slot()
5307                || stage.head.value.successor != candidate.head.successor
5308                || stage.head.value.author_registration != candidate.commit.author_registration
5309            {
5310                return Err(DbError::Message(
5311                    "Merge abandonment head differs from the candidate competition point"
5312                        .to_string(),
5313                ));
5314            }
5315            StoreDeviceHead::parse_at(
5316                &stage.head.value.to_bytes(),
5317                root.store_root_hash,
5318                &registration,
5319                &authority_ref,
5320            )
5321            .map_err(|error| DbError::Message(format!("verify Merge abandonment head: {error}")))?;
5322            let authority_commit = RemoteObjectRecord::candidate_commit(
5323                authority_ref.clone(),
5324                stage.commit.value.to_bytes(),
5325                stage.commit.prepared.stored_bytes().to_vec(),
5326            )
5327            .map_err(|error| DbError::Message(format!("Merge abandonment commit: {error}")))?;
5328            persist_exact_remote_object_on(&tx, &authority_commit, "Merge abandonment commit")?;
5329            let authority_head_ref = crate::sync::store_commit::StoreDeviceHeadRef {
5330                head_hash: stage.head.value.head_hash(),
5331                object: stage.head.prepared.reference().clone(),
5332            };
5333            let authority_head = RemoteObjectRecord::candidate_activated_store_head(
5334                authority_head_ref,
5335                stage.head.value.to_bytes(),
5336                stage.head.prepared.stored_bytes().to_vec(),
5337                authority_ref,
5338            )
5339            .map_err(|error| DbError::Message(format!("Merge abandonment head: {error}")))?;
5340            persist_exact_remote_object_on(&tx, &authority_head, "Merge abandonment head")?;
5341            let replacement = PreparedStoreWriteState::MergeAbandonment {
5342                candidate_commit,
5343                candidate_head,
5344                authority_commit: DurablePreparedProtocolObject {
5345                    semantic_bytes: stage.commit.value.to_bytes(),
5346                    prepared: stage.commit.prepared,
5347                },
5348                authority_head: DurablePreparedProtocolObject {
5349                    semantic_bytes: stage.head.value.to_bytes(),
5350                    prepared: stage.head.prepared,
5351                },
5352                outcome: MergeAbandonmentOutcome::Prepared,
5353                local_cleanup,
5354                completion,
5355            };
5356            let replacement = serde_json::to_string(&replacement).map_err(|error| {
5357                DbError::Message(format!("serialize Merge abandonment: {error}"))
5358            })?;
5359            let publishing = serde_json::to_string(&WriteStatus::Publishing).map_err(|error| {
5360                DbError::Message(format!("serialize Merge abandonment status: {error}"))
5361            })?;
5362            let updated = tx
5363                .execute(
5364                    "UPDATE store_writes SET prepared = ?2, status = ?3
5365                     WHERE write_id = ?1 AND prepared = ?4
5366                       AND json_extract(status, '$.blocked') IS NOT NULL",
5367                    rusqlite::params![
5368                        stage.write_id.as_str(),
5369                        replacement,
5370                        publishing,
5371                        raw_prepared
5372                    ],
5373                )
5374                .map_err(DbError::from)?;
5375            if updated != 1 {
5376                return Err(DbError::Message(
5377                    "blocked Merge candidate changed during abandonment preparation".to_string(),
5378                ));
5379            }
5380            tx.commit().map_err(DbError::from)?;
5381            Self::notify_write_status_in(&statuses, &notified_write_id, WriteStatus::Publishing);
5382            Ok(())
5383        })
5384        .await
5385    }
5386
5387    pub(crate) async fn prepare_serial_candidate_abandonment(
5388        &self,
5389        stage: SerialCandidateAbandonmentPreparation,
5390    ) -> Result<(), DbError> {
5391        if self.write_policy() != WritePolicy::Serial {
5392            return Err(DbError::Message(
5393                "Serial candidate abandonment requires the Serial write policy".to_string(),
5394            ));
5395        }
5396        self.call(move |conn| {
5397            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
5398            let existing: Option<String> = tx
5399                .query_row(
5400                    "SELECT value FROM protocol_state WHERE key = ?1",
5401                    [SERIAL_CANDIDATE_ABANDONMENT_STATE_KEY],
5402                    |row| row.get(0),
5403                )
5404                .optional()
5405                .map_err(DbError::from)?;
5406            if existing.is_some() {
5407                return Err(DbError::Message(
5408                    "a Serial candidate abandonment is already prepared".to_string(),
5409                ));
5410            }
5411            let (raw_base, raw_prepared): (String, String) = tx
5412                .query_row(
5413                    "SELECT base, prepared FROM store_writes
5414                     WHERE prepared IS NOT NULL AND status = '\"publishing\"'
5415                     ORDER BY ordinal LIMIT 1",
5416                    [],
5417                    |row| Ok((row.get(0)?, row.get(1)?)),
5418                )
5419                .map_err(DbError::from)?;
5420            let StoreWriteBase::Serial { branch_id, base } = serde_json::from_str(&raw_base)
5421                .map_err(|error| {
5422                    DbError::Message(format!("Serial abandonment branch base: {error}"))
5423                })?
5424            else {
5425                return Err(DbError::Message(
5426                    "Merge branch reached Serial candidate abandonment".to_string(),
5427                ));
5428            };
5429            if branch_id != stage.branch_id {
5430                return Err(DbError::Message(
5431                    "Serial abandonment names another pending branch".to_string(),
5432                ));
5433            }
5434            let prepared: PreparedStoreWriteState =
5435                serde_json::from_str(&raw_prepared).map_err(|error| {
5436                    DbError::Message(format!("prepared Serial abandonment candidate: {error}"))
5437                })?;
5438            let PreparedStoreWriteState::Serial {
5439                base_head_bytes,
5440                base_head_version,
5441                commit: candidate_commit,
5442                ..
5443            } = &prepared
5444            else {
5445                return Err(DbError::Message(
5446                    "Serial abandonment requires an exact prepared branch".to_string(),
5447                ));
5448            };
5449            let base_head = VersionedObject {
5450                bytes: base_head_bytes.clone().ok_or_else(|| {
5451                    DbError::Message(
5452                        "Serial abandonment base has no signed coordination head".to_string(),
5453                    )
5454                })?,
5455                version: base_head_version.clone().ok_or_else(|| {
5456                    DbError::Message(
5457                        "Serial abandonment base has no provider version receipt".to_string(),
5458                    )
5459                })?,
5460            };
5461            let candidate = parse_prepared_serial_candidate(&raw_prepared)?
5462                .expect("matched exact Serial candidate");
5463            if candidate.reference != stage.candidate {
5464                return Err(DbError::Message(
5465                    "Serial abandonment target differs from the branch first candidate".to_string(),
5466                ));
5467            }
5468            let root = required_store_root_authority_on(&tx)?;
5469            let registration =
5470                load_activated_registration_on(&tx, &root, &candidate.commit.author_registration)?;
5471            stage
5472                .commit
5473                .value
5474                .verify_at(
5475                    root.store_root_hash,
5476                    &candidate.reference.coord,
5477                    &registration,
5478                )
5479                .map_err(|error| {
5480                    DbError::Message(format!("verify Serial abandonment commit: {error}"))
5481                })?;
5482            if stage.commit.value.write_id != candidate.commit.write_id
5483                || stage.commit.value.order != candidate.commit.order
5484                || stage.commit.value.abandoned_candidates()
5485                    != [crate::sync::store_commit::CandidateCleanupManifest {
5486                        candidate: crate::sync::store_commit::StoreBatchCommitDeletionTarget {
5487                            coord: candidate.reference.coord.clone(),
5488                            object: candidate.reference.object.clone(),
5489                            canonical_signed_bytes: candidate.canonical_signed_bytes.clone(),
5490                        },
5491                    }]
5492            {
5493                return Err(DbError::Message(
5494                    "Serial abandonment does not replace its exact first candidate".to_string(),
5495                ));
5496            }
5497            let authority_ref = StoreBatchCommitRef::from_commit(
5498                &stage.commit.value,
5499                candidate.reference.coord.clone(),
5500                stage.commit.prepared.reference().clone(),
5501            )
5502            .map_err(|error| DbError::Message(error.to_string()))?;
5503            let parsed_head =
5504                StoreSerialHead::parse(&stage.head.to_bytes(), root.store_root_hash, &registration)
5505                    .map_err(|error| {
5506                        DbError::Message(format!("verify Serial abandonment head: {error}"))
5507                    })?;
5508            if parsed_head != stage.head
5509                || !matches!(
5510                    &stage.head.state,
5511                    StoreSerialHeadState::Commit {
5512                        author_registration,
5513                        commit,
5514                    } if author_registration == &candidate.commit.author_registration
5515                        && commit == &authority_ref
5516                )
5517            {
5518                return Err(DbError::Message(
5519                    "Serial abandonment head does not activate its exact authority".to_string(),
5520                ));
5521            }
5522            let (raw_tip_base, raw_tip_prepared): (String, String) = tx
5523                .query_row(
5524                    "SELECT base, prepared FROM store_writes
5525                     WHERE prepared IS NOT NULL AND status = '\"publishing\"'
5526                     ORDER BY ordinal DESC LIMIT 1",
5527                    [],
5528                    |row| Ok((row.get(0)?, row.get(1)?)),
5529                )
5530                .map_err(DbError::from)?;
5531            let StoreWriteBase::Serial {
5532                branch_id: tip_branch_id,
5533                base: tip_base,
5534            } = serde_json::from_str(&raw_tip_base).map_err(|error| {
5535                DbError::Message(format!("Serial abandonment tip branch base: {error}"))
5536            })?
5537            else {
5538                return Err(DbError::Message(
5539                    "Merge tip reached Serial candidate abandonment".to_string(),
5540                ));
5541            };
5542            let tip_prepared: PreparedStoreWriteState = serde_json::from_str(&raw_tip_prepared)
5543                .map_err(|error| {
5544                    DbError::Message(format!("prepared Serial abandonment tip: {error}"))
5545                })?;
5546            let PreparedStoreWriteState::Serial {
5547                tip_head_bytes: Some(tip_head_bytes),
5548                ..
5549            } = tip_prepared
5550            else {
5551                return Err(DbError::Message(
5552                    "Serial abandonment branch has no activating tip head".to_string(),
5553                ));
5554            };
5555            if tip_branch_id != branch_id
5556                || tip_base != base
5557                || tip_head_bytes != stage.original_head_bytes
5558            {
5559                return Err(DbError::Message(
5560                    "Serial abandonment original head differs from its prepared branch".to_string(),
5561                ));
5562            }
5563            let authority_remote = RemoteObjectRecord::candidate_commit(
5564                authority_ref,
5565                stage.commit.value.to_bytes(),
5566                stage.commit.prepared.stored_bytes().to_vec(),
5567            )
5568            .map_err(|error| DbError::Message(format!("Serial abandonment commit: {error}")))?;
5569            persist_exact_remote_object_on(&tx, &authority_remote, "Serial abandonment commit")?;
5570            let durable = DurableSerialCandidateAbandonment {
5571                branch_id,
5572                base,
5573                base_head,
5574                candidate: candidate.reference,
5575                commit: DurablePreparedProtocolObject {
5576                    semantic_bytes: stage.commit.value.to_bytes(),
5577                    prepared: stage.commit.prepared,
5578                },
5579                head_bytes: stage.head.to_bytes(),
5580                original_head_bytes: stage.original_head_bytes,
5581            };
5582            let durable = serde_json::to_string(&durable).map_err(|error| {
5583                DbError::Message(format!("serialize Serial candidate abandonment: {error}"))
5584            })?;
5585            tx.execute(
5586                "INSERT INTO protocol_state (key, value) VALUES (?1, ?2)",
5587                (SERIAL_CANDIDATE_ABANDONMENT_STATE_KEY, durable),
5588            )
5589            .map_err(DbError::from)?;
5590            if candidate_commit.prepared.reference() != &stage.candidate.object {
5591                return Err(DbError::Message(
5592                    "Serial abandonment candidate storage identity changed".to_string(),
5593                ));
5594            }
5595            tx.commit().map_err(DbError::from)
5596        })
5597        .await
5598    }
5599
5600    pub(crate) async fn prepare_serial_store_branch_commit(
5601        &self,
5602        stage: SerialStoreWritePreparation,
5603    ) -> Result<(), DbError> {
5604        if self.write_policy() != WritePolicy::Serial {
5605            return Err(DbError::Message(
5606                "Serial branch preparation requires the Serial write policy".to_string(),
5607            ));
5608        }
5609        if stage.writes.is_empty() {
5610            return Err(DbError::Message(
5611                "prepared Serial branch is empty".to_string(),
5612            ));
5613        }
5614        self.call(move |conn| {
5615            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
5616            let local_device_id: String = tx
5617                .query_row(
5618                    "SELECT value FROM protocol_state WHERE key = ?1",
5619                    [LOCAL_DEVICE_ID_STATE_KEY],
5620                    |row| row.get(0),
5621                )
5622                .map_err(DbError::from)?;
5623            let root = required_store_root_authority_on(&tx)?;
5624            let expected_base = StoreWriteBase::Serial {
5625                branch_id: stage.branch_id.clone(),
5626                base: stage.base.clone(),
5627            };
5628            if stage.base_head_bytes.is_some() != stage.base_head_version.is_some() {
5629                return Err(DbError::Message(
5630                    "Serial base head bytes and opaque version receipt must be present together"
5631                        .to_string(),
5632                ));
5633            }
5634            match stage.base_head_bytes.as_deref() {
5635                Some(bytes) => {
5636                    let unverified: StoreSerialHead = serde_json::from_slice(bytes)
5637                        .map_err(|error| DbError::Message(format!("Serial base head: {error}")))?;
5638                    let executor_ref = match &unverified.state {
5639                        StoreSerialHeadState::Genesis {
5640                            founder_registration,
5641                            ..
5642                        } => founder_registration,
5643                        StoreSerialHeadState::Commit {
5644                            author_registration,
5645                            ..
5646                        } => author_registration,
5647                    };
5648                    let executor = load_activated_registration_on(&tx, &root, executor_ref)?;
5649                    let verified = StoreSerialHead::parse(bytes, root.store_root_hash, &executor)
5650                        .map_err(|error| {
5651                        DbError::Message(format!("verify Serial base head: {error}"))
5652                    })?;
5653                    let base_ref = match verified.state {
5654                        StoreSerialHeadState::Genesis {
5655                            root: head_root,
5656                            founder_registration,
5657                        } => {
5658                            if head_root != root || founder_registration != executor_ref.clone() {
5659                                return Err(DbError::Message(
5660                                    "Serial genesis head differs from exact Store authority"
5661                                        .to_string(),
5662                                ));
5663                            }
5664                            None
5665                        }
5666                        StoreSerialHeadState::Commit { commit, .. } => Some(commit),
5667                    };
5668                    if base_ref != stage.base {
5669                        return Err(DbError::Message(
5670                            "Serial branch base differs from the exact observed head".to_string(),
5671                        ));
5672                    }
5673                }
5674                None if stage.base.is_some() => {
5675                    return Err(DbError::Message(
5676                        "Serial branch has a base commit without observed head evidence"
5677                            .to_string(),
5678                    ));
5679                }
5680                None => {}
5681            }
5682            let head_bytes = stage.head.to_bytes();
5683            let mut predecessor = stage.base.clone();
5684            let mut tip_ref = None;
5685            let mut tip_registration = None;
5686            for (index, write) in stage.writes.iter().enumerate() {
5687                if write.commit.value.write_id != write.write_id
5688                    || write.commit.value.author_registration.device_id.to_string()
5689                        != local_device_id
5690                {
5691                    return Err(DbError::Message(format!(
5692                        "prepared Serial write {} identity differs from its commit",
5693                        write.write_id
5694                    )));
5695                }
5696                let expected_seq = predecessor
5697                    .as_ref()
5698                    .map_or(1, |reference| reference.coord.sequence().saturating_add(1));
5699                let coord = StoreCommitCoord::Serial {
5700                    sequence: expected_seq,
5701                };
5702                let registration = load_activated_registration_on(
5703                    &tx,
5704                    &root,
5705                    &write.commit.value.author_registration,
5706                )?;
5707                write
5708                    .commit
5709                    .value
5710                    .verify_at(root.store_root_hash, &coord, &registration)
5711                    .map_err(|error| {
5712                        DbError::Message(format!("verify prepared Serial commit: {error}"))
5713                    })?;
5714                let order_matches = match (&predecessor, &write.commit.value.order) {
5715                    (
5716                        Some(expected),
5717                        crate::sync::store_commit::StoreCommitOrder::Serial {
5718                            predecessor: StoreSerialPredecessor::Commit(actual),
5719                            ..
5720                        },
5721                    ) => actual == expected,
5722                    (
5723                        None,
5724                        crate::sync::store_commit::StoreCommitOrder::Serial {
5725                            predecessor:
5726                                StoreSerialPredecessor::Genesis {
5727                                    root: commit_root,
5728                                    founder_registration,
5729                                },
5730                            ..
5731                        },
5732                    ) => {
5733                        commit_root == &root
5734                            && founder_registration == &write.commit.value.author_registration
5735                    }
5736                    _ => false,
5737                };
5738                if !order_matches {
5739                    return Err(DbError::Message(format!(
5740                        "prepared Serial commit {} has the wrong exact predecessor",
5741                        write.write_id
5742                    )));
5743                }
5744                let commit_ref = StoreBatchCommitRef::from_commit(
5745                    &write.commit.value,
5746                    coord,
5747                    write.commit.prepared.reference().clone(),
5748                )
5749                .map_err(|error| DbError::Message(error.to_string()))?;
5750                let (stored_changeset, stored_base, status, prepared): (
5751                    Vec<u8>,
5752                    String,
5753                    String,
5754                    String,
5755                ) = tx
5756                    .query_row(
5757                        "SELECT changeset, base, status, prepared FROM store_writes
5758                         WHERE write_id = ?1",
5759                        [write.write_id.as_str()],
5760                        |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
5761                    )
5762                    .map_err(DbError::from)?;
5763                let stored_base: StoreWriteBase = serde_json::from_str(&stored_base)
5764                    .map_err(|error| DbError::Message(format!("stored Serial base: {error}")))?;
5765                let stored_prepared: PreparedStoreWriteState = serde_json::from_str(&prepared)
5766                    .map_err(|error| {
5767                        DbError::Message(format!("stored Serial reservation: {error}"))
5768                    })?;
5769                if stored_base != expected_base
5770                    || status != "\"publishing\""
5771                    || !matches!(stored_prepared, PreparedStoreWriteState::SerialPreparing)
5772                {
5773                    return Err(DbError::Message(format!(
5774                        "Serial write {} no longer owns its exact branch reservation",
5775                        write.write_id
5776                    )));
5777                }
5778                let partitions = Self::prepared_store_write_partitions_on(
5779                    &tx,
5780                    write.write_id.as_str(),
5781                    &stored_changeset,
5782                    WritePolicy::Serial,
5783                )?;
5784                Self::persist_closed_write_objects_on(
5785                    &tx,
5786                    &write.write_id,
5787                    root.store_root_hash,
5788                    &commit_ref,
5789                    &write.commit.value,
5790                    write.commit.prepared.stored_bytes(),
5791                    &partitions,
5792                    &write.remote_objects,
5793                    &write.audiences,
5794                )?;
5795                let tip_head_bytes = (index + 1 == stage.writes.len()).then(|| head_bytes.clone());
5796                let durable = PreparedStoreWriteState::Serial {
5797                    base_head_bytes: stage.base_head_bytes.clone(),
5798                    base_head_version: stage.base_head_version.clone(),
5799                    commit: DurablePreparedProtocolObject {
5800                        semantic_bytes: write.commit.value.to_bytes(),
5801                        prepared: write.commit.prepared.clone(),
5802                    },
5803                    tip_head_bytes,
5804                    local_cleanup: write.local_cleanup.clone(),
5805                    completion: write.completion.clone(),
5806                };
5807                let durable = serde_json::to_string(&durable).map_err(|error| {
5808                    DbError::Message(format!("serialize prepared Serial write: {error}"))
5809                })?;
5810                let updated = tx
5811                    .execute(
5812                        "UPDATE store_writes SET prepared = ?2
5813                         WHERE write_id = ?1 AND status = '\"publishing\"'",
5814                        rusqlite::params![write.write_id.as_str(), durable],
5815                    )
5816                    .map_err(DbError::from)?;
5817                if updated != 1 {
5818                    return Err(DbError::Message(format!(
5819                        "Serial write {} lost exact preparation ownership",
5820                        write.write_id
5821                    )));
5822                }
5823                predecessor = Some(commit_ref.clone());
5824                tip_ref = Some(commit_ref);
5825                tip_registration = Some(registration);
5826            }
5827            let tip_ref = tip_ref.expect("nonempty checked above");
5828            let tip_registration = tip_registration.expect("nonempty checked above");
5829            let parsed_head =
5830                StoreSerialHead::parse(&head_bytes, root.store_root_hash, &tip_registration)
5831                    .map_err(|error| {
5832                        DbError::Message(format!("verify prepared Serial head: {error}"))
5833                    })?;
5834            let tip_author_ref = &stage
5835                .writes
5836                .last()
5837                .expect("nonempty checked above")
5838                .commit
5839                .value
5840                .author_registration;
5841            if parsed_head != stage.head
5842                || !matches!(
5843                    &stage.head.state,
5844                    StoreSerialHeadState::Commit {
5845                        author_registration,
5846                        commit,
5847                    } if author_registration == tip_author_ref
5848                        && commit == &tip_ref
5849                )
5850            {
5851                return Err(DbError::Message(
5852                    "prepared Serial head does not activate the exact branch tip".to_string(),
5853                ));
5854            }
5855            let reserved_count: i64 = tx
5856                .query_row(
5857                    "SELECT COUNT(*) FROM store_writes
5858                     WHERE status = '\"publishing\"' AND json_type(base, '$.serial') IS NOT NULL",
5859                    [],
5860                    |row| row.get(0),
5861                )
5862                .map_err(DbError::from)?;
5863            if reserved_count
5864                != i64::try_from(stage.writes.len()).map_err(|_| {
5865                    DbError::Message("Serial branch length exceeds SQLite integer".into())
5866                })?
5867            {
5868                return Err(DbError::Message(format!(
5869                    "prepared Serial branch contains {} writes but {reserved_count} are reserved",
5870                    stage.writes.len()
5871                )));
5872            }
5873            tx.commit().map_err(DbError::from)
5874        })
5875        .await
5876    }
5877
5878    fn write_ids_matching_serial_base<I>(
5879        rows: I,
5880        expected_base: &StoreWriteBase,
5881    ) -> Result<Vec<WriteId>, DbError>
5882    where
5883        I: IntoIterator<Item = rusqlite::Result<(String, String)>>,
5884    {
5885        let mut write_ids = Vec::new();
5886        for row in rows {
5887            let (write_id, raw_base) = row.map_err(DbError::from)?;
5888            let stored_base: StoreWriteBase = serde_json::from_str(&raw_base)
5889                .map_err(|error| DbError::Message(format!("stored Serial base: {error}")))?;
5890            if &stored_base == expected_base {
5891                write_ids.push(WriteId::from_generated(write_id));
5892            }
5893        }
5894        Ok(write_ids)
5895    }
5896
5897    pub(crate) async fn release_serial_store_branch_reservation(
5898        &self,
5899        branch_id: PendingBranchId,
5900        base: Option<StoreBatchCommitRef>,
5901        status: WriteStatus,
5902    ) -> Result<(), DbError> {
5903        if !matches!(status, WriteStatus::Pending | WriteStatus::Blocked(_)) {
5904            return Err(DbError::Message(
5905                "Serial preparation can only return a reservation to pending or blocked"
5906                    .to_string(),
5907            ));
5908        }
5909        let statuses = self.state.write_statuses.clone();
5910        self.call(move |conn| {
5911            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
5912            let expected_base = StoreWriteBase::Serial { branch_id, base };
5913            let preparing = serde_json::to_string(&PreparedStoreWriteState::SerialPreparing)
5914                .map_err(|error| {
5915                    DbError::Message(format!("serialize Serial reservation: {error}"))
5916                })?;
5917            let status_json = serde_json::to_string(&status)
5918                .map_err(|error| DbError::Message(format!("serialize write status: {error}")))?;
5919            let mut statement = tx
5920                .prepare(
5921                    "SELECT write_id, base FROM store_writes
5922                     WHERE status = '\"publishing\"' AND prepared = ?1
5923                     ORDER BY ordinal",
5924                )
5925                .map_err(DbError::from)?;
5926            let rows = statement
5927                .query_map([&preparing], |row| {
5928                    Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
5929                })
5930                .map_err(DbError::from)?;
5931            let write_ids = Self::write_ids_matching_serial_base(rows, &expected_base)?;
5932            drop(statement);
5933            if write_ids.is_empty() {
5934                return Err(DbError::Message(
5935                    "reserved Serial branch disappeared during preparation".to_string(),
5936                ));
5937            }
5938            for write_id in &write_ids {
5939                let updated = tx
5940                    .execute(
5941                        "UPDATE store_writes SET status = ?2, prepared = NULL
5942                         WHERE write_id = ?1 AND status = '\"publishing\"' AND prepared = ?3",
5943                        rusqlite::params![write_id.as_str(), &status_json, &preparing],
5944                    )
5945                    .map_err(DbError::from)?;
5946                if updated != 1 {
5947                    return Err(DbError::Message(format!(
5948                        "reserved Serial write {write_id} disappeared during release"
5949                    )));
5950                }
5951            }
5952            tx.commit().map_err(DbError::from)?;
5953            for write_id in write_ids {
5954                Self::notify_write_status_in(&statuses, &write_id, status.clone());
5955            }
5956            Ok(())
5957        })
5958        .await
5959    }
5960
5961    pub(crate) async fn oldest_prepared_store_write(
5962        &self,
5963    ) -> Result<Option<PreparedStoreWriteCommit>, DbError> {
5964        let loaded = self
5965            .call(|conn| {
5966                let row = conn
5967                    .query_row(
5968                        "SELECT write_id, changeset, base, prepared FROM store_writes
5969                 WHERE prepared IS NOT NULL
5970                   AND status = '\"publishing\"'
5971                 ORDER BY ordinal LIMIT 1",
5972                        [],
5973                        |row| {
5974                            Ok((
5975                                row.get::<_, String>(0)?,
5976                                row.get::<_, Vec<u8>>(1)?,
5977                                row.get::<_, String>(2)?,
5978                                row.get::<_, String>(3)?,
5979                            ))
5980                        },
5981                    )
5982                    .optional()
5983                    .map_err(DbError::from)?;
5984                row.map(|(write_id, stored_changeset, base, prepared)| {
5985                    let prepared: PreparedStoreWriteState = serde_json::from_str(&prepared)
5986                        .map_err(|error| {
5987                            DbError::Message(format!("prepared Store write: {error}"))
5988                        })?;
5989                    let (commit, head, graph_commit) = match &prepared {
5990                        PreparedStoreWriteState::MergeConcurrent { commit, head, .. } => {
5991                            (commit, head, None)
5992                        }
5993                        PreparedStoreWriteState::MergeAbandonment {
5994                            candidate_commit,
5995                            authority_commit,
5996                            authority_head,
5997                            ..
5998                        } => (authority_commit, authority_head, Some(candidate_commit)),
5999                        PreparedStoreWriteState::SerialPreparing
6000                        | PreparedStoreWriteState::Serial { .. } => {
6001                            return Err(DbError::Message(
6002                                "serial branch reached MergeConcurrent publication".to_string(),
6003                            ));
6004                        }
6005                    };
6006                    let write_id = WriteId::from_generated(write_id);
6007                    let unverified_commit: StoreBatchCommit =
6008                        serde_json::from_slice(&commit.semantic_bytes).map_err(|error| {
6009                            DbError::Message(format!("prepared Store commit: {error}"))
6010                        })?;
6011                    if unverified_commit.write_id != write_id {
6012                        return Err(DbError::Message(
6013                            "prepared write id differs from signed commit".to_string(),
6014                        ));
6015                    }
6016                    let root = required_store_root_authority_on(conn)?;
6017                    let registration_ref = &unverified_commit.author_registration;
6018                    let (registration_bytes, stored_registration_ref): (Vec<u8>, String) = conn
6019                        .query_row(
6020                            "SELECT registration_bytes, registration_object \
6021                         FROM store_device_registration_activations \
6022                         WHERE device_id = ?1 AND registration_hash = ?2",
6023                            (
6024                                registration_ref.device_id.to_string(),
6025                                registration_ref.registration_hash.to_string(),
6026                            ),
6027                            |row| Ok((row.get(0)?, row.get(1)?)),
6028                        )
6029                        .map_err(DbError::from)?;
6030                    let stored_registration_ref: StoreDeviceRegistrationRef =
6031                        serde_json::from_str(&stored_registration_ref).map_err(|error| {
6032                            DbError::Message(format!("prepared write registration ref: {error}"))
6033                        })?;
6034                    if stored_registration_ref != *registration_ref {
6035                        return Err(DbError::Message(
6036                            "prepared commit registration differs from its activation".to_string(),
6037                        ));
6038                    }
6039                    let registration = StoreDeviceRegistration::parse_at(
6040                        &registration_bytes,
6041                        &root,
6042                        registration_ref.device_id,
6043                    )
6044                    .map_err(|error| {
6045                        DbError::Message(format!("prepared write registration: {error}"))
6046                    })?;
6047                    let stream_id = AuthorStreamId::store_announcements(&root, registration_ref);
6048                    let coord = StoreCommitCoord::MergeConcurrent {
6049                        stream_id,
6050                        sequence: unverified_commit.seq(),
6051                    };
6052                    let commit_value = StoreBatchCommit::parse_at(
6053                        &commit.semantic_bytes,
6054                        root.store_root_hash,
6055                        &coord,
6056                        &registration,
6057                    )
6058                    .map_err(|error| {
6059                        DbError::Message(format!("verify prepared Store commit: {error}"))
6060                    })?;
6061                    let commit_ref = StoreBatchCommitRef::from_commit(
6062                        &commit_value,
6063                        coord,
6064                        commit.prepared.reference().clone(),
6065                    )
6066                    .map_err(|error| DbError::Message(error.to_string()))?;
6067                    let head_value = StoreDeviceHead::parse_at(
6068                        &head.semantic_bytes,
6069                        root.store_root_hash,
6070                        &registration,
6071                        &commit_ref,
6072                    )
6073                    .map_err(|error| {
6074                        DbError::Message(format!("verify prepared Store head: {error}"))
6075                    })?;
6076                    let base: StoreWriteBase = serde_json::from_str(&base).map_err(|error| {
6077                        DbError::Message(format!("prepared write base: {error}"))
6078                    })?;
6079                    let StoreWriteBase::MergeConcurrent { dependencies } = base else {
6080                        return Err(DbError::Message(
6081                            "serial base reached MergeConcurrent publication".to_string(),
6082                        ));
6083                    };
6084                    let dependencies =
6085                        CommitFrontier::from_refs(WritePolicy::MergeConcurrent, dependencies)
6086                            .map_err(|error| {
6087                                DbError::Message(format!("prepared dependency frontier: {error}"))
6088                            })?;
6089                    if dependencies
6090                        .merge_commits()
6091                        .map_err(|error| DbError::Message(error.to_string()))?
6092                        != commit_value.merge_dependencies().map_err(|error| {
6093                            DbError::Message(format!("prepared Store commit policy: {error}"))
6094                        })?
6095                    {
6096                        return Err(DbError::Message(
6097                            "prepared commit differs from its write dependency frontier"
6098                                .to_string(),
6099                        ));
6100                    }
6101                    let partitions = Self::prepared_store_write_partitions_on(
6102                        conn,
6103                        write_id.as_str(),
6104                        &stored_changeset,
6105                        WritePolicy::MergeConcurrent,
6106                    )?;
6107                    let audiences = load_prepared_audience_objects_on(conn, &write_id)?;
6108                    let graph_commit = match graph_commit {
6109                        Some(graph_commit) => {
6110                            let candidate = parse_prepared_merge_candidate_parts_on(
6111                                conn,
6112                                graph_commit,
6113                                match &prepared {
6114                                    PreparedStoreWriteState::MergeAbandonment {
6115                                        candidate_head,
6116                                        ..
6117                                    } => candidate_head,
6118                                    _ => unreachable!("matched Merge abandonment"),
6119                                },
6120                            )?;
6121                            candidate.commit
6122                        }
6123                        None => commit_value.clone(),
6124                    };
6125                    let expected_package_count =
6126                        usize::from(graph_commit.store_package().is_some())
6127                            .checked_add(graph_commit.circle_packages().len())
6128                            .ok_or_else(|| {
6129                                DbError::Message("package count overflow".to_string())
6130                            })?;
6131                    if audiences.packages.len() != expected_package_count
6132                        || audiences.packages.len()
6133                            != usize::from(partitions.store.is_some()) + partitions.circles.len()
6134                    {
6135                        return Err(DbError::Message(
6136                            "prepared package indexes do not exactly cover commit audiences"
6137                                .to_string(),
6138                        ));
6139                    }
6140                    for package in &audiences.packages {
6141                        let value = package.package();
6142                        if value.write_id() != &write_id
6143                            || value.commit_coord() != &commit_ref.coord
6144                            || value.candidate_family() != commit_value.candidate_family()
6145                        {
6146                            return Err(DbError::Message(
6147                                "indexed audience package differs from its exact commit"
6148                                    .to_string(),
6149                            ));
6150                        }
6151                        let expected_object = match value.audience() {
6152                            crate::sync::audience_package::PackageAudience::Store => {
6153                                graph_commit
6154                                    .verify_store_package(package.semantic_bytes())
6155                                    .map_err(|error| DbError::Message(error.to_string()))?;
6156                                &graph_commit
6157                                    .store_package()
6158                                    .as_ref()
6159                                    .expect("verified present")
6160                                    .object
6161                            }
6162                            crate::sync::audience_package::PackageAudience::Circle {
6163                                circle_id,
6164                                ..
6165                            } => {
6166                                graph_commit
6167                                    .verify_circle_package(*circle_id, package.semantic_bytes())
6168                                    .map_err(|error| DbError::Message(error.to_string()))?;
6169                                &graph_commit
6170                                    .circle_packages()
6171                                    .iter()
6172                                    .find(|entry| entry.circle_id == *circle_id)
6173                                    .expect("verified present")
6174                                    .package
6175                                    .object
6176                            }
6177                        };
6178                        if package.object() != expected_object {
6179                            return Err(DbError::Message(
6180                                "indexed audience package exact object differs from its commit"
6181                                    .to_string(),
6182                            ));
6183                        }
6184                    }
6185                    for package in &audiences.packages {
6186                        let audience = package.package().audience().remote_audience();
6187                        for binding in package.package().blob_bindings() {
6188                            if !audiences.blobs.iter().any(|blob| {
6189                                blob.audience() == &audience && blob.blob() == binding.blob()
6190                            }) {
6191                                return Err(DbError::Message(
6192                                    "prepared package blob binding has no exact blob index"
6193                                        .to_string(),
6194                                ));
6195                            }
6196                        }
6197                    }
6198                    for blob in &audiences.blobs {
6199                        if !audiences.packages.iter().any(|package| {
6200                            package.package().audience().remote_audience() == *blob.audience()
6201                                && package
6202                                    .package()
6203                                    .blob_bindings()
6204                                    .iter()
6205                                    .any(|binding| binding.blob() == blob.blob())
6206                        }) {
6207                            return Err(DbError::Message(
6208                                "prepared blob index has no exact package binding".to_string(),
6209                            ));
6210                        }
6211                    }
6212                    Ok(PreparedStoreWriteCommit {
6213                        audiences,
6214                        commit: ExactProtocolObject {
6215                            value: commit_value,
6216                            bytes: commit.semantic_bytes.clone(),
6217                            object: commit.prepared.reference().clone(),
6218                            prepared: commit.prepared.clone(),
6219                        },
6220                        head: ExactProtocolObject {
6221                            value: head_value,
6222                            bytes: head.semantic_bytes.clone(),
6223                            object: head.prepared.reference().clone(),
6224                            prepared: head.prepared.clone(),
6225                        },
6226                    })
6227                })
6228                .transpose()
6229            })
6230            .await?;
6231        if let Some(batch) = &loaded {
6232            for blob in &batch.audiences.blobs {
6233                if let Some(spool_path) = blob.spool_path() {
6234                    crate::local_blob::verify_exact_file(blob.blob().object(), spool_path)
6235                        .await
6236                        .map_err(|error| {
6237                            DbError::Message(format!("prepared blob spool: {error}"))
6238                        })?;
6239                }
6240            }
6241        }
6242        Ok(loaded)
6243    }
6244
6245    pub(crate) async fn prepared_serial_store_branch(
6246        &self,
6247    ) -> Result<Option<PreparedSerialStoreBranch>, DbError> {
6248        let loaded = self
6249            .call(|conn| {
6250                let root = required_store_root_authority_on(conn)?;
6251                let mut statement = conn
6252                    .prepare(
6253                        "SELECT write_id, changeset, base, prepared FROM store_writes
6254                     WHERE prepared IS NOT NULL AND status = '\"publishing\"'
6255                     ORDER BY ordinal",
6256                    )
6257                    .map_err(DbError::from)?;
6258                let rows = statement
6259                    .query_map([], |row| {
6260                        Ok((
6261                            row.get::<_, String>(0)?,
6262                            row.get::<_, Vec<u8>>(1)?,
6263                            row.get::<_, String>(2)?,
6264                            row.get::<_, String>(3)?,
6265                        ))
6266                    })
6267                    .map_err(DbError::from)?
6268                    .collect::<Result<Vec<_>, _>>()
6269                    .map_err(DbError::from)?;
6270                drop(statement);
6271                let mut branch_id = None;
6272                let mut base = None;
6273                let mut base_head_bytes: Option<Option<Vec<u8>>> = None;
6274                let mut base_head_version: Option<Option<VersionToken>> = None;
6275                let mut writes = Vec::new();
6276                let mut head = None;
6277                let mut predecessor: Option<StoreBatchCommitRef> = None;
6278                for row in rows {
6279                    let (write_id, stored_changeset, raw_base, prepared) = row;
6280                    let prepared: PreparedStoreWriteState = serde_json::from_str(&prepared)
6281                        .map_err(|error| {
6282                            DbError::Message(format!("prepared Serial write: {error}"))
6283                        })?;
6284                    if matches!(prepared, PreparedStoreWriteState::SerialPreparing) {
6285                        if writes.is_empty() {
6286                            return Ok(None);
6287                        }
6288                        return Err(DbError::Message(
6289                            "Serial branch mixes reserved and exact prepared writes".to_string(),
6290                        ));
6291                    }
6292                    let PreparedStoreWriteState::Serial {
6293                        base_head_bytes: row_base_head_bytes,
6294                        base_head_version: row_base_head_version,
6295                        commit,
6296                        tip_head_bytes,
6297                        ..
6298                    } = prepared
6299                    else {
6300                        return Err(DbError::Message(
6301                            "MergeConcurrent write reached Serial publication".to_string(),
6302                        ));
6303                    };
6304                    let StoreWriteBase::Serial {
6305                        branch_id: row_branch_id,
6306                        base: row_base,
6307                    } = serde_json::from_str(&raw_base).map_err(|error| {
6308                        DbError::Message(format!("prepared Serial base: {error}"))
6309                    })?
6310                    else {
6311                        return Err(DbError::Message(
6312                            "MergeConcurrent base reached Serial publication".to_string(),
6313                        ));
6314                    };
6315                    if branch_id
6316                        .as_ref()
6317                        .is_some_and(|value| value != &row_branch_id)
6318                        || base.as_ref().is_some_and(|value| value != &row_base)
6319                        || base_head_bytes
6320                            .as_ref()
6321                            .is_some_and(|value| value != &row_base_head_bytes)
6322                        || base_head_version
6323                            .as_ref()
6324                            .is_some_and(|value| value != &row_base_head_version)
6325                    {
6326                        return Err(DbError::Message(
6327                            "prepared Serial writes do not share one branch base".to_string(),
6328                        ));
6329                    }
6330                    branch_id.get_or_insert(row_branch_id);
6331                    base.get_or_insert(row_base);
6332                    base_head_bytes.get_or_insert(row_base_head_bytes);
6333                    base_head_version.get_or_insert(row_base_head_version);
6334                    let unverified: StoreBatchCommit =
6335                        serde_json::from_slice(&commit.semantic_bytes).map_err(|error| {
6336                            DbError::Message(format!("prepared Serial commit: {error}"))
6337                        })?;
6338                    if unverified.write_id.as_str() != write_id {
6339                        return Err(DbError::Message(
6340                            "prepared Serial write id differs from signed commit".to_string(),
6341                        ));
6342                    }
6343                    if writes.is_empty() {
6344                        predecessor = base.as_ref().expect("first row stored base").clone();
6345                    }
6346                    let expected_sequence = predecessor
6347                        .as_ref()
6348                        .map_or(1, |reference| reference.coord.sequence().saturating_add(1));
6349                    let coord = StoreCommitCoord::Serial {
6350                        sequence: expected_sequence,
6351                    };
6352                    let registration = load_activated_registration_on(
6353                        conn,
6354                        &root,
6355                        &unverified.author_registration,
6356                    )?;
6357                    let commit_value = StoreBatchCommit::parse_at(
6358                        &commit.semantic_bytes,
6359                        root.store_root_hash,
6360                        &coord,
6361                        &registration,
6362                    )
6363                    .map_err(|error| {
6364                        DbError::Message(format!("verify prepared Serial commit: {error}"))
6365                    })?;
6366                    let order_matches = match (&predecessor, &commit_value.order) {
6367                        (
6368                            Some(expected),
6369                            crate::sync::store_commit::StoreCommitOrder::Serial {
6370                                predecessor: StoreSerialPredecessor::Commit(actual),
6371                                ..
6372                            },
6373                        ) => actual == expected,
6374                        (
6375                            None,
6376                            crate::sync::store_commit::StoreCommitOrder::Serial {
6377                                predecessor:
6378                                    StoreSerialPredecessor::Genesis {
6379                                        root: commit_root,
6380                                        founder_registration,
6381                                    },
6382                                ..
6383                            },
6384                        ) => {
6385                            commit_root == &root
6386                                && founder_registration == &commit_value.author_registration
6387                        }
6388                        _ => false,
6389                    };
6390                    if !order_matches {
6391                        return Err(DbError::Message(
6392                            "prepared Serial commit chain has a different exact predecessor"
6393                                .to_string(),
6394                        ));
6395                    }
6396                    let commit_ref = StoreBatchCommitRef::from_commit(
6397                        &commit_value,
6398                        coord,
6399                        commit.prepared.reference().clone(),
6400                    )
6401                    .map_err(|error| DbError::Message(error.to_string()))?;
6402                    let write_id = WriteId::from_generated(write_id);
6403                    let partitions = Self::prepared_store_write_partitions_on(
6404                        conn,
6405                        write_id.as_str(),
6406                        &stored_changeset,
6407                        WritePolicy::Serial,
6408                    )?;
6409                    let audiences = load_prepared_audience_objects_on(conn, &write_id)?;
6410                    Self::validate_loaded_write_objects(
6411                        &write_id,
6412                        &commit_ref,
6413                        &commit_value,
6414                        &partitions,
6415                        &audiences,
6416                    )?;
6417                    if let Some(head_bytes) = tip_head_bytes {
6418                        if head.is_some() {
6419                            return Err(DbError::Message(
6420                                "prepared Serial branch has more than one tip head".to_string(),
6421                            ));
6422                        }
6423                        let value = StoreSerialHead::parse(
6424                            &head_bytes,
6425                            root.store_root_hash,
6426                            &registration,
6427                        )
6428                        .map_err(|error| {
6429                            DbError::Message(format!("verify prepared Serial head: {error}"))
6430                        })?;
6431                        head = Some(CanonicalProtocolObject {
6432                            value,
6433                            bytes: head_bytes,
6434                        });
6435                    }
6436                    writes.push(PreparedSerialStoreWriteCommit {
6437                        audiences,
6438                        commit: ExactProtocolObject {
6439                            value: commit_value,
6440                            bytes: commit.semantic_bytes,
6441                            object: commit.prepared.reference().clone(),
6442                            prepared: commit.prepared,
6443                        },
6444                    });
6445                    predecessor = Some(commit_ref);
6446                }
6447                if writes.is_empty() {
6448                    return Ok(None);
6449                }
6450                let head = head.ok_or_else(|| {
6451                    DbError::Message(
6452                        "prepared Serial branch has no activating tip head".to_string(),
6453                    )
6454                })?;
6455                let tip_ref = predecessor.expect("nonempty branch has exact tip");
6456                let tip_author = &writes
6457                    .last()
6458                    .expect("nonempty branch")
6459                    .commit
6460                    .value
6461                    .author_registration;
6462                if !matches!(
6463                    &head.value.state,
6464                    StoreSerialHeadState::Commit {
6465                        author_registration,
6466                        commit,
6467                    } if author_registration == tip_author && commit == &tip_ref
6468                ) {
6469                    return Err(DbError::Message(
6470                        "prepared Serial head does not activate the exact final commit".to_string(),
6471                    ));
6472                }
6473                let base_value = base.expect("nonempty branch");
6474                let base_bytes = base_head_bytes.expect("nonempty branch");
6475                let base_version = base_head_version.expect("nonempty branch");
6476                if base_bytes.is_some() != base_version.is_some() {
6477                    return Err(DbError::Message(
6478                        "Serial base head bytes and opaque version receipt differ in presence"
6479                            .to_string(),
6480                    ));
6481                }
6482                match base_bytes.as_deref() {
6483                    Some(bytes) => {
6484                        let unverified: StoreSerialHead =
6485                            serde_json::from_slice(bytes).map_err(|error| {
6486                                DbError::Message(format!("stored Serial base head: {error}"))
6487                            })?;
6488                        let executor_ref = match &unverified.state {
6489                            StoreSerialHeadState::Genesis {
6490                                founder_registration,
6491                                ..
6492                            } => founder_registration,
6493                            StoreSerialHeadState::Commit {
6494                                author_registration,
6495                                ..
6496                            } => author_registration,
6497                        };
6498                        let executor = load_activated_registration_on(conn, &root, executor_ref)?;
6499                        let verified =
6500                            StoreSerialHead::parse(bytes, root.store_root_hash, &executor)
6501                                .map_err(|error| {
6502                                    DbError::Message(format!(
6503                                        "verify stored Serial base head: {error}"
6504                                    ))
6505                                })?;
6506                        let observed = match verified.state {
6507                            StoreSerialHeadState::Genesis {
6508                                root: observed_root,
6509                                ..
6510                            } => {
6511                                if observed_root != root {
6512                                    return Err(DbError::Message(
6513                                        "stored Serial genesis head has a different exact root"
6514                                            .to_string(),
6515                                    ));
6516                                }
6517                                None
6518                            }
6519                            StoreSerialHeadState::Commit { commit, .. } => Some(commit),
6520                        };
6521                        if observed != base_value {
6522                            return Err(DbError::Message(
6523                                "stored Serial base evidence differs from branch base".to_string(),
6524                            ));
6525                        }
6526                    }
6527                    None if base_value.is_some() => {
6528                        return Err(DbError::Message(
6529                            "stored Serial base commit has no head evidence".to_string(),
6530                        ));
6531                    }
6532                    None => {}
6533                }
6534                Ok(Some(PreparedSerialStoreBranch {
6535                    branch_id: branch_id.expect("nonempty branch"),
6536                    base: base_value,
6537                    base_head_bytes: base_bytes,
6538                    base_head_version: base_version,
6539                    writes,
6540                    head,
6541                }))
6542            })
6543            .await?;
6544        if let Some(branch) = &loaded {
6545            for write in &branch.writes {
6546                for blob in &write.audiences.blobs {
6547                    if let Some(spool_path) = blob.spool_path() {
6548                        crate::local_blob::verify_exact_file(blob.blob().object(), spool_path)
6549                            .await
6550                            .map_err(|error| {
6551                                DbError::Message(format!("prepared Serial blob spool: {error}"))
6552                            })?;
6553                    }
6554                }
6555            }
6556        }
6557        Ok(loaded)
6558    }
6559
6560    pub(crate) async fn prepared_serial_candidate_abandonment(
6561        &self,
6562    ) -> Result<Option<PreparedSerialCandidateAbandonment>, DbError> {
6563        self.call(|conn| {
6564            let raw: Option<String> = conn
6565                .query_row(
6566                    "SELECT value FROM protocol_state WHERE key = ?1",
6567                    [SERIAL_CANDIDATE_ABANDONMENT_STATE_KEY],
6568                    |row| row.get(0),
6569                )
6570                .optional()
6571                .map_err(DbError::from)?;
6572            let Some(raw) = raw else {
6573                return Ok(None);
6574            };
6575            let durable: DurableSerialCandidateAbandonment =
6576                serde_json::from_str(&raw).map_err(|error| {
6577                    DbError::Message(format!("prepared Serial candidate abandonment: {error}"))
6578                })?;
6579            let expected_base = serde_json::to_string(&StoreWriteBase::Serial {
6580                branch_id: durable.branch_id.clone(),
6581                base: durable.base.clone(),
6582            })
6583            .map_err(|error| {
6584                DbError::Message(format!("Serial abandonment branch base: {error}"))
6585            })?;
6586            let raw_prepared: String = conn
6587                .query_row(
6588                    "SELECT prepared FROM store_writes
6589                     WHERE base = ?1 AND prepared IS NOT NULL
6590                     ORDER BY ordinal LIMIT 1",
6591                    [expected_base.as_str()],
6592                    |row| row.get(0),
6593                )
6594                .map_err(DbError::from)?;
6595            let parsed = parse_prepared_serial_candidate(&raw_prepared)?.ok_or_else(|| {
6596                DbError::Message("Serial abandonment target is not an exact candidate".to_string())
6597            })?;
6598            if parsed.reference != durable.candidate {
6599                return Err(DbError::Message(
6600                    "Serial abandonment target differs from its durable candidate".to_string(),
6601                ));
6602            }
6603            let prepared: PreparedStoreWriteState =
6604                serde_json::from_str(&raw_prepared).map_err(|error| {
6605                    DbError::Message(format!("Serial abandonment candidate state: {error}"))
6606                })?;
6607            let PreparedStoreWriteState::Serial {
6608                base_head_bytes,
6609                base_head_version,
6610                commit,
6611                ..
6612            } = prepared
6613            else {
6614                return Err(DbError::Message(
6615                    "Serial abandonment target is not an exact candidate".to_string(),
6616                ));
6617            };
6618            if base_head_bytes.as_deref() != Some(durable.base_head.bytes.as_slice())
6619                || base_head_version.as_ref() != Some(&durable.base_head.version)
6620            {
6621                return Err(DbError::Message(
6622                    "Serial abandonment differs from its durable branch base".to_string(),
6623                ));
6624            }
6625            let candidate = ExactProtocolObject {
6626                value: parsed.commit,
6627                bytes: parsed.canonical_signed_bytes,
6628                object: parsed.reference.object,
6629                prepared: commit.prepared,
6630            };
6631            let raw_tip_prepared: String = conn
6632                .query_row(
6633                    "SELECT prepared FROM store_writes
6634                     WHERE base = ?1 AND prepared IS NOT NULL
6635                     ORDER BY ordinal DESC LIMIT 1",
6636                    [expected_base],
6637                    |row| row.get(0),
6638                )
6639                .map_err(DbError::from)?;
6640            let tip_prepared: PreparedStoreWriteState = serde_json::from_str(&raw_tip_prepared)
6641                .map_err(|error| {
6642                    DbError::Message(format!("Serial abandonment tip state: {error}"))
6643                })?;
6644            if !matches!(
6645                tip_prepared,
6646                PreparedStoreWriteState::Serial {
6647                    tip_head_bytes: Some(ref tip_head_bytes),
6648                    ..
6649                } if tip_head_bytes == &durable.original_head_bytes
6650            ) {
6651                return Err(DbError::Message(
6652                    "Serial abandonment original head differs from its durable branch tip"
6653                        .to_string(),
6654                ));
6655            }
6656            let root = required_store_root_authority_on(conn)?;
6657            let unverified: StoreBatchCommit =
6658                serde_json::from_slice(&durable.commit.semantic_bytes).map_err(|error| {
6659                    DbError::Message(format!("Serial abandonment commit: {error}"))
6660                })?;
6661            let registration =
6662                load_activated_registration_on(conn, &root, &unverified.author_registration)?;
6663            let value = StoreBatchCommit::parse_at(
6664                &durable.commit.semantic_bytes,
6665                root.store_root_hash,
6666                &durable.candidate.coord,
6667                &registration,
6668            )
6669            .map_err(|error| {
6670                DbError::Message(format!("verify Serial abandonment commit: {error}"))
6671            })?;
6672            let reference = StoreBatchCommitRef::from_commit(
6673                &value,
6674                durable.candidate.coord.clone(),
6675                durable.commit.prepared.reference().clone(),
6676            )
6677            .map_err(|error| DbError::Message(error.to_string()))?;
6678            if value.abandoned_candidates()
6679                != [crate::sync::store_commit::CandidateCleanupManifest {
6680                    candidate: crate::sync::store_commit::StoreBatchCommitDeletionTarget {
6681                        coord: durable.candidate.coord.clone(),
6682                        object: durable.candidate.object.clone(),
6683                        canonical_signed_bytes: candidate.bytes.clone(),
6684                    },
6685                }]
6686            {
6687                return Err(DbError::Message(
6688                    "durable Serial abandonment names another candidate".to_string(),
6689                ));
6690            }
6691            let head =
6692                StoreSerialHead::parse(&durable.head_bytes, root.store_root_hash, &registration)
6693                    .map_err(|error| {
6694                        DbError::Message(format!("verify Serial abandonment head: {error}"))
6695                    })?;
6696            if !matches!(
6697                &head.state,
6698                StoreSerialHeadState::Commit { commit, .. } if commit == &reference
6699            ) {
6700                return Err(DbError::Message(
6701                    "durable Serial abandonment head names another commit".to_string(),
6702                ));
6703            }
6704            let unverified_original: StoreSerialHead =
6705                serde_json::from_slice(&durable.original_head_bytes).map_err(|error| {
6706                    DbError::Message(format!("Serial abandonment original head: {error}"))
6707                })?;
6708            let original_author = match &unverified_original.state {
6709                StoreSerialHeadState::Commit {
6710                    author_registration,
6711                    ..
6712                } => author_registration,
6713                StoreSerialHeadState::Genesis { .. } => {
6714                    return Err(DbError::Message(
6715                        "Serial abandonment original head is not a commit".to_string(),
6716                    ));
6717                }
6718            };
6719            let original_registration =
6720                load_activated_registration_on(conn, &root, original_author)?;
6721            StoreSerialHead::parse(
6722                &durable.original_head_bytes,
6723                root.store_root_hash,
6724                &original_registration,
6725            )
6726            .map_err(|error| {
6727                DbError::Message(format!("verify Serial abandonment original head: {error}"))
6728            })?;
6729            Ok(Some(PreparedSerialCandidateAbandonment {
6730                branch_id: durable.branch_id,
6731                base: durable.base,
6732                base_head: durable.base_head,
6733                authority: ExactProtocolObject {
6734                    value,
6735                    bytes: durable.commit.semantic_bytes,
6736                    object: reference.object,
6737                    prepared: durable.commit.prepared,
6738                },
6739                head: CanonicalProtocolObject {
6740                    value: head,
6741                    bytes: durable.head_bytes,
6742                },
6743                original_head_bytes: durable.original_head_bytes,
6744                durable_state: raw,
6745            }))
6746        })
6747        .await
6748    }
6749
6750    pub(crate) async fn latest_local_store_position(
6751        &self,
6752    ) -> Result<Option<StoreBatchCommitRef>, DbError> {
6753        let write_policy = self.write_policy();
6754        self.call(move |conn| {
6755            let stream_id = match write_policy {
6756                WritePolicy::MergeConcurrent => {
6757                    let (root, registration, _) = local_store_authority_on(conn)?;
6758                    AuthorStreamId::store_announcements(&root, &registration).to_string()
6759                }
6760                WritePolicy::Serial => SERIAL_STREAM_ID.to_string(),
6761            };
6762            Self::latest_position_for_device_on(conn, &stream_id)
6763        })
6764        .await
6765    }
6766
6767    pub(crate) async fn complete_prepared_store_write(
6768        &self,
6769        accepted: StoreBatchCommitRef,
6770    ) -> Result<(), DbError> {
6771        let statuses = self.state.write_statuses.clone();
6772        let gates = self.state.gates.clone();
6773        let synced_tables = self.state.synced_tables.clone();
6774        self.call(move |conn| {
6775            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
6776            let local_device_id: String = tx
6777                .query_row(
6778                    "SELECT value FROM protocol_state WHERE key = ?1",
6779                    [LOCAL_DEVICE_ID_STATE_KEY],
6780                    |row| row.get(0),
6781                )
6782                .map_err(DbError::from)?;
6783            let prepared_count: i64 = tx
6784                .query_row(
6785                    "SELECT COUNT(*) FROM store_writes WHERE prepared IS NOT NULL",
6786                    [],
6787                    |row| row.get(0),
6788                )
6789                .map_err(DbError::from)?;
6790            if prepared_count != 1 {
6791                return Err(DbError::Message(format!(
6792                    "Store publication expected one prepared write, found {prepared_count}"
6793                )));
6794            }
6795            let (stored_write_id, raw_prepared): (String, String) = tx
6796                .query_row(
6797                    "SELECT write_id, prepared FROM store_writes
6798                     WHERE prepared IS NOT NULL ORDER BY ordinal LIMIT 1",
6799                    [],
6800                    |row| Ok((row.get(0)?, row.get(1)?)),
6801                )
6802                .map_err(DbError::from)?;
6803            let prepared: PreparedStoreWriteState = serde_json::from_str(&raw_prepared)
6804                .map_err(|error| DbError::Message(format!("prepared Store write: {error}")))?;
6805            if let PreparedStoreWriteState::MergeAbandonment {
6806                candidate_commit,
6807                candidate_head,
6808                authority_commit,
6809                authority_head,
6810                ..
6811            } = &prepared
6812            {
6813                let root = required_store_root_authority_on(&tx)?;
6814                let candidate =
6815                    parse_prepared_merge_candidate_parts_on(&tx, candidate_commit, candidate_head)?;
6816                let authority =
6817                    parse_prepared_merge_candidate_parts_on(&tx, authority_commit, authority_head)?;
6818                if authority.commit.write_id.as_str() != stored_write_id
6819                    || accepted != authority.reference
6820                    || !matches!(
6821                        &authority.commit.body,
6822                        crate::sync::store_commit::StoreCommitBody::AbandonCandidates { .. }
6823                    )
6824                {
6825                    return Err(DbError::Message(
6826                        "accepted Merge abandonment differs from its durable authority".to_string(),
6827                    ));
6828                }
6829                let registration = load_activated_registration_on(
6830                    &tx,
6831                    &root,
6832                    &authority.commit.author_registration,
6833                )?;
6834                StoreDeviceHead::parse_at(
6835                    &authority.head.to_bytes(),
6836                    root.store_root_hash,
6837                    &registration,
6838                    &accepted,
6839                )
6840                .map_err(|error| {
6841                    DbError::Message(format!("verify accepted Merge abandonment head: {error}"))
6842                })?;
6843                for object in [
6844                    authority_commit.prepared.reference(),
6845                    authority_head.prepared.reference(),
6846                ] {
6847                    let object_id = remote_object_id(object);
6848                    let remote = load_remote_object_on(&tx, object_id)?
6849                        .into_activated(&accepted)
6850                        .map_err(|error| {
6851                            DbError::Message(format!(
6852                                "activate Merge abandonment object {object_id}: {error}"
6853                            ))
6854                        })?;
6855                    update_remote_object_on(&tx, object_id, &remote)?;
6856                }
6857                let winner_head = crate::sync::store_commit::StoreDeviceHeadRef {
6858                    head_hash: authority.head.head_hash(),
6859                    object: authority.head_prepared.reference().clone(),
6860                };
6861                begin_merge_candidate_nonactivation_on(
6862                    &tx,
6863                    &WriteId::from_generated(stored_write_id.clone()),
6864                    &candidate,
6865                    winner_head.clone(),
6866                    true,
6867                )?;
6868                Self::record_materialized_commit_on(&tx, &authority.commit, &accepted)?;
6869                let mut completed_preparation = prepared.clone();
6870                let PreparedStoreWriteState::MergeAbandonment { outcome, .. } =
6871                    &mut completed_preparation
6872                else {
6873                    unreachable!("matched Merge abandonment")
6874                };
6875                *outcome = MergeAbandonmentOutcome::Accepted {
6876                    authority: accepted.clone(),
6877                };
6878                let completed_preparation =
6879                    serde_json::to_string(&completed_preparation).map_err(|error| {
6880                        DbError::Message(format!("serialize accepted Merge abandonment: {error}"))
6881                    })?;
6882                let updated = tx
6883                    .execute(
6884                        "UPDATE store_writes SET prepared = ?2
6885                         WHERE write_id = ?1 AND prepared = ?3",
6886                        rusqlite::params![
6887                            stored_write_id.as_str(),
6888                            completed_preparation,
6889                            raw_prepared
6890                        ],
6891                    )
6892                    .map_err(DbError::from)?;
6893                if updated != 1 {
6894                    return Err(DbError::Message(
6895                        "Merge abandonment changed during activation".to_string(),
6896                    ));
6897                }
6898                let blocked = WriteStatus::Blocked(crate::WriteBlock::InvalidProtocolState {
6899                    reason: format!(
6900                        "candidate abandonment {} is accepted; exact cleanup is pending",
6901                        winner_head.head_hash
6902                    ),
6903                });
6904                let write_id = authority.commit.write_id.clone();
6905                Self::set_write_status_on(&tx, &write_id, &blocked)?;
6906                tx.commit().map_err(DbError::from)?;
6907                Self::notify_write_status_in(&statuses, &write_id, blocked);
6908                return Ok(());
6909            }
6910            let PreparedStoreWriteState::MergeConcurrent {
6911                commit,
6912                head,
6913                local_cleanup,
6914                ..
6915            } = prepared
6916            else {
6917                return Err(DbError::Message(
6918                    "serial branch reached MergeConcurrent completion".to_string(),
6919                ));
6920            };
6921            let root = required_store_root_authority_on(&tx)?;
6922            let unverified: StoreBatchCommit = serde_json::from_slice(&commit.semantic_bytes)
6923                .map_err(|error| DbError::Message(format!("prepared Store commit: {error}")))?;
6924            let registration =
6925                load_activated_registration_on(&tx, &root, &unverified.author_registration)?;
6926            let expected_stream =
6927                AuthorStreamId::store_announcements(&root, &unverified.author_registration);
6928            if !matches!(
6929                accepted.coord,
6930                StoreCommitCoord::MergeConcurrent { stream_id, .. }
6931                    if stream_id == expected_stream
6932            ) || accepted.object != *commit.prepared.reference()
6933            {
6934                return Err(DbError::Message(
6935                    "accepted Merge head differs from the exact prepared commit".to_string(),
6936                ));
6937            }
6938            let commit_value = StoreBatchCommit::parse_at(
6939                &commit.semantic_bytes,
6940                root.store_root_hash,
6941                &accepted.coord,
6942                &registration,
6943            )
6944            .map_err(|error| DbError::Message(format!("outbound commit: {error}")))?;
6945            accepted
6946                .verify_commit(&commit_value)
6947                .map_err(|error| DbError::Message(error.to_string()))?;
6948            if commit_value.write_id.as_str() != stored_write_id {
6949                return Err(DbError::Message(
6950                    "prepared write id differs from signed commit".to_string(),
6951                ));
6952            }
6953            let write_id = commit_value.write_id.clone();
6954            let head_object_id = remote_object_id(head.prepared.reference());
6955            Self::activate_prepared_write_on(
6956                &tx,
6957                &gates,
6958                &synced_tables,
6959                &write_id,
6960                &commit_value,
6961                &accepted,
6962                local_cleanup,
6963                &[head_object_id],
6964            )?;
6965            let cleared = tx
6966                .execute(
6967                    "UPDATE store_writes SET prepared = NULL
6968                     WHERE write_id = ?1 AND prepared IS NOT NULL",
6969                    [stored_write_id.as_str()],
6970                )
6971                .map_err(DbError::from)?;
6972            if cleared != 1 {
6973                return Err(DbError::Message(
6974                    "prepared Store write disappeared".to_string(),
6975                ));
6976            }
6977            let status = WriteStatus::Published(Box::new(PublishedPosition::MergeConcurrent {
6978                device_id: local_device_id,
6979                commit: accepted.clone(),
6980            }));
6981            Self::set_write_status_on(&tx, &write_id, &status)?;
6982            tx.commit().map_err(DbError::from)?;
6983            Self::notify_write_status_in(&statuses, &write_id, status);
6984            Ok(())
6985        })
6986        .await
6987    }
6988
6989    pub(crate) async fn mark_serial_branch_conflict(
6990        &self,
6991        branch_id: PendingBranchId,
6992        base: Option<StoreBatchCommitRef>,
6993        current: StoreSerialPredecessor,
6994    ) -> Result<(), DbError> {
6995        let statuses = self.state.write_statuses.clone();
6996        self.call(move |conn| {
6997            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
6998            let expected_base = StoreWriteBase::Serial {
6999                branch_id: branch_id.clone(),
7000                base: base.clone(),
7001            };
7002            let conflict = crate::SerializationConflict {
7003                branch_id: branch_id.clone(),
7004                base: store_serial_predecessor_on(&tx, base.as_ref())?,
7005                current,
7006            };
7007            let status = WriteStatus::Conflict(Box::new(conflict));
7008            let status_json = serde_json::to_string(&status)
7009                .map_err(|error| DbError::Message(format!("serialize Serial conflict: {error}")))?;
7010            let mut statement = tx
7011                .prepare(
7012                    "SELECT write_id, base FROM store_writes
7013                     WHERE status != '\"local_only\"'
7014                       AND json_extract(status, '$.published') IS NULL
7015                       AND json_extract(status, '$.resolved') IS NULL
7016                     ORDER BY ordinal",
7017                )
7018                .map_err(DbError::from)?;
7019            let rows = statement
7020                .query_map([], |row| {
7021                    Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
7022                })
7023                .map_err(DbError::from)?;
7024            let write_ids = Self::write_ids_matching_serial_base(rows, &expected_base)?;
7025            drop(statement);
7026            if write_ids.is_empty() {
7027                return Err(DbError::Message(format!(
7028                    "Serial branch {:?} has no pending writes",
7029                    branch_id.first_write_id()
7030                )));
7031            }
7032            for write_id in &write_ids {
7033                let updated = tx
7034                    .execute(
7035                        "UPDATE store_writes SET status = ?2 WHERE write_id = ?1",
7036                        rusqlite::params![write_id.as_str(), &status_json],
7037                    )
7038                    .map_err(DbError::from)?;
7039                if updated != 1 {
7040                    return Err(DbError::Message(format!(
7041                        "Serial conflict write {write_id} disappeared"
7042                    )));
7043                }
7044            }
7045            tx.commit().map_err(DbError::from)?;
7046            for write_id in write_ids {
7047                Self::notify_write_status_in(&statuses, &write_id, status.clone());
7048            }
7049            Ok(())
7050        })
7051        .await
7052    }
7053
7054    pub(crate) async fn mark_merge_candidate_conflict(
7055        &self,
7056        write_id: WriteId,
7057        winner_commit: StoreBatchCommitRef,
7058        winner_head: crate::sync::store_commit::StoreDeviceHeadRef,
7059    ) -> Result<(), DbError> {
7060        let statuses = self.state.write_statuses.clone();
7061        let notified_write_id = write_id.clone();
7062        self.call(move |conn| {
7063            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
7064            let (raw_status, raw_prepared): (String, String) = tx
7065                .query_row(
7066                    "SELECT status, prepared FROM store_writes WHERE write_id = ?1",
7067                    [write_id.as_str()],
7068                    |row| Ok((row.get(0)?, row.get(1)?)),
7069                )
7070                .map_err(DbError::from)?;
7071            let status: WriteStatus = serde_json::from_str(&raw_status)
7072                .map_err(|error| DbError::Message(format!("Merge candidate status: {error}")))?;
7073            if !matches!(status, WriteStatus::Publishing) {
7074                return Err(DbError::Message(format!(
7075                    "Merge candidate {write_id} is not publishing"
7076                )));
7077            }
7078            let prepared: PreparedStoreWriteState = serde_json::from_str(&raw_prepared)
7079                .map_err(|error| DbError::Message(format!("prepared Merge candidate: {error}")))?;
7080            let prepared_candidate = parse_prepared_merge_candidate_on(&tx, &prepared)?
7081                .ok_or_else(|| {
7082                    DbError::Message("Serial branch reached Merge candidate conflict".to_string())
7083                })?;
7084            let publication = parse_prepared_merge_publication_on(&tx, &prepared)?
7085                .expect("parsed Merge publication");
7086            if winner_head.object.slot() != publication.head_prepared.reference().slot()
7087                || winner_head.object == *publication.head_prepared.reference()
7088            {
7089                return Err(DbError::Message(
7090                    "Merge winner does not replace the prepared exact head slot".to_string(),
7091                ));
7092            }
7093            if prepared_candidate.commit.write_id != write_id {
7094                return Err(DbError::Message(
7095                    "prepared Merge graph differs from its write identity".to_string(),
7096                ));
7097            }
7098            if matches!(&prepared, PreparedStoreWriteState::MergeAbandonment { .. }) {
7099                begin_merge_candidate_nonactivation_on(
7100                    &tx,
7101                    &write_id,
7102                    &publication,
7103                    winner_head.clone(),
7104                    false,
7105                )?;
7106                if winner_commit != prepared_candidate.reference {
7107                    begin_merge_candidate_nonactivation_on(
7108                        &tx,
7109                        &write_id,
7110                        &prepared_candidate,
7111                        winner_head.clone(),
7112                        true,
7113                    )?;
7114                }
7115                let mut lost_preparation = prepared.clone();
7116                let PreparedStoreWriteState::MergeAbandonment { outcome, .. } =
7117                    &mut lost_preparation
7118                else {
7119                    unreachable!("matched Merge abandonment")
7120                };
7121                *outcome = MergeAbandonmentOutcome::Lost {
7122                    winner_commit: winner_commit.clone(),
7123                    winner_head: winner_head.clone(),
7124                };
7125                let lost_preparation =
7126                    serde_json::to_string(&lost_preparation).map_err(|error| {
7127                        DbError::Message(format!("serialize lost Merge abandonment: {error}"))
7128                    })?;
7129                let updated = tx
7130                    .execute(
7131                        "UPDATE store_writes SET prepared = ?2
7132                         WHERE write_id = ?1 AND prepared = ?3",
7133                        rusqlite::params![write_id.as_str(), lost_preparation, raw_prepared],
7134                    )
7135                    .map_err(DbError::from)?;
7136                if updated != 1 {
7137                    return Err(DbError::Message(
7138                        "Merge abandonment changed while recording its winner".to_string(),
7139                    ));
7140                }
7141            } else {
7142                begin_merge_candidate_nonactivation_on(
7143                    &tx,
7144                    &write_id,
7145                    &prepared_candidate,
7146                    winner_head.clone(),
7147                    true,
7148                )?;
7149            }
7150            let blocked = WriteStatus::Blocked(crate::WriteBlock::InvalidProtocolState {
7151                reason: format!(
7152                    "Merge successor slot is occupied by signed head {}",
7153                    winner_head.head_hash
7154                ),
7155            });
7156            Self::set_write_status_on(&tx, &write_id, &blocked)?;
7157            tx.commit().map_err(DbError::from)?;
7158            Self::notify_write_status_in(&statuses, &notified_write_id, blocked);
7159            Ok(())
7160        })
7161        .await
7162    }
7163
7164    pub(crate) async fn exact_serial_predecessor(
7165        &self,
7166        commit: Option<StoreBatchCommitRef>,
7167    ) -> Result<StoreSerialPredecessor, DbError> {
7168        self.call(move |conn| store_serial_predecessor_on(conn, commit.as_ref()))
7169            .await
7170    }
7171
7172    pub(crate) async fn complete_prepared_serial_branch(
7173        &self,
7174        accepted: VersionedObject,
7175    ) -> Result<u64, DbError> {
7176        let statuses = self.state.write_statuses.clone();
7177        let gates = self.state.gates.clone();
7178        let synced_tables = self.state.synced_tables.clone();
7179        self.call(move |conn| {
7180            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
7181            let root = required_store_root_authority_on(&tx)?;
7182            let accepted_unverified: StoreSerialHead = serde_json::from_slice(&accepted.bytes)
7183                .map_err(|error| DbError::Message(format!("accepted Serial head: {error}")))?;
7184            let accepted_author_ref = match &accepted_unverified.state {
7185                StoreSerialHeadState::Commit {
7186                    author_registration,
7187                    ..
7188                } => author_registration,
7189                StoreSerialHeadState::Genesis { .. } => {
7190                    return Err(DbError::Message(
7191                        "prepared Serial branch cannot complete at a genesis head".to_string(),
7192                    ));
7193                }
7194            };
7195            let accepted_author = load_activated_registration_on(&tx, &root, accepted_author_ref)?;
7196            let accepted_head =
7197                StoreSerialHead::parse(&accepted.bytes, root.store_root_hash, &accepted_author)
7198                    .map_err(|error| {
7199                        DbError::Message(format!("verify accepted Serial head: {error}"))
7200                    })?;
7201            let accepted_tip = match &accepted_head.state {
7202                StoreSerialHeadState::Commit { commit, .. } => commit.clone(),
7203                StoreSerialHeadState::Genesis { .. } => unreachable!("rejected above"),
7204            };
7205            let mut statement = tx
7206                .prepare(
7207                    "SELECT write_id, prepared, base FROM store_writes
7208                     WHERE prepared IS NOT NULL AND status = '\"publishing\"'
7209                     ORDER BY ordinal",
7210                )
7211                .map_err(DbError::from)?;
7212            let rows = statement
7213                .query_map([], |row| {
7214                    Ok((
7215                        row.get::<_, String>(0)?,
7216                        row.get::<_, String>(1)?,
7217                        row.get::<_, String>(2)?,
7218                    ))
7219                })
7220                .map_err(DbError::from)?
7221                .collect::<Result<Vec<_>, _>>()
7222                .map_err(DbError::from)?;
7223            drop(statement);
7224            let mut completed = Vec::new();
7225            let mut completed_base = None;
7226            let mut predecessor = None;
7227            let mut prepared_tip_head = None;
7228            for row in rows {
7229                let (stored_write_id, prepared, raw_base) = row;
7230                let stored_base: StoreWriteBase = serde_json::from_str(&raw_base)
7231                    .map_err(|error| DbError::Message(format!("prepared Serial base: {error}")))?;
7232                match &completed_base {
7233                    Some(expected) if expected != &stored_base => {
7234                        return Err(DbError::Message(
7235                            "prepared Serial branch contains inconsistent bases".to_string(),
7236                        ));
7237                    }
7238                    None => completed_base = Some(stored_base.clone()),
7239                    Some(_) => {}
7240                }
7241                let PreparedStoreWriteState::Serial {
7242                    commit,
7243                    tip_head_bytes,
7244                    local_cleanup,
7245                    ..
7246                } = serde_json::from_str(&prepared)
7247                    .map_err(|error| DbError::Message(format!("prepared Serial write: {error}")))?
7248                else {
7249                    return Err(DbError::Message(
7250                        "non-Serial write reached Serial completion".to_string(),
7251                    ));
7252                };
7253                let unverified: StoreBatchCommit = serde_json::from_slice(&commit.semantic_bytes)
7254                    .map_err(|error| {
7255                    DbError::Message(format!("prepared Serial commit: {error}"))
7256                })?;
7257                if predecessor.is_none() {
7258                    let StoreWriteBase::Serial { base, .. } = &stored_base else {
7259                        return Err(DbError::Message(
7260                            "Merge base reached Serial completion".to_string(),
7261                        ));
7262                    };
7263                    predecessor = base.clone();
7264                }
7265                let sequence = predecessor
7266                    .as_ref()
7267                    .map_or(1, |reference: &StoreBatchCommitRef| {
7268                        reference.coord.sequence().saturating_add(1)
7269                    });
7270                let coord = StoreCommitCoord::Serial { sequence };
7271                let registration =
7272                    load_activated_registration_on(&tx, &root, &unverified.author_registration)?;
7273                let commit_value = StoreBatchCommit::parse_at(
7274                    &commit.semantic_bytes,
7275                    root.store_root_hash,
7276                    &coord,
7277                    &registration,
7278                )
7279                .map_err(|error| DbError::Message(format!("outbound Serial commit: {error}")))?;
7280                let commit_ref = StoreBatchCommitRef::from_commit(
7281                    &commit_value,
7282                    coord,
7283                    commit.prepared.reference().clone(),
7284                )
7285                .map_err(|error| DbError::Message(error.to_string()))?;
7286                let order_matches = match (&predecessor, &commit_value.order) {
7287                    (
7288                        Some(expected),
7289                        crate::sync::store_commit::StoreCommitOrder::Serial {
7290                            predecessor: StoreSerialPredecessor::Commit(actual),
7291                            ..
7292                        },
7293                    ) => actual == expected,
7294                    (
7295                        None,
7296                        crate::sync::store_commit::StoreCommitOrder::Serial {
7297                            predecessor:
7298                                StoreSerialPredecessor::Genesis {
7299                                    root: commit_root,
7300                                    founder_registration,
7301                                },
7302                            ..
7303                        },
7304                    ) => {
7305                        commit_root == &root
7306                            && founder_registration == &commit_value.author_registration
7307                    }
7308                    _ => false,
7309                };
7310                if !order_matches {
7311                    return Err(DbError::Message(
7312                        "prepared Serial completion chain has a different exact predecessor"
7313                            .to_string(),
7314                    ));
7315                }
7316                if commit_value.write_id.as_str() != stored_write_id {
7317                    return Err(DbError::Message(
7318                        "prepared Serial write id differs from signed commit".to_string(),
7319                    ));
7320                }
7321                let write_id = commit_value.write_id.clone();
7322                Self::activate_prepared_write_on(
7323                    &tx,
7324                    &gates,
7325                    &synced_tables,
7326                    &write_id,
7327                    &commit_value,
7328                    &commit_ref,
7329                    local_cleanup,
7330                    &[],
7331                )?;
7332                let cleared = tx
7333                    .execute(
7334                        "UPDATE store_writes SET prepared = NULL WHERE write_id = ?1",
7335                        [write_id.as_str()],
7336                    )
7337                    .map_err(DbError::from)?;
7338                if cleared != 1 {
7339                    return Err(DbError::Message(format!(
7340                        "prepared Serial write {write_id} disappeared during completion"
7341                    )));
7342                }
7343                let status = WriteStatus::Published(Box::new(PublishedPosition::Serial {
7344                    commit: commit_ref.clone(),
7345                }));
7346                Self::set_write_status_on(&tx, &write_id, &status)?;
7347                if let Some(bytes) = tip_head_bytes {
7348                    if prepared_tip_head.replace(bytes).is_some() {
7349                        return Err(DbError::Message(
7350                            "prepared Serial branch has multiple tip heads".to_string(),
7351                        ));
7352                    }
7353                }
7354                predecessor = Some(commit_ref.clone());
7355                completed.push((write_id, status, commit_ref));
7356            }
7357            let Some((_, _, final_ref)) = completed.last() else {
7358                return Err(DbError::Message(
7359                    "prepared Serial branch is absent".to_string(),
7360                ));
7361            };
7362            if final_ref != &accepted_tip
7363                || prepared_tip_head.as_deref() != Some(accepted.bytes.as_slice())
7364            {
7365                return Err(DbError::Message(
7366                    "accepted Serial head differs from the exact prepared branch tip".to_string(),
7367                ));
7368            }
7369            tx.execute(
7370                "INSERT INTO serial_head_receipt \
7371                 (singleton, head_bytes, version_token, commit_ref) VALUES (1, ?1, ?2, ?3) \
7372                 ON CONFLICT(singleton) DO UPDATE SET \
7373                   head_bytes = excluded.head_bytes, \
7374                   version_token = excluded.version_token, \
7375                   commit_ref = excluded.commit_ref",
7376                (
7377                    &accepted.bytes,
7378                    serde_json::to_string(&accepted.version).map_err(|error| {
7379                        DbError::Message(format!("serialize Serial version receipt: {error}"))
7380                    })?,
7381                    serde_json::to_string(&accepted_tip).map_err(|error| {
7382                        DbError::Message(format!("serialize accepted Serial commit ref: {error}"))
7383                    })?,
7384                ),
7385            )
7386            .map_err(DbError::from)?;
7387            let completed_base = completed_base.ok_or_else(|| {
7388                DbError::Message("prepared Serial branch base is absent".to_string())
7389            })?;
7390            let suffix_first: Option<String> = tx
7391                .query_row(
7392                    "SELECT write_id FROM store_writes
7393                     WHERE status = '\"pending\"' AND prepared IS NULL AND base = ?1
7394                     ORDER BY ordinal LIMIT 1",
7395                    [serde_json::to_string(&completed_base).map_err(|error| {
7396                        DbError::Message(format!("serialize Serial base: {error}"))
7397                    })?],
7398                    |row| row.get(0),
7399                )
7400                .optional()
7401                .map_err(DbError::from)?;
7402            if let Some(suffix_first) = suffix_first {
7403                let rebased = StoreWriteBase::Serial {
7404                    branch_id: PendingBranchId::from_first_write(WriteId::from_generated(
7405                        suffix_first,
7406                    )),
7407                    base: Some(accepted_tip.clone()),
7408                };
7409                tx.execute(
7410                    "UPDATE store_writes SET base = ?2
7411                     WHERE status = '\"pending\"' AND prepared IS NULL AND base = ?1",
7412                    rusqlite::params![
7413                        serde_json::to_string(&completed_base).map_err(
7414                            |error| DbError::Message(format!(
7415                                "serialize completed Serial base: {error}"
7416                            ))
7417                        )?,
7418                        serde_json::to_string(&rebased).map_err(|error| DbError::Message(
7419                            format!("serialize rebased Serial suffix: {error}")
7420                        ))?,
7421                    ],
7422                )
7423                .map_err(DbError::from)?;
7424            }
7425            let count = u64::try_from(completed.len())
7426                .map_err(|_| DbError::Message("Serial completion count exceeds u64".to_string()))?;
7427            tx.commit().map_err(DbError::from)?;
7428            for (write_id, status, _) in completed {
7429                Self::notify_write_status_in(&statuses, &write_id, status);
7430            }
7431            Ok(count)
7432        })
7433        .await
7434    }
7435
7436    fn validate_serial_candidate_cleanup_on(
7437        conn: &Connection,
7438        write_id: &WriteId,
7439        raw_prepared: Option<&str>,
7440    ) -> Result<(), DbError> {
7441        let Some(raw_prepared) = raw_prepared else {
7442            return Ok(());
7443        };
7444        let Some(candidate) = parse_prepared_serial_candidate(raw_prepared)? else {
7445            return Ok(());
7446        };
7447        let mut object_ids = vec![remote_object_id(&candidate.reference.object)];
7448        let mut statement = conn
7449            .prepare(
7450                "SELECT remote_object_id FROM store_write_packages WHERE write_id = ?1
7451                 UNION
7452                 SELECT remote_object_id FROM store_write_blobs WHERE write_id = ?1",
7453            )
7454            .map_err(DbError::from)?;
7455        let indexed = statement
7456            .query_map([write_id.as_str()], |row| row.get::<_, String>(0))
7457            .map_err(DbError::from)?
7458            .collect::<Result<Vec<_>, _>>()
7459            .map_err(DbError::from)?;
7460        drop(statement);
7461        for encoded in indexed {
7462            object_ids.push(encoded.parse().map_err(|error| {
7463                DbError::Message(format!("Serial cleanup remote object id: {error}"))
7464            })?);
7465        }
7466        for object_id in object_ids {
7467            let remote = load_remote_object_on(conn, object_id)?;
7468            if !remote
7469                .candidate_cleanup_complete(&candidate.reference)
7470                .map_err(|error| {
7471                    DbError::Message(format!(
7472                        "validate candidate cleanup for {object_id}: {error}"
7473                    ))
7474                })?
7475            {
7476                return Err(DbError::Message(format!(
7477                    "candidate cleanup for remote object {object_id} is incomplete"
7478                )));
7479            }
7480        }
7481        Ok(())
7482    }
7483
7484    fn apply_serial_resolution_on(
7485        tx: &rusqlite::Transaction<'_>,
7486        synced_tables: &[SyncedTable],
7487        branch_id: &PendingBranchId,
7488        plan: crate::sync::store_pull::SerialResolutionPlan,
7489    ) -> Result<Vec<WriteId>, DbError> {
7490        let schema = Arc::new(crate::sync::conflict::TableSchema::from_db(
7491            tx,
7492            synced_tables,
7493        )?);
7494        let mut statement = tx
7495            .prepare(
7496                "SELECT write_id, status, inverse_changeset, base, prepared FROM store_writes
7497                 WHERE status != '\"local_only\"'
7498                   AND json_extract(status, '$.published') IS NULL
7499                   AND json_extract(status, '$.resolved') IS NULL
7500                   AND json_type(base, '$.serial') IS NOT NULL
7501                 ORDER BY ordinal",
7502            )
7503            .map_err(DbError::from)?;
7504        let rows = statement
7505            .query_map([], |row| {
7506                Ok((
7507                    row.get::<_, String>(0)?,
7508                    row.get::<_, String>(1)?,
7509                    row.get::<_, Vec<u8>>(2)?,
7510                    row.get::<_, String>(3)?,
7511                    row.get::<_, Option<String>>(4)?,
7512                ))
7513            })
7514            .map_err(DbError::from)?;
7515        let mut branch = Vec::new();
7516        let mut branch_base = None;
7517        let mut saw_conflict = false;
7518        for row in rows {
7519            let (write_id, status, inverse, raw_base, prepared) = row.map_err(DbError::from)?;
7520            let status: WriteStatus = serde_json::from_str(&status)
7521                .map_err(|error| DbError::Message(format!("Serial branch status: {error}")))?;
7522            let base: StoreWriteBase = serde_json::from_str(&raw_base)
7523                .map_err(|error| DbError::Message(format!("Serial branch base: {error}")))?;
7524            let StoreWriteBase::Serial {
7525                branch_id: stored_branch_id,
7526                base,
7527            } = base
7528            else {
7529                return Err(DbError::Message(
7530                    "MergeConcurrent base reached Serial resolution".to_string(),
7531                ));
7532            };
7533            if &stored_branch_id != branch_id {
7534                return Err(DbError::Message(
7535                    "Serial database contains more than one unresolved branch".to_string(),
7536                ));
7537            }
7538            match status {
7539                WriteStatus::Conflict(conflict) => {
7540                    if conflict.branch_id != stored_branch_id
7541                        || conflict.base != store_serial_predecessor_on(tx, base.as_ref())?
7542                    {
7543                        return Err(DbError::Message(
7544                            "Serial conflict status differs from its durable branch base"
7545                                .to_string(),
7546                        ));
7547                    }
7548                    saw_conflict = true;
7549                }
7550                WriteStatus::Pending if prepared.is_none() => {}
7551                status => {
7552                    return Err(DbError::Message(format!(
7553                        "conflicted Serial branch write {write_id} has non-resolvable status {status:?}"
7554                    )))
7555                }
7556            }
7557            Self::validate_serial_candidate_cleanup_on(
7558                tx,
7559                &WriteId::from_generated(write_id.clone()),
7560                prepared.as_deref(),
7561            )?;
7562            if branch_base.as_ref().is_some_and(|stored| stored != &base) {
7563                return Err(DbError::Message(
7564                    "Serial conflict branch has inconsistent bases".to_string(),
7565                ));
7566            }
7567            branch_base.get_or_insert(base);
7568            branch.push((WriteId::from_generated(write_id), inverse));
7569        }
7570        drop(statement);
7571        if branch.is_empty() || !saw_conflict {
7572            return Err(DbError::Message(format!(
7573                "Serial branch {} is not conflicted",
7574                branch_id.first_write_id()
7575            )));
7576        }
7577        let branch_base = branch_base.expect("nonempty branch has a base value");
7578        let durable_base = Self::latest_position_for_device_on(tx, SERIAL_STREAM_ID)?;
7579        if durable_base != branch_base {
7580            return Err(DbError::Message(format!(
7581                "local Serial position {durable_base:?} differs from branch base {branch_base:?}"
7582            )));
7583        }
7584        for (_, inverse) in branch.iter().rev() {
7585            let inverse = crate::sync::apply::ValidatedChangeset::new(inverse, schema.clone())
7586                .map_err(|error| DbError::Message(format!("invalid Serial inverse: {error}")))?;
7587            crate::sync::apply::apply_changeset_strict_on(tx, inverse)
7588                .map_err(|error| DbError::Message(format!("reverse Serial branch: {error}")))?;
7589        }
7590        let mut predecessor = branch_base;
7591        for resolution in plan.commits {
7592            let expected_seq = match predecessor.as_ref() {
7593                Some(reference) => reference.coord.sequence().checked_add(1).ok_or_else(|| {
7594                    DbError::Message("Serial resolution sequence overflow".to_string())
7595                })?,
7596                None => 1,
7597            };
7598            if resolution.commit.seq() != expected_seq
7599                || resolution.commit_ref.coord
7600                    != (StoreCommitCoord::Serial {
7601                        sequence: expected_seq,
7602                    })
7603                || resolution.commit.order.predecessor() != predecessor.as_ref()
7604            {
7605                return Err(DbError::Message(format!(
7606                    "Serial resolution commit {} does not follow the branch base",
7607                    resolution.commit.seq()
7608                )));
7609            }
7610            if let Some(package) = resolution.package {
7611                let changeset = crate::sync::apply::ValidatedChangeset::new(
7612                    package,
7613                    schema.clone(),
7614                )
7615                .map_err(|error| {
7616                    DbError::Message(format!("invalid Serial resolution changeset: {error}"))
7617                })?;
7618                crate::sync::apply::apply_changeset_strict_on(tx, changeset).map_err(|error| {
7619                    DbError::Message(format!(
7620                        "apply Serial resolution commit {}: {error}",
7621                        resolution.commit.seq()
7622                    ))
7623                })?;
7624                let blob_decls = BlobDecls::from_tables(tx, synced_tables)
7625                    .map_err(|error| DbError::Message(error.to_string()))?;
7626                for intent in resolution.cleanup {
7627                    crate::blob::local_cleanup::record_if_unreferenced_on(
7628                        tx,
7629                        &blob_decls,
7630                        &intent,
7631                    )?;
7632                }
7633            }
7634            Self::record_activated_store_device_registrations_on(
7635                tx,
7636                &resolution.commit,
7637                &resolution.registrations,
7638            )?;
7639            Self::record_verified_circle_activations_on(
7640                tx,
7641                &resolution.commit,
7642                &resolution.commit_ref,
7643                &resolution.circle_activations,
7644            )?;
7645            Self::record_materialized_serial_commit_on(
7646                tx,
7647                &resolution.commit,
7648                &resolution.commit_ref,
7649                &resolution.authorization_after,
7650            )?;
7651            predecessor = Some(resolution.commit_ref);
7652        }
7653        let head_commit = match plan.head.state {
7654            StoreSerialHeadState::Commit { commit, .. } => Some(commit),
7655            StoreSerialHeadState::Genesis { .. } => None,
7656        };
7657        if predecessor != head_commit {
7658            return Err(DbError::Message(
7659                "Serial resolution commits do not reach the verified global head".to_string(),
7660            ));
7661        }
7662        Ok(branch.into_iter().map(|(write_id, _)| write_id).collect())
7663    }
7664
7665    fn resolve_unpublished_writes_on(
7666        tx: &rusqlite::Transaction<'_>,
7667        write_ids: &[WriteId],
7668        resolution: &WriteResolution,
7669    ) -> Result<(), DbError> {
7670        let status = WriteStatus::Resolved(resolution.clone());
7671        for write_id in write_ids {
7672            let raw_prepared: Option<String> = tx
7673                .query_row(
7674                    "SELECT prepared FROM store_writes WHERE write_id = ?1",
7675                    [write_id.as_str()],
7676                    |row| row.get(0),
7677                )
7678                .map_err(DbError::from)?;
7679            let mut removable = Vec::new();
7680            let mut candidate = None;
7681            if let Some(raw_prepared) = raw_prepared.as_deref() {
7682                let prepared: PreparedStoreWriteState = serde_json::from_str(raw_prepared)
7683                    .map_err(|error| {
7684                        DbError::Message(format!("resolved prepared write: {error}"))
7685                    })?;
7686                match &prepared {
7687                    PreparedStoreWriteState::MergeConcurrent { .. }
7688                    | PreparedStoreWriteState::MergeAbandonment { .. } => {
7689                        let merge = parse_prepared_merge_candidate_on(tx, &prepared)?
7690                            .expect("matched Merge preparation");
7691                        removable.push(remote_object_id(&merge.reference.object));
7692                        removable.push(remote_object_id(merge.head_prepared.reference()));
7693                        candidate = Some(merge.reference);
7694                    }
7695                    PreparedStoreWriteState::Serial { .. } => {
7696                        if let Some(serial) = parse_prepared_serial_candidate(raw_prepared)? {
7697                            removable.push(remote_object_id(&serial.reference.object));
7698                            candidate = Some(serial.reference);
7699                        }
7700                    }
7701                    PreparedStoreWriteState::SerialPreparing => {}
7702                }
7703            }
7704            let mut statement = tx
7705                .prepare(
7706                    "SELECT remote_object_id FROM store_write_packages WHERE write_id = ?1
7707                     UNION
7708                     SELECT remote_object_id FROM store_write_blobs WHERE write_id = ?1",
7709                )
7710                .map_err(DbError::from)?;
7711            let indexed = statement
7712                .query_map([write_id.as_str()], |row| row.get::<_, String>(0))
7713                .map_err(DbError::from)?
7714                .collect::<Result<Vec<_>, _>>()
7715                .map_err(DbError::from)?;
7716            drop(statement);
7717            for encoded in indexed {
7718                removable.push(encoded.parse().map_err(|error| {
7719                    DbError::Message(format!("resolved remote object id: {error}"))
7720                })?);
7721            }
7722            if let Some(candidate) = &candidate {
7723                for object_id in &removable {
7724                    let remote = load_remote_object_on(tx, *object_id)?;
7725                    if !remote
7726                        .candidate_cleanup_complete(candidate)
7727                        .map_err(|error| {
7728                            DbError::Message(format!(
7729                                "validate candidate cleanup for {object_id}: {error}"
7730                            ))
7731                        })?
7732                    {
7733                        return Err(DbError::Message(format!(
7734                            "candidate cleanup for remote object {object_id} is incomplete"
7735                        )));
7736                    }
7737                }
7738            }
7739            tx.execute(
7740                "DELETE FROM store_write_blob_leases WHERE write_id = ?1",
7741                [write_id.as_str()],
7742            )
7743            .map_err(DbError::from)?;
7744            tx.execute(
7745                "DELETE FROM store_write_packages WHERE write_id = ?1",
7746                [write_id.as_str()],
7747            )
7748            .map_err(DbError::from)?;
7749            tx.execute(
7750                "DELETE FROM store_write_blobs WHERE write_id = ?1",
7751                [write_id.as_str()],
7752            )
7753            .map_err(DbError::from)?;
7754            for object_id in removable {
7755                let remote = load_remote_object_on(tx, object_id)?;
7756                let absent = matches!(
7757                    remote,
7758                    RemoteObjectRecord::CandidateCommit(
7759                        crate::sync::remote_object::CandidateCommitRecord {
7760                            state:
7761                                crate::sync::remote_object::CandidateCommitState::AbsentVerified { .. },
7762                            ..
7763                        }
7764                    ) | RemoteObjectRecord::CandidateExclusive(
7765                        crate::sync::remote_object::CandidateObjectRecord {
7766                            state:
7767                                crate::sync::remote_object::CandidateObjectState::AbsentVerified { .. },
7768                            ..
7769                        }
7770                    ) | RemoteObjectRecord::RetainedAuthority(
7771                        crate::sync::remote_object::RetainedAuthorityRecord {
7772                            state:
7773                                crate::sync::remote_object::RetainedAuthorityObjectState::UncreatedVerified { .. },
7774                            ..
7775                        }
7776                    )
7777                );
7778                if absent {
7779                    tx.execute(
7780                        "DELETE FROM remote_objects WHERE object_id = ?1",
7781                        [object_id.to_string()],
7782                    )
7783                    .map_err(DbError::from)?;
7784                }
7785            }
7786            tx.execute(
7787                "UPDATE store_writes SET prepared = NULL WHERE write_id = ?1",
7788                [write_id.as_str()],
7789            )
7790            .map_err(DbError::from)?;
7791            Self::set_write_status_on(tx, write_id, &status)?;
7792        }
7793        Ok(())
7794    }
7795
7796    #[doc(hidden)]
7797    pub async fn discard_pending_serial_branch(
7798        &self,
7799        branch_id: PendingBranchId,
7800        plan: crate::sync::store_pull::SerialResolutionPlan,
7801    ) -> Result<(), DbError> {
7802        let synced_tables = self.synced_tables().to_vec();
7803        let statuses = self.state.write_statuses.clone();
7804        self.call(move |conn| {
7805            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
7806            let write_ids =
7807                Self::apply_serial_resolution_on(&tx, &synced_tables, &branch_id, plan)?;
7808            let resolution = WriteResolution::Discarded;
7809            Self::resolve_unpublished_writes_on(&tx, &write_ids, &resolution)?;
7810            tx.commit().map_err(DbError::from)?;
7811            let status = WriteStatus::Resolved(resolution);
7812            for write_id in write_ids {
7813                Self::notify_write_status_in(&statuses, &write_id, status.clone());
7814            }
7815            Ok(())
7816        })
7817        .await
7818    }
7819
7820    #[doc(hidden)]
7821    pub async fn replace_pending_serial_branch<R, E, F>(
7822        &self,
7823        branch_id: PendingBranchId,
7824        plan: crate::sync::store_pull::SerialResolutionPlan,
7825        replacement_write_id: WriteId,
7826        f: F,
7827    ) -> Result<WriteReceipt<R>, E>
7828    where
7829        R: Send + 'static,
7830        E: From<DbError> + Send + 'static,
7831        F: FnOnce(&rusqlite::Transaction<'_>) -> Result<R, E> + Send + 'static,
7832    {
7833        let synced_tables = self.synced_tables().to_vec();
7834        let gates = self.gates();
7835        let blob_decls = self.blob_decls();
7836        let statuses = self.state.write_statuses.clone();
7837        let outcome = self
7838            .call(move |conn| {
7839                Ok((|| {
7840                    let tx = conn
7841                        .unchecked_transaction()
7842                        .map_err(DbError::from)
7843                        .map_err(E::from)?;
7844                    let old_write_ids =
7845                        Self::apply_serial_resolution_on(&tx, &synced_tables, &branch_id, plan)
7846                            .map_err(E::from)?;
7847                    let changes_before = tx.total_changes();
7848                    let (value, captured) =
7849                        Self::capture_host_changes_on(&tx, &synced_tables, || f(&tx))?;
7850                    let partitions = Self::partition_captured_write_on(
7851                        &tx,
7852                        &captured,
7853                        &gates,
7854                        WritePolicy::Serial,
7855                        StoreWriteRouting::SerialScoped,
7856                    )
7857                    .map_err(E::from)?;
7858                    let blob_facts =
7859                        Self::capture_partition_blob_facts_on(&tx, &partitions, &blob_decls)
7860                            .map_err(E::from)?;
7861                    let rows_changed = tx.total_changes().saturating_sub(changes_before);
7862                    let inverse_changeset = Self::invert_changeset(&captured).map_err(E::from)?;
7863                    let base = StoreWriteBase::Serial {
7864                        branch_id: PendingBranchId::from_first_write(replacement_write_id.clone()),
7865                        base: Self::latest_position_for_device_on(&tx, SERIAL_STREAM_ID)
7866                            .map_err(E::from)?,
7867                    };
7868                    let status = Self::insert_store_write_on(
7869                        &tx,
7870                        &replacement_write_id,
7871                        &partitions,
7872                        &inverse_changeset,
7873                        &base,
7874                        &blob_facts,
7875                        rows_changed,
7876                    )
7877                    .map_err(E::from)?;
7878                    let pending_branch_id = (!matches!(&status, WriteStatus::LocalOnly))
7879                        .then(|| PendingBranchId::from_first_write(replacement_write_id.clone()));
7880                    let resolution = WriteResolution::Replaced {
7881                        replacement: replacement_write_id.clone(),
7882                    };
7883                    Self::resolve_unpublished_writes_on(&tx, &old_write_ids, &resolution)
7884                        .map_err(E::from)?;
7885                    tx.commit().map_err(DbError::from).map_err(E::from)?;
7886                    let old_status = WriteStatus::Resolved(resolution);
7887                    for write_id in old_write_ids {
7888                        Self::notify_write_status_in(&statuses, &write_id, old_status.clone());
7889                    }
7890                    Ok(WriteReceipt {
7891                        value,
7892                        write_id: replacement_write_id,
7893                        status,
7894                        pending_branch_id,
7895                    })
7896                })())
7897            })
7898            .await
7899            .map_err(E::from)?;
7900        outcome
7901    }
7902
7903    pub(crate) async fn latest_local_store_ack(
7904        &self,
7905    ) -> Result<Option<PublishedStoreAck>, DbError> {
7906        self.call(load_published_store_ack_on).await
7907    }
7908
7909    pub(crate) async fn stage_store_ack(
7910        &self,
7911        ack: StoreAck,
7912        prepared: PreparedExactObject,
7913    ) -> Result<StoreAckRef, DbError> {
7914        self.call(move |conn| {
7915            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
7916            let (root, registration_ref, registration) = local_store_authority_on(&tx)?;
7917            if ack.author_registration != registration_ref {
7918                return Err(DbError::Message(
7919                    "staged Store acknowledgement author differs from local activation".to_string(),
7920                ));
7921            }
7922            let reference = StoreAckRef {
7923                revision: ack.revision,
7924                ack_hash: ack.ack_hash(),
7925                object: prepared.reference().clone(),
7926            };
7927            let verified = StoreAck::parse_at(
7928                &ack.to_bytes(),
7929                root.store_root_hash,
7930                &reference,
7931                &registration,
7932            )
7933            .map_err(|error| {
7934                DbError::Message(format!("verify staged Store acknowledgement: {error}"))
7935            })?;
7936            if verified != ack {
7937                return Err(DbError::Message(
7938                    "staged Store acknowledgement changed during exact verification".to_string(),
7939                ));
7940            }
7941            let previous = load_published_store_ack_on(&tx)?;
7942            let (expected_revision, expected_predecessor, expected_slot) = match &previous {
7943                Some(previous) => (
7944                    previous.reference.revision.checked_add(1).ok_or_else(|| {
7945                        DbError::Message("Store acknowledgement revision overflow".to_string())
7946                    })?,
7947                    Some(previous.reference.clone()),
7948                    previous.successor_slot.clone(),
7949                ),
7950                None => (1, None, store_ack_first_slot(&registration)?.clone()),
7951            };
7952            if ack.revision != expected_revision
7953                || ack.predecessor != expected_predecessor
7954                || prepared.reference().slot() != &expected_slot
7955            {
7956                return Err(DbError::Message(
7957                    "Store acknowledgement does not extend the exact local stream".to_string(),
7958                ));
7959            }
7960            if ack.successor.predecessor
7961                != previous
7962                    .as_ref()
7963                    .map(|value| value.reference.object.clone())
7964            {
7965                return Err(DbError::Message(
7966                    "Store acknowledgement successor names a different predecessor".to_string(),
7967                ));
7968            }
7969            let next_revision = ack.revision.checked_add(1).ok_or_else(|| {
7970                DbError::Message("Store acknowledgement revision overflow".to_string())
7971            })?;
7972            if ack.successor.activation
7973                != StreamActivationId::store_acknowledgements(&root, &registration_ref)
7974                || ack.successor.next_slot.logical_key()
7975                    != format!(
7976                        "{}.json",
7977                        ack_slot_prefix(&registration.device_id.to_string(), next_revision)
7978                    )
7979            {
7980                return Err(DbError::Message(
7981                    "Store acknowledgement successor is outside its activated exact stream"
7982                        .to_string(),
7983                ));
7984            }
7985            let ack_ref = serde_json::to_string(&reference).map_err(|error| {
7986                DbError::Message(format!(
7987                    "serialize exact Store acknowledgement ref: {error}"
7988                ))
7989            })?;
7990            let prepared = serde_json::to_string(&prepared).map_err(|error| {
7991                DbError::Message(format!("serialize prepared Store acknowledgement: {error}"))
7992            })?;
7993            tx.execute(
7994                "INSERT INTO outbound_store_acks \
7995                 (singleton, ack_ref, ack_bytes, prepared_object) \
7996                 VALUES (1, ?1, ?2, ?3)",
7997                rusqlite::params![ack_ref, ack.to_bytes(), prepared],
7998            )
7999            .map_err(DbError::from)?;
8000            tx.commit().map_err(DbError::from)?;
8001            Ok(reference)
8002        })
8003        .await
8004    }
8005
8006    pub(crate) async fn oldest_outbound_store_ack(
8007        &self,
8008    ) -> Result<Option<OutboundStoreAck>, DbError> {
8009        self.call(load_outbound_store_ack_on).await
8010    }
8011
8012    pub(crate) async fn complete_outbound_store_ack(
8013        &self,
8014        accepted: StoreAckRef,
8015    ) -> Result<(), DbError> {
8016        self.call(move |conn| {
8017            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
8018            let outbound = load_outbound_store_ack_on(&tx)?.ok_or_else(|| {
8019                DbError::Message("outbound Store acknowledgement is absent".to_string())
8020            })?;
8021            if outbound.reference != accepted {
8022                return Err(DbError::Message(
8023                    "accepted Store acknowledgement differs from the prepared exact object"
8024                        .to_string(),
8025                ));
8026            }
8027            let deleted = tx
8028                .execute(
8029                    "DELETE FROM outbound_store_acks WHERE singleton = 1 AND ack_ref = ?1",
8030                    [serde_json::to_string(&accepted).map_err(|error| {
8031                        DbError::Message(format!(
8032                            "serialize accepted Store acknowledgement ref: {error}"
8033                        ))
8034                    })?],
8035                )
8036                .map_err(DbError::from)?;
8037            if deleted != 1 {
8038                return Err(DbError::Message(
8039                    "outbound Store acknowledgement disappeared".to_string(),
8040                ));
8041            }
8042            let successor_slot = serde_json::to_string(&outbound.ack.value.successor.next_slot)
8043                .map_err(|error| {
8044                    DbError::Message(format!(
8045                        "serialize Store acknowledgement successor slot: {error}"
8046                    ))
8047                })?;
8048            tx.execute(
8049                "INSERT INTO published_store_acks (singleton, ack_ref, successor_slot) \
8050                 VALUES (1, ?1, ?2) \
8051                 ON CONFLICT(singleton) DO UPDATE SET \
8052                   ack_ref = excluded.ack_ref, successor_slot = excluded.successor_slot",
8053                (
8054                    serde_json::to_string(&accepted).map_err(|error| {
8055                        DbError::Message(format!(
8056                            "serialize published Store acknowledgement ref: {error}"
8057                        ))
8058                    })?,
8059                    successor_slot,
8060                ),
8061            )
8062            .map_err(DbError::from)?;
8063            tx.commit().map_err(DbError::from)
8064        })
8065        .await
8066    }
8067
8068    pub(crate) async fn outbound_membership_mutation(
8069        &self,
8070    ) -> Result<Option<DurableMembershipMutation>, DbError> {
8071        self.call(|conn| {
8072            conn.query_row(
8073                "SELECT intent_hash, plan_bytes, progress_bytes \
8074                 FROM outbound_membership_mutation WHERE singleton = 1",
8075                [],
8076                |row| {
8077                    Ok((
8078                        row.get::<_, String>(0)?,
8079                        row.get::<_, Vec<u8>>(1)?,
8080                        row.get::<_, Vec<u8>>(2)?,
8081                    ))
8082                },
8083            )
8084            .optional()
8085            .map_err(DbError::from)?
8086            .map(|(hash, plan_bytes, progress_bytes)| {
8087                let intent_hash: ObjectHash = hash.parse().map_err(|error| {
8088                    DbError::Message(format!("membership intent hash: {error}"))
8089                })?;
8090                if ObjectHash::digest(&plan_bytes) != intent_hash {
8091                    return Err(DbError::Message(
8092                        "membership intent hash differs from its exact plan bytes".to_string(),
8093                    ));
8094                }
8095                Ok(DurableMembershipMutation {
8096                    intent_hash,
8097                    plan_bytes,
8098                    progress_bytes,
8099                })
8100            })
8101            .transpose()
8102        })
8103        .await
8104    }
8105
8106    pub(crate) async fn select_membership_author_stream(
8107        &self,
8108        author_pubkey: &str,
8109        author_owner_grant: &crate::sync::membership::MembershipGrantId,
8110        reusable: std::collections::BTreeSet<crate::sync::membership::AuthorStreamId>,
8111    ) -> Result<crate::sync::membership::AuthorStreamId, DbError> {
8112        self.select_causal_author_stream(
8113            format!("membership_author_stream/{author_pubkey}/{author_owner_grant}"),
8114            reusable,
8115        )
8116        .await
8117    }
8118
8119    pub(crate) async fn select_causal_author_stream(
8120        &self,
8121        key: String,
8122        reusable: std::collections::BTreeSet<crate::sync::membership::AuthorStreamId>,
8123    ) -> Result<crate::sync::membership::AuthorStreamId, DbError> {
8124        let candidate = crate::sync::membership::AuthorStreamId::generate(self.id_provider());
8125        self.call(move |conn| {
8126            let existing = conn
8127                .query_row(
8128                    "SELECT value FROM protocol_state WHERE key = ?1",
8129                    [&key],
8130                    |row| row.get::<_, String>(0),
8131                )
8132                .optional()
8133                .map_err(DbError::from)?
8134                .map(|value| {
8135                    value.parse().map_err(|error| {
8136                        DbError::Message(format!(
8137                            "membership author stream state is malformed: {error}"
8138                        ))
8139                    })
8140                })
8141                .transpose()?;
8142            if let Some(existing) = existing {
8143                if reusable.contains(&existing) {
8144                    return Ok(existing);
8145                }
8146            }
8147            let selected = reusable.iter().next_back().copied().unwrap_or(candidate);
8148            conn.execute(
8149                "INSERT INTO protocol_state (key, value) VALUES (?1, ?2) \
8150                 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
8151                rusqlite::params![key, selected.to_string()],
8152            )
8153            .map_err(DbError::from)?;
8154            Ok(selected)
8155        })
8156        .await
8157    }
8158
8159    pub(crate) async fn stage_membership_mutation(
8160        &self,
8161        plan_bytes: Vec<u8>,
8162        progress_bytes: Vec<u8>,
8163    ) -> Result<ObjectHash, DbError> {
8164        self.call(move |conn| {
8165            let intent_hash = ObjectHash::digest(&plan_bytes);
8166            let existing = conn
8167                .query_row(
8168                    "SELECT intent_hash, plan_bytes FROM outbound_membership_mutation \
8169                     WHERE singleton = 1",
8170                    [],
8171                    |row| Ok((row.get::<_, String>(0)?, row.get::<_, Vec<u8>>(1)?)),
8172                )
8173                .optional()
8174                .map_err(DbError::from)?;
8175            if let Some((existing_hash, existing_plan)) = existing {
8176                if existing_hash == intent_hash.to_string() && existing_plan == plan_bytes {
8177                    return Ok(intent_hash);
8178                }
8179                return Err(DbError::Message(
8180                    "a different membership mutation is already pending".to_string(),
8181                ));
8182            }
8183            conn.execute(
8184                "INSERT INTO outbound_membership_mutation \
8185                 (singleton, intent_hash, plan_bytes, progress_bytes) \
8186                 VALUES (1, ?1, ?2, ?3)",
8187                rusqlite::params![intent_hash.to_string(), plan_bytes, progress_bytes],
8188            )
8189            .map_err(DbError::from)?;
8190            Ok(intent_hash)
8191        })
8192        .await
8193    }
8194
8195    pub(crate) async fn update_membership_mutation_progress(
8196        &self,
8197        intent_hash: ObjectHash,
8198        progress_bytes: Vec<u8>,
8199    ) -> Result<(), DbError> {
8200        self.call(move |conn| {
8201            let updated = conn
8202                .execute(
8203                    "UPDATE outbound_membership_mutation SET progress_bytes = ?1 \
8204                     WHERE singleton = 1 AND intent_hash = ?2",
8205                    rusqlite::params![progress_bytes, intent_hash.to_string()],
8206                )
8207                .map_err(DbError::from)?;
8208            if updated != 1 {
8209                return Err(DbError::Message(
8210                    "membership mutation ownership row is absent or changed".to_string(),
8211                ));
8212            }
8213            Ok(())
8214        })
8215        .await
8216    }
8217
8218    pub(crate) async fn complete_membership_mutation(
8219        &self,
8220        intent_hash: ObjectHash,
8221    ) -> Result<(), DbError> {
8222        self.call(move |conn| {
8223            let deleted = conn
8224                .execute(
8225                    "DELETE FROM outbound_membership_mutation \
8226                     WHERE singleton = 1 AND intent_hash = ?1",
8227                    [intent_hash.to_string()],
8228                )
8229                .map_err(DbError::from)?;
8230            if deleted != 1 {
8231                return Err(DbError::Message(
8232                    "membership mutation ownership row is absent or changed".to_string(),
8233                ));
8234            }
8235            Ok(())
8236        })
8237        .await
8238    }
8239
8240    pub(crate) async fn outbound_snapshot_publication(
8241        &self,
8242    ) -> Result<Option<DurableSnapshotPublication>, DbError> {
8243        let pending = self.call(load_outbound_store_snapshot_on).await?;
8244        if let Some(pending) = &pending {
8245            for blob in &pending.blobs {
8246                if let Some(spool_path) = &blob.spool_path {
8247                    crate::local_blob::verify_exact_file(blob.remote.object(), spool_path)
8248                        .await
8249                        .map_err(|error| {
8250                            DbError::Message(format!("prepared snapshot blob spool: {error}"))
8251                        })?;
8252                }
8253            }
8254        }
8255        Ok(pending)
8256    }
8257
8258    pub(crate) async fn stage_snapshot_publication(
8259        &self,
8260        meta: SnapshotMeta,
8261        meta_prepared: PreparedExactObject,
8262        image_bytes: Vec<u8>,
8263        image_prepared: PreparedExactObject,
8264        blobs: Vec<PreparedSnapshotBlob>,
8265    ) -> Result<StoreSnapshotRef, DbError> {
8266        let synced_tables = self.synced_tables().to_vec();
8267        let gates = self.gates();
8268        self.call(move |conn| {
8269            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
8270            let (root, registration_ref, registration) = local_store_authority_on(&tx)?;
8271            if meta.author_registration != registration_ref {
8272                return Err(DbError::Message(
8273                    "staged Store snapshot author differs from local activation".to_string(),
8274                ));
8275            }
8276            if meta.image.object != *image_prepared.reference()
8277                || ObjectHash::digest(&image_bytes) != meta.image.image_hash
8278                || meta.image.object.slot().logical_key()
8279                    != format!(
8280                        "{}.db",
8281                        snapshot_image_semantic_prefix(
8282                            &registration.device_id.to_string(),
8283                            meta.image.image_hash,
8284                        )
8285                    )
8286            {
8287                return Err(DbError::Message(
8288                    "staged Store snapshot image differs from its exact reference".to_string(),
8289                ));
8290            }
8291            let reference = StoreSnapshotRef {
8292                sequence: meta.sequence,
8293                snapshot_hash: meta.snapshot_hash(),
8294                object: meta_prepared.reference().clone(),
8295            };
8296            let verified = SnapshotMeta::parse_at(
8297                &meta.to_bytes(),
8298                root.store_root_hash,
8299                &reference,
8300                &registration,
8301            )
8302            .map_err(|error| {
8303                DbError::Message(format!("verify staged Store snapshot metadata: {error}"))
8304            })?;
8305            if verified != meta {
8306                return Err(DbError::Message(
8307                    "staged Store snapshot changed during exact verification".to_string(),
8308                ));
8309            }
8310            let previous = load_published_store_snapshot_on(&tx)?;
8311            let (expected_sequence, expected_predecessor, expected_slot) = match &previous {
8312                Some(previous) => (
8313                    previous.reference.sequence.checked_add(1).ok_or_else(|| {
8314                        DbError::Message("Store snapshot sequence overflow".to_string())
8315                    })?,
8316                    Some(previous.reference.clone()),
8317                    previous.successor_slot.clone(),
8318                ),
8319                None => (1, None, store_snapshot_first_slot(&registration)?.clone()),
8320            };
8321            if meta.sequence != expected_sequence
8322                || meta.predecessor != expected_predecessor
8323                || meta_prepared.reference().slot() != &expected_slot
8324                || meta.successor.predecessor
8325                    != previous
8326                        .as_ref()
8327                        .map(|value| value.reference.object.clone())
8328            {
8329                return Err(DbError::Message(
8330                    "Store snapshot does not extend the exact local stream".to_string(),
8331                ));
8332            }
8333            let next_sequence = meta
8334                .sequence
8335                .checked_add(1)
8336                .ok_or_else(|| DbError::Message("Store snapshot sequence overflow".to_string()))?;
8337            if meta.successor.activation
8338                != StreamActivationId::store_snapshots(&root, &registration_ref)
8339                || meta.successor.next_slot.logical_key()
8340                    != format!(
8341                        "{}.json",
8342                        snapshot_slot_prefix(&registration.device_id.to_string(), next_sequence)
8343                    )
8344            {
8345                return Err(DbError::Message(
8346                    "Store snapshot successor is outside its activated exact stream".to_string(),
8347                ));
8348            }
8349            for blob in &blobs {
8350                validate_snapshot_blob_plan_on(conn, &gates, &synced_tables, &meta, blob)?;
8351            }
8352            tx.execute(
8353                "INSERT INTO outbound_store_snapshot \
8354                 (singleton, snapshot_ref, meta_prepared, image_ref, image_prepared, \
8355                  image_bytes, meta_bytes, blobs) \
8356                 VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7)",
8357                rusqlite::params![
8358                    serde_json::to_string(&reference).map_err(|error| DbError::Message(
8359                        format!("serialize exact Store snapshot ref: {error}")
8360                    ))?,
8361                    serde_json::to_string(&meta_prepared).map_err(|error| DbError::Message(
8362                        format!("serialize prepared Store snapshot metadata: {error}")
8363                    ))?,
8364                    serde_json::to_string(&meta.image).map_err(|error| DbError::Message(
8365                        format!("serialize exact Store snapshot image ref: {error}")
8366                    ))?,
8367                    serde_json::to_string(&image_prepared).map_err(|error| DbError::Message(
8368                        format!("serialize prepared Store snapshot image: {error}")
8369                    ))?,
8370                    image_bytes,
8371                    meta.to_bytes(),
8372                    serde_json::to_string(&blobs).map_err(|error| DbError::Message(format!(
8373                        "serialize prepared Store snapshot blobs: {error}"
8374                    )))?,
8375                ],
8376            )
8377            .map_err(DbError::from)?;
8378            tx.commit().map_err(DbError::from)?;
8379            Ok(reference)
8380        })
8381        .await
8382    }
8383
8384    pub(crate) async fn latest_local_store_snapshot(
8385        &self,
8386    ) -> Result<Option<PublishedStoreSnapshot>, DbError> {
8387        self.call(load_published_store_snapshot_on).await
8388    }
8389
8390    pub(crate) async fn complete_snapshot_publication(
8391        &self,
8392        accepted: StoreSnapshotRef,
8393    ) -> Result<(), DbError> {
8394        self.call(move |conn| {
8395            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
8396            let outbound = load_outbound_store_snapshot_on(&tx)?
8397                .ok_or_else(|| DbError::Message("outbound Store snapshot is absent".to_string()))?;
8398            if outbound.reference != accepted {
8399                return Err(DbError::Message(
8400                    "accepted Store snapshot differs from the prepared exact object".to_string(),
8401                ));
8402            }
8403            for blob in &outbound.blobs {
8404                install_snapshot_blob_plan_on(&tx, blob)?;
8405                if let Some(path) = &blob.spool_path {
8406                    tx.execute(
8407                        "INSERT INTO snapshot_blob_spool_cleanup (path) VALUES (?1)
8408                         ON CONFLICT(path) DO NOTHING",
8409                        [path.to_string_lossy().as_ref()],
8410                    )
8411                    .map_err(DbError::from)?;
8412                }
8413            }
8414            let deleted = tx
8415                .execute(
8416                    "DELETE FROM outbound_store_snapshot \
8417                     WHERE singleton = 1 AND snapshot_ref = ?1",
8418                    [serde_json::to_string(&accepted).map_err(|error| {
8419                        DbError::Message(format!("serialize accepted Store snapshot ref: {error}"))
8420                    })?],
8421                )
8422                .map_err(DbError::from)?;
8423            if deleted != 1 {
8424                return Err(DbError::Message(
8425                    "outbound snapshot ownership row is absent or changed".to_string(),
8426                ));
8427            }
8428            tx.execute(
8429                "INSERT INTO published_store_snapshot \
8430                 (singleton, snapshot_ref, successor_slot, meta_bytes) VALUES (1, ?1, ?2, ?3) \
8431                 ON CONFLICT(singleton) DO UPDATE SET \
8432                   snapshot_ref = excluded.snapshot_ref, \
8433                   successor_slot = excluded.successor_slot, \
8434                   meta_bytes = excluded.meta_bytes",
8435                rusqlite::params![
8436                    serde_json::to_string(&accepted).map_err(|error| DbError::Message(format!(
8437                        "serialize published Store snapshot ref: {error}"
8438                    )))?,
8439                    serde_json::to_string(&outbound.meta.value.successor.next_slot).map_err(
8440                        |error| DbError::Message(format!(
8441                            "serialize Store snapshot successor slot: {error}"
8442                        ))
8443                    )?,
8444                    outbound.meta.bytes,
8445                ],
8446            )
8447            .map_err(DbError::from)?;
8448            tx.commit().map_err(DbError::from)
8449        })
8450        .await
8451    }
8452
8453    pub(crate) async fn snapshot_blob_spool_cleanup_paths(&self) -> Result<Vec<PathBuf>, DbError> {
8454        self.call(|conn| {
8455            let mut statement = conn
8456                .prepare("SELECT path FROM snapshot_blob_spool_cleanup ORDER BY path")
8457                .map_err(DbError::from)?;
8458            let paths = statement
8459                .query_map([], |row| row.get::<_, String>(0))
8460                .map_err(DbError::from)?
8461                .map(|row| row.map(PathBuf::from).map_err(DbError::from))
8462                .collect();
8463            paths
8464        })
8465        .await
8466    }
8467
8468    pub(crate) async fn complete_snapshot_blob_spool_cleanup(
8469        &self,
8470        path: &Path,
8471    ) -> Result<(), DbError> {
8472        let path = path.to_string_lossy().into_owned();
8473        self.call(move |conn| {
8474            let deleted = conn
8475                .execute(
8476                    "DELETE FROM snapshot_blob_spool_cleanup WHERE path = ?1",
8477                    [&path],
8478                )
8479                .map_err(DbError::from)?;
8480            if deleted != 1 {
8481                return Err(DbError::Message(
8482                    "snapshot blob spool cleanup ownership is absent".to_string(),
8483                ));
8484            }
8485            Ok(())
8486        })
8487        .await
8488    }
8489
8490    pub async fn local_store_root_ref(
8491        &self,
8492    ) -> Result<Option<crate::sync::store_commit::StoreRootRef>, DbError> {
8493        self.call(|conn| {
8494            load_store_root_authority_on(conn)
8495                .map(|authority| authority.map(|(reference, _)| reference))
8496        })
8497        .await
8498    }
8499
8500    pub(crate) async fn install_store_root_authority(
8501        &self,
8502        reference: crate::sync::store_commit::StoreRootRef,
8503        bytes: Vec<u8>,
8504    ) -> Result<(), DbError> {
8505        self.call(move |conn| {
8506            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
8507            install_store_root_authority_on(&tx, &reference, &bytes)?;
8508            tx.commit().map_err(DbError::from)
8509        })
8510        .await
8511    }
8512
8513    pub(crate) async fn install_store_owner_anchor(
8514        &self,
8515        root: crate::sync::store_commit::StoreRootRef,
8516        founder_reference: StoreDeviceRegistrationRef,
8517        founder: StoreDeviceRegistration,
8518        founder_bytes: Vec<u8>,
8519        genesis: ResolvedStoreDeviceState,
8520        owner: String,
8521        head_refs: Vec<crate::sync::membership::MembershipHeadRef>,
8522    ) -> Result<(), DbError> {
8523        if founder.author_pubkey != owner {
8524            return Err(DbError::Message(
8525                "Store founder registration author differs from the owner anchor".to_string(),
8526            ));
8527        }
8528        self.call(move |conn| {
8529            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
8530            install_store_founder_state_on(
8531                &tx,
8532                &root,
8533                &founder_reference,
8534                &founder,
8535                &founder_bytes,
8536                &genesis,
8537            )?;
8538            tx.execute(
8539                "INSERT INTO protocol_state (key, value) VALUES (?1, ?2) \
8540                 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
8541                rusqlite::params![crate::sync::membership_ops::OWNER_PUBKEY_STATE_KEY, owner,],
8542            )
8543            .map_err(DbError::from)?;
8544            for reference in &head_refs {
8545                crate::sync::membership_ops::upsert_head_cursor_on(&tx, reference)?;
8546            }
8547            tx.commit().map_err(DbError::from)
8548        })
8549        .await
8550    }
8551
8552    pub(crate) async fn local_store_founder_graph(
8553        &self,
8554    ) -> Result<Option<DurableFounderGraph>, DbError> {
8555        self.call(load_local_store_founder_graph_on).await
8556    }
8557
8558    pub(crate) async fn stage_store_founder_graph(
8559        &self,
8560        graph: DurableFounderGraph,
8561    ) -> Result<(), DbError> {
8562        validate_founder_graph(&graph)?;
8563        self.call(move |conn| {
8564            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
8565            if let Some(existing) = load_local_store_founder_graph_on(&tx)? {
8566                validate_founder_graph(&existing)?;
8567                if founder_graph_identity(&existing) == founder_graph_identity(&graph) {
8568                    return Ok(());
8569                }
8570                return Err(DbError::Message(
8571                    "local Store founder graph already owns different exact objects".to_string(),
8572                ));
8573            }
8574            consume_store_creation_probes_on(&tx, &graph)?;
8575            tx.execute(
8576                "INSERT INTO local_store_protocol_root \
8577                 (singleton, store_root_hash, store_protocol_root_bytes, prepared_object) \
8578                 VALUES (1, ?1, ?2, ?3)",
8579                rusqlite::params![
8580                    graph.root.value.object_hash().to_string(),
8581                    graph.root.bytes,
8582                    serde_json::to_string(&graph.root.prepared).map_err(|error| {
8583                        DbError::Message(format!("serialize prepared Store root: {error}"))
8584                    })?,
8585                ],
8586            )
8587            .map_err(DbError::from)?;
8588            tx.execute(
8589                "INSERT INTO local_store_device_registration \
8590                 (singleton, device_id, registration_hash, registration_bytes, prepared_object, \
8591                  initial_ack_ref, initial_ack_bytes, initial_ack_prepared, state) \
8592                 VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
8593                rusqlite::params![
8594                    graph.registration.value.device_id.to_string(),
8595                    graph.registration.value.registration_hash().to_string(),
8596                    graph.registration.bytes,
8597                    serde_json::to_string(&graph.registration.prepared).map_err(|error| {
8598                        DbError::Message(format!(
8599                            "serialize prepared founder registration: {error}"
8600                        ))
8601                    })?,
8602                    serde_json::to_string(&graph.initial_ack_ref).map_err(|error| {
8603                        DbError::Message(format!("serialize founder initial ack ref: {error}"))
8604                    })?,
8605                    graph.initial_ack.bytes,
8606                    serde_json::to_string(&graph.initial_ack.prepared).map_err(|error| {
8607                        DbError::Message(format!("serialize founder initial ack object: {error}"))
8608                    })?,
8609                    serde_json::to_string(&LocalDeviceRegistrationState::Prepared).map_err(
8610                        |error| DbError::Message(format!(
8611                            "serialize registration journal state: {error}"
8612                        ))
8613                    )?,
8614                ],
8615            )
8616            .map_err(DbError::from)?;
8617            tx.execute(
8618                "INSERT INTO local_store_founder_graph \
8619                 (singleton, membership_graph) VALUES (1, ?1)",
8620                rusqlite::params![serde_json::to_string(
8621                    &DurableFounderMembershipJournal::from_graph(&graph.membership,)
8622                )
8623                .map_err(|error| DbError::Message(format!(
8624                    "serialize founder membership graph: {error}"
8625                )))?,],
8626            )
8627            .map_err(DbError::from)?;
8628            tx.commit().map_err(DbError::from)
8629        })
8630        .await
8631    }
8632
8633    pub(crate) async fn complete_store_founder_graph(
8634        &self,
8635        expected_root: crate::sync::store_commit::StoreRootRef,
8636        expected_registration: StoreDeviceRegistrationRef,
8637        expected_initial_ack: StoreAckRef,
8638        expected_membership: FounderMembershipRefs,
8639    ) -> Result<(), DbError> {
8640        self.call(move |conn| {
8641            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
8642            let graph = load_local_store_founder_graph_on(&tx)?.ok_or_else(|| {
8643                DbError::Message("local Store founder graph is absent".to_string())
8644            })?;
8645            let root = crate::sync::store_commit::StoreRootRef {
8646                store_root_id: graph.root.value.descriptor.store_root_id(),
8647                store_root_hash: graph.root.value.object_hash(),
8648                object: graph.root.object.clone(),
8649            };
8650            let registration = StoreDeviceRegistrationRef::from_registration(
8651                &graph.registration.value,
8652                graph.registration.object.clone(),
8653            );
8654            if root != expected_root
8655                || registration != expected_registration
8656                || graph.initial_ack_ref != expected_initial_ack
8657                || match (&graph.membership, &expected_membership) {
8658                    (
8659                        DurableFounderMembership::MergeConcurrent {
8660                            entry_ref,
8661                            head_ref,
8662                            ..
8663                        },
8664                        FounderMembershipRefs::MergeConcurrent { entry, head },
8665                    ) => entry_ref != entry || head_ref != head,
8666                    (DurableFounderMembership::Serial, FounderMembershipRefs::Serial) => false,
8667                    _ => true,
8668                }
8669            {
8670                return Err(DbError::Message(
8671                    "verified founder graph differs from its durable exact references".to_string(),
8672                ));
8673            }
8674            let founder_authority =
8675                crate::sync::store_commit::StoreDeviceRegistrationActivation::Founder {
8676                    root: root.clone(),
8677                };
8678            let device_genesis = ResolvedStoreDeviceState::founder(
8679                &root,
8680                registration.clone(),
8681                &graph.root.value.descriptor.founder_pubkey,
8682                graph.root.value.descriptor.founder_grant.clone(),
8683                &graph.root.value.descriptor.founder_recovery,
8684            )
8685            .map_err(|error| DbError::Message(error.to_string()))?;
8686            let device_genesis_json = serde_json::to_string(&device_genesis).map_err(|error| {
8687                DbError::Message(format!("serialize Store device genesis state: {error}"))
8688            })?;
8689            let device_id = registration.device_id.to_string();
8690            let registration_hash = registration.registration_hash.to_string();
8691            match &graph.registration_state {
8692                LocalDeviceRegistrationState::Prepared => {
8693                    return Err(DbError::Message(
8694                        "founder registration and initial acknowledgement are not exact-created"
8695                            .to_string(),
8696                    ));
8697                }
8698                LocalDeviceRegistrationState::Created => {}
8699                LocalDeviceRegistrationState::Activated { authority } => {
8700                    if authority != &founder_authority {
8701                        return Err(DbError::Message(
8702                            "founder registration journal carries another activation authority"
8703                                .to_string(),
8704                        ));
8705                    }
8706                    let installed = load_store_root_authority_on(&tx)?;
8707                    let stored: Option<(String, String, String)> = tx
8708                        .query_row(
8709                            "SELECT registration_object, activation_authority, registration_hash \
8710                             FROM store_device_registration_activations WHERE device_id = ?1",
8711                            [&device_id],
8712                            |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
8713                        )
8714                        .optional()
8715                        .map_err(DbError::from)?;
8716                    let ack: Option<String> = tx
8717                        .query_row(
8718                            "SELECT ack_ref FROM published_store_acks WHERE singleton = 1",
8719                            [],
8720                            |row| row.get(0),
8721                        )
8722                        .optional()
8723                        .map_err(DbError::from)?;
8724                    let stored_device_genesis: Option<String> = tx
8725                        .query_row(
8726                            "SELECT value FROM protocol_state WHERE key = ?1",
8727                            [STORE_DEVICE_GENESIS_STATE_KEY],
8728                            |row| row.get(0),
8729                        )
8730                        .optional()
8731                        .map_err(DbError::from)?;
8732                    if installed
8733                        .as_ref()
8734                        .map(|(reference, value)| (reference, value))
8735                        != Some((&root, &graph.root.value))
8736                        || stored
8737                            != Some((
8738                                serde_json::to_string(&registration).map_err(|error| {
8739                                    DbError::Message(format!(
8740                                        "serialize founder registration ref: {error}"
8741                                    ))
8742                                })?,
8743                                serde_json::to_string(&founder_authority).map_err(|error| {
8744                                    DbError::Message(format!(
8745                                        "serialize founder authority: {error}"
8746                                    ))
8747                                })?,
8748                                registration.registration_hash.to_string(),
8749                            ))
8750                        || ack
8751                            != Some(serde_json::to_string(&graph.initial_ack_ref).map_err(
8752                                |error| {
8753                                    DbError::Message(format!("serialize founder ack ref: {error}"))
8754                                },
8755                            )?)
8756                        || stored_device_genesis.as_deref() != Some(&device_genesis_json)
8757                    {
8758                        return Err(DbError::Message(
8759                            "activated founder journal differs from installed exact authority"
8760                                .to_string(),
8761                        ));
8762                    }
8763                    return Ok(());
8764                }
8765                LocalDeviceRegistrationState::Retired { .. } => {
8766                    return Err(DbError::Message(
8767                        "founder graph cannot complete through a retired local registration".into(),
8768                    ));
8769                }
8770            }
8771            install_store_root_authority_on(&tx, &root, &graph.root.bytes)?;
8772            let activation = serde_json::to_string(
8773                &crate::sync::store_commit::StoreDeviceRegistrationActivation::Founder {
8774                    root: root.clone(),
8775                },
8776            )
8777            .map_err(|error| {
8778                DbError::Message(format!(
8779                    "serialize founder registration activation: {error}"
8780                ))
8781            })?;
8782            let journal_state = serde_json::to_string(&LocalDeviceRegistrationState::Activated {
8783                authority: founder_authority,
8784            })
8785            .map_err(|error| {
8786                DbError::Message(format!("serialize founder registration journal: {error}"))
8787            })?;
8788            let updated = tx
8789                .execute(
8790                    "UPDATE local_store_device_registration SET state = ?1 \
8791                     WHERE singleton = 1 AND device_id = ?2 AND registration_hash = ?3 \
8792                       AND initial_ack_ref = ?4 AND state = ?5",
8793                    rusqlite::params![
8794                        journal_state,
8795                        &device_id,
8796                        &registration_hash,
8797                        serde_json::to_string(&graph.initial_ack_ref).map_err(|error| {
8798                            DbError::Message(format!("serialize founder initial ack ref: {error}"))
8799                        })?,
8800                        serde_json::to_string(&LocalDeviceRegistrationState::Created).map_err(
8801                            |error| DbError::Message(format!(
8802                                "serialize created journal state: {error}"
8803                            ))
8804                        )?,
8805                    ],
8806                )
8807                .map_err(DbError::from)?;
8808            if updated != 1 {
8809                return Err(DbError::Message(
8810                    "founder registration journal did not activate".to_string(),
8811                ));
8812            }
8813            tx.execute(
8814                "INSERT INTO store_device_registration_activations \
8815                 (device_id, registration_hash, author_pubkey, device_signing_pubkey, \
8816                  registration_bytes, registration_object, activation_authority) \
8817                 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
8818                rusqlite::params![
8819                    &device_id,
8820                    &registration_hash,
8821                    graph.registration.value.author_pubkey,
8822                    graph.registration.value.device_signing_pubkey,
8823                    graph.registration.bytes,
8824                    serde_json::to_string(&registration).map_err(|error| {
8825                        DbError::Message(format!("serialize founder registration ref: {error}"))
8826                    })?,
8827                    activation,
8828                ],
8829            )
8830            .map_err(DbError::from)?;
8831            tx.execute(
8832                "INSERT INTO published_store_acks \
8833                 (singleton, ack_ref, successor_slot) VALUES (1, ?1, ?2)",
8834                rusqlite::params![
8835                    serde_json::to_string(&graph.initial_ack_ref).map_err(|error| {
8836                        DbError::Message(format!("serialize founder initial ack ref: {error}"))
8837                    })?,
8838                    serde_json::to_string(&graph.initial_ack.value.successor.next_slot).map_err(
8839                        |error| DbError::Message(format!(
8840                            "serialize founder ack successor: {error}"
8841                        ))
8842                    )?,
8843                ],
8844            )
8845            .map_err(DbError::from)?;
8846            for (key, value) in [
8847                (LOCAL_DEVICE_ID_STATE_KEY, device_id),
8848                (STORE_DEVICE_GENESIS_STATE_KEY, device_genesis_json),
8849            ] {
8850                tx.execute(
8851                    "INSERT INTO protocol_state (key, value) VALUES (?1, ?2) \
8852                     ON CONFLICT(key) DO UPDATE SET value = excluded.value",
8853                    (key, value),
8854                )
8855                .map_err(DbError::from)?;
8856            }
8857            match &graph.membership {
8858                DurableFounderMembership::MergeConcurrent { head_ref, .. } => {
8859                    tx.execute(
8860                        "INSERT INTO protocol_state (key, value) VALUES (?1, ?2) \
8861                         ON CONFLICT(key) DO UPDATE SET value = excluded.value",
8862                        (
8863                            crate::sync::membership_ops::OWNER_PUBKEY_STATE_KEY,
8864                            &graph.root.value.descriptor.founder_pubkey,
8865                        ),
8866                    )
8867                    .map_err(DbError::from)?;
8868                    crate::sync::membership_ops::upsert_head_cursor_on(&tx, head_ref)?;
8869                }
8870                DurableFounderMembership::Serial => {
8871                    let authorization = SerialAuthorizationState::from_founder(
8872                        &root,
8873                        &graph.root.value,
8874                        &registration,
8875                        &graph.registration.value,
8876                    )
8877                    .map_err(|error| DbError::Message(error.to_string()))?;
8878                    Self::install_serial_root_authorization_on(
8879                        &tx,
8880                        &graph.root.value.descriptor.founder_pubkey,
8881                        &authorization,
8882                    )?;
8883                }
8884            }
8885            tx.commit().map_err(DbError::from)
8886        })
8887        .await
8888    }
8889
8890    pub(crate) async fn latest_local_store_device_registration(
8891        &self,
8892    ) -> Result<Option<DurableDeviceRegistration>, DbError> {
8893        self.read_local_store_device_registration(
8894            "SELECT device_id, registration_hash, registration_bytes, prepared_object, \
8895                    initial_ack_ref, initial_ack_bytes, initial_ack_prepared, state \
8896             FROM local_store_device_registration WHERE singleton = 1",
8897        )
8898        .await
8899    }
8900
8901    pub async fn export_activated_device_continuation(
8902        &self,
8903        identity_signer: &crate::keys::UserKeypair,
8904    ) -> Result<crate::sync::restore_code::ActivatedContinuation, DbError> {
8905        let durable = self
8906            .latest_local_store_device_registration()
8907            .await?
8908            .ok_or_else(|| DbError::Message("local Store device registration is absent".into()))?;
8909        let LocalDeviceRegistrationState::Activated { authority } = durable.state else {
8910            return Err(DbError::Message(
8911                "local Store device registration is not activated".into(),
8912            ));
8913        };
8914        let root = self
8915            .local_store_root_ref()
8916            .await?
8917            .ok_or_else(DbError::missing_store_root_hash)?;
8918        let registration = StoreDeviceRegistration::parse_at(
8919            &durable.registration_bytes,
8920            &root,
8921            durable.device_id,
8922        )
8923        .map_err(|error| DbError::Message(format!("local Store registration: {error}")))?;
8924        let registration_ref = StoreDeviceRegistrationRef::from_registration(
8925            &registration,
8926            durable.prepared.reference().clone(),
8927        );
8928        if registration_ref.registration_hash != durable.registration_hash {
8929            return Err(DbError::Message(
8930                "local Store registration hash differs from its exact object".into(),
8931            ));
8932        }
8933        let device_signer = registration
8934            .device_signer(identity_signer)
8935            .map_err(|error| DbError::Message(format!("local device signer: {error}")))?;
8936        let latest_ack = self
8937            .latest_local_store_ack()
8938            .await?
8939            .ok_or_else(|| DbError::Message("local Store acknowledgement is absent".into()))?;
8940        Ok(crate::sync::restore_code::ActivatedContinuation {
8941            identity_signing_secret: hex::encode(identity_signer.to_keypair_bytes()),
8942            device_signing_secret: hex::encode(device_signer.to_keypair_bytes()),
8943            registration: registration_ref,
8944            registration_bytes: durable.registration_bytes,
8945            registration_prepared: durable.prepared,
8946            initial_ack: durable.initial_ack_ref,
8947            initial_ack_bytes: durable.initial_ack.bytes,
8948            initial_ack_prepared: durable.initial_ack.prepared,
8949            activation: authority,
8950            latest_ack: latest_ack.reference,
8951            latest_snapshot: self
8952                .latest_local_store_snapshot()
8953                .await?
8954                .map(|snapshot| snapshot.reference),
8955            latest_position: self.latest_local_store_position().await?,
8956        })
8957    }
8958
8959    pub async fn install_activated_device_continuation(
8960        &self,
8961        continuation: crate::sync::restore_code::ActivatedContinuation,
8962        identity_signer: &crate::keys::UserKeypair,
8963        device_signer: &crate::keys::UserKeypair,
8964        ack_chain: Vec<(StoreAckRef, StoreAck)>,
8965        latest_snapshot: Option<(StoreSnapshotRef, SnapshotMeta)>,
8966    ) -> Result<(), DbError> {
8967        let root = self
8968            .local_store_root_ref()
8969            .await?
8970            .ok_or_else(DbError::missing_store_root_hash)?;
8971        let registration = StoreDeviceRegistration::parse_at(
8972            &continuation.registration_bytes,
8973            &root,
8974            continuation.registration.device_id,
8975        )
8976        .map_err(|error| DbError::Message(format!("continued Store registration: {error}")))?;
8977        continuation
8978            .registration
8979            .verify_registration(&registration)
8980            .map_err(|error| DbError::Message(error.to_string()))?;
8981        let derived_device = registration
8982            .device_signer(identity_signer)
8983            .map_err(|error| DbError::Message(format!("continued device signer: {error}")))?;
8984        if derived_device.to_keypair_bytes() != device_signer.to_keypair_bytes()
8985            || continuation.registration_prepared.reference() != &continuation.registration.object
8986            || continuation.initial_ack_prepared.reference() != &continuation.initial_ack.object
8987        {
8988            return Err(DbError::Message(
8989                "continued device keys or exact registration objects differ".into(),
8990            ));
8991        }
8992        let initial_ack = StoreAck::parse_at(
8993            &continuation.initial_ack_bytes,
8994            root.store_root_hash,
8995            &continuation.initial_ack,
8996            &registration,
8997        )
8998        .map_err(|error| DbError::Message(format!("continued initial ack: {error}")))?;
8999        let Some((latest_ack_ref, latest_ack)) = ack_chain.first() else {
9000            return Err(DbError::Message(
9001                "continued acknowledgement chain is empty".into(),
9002            ));
9003        };
9004        if initial_ack.revision != 1
9005            || initial_ack.predecessor.is_some()
9006            || latest_ack.author_registration != continuation.registration
9007            || latest_ack_ref != &continuation.latest_ack
9008            || ack_chain.last().map(|(reference, _)| reference) != Some(&continuation.initial_ack)
9009            || ack_chain.windows(2).any(|pair| {
9010                pair[0].1.predecessor.as_ref() != Some(&pair[1].0)
9011                    || pair[0].0.revision != pair[0].1.revision
9012                    || pair[1].0.revision != pair[1].1.revision
9013            })
9014        {
9015            return Err(DbError::Message(
9016                "continued acknowledgement chain differs from its exact authority".into(),
9017            ));
9018        }
9019        let latest_successor_slot = latest_ack.successor.next_slot.clone();
9020        match (&continuation.latest_snapshot, &latest_snapshot) {
9021            (None, None) => {}
9022            (Some(expected), Some((reference, meta))) if expected == reference => {
9023                let verified = crate::sync::store_snapshot::verify_store_snapshot_bytes(
9024                    &root,
9025                    &continuation.registration,
9026                    &registration,
9027                    reference,
9028                    &meta.to_bytes(),
9029                )
9030                .map_err(|error| DbError::Message(error.to_string()))?;
9031                if verified != *meta {
9032                    return Err(DbError::Message(
9033                        "continued snapshot changed during exact verification".into(),
9034                    ));
9035                }
9036            }
9037            _ => {
9038                return Err(DbError::Message(
9039                    "continued snapshot stream differs from its exact authority".into(),
9040                ));
9041            }
9042        }
9043
9044        self.call(move |conn| {
9045            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
9046            let activated = load_activated_registration_on(&tx, &root, &continuation.registration)?;
9047            if activated != registration {
9048                return Err(DbError::Message(
9049                    "continued registration differs from activated Store state".into(),
9050                ));
9051            }
9052            let stored_authority: String = tx
9053                .query_row(
9054                    "SELECT activation_authority FROM store_device_registration_activations \
9055                     WHERE device_id = ?1 AND registration_hash = ?2",
9056                    (
9057                        continuation.registration.device_id.to_string(),
9058                        continuation.registration.registration_hash.to_string(),
9059                    ),
9060                    |row| row.get(0),
9061                )
9062                .map_err(DbError::from)?;
9063            let stored_authority: crate::sync::store_commit::StoreDeviceRegistrationActivation =
9064                serde_json::from_str(&stored_authority).map_err(|error| {
9065                    DbError::Message(format!("continued activation authority: {error}"))
9066                })?;
9067            if stored_authority != continuation.activation {
9068                return Err(DbError::Message(
9069                    "continued registration has another activation authority".into(),
9070                ));
9071            }
9072            if let Some(position) = &continuation.latest_position {
9073                let stream_id = match &position.coord {
9074                    StoreCommitCoord::MergeConcurrent { stream_id, .. } => stream_id.to_string(),
9075                    StoreCommitCoord::Serial { .. } => SERIAL_STREAM_ID.to_string(),
9076                };
9077                let restored_position = Self::latest_position_for_device_on(&tx, &stream_id)?;
9078                if restored_position.as_ref() != Some(position) {
9079                    return Err(DbError::Message(
9080                        "continued device position is absent from restored history".into(),
9081                    ));
9082                }
9083            }
9084            let existing_local: i64 = tx
9085                .query_row(
9086                    "SELECT COUNT(*) FROM local_store_device_registration",
9087                    [],
9088                    |row| row.get(0),
9089                )
9090                .map_err(DbError::from)?;
9091            let existing_ack: i64 = tx
9092                .query_row("SELECT COUNT(*) FROM published_store_acks", [], |row| {
9093                    row.get(0)
9094                })
9095                .map_err(DbError::from)?;
9096            let existing_snapshot: i64 = tx
9097                .query_row("SELECT COUNT(*) FROM published_store_snapshot", [], |row| {
9098                    row.get(0)
9099                })
9100                .map_err(DbError::from)?;
9101            let existing_device: Option<String> = tx
9102                .query_row(
9103                    "SELECT value FROM protocol_state WHERE key = ?1",
9104                    [LOCAL_DEVICE_ID_STATE_KEY],
9105                    |row| row.get(0),
9106                )
9107                .optional()
9108                .map_err(DbError::from)?;
9109            let state = serde_json::to_string(&LocalDeviceRegistrationState::Activated {
9110                authority: continuation.activation.clone(),
9111            })
9112            .map_err(|error| DbError::Message(format!("continued activation: {error}")))?;
9113            let expected_local = (
9114                continuation.registration.device_id.to_string(),
9115                continuation.registration.registration_hash.to_string(),
9116                continuation.registration_bytes.clone(),
9117                serde_json::to_string(&continuation.registration_prepared).map_err(|error| {
9118                    DbError::Message(format!("continued registration object: {error}"))
9119                })?,
9120                serde_json::to_string(&continuation.initial_ack).map_err(|error| {
9121                    DbError::Message(format!("continued initial ack ref: {error}"))
9122                })?,
9123                continuation.initial_ack_bytes.clone(),
9124                serde_json::to_string(&continuation.initial_ack_prepared).map_err(|error| {
9125                    DbError::Message(format!("continued initial ack object: {error}"))
9126                })?,
9127                state,
9128            );
9129            match existing_local {
9130                0 => {
9131                    tx.execute(
9132                        "INSERT INTO local_store_device_registration \
9133                         (singleton, device_id, registration_hash, registration_bytes, \
9134                          prepared_object, initial_ack_ref, initial_ack_bytes, \
9135                          initial_ack_prepared, state) \
9136                         VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
9137                        rusqlite::params![
9138                            expected_local.0,
9139                            expected_local.1,
9140                            expected_local.2,
9141                            expected_local.3,
9142                            expected_local.4,
9143                            expected_local.5,
9144                            expected_local.6,
9145                            expected_local.7,
9146                        ],
9147                    )
9148                    .map_err(DbError::from)?;
9149                }
9150                1 => {
9151                    let actual = tx
9152                        .query_row(
9153                            "SELECT device_id, registration_hash, registration_bytes, \
9154                             prepared_object, initial_ack_ref, initial_ack_bytes, \
9155                             initial_ack_prepared, state FROM local_store_device_registration \
9156                             WHERE singleton = 1",
9157                            [],
9158                            |row| {
9159                                Ok((
9160                                    row.get::<_, String>(0)?,
9161                                    row.get::<_, String>(1)?,
9162                                    row.get::<_, Vec<u8>>(2)?,
9163                                    row.get::<_, String>(3)?,
9164                                    row.get::<_, String>(4)?,
9165                                    row.get::<_, Vec<u8>>(5)?,
9166                                    row.get::<_, String>(6)?,
9167                                    row.get::<_, String>(7)?,
9168                                ))
9169                            },
9170                        )
9171                        .map_err(DbError::from)?;
9172                    if actual != expected_local {
9173                        return Err(DbError::Message(
9174                            "restored local device state differs from continuation".into(),
9175                        ));
9176                    }
9177                }
9178                _ => {
9179                    return Err(DbError::Message(
9180                        "restored database carries multiple local device journals".into(),
9181                    ));
9182                }
9183            }
9184            match existing_ack {
9185                0 => {}
9186                1 => {
9187                    let (stored_ref, stored_successor): (String, String) = tx
9188                        .query_row(
9189                            "SELECT ack_ref, successor_slot FROM published_store_acks \
9190                             WHERE singleton = 1",
9191                            [],
9192                            |row| Ok((row.get(0)?, row.get(1)?)),
9193                        )
9194                        .map_err(DbError::from)?;
9195                    let stored_ref: StoreAckRef =
9196                        serde_json::from_str(&stored_ref).map_err(|error| {
9197                            DbError::Message(format!("restored acknowledgement: {error}"))
9198                        })?;
9199                    let Some((_, stored_ack)) = ack_chain
9200                        .iter()
9201                        .find(|(reference, _)| reference == &stored_ref)
9202                    else {
9203                        return Err(DbError::Message(
9204                            "restored acknowledgement is outside the continuation chain".into(),
9205                        ));
9206                    };
9207                    if stored_successor
9208                        != serde_json::to_string(&stored_ack.successor.next_slot).map_err(
9209                            |error| DbError::Message(format!("restored ack successor: {error}")),
9210                        )?
9211                    {
9212                        return Err(DbError::Message(
9213                            "restored acknowledgement successor differs from its signature".into(),
9214                        ));
9215                    }
9216                }
9217                _ => {
9218                    return Err(DbError::Message(
9219                        "restored database carries multiple local acknowledgements".into(),
9220                    ));
9221                }
9222            }
9223            match (existing_snapshot, latest_snapshot.as_ref()) {
9224                (0, None) => {}
9225                (0, Some((reference, meta))) => {
9226                    tx.execute(
9227                        "INSERT INTO published_store_snapshot \
9228                         (singleton, snapshot_ref, successor_slot, meta_bytes) \
9229                         VALUES (1, ?1, ?2, ?3)",
9230                        rusqlite::params![
9231                            serde_json::to_string(reference).map_err(|error| {
9232                                DbError::Message(format!(
9233                                    "serialize continued Store snapshot ref: {error}"
9234                                ))
9235                            })?,
9236                            serde_json::to_string(&meta.successor.next_slot).map_err(|error| {
9237                                DbError::Message(format!(
9238                                    "serialize continued Store snapshot successor: {error}"
9239                                ))
9240                            })?,
9241                            meta.to_bytes(),
9242                        ],
9243                    )
9244                    .map_err(DbError::from)?;
9245                }
9246                (1, Some((reference, meta))) => {
9247                    let actual: (String, String, Vec<u8>) = tx
9248                        .query_row(
9249                            "SELECT snapshot_ref, successor_slot, meta_bytes \
9250                             FROM published_store_snapshot WHERE singleton = 1",
9251                            [],
9252                            |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
9253                        )
9254                        .map_err(DbError::from)?;
9255                    let expected = (
9256                        serde_json::to_string(reference).map_err(|error| {
9257                            DbError::Message(format!(
9258                                "serialize continued Store snapshot ref: {error}"
9259                            ))
9260                        })?,
9261                        serde_json::to_string(&meta.successor.next_slot).map_err(|error| {
9262                            DbError::Message(format!(
9263                                "serialize continued Store snapshot successor: {error}"
9264                            ))
9265                        })?,
9266                        meta.to_bytes(),
9267                    );
9268                    if actual != expected {
9269                        return Err(DbError::Message(
9270                            "restored local snapshot stream differs from continuation".into(),
9271                        ));
9272                    }
9273                }
9274                (1, None) => {
9275                    return Err(DbError::Message(
9276                        "restored database carries a snapshot outside the continuation".into(),
9277                    ));
9278                }
9279                _ => {
9280                    return Err(DbError::Message(
9281                        "restored database carries multiple local snapshots".into(),
9282                    ));
9283                }
9284            }
9285            let latest_ref = serde_json::to_string(&continuation.latest_ack)
9286                .map_err(|error| DbError::Message(format!("continued latest ack: {error}")))?;
9287            let latest_successor = serde_json::to_string(&latest_successor_slot)
9288                .map_err(|error| DbError::Message(format!("continued ack successor: {error}")))?;
9289            if existing_ack == 0 {
9290                tx.execute(
9291                    "INSERT INTO published_store_acks (singleton, ack_ref, successor_slot) \
9292                     VALUES (1, ?1, ?2)",
9293                    (&latest_ref, &latest_successor),
9294                )
9295                .map_err(DbError::from)?;
9296            } else {
9297                tx.execute(
9298                    "UPDATE published_store_acks SET ack_ref = ?1, successor_slot = ?2 \
9299                     WHERE singleton = 1",
9300                    (&latest_ref, &latest_successor),
9301                )
9302                .map_err(DbError::from)?;
9303            }
9304            match existing_device {
9305                Some(existing) if existing == continuation.registration.device_id.to_string() => {}
9306                Some(_) => {
9307                    return Err(DbError::Message(
9308                        "restored local device id differs from continuation".into(),
9309                    ));
9310                }
9311                None => {
9312                    tx.execute(
9313                        "INSERT INTO protocol_state (key, value) VALUES (?1, ?2)",
9314                        (
9315                            LOCAL_DEVICE_ID_STATE_KEY,
9316                            continuation.registration.device_id.to_string(),
9317                        ),
9318                    )
9319                    .map_err(DbError::from)?;
9320                }
9321            }
9322            tx.commit().map_err(DbError::from)
9323        })
9324        .await
9325    }
9326
9327    pub(crate) async fn complete_merge_owner_recovery(
9328        &self,
9329        commit: StoreBatchCommit,
9330        commit_ref: StoreBatchCommitRef,
9331        registration: StoreDeviceRegistration,
9332        authority: crate::sync::store_commit::StoreDeviceRegistrationActivation,
9333    ) -> Result<(), DbError> {
9334        self.call(move |conn| {
9335            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
9336            Self::record_activated_store_device_registrations_on(
9337                &tx,
9338                &commit,
9339                &[(registration, authority)],
9340            )?;
9341            Self::record_materialized_commit_on(&tx, &commit, &commit_ref)?;
9342            tx.commit().map_err(DbError::from)
9343        })
9344        .await
9345    }
9346
9347    pub(crate) async fn complete_serial_owner_recovery(
9348        &self,
9349        commit: StoreBatchCommit,
9350        commit_ref: StoreBatchCommitRef,
9351        registration: StoreDeviceRegistration,
9352        authority: crate::sync::store_commit::StoreDeviceRegistrationActivation,
9353        authorization: SerialAuthorizationState,
9354    ) -> Result<(), DbError> {
9355        self.call(move |conn| {
9356            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
9357            Self::record_activated_store_device_registrations_on(
9358                &tx,
9359                &commit,
9360                &[(registration, authority)],
9361            )?;
9362            Self::record_materialized_serial_commit_on(&tx, &commit, &commit_ref, &authorization)?;
9363            tx.commit().map_err(DbError::from)
9364        })
9365        .await
9366    }
9367
9368    pub(crate) async fn stage_local_store_device_registration(
9369        &self,
9370        registration: ExactProtocolObject<StoreDeviceRegistration>,
9371        initial_ack_ref: StoreAckRef,
9372        initial_ack: ExactProtocolObject<StoreAck>,
9373    ) -> Result<(), DbError> {
9374        let registration_ref = StoreDeviceRegistrationRef::from_registration(
9375            &registration.value,
9376            registration.object.clone(),
9377        );
9378        if registration.value.to_bytes() != registration.bytes
9379            || registration.object != *registration.prepared.reference()
9380            || initial_ack.value.to_bytes() != initial_ack.bytes
9381            || initial_ack.object != *initial_ack.prepared.reference()
9382            || initial_ack_ref.object != initial_ack.object
9383            || initial_ack_ref.ack_hash != initial_ack.value.ack_hash()
9384            || initial_ack_ref.revision != initial_ack.value.revision
9385            || initial_ack.value.author_registration != registration_ref
9386        {
9387            return Err(DbError::Message(
9388                "local registration staging graph contains mismatched exact objects".to_string(),
9389            ));
9390        }
9391        self.call(move |conn| {
9392            let root = required_store_root_authority_on(conn)?;
9393            if registration.value.store_root != root {
9394                return Err(DbError::Message(
9395                    "local registration staging graph belongs to another Store root".to_string(),
9396                ));
9397            }
9398            let prepared = serde_json::to_string(&registration.prepared).map_err(|error| {
9399                DbError::Message(format!("serialize prepared local registration: {error}"))
9400            })?;
9401            let ack_ref = serde_json::to_string(&initial_ack_ref).map_err(|error| {
9402                DbError::Message(format!("serialize local initial ack ref: {error}"))
9403            })?;
9404            let ack_prepared = serde_json::to_string(&initial_ack.prepared).map_err(|error| {
9405                DbError::Message(format!("serialize prepared local initial ack: {error}"))
9406            })?;
9407            let existing: Option<PreparedLocalDeviceRegistrationRow> = conn
9408                .query_row(
9409                    "SELECT device_id, registration_hash, registration_bytes, prepared_object, \
9410                            initial_ack_ref, initial_ack_bytes, initial_ack_prepared \
9411                     FROM local_store_device_registration WHERE singleton = 1",
9412                    [],
9413                    |row| {
9414                        Ok((
9415                            row.get(0)?,
9416                            row.get(1)?,
9417                            row.get(2)?,
9418                            row.get(3)?,
9419                            row.get(4)?,
9420                            row.get(5)?,
9421                            row.get(6)?,
9422                        ))
9423                    },
9424                )
9425                .optional()
9426                .map_err(DbError::from)?;
9427            let expected = (
9428                registration_ref.device_id.to_string(),
9429                registration_ref.registration_hash.to_string(),
9430                registration.bytes.clone(),
9431                prepared.clone(),
9432                ack_ref.clone(),
9433                initial_ack.bytes.clone(),
9434                ack_prepared.clone(),
9435            );
9436            match existing {
9437                Some(existing) if existing == expected => Ok(()),
9438                Some(_) => Err(DbError::Message(
9439                    "local registration journal already owns different exact objects".to_string(),
9440                )),
9441                None => conn
9442                    .execute(
9443                        "INSERT INTO local_store_device_registration \
9444                         (singleton, device_id, registration_hash, registration_bytes, \
9445                          prepared_object, initial_ack_ref, initial_ack_bytes, \
9446                          initial_ack_prepared, state) \
9447                         VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
9448                        rusqlite::params![
9449                            expected.0,
9450                            expected.1,
9451                            expected.2,
9452                            expected.3,
9453                            expected.4,
9454                            expected.5,
9455                            expected.6,
9456                            serde_json::to_string(&LocalDeviceRegistrationState::Prepared)
9457                                .map_err(|error| DbError::Message(format!(
9458                                    "serialize local registration state: {error}"
9459                                )))?,
9460                        ],
9461                    )
9462                    .map(|_| ())
9463                    .map_err(DbError::from),
9464            }
9465        })
9466        .await
9467    }
9468
9469    pub(crate) async fn install_existing_local_founder_device(
9470        &self,
9471        registration: ExactProtocolObject<StoreDeviceRegistration>,
9472        initial_ack_ref: StoreAckRef,
9473        initial_ack: ExactProtocolObject<StoreAck>,
9474    ) -> Result<(), DbError> {
9475        let registration_ref = StoreDeviceRegistrationRef::from_registration(
9476            &registration.value,
9477            registration.object.clone(),
9478        );
9479        if registration.value.to_bytes() != registration.bytes
9480            || registration.object != *registration.prepared.reference()
9481            || initial_ack.value.to_bytes() != initial_ack.bytes
9482            || initial_ack.object != *initial_ack.prepared.reference()
9483            || initial_ack_ref.object != initial_ack.object
9484            || initial_ack_ref.ack_hash != initial_ack.value.ack_hash()
9485            || initial_ack_ref.revision != 1
9486            || initial_ack.value.revision != 1
9487            || initial_ack.value.predecessor.is_some()
9488            || initial_ack.value.author_registration != registration_ref
9489        {
9490            return Err(DbError::Message(
9491                "existing founder device graph contains mismatched exact objects".to_string(),
9492            ));
9493        }
9494        self.call(move |conn| {
9495            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
9496            let root = required_store_root_authority_on(&tx)?;
9497            let crate::sync::store_commit::StoreDeviceRegistrationOrigin::Founder { .. } =
9498                &registration.value.origin
9499            else {
9500                return Err(DbError::Message(
9501                    "existing local founder device has a non-founder origin".to_string(),
9502                ));
9503            };
9504            if registration.value.store_root != root {
9505                return Err(DbError::Message(
9506                    "existing local founder device belongs to another Store root".to_string(),
9507                ));
9508            }
9509            let activated = load_activated_registration_on(&tx, &root, &registration_ref)?;
9510            if activated != registration.value {
9511                return Err(DbError::Message(
9512                    "existing local founder device differs from its installed activation"
9513                        .to_string(),
9514                ));
9515            }
9516            let authority = crate::sync::store_commit::StoreDeviceRegistrationActivation::Founder {
9517                root: root.clone(),
9518            };
9519            let expected = (
9520                registration_ref.device_id.to_string(),
9521                registration_ref.registration_hash.to_string(),
9522                registration.bytes.clone(),
9523                serde_json::to_string(&registration.prepared).map_err(|error| {
9524                    DbError::Message(format!("serialize existing founder registration: {error}"))
9525                })?,
9526                serde_json::to_string(&initial_ack_ref).map_err(|error| {
9527                    DbError::Message(format!("serialize existing founder ack ref: {error}"))
9528                })?,
9529                initial_ack.bytes.clone(),
9530                serde_json::to_string(&initial_ack.prepared).map_err(|error| {
9531                    DbError::Message(format!("serialize existing founder ack object: {error}"))
9532                })?,
9533                serde_json::to_string(&LocalDeviceRegistrationState::Activated { authority })
9534                    .map_err(|error| {
9535                        DbError::Message(format!(
9536                            "serialize existing founder registration state: {error}"
9537                        ))
9538                    })?,
9539            );
9540            tx.execute(
9541                "INSERT INTO local_store_device_registration
9542                 (singleton, device_id, registration_hash, registration_bytes,
9543                  prepared_object, initial_ack_ref, initial_ack_bytes,
9544                  initial_ack_prepared, state)
9545                 VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
9546                 ON CONFLICT(singleton) DO NOTHING",
9547                rusqlite::params![
9548                    &expected.0,
9549                    &expected.1,
9550                    &expected.2,
9551                    &expected.3,
9552                    &expected.4,
9553                    &expected.5,
9554                    &expected.6,
9555                    &expected.7,
9556                ],
9557            )
9558            .map_err(DbError::from)?;
9559            let stored: LocalDeviceRegistrationJournalRow = tx
9560                .query_row(
9561                    "SELECT device_id, registration_hash, registration_bytes, prepared_object,
9562                            initial_ack_ref, initial_ack_bytes, initial_ack_prepared, state
9563                     FROM local_store_device_registration WHERE singleton = 1",
9564                    [],
9565                    |row| {
9566                        Ok((
9567                            row.get(0)?,
9568                            row.get(1)?,
9569                            row.get(2)?,
9570                            row.get(3)?,
9571                            row.get(4)?,
9572                            row.get(5)?,
9573                            row.get(6)?,
9574                            row.get(7)?,
9575                        ))
9576                    },
9577                )
9578                .map_err(DbError::from)?;
9579            if stored != expected {
9580                return Err(DbError::Message(
9581                    "existing local founder journal owns different exact objects".to_string(),
9582                ));
9583            }
9584            let ack_ref = serde_json::to_string(&initial_ack_ref).map_err(|error| {
9585                DbError::Message(format!("serialize existing founder ack ref: {error}"))
9586            })?;
9587            let successor =
9588                serde_json::to_string(&initial_ack.value.successor.next_slot).map_err(|error| {
9589                    DbError::Message(format!("serialize existing founder ack successor: {error}"))
9590                })?;
9591            tx.execute(
9592                "INSERT INTO published_store_acks (singleton, ack_ref, successor_slot)
9593                 VALUES (1, ?1, ?2) ON CONFLICT(singleton) DO NOTHING",
9594                (&ack_ref, &successor),
9595            )
9596            .map_err(DbError::from)?;
9597            let stored_ack: (String, String) = tx
9598                .query_row(
9599                    "SELECT ack_ref, successor_slot FROM published_store_acks WHERE singleton = 1",
9600                    [],
9601                    |row| Ok((row.get(0)?, row.get(1)?)),
9602                )
9603                .map_err(DbError::from)?;
9604            if stored_ack != (ack_ref, successor) {
9605                return Err(DbError::Message(
9606                    "existing local founder acknowledgement differs from exact cloud state"
9607                        .to_string(),
9608                ));
9609            }
9610            tx.execute(
9611                "INSERT INTO protocol_state (key, value) VALUES (?1, ?2)
9612                 ON CONFLICT(key) DO NOTHING",
9613                (LOCAL_DEVICE_ID_STATE_KEY, &expected.0),
9614            )
9615            .map_err(DbError::from)?;
9616            let stored_device_id: String = tx
9617                .query_row(
9618                    "SELECT value FROM protocol_state WHERE key = ?1",
9619                    [LOCAL_DEVICE_ID_STATE_KEY],
9620                    |row| row.get(0),
9621                )
9622                .map_err(DbError::from)?;
9623            if stored_device_id != expected.0 {
9624                return Err(DbError::Message(
9625                    "existing local founder device id conflicts with installed state".to_string(),
9626                ));
9627            }
9628            tx.commit().map_err(DbError::from)
9629        })
9630        .await
9631    }
9632
9633    pub(crate) async fn stage_owner_recovery_registration(
9634        &self,
9635        registration: ExactProtocolObject<StoreDeviceRegistration>,
9636        initial_ack_ref: StoreAckRef,
9637        initial_ack: ExactProtocolObject<StoreAck>,
9638        activation: crate::sync::store_commit::StoreDeviceRegistrationActivation,
9639    ) -> Result<bool, DbError> {
9640        let (
9641            crate::sync::store_commit::StoreDeviceRegistrationOrigin::Recovery {
9642                recovery_id: origin_recovery_id,
9643                recovery_slot,
9644                owner_grant,
9645                ..
9646            },
9647            crate::sync::store_commit::StoreDeviceRegistrationActivation::Recovery {
9648                recovery_id: activation_recovery_id,
9649                node,
9650            },
9651        ) = (&registration.value.origin, &activation)
9652        else {
9653            return Err(DbError::Message(
9654                "Owner recovery journal requires one Recovery registration authority".into(),
9655            ));
9656        };
9657        if origin_recovery_id != activation_recovery_id
9658            || node.object.slot() != recovery_slot
9659            || node.owner_grant != *owner_grant
9660        {
9661            return Err(DbError::Message(
9662                "Owner recovery registration differs from its activation authority".into(),
9663            ));
9664        }
9665        let registration_ref = StoreDeviceRegistrationRef::from_registration(
9666            &registration.value,
9667            registration.object.clone(),
9668        );
9669        if registration.value.to_bytes() != registration.bytes
9670            || registration.object != *registration.prepared.reference()
9671            || initial_ack.value.to_bytes() != initial_ack.bytes
9672            || initial_ack.object != *initial_ack.prepared.reference()
9673            || initial_ack_ref.object != initial_ack.object
9674            || initial_ack_ref.ack_hash != initial_ack.value.ack_hash()
9675            || initial_ack_ref.revision != 1
9676            || initial_ack.value.predecessor.is_some()
9677            || initial_ack.value.author_registration != registration_ref
9678        {
9679            return Err(DbError::Message(
9680                "Owner recovery registration graph contains mismatched exact objects".into(),
9681            ));
9682        }
9683        self.call(move |conn| {
9684            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
9685            let root = required_store_root_authority_on(&tx)?;
9686            if registration.value.store_root != root {
9687                return Err(DbError::Message(
9688                    "Owner recovery registration belongs to another Store root".into(),
9689                ));
9690            }
9691            let exact_registration_ref =
9692                serde_json::to_string(&registration_ref).map_err(|error| {
9693                    DbError::Message(format!("Owner recovery registration ref: {error}"))
9694                })?;
9695            let exact_activation = serde_json::to_string(&activation).map_err(|error| {
9696                DbError::Message(format!("Owner recovery activation authority: {error}"))
9697            })?;
9698            let activated = tx
9699                .query_row(
9700                    "SELECT registration_hash, registration_bytes, registration_object, \
9701                            activation_authority \
9702                     FROM store_device_registration_activations WHERE device_id = ?1",
9703                    [registration_ref.device_id.to_string()],
9704                    |row| {
9705                        Ok((
9706                            row.get::<_, String>(0)?,
9707                            row.get::<_, Vec<u8>>(1)?,
9708                            row.get::<_, String>(2)?,
9709                            row.get::<_, String>(3)?,
9710                        ))
9711                    },
9712                )
9713                .optional()
9714                .map_err(DbError::from)?;
9715            let activated = match activated {
9716                None => false,
9717                Some(existing)
9718                    if existing
9719                        == (
9720                            registration_ref.registration_hash.to_string(),
9721                            registration.bytes.clone(),
9722                            exact_registration_ref,
9723                            exact_activation,
9724                        ) =>
9725                {
9726                    true
9727                }
9728                Some(_) => {
9729                    return Err(DbError::Message(
9730                        "Owner recovery device already has different exact activation authority"
9731                            .into(),
9732                    ));
9733                }
9734            };
9735            tx.execute("DELETE FROM local_store_device_registration", [])
9736                .map_err(DbError::from)?;
9737            tx.execute("DELETE FROM published_store_acks", [])
9738                .map_err(DbError::from)?;
9739            tx.execute(
9740                "DELETE FROM protocol_state WHERE key = ?1",
9741                [LOCAL_DEVICE_ID_STATE_KEY],
9742            )
9743            .map_err(DbError::from)?;
9744            tx.execute(
9745                "INSERT INTO local_store_device_registration \
9746                 (singleton, device_id, registration_hash, registration_bytes, \
9747                  prepared_object, initial_ack_ref, initial_ack_bytes, \
9748                  initial_ack_prepared, state) \
9749                 VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
9750                rusqlite::params![
9751                    registration_ref.device_id.to_string(),
9752                    registration_ref.registration_hash.to_string(),
9753                    registration.bytes,
9754                    serde_json::to_string(&registration.prepared).map_err(|error| {
9755                        DbError::Message(format!("Owner recovery registration object: {error}"))
9756                    })?,
9757                    serde_json::to_string(&initial_ack_ref).map_err(|error| {
9758                        DbError::Message(format!("Owner recovery initial ack ref: {error}"))
9759                    })?,
9760                    initial_ack.bytes,
9761                    serde_json::to_string(&initial_ack.prepared).map_err(|error| {
9762                        DbError::Message(format!("Owner recovery initial ack object: {error}"))
9763                    })?,
9764                    serde_json::to_string(&if activated {
9765                        LocalDeviceRegistrationState::Activated {
9766                            authority: activation,
9767                        }
9768                    } else {
9769                        LocalDeviceRegistrationState::Prepared
9770                    })
9771                    .map_err(|error| DbError::Message(format!(
9772                        "Owner recovery journal state: {error}"
9773                    )))?,
9774                ],
9775            )
9776            .map_err(DbError::from)?;
9777            if activated {
9778                tx.execute(
9779                    "INSERT INTO protocol_state (key, value) VALUES (?1, ?2)",
9780                    (
9781                        LOCAL_DEVICE_ID_STATE_KEY,
9782                        registration_ref.device_id.to_string(),
9783                    ),
9784                )
9785                .map_err(DbError::from)?;
9786                tx.execute(
9787                    "INSERT INTO published_store_acks (singleton, ack_ref, successor_slot) \
9788                     VALUES (1, ?1, ?2)",
9789                    rusqlite::params![
9790                        serde_json::to_string(&initial_ack_ref).map_err(|error| {
9791                            DbError::Message(format!(
9792                                "Owner recovery published initial ack ref: {error}"
9793                            ))
9794                        })?,
9795                        serde_json::to_string(&initial_ack.value.successor.next_slot).map_err(
9796                            |error| DbError::Message(format!(
9797                                "Owner recovery initial ack successor: {error}"
9798                            ))
9799                        )?,
9800                    ],
9801                )
9802                .map_err(DbError::from)?;
9803            }
9804            tx.commit().map_err(DbError::from)?;
9805            Ok(activated)
9806        })
9807        .await
9808    }
9809
9810    pub(crate) async fn stage_local_store_device_retirement(
9811        &self,
9812        payload: Vec<u8>,
9813    ) -> Result<(), DbError> {
9814        self.call(move |conn| {
9815            let existing: Option<(Vec<u8>, i64)> = conn
9816                .query_row(
9817                    "SELECT payload, published FROM local_store_device_retirement WHERE singleton = 1",
9818                    [],
9819                    |row| Ok((row.get(0)?, row.get(1)?)),
9820                )
9821                .optional()
9822                .map_err(DbError::from)?;
9823            match existing {
9824                Some((existing, _)) if existing == payload => Ok(()),
9825                Some(_) => Err(DbError::Message(
9826                    "local Store device retirement already owns different exact objects".into(),
9827                )),
9828                None => conn
9829                    .execute(
9830                        "INSERT INTO local_store_device_retirement (singleton, payload, published) VALUES (1, ?1, 0)",
9831                        [payload],
9832                    )
9833                    .map(|_| ())
9834                    .map_err(DbError::from),
9835            }
9836        })
9837        .await
9838    }
9839
9840    pub(crate) async fn local_store_device_retirement(
9841        &self,
9842    ) -> Result<Option<(Vec<u8>, bool)>, DbError> {
9843        self.call(|conn| {
9844            conn.query_row(
9845                "SELECT payload, published FROM local_store_device_retirement WHERE singleton = 1",
9846                [],
9847                |row| Ok((row.get::<_, Vec<u8>>(0)?, row.get::<_, i64>(1)? != 0)),
9848            )
9849            .optional()
9850            .map_err(DbError::from)
9851        })
9852        .await
9853    }
9854
9855    pub(crate) async fn complete_local_store_device_retirement(
9856        &self,
9857        expected_payload: Vec<u8>,
9858        retirement: crate::sync::store_commit::StoreDeviceSelfRetirementRef,
9859        commit: StoreBatchCommit,
9860        commit_ref: StoreBatchCommitRef,
9861        serial_authorization: Option<SerialAuthorizationState>,
9862    ) -> Result<(), DbError> {
9863        self.call(move |conn| {
9864            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
9865            let (payload, published): (Vec<u8>, i64) = tx
9866                .query_row(
9867                    "SELECT payload, published FROM local_store_device_retirement WHERE singleton = 1",
9868                    [],
9869                    |row| Ok((row.get(0)?, row.get(1)?)),
9870                )
9871                .map_err(DbError::from)?;
9872            if payload != expected_payload {
9873                return Err(DbError::Message(
9874                    "local Store device retirement changed exact ownership".into(),
9875                ));
9876            }
9877            if published == 1 {
9878                return Ok(());
9879            }
9880            match serial_authorization {
9881                Some(authorization) => Self::record_materialized_serial_commit_on(
9882                    &tx,
9883                    &commit,
9884                    &commit_ref,
9885                    &authorization,
9886                )?,
9887                None => Self::record_materialized_commit_on(&tx, &commit, &commit_ref)?,
9888            }
9889            let state: String = tx
9890                .query_row(
9891                    "SELECT state FROM local_store_device_registration WHERE singleton = 1",
9892                    [],
9893                    |row| row.get(0),
9894                )
9895                .map_err(DbError::from)?;
9896            let LocalDeviceRegistrationState::Activated { authority } =
9897                serde_json::from_str(&state).map_err(|error| {
9898                    DbError::Message(format!("parse active registration journal: {error}"))
9899                })?
9900            else {
9901                return Err(DbError::Message(
9902                    "only an activated local registration can retire".into(),
9903                ));
9904            };
9905            tx.execute(
9906                "UPDATE local_store_device_registration SET state = ?1 WHERE singleton = 1",
9907                [serde_json::to_string(&LocalDeviceRegistrationState::Retired {
9908                    authority,
9909                    retirement,
9910                })
9911                .map_err(|error| DbError::Message(format!("serialize retired registration: {error}")))?],
9912            )
9913            .map_err(DbError::from)?;
9914            tx.execute(
9915                "UPDATE local_store_device_retirement SET published = 1 WHERE singleton = 1 AND published = 0",
9916                [],
9917            )
9918            .map_err(DbError::from)?;
9919            tx.commit().map_err(DbError::from)
9920        })
9921        .await
9922    }
9923
9924    pub(crate) async fn oldest_unpublished_store_device_registration(
9925        &self,
9926    ) -> Result<Option<DurableDeviceRegistration>, DbError> {
9927        self.read_local_store_device_registration(
9928            "SELECT device_id, registration_hash, registration_bytes, prepared_object, \
9929                    initial_ack_ref, initial_ack_bytes, initial_ack_prepared, state \
9930             FROM local_store_device_registration WHERE singleton = 1 AND state = '\"prepared\"'",
9931        )
9932        .await
9933    }
9934
9935    async fn read_local_store_device_registration(
9936        &self,
9937        sql: &'static str,
9938    ) -> Result<Option<DurableDeviceRegistration>, DbError> {
9939        self.call(move |conn| {
9940            conn.query_row(sql, [], |row| {
9941                Ok((
9942                    row.get::<_, String>(0)?,
9943                    row.get::<_, String>(1)?,
9944                    row.get::<_, Vec<u8>>(2)?,
9945                    row.get::<_, String>(3)?,
9946                    row.get::<_, String>(4)?,
9947                    row.get::<_, Vec<u8>>(5)?,
9948                    row.get::<_, String>(6)?,
9949                    row.get::<_, String>(7)?,
9950                ))
9951            })
9952            .optional()
9953            .map_err(DbError::from)?
9954            .map(
9955                |(device_id, hash, bytes, prepared, ack_ref, ack_bytes, ack_prepared, state)| {
9956                    let device_id = device_id.parse().map_err(|error| {
9957                        DbError::Message(format!("local Store device id: {error}"))
9958                    })?;
9959                    let prepared: PreparedExactObject =
9960                        serde_json::from_str(&prepared).map_err(|error| {
9961                            DbError::Message(format!(
9962                                "local Store device registration prepared object: {error}"
9963                            ))
9964                        })?;
9965                    let registration = StoreDeviceRegistration::parse_at(
9966                        &bytes,
9967                        &required_store_root_authority_on(conn)?,
9968                        device_id,
9969                    )
9970                    .map_err(|error| {
9971                        DbError::Message(format!("local Store device registration: {error}"))
9972                    })?;
9973                    let initial_ack_ref: StoreAckRef =
9974                        serde_json::from_str(&ack_ref).map_err(|error| {
9975                            DbError::Message(format!(
9976                                "local Store initial acknowledgement ref: {error}"
9977                            ))
9978                        })?;
9979                    let initial_ack_value = StoreAck::parse_at(
9980                        &ack_bytes,
9981                        registration.store_root.store_root_hash,
9982                        &initial_ack_ref,
9983                        &registration,
9984                    )
9985                    .map_err(|error| {
9986                        DbError::Message(format!("local Store initial acknowledgement: {error}"))
9987                    })?;
9988                    let initial_ack_prepared: PreparedExactObject =
9989                        serde_json::from_str(&ack_prepared).map_err(|error| {
9990                            DbError::Message(format!("local Store initial ack object: {error}"))
9991                        })?;
9992                    Ok(DurableDeviceRegistration {
9993                        device_id,
9994                        registration_hash: hash.parse().map_err(|error| {
9995                            DbError::Message(format!(
9996                                "local Store device registration hash: {error}"
9997                            ))
9998                        })?,
9999                        registration_bytes: bytes,
10000                        prepared,
10001                        initial_ack_ref,
10002                        initial_ack: ExactProtocolObject {
10003                            value: initial_ack_value,
10004                            bytes: ack_bytes,
10005                            object: initial_ack_prepared.reference().clone(),
10006                            prepared: initial_ack_prepared,
10007                        },
10008                        state: serde_json::from_str(&state).map_err(|error| {
10009                            DbError::Message(format!(
10010                                "local Store registration journal state: {error}"
10011                            ))
10012                        })?,
10013                    })
10014                },
10015            )
10016            .transpose()
10017        })
10018        .await
10019    }
10020
10021    pub(crate) async fn mark_local_store_device_registration_created(
10022        &self,
10023        registration: ExactProtocolObject<StoreDeviceRegistration>,
10024        initial_ack: StoreAckRef,
10025        initial_ack_object: ExactProtocolObject<StoreAck>,
10026    ) -> Result<(), DbError> {
10027        self.call(move |conn| {
10028            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
10029            let registration_ref = StoreDeviceRegistrationRef::from_registration(
10030                &registration.value,
10031                registration.object.clone(),
10032            );
10033            if registration_ref.object != *registration.prepared.reference()
10034                || initial_ack.object != *initial_ack_object.prepared.reference()
10035            {
10036                return Err(DbError::Message(
10037                    "created registration refs differ from their prepared objects".to_string(),
10038                ));
10039            }
10040            let durable: (Vec<u8>, String, String, Vec<u8>, String, String) = tx
10041                .query_row(
10042                    "SELECT registration_bytes, prepared_object, initial_ack_ref, \
10043                            initial_ack_bytes, initial_ack_prepared, state \
10044                     FROM local_store_device_registration \
10045                     WHERE singleton = 1 AND device_id = ?1 AND registration_hash = ?2",
10046                    (
10047                        registration_ref.device_id.to_string(),
10048                        registration_ref.registration_hash.to_string(),
10049                    ),
10050                    |row| {
10051                        Ok((
10052                            row.get(0)?,
10053                            row.get(1)?,
10054                            row.get(2)?,
10055                            row.get(3)?,
10056                            row.get(4)?,
10057                            row.get(5)?,
10058                        ))
10059                    },
10060                )
10061                .map_err(DbError::from)?;
10062            let stored_registration_prepared: PreparedExactObject =
10063                serde_json::from_str(&durable.1).map_err(|error| {
10064                    DbError::Message(format!("stored registration object: {error}"))
10065                })?;
10066            let stored_ack_ref: StoreAckRef = serde_json::from_str(&durable.2)
10067                .map_err(|error| DbError::Message(format!("stored initial ack ref: {error}")))?;
10068            let stored_ack_prepared: PreparedExactObject = serde_json::from_str(&durable.4)
10069                .map_err(|error| DbError::Message(format!("stored initial ack object: {error}")))?;
10070            if registration_ref.object != *stored_registration_prepared.reference()
10071                || registration.prepared.reference() != stored_registration_prepared.reference()
10072                || registration.prepared.stored_bytes()
10073                    != stored_registration_prepared.stored_bytes()
10074                || registration.bytes != durable.0
10075                || initial_ack != stored_ack_ref
10076                || initial_ack_object.prepared.reference() != stored_ack_prepared.reference()
10077                || initial_ack_object.prepared.stored_bytes() != stored_ack_prepared.stored_bytes()
10078                || initial_ack_object.bytes != durable.3
10079            {
10080                return Err(DbError::Message(
10081                    "created registration differs from its complete durable exact objects"
10082                        .to_string(),
10083                ));
10084            }
10085            let prepared = serde_json::to_string(&LocalDeviceRegistrationState::Prepared).map_err(
10086                |error| DbError::Message(format!("serialize prepared journal: {error}")),
10087            )?;
10088            let created = serde_json::to_string(&LocalDeviceRegistrationState::Created)
10089                .map_err(|error| DbError::Message(format!("serialize created journal: {error}")))?;
10090            let updated = tx
10091                .execute(
10092                    "UPDATE local_store_device_registration SET state = ?1 \
10093                     WHERE singleton = 1 AND device_id = ?2 AND registration_hash = ?3 \
10094                       AND initial_ack_ref = ?4 AND state = ?5",
10095                    rusqlite::params![
10096                        created,
10097                        registration_ref.device_id.to_string(),
10098                        registration_ref.registration_hash.to_string(),
10099                        serde_json::to_string(&initial_ack).map_err(|error| {
10100                            DbError::Message(format!("serialize initial ack ref: {error}"))
10101                        })?,
10102                        prepared,
10103                    ],
10104                )
10105                .map_err(DbError::from)?;
10106            if updated != 1 {
10107                let already: Option<String> = tx
10108                    .query_row(
10109                        "SELECT state FROM local_store_device_registration \
10110                         WHERE singleton = 1 AND device_id = ?1 AND registration_hash = ?2",
10111                        (
10112                            registration_ref.device_id.to_string(),
10113                            registration_ref.registration_hash.to_string(),
10114                        ),
10115                        |row| row.get(0),
10116                    )
10117                    .optional()
10118                    .map_err(DbError::from)?;
10119                if already.as_deref() != Some(created.as_str()) {
10120                    return Err(DbError::Message(
10121                        "local registration journal is absent or differs from the created object"
10122                            .to_string(),
10123                    ));
10124                }
10125            }
10126            tx.commit().map_err(DbError::from)
10127        })
10128        .await
10129    }
10130
10131    fn required_store_root_hash_on(conn: &Connection) -> Result<ObjectHash, DbError> {
10132        load_store_root_authority_on(conn)?
10133            .map(|(reference, _)| reference.store_root_hash)
10134            .ok_or_else(DbError::missing_store_root_hash)
10135    }
10136
10137    #[cfg(test)]
10138    pub(crate) async fn required_store_root_hash(&self) -> Result<ObjectHash, DbError> {
10139        self.call(Self::required_store_root_hash_on).await
10140    }
10141
10142    pub async fn get_protocol_state(&self, key: &str) -> Result<Option<String>, DbError> {
10143        let key = key.to_string();
10144        self.call(move |conn| {
10145            conn.query_row(
10146                "SELECT value FROM protocol_state WHERE key = ?1",
10147                [key],
10148                |r| r.get::<_, String>(0),
10149            )
10150            .optional()
10151            .map_err(DbError::from)
10152        })
10153        .await
10154    }
10155
10156    pub(crate) async fn begin_store_creation_attempt(
10157        &self,
10158        initialized: crate::sync::store_protocol_root::StoreCreationAttempt,
10159    ) -> Result<crate::sync::store_protocol_root::StoreCreationAttempt, DbError> {
10160        let value = serde_json::to_string(&initialized).map_err(|error| {
10161            DbError::Message(format!("serialize Store creation attempt: {error}"))
10162        })?;
10163        self.call(move |conn| {
10164            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
10165            tx.execute(
10166                "INSERT OR IGNORE INTO protocol_state (key, value) VALUES (?1, ?2)",
10167                (
10168                    crate::sync::store_protocol_root::STORE_CREATION_ATTEMPT_STATE_KEY,
10169                    &value,
10170                ),
10171            )
10172            .map_err(DbError::from)?;
10173            let actual: String = tx
10174                .query_row(
10175                    "SELECT value FROM protocol_state WHERE key = ?1",
10176                    [crate::sync::store_protocol_root::STORE_CREATION_ATTEMPT_STATE_KEY],
10177                    |row| row.get(0),
10178                )
10179                .map_err(DbError::from)?;
10180            tx.commit().map_err(DbError::from)?;
10181            serde_json::from_str(&actual)
10182                .map_err(|error| DbError::Message(format!("parse Store creation attempt: {error}")))
10183        })
10184        .await
10185    }
10186
10187    pub(crate) async fn load_store_creation_attempt(
10188        &self,
10189    ) -> Result<Option<crate::sync::store_protocol_root::StoreCreationAttempt>, DbError> {
10190        self.call(move |conn| {
10191            let value = conn
10192                .query_row(
10193                    "SELECT value FROM protocol_state WHERE key = ?1",
10194                    [crate::sync::store_protocol_root::STORE_CREATION_ATTEMPT_STATE_KEY],
10195                    |row| row.get::<_, String>(0),
10196                )
10197                .optional()
10198                .map_err(DbError::from)?;
10199            value
10200                .map(|value| {
10201                    serde_json::from_str(&value).map_err(|error| {
10202                        DbError::Message(format!("parse Store creation attempt: {error}"))
10203                    })
10204                })
10205                .transpose()
10206        })
10207        .await
10208    }
10209
10210    pub(crate) async fn advance_store_creation_attempt(
10211        &self,
10212        previous: crate::sync::store_protocol_root::StoreCreationAttempt,
10213        next: crate::sync::store_protocol_root::StoreCreationAttempt,
10214    ) -> Result<(), DbError> {
10215        let previous = serde_json::to_string(&previous).map_err(|error| {
10216            DbError::Message(format!("serialize Store creation predecessor: {error}"))
10217        })?;
10218        let next = serde_json::to_string(&next).map_err(|error| {
10219            DbError::Message(format!("serialize Store creation successor: {error}"))
10220        })?;
10221        self.call(move |conn| {
10222            let changed = conn
10223                .execute(
10224                    "UPDATE protocol_state SET value = ?1 WHERE key = ?2 AND value = ?3",
10225                    (
10226                        &next,
10227                        crate::sync::store_protocol_root::STORE_CREATION_ATTEMPT_STATE_KEY,
10228                        &previous,
10229                    ),
10230                )
10231                .map_err(DbError::from)?;
10232            if changed != 1 {
10233                return Err(DbError::Message(
10234                    "Store creation attempt advance lost its exact predecessor".to_string(),
10235                ));
10236            }
10237            Ok(())
10238        })
10239        .await
10240    }
10241
10242    pub(crate) async fn load_provider_probe_journal(
10243        &self,
10244        probe_id: crate::sync::provider::ProviderProbeId,
10245    ) -> Result<Option<crate::sync::provider::ProviderProbeJournalRecord>, DbError> {
10246        let key = format!("provider_probe/{}", hex::encode(probe_id.as_bytes()));
10247        self.call(move |conn| {
10248            let value = conn
10249                .query_row(
10250                    "SELECT value FROM protocol_state WHERE key = ?1",
10251                    [key],
10252                    |row| row.get::<_, String>(0),
10253                )
10254                .optional()
10255                .map_err(DbError::from)?;
10256            value
10257                .map(|value| {
10258                    serde_json::from_str(&value).map_err(|error| {
10259                        DbError::Message(format!("parse provider probe journal: {error}"))
10260                    })
10261                })
10262                .transpose()
10263        })
10264        .await
10265    }
10266
10267    pub(crate) async fn begin_provider_probe_journal(
10268        &self,
10269        prepared: crate::sync::provider::ProviderProbeJournalRecord,
10270    ) -> Result<crate::sync::provider::ProviderProbeJournalRecord, DbError> {
10271        prepared.validate_begin().map_err(|error| {
10272            DbError::Message(format!("invalid provider probe journal beginning: {error}"))
10273        })?;
10274        let key = format!(
10275            "provider_probe/{}",
10276            hex::encode(prepared.probe_id().as_bytes())
10277        );
10278        let value = serde_json::to_string(&prepared).map_err(|error| {
10279            DbError::Message(format!("serialize provider probe journal: {error}"))
10280        })?;
10281        self.call(move |conn| {
10282            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
10283            tx.execute(
10284                "INSERT OR IGNORE INTO protocol_state (key, value) VALUES (?1, ?2)",
10285                (&key, &value),
10286            )
10287            .map_err(DbError::from)?;
10288            let actual: String = tx
10289                .query_row(
10290                    "SELECT value FROM protocol_state WHERE key = ?1",
10291                    [&key],
10292                    |row| row.get(0),
10293                )
10294                .map_err(DbError::from)?;
10295            tx.commit().map_err(DbError::from)?;
10296            serde_json::from_str(&actual)
10297                .map_err(|error| DbError::Message(format!("parse provider probe journal: {error}")))
10298        })
10299        .await
10300    }
10301
10302    pub(crate) async fn advance_provider_probe_journal(
10303        &self,
10304        previous: crate::sync::provider::ProviderProbeJournalRecord,
10305        next: crate::sync::provider::ProviderProbeJournalRecord,
10306    ) -> Result<(), DbError> {
10307        previous.validate_transition(&next).map_err(|error| {
10308            DbError::Message(format!("invalid provider probe journal advance: {error}"))
10309        })?;
10310        let key = format!(
10311            "provider_probe/{}",
10312            hex::encode(previous.probe_id().as_bytes())
10313        );
10314        let previous = serde_json::to_string(&previous).map_err(|error| {
10315            DbError::Message(format!("serialize provider probe journal: {error}"))
10316        })?;
10317        let next = serde_json::to_string(&next).map_err(|error| {
10318            DbError::Message(format!("serialize provider probe journal: {error}"))
10319        })?;
10320        self.call(move |conn| {
10321            let changed = conn
10322                .execute(
10323                    "UPDATE protocol_state SET value = ?1 WHERE key = ?2 AND value = ?3",
10324                    (&next, &key, &previous),
10325                )
10326                .map_err(DbError::from)?;
10327            if changed != 1 {
10328                return Err(DbError::Message(
10329                    "provider probe journal advance lost its exact predecessor".to_string(),
10330                ));
10331            }
10332            Ok(())
10333        })
10334        .await
10335    }
10336
10337    pub(crate) async fn prepare_device_join_challenge_publication(
10338        &self,
10339        challenge: crate::sync::provider::CrossPrincipalProbeChallenge,
10340    ) -> Result<crate::sync::provider::DeviceJoinChallengePublicationRecord, DbError> {
10341        use crate::sync::provider::{
10342            DeviceJoinChallengePublicationProgress, DeviceJoinChallengePublicationRecord,
10343        };
10344
10345        let key = format!(
10346            "device_join_challenge_publication/{}",
10347            hex::encode(challenge.probe_id.as_bytes())
10348        );
10349        let prepared = DeviceJoinChallengePublicationRecord {
10350            challenge,
10351            progress: DeviceJoinChallengePublicationProgress::Prepared,
10352        };
10353        let value = serde_json::to_string(&prepared).map_err(|error| {
10354            DbError::Message(format!(
10355                "serialize device join challenge publication: {error}"
10356            ))
10357        })?;
10358        self.call(move |conn| {
10359            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
10360            tx.execute(
10361                "INSERT OR IGNORE INTO protocol_state (key, value) VALUES (?1, ?2)",
10362                (&key, &value),
10363            )
10364            .map_err(DbError::from)?;
10365            let actual: String = tx
10366                .query_row(
10367                    "SELECT value FROM protocol_state WHERE key = ?1",
10368                    [&key],
10369                    |row| row.get(0),
10370                )
10371                .map_err(DbError::from)?;
10372            tx.commit().map_err(DbError::from)?;
10373            let actual: DeviceJoinChallengePublicationRecord = serde_json::from_str(&actual)
10374                .map_err(|error| {
10375                    DbError::Message(format!("parse device join challenge publication: {error}"))
10376                })?;
10377            if actual.challenge != prepared.challenge {
10378                return Err(DbError::Message(
10379                    "device join challenge probe id was reused with different bytes".to_string(),
10380                ));
10381            }
10382            Ok(actual)
10383        })
10384        .await
10385    }
10386
10387    pub(crate) async fn publish_device_join_challenge(
10388        &self,
10389        authorization: crate::sync::provider::DeviceJoinChallengePublicationAuthorization,
10390        challenge: crate::sync::provider::CrossPrincipalProbeChallenge,
10391    ) -> Result<(), DbError> {
10392        use crate::sync::provider::{
10393            DeviceJoinChallengePublicationProgress, DeviceJoinChallengePublicationRecord,
10394        };
10395
10396        let key = format!(
10397            "device_join_challenge_publication/{}",
10398            hex::encode(challenge.probe_id.as_bytes())
10399        );
10400        self.call(move |conn| {
10401            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
10402            let previous_json: String = tx
10403                .query_row(
10404                    "SELECT value FROM protocol_state WHERE key = ?1",
10405                    [&key],
10406                    |row| row.get(0),
10407                )
10408                .map_err(DbError::from)?;
10409            let previous: DeviceJoinChallengePublicationRecord =
10410                serde_json::from_str(&previous_json).map_err(|error| {
10411                    DbError::Message(format!("parse device join challenge publication: {error}"))
10412                })?;
10413            if previous.challenge != challenge {
10414                return Err(DbError::Message(
10415                    "device join challenge publication differs from prepared bytes".to_string(),
10416                ));
10417            }
10418            match &previous.progress {
10419                DeviceJoinChallengePublicationProgress::Prepared => {
10420                    let next = DeviceJoinChallengePublicationRecord {
10421                        challenge,
10422                        progress: DeviceJoinChallengePublicationProgress::Published {
10423                            authorization,
10424                        },
10425                    };
10426                    let next_json = serde_json::to_string(&next).map_err(|error| {
10427                        DbError::Message(format!(
10428                            "serialize device join challenge publication: {error}"
10429                        ))
10430                    })?;
10431                    let changed = tx
10432                        .execute(
10433                            "UPDATE protocol_state SET value = ?1 WHERE key = ?2 AND value = ?3",
10434                            (&next_json, &key, &previous_json),
10435                        )
10436                        .map_err(DbError::from)?;
10437                    if changed != 1 {
10438                        return Err(DbError::Message(
10439                            "device join challenge publication lost its exact predecessor"
10440                                .to_string(),
10441                        ));
10442                    }
10443                }
10444                DeviceJoinChallengePublicationProgress::Published {
10445                    authorization: existing,
10446                } if existing == &authorization => {}
10447                DeviceJoinChallengePublicationProgress::Published { .. } => {
10448                    return Err(DbError::Message(
10449                        "device join challenge publication authorization changed".to_string(),
10450                    ));
10451                }
10452                DeviceJoinChallengePublicationProgress::ProducerClosed { .. }
10453                | DeviceJoinChallengePublicationProgress::CancelledBeforeCreate { .. } => {
10454                    return Err(DbError::Message(
10455                        "device join challenge producer is closed".to_string(),
10456                    ));
10457                }
10458            }
10459            tx.commit().map_err(DbError::from)
10460        })
10461        .await
10462    }
10463
10464    pub(crate) async fn close_published_device_join_challenge(
10465        &self,
10466        authorization: crate::sync::provider::DeviceJoinChallengePublicationAuthorization,
10467        challenge: crate::sync::provider::CrossPrincipalProbeChallenge,
10468    ) -> Result<(), DbError> {
10469        use crate::sync::provider::{
10470            DeviceJoinChallengePublicationProgress, DeviceJoinChallengePublicationRecord,
10471        };
10472
10473        let key = format!(
10474            "device_join_challenge_publication/{}",
10475            hex::encode(challenge.probe_id.as_bytes())
10476        );
10477        self.call(move |conn| {
10478            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
10479            let previous_json: String = tx
10480                .query_row(
10481                    "SELECT value FROM protocol_state WHERE key = ?1",
10482                    [&key],
10483                    |row| row.get(0),
10484                )
10485                .map_err(DbError::from)?;
10486            let previous: DeviceJoinChallengePublicationRecord =
10487                serde_json::from_str(&previous_json).map_err(|error| {
10488                    DbError::Message(format!("parse device join challenge publication: {error}"))
10489                })?;
10490            if previous.challenge != challenge {
10491                return Err(DbError::Message(
10492                    "device join challenge closure differs from prepared bytes".to_string(),
10493                ));
10494            }
10495            match &previous.progress {
10496                DeviceJoinChallengePublicationProgress::Published {
10497                    authorization: existing,
10498                } if existing == &authorization => {
10499                    let next = DeviceJoinChallengePublicationRecord {
10500                        challenge,
10501                        progress: DeviceJoinChallengePublicationProgress::ProducerClosed {
10502                            authorization,
10503                        },
10504                    };
10505                    let next_json = serde_json::to_string(&next).map_err(|error| {
10506                        DbError::Message(format!(
10507                            "serialize device join challenge closure: {error}"
10508                        ))
10509                    })?;
10510                    let changed = tx
10511                        .execute(
10512                            "UPDATE protocol_state SET value = ?1 WHERE key = ?2 AND value = ?3",
10513                            (&next_json, &key, &previous_json),
10514                        )
10515                        .map_err(DbError::from)?;
10516                    if changed != 1 {
10517                        return Err(DbError::Message(
10518                            "device join challenge closure lost its exact predecessor".to_string(),
10519                        ));
10520                    }
10521                }
10522                DeviceJoinChallengePublicationProgress::ProducerClosed {
10523                    authorization: existing,
10524                } if existing == &authorization => {}
10525                _ => {
10526                    return Err(DbError::Message(
10527                        "device join challenge cannot close from its current state".to_string(),
10528                    ));
10529                }
10530            }
10531            tx.commit().map_err(DbError::from)
10532        })
10533        .await
10534    }
10535
10536    pub(crate) async fn cancel_unpublished_device_join_challenge(
10537        &self,
10538        authorization: crate::sync::provider::DeviceJoinChallengePublicationAuthorization,
10539        challenge: crate::sync::provider::CrossPrincipalProbeChallenge,
10540        cancellation: crate::sync::store_commit::DeviceJoinOutcomeRef,
10541    ) -> Result<(), DbError> {
10542        use crate::sync::provider::{
10543            DeviceJoinChallengePublicationProgress, DeviceJoinChallengePublicationRecord,
10544        };
10545
10546        if cancellation.attempt() != &authorization.attempt {
10547            return Err(DbError::Message(
10548                "device join challenge cancellation names another attempt".to_string(),
10549            ));
10550        }
10551        let key = format!(
10552            "device_join_challenge_publication/{}",
10553            hex::encode(challenge.probe_id.as_bytes())
10554        );
10555        self.call(move |conn| {
10556            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
10557            let previous_json: String = tx
10558                .query_row(
10559                    "SELECT value FROM protocol_state WHERE key = ?1",
10560                    [&key],
10561                    |row| row.get(0),
10562                )
10563                .map_err(DbError::from)?;
10564            let previous: DeviceJoinChallengePublicationRecord =
10565                serde_json::from_str(&previous_json).map_err(|error| {
10566                    DbError::Message(format!("parse device join challenge publication: {error}"))
10567                })?;
10568            if previous.challenge != challenge {
10569                return Err(DbError::Message(
10570                    "device join challenge cancellation differs from prepared bytes".to_string(),
10571                ));
10572            }
10573            match &previous.progress {
10574                DeviceJoinChallengePublicationProgress::Prepared => {
10575                    let next = DeviceJoinChallengePublicationRecord {
10576                        challenge,
10577                        progress: DeviceJoinChallengePublicationProgress::CancelledBeforeCreate {
10578                            authorization,
10579                            cancellation,
10580                        },
10581                    };
10582                    let next_json = serde_json::to_string(&next).map_err(|error| {
10583                        DbError::Message(format!(
10584                            "serialize device join challenge cancellation: {error}"
10585                        ))
10586                    })?;
10587                    let changed = tx
10588                        .execute(
10589                            "UPDATE protocol_state SET value = ?1 WHERE key = ?2 AND value = ?3",
10590                            (&next_json, &key, &previous_json),
10591                        )
10592                        .map_err(DbError::from)?;
10593                    if changed != 1 {
10594                        return Err(DbError::Message(
10595                            "device join challenge cancellation lost its exact predecessor"
10596                                .to_string(),
10597                        ));
10598                    }
10599                }
10600                DeviceJoinChallengePublicationProgress::CancelledBeforeCreate {
10601                    authorization: existing_authorization,
10602                    cancellation: existing_cancellation,
10603                } if existing_authorization == &authorization
10604                    && existing_cancellation == &cancellation => {}
10605                _ => {
10606                    return Err(DbError::Message(
10607                        "device join challenge cannot cancel before create from its current state"
10608                            .to_string(),
10609                    ));
10610                }
10611            }
10612            tx.commit().map_err(DbError::from)
10613        })
10614        .await
10615    }
10616
10617    pub async fn set_protocol_state(&self, key: &str, value: &str) -> Result<(), DbError> {
10618        let (key, value) = (key.to_string(), value.to_string());
10619        self.call(move |conn| {
10620            conn.execute(
10621                "INSERT INTO protocol_state (key, value) VALUES (?1, ?2) \
10622                 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
10623                (key, value),
10624            )
10625            .map(|_| ())
10626            .map_err(DbError::from)
10627        })
10628        .await
10629    }
10630
10631    pub async fn delete_protocol_state(&self, key: &str) -> Result<(), DbError> {
10632        let key = key.to_string();
10633        self.call(move |conn| {
10634            conn.execute("DELETE FROM protocol_state WHERE key = ?1", [key])
10635                .map(|_| ())
10636                .map_err(DbError::from)
10637        })
10638        .await
10639    }
10640
10641    /// `namespace`'s device-local cache-size budget in bytes, or `None` if the host
10642    /// has not set one for it. `None` means unlimited — eviction is off for that
10643    /// namespace and its cache grows without bound; the host opts a namespace into a
10644    /// budget by calling [`Self::set_cache_budget`]. Budgets are per namespace so a
10645    /// small namespace (`covers`) is never wiped by pressure from a big one
10646    /// (`release_files`): each evicts against its own budget. Stored as a single
10647    /// decimal value under [`crate::blob::cache::cache_budget_state_key`] in
10648    /// `protocol_state` (config, not per-blob accounting — the cache's truth is still the
10649    /// folder on disk).
10650    pub async fn get_cache_budget(&self, namespace: &str) -> Result<Option<u64>, DbError> {
10651        let key = crate::blob::cache::cache_budget_state_key(namespace);
10652        match self.get_protocol_state(&key).await? {
10653            Some(raw) => raw.parse::<u64>().map(Some).map_err(|e| {
10654                DbError::Message(format!(
10655                    "cache budget for {namespace:?} in protocol_state is not a byte count: {e}"
10656                ))
10657            }),
10658            None => Ok(None),
10659        }
10660    }
10661
10662    /// Set `namespace`'s device-local cache-size budget in bytes. Once set, a populate
10663    /// into that namespace's cache that pushes `storage/cache/<namespace>/` over this
10664    /// total evicts its oldest files (by mtime) back under it; `pinned/` is never
10665    /// counted or touched, and another namespace's files are never walked. Stored
10666    /// under [`crate::blob::cache::cache_budget_state_key`] in `protocol_state`.
10667    pub async fn set_cache_budget(&self, namespace: &str, max_bytes: u64) -> Result<(), DbError> {
10668        let key = crate::blob::cache::cache_budget_state_key(namespace);
10669        self.set_protocol_state(&key, &max_bytes.to_string()).await
10670    }
10671
10672    // ---- Materialized Store commit ledger ----
10673
10674    pub(crate) async fn insert_circle_operation(
10675        &self,
10676        journal: crate::sync::circle_ops::CircleOperationJournal,
10677    ) -> Result<(), DbError> {
10678        let row = PreparedCircleOperationRow::from_journal(journal)?;
10679        self.call(move |conn| {
10680            conn.execute(
10681                "INSERT INTO circle_operations (operation_id, circle_id, payload)
10682                 VALUES (?1, ?2, ?3)",
10683                rusqlite::params![row.operation_id, row.circle_id, row.payload],
10684            )
10685            .map(|_| ())
10686            .map_err(DbError::from)
10687        })
10688        .await
10689    }
10690
10691    pub(crate) async fn circle_operation(
10692        &self,
10693        circle_id: crate::sync::circle::CircleId,
10694    ) -> Result<Option<crate::sync::circle_ops::CircleOperationJournal>, DbError> {
10695        let circle_id = circle_id.to_string();
10696        self.call(move |conn| {
10697            conn.query_row(
10698                "SELECT payload FROM circle_operations WHERE circle_id = ?1",
10699                [circle_id],
10700                |row| row.get::<_, Vec<u8>>(0),
10701            )
10702            .optional()
10703            .map_err(DbError::from)?
10704            .map(|payload| {
10705                serde_json::from_slice(&payload).map_err(|error| {
10706                    DbError::Message(format!("parse circle operation journal: {error}"))
10707                })
10708            })
10709            .transpose()
10710        })
10711        .await
10712    }
10713
10714    pub(crate) async fn oldest_pending_circle_operation(
10715        &self,
10716    ) -> Result<Option<crate::sync::circle_ops::CircleOperationJournal>, DbError> {
10717        self.call(|conn| {
10718            let mut statement = conn
10719                .prepare("SELECT payload FROM circle_operations ORDER BY rowid")
10720                .map_err(DbError::from)?;
10721            let rows = statement
10722                .query_map([], |row| row.get::<_, Vec<u8>>(0))
10723                .map_err(DbError::from)?;
10724            for payload in rows {
10725                let payload = payload.map_err(DbError::from)?;
10726                let journal: crate::sync::circle_ops::CircleOperationJournal =
10727                    serde_json::from_slice(&payload).map_err(|error| {
10728                        DbError::Message(format!("parse circle operation journal: {error}"))
10729                    })?;
10730                if matches!(
10731                    journal.status,
10732                    crate::sync::circle::CircleOperationState::Pending
10733                ) {
10734                    return Ok(Some(journal));
10735                }
10736            }
10737            Ok(None)
10738        })
10739        .await
10740    }
10741
10742    #[doc(hidden)]
10743    pub async fn discard_blocked_circle_operation(
10744        &self,
10745        circle_id: crate::sync::circle::CircleId,
10746    ) -> Result<(), DbError> {
10747        let circle_id = circle_id.to_string();
10748        self.call(move |conn| {
10749            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
10750            let payload = tx
10751                .query_row(
10752                    "SELECT payload FROM circle_operations WHERE circle_id = ?1",
10753                    [&circle_id],
10754                    |row| row.get::<_, Vec<u8>>(0),
10755                )
10756                .optional()
10757                .map_err(DbError::from)?;
10758            let Some(payload) = payload else {
10759                tx.commit().map_err(DbError::from)?;
10760                return Ok(());
10761            };
10762            let journal: crate::sync::circle_ops::CircleOperationJournal =
10763                serde_json::from_slice(&payload).map_err(|error| {
10764                    DbError::Message(format!("parse circle operation journal: {error}"))
10765                })?;
10766            match journal.status {
10767                crate::sync::circle::CircleOperationState::Pending => {
10768                    return Err(DbError::Message(format!(
10769                        "pending circle operation {circle_id} cannot be discarded"
10770                    )));
10771                }
10772                crate::sync::circle::CircleOperationState::Blocked { .. } => {}
10773            }
10774            let deleted = tx
10775                .execute(
10776                    "DELETE FROM circle_operations WHERE circle_id = ?1",
10777                    [&circle_id],
10778                )
10779                .map_err(DbError::from)?;
10780            if deleted != 1 {
10781                return Err(DbError::Message(format!(
10782                    "blocked circle operation {circle_id} disappeared during discard"
10783                )));
10784            }
10785            tx.commit().map_err(DbError::from)
10786        })
10787        .await
10788    }
10789
10790    pub async fn get_circle_operations(
10791        &self,
10792    ) -> Result<Vec<crate::sync::circle::CircleOperationInfo>, DbError> {
10793        self.call(|conn| {
10794            let mut statement = conn
10795                .prepare("SELECT payload FROM circle_operations ORDER BY rowid")
10796                .map_err(DbError::from)?;
10797            let operations = statement
10798                .query_map([], |row| row.get::<_, Vec<u8>>(0))
10799                .map_err(DbError::from)?
10800                .map(|payload| {
10801                    let payload = payload.map_err(DbError::from)?;
10802                    let journal: crate::sync::circle_ops::CircleOperationJournal =
10803                        serde_json::from_slice(&payload).map_err(|error| {
10804                            DbError::Message(format!("parse circle operation journal: {error}"))
10805                        })?;
10806                    let circle_id = journal.circle_id();
10807                    Ok(crate::sync::circle::CircleOperationInfo {
10808                        circle_id,
10809                        name: journal.creation.metadata.name,
10810                        state: journal.status,
10811                    })
10812                })
10813                .collect();
10814            operations
10815        })
10816        .await
10817    }
10818
10819    pub async fn get_circles(
10820        &self,
10821        identity_pubkey: &str,
10822    ) -> Result<Vec<crate::sync::circle::CircleInfo>, DbError> {
10823        let identity_pubkey = identity_pubkey.to_string();
10824        self.call(move |conn| {
10825            let mut statement = conn
10826                .prepare(
10827                    "SELECT activation.circle_id, activation.control_bytes,
10828                            metadata.metadata_bytes, roster.roster_bytes
10829                     FROM circle_control_activations AS activation
10830                     JOIN circle_access_cache AS access
10831                       ON access.circle_id = activation.circle_id
10832                      AND access.control_coord = activation.control_coord
10833                     JOIN circle_metadata_cache AS metadata
10834                       ON metadata.circle_id = activation.circle_id
10835                      AND metadata.control_coord = activation.control_coord
10836                     JOIN circle_roster_cache AS roster
10837                       ON roster.circle_id = activation.circle_id
10838                      AND roster.control_coord = activation.control_coord
10839                     WHERE access.disposition = 'active'
10840                     ORDER BY activation.circle_id, activation.seq",
10841                )
10842                .map_err(DbError::from)?;
10843            let rows = statement
10844                .query_map([], |row| {
10845                    Ok((
10846                        row.get::<_, String>(0)?,
10847                        row.get::<_, Vec<u8>>(1)?,
10848                        row.get::<_, Vec<u8>>(2)?,
10849                        row.get::<_, Vec<u8>>(3)?,
10850                    ))
10851                })
10852                .map_err(DbError::from)?;
10853            let mut circles = Vec::new();
10854            for row in rows {
10855                let (circle_id, control_bytes, metadata_bytes, roster_bytes) =
10856                    row.map_err(DbError::from)?;
10857                let circle_id: crate::sync::circle::CircleId =
10858                    circle_id.parse().map_err(|error| {
10859                        DbError::Message(format!("parse activated circle id: {error}"))
10860                    })?;
10861                let control: crate::sync::circle::CircleControl =
10862                    serde_json::from_slice(&control_bytes).map_err(|error| {
10863                        DbError::Message(format!("parse activated circle control: {error}"))
10864                    })?;
10865                let metadata: crate::sync::circle::CircleMetadata =
10866                    serde_json::from_slice(&metadata_bytes).map_err(|error| {
10867                        DbError::Message(format!("parse activated circle metadata: {error}"))
10868                    })?;
10869                let roster: crate::sync::circle::CircleMaterializedRoster =
10870                    serde_json::from_slice(&roster_bytes).map_err(|error| {
10871                        DbError::Message(format!("parse activated circle roster: {error}"))
10872                    })?;
10873                if !control.verify()
10874                    || !metadata.verify()
10875                    || !roster.verify()
10876                    || control.circle_id != circle_id
10877                    || metadata.circle_id != circle_id
10878                    || match &control.metadata {
10879                        crate::sync::circle::CircleMetadataStateRef::MergeConcurrent {
10880                            selected,
10881                            state_hash,
10882                            ..
10883                        } => {
10884                            selected != &metadata.coord() || *state_hash != metadata.metadata_hash()
10885                        }
10886                        crate::sync::circle::CircleMetadataStateRef::Serial { current } => {
10887                            current != &metadata.coord()
10888                        }
10889                    }
10890                    || match &control.roster {
10891                        crate::sync::circle::CircleRosterStateRef::MergeConcurrent {
10892                            state_hash,
10893                            ..
10894                        }
10895                        | crate::sync::circle::CircleRosterStateRef::Serial { state_hash } => {
10896                            *state_hash != roster.state_hash()
10897                        }
10898                    }
10899                {
10900                    return Err(DbError::Message(format!(
10901                        "activated circle {circle_id} has inconsistent cached state"
10902                    )));
10903                }
10904                let role = roster
10905                    .members()
10906                    .get(&identity_pubkey)
10907                    .copied()
10908                    .ok_or_else(|| {
10909                        DbError::Message(format!(
10910                            "activated circle {circle_id} excludes the local identity"
10911                        ))
10912                    })?;
10913                circles.push(crate::sync::circle::CircleInfo {
10914                    id: circle_id,
10915                    name: metadata.name,
10916                    role,
10917                });
10918            }
10919            Ok(circles)
10920        })
10921        .await
10922    }
10923
10924    pub(crate) async fn circle_publication_context(
10925        &self,
10926        circle_id: crate::sync::circle::CircleId,
10927        expected_control: crate::sync::circle::CircleControlCoord,
10928    ) -> Result<(EncryptionService, crate::KeyFingerprint), DbError> {
10929        self.call(move |conn| {
10930            let control_coord = serde_json::to_string(&expected_control).map_err(|error| {
10931                DbError::Message(format!("serialize Circle publication coordinate: {error}"))
10932            })?;
10933            let (control_bytes, access_bytes): (Vec<u8>, Vec<u8>) = conn
10934                .query_row(
10935                    "SELECT activation.control_bytes, access.access_bytes
10936                     FROM circle_control_activations AS activation
10937                     JOIN circle_access_cache AS access
10938                       ON access.circle_id = activation.circle_id
10939                      AND access.control_coord = activation.control_coord
10940                     WHERE activation.circle_id = ?1
10941                       AND activation.control_coord = ?2
10942                       AND access.disposition = 'active'",
10943                    (circle_id.to_string(), control_coord),
10944                    |row| Ok((row.get(0)?, row.get(1)?)),
10945                )
10946                .map_err(DbError::from)?;
10947            let control: crate::sync::circle::CircleControl =
10948                serde_json::from_slice(&control_bytes).map_err(|error| {
10949                    DbError::Message(format!("parse Circle publication control: {error}"))
10950                })?;
10951            let access: crate::sync::circle::CircleAccessLeaf =
10952                serde_json::from_slice(&access_bytes).map_err(|error| {
10953                    DbError::Message(format!("parse Circle publication access: {error}"))
10954                })?;
10955            if !control.verify()
10956                || control.circle_id != circle_id
10957                || control.coord() != expected_control
10958                || !access.verify_signature()
10959                || access.circle_id != circle_id
10960                || access.epoch_id != control.epoch_id
10961            {
10962                return Err(DbError::Message(format!(
10963                    "Circle {circle_id} publication authority differs from its exact activation"
10964                )));
10965            }
10966            let crate::sync::circle::CircleAccessDisposition::Active {
10967                keyring,
10968                key_fingerprint,
10969                roster,
10970            } = access.disposition
10971            else {
10972                return Err(DbError::Message(format!(
10973                    "Circle {circle_id} has no active publication key"
10974                )));
10975            };
10976            if key_fingerprint != control.key_fingerprint || roster != control.roster {
10977                return Err(DbError::Message(format!(
10978                    "Circle {circle_id} publication key differs from its exact control"
10979                )));
10980            }
10981            let encryption = EncryptionService::from(
10982                crate::encryption::MasterKeyring::from_serialized(&keyring).map_err(|error| {
10983                    DbError::Message(format!("parse Circle publication keyring: {error}"))
10984                })?,
10985            );
10986            if encryption.seal_key_fingerprint() != key_fingerprint {
10987                return Err(DbError::Message(format!(
10988                    "Circle {circle_id} publication key fingerprint is invalid"
10989                )));
10990            }
10991            Ok((encryption, key_fingerprint))
10992        })
10993        .await
10994    }
10995
10996    pub(crate) async fn update_circle_operation(
10997        &self,
10998        journal: crate::sync::circle_ops::CircleOperationJournal,
10999    ) -> Result<(), DbError> {
11000        let row = PreparedCircleOperationRow::from_journal(journal)?;
11001        self.call(move |conn| {
11002            let updated = conn
11003                .execute(
11004                    "UPDATE circle_operations SET payload = ?3
11005                     WHERE operation_id = ?1 AND circle_id = ?2",
11006                    rusqlite::params![row.operation_id, row.circle_id, row.payload],
11007                )
11008                .map_err(DbError::from)?;
11009            if updated != 1 {
11010                return Err(DbError::Message(
11011                    "circle operation disappeared during publication".to_string(),
11012                ));
11013            }
11014            Ok(())
11015        })
11016        .await
11017    }
11018
11019    pub(crate) async fn block_circle_operation(
11020        &self,
11021        circle_id: crate::sync::circle::CircleId,
11022        reason: String,
11023    ) -> Result<(), DbError> {
11024        let mut journal = self
11025            .circle_operation(circle_id)
11026            .await?
11027            .ok_or_else(|| DbError::Message(format!("circle operation {circle_id} is absent")))?;
11028        journal.status = crate::sync::circle::CircleOperationState::Blocked { reason };
11029        self.update_circle_operation(journal).await
11030    }
11031
11032    pub(crate) async fn activate_circle_operation(
11033        &self,
11034        journal: crate::sync::circle_ops::CircleOperationJournal,
11035        identity: &crate::keys::UserKeypair,
11036    ) -> Result<(), DbError> {
11037        let identity = identity.clone();
11038        self.call(move |conn| {
11039            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
11040            if !matches!(
11041                journal.status,
11042                crate::sync::circle::CircleOperationState::Pending
11043            ) {
11044                return Err(DbError::Message(
11045                    "blocked circle operation cannot activate".to_string(),
11046                ));
11047            }
11048            let creation = &journal.creation;
11049            let resolved_roster = creation.resolved_roster();
11050            if !creation.control.verify()
11051                || !creation.metadata.verify()
11052                || !resolved_roster.verify()
11053            {
11054                return Err(DbError::Message(
11055                    "circle operation contains invalid signed objects".to_string(),
11056                ));
11057            }
11058            let unverified_commit: StoreBatchCommit = serde_json::from_slice(&journal.commit_bytes)
11059                .map_err(|error| DbError::Message(format!("parse circle Store commit: {error}")))?;
11060            let root = required_store_root_authority_on(&tx)?;
11061            let author =
11062                load_activated_registration_on(&tx, &root, &unverified_commit.author_registration)?;
11063            let verify_commit = || {
11064                let commit = StoreBatchCommit::parse_at(
11065                    &journal.commit_bytes,
11066                    root.store_root_hash,
11067                    &journal.commit_ref.coord,
11068                    &author,
11069                )
11070                .map_err(|error| {
11071                    DbError::Message(format!("verify circle Store commit: {error}"))
11072                })?;
11073                journal
11074                    .commit_ref
11075                    .verify_commit(&commit)
11076                    .map_err(|error| DbError::Message(error.to_string()))?;
11077                if journal.commit_ref.object.slot().logical_key()
11078                    != commit_semantic_prefix(
11079                        commit.candidate_family(),
11080                        &match &journal.commit_ref.coord {
11081                            StoreCommitCoord::MergeConcurrent { stream_id, .. } => {
11082                                stream_id.to_string()
11083                            }
11084                            StoreCommitCoord::Serial { .. } => SERIAL_STREAM_ID.to_string(),
11085                        },
11086                        commit.seq(),
11087                        commit.commit_hash(),
11088                    ) + ".json"
11089                {
11090                    return Err(DbError::Message(
11091                        "circle commit exact object occupies a different semantic slot".to_string(),
11092                    ));
11093                }
11094                let [control_ref] = commit.circle_controls() else {
11095                    return Err(DbError::Message(
11096                        "circle creation Store commit is not an exact control-only batch"
11097                            .to_string(),
11098                    ));
11099                };
11100                let expected_ref = creation.control_ref(control_ref.objects().clone());
11101                if control_ref != &expected_ref
11102                    || commit.store_package().is_some()
11103                    || !commit.circle_packages().is_empty()
11104                    || commit.control().is_some()
11105                    || !commit.device_registrations().is_empty()
11106                {
11107                    return Err(DbError::Message(
11108                        "circle creation Store commit is not an exact control-only batch"
11109                            .to_string(),
11110                    ));
11111                }
11112                let activation = crate::sync::circle_ops::verify_local_circle_activation(
11113                    &journal,
11114                    &journal.commit_ref,
11115                    &commit,
11116                    &author,
11117                    &identity,
11118                )
11119                .map_err(|error| DbError::Message(error.to_string()))?;
11120                Ok((commit, activation))
11121            };
11122            let (commit, activation) = match &journal.policy {
11123                crate::sync::circle_ops::CircleOperationPolicy::MergeConcurrent { head } => {
11124                    let (commit, activation) = verify_commit()?;
11125                    let parsed = StoreDeviceHead::parse_at(
11126                        &head.to_bytes(),
11127                        commit.store_root_hash,
11128                        &author,
11129                        &journal.commit_ref,
11130                    )
11131                    .map_err(|error| {
11132                        DbError::Message(format!("verify circle activation head: {error}"))
11133                    })?;
11134                    if parsed.commit != journal.commit_ref {
11135                        return Err(DbError::Message(
11136                            "circle activation head names a different commit".to_string(),
11137                        ));
11138                    }
11139                    Self::record_materialized_commit_on(&tx, &commit, &journal.commit_ref)?;
11140                    (commit, activation)
11141                }
11142                crate::sync::circle_ops::CircleOperationPolicy::Serial {
11143                    head,
11144                    authorization,
11145                    ..
11146                } => {
11147                    let (commit, activation) = verify_commit()?;
11148                    let parsed =
11149                        StoreSerialHead::parse(&head.to_bytes(), commit.store_root_hash, &author)
11150                            .map_err(|error| {
11151                            DbError::Message(format!("verify circle Serial head: {error}"))
11152                        })?;
11153                    if !matches!(
11154                        parsed.state,
11155                        StoreSerialHeadState::Commit { ref commit, .. }
11156                            if commit == &journal.commit_ref
11157                    ) {
11158                        return Err(DbError::Message(
11159                            "circle Serial head names a different commit".to_string(),
11160                        ));
11161                    }
11162                    Self::record_materialized_serial_commit_on(
11163                        &tx,
11164                        &commit,
11165                        &journal.commit_ref,
11166                        authorization,
11167                    )?;
11168                    (commit, activation)
11169                }
11170            };
11171            Self::record_verified_circle_activations_on(
11172                &tx,
11173                &commit,
11174                &journal.commit_ref,
11175                &[activation],
11176            )?;
11177            let deleted = tx
11178                .execute(
11179                    "DELETE FROM circle_operations WHERE operation_id = ?1 AND circle_id = ?2",
11180                    rusqlite::params![journal.operation_id, creation.circle_id.to_string()],
11181                )
11182                .map_err(DbError::from)?;
11183            if deleted != 1 {
11184                return Err(DbError::Message(
11185                    "circle operation disappeared during activation".to_string(),
11186                ));
11187            }
11188            tx.commit().map_err(DbError::from)
11189        })
11190        .await
11191    }
11192
11193    pub(crate) async fn materialized_frontier(
11194        &self,
11195    ) -> Result<BTreeMap<String, StoreBatchCommitRef>, DbError> {
11196        self.call(|conn| Self::materialized_frontier_on(conn, None))
11197            .await
11198    }
11199
11200    pub(crate) async fn exact_materialized_ref(
11201        &self,
11202        stream_id: &str,
11203        sequence: u64,
11204    ) -> Result<Option<StoreBatchCommitRef>, DbError> {
11205        let stream_id = stream_id.to_string();
11206        self.call(move |conn| Self::materialized_commit_ref_on(conn, &stream_id, sequence))
11207            .await
11208    }
11209
11210    pub(crate) async fn snapshot_coverage_frontier(&self) -> Result<CommitFrontier, DbError> {
11211        let policy = self.write_policy();
11212        self.call(move |conn| {
11213            let mut stmt = conn
11214                .prepare("SELECT device_id, seq, commit_ref FROM snapshot_coverage")
11215                .map_err(DbError::from)?;
11216            let rows = stmt
11217                .query_map([], |row| {
11218                    Ok((
11219                        row.get::<_, String>(0)?,
11220                        row.get::<_, i64>(1)?,
11221                        row.get::<_, String>(2)?,
11222                    ))
11223                })
11224                .map_err(DbError::from)?;
11225            let mut frontier = BTreeMap::new();
11226            for row in rows {
11227                let (device_id, seq, reference) = row.map_err(DbError::from)?;
11228                let seq = Self::sequence_from_sqlite(&device_id, seq)?;
11229                let reference = Self::parse_stored_commit_ref(&device_id, seq, &reference)?;
11230                frontier.insert(device_id.clone(), reference);
11231            }
11232            CommitFrontier::from_refs(policy, frontier)
11233                .map_err(|error| DbError::Message(format!("snapshot coverage frontier: {error}")))
11234        })
11235        .await
11236    }
11237
11238    pub(crate) fn materialized_frontier_on(
11239        conn: &Connection,
11240        exclude_device: Option<&str>,
11241    ) -> Result<BTreeMap<String, StoreBatchCommitRef>, DbError> {
11242        let mut frontier = BTreeMap::new();
11243        let mut stmt = conn
11244            .prepare(
11245                "SELECT m.device_id, m.seq, m.commit_ref \
11246                 FROM materialized_commits m \
11247                 JOIN (SELECT device_id, MAX(seq) AS seq FROM materialized_commits \
11248                       GROUP BY device_id) latest \
11249                   ON latest.device_id = m.device_id AND latest.seq = m.seq",
11250            )
11251            .map_err(DbError::from)?;
11252        let rows = stmt
11253            .query_map([], |row| {
11254                Ok((
11255                    row.get::<_, String>(0)?,
11256                    row.get::<_, i64>(1)?,
11257                    row.get::<_, String>(2)?,
11258                ))
11259            })
11260            .map_err(DbError::from)?;
11261        for row in rows {
11262            let (device_id, seq, reference) = row.map_err(DbError::from)?;
11263            if exclude_device == Some(device_id.as_str()) {
11264                continue;
11265            }
11266            let seq = Self::sequence_from_sqlite(&device_id, seq)?;
11267            frontier.insert(
11268                device_id.clone(),
11269                Self::parse_stored_commit_ref(&device_id, seq, &reference)?,
11270            );
11271        }
11272
11273        let mut coverage = conn
11274            .prepare("SELECT device_id, seq, commit_ref FROM snapshot_coverage")
11275            .map_err(DbError::from)?;
11276        let rows = coverage
11277            .query_map([], |row| {
11278                Ok((
11279                    row.get::<_, String>(0)?,
11280                    row.get::<_, i64>(1)?,
11281                    row.get::<_, String>(2)?,
11282                ))
11283            })
11284            .map_err(DbError::from)?;
11285        for row in rows {
11286            let (device_id, seq, reference) = row.map_err(DbError::from)?;
11287            if exclude_device == Some(device_id.as_str()) {
11288                continue;
11289            }
11290            let seq = Self::sequence_from_sqlite(&device_id, seq)?;
11291            let reference = Self::parse_stored_commit_ref(&device_id, seq, &reference)?;
11292            if frontier
11293                .get(&device_id)
11294                .is_none_or(|current| current.coord.sequence() < reference.coord.sequence())
11295            {
11296                frontier.insert(device_id, reference);
11297            }
11298        }
11299        Ok(frontier)
11300    }
11301
11302    fn parse_stored_commit_ref(
11303        stream_id: &str,
11304        sequence: u64,
11305        encoded: &str,
11306    ) -> Result<StoreBatchCommitRef, DbError> {
11307        let reference: StoreBatchCommitRef = serde_json::from_str(encoded)
11308            .map_err(|error| DbError::Message(format!("stored exact Store commit ref: {error}")))?;
11309        let coordinate_matches = match &reference.coord {
11310            StoreCommitCoord::Serial { sequence: declared } => {
11311                stream_id == SERIAL_STREAM_ID && *declared == sequence
11312            }
11313            StoreCommitCoord::MergeConcurrent {
11314                stream_id: declared,
11315                sequence: declared_sequence,
11316            } => declared.to_string() == stream_id && *declared_sequence == sequence,
11317        };
11318        if !coordinate_matches {
11319            return Err(DbError::Message(format!(
11320                "stored exact Store commit ref differs from {stream_id}/{sequence}"
11321            )));
11322        }
11323        Ok(reference)
11324    }
11325
11326    pub(crate) fn materialized_commit_ref_on(
11327        conn: &Connection,
11328        stream_id: &str,
11329        sequence: u64,
11330    ) -> Result<Option<StoreBatchCommitRef>, DbError> {
11331        let seq = Self::sequence_to_sqlite(stream_id, sequence)?;
11332        conn.query_row(
11333            "SELECT commit_ref FROM materialized_commits WHERE device_id = ?1 AND seq = ?2",
11334            (stream_id, seq),
11335            |row| row.get::<_, String>(0),
11336        )
11337        .optional()
11338        .map_err(DbError::from)?
11339        .map(|encoded| Self::parse_stored_commit_ref(stream_id, sequence, &encoded))
11340        .transpose()
11341    }
11342
11343    fn latest_position_for_device_on(
11344        conn: &Connection,
11345        device_id: &str,
11346    ) -> Result<Option<StoreBatchCommitRef>, DbError> {
11347        let materialized = conn
11348            .query_row(
11349                "SELECT seq, commit_ref FROM materialized_commits
11350                 WHERE device_id = ?1 ORDER BY seq DESC LIMIT 1",
11351                [device_id],
11352                |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
11353            )
11354            .optional()
11355            .map_err(DbError::from)?;
11356        let coverage = conn
11357            .query_row(
11358                "SELECT seq, commit_ref FROM snapshot_coverage WHERE device_id = ?1",
11359                [device_id],
11360                |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
11361            )
11362            .optional()
11363            .map_err(DbError::from)?;
11364        let references = [materialized, coverage]
11365            .into_iter()
11366            .flatten()
11367            .map(|(seq, reference)| {
11368                let seq = Self::sequence_from_sqlite(device_id, seq)?;
11369                Self::parse_stored_commit_ref(device_id, seq, &reference)
11370            })
11371            .collect::<Result<Vec<_>, DbError>>()?;
11372        if references.len() == 2
11373            && references[0].coord.sequence() == references[1].coord.sequence()
11374            && references[0] != references[1]
11375        {
11376            return Err(DbError::Message(format!(
11377                "materialized ledger and snapshot coverage fork {device_id:?} at sequence {}",
11378                references[0].coord.sequence()
11379            )));
11380        }
11381        Ok(references
11382            .into_iter()
11383            .max_by_key(|reference| reference.coord.sequence()))
11384    }
11385
11386    pub(crate) fn record_activated_store_device_registrations_on(
11387        conn: &Connection,
11388        commit: &StoreBatchCommit,
11389        registrations: &[(
11390            StoreDeviceRegistration,
11391            crate::sync::store_commit::StoreDeviceRegistrationActivation,
11392        )],
11393    ) -> Result<(), DbError> {
11394        if registrations.len() != commit.device_registrations().len() {
11395            return Err(DbError::Message(
11396                "Store device registration activation count differs from the signed commit"
11397                    .to_string(),
11398            ));
11399        }
11400        for signed in commit.device_registrations() {
11401            let (registration, authority) = registrations
11402                .iter()
11403                .find(|(registration, _)| registration.device_id == signed.registration.device_id)
11404                .ok_or_else(|| {
11405                    DbError::Message(format!(
11406                        "Store commit is missing registration bytes for {}",
11407                        signed.registration.device_id
11408                    ))
11409                })?;
11410            signed
11411                .registration
11412                .verify_registration(registration)
11413                .map_err(|error| DbError::Message(error.to_string()))?;
11414            if registration.store_root.store_root_hash != commit.store_root_hash {
11415                return Err(DbError::Message(format!(
11416                    "Store registration {} belongs to a different Store",
11417                    registration.device_id
11418                )));
11419            }
11420            let expected_authority = match (&registration.origin, &signed.authority) {
11421                (
11422                    crate::sync::store_commit::StoreDeviceRegistrationOrigin::Join {
11423                        attempt_id: origin_attempt,
11424                        outcome_slot,
11425                        ..
11426                    },
11427                    crate::sync::store_commit::StoreDeviceRegistrationActivationRef::Join {
11428                        attempt_id,
11429                        outcome,
11430                    },
11431                ) if origin_attempt == attempt_id && outcome_slot == outcome.slot() => {
11432                    crate::sync::store_commit::StoreDeviceRegistrationActivation::Join {
11433                        attempt_id: *attempt_id,
11434                        outcome: outcome.clone(),
11435                    }
11436                }
11437                (
11438                    crate::sync::store_commit::StoreDeviceRegistrationOrigin::Recovery {
11439                        recovery_id: origin_recovery,
11440                        recovery_slot,
11441                        ..
11442                    },
11443                    crate::sync::store_commit::StoreDeviceRegistrationActivationRef::Recovery {
11444                        recovery_id,
11445                        node,
11446                    },
11447                ) if origin_recovery == recovery_id && recovery_slot == node.slot() => {
11448                    crate::sync::store_commit::StoreDeviceRegistrationActivation::Recovery {
11449                        recovery_id: *recovery_id,
11450                        node: node.clone(),
11451                    }
11452                }
11453                _ => {
11454                    return Err(DbError::Message(format!(
11455                        "Store registration {} origin differs from its signed activation authority",
11456                        registration.device_id
11457                    )))
11458                }
11459            };
11460            if authority != &expected_authority {
11461                return Err(DbError::Message(format!(
11462                    "verified Store registration {} authority differs from the signed commit",
11463                    registration.device_id
11464                )));
11465            }
11466            let registration_bytes = registration.to_bytes();
11467            let registration_object =
11468                serde_json::to_string(&signed.registration).map_err(|error| {
11469                    DbError::Message(format!("serialize Store registration exact ref: {error}"))
11470                })?;
11471            let activation_authority = serde_json::to_string(authority).map_err(|error| {
11472                DbError::Message(format!("serialize Store registration authority: {error}"))
11473            })?;
11474            let inserted = conn
11475                .execute(
11476                    "INSERT INTO store_device_registration_activations
11477                     (device_id, registration_hash, author_pubkey, device_signing_pubkey,
11478                      registration_bytes, registration_object, activation_authority)
11479                     VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
11480                     ON CONFLICT(device_id) DO NOTHING",
11481                    rusqlite::params![
11482                        registration.device_id.to_string(),
11483                        signed.registration.registration_hash.to_string(),
11484                        registration.author_pubkey,
11485                        registration.device_signing_pubkey,
11486                        registration_bytes,
11487                        registration_object,
11488                        activation_authority,
11489                    ],
11490                )
11491                .map_err(DbError::from)?;
11492            if inserted == 0 {
11493                let existing: (String, Vec<u8>, String, String) = conn
11494                    .query_row(
11495                        "SELECT registration_hash, registration_bytes, registration_object,
11496                                activation_authority
11497                         FROM store_device_registration_activations WHERE device_id = ?1",
11498                        [registration.device_id.to_string()],
11499                        |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
11500                    )
11501                    .map_err(DbError::from)?;
11502                if existing
11503                    != (
11504                        signed.registration.registration_hash.to_string(),
11505                        registration.to_bytes(),
11506                        serde_json::to_string(&signed.registration).map_err(|error| {
11507                            DbError::Message(format!(
11508                                "serialize Store registration exact ref: {error}"
11509                            ))
11510                        })?,
11511                        serde_json::to_string(authority).map_err(|error| {
11512                            DbError::Message(format!(
11513                                "serialize Store registration authority: {error}"
11514                            ))
11515                        })?,
11516                    )
11517                {
11518                    return Err(DbError::Message(format!(
11519                        "Store device {} already has a different one-shot registration",
11520                        registration.device_id
11521                    )));
11522                }
11523            }
11524            let local: Option<LocalDeviceRegistrationJournalRow> = conn
11525                .query_row(
11526                    "SELECT device_id, registration_hash, registration_bytes, prepared_object, \
11527                            initial_ack_ref, initial_ack_bytes, initial_ack_prepared, state \
11528                     FROM local_store_device_registration WHERE singleton = 1",
11529                    [],
11530                    |row| {
11531                        Ok((
11532                            row.get(0)?,
11533                            row.get(1)?,
11534                            row.get(2)?,
11535                            row.get(3)?,
11536                            row.get(4)?,
11537                            row.get(5)?,
11538                            row.get(6)?,
11539                            row.get(7)?,
11540                        ))
11541                    },
11542                )
11543                .optional()
11544                .map_err(DbError::from)?;
11545            let Some((
11546                local_device,
11547                local_hash,
11548                local_bytes,
11549                local_prepared,
11550                local_ack_ref,
11551                local_ack_bytes,
11552                local_ack_prepared,
11553                local_state,
11554            )) = local
11555            else {
11556                continue;
11557            };
11558            if local_device != registration.device_id.to_string() {
11559                continue;
11560            }
11561            let local_prepared: PreparedExactObject = serde_json::from_str(&local_prepared)
11562                .map_err(|error| DbError::Message(format!("local registration object: {error}")))?;
11563            let local_ack_ref: StoreAckRef = serde_json::from_str(&local_ack_ref)
11564                .map_err(|error| DbError::Message(format!("local initial ack ref: {error}")))?;
11565            let local_ack_prepared: PreparedExactObject = serde_json::from_str(&local_ack_prepared)
11566                .map_err(|error| DbError::Message(format!("local initial ack object: {error}")))?;
11567            if local_hash != signed.registration.registration_hash.to_string()
11568                || local_bytes != registration.to_bytes()
11569                || local_prepared.reference() != &signed.registration.object
11570                || local_ack_prepared.reference() != &local_ack_ref.object
11571            {
11572                return Err(DbError::Message(
11573                    "activating commit differs from the complete local registration ref"
11574                        .to_string(),
11575                ));
11576            }
11577            let ack = StoreAck::parse_at(
11578                &local_ack_bytes,
11579                registration.store_root.store_root_hash,
11580                &local_ack_ref,
11581                registration,
11582            )
11583            .map_err(|error| DbError::Message(format!("local initial ack: {error}")))?;
11584            if ack.revision != 1
11585                || ack.predecessor.is_some()
11586                || local_ack_prepared
11587                    .reference()
11588                    .verify(local_ack_prepared.stored_bytes())
11589                    .is_err()
11590            {
11591                return Err(DbError::Message(
11592                    "local registration journal does not carry an initial acknowledgement"
11593                        .to_string(),
11594                ));
11595            }
11596            let state: LocalDeviceRegistrationState =
11597                serde_json::from_str(&local_state).map_err(|error| {
11598                    DbError::Message(format!("local registration journal: {error}"))
11599                })?;
11600            let activated_state = LocalDeviceRegistrationState::Activated {
11601                authority: authority.clone(),
11602            };
11603            match state {
11604                LocalDeviceRegistrationState::Prepared => {
11605                    return Err(DbError::Message(
11606                        "Store commit cannot activate a registration before exact creation"
11607                            .to_string(),
11608                    ));
11609                }
11610                LocalDeviceRegistrationState::Created => {
11611                    let updated = conn
11612                        .execute(
11613                            "UPDATE local_store_device_registration SET state = ?1 \
11614                             WHERE singleton = 1 AND device_id = ?2 AND registration_hash = ?3 \
11615                               AND initial_ack_ref = ?4 AND state = ?5",
11616                            rusqlite::params![
11617                                serde_json::to_string(&activated_state).map_err(|error| {
11618                                    DbError::Message(format!(
11619                                        "serialize activated journal: {error}"
11620                                    ))
11621                                })?,
11622                                local_device,
11623                                local_hash,
11624                                serde_json::to_string(&local_ack_ref).map_err(|error| {
11625                                    DbError::Message(format!(
11626                                        "serialize local initial ack: {error}"
11627                                    ))
11628                                })?,
11629                                serde_json::to_string(&LocalDeviceRegistrationState::Created)
11630                                    .map_err(|error| DbError::Message(format!(
11631                                        "serialize created journal: {error}"
11632                                    )))?,
11633                            ],
11634                        )
11635                        .map_err(DbError::from)?;
11636                    if updated != 1 {
11637                        return Err(DbError::Message(
11638                            "local registration journal changed during activation".to_string(),
11639                        ));
11640                    }
11641                    conn.execute(
11642                        "INSERT INTO published_store_acks (singleton, ack_ref, successor_slot) \
11643                         VALUES (1, ?1, ?2)",
11644                        rusqlite::params![
11645                            serde_json::to_string(&local_ack_ref).map_err(|error| {
11646                                DbError::Message(format!(
11647                                    "serialize activated initial ack: {error}"
11648                                ))
11649                            })?,
11650                            serde_json::to_string(&ack.successor.next_slot).map_err(|error| {
11651                                DbError::Message(format!(
11652                                    "serialize activated ack successor: {error}"
11653                                ))
11654                            })?,
11655                        ],
11656                    )
11657                    .map_err(DbError::from)?;
11658                    conn.execute(
11659                        "INSERT INTO protocol_state (key, value) VALUES (?1, ?2) \
11660                         ON CONFLICT(key) DO UPDATE SET value = excluded.value",
11661                        (
11662                            LOCAL_DEVICE_ID_STATE_KEY,
11663                            registration.device_id.to_string(),
11664                        ),
11665                    )
11666                    .map_err(DbError::from)?;
11667                }
11668                LocalDeviceRegistrationState::Activated {
11669                    authority: existing,
11670                } if existing == *authority => {
11671                    let stored_ack: (String, String) = conn
11672                        .query_row(
11673                            "SELECT ack_ref, successor_slot FROM published_store_acks \
11674                             WHERE singleton = 1",
11675                            [],
11676                            |row| Ok((row.get(0)?, row.get(1)?)),
11677                        )
11678                        .map_err(DbError::from)?;
11679                    let local_device_id: Option<String> = conn
11680                        .query_row(
11681                            "SELECT value FROM protocol_state WHERE key = ?1",
11682                            [LOCAL_DEVICE_ID_STATE_KEY],
11683                            |row| row.get(0),
11684                        )
11685                        .optional()
11686                        .map_err(DbError::from)?;
11687                    if stored_ack.0
11688                        != serde_json::to_string(&local_ack_ref).map_err(|error| {
11689                            DbError::Message(format!("serialize replayed initial ack: {error}"))
11690                        })?
11691                        || stored_ack.1
11692                            != serde_json::to_string(&ack.successor.next_slot).map_err(|error| {
11693                                DbError::Message(format!(
11694                                    "serialize replayed ack successor: {error}"
11695                                ))
11696                            })?
11697                        || local_device_id.as_deref() != Some(local_device.as_str())
11698                    {
11699                        return Err(DbError::Message(
11700                            "activated local journal differs from its exact initial ack"
11701                                .to_string(),
11702                        ));
11703                    }
11704                }
11705                LocalDeviceRegistrationState::Activated { .. } => {
11706                    return Err(DbError::Message(
11707                        "local registration already has another exact activation authority"
11708                            .to_string(),
11709                    ));
11710                }
11711                LocalDeviceRegistrationState::Retired { .. } => {
11712                    return Err(DbError::Message(
11713                        "retired local registration cannot be activated again".to_string(),
11714                    ));
11715                }
11716            }
11717        }
11718        Ok(())
11719    }
11720
11721    #[cfg(test)]
11722    pub(crate) async fn activated_store_device_registrations(
11723        &self,
11724    ) -> Result<Vec<StoreDeviceRegistration>, DbError> {
11725        Ok(self
11726            .activated_store_device_registration_records()
11727            .await?
11728            .into_iter()
11729            .map(|(_, registration)| registration)
11730            .collect())
11731    }
11732
11733    pub(crate) async fn store_device_state_for_order(
11734        &self,
11735        order: &crate::sync::store_commit::StoreCommitOrder,
11736    ) -> Result<(StoreDeviceStateRef, ResolvedStoreDeviceState), DbError> {
11737        let order = order.clone();
11738        self.call(move |conn| {
11739            let genesis = load_store_device_genesis_state_on(conn)?;
11740            match order {
11741                crate::sync::store_commit::StoreCommitOrder::MergeConcurrent {
11742                    predecessor,
11743                    dependencies,
11744                    ..
11745                } => {
11746                    let mut frontier = dependencies;
11747                    if let Some(predecessor) = predecessor {
11748                        let StoreCommitCoord::MergeConcurrent { stream_id, .. } = predecessor.coord
11749                        else {
11750                            return Err(DbError::Message(
11751                                "Merge predecessor has a Serial coordinate".to_string(),
11752                            ));
11753                        };
11754                        if frontier
11755                            .insert(stream_id, predecessor.clone())
11756                            .is_some_and(|current| current != predecessor)
11757                        {
11758                            return Err(DbError::Message(
11759                                "Merge predecessor differs from the same-stream dependency"
11760                                    .to_string(),
11761                            ));
11762                        }
11763                    }
11764                    let state = if frontier.is_empty() {
11765                        genesis
11766                    } else {
11767                        ResolvedStoreDeviceState::merge(
11768                            frontier
11769                                .values()
11770                                .map(|reference| load_store_device_snapshot_on(conn, reference))
11771                                .collect::<Result<Vec<_>, _>>()?,
11772                        )
11773                        .map_err(|error| DbError::Message(error.to_string()))?
11774                    };
11775                    let reference = StoreDeviceStateRef::merge_concurrent(
11776                        CommitFrontier::MergeConcurrent(frontier),
11777                        &state,
11778                    )
11779                    .map_err(|error| DbError::Message(error.to_string()))?;
11780                    Ok((reference, state))
11781                }
11782                crate::sync::store_commit::StoreCommitOrder::Serial { predecessor, .. } => {
11783                    let state = match &predecessor {
11784                        StoreSerialPredecessor::Genesis { .. } => genesis,
11785                        StoreSerialPredecessor::Commit(reference) => {
11786                            load_store_device_snapshot_on(conn, reference)?
11787                        }
11788                    };
11789                    let reference = StoreDeviceStateRef::serial(predecessor, &state)
11790                        .map_err(|error| DbError::Message(error.to_string()))?;
11791                    Ok((reference, state))
11792                }
11793            }
11794        })
11795        .await
11796    }
11797
11798    pub(crate) async fn activated_store_device_registration_records(
11799        &self,
11800    ) -> Result<Vec<(StoreDeviceRegistrationRef, StoreDeviceRegistration)>, DbError> {
11801        let root = self
11802            .local_store_root_ref()
11803            .await?
11804            .ok_or_else(DbError::missing_store_root_hash)?;
11805        self.call(move |conn| {
11806            let mut statement = conn
11807                .prepare(
11808                    "SELECT device_id, registration_hash, registration_bytes,
11809                            registration_object
11810                     FROM store_device_registration_activations ORDER BY device_id",
11811                )
11812                .map_err(DbError::from)?;
11813                let rows = statement
11814                .query_map([], |row| {
11815                    Ok((
11816                        row.get::<_, String>(0)?,
11817                        row.get::<_, String>(1)?,
11818                        row.get::<_, Vec<u8>>(2)?,
11819                        row.get::<_, String>(3)?,
11820                    ))
11821                })
11822                .map_err(DbError::from)?;
11823            rows
11824                .map(|row| {
11825                    let (device_id, registration_hash, bytes, object) =
11826                        row.map_err(DbError::from)?;
11827                    let device_id = device_id.parse().map_err(|error| {
11828                        DbError::Message(format!("activated Store device id: {error}"))
11829                    })?;
11830                    let registration_hash = registration_hash.parse().map_err(|error| {
11831                        DbError::Message(format!(
11832                            "activated Store device registration hash: {error}"
11833                        ))
11834                    })?;
11835                    let reference: StoreDeviceRegistrationRef =
11836                        serde_json::from_str(&object).map_err(|error| {
11837                        DbError::Message(format!(
11838                            "activated Store device exact reference: {error}"
11839                        ))
11840                    })?;
11841                    if reference.device_id != device_id
11842                        || reference.registration_hash != registration_hash
11843                    {
11844                        return Err(DbError::Message(
11845                            "activated Store registration columns differ from its exact reference"
11846                                .to_string(),
11847                        ));
11848                    }
11849                    let registration = StoreDeviceRegistration::parse_at(&bytes, &root, device_id)
11850                        .map_err(|error| {
11851                            DbError::Message(format!(
11852                                "activated Store device registration {device_id}: {error}"
11853                            ))
11854                        })?;
11855                    reference.verify_registration(&registration).map_err(|error| {
11856                        DbError::Message(format!(
11857                            "activated Store device registration {device_id} exact reference: {error}"
11858                        ))
11859                    })?;
11860                    Ok((reference, registration))
11861                })
11862                .collect::<Result<Vec<_>, DbError>>()
11863        })
11864        .await
11865    }
11866
11867    pub(crate) async fn activated_store_device_registration(
11868        &self,
11869        reference: StoreDeviceRegistrationRef,
11870    ) -> Result<StoreDeviceRegistration, DbError> {
11871        let root = self.local_store_root_ref().await?.ok_or_else(|| {
11872            DbError::Message("Store root is absent while loading an activated device".to_string())
11873        })?;
11874        self.call(move |conn| load_activated_registration_on(conn, &root, &reference))
11875            .await
11876    }
11877
11878    pub(crate) async fn local_blob_write_authority(
11879        &self,
11880    ) -> Result<(StoreDeviceRegistrationRef, StoreDeviceRegistration), DbError> {
11881        self.call(|conn| {
11882            local_store_authority_on(conn)
11883                .map(|(_, reference, registration)| (reference, registration))
11884        })
11885        .await
11886    }
11887
11888    pub(crate) async fn activated_store_device_registration_with_authority(
11889        &self,
11890        reference: StoreDeviceRegistrationRef,
11891    ) -> Result<
11892        (
11893            StoreDeviceRegistration,
11894            crate::sync::store_commit::StoreDeviceRegistrationActivation,
11895        ),
11896        DbError,
11897    > {
11898        let root = self.local_store_root_ref().await?.ok_or_else(|| {
11899            DbError::Message("Store root is absent while loading an activated device".to_string())
11900        })?;
11901        self.call(move |conn| {
11902            let registration = load_activated_registration_on(conn, &root, &reference)?;
11903            let authority: String = conn
11904                .query_row(
11905                    "SELECT activation_authority FROM store_device_registration_activations \
11906                     WHERE device_id = ?1 AND registration_hash = ?2",
11907                    (
11908                        reference.device_id.to_string(),
11909                        reference.registration_hash.to_string(),
11910                    ),
11911                    |row| row.get(0),
11912                )
11913                .map_err(DbError::from)?;
11914            let authority = serde_json::from_str(&authority).map_err(|error| {
11915                DbError::Message(format!("activated Store registration authority: {error}"))
11916            })?;
11917            Ok((registration, authority))
11918        })
11919        .await
11920    }
11921
11922    pub(crate) async fn activated_store_device_registration_for_device(
11923        &self,
11924        device_id: crate::sync::store_commit::StoreDeviceId,
11925    ) -> Result<
11926        Option<(
11927            StoreDeviceRegistrationRef,
11928            StoreDeviceRegistration,
11929            crate::sync::store_commit::StoreDeviceRegistrationActivation,
11930        )>,
11931        DbError,
11932    > {
11933        let root = self.local_store_root_ref().await?.ok_or_else(|| {
11934            DbError::Message("Store root is absent while loading an activated device".to_string())
11935        })?;
11936        self.call(move |conn| {
11937            let stored: Option<(String, String)> = conn
11938                .query_row(
11939                    "SELECT registration_object, activation_authority \
11940                     FROM store_device_registration_activations WHERE device_id = ?1",
11941                    [device_id.to_string()],
11942                    |row| Ok((row.get(0)?, row.get(1)?)),
11943                )
11944                .optional()
11945                .map_err(DbError::from)?;
11946            let Some((reference, authority)) = stored else {
11947                return Ok(None);
11948            };
11949            let reference: StoreDeviceRegistrationRef =
11950                serde_json::from_str(&reference).map_err(|error| {
11951                    DbError::Message(format!("activated Store registration ref: {error}"))
11952                })?;
11953            if reference.device_id != device_id {
11954                return Err(DbError::Message(
11955                    "activated Store registration row names another device".to_string(),
11956                ));
11957            }
11958            let registration = load_activated_registration_on(conn, &root, &reference)?;
11959            let authority = serde_json::from_str(&authority).map_err(|error| {
11960                DbError::Message(format!("activated Store registration authority: {error}"))
11961            })?;
11962            Ok(Some((reference, registration, authority)))
11963        })
11964        .await
11965    }
11966
11967    pub(crate) fn record_materialized_commit_on(
11968        conn: &Connection,
11969        commit: &StoreBatchCommit,
11970        commit_ref: &StoreBatchCommitRef,
11971    ) -> Result<(), DbError> {
11972        commit_ref
11973            .verify_commit(commit)
11974            .map_err(|error| DbError::Message(error.to_string()))?;
11975        let stored_registration: String = conn
11976            .query_row(
11977                "SELECT registration_object FROM store_device_registration_activations \
11978                 WHERE device_id = ?1 AND registration_hash = ?2",
11979                (
11980                    commit.author_registration.device_id.to_string(),
11981                    commit.author_registration.registration_hash.to_string(),
11982                ),
11983                |row| row.get(0),
11984            )
11985            .map_err(DbError::from)?;
11986        let stored_registration: StoreDeviceRegistrationRef =
11987            serde_json::from_str(&stored_registration).map_err(|error| {
11988                DbError::Message(format!("materialized author registration ref: {error}"))
11989            })?;
11990        if stored_registration != commit.author_registration {
11991            return Err(DbError::Message(
11992                "materialized commit author registration differs from its activation".to_string(),
11993            ));
11994        }
11995        let root = required_store_root_authority_on(conn)?;
11996        if root.store_root_hash != commit.store_root_hash {
11997            return Err(DbError::Message(
11998                "materialized commit belongs to a different Store root".to_string(),
11999            ));
12000        }
12001        let expected_stream =
12002            AuthorStreamId::store_announcements(&root, &commit.author_registration);
12003        let (stream_id, sequence) = match commit_ref.coord {
12004            StoreCommitCoord::MergeConcurrent {
12005                stream_id,
12006                sequence,
12007            } if stream_id == expected_stream => (stream_id.to_string(), sequence),
12008            StoreCommitCoord::MergeConcurrent { .. } => {
12009                return Err(DbError::Message(
12010                    "Merge materialization stream differs from its exact author registration"
12011                        .to_string(),
12012                ));
12013            }
12014            StoreCommitCoord::Serial { sequence } => (SERIAL_STREAM_ID.to_string(), sequence),
12015        };
12016        if sequence != commit.seq() || commit_ref.coord.policy() != commit.policy() {
12017            return Err(DbError::Message(
12018                "materialization coordinate differs from its signed commit".to_string(),
12019            ));
12020        }
12021        let predecessor = if commit.seq() == 1 {
12022            None
12023        } else if let Some(reference) =
12024            Self::materialized_commit_ref_on(conn, &stream_id, commit.seq() - 1)?
12025        {
12026            Some(reference)
12027        } else {
12028            conn.query_row(
12029                "SELECT commit_ref FROM snapshot_coverage \
12030                 WHERE device_id = ?1 AND seq = ?2",
12031                (
12032                    &stream_id,
12033                    Self::sequence_to_sqlite(&stream_id, commit.seq() - 1)?,
12034                ),
12035                |row| row.get::<_, String>(0),
12036            )
12037            .optional()
12038            .map_err(DbError::from)?
12039            .map(|reference| {
12040                serde_json::from_str(&reference).map_err(|error| {
12041                    DbError::Message(format!("snapshot coverage exact commit ref: {error}"))
12042                })
12043            })
12044            .transpose()?
12045        };
12046        if predecessor.as_ref() != commit.order.predecessor() {
12047            return Err(DbError::Message(format!(
12048                "Store commit {}/{} names predecessor {:?}, durable predecessor is {:?}",
12049                stream_id,
12050                commit.seq(),
12051                commit.order.predecessor(),
12052                predecessor
12053            )));
12054        }
12055        let mut device_state = load_declared_store_device_state_on(conn, &commit.device_state)?;
12056        let recovery_author = commit.device_registrations().iter().find_map(|activation| {
12057            if activation.registration != commit.author_registration {
12058                return None;
12059            }
12060            let crate::sync::store_commit::StoreDeviceRegistrationActivationRef::Recovery {
12061                node,
12062                ..
12063            } = &activation.authority
12064            else {
12065                return None;
12066            };
12067            let registration =
12068                load_activated_registration_on(conn, &root, &activation.registration).ok()?;
12069            let crate::sync::store_commit::StoreDeviceRegistrationOrigin::Recovery {
12070                owner_grant,
12071                ..
12072            } = registration.origin
12073            else {
12074                return None;
12075            };
12076            Some((
12077                activation.registration.clone(),
12078                crate::sync::store_commit::OwnerRecoveryCursor {
12079                    owner_grant,
12080                    position: crate::sync::store_commit::OwnerRecoveryPosition::At {
12081                        node: node.clone(),
12082                    },
12083                },
12084            ))
12085        });
12086        if let Some((registration, recovery)) = &recovery_author {
12087            device_state = device_state
12088                .activate_registration(registration.clone(), Some(recovery.clone()))
12089                .map_err(|error| DbError::Message(error.to_string()))?;
12090        }
12091        let active_author = device_state
12092            .devices
12093            .get(&commit.author_registration.device_id)
12094            .is_some_and(|record| {
12095                record.registration == commit.author_registration
12096                    && matches!(
12097                        record.status,
12098                        crate::sync::store_commit::StoreDeviceStatus::Active
12099                    )
12100            });
12101        if !active_author {
12102            return Err(DbError::Message(
12103                "materialized commit author is not active at its exact predecessor state".into(),
12104            ));
12105        }
12106        for activation in commit.device_registrations() {
12107            if recovery_author
12108                .as_ref()
12109                .is_some_and(|(registration, _)| registration == &activation.registration)
12110            {
12111                continue;
12112            }
12113            device_state = device_state
12114                .activate_registration(activation.registration.clone(), None)
12115                .map_err(|error| DbError::Message(error.to_string()))?;
12116        }
12117        for retirement in commit.device_retirements() {
12118            device_state = device_state
12119                .self_retire(retirement.clone())
12120                .map_err(|error| DbError::Message(error.to_string()))?;
12121        }
12122        let seq = Self::sequence_to_sqlite(&stream_id, commit.seq())?;
12123        let commit_ref_json = serde_json::to_string(commit_ref).map_err(|error| {
12124            DbError::Message(format!("serialize exact Store commit ref: {error}"))
12125        })?;
12126        conn.execute(
12127            "INSERT INTO store_device_state_snapshots (commit_ref, state) VALUES (?1, ?2)",
12128            rusqlite::params![
12129                &commit_ref_json,
12130                serde_json::to_string(&device_state).map_err(|error| {
12131                    DbError::Message(format!(
12132                        "serialize materialized Store device state: {error}"
12133                    ))
12134                })?,
12135            ],
12136        )
12137        .map_err(DbError::from)?;
12138        conn.execute(
12139            "INSERT INTO materialized_commits (device_id, seq, commit_ref) \
12140             VALUES (?1, ?2, ?3)",
12141            (&stream_id, seq, commit_ref_json),
12142        )
12143        .map(|_| ())
12144        .map_err(DbError::from)
12145    }
12146
12147    pub(crate) fn record_verified_circle_activations_on(
12148        conn: &Connection,
12149        commit: &StoreBatchCommit,
12150        commit_ref: &StoreBatchCommitRef,
12151        activations: &[crate::sync::circle_ops::VerifiedCircleReference],
12152    ) -> Result<(), DbError> {
12153        if activations.len() != commit.circle_controls().len() {
12154            return Err(DbError::Message(
12155                "verified circle activations do not cover every control reference".to_string(),
12156            ));
12157        }
12158        commit_ref
12159            .verify_commit(commit)
12160            .map_err(|error| DbError::Message(error.to_string()))?;
12161        let stream_id = match &commit_ref.coord {
12162            StoreCommitCoord::MergeConcurrent { stream_id, .. } => stream_id.to_string(),
12163            StoreCommitCoord::Serial { .. } => SERIAL_STREAM_ID.to_string(),
12164        };
12165        let seq = Self::sequence_to_sqlite(&stream_id, commit_ref.coord.sequence())?;
12166        for activation in activations {
12167            if !commit.circle_controls().contains(&activation.reference)
12168                || activation.reference.circle_id() != activation.circle_id
12169                || activation.reference.control() != &activation.control.coord
12170                || !activation.control.verify()
12171            {
12172                return Err(DbError::Message(
12173                    "verified circle activation differs from Store control reference".to_string(),
12174                ));
12175            }
12176            let circle_id = activation.circle_id.to_string();
12177            if let Some(access) = &activation.local_access {
12178                let leaf = &access.leaf.value;
12179                if activation
12180                    .control
12181                    .value
12182                    .owners
12183                    .binary_search(&leaf.owner_pubkey)
12184                    .is_err()
12185                {
12186                    return Err(DbError::Message(format!(
12187                        "circle {circle_id} local access signer is not a control Owner"
12188                    )));
12189                }
12190                match (&leaf.disposition, &access.active) {
12191                    (crate::sync::circle::CircleAccessDisposition::Active { .. }, Some(_))
12192                    | (crate::sync::circle::CircleAccessDisposition::Inactive, None) => {}
12193                    _ => {
12194                        return Err(DbError::Message(format!(
12195                            "circle {circle_id} access state differs from its disposition"
12196                        )));
12197                    }
12198                }
12199            }
12200            let mut statement = conn
12201                .prepare(
12202                    "SELECT control_bytes FROM circle_control_activations
12203                     WHERE circle_id = ?1",
12204                )
12205                .map_err(DbError::from)?;
12206            let rows = statement
12207                .query_map([&circle_id], |row| row.get::<_, Vec<u8>>(0))
12208                .map_err(DbError::from)?;
12209            let mut existing_controls = Vec::new();
12210            for bytes in rows {
12211                let bytes = bytes.map_err(DbError::from)?;
12212                let control: crate::sync::circle::CircleControl = serde_json::from_slice(&bytes)
12213                    .map_err(|error| {
12214                        DbError::Message(format!("parse activated circle control: {error}"))
12215                    })?;
12216                existing_controls.push(control);
12217            }
12218            drop(statement);
12219            match activation.control.value.order.previous_control_hash() {
12220                None if !existing_controls.is_empty() => {
12221                    return Err(DbError::Message(format!(
12222                        "circle {circle_id} already has a founder control"
12223                    )));
12224                }
12225                None => {}
12226                Some(previous_hash) => {
12227                    let previous = existing_controls
12228                        .iter()
12229                        .find(|control| control.control_hash() == previous_hash)
12230                        .ok_or_else(|| {
12231                            DbError::Message(format!(
12232                                "circle {circle_id} control predecessor {previous_hash} is absent"
12233                            ))
12234                        })?;
12235                    if previous.store_root_hash != activation.control.value.store_root_hash
12236                        || previous.circle_id != activation.circle_id
12237                        || previous
12238                            .owners
12239                            .binary_search(&activation.control.value.author_pubkey)
12240                            .is_err()
12241                        || activation.control.value.order.ordinal()
12242                            != previous.order.ordinal().checked_add(1).ok_or_else(|| {
12243                                DbError::Message("circle control ordinal overflow".to_string())
12244                            })?
12245                    {
12246                        return Err(DbError::Message(format!(
12247                            "circle {circle_id} control does not extend its authorized predecessor"
12248                        )));
12249                    }
12250                }
12251            }
12252            let control_coord =
12253                serde_json::to_string(&activation.control.coord).map_err(|error| {
12254                    DbError::Message(format!("serialize circle control coordinate: {error}"))
12255                })?;
12256            conn.execute(
12257                "INSERT INTO circle_control_activations
12258                 (circle_id, control_coord, stream_id, seq, commit_hash, control_bytes)
12259                 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
12260                rusqlite::params![
12261                    &circle_id,
12262                    &control_coord,
12263                    stream_id,
12264                    seq,
12265                    commit.commit_hash().to_string(),
12266                    &activation.control.bytes,
12267                ],
12268            )
12269            .map_err(DbError::from)?;
12270            let Some(access) = &activation.local_access else {
12271                continue;
12272            };
12273            let disposition = match access.leaf.value.disposition {
12274                crate::sync::circle::CircleAccessDisposition::Active { .. } => "active",
12275                crate::sync::circle::CircleAccessDisposition::Inactive => "inactive",
12276            };
12277            conn.execute(
12278                "INSERT INTO circle_access_cache
12279                 (circle_id, control_coord, owner_pubkey, disposition, access_bytes)
12280                 VALUES (?1, ?2, ?3, ?4, ?5)",
12281                rusqlite::params![
12282                    &circle_id,
12283                    &control_coord,
12284                    &access.leaf.value.owner_pubkey,
12285                    disposition,
12286                    &access.leaf.bytes,
12287                ],
12288            )
12289            .map_err(DbError::from)?;
12290            if let Some(active) = &access.active {
12291                let roster_bytes = serde_json::to_vec(&active.roster).map_err(|error| {
12292                    DbError::Message(format!("serialize activated circle roster: {error}"))
12293                })?;
12294                conn.execute(
12295                    "INSERT INTO circle_roster_cache (circle_id, control_coord, roster_bytes)
12296                     VALUES (?1, ?2, ?3)",
12297                    rusqlite::params![&circle_id, &control_coord, roster_bytes,],
12298                )
12299                .map_err(DbError::from)?;
12300                let metadata_bytes = serde_json::to_vec(&active.metadata).map_err(|error| {
12301                    DbError::Message(format!("serialize activated circle metadata: {error}"))
12302                })?;
12303                conn.execute(
12304                    "INSERT INTO circle_metadata_cache
12305                     (circle_id, control_coord, metadata_bytes) VALUES (?1, ?2, ?3)",
12306                    rusqlite::params![&circle_id, &control_coord, metadata_bytes,],
12307                )
12308                .map_err(DbError::from)?;
12309            }
12310        }
12311        Ok(())
12312    }
12313
12314    pub(crate) fn record_materialized_serial_commit_on(
12315        conn: &Connection,
12316        commit: &StoreBatchCommit,
12317        commit_ref: &StoreBatchCommitRef,
12318        authorization: &SerialAuthorizationState,
12319    ) -> Result<(), DbError> {
12320        if commit.policy() != WritePolicy::Serial {
12321            return Err(DbError::Message(
12322                "Serial membership state cannot accompany a MergeConcurrent commit".to_string(),
12323            ));
12324        }
12325        Self::record_materialized_commit_on(conn, commit, commit_ref)?;
12326        let membership = serde_json::to_string(&authorization.membership).map_err(|error| {
12327            DbError::Message(format!("serialize Serial membership state: {error}"))
12328        })?;
12329        let provider_admin =
12330            serde_json::to_string(&authorization.provider_admin).map_err(|error| {
12331                DbError::Message(format!(
12332                    "serialize Serial provider administrator state: {error}"
12333                ))
12334            })?;
12335        conn.execute(
12336            "INSERT INTO protocol_state (key, value) VALUES (?1, ?2) \
12337             ON CONFLICT(key) DO UPDATE SET value = excluded.value",
12338            (SERIAL_MEMBERSHIP_STATE_KEY, membership),
12339        )
12340        .map_err(DbError::from)?;
12341        conn.execute(
12342            "INSERT INTO protocol_state (key, value) VALUES (?1, ?2) \
12343             ON CONFLICT(key) DO UPDATE SET value = excluded.value",
12344            (SERIAL_PROVIDER_ADMIN_STATE_KEY, provider_admin),
12345        )
12346        .map_err(DbError::from)?;
12347        conn.execute(
12348            "INSERT INTO protocol_state (key, value) VALUES (?1, ?2) \
12349             ON CONFLICT(key) DO UPDATE SET value = excluded.value",
12350            (
12351                SERIAL_KEY_GENERATION_STATE_KEY,
12352                authorization.key_generation.to_string(),
12353            ),
12354        )
12355        .map(|_| ())
12356        .map_err(DbError::from)
12357    }
12358
12359    pub async fn serial_membership_state(&self) -> Result<Option<SerialMembershipState>, DbError> {
12360        let Some(raw) = self.get_protocol_state(SERIAL_MEMBERSHIP_STATE_KEY).await? else {
12361            return Ok(None);
12362        };
12363        serde_json::from_str(&raw)
12364            .map(Some)
12365            .map_err(|error| DbError::Message(format!("parse Serial membership state: {error}")))
12366    }
12367
12368    pub async fn serial_authorization_state(
12369        &self,
12370    ) -> Result<Option<SerialAuthorizationState>, DbError> {
12371        let membership = self.get_protocol_state(SERIAL_MEMBERSHIP_STATE_KEY).await?;
12372        let provider_admin = self
12373            .get_protocol_state(SERIAL_PROVIDER_ADMIN_STATE_KEY)
12374            .await?;
12375        let key_generation = self
12376            .get_protocol_state(SERIAL_KEY_GENERATION_STATE_KEY)
12377            .await?;
12378        match (membership, provider_admin, key_generation) {
12379            (None, None, None) => Ok(None),
12380            (Some(membership), Some(provider_admin), Some(key_generation)) => {
12381                let membership = serde_json::from_str(&membership).map_err(|error| {
12382                    DbError::Message(format!("parse Serial membership state: {error}"))
12383                })?;
12384                let provider_admin: ProviderAdminState = serde_json::from_str(&provider_admin)
12385                    .map_err(|error| {
12386                        DbError::Message(format!(
12387                            "parse Serial provider administrator state: {error}"
12388                        ))
12389                    })?;
12390                let key_generation = key_generation.parse::<u64>().map_err(|error| {
12391                    DbError::Message(format!("parse Serial key generation: {error}"))
12392                })?;
12393                Ok(Some(SerialAuthorizationState {
12394                    membership,
12395                    provider_admin,
12396                    key_generation,
12397                }))
12398            }
12399            _ => Err(DbError::Message(
12400                "Serial authorization state is only partially durable".to_string(),
12401            )),
12402        }
12403    }
12404
12405    pub async fn serial_key_generation(&self) -> Result<Option<u64>, DbError> {
12406        let Some(raw) = self
12407            .get_protocol_state(SERIAL_KEY_GENERATION_STATE_KEY)
12408            .await?
12409        else {
12410            return Ok(None);
12411        };
12412        raw.parse::<u64>()
12413            .map(Some)
12414            .map_err(|error| DbError::Message(format!("parse Serial key generation: {error}")))
12415    }
12416
12417    fn install_serial_root_authorization_on(
12418        conn: &Connection,
12419        founder_pubkey: &str,
12420        authorization: &SerialAuthorizationState,
12421    ) -> Result<(), DbError> {
12422        if Self::latest_position_for_device_on(conn, SERIAL_STREAM_ID)?.is_some() {
12423            return Err(DbError::Message(
12424                "cannot install founder-only Serial authorization after a materialized commit"
12425                    .to_string(),
12426            ));
12427        }
12428        let existing_state: i64 = conn
12429            .query_row(
12430                "SELECT COUNT(*) FROM protocol_state WHERE key IN (?1, ?2, ?3, ?4)",
12431                rusqlite::params![
12432                    crate::sync::membership_ops::OWNER_PUBKEY_STATE_KEY,
12433                    SERIAL_MEMBERSHIP_STATE_KEY,
12434                    SERIAL_KEY_GENERATION_STATE_KEY,
12435                    SERIAL_PROVIDER_ADMIN_STATE_KEY,
12436                ],
12437                |row| row.get(0),
12438            )
12439            .map_err(DbError::from)?;
12440        if existing_state != 0 {
12441            return Err(DbError::Message(
12442                "cannot install founder-only Serial authorization over existing state".to_string(),
12443            ));
12444        }
12445        let membership = serde_json::to_string(&authorization.membership).map_err(|error| {
12446            DbError::Message(format!(
12447                "serialize Serial founder membership state: {error}"
12448            ))
12449        })?;
12450        let provider_admin =
12451            serde_json::to_string(&authorization.provider_admin).map_err(|error| {
12452                DbError::Message(format!(
12453                    "serialize Serial founder provider administrator state: {error}"
12454                ))
12455            })?;
12456        for (key, value) in [
12457            (
12458                crate::sync::membership_ops::OWNER_PUBKEY_STATE_KEY,
12459                founder_pubkey.to_string(),
12460            ),
12461            (SERIAL_MEMBERSHIP_STATE_KEY, membership),
12462            (SERIAL_PROVIDER_ADMIN_STATE_KEY, provider_admin),
12463            (
12464                SERIAL_KEY_GENERATION_STATE_KEY,
12465                authorization.key_generation.to_string(),
12466            ),
12467        ] {
12468            conn.execute(
12469                "INSERT INTO protocol_state (key, value) VALUES (?1, ?2) \
12470                 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
12471                (key, value),
12472            )
12473            .map_err(DbError::from)?;
12474        }
12475        Ok(())
12476    }
12477
12478    pub(crate) async fn install_serial_root_authorization(
12479        &self,
12480        founder_pubkey: String,
12481        authorization: SerialAuthorizationState,
12482    ) -> Result<(), DbError> {
12483        self.call(move |conn| {
12484            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
12485            Self::install_serial_root_authorization_on(&tx, &founder_pubkey, &authorization)?;
12486            tx.commit().map_err(DbError::from)
12487        })
12488        .await
12489    }
12490
12491    pub(crate) async fn materialize_serial_control_commit(
12492        &self,
12493        commit: StoreBatchCommit,
12494        commit_ref: StoreBatchCommitRef,
12495        authorization_after: SerialAuthorizationState,
12496    ) -> Result<(), DbError> {
12497        if commit.control().is_none() || commit.store_package().is_some() {
12498            return Err(DbError::Message(
12499                "Serial control materialization requires a control-only Store batch".to_string(),
12500            ));
12501        }
12502        self.call(move |conn| {
12503            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
12504            Self::record_materialized_serial_commit_on(
12505                &tx,
12506                &commit,
12507                &commit_ref,
12508                &authorization_after,
12509            )?;
12510            tx.commit().map_err(DbError::from)
12511        })
12512        .await
12513    }
12514
12515    pub(crate) async fn install_serial_authorization_at_position(
12516        &self,
12517        expected: StoreBatchCommitRef,
12518        authorization: SerialAuthorizationState,
12519    ) -> Result<(), DbError> {
12520        self.call(move |conn| {
12521            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
12522            let actual = Self::latest_position_for_device_on(&tx, SERIAL_STREAM_ID)?;
12523            if actual.as_ref() != Some(&expected) {
12524                return Err(DbError::Message(format!(
12525                    "cannot install Serial authorization at {expected:?}; durable position is {actual:?}"
12526                )));
12527            }
12528            let membership = serde_json::to_string(&authorization.membership).map_err(|error| {
12529                DbError::Message(format!("serialize Serial membership state: {error}"))
12530            })?;
12531            let provider_admin = serde_json::to_string(&authorization.provider_admin).map_err(
12532                |error| {
12533                    DbError::Message(format!(
12534                        "serialize Serial provider administrator state: {error}"
12535                    ))
12536                },
12537            )?;
12538            tx.execute(
12539                "INSERT INTO protocol_state (key, value) VALUES (?1, ?2) \
12540                 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
12541                (SERIAL_MEMBERSHIP_STATE_KEY, membership),
12542            )
12543            .map_err(DbError::from)?;
12544            tx.execute(
12545                "INSERT INTO protocol_state (key, value) VALUES (?1, ?2) \
12546                 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
12547                (SERIAL_PROVIDER_ADMIN_STATE_KEY, provider_admin),
12548            )
12549            .map_err(DbError::from)?;
12550            tx.execute(
12551                "INSERT INTO protocol_state (key, value) VALUES (?1, ?2) \
12552                 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
12553                (
12554                    SERIAL_KEY_GENERATION_STATE_KEY,
12555                    authorization.key_generation.to_string(),
12556                ),
12557            )
12558            .map_err(DbError::from)?;
12559            tx.commit().map_err(DbError::from)
12560        })
12561        .await
12562    }
12563
12564    pub(crate) async fn install_bootstrap_state(
12565        &self,
12566        coverage: &CommitFrontier,
12567        snapshot: PublishedStoreSnapshot,
12568        store_root: crate::sync::store_objects::VerifiedObject<StoreProtocolRoot>,
12569        founder: crate::sync::store_objects::VerifiedObject<StoreDeviceRegistration>,
12570    ) -> Result<(), DbError> {
12571        if store_root.value.to_bytes() != store_root.bytes
12572            || store_root.value.object_hash() != store_root.semantic_hash
12573        {
12574            return Err(DbError::Message(
12575                "bootstrap Store root differs from its verified object".to_string(),
12576            ));
12577        }
12578        let store_root_ref = crate::sync::store_commit::StoreRootRef {
12579            store_root_id: store_root.value.descriptor.store_root_id(),
12580            store_root_hash: store_root.semantic_hash,
12581            object: store_root.object,
12582        };
12583        let founder_reference =
12584            StoreDeviceRegistrationRef::from_registration(&founder.value, founder.object.clone());
12585        if founder.semantic_hash != founder_reference.registration_hash {
12586            return Err(DbError::Message(
12587                "bootstrap founder semantic hash differs from its exact registration".to_string(),
12588            ));
12589        }
12590        let genesis = ResolvedStoreDeviceState::founder(
12591            &store_root_ref,
12592            founder_reference.clone(),
12593            &store_root.value.descriptor.founder_pubkey,
12594            store_root.value.descriptor.founder_grant.clone(),
12595            &store_root.value.descriptor.founder_recovery,
12596        )
12597        .map_err(|error| DbError::Message(error.to_string()))?;
12598        if coverage.policy() != self.write_policy() {
12599            return Err(DbError::Message(format!(
12600                "snapshot coverage uses {:?}, database uses {:?}",
12601                coverage.policy(),
12602                self.write_policy()
12603            )));
12604        }
12605        if snapshot.meta.coverage != *coverage
12606            || snapshot.meta.store_root_hash != store_root_ref.store_root_hash
12607            || snapshot.meta.snapshot_hash() != snapshot.reference.snapshot_hash
12608            || snapshot.meta.successor.next_slot != snapshot.successor_slot
12609        {
12610            return Err(DbError::Message(
12611                "bootstrap snapshot state differs from its exact signed metadata".to_string(),
12612            ));
12613        }
12614        let coverage = coverage.clone().into_refs();
12615        self.call(move |conn| {
12616            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
12617            validate_snapshot_object_owners_on(&tx, &store_root_ref, &snapshot.meta)?;
12618            install_store_root_authority_on(&tx, &store_root_ref, &store_root.bytes)?;
12619            install_store_founder_state_on(
12620                &tx,
12621                &store_root_ref,
12622                &founder_reference,
12623                &founder.value,
12624                &founder.bytes,
12625                &genesis,
12626            )?;
12627            tx.execute("DELETE FROM snapshot_coverage", [])
12628                .map_err(DbError::from)?;
12629            for (stream_id, reference) in coverage {
12630                let encoded = serde_json::to_string(&reference).map_err(|error| {
12631                    DbError::Message(format!("serialize snapshot exact commit ref: {error}"))
12632                })?;
12633                tx.execute(
12634                    "INSERT INTO snapshot_coverage \
12635                     (device_id, seq, commit_ref, snapshot_hash) VALUES (?1, ?2, ?3, ?4)",
12636                    (
12637                        &stream_id,
12638                        Self::sequence_to_sqlite(&stream_id, reference.coord.sequence())?,
12639                        encoded,
12640                        snapshot.reference.snapshot_hash.to_string(),
12641                    ),
12642                )
12643                .map_err(DbError::from)?;
12644            }
12645            tx.commit().map_err(DbError::from)
12646        })
12647        .await
12648    }
12649
12650    pub(crate) async fn install_device_join_bootstrap(
12651        &self,
12652        root: crate::sync::store_commit::StoreRootRef,
12653        plan: crate::sync::store_pull::DeviceJoinBootstrapPlan,
12654    ) -> Result<(), DbError> {
12655        if plan.coverage.policy() != self.write_policy() {
12656            return Err(DbError::Message(
12657                "device join bootstrap cut differs from the database write policy".to_string(),
12658            ));
12659        }
12660        self.call(move |conn| {
12661            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
12662            let installed_root = required_store_root_authority_on(&tx)?;
12663            if installed_root != root || plan.founder.store_root != root {
12664                return Err(DbError::Message(
12665                    "device join bootstrap root differs from the installed exact root".to_string(),
12666                ));
12667            }
12668            install_store_founder_state_on(
12669                &tx,
12670                &root,
12671                &plan.founder_reference,
12672                &plan.founder,
12673                &plan.founder_bytes,
12674                &plan.genesis,
12675            )?;
12676
12677            let coverage_refs = match plan.coverage.clone() {
12678                crate::sync::store_commit::StoreHistoryCut::MergeConcurrent(frontier) => frontier
12679                    .into_iter()
12680                    .map(|(stream_id, reference)| (stream_id.to_string(), reference))
12681                    .collect::<BTreeMap<_, _>>(),
12682                crate::sync::store_commit::StoreHistoryCut::Serial(
12683                    StoreSerialPredecessor::Commit(reference),
12684                ) => BTreeMap::from([(SERIAL_STREAM_ID.to_string(), reference)]),
12685                crate::sync::store_commit::StoreHistoryCut::Serial(
12686                    StoreSerialPredecessor::Genesis { .. },
12687                ) => BTreeMap::new(),
12688            };
12689            let mut coverage = BTreeMap::new();
12690            for (stream_id, reference) in coverage_refs {
12691                let sequence = reference.coord.sequence();
12692                let materialized = Self::materialized_commit_ref_on(&tx, &stream_id, sequence)?;
12693                if materialized.as_ref().is_some_and(|stored| stored != &reference) {
12694                    return Err(DbError::Message(format!(
12695                        "device join bootstrap conflicts with materialized commit at {stream_id}/{sequence}"
12696                    )));
12697                }
12698                let snapshot = tx
12699                    .query_row(
12700                        "SELECT commit_ref FROM snapshot_coverage
12701                         WHERE device_id = ?1 AND seq = ?2",
12702                        (
12703                            &stream_id,
12704                            Self::sequence_to_sqlite(&stream_id, sequence)?,
12705                        ),
12706                        |row| row.get::<_, String>(0),
12707                    )
12708                    .optional()
12709                    .map_err(DbError::from)?
12710                    .map(|encoded| Self::parse_stored_commit_ref(&stream_id, sequence, &encoded))
12711                    .transpose()?;
12712                if snapshot.as_ref().is_some_and(|stored| stored != &reference) {
12713                    return Err(DbError::Message(format!(
12714                        "device join bootstrap conflicts with snapshot coverage at {stream_id}/{sequence}"
12715                    )));
12716                }
12717                coverage.insert(
12718                    stream_id,
12719                    (reference, materialized.is_some() || snapshot.is_some()),
12720                );
12721            }
12722
12723            for prepared in &plan.commits {
12724                let stream_id = match &prepared.reference.coord {
12725                    StoreCommitCoord::MergeConcurrent { stream_id, .. } => stream_id.to_string(),
12726                    StoreCommitCoord::Serial { .. } => SERIAL_STREAM_ID.to_string(),
12727                };
12728                let already_represented = coverage.get(&stream_id).is_some_and(
12729                    |(reference, represented)| {
12730                        *represented
12731                            && prepared.reference.coord.sequence()
12732                                <= reference.coord.sequence()
12733                    },
12734                );
12735                if !already_represented
12736                    && (prepared.commit.store_package().is_some()
12737                        || !prepared.commit.circle_packages().is_empty())
12738                {
12739                    return Err(DbError::Message(format!(
12740                        "device join bootstrap cannot advance over unmaterialized row data at {stream_id}/{}",
12741                        prepared.reference.coord.sequence()
12742                    )));
12743                }
12744            }
12745
12746            for prepared in plan.commits {
12747                let stream_id = match &prepared.reference.coord {
12748                    StoreCommitCoord::MergeConcurrent { stream_id, .. } => stream_id.to_string(),
12749                    StoreCommitCoord::Serial { .. } => SERIAL_STREAM_ID.to_string(),
12750                };
12751                if coverage.get(&stream_id).is_some_and(|(reference, represented)| {
12752                    *represented
12753                        && prepared.reference.coord.sequence() <= reference.coord.sequence()
12754                }) {
12755                    continue;
12756                }
12757                if let Some(existing) = Self::materialized_commit_ref_on(
12758                    &tx,
12759                    &stream_id,
12760                    prepared.reference.coord.sequence(),
12761                )? {
12762                    if existing != prepared.reference {
12763                        return Err(DbError::Message(format!(
12764                            "device join bootstrap conflicts at {stream_id}/{}",
12765                            prepared.reference.coord.sequence()
12766                        )));
12767                    }
12768                    continue;
12769                }
12770                Self::record_activated_store_device_registrations_on(
12771                    &tx,
12772                    &prepared.commit,
12773                    &prepared.registrations,
12774                )?;
12775                Self::record_materialized_commit_on(&tx, &prepared.commit, &prepared.reference)?;
12776            }
12777            tx.commit().map_err(DbError::from)
12778        })
12779        .await
12780    }
12781
12782    fn sequence_from_sqlite(device_id: &str, value: i64) -> Result<u64, DbError> {
12783        let value = u64::try_from(value).map_err(|_| {
12784            DbError::Message(format!(
12785                "Store position for {device_id:?} contains negative sequence {value}"
12786            ))
12787        })?;
12788        if value == 0 {
12789            return Err(DbError::Message(format!(
12790                "Store position for {device_id:?} contains sequence zero"
12791            )));
12792        }
12793        Ok(value)
12794    }
12795
12796    fn sequence_to_sqlite(device_id: &str, value: u64) -> Result<i64, DbError> {
12797        if value == 0 {
12798            return Err(DbError::Message(format!(
12799                "Store position for {device_id:?} cannot use sequence zero"
12800            )));
12801        }
12802        i64::try_from(value).map_err(|_| {
12803            DbError::Message(format!(
12804                "Store position for {device_id:?} exceeds SQLite INTEGER"
12805            ))
12806        })
12807    }
12808
12809    pub(crate) fn persist_prepared_audience_objects_on(
12810        conn: &Connection,
12811        write_id: &WriteId,
12812        packages: &[PreparedAudiencePackage],
12813        blobs: &[PreparedAudienceBlob],
12814    ) -> Result<(), DbError> {
12815        let package_audiences = packages
12816            .iter()
12817            .map(|prepared| {
12818                if prepared.package().write_id() != write_id {
12819                    return Err(DbError::Message(format!(
12820                        "prepared audience package write {} differs from journal write {write_id}",
12821                        prepared.package().write_id()
12822                    )));
12823                }
12824                Ok(prepared.package().audience().remote_audience())
12825            })
12826            .collect::<Result<std::collections::BTreeSet<_>, DbError>>()?;
12827        if package_audiences.len() != packages.len() {
12828            return Err(DbError::Message(format!(
12829                "write {write_id} has duplicate prepared package audiences"
12830            )));
12831        }
12832        for prepared in packages {
12833            let audience = prepared.package().audience().remote_audience();
12834            validate_remote_object_on(
12835                conn,
12836                prepared.remote_object_id(),
12837                prepared.object(),
12838                prepared.semantic_bytes(),
12839                RemoteStoredRepresentationRef::Inline(prepared.stored_bytes()),
12840            )?;
12841            conn.execute(
12842                "INSERT INTO store_write_packages
12843                 (write_id, audience, remote_object_id)
12844                 VALUES (?1, ?2, ?3)
12845                 ON CONFLICT(write_id, audience) DO NOTHING",
12846                rusqlite::params![
12847                    write_id.as_str(),
12848                    remote_audience_to_db(&audience),
12849                    prepared.remote_object_id().to_string(),
12850                ],
12851            )
12852            .map_err(DbError::from)?;
12853            validate_prepared_package_on(conn, write_id, prepared)?;
12854        }
12855        for prepared in blobs {
12856            if !package_audiences.contains(prepared.audience()) {
12857                return Err(DbError::Message(format!(
12858                    "write {write_id} has a prepared blob for {:?} without that audience's package",
12859                    prepared.audience()
12860                )));
12861            }
12862            let locator = prepared.blob().locator();
12863            validate_remote_object_on(
12864                conn,
12865                prepared.remote_object_id(),
12866                prepared.blob().object(),
12867                &locator.to_bytes(),
12868                RemoteStoredRepresentationRef::Blob,
12869            )?;
12870            let locator_hash = locator.locator_hash();
12871            let spool_path = prepared
12872                .spool_path()
12873                .map(|path| {
12874                    path.to_str().map(str::to_string).ok_or_else(|| {
12875                        DbError::Message("prepared blob spool path is not UTF-8".to_string())
12876                    })
12877                })
12878                .transpose()?;
12879            conn.execute(
12880                "INSERT INTO store_write_blobs
12881                 (write_id, audience, locator_hash, remote_object_id, spool_path)
12882                 VALUES (?1, ?2, ?3, ?4, ?5)
12883                 ON CONFLICT(write_id, audience, remote_object_id) DO NOTHING",
12884                rusqlite::params![
12885                    write_id.as_str(),
12886                    remote_audience_to_db(prepared.audience()),
12887                    locator_hash.to_string(),
12888                    prepared.remote_object_id().to_string(),
12889                    spool_path,
12890                ],
12891            )
12892            .map_err(DbError::from)?;
12893            validate_prepared_blob_on(conn, write_id, prepared)?;
12894        }
12895        Ok(())
12896    }
12897
12898    fn persist_closed_write_objects_on(
12899        conn: &Connection,
12900        write_id: &WriteId,
12901        store_root_hash: ObjectHash,
12902        commit_ref: &StoreBatchCommitRef,
12903        commit: &StoreBatchCommit,
12904        commit_stored_bytes: &[u8],
12905        partitions: &PreparedStoreWritePartitions,
12906        remote_objects: &[RemoteObjectRecord],
12907        audiences: &PreparedAudienceObjects,
12908    ) -> Result<(), DbError> {
12909        let mut object_ids = std::collections::BTreeSet::new();
12910        for remote in remote_objects {
12911            remote
12912                .validate()
12913                .map_err(|error| DbError::Message(format!("prepared remote object: {error}")))?;
12914            if !object_ids.insert(remote.object_id()) {
12915                return Err(DbError::Message(
12916                    "prepared write contains a duplicate remote object".to_string(),
12917                ));
12918            }
12919        }
12920        validate_prepared_audience_blob_graph(&object_ids, audiences)?;
12921        for remote in remote_objects {
12922            let object_id = remote.object_id();
12923            let existing = conn
12924                .query_row(
12925                    "SELECT state FROM remote_objects WHERE object_id = ?1",
12926                    [object_id.to_string()],
12927                    |row| row.get::<_, String>(0),
12928                )
12929                .optional()
12930                .map_err(DbError::from)?;
12931            let persisted = match existing {
12932                Some(state) => {
12933                    let existing: RemoteObjectRecord = serde_json::from_str(&state).map_err(
12934                        |error| {
12935                            DbError::Message(format!(
12936                                "prepared remote object {object_id} has invalid closed state: {error}"
12937                            ))
12938                        },
12939                    )?;
12940                    merge_prepared_remote_object(existing, remote, commit_ref)?
12941                }
12942                None => remote.clone(),
12943            };
12944            let state = serde_json::to_string(&persisted).map_err(|error| {
12945                DbError::Message(format!("serialize closed remote object: {error}"))
12946            })?;
12947            conn.execute(
12948                "INSERT INTO remote_objects (object_id, state) VALUES (?1, ?2)
12949                 ON CONFLICT(object_id) DO UPDATE SET state = excluded.state",
12950                (object_id.to_string(), state),
12951            )
12952            .map_err(DbError::from)?;
12953        }
12954        let commit_remote = RemoteObjectRecord::candidate_commit(
12955            commit_ref.clone(),
12956            commit.to_bytes(),
12957            commit_stored_bytes.to_vec(),
12958        )
12959        .map_err(|error| DbError::Message(format!("prepared candidate commit: {error}")))?;
12960        persist_exact_remote_object_on(conn, &commit_remote, "candidate commit")?;
12961        let expected_partition_count = usize::from(partitions.store.is_some())
12962            .checked_add(partitions.circles.len())
12963            .ok_or_else(|| DbError::Message("audience partition count overflow".to_string()))?;
12964        if audiences.packages.len() != expected_partition_count {
12965            return Err(DbError::Message(
12966                "prepared audience packages do not cover every write partition".to_string(),
12967            ));
12968        }
12969        let mut indexed = std::collections::BTreeSet::new();
12970        for package in &audiences.packages {
12971            let value = package.package();
12972            if value.store_root_hash() != store_root_hash
12973                || value.write_id() != write_id
12974                || value.commit_coord() != &commit_ref.coord
12975                || value.candidate_family() != commit.candidate_family()
12976            {
12977                return Err(DbError::Message(
12978                    "prepared audience package differs from its exact Store commit".to_string(),
12979                ));
12980            }
12981            match value.audience() {
12982                crate::sync::audience_package::PackageAudience::Store => {
12983                    let partition = partitions.store.as_ref().ok_or_else(|| {
12984                        DbError::Message(
12985                            "prepared Store package has no Store partition".to_string(),
12986                        )
12987                    })?;
12988                    if value.changeset() != partition.changeset {
12989                        return Err(DbError::Message(
12990                            "prepared Store package changeset differs from its partition"
12991                                .to_string(),
12992                        ));
12993                    }
12994                    commit
12995                        .verify_store_package(package.semantic_bytes())
12996                        .map_err(|error| DbError::Message(error.to_string()))?;
12997                }
12998                crate::sync::audience_package::PackageAudience::Circle { circle_id, .. } => {
12999                    let partition = partitions
13000                        .circles
13001                        .iter()
13002                        .find(|partition| partition.audience == Audience::Circle(*circle_id))
13003                        .ok_or_else(|| {
13004                            DbError::Message(format!(
13005                                "prepared Circle package {circle_id} has no partition"
13006                            ))
13007                        })?;
13008                    if value.changeset() != partition.changeset {
13009                        return Err(DbError::Message(format!(
13010                            "prepared Circle package {circle_id} changeset differs from its partition"
13011                        )));
13012                    }
13013                    commit
13014                        .verify_circle_package(*circle_id, package.semantic_bytes())
13015                        .map_err(|error| DbError::Message(error.to_string()))?;
13016                }
13017            }
13018            indexed.insert(package.remote_object_id());
13019        }
13020        indexed.extend(
13021            audiences
13022                .blobs
13023                .iter()
13024                .map(PreparedAudienceBlob::remote_object_id),
13025        );
13026        debug_assert_eq!(indexed, object_ids);
13027        Self::persist_prepared_audience_objects_on(
13028            conn,
13029            write_id,
13030            &audiences.packages,
13031            &audiences.blobs,
13032        )
13033    }
13034
13035    fn validate_loaded_write_objects(
13036        write_id: &WriteId,
13037        commit_ref: &StoreBatchCommitRef,
13038        commit: &StoreBatchCommit,
13039        partitions: &PreparedStoreWritePartitions,
13040        audiences: &PreparedAudienceObjects,
13041    ) -> Result<(), DbError> {
13042        let expected_package_count = usize::from(commit.store_package().is_some())
13043            .checked_add(commit.circle_packages().len())
13044            .ok_or_else(|| DbError::Message("package count overflow".to_string()))?;
13045        let partition_count = usize::from(partitions.store.is_some())
13046            .checked_add(partitions.circles.len())
13047            .ok_or_else(|| DbError::Message("audience partition count overflow".to_string()))?;
13048        if audiences.packages.len() != expected_package_count
13049            || audiences.packages.len() != partition_count
13050        {
13051            return Err(DbError::Message(
13052                "prepared package indexes do not exactly cover commit audiences".to_string(),
13053            ));
13054        }
13055        for package in &audiences.packages {
13056            let value = package.package();
13057            if value.write_id() != write_id
13058                || value.commit_coord() != &commit_ref.coord
13059                || value.candidate_family() != commit.candidate_family()
13060            {
13061                return Err(DbError::Message(
13062                    "indexed audience package differs from its exact commit".to_string(),
13063                ));
13064            }
13065            let expected_object = match value.audience() {
13066                crate::sync::audience_package::PackageAudience::Store => {
13067                    commit
13068                        .verify_store_package(package.semantic_bytes())
13069                        .map_err(|error| DbError::Message(error.to_string()))?;
13070                    &commit
13071                        .store_package()
13072                        .as_ref()
13073                        .expect("verified present")
13074                        .object
13075                }
13076                crate::sync::audience_package::PackageAudience::Circle { circle_id, .. } => {
13077                    commit
13078                        .verify_circle_package(*circle_id, package.semantic_bytes())
13079                        .map_err(|error| DbError::Message(error.to_string()))?;
13080                    &commit
13081                        .circle_packages()
13082                        .iter()
13083                        .find(|entry| entry.circle_id == *circle_id)
13084                        .expect("verified present")
13085                        .package
13086                        .object
13087                }
13088            };
13089            if package.object() != expected_object {
13090                return Err(DbError::Message(
13091                    "indexed audience package exact object differs from its commit".to_string(),
13092                ));
13093            }
13094            value
13095                .validate_blob_uploader(&commit.author_registration)
13096                .map_err(|error| DbError::Message(error.to_string()))?;
13097        }
13098        validate_prepared_audience_blob_bindings(audiences)
13099    }
13100
13101    fn activate_prepared_write_on(
13102        conn: &Connection,
13103        gates: &Gates,
13104        synced_tables: &[SyncedTable],
13105        write_id: &WriteId,
13106        commit: &StoreBatchCommit,
13107        commit_ref: &StoreBatchCommitRef,
13108        local_cleanup: StoreBatchLocalCleanup,
13109        additional_object_ids: &[ObjectHash],
13110    ) -> Result<(), DbError> {
13111        commit_ref
13112            .verify_commit(commit)
13113            .map_err(|error| DbError::Message(error.to_string()))?;
13114        let audiences = load_prepared_audience_objects_on(conn, write_id)?;
13115        for package in &audiences.packages {
13116            package
13117                .package()
13118                .validate_blob_uploader(&commit.author_registration)
13119                .map_err(|error| DbError::Message(error.to_string()))?;
13120        }
13121        let mut object_ids = std::collections::BTreeSet::new();
13122        object_ids.insert(remote_object_id(&commit_ref.object));
13123        object_ids.extend(
13124            audiences
13125                .packages
13126                .iter()
13127                .map(PreparedAudiencePackage::remote_object_id),
13128        );
13129        object_ids.extend(
13130            audiences
13131                .blobs
13132                .iter()
13133                .map(PreparedAudienceBlob::remote_object_id),
13134        );
13135        object_ids.extend(additional_object_ids.iter().copied());
13136        for object_id in object_ids {
13137            let remote = load_remote_object_on(conn, object_id)?
13138                .into_activated(commit_ref)
13139                .map_err(|error| {
13140                    DbError::Message(format!("activate remote object {object_id}: {error}"))
13141                })?;
13142            let state = serde_json::to_string(&remote).map_err(|error| {
13143                DbError::Message(format!("serialize activated remote object: {error}"))
13144            })?;
13145            let updated = conn
13146                .execute(
13147                    "UPDATE remote_objects SET state = ?2 WHERE object_id = ?1",
13148                    (object_id.to_string(), state),
13149                )
13150                .map_err(DbError::from)?;
13151            if updated != 1 {
13152                return Err(DbError::Message(format!(
13153                    "remote object {object_id} disappeared during activation"
13154                )));
13155            }
13156        }
13157        let activation = BlobActivation {
13158            coord: commit_ref.coord.clone(),
13159        };
13160        let mut consumed_uploads = 0;
13161        for package in &audiences.packages {
13162            let winning_rows = crate::sync::apply::current_winning_rows(
13163                conn,
13164                synced_tables,
13165                package.package().changeset(),
13166            )?;
13167            Self::install_winning_blob_bindings_on(
13168                conn,
13169                gates,
13170                synced_tables,
13171                package.package(),
13172                &activation,
13173                &winning_rows,
13174            )?;
13175            for binding in package.package().blob_bindings() {
13176                if consume_created_upload_handoff_on(conn, package.package(), binding)? {
13177                    consumed_uploads += 1;
13178                }
13179            }
13180        }
13181        match Self::make_remote_publication_root_on(conn, write_id)? {
13182            Some((root_table, root_id)) => {
13183                if consumed_uploads == 0 {
13184                    return Err(DbError::Message(format!(
13185                        "make_remote publication {write_id} for {root_table:?}/{root_id:?} contains no Created upload handoff"
13186                    )));
13187                }
13188                let remaining: i64 = conn
13189                    .query_row(
13190                        "SELECT COUNT(*) FROM cloud_outbox
13191                         WHERE operation = 'upload' AND root_table = ?1 AND root_id = ?2",
13192                        (&root_table, &root_id),
13193                        |row| row.get(0),
13194                    )
13195                    .map_err(DbError::from)?;
13196                if remaining != 0 {
13197                    return Err(DbError::Message(format!(
13198                        "make_remote publication {write_id} left {remaining} upload handoff(s) for {root_table:?}/{root_id:?}"
13199                    )));
13200                }
13201                Self::complete_make_remote_publication_on(conn, write_id)?;
13202            }
13203            None if consumed_uploads != 0 => {
13204                return Err(DbError::Message(format!(
13205                    "Store write {write_id} consumed Created upload handoffs without a make_remote publication intent"
13206                )));
13207            }
13208            None => {}
13209        }
13210        Self::record_materialized_commit_on(conn, commit, commit_ref)?;
13211        for drop in local_cleanup.drops {
13212            conn.execute(
13213                "INSERT INTO published_blob_drop_intents
13214                 (seq, namespace, blob_id, size, disposition)
13215                 VALUES (?1, ?2, ?3, ?4, ?5)
13216                 ON CONFLICT(seq, namespace, blob_id) DO NOTHING",
13217                rusqlite::params![
13218                    Self::sequence_to_sqlite(
13219                        &match &commit_ref.coord {
13220                            StoreCommitCoord::MergeConcurrent { stream_id, .. } => {
13221                                stream_id.to_string()
13222                            }
13223                            StoreCommitCoord::Serial { .. } => SERIAL_STREAM_ID.to_string(),
13224                        },
13225                        commit_ref.coord.sequence(),
13226                    )?,
13227                    drop.namespace,
13228                    drop.id,
13229                    i64::try_from(drop.size).map_err(|_| DbError::Message(
13230                        "outbound local cleanup size exceeds SQLite integer".to_string()
13231                    ))?,
13232                    drop.disposition.as_db(),
13233                ],
13234            )
13235            .map_err(DbError::from)?;
13236        }
13237        conn.execute(
13238            "DELETE FROM store_write_packages WHERE write_id = ?1",
13239            [write_id.as_str()],
13240        )
13241        .map_err(DbError::from)?;
13242        conn.execute(
13243            "DELETE FROM store_write_blobs WHERE write_id = ?1",
13244            [write_id.as_str()],
13245        )
13246        .map_err(DbError::from)?;
13247        conn.execute(
13248            "DELETE FROM store_write_blob_leases WHERE write_id = ?1",
13249            [write_id.as_str()],
13250        )
13251        .map_err(DbError::from)?;
13252        Ok(())
13253    }
13254
13255    #[cfg(test)]
13256    pub(crate) async fn prepared_audience_objects(
13257        &self,
13258        write_id: &WriteId,
13259    ) -> Result<PreparedAudienceObjects, DbError> {
13260        let write_id = write_id.clone();
13261        let loaded = self
13262            .call(move |conn| load_prepared_audience_objects_on(conn, &write_id))
13263            .await?;
13264
13265        let mut verified_blobs = Vec::with_capacity(loaded.blobs.len());
13266        for prepared in loaded.blobs {
13267            if let Some(spool_path) = prepared.spool_path() {
13268                crate::local_blob::verify_exact_file(prepared.blob().object(), spool_path)
13269                    .await
13270                    .map_err(|error| DbError::Message(format!("prepared blob spool: {error}")))?;
13271            }
13272            verified_blobs.push(prepared);
13273        }
13274        Ok(PreparedAudienceObjects {
13275            packages: loaded.packages,
13276            blobs: verified_blobs,
13277        })
13278    }
13279
13280    pub(crate) async fn prepared_remote_objects(
13281        &self,
13282        write_id: &WriteId,
13283    ) -> Result<Vec<PreparedRemoteObject>, DbError> {
13284        let write_id = write_id.clone();
13285        self.call(move |conn| {
13286            let mut statement = conn
13287                .prepare(
13288                    "SELECT remote_object_id, NULL AS spool_path
13289                     FROM store_write_packages WHERE write_id = ?1
13290                     UNION
13291                     SELECT remote_object_id, spool_path
13292                     FROM store_write_blobs WHERE write_id = ?1
13293                     ORDER BY remote_object_id",
13294                )
13295                .map_err(DbError::from)?;
13296            let ids = statement
13297                .query_map([write_id.as_str()], |row| {
13298                    Ok((row.get::<_, String>(0)?, row.get::<_, Option<String>>(1)?))
13299                })
13300                .map_err(DbError::from)?
13301                .collect::<Result<Vec<_>, _>>()
13302                .map_err(DbError::from)?;
13303            ids.into_iter()
13304                .map(|(encoded, spool_path)| {
13305                    let id = encoded.parse().map_err(|error| {
13306                        DbError::Message(format!("prepared remote object id: {error}"))
13307                    })?;
13308                    Ok(PreparedRemoteObject {
13309                        record: load_remote_object_on(conn, id)?,
13310                        spool_path: spool_path.map(PathBuf::from),
13311                    })
13312                })
13313                .collect()
13314        })
13315        .await
13316    }
13317
13318    pub(crate) async fn mark_remote_object_uploaded(
13319        &self,
13320        expected: RemoteObjectRecord,
13321    ) -> Result<RemoteObjectRecord, DbError> {
13322        self.call(move |conn| mark_remote_object_uploaded_on(conn, expected))
13323            .await
13324    }
13325
13326    pub(crate) async fn mark_candidate_commit_uploaded(
13327        &self,
13328        commit: StoreBatchCommitRef,
13329    ) -> Result<(), DbError> {
13330        self.call(move |conn| {
13331            let object_id = remote_object_id(&commit.object);
13332            let current = load_remote_object_on(conn, object_id)?;
13333            if !matches!(
13334                &current,
13335                RemoteObjectRecord::CandidateCommit(record) if record.identity == commit
13336            ) {
13337                return Err(DbError::Message(format!(
13338                    "remote object {object_id} is not the exact candidate commit"
13339                )));
13340            }
13341            mark_remote_object_uploaded_on(conn, current)?;
13342            Ok(())
13343        })
13344        .await
13345    }
13346
13347    pub(crate) async fn mark_store_head_uploaded(
13348        &self,
13349        head: crate::sync::store_commit::StoreDeviceHeadRef,
13350    ) -> Result<(), DbError> {
13351        self.call(move |conn| {
13352            let object_id = remote_object_id(&head.object);
13353            let current = load_remote_object_on(conn, object_id)?;
13354            if !matches!(
13355                &current,
13356                RemoteObjectRecord::RetainedAuthority(record)
13357                    if matches!(
13358                        &record.identity.domain,
13359                        crate::sync::remote_object::RetainedAuthorityObjectDomain::StoreDeviceHead {
13360                            reference
13361                        } if reference == &head
13362                    )
13363            ) {
13364                return Err(DbError::Message(format!(
13365                    "remote object {object_id} is not the exact prepared Store head"
13366                )));
13367            }
13368            mark_remote_object_uploaded_on(conn, current)?;
13369            Ok(())
13370        })
13371        .await
13372    }
13373
13374    pub(crate) async fn blocked_merge_candidate(
13375        &self,
13376        write_id: WriteId,
13377    ) -> Result<Option<BlockedMergeCandidate>, DbError> {
13378        self.call(move |conn| {
13379            let row: Option<(String, Option<String>)> = conn
13380                .query_row(
13381                    "SELECT status, prepared FROM store_writes WHERE write_id = ?1",
13382                    [write_id.as_str()],
13383                    |row| Ok((row.get(0)?, row.get(1)?)),
13384                )
13385                .optional()
13386                .map_err(DbError::from)?;
13387            let Some((raw_status, raw_prepared)) = row else {
13388                return Err(DbError::Message(format!("write {write_id} is absent")));
13389            };
13390            let status: WriteStatus = serde_json::from_str(&raw_status).map_err(|error| {
13391                DbError::Message(format!("blocked Merge candidate status: {error}"))
13392            })?;
13393            if !matches!(status, WriteStatus::Blocked(_)) {
13394                return Err(DbError::Message(format!("write {write_id} is not blocked")));
13395            }
13396            let Some(raw_prepared) = raw_prepared else {
13397                return Ok(None);
13398            };
13399            let prepared: PreparedStoreWriteState =
13400                serde_json::from_str(&raw_prepared).map_err(|error| {
13401                    DbError::Message(format!("blocked Merge candidate preparation: {error}"))
13402                })?;
13403            let PreparedStoreWriteState::MergeConcurrent { .. } = &prepared else {
13404                return match prepared {
13405                    PreparedStoreWriteState::MergeAbandonment { .. } => Ok(None),
13406                    PreparedStoreWriteState::SerialPreparing
13407                    | PreparedStoreWriteState::Serial { .. } => Err(DbError::Message(
13408                        "Serial state reached Merge candidate abandonment".to_string(),
13409                    )),
13410                    PreparedStoreWriteState::MergeConcurrent { .. } => unreachable!(),
13411                };
13412            };
13413            let candidate = parse_prepared_merge_candidate_on(conn, &prepared)?
13414                .expect("matched Merge preparation");
13415            if candidate.commit.write_id != write_id {
13416                return Err(DbError::Message(
13417                    "blocked Merge candidate differs from its write identity".to_string(),
13418                ));
13419            }
13420            Ok(Some(BlockedMergeCandidate {
13421                commit: ExactProtocolObject {
13422                    value: candidate.commit,
13423                    bytes: candidate.canonical_signed_bytes,
13424                    object: candidate.reference.object.clone(),
13425                    prepared: match &prepared {
13426                        PreparedStoreWriteState::MergeConcurrent { commit, .. } => {
13427                            commit.prepared.clone()
13428                        }
13429                        _ => unreachable!("matched Merge preparation"),
13430                    },
13431                },
13432                head: ExactProtocolObject {
13433                    value: candidate.head,
13434                    bytes: match &prepared {
13435                        PreparedStoreWriteState::MergeConcurrent { head, .. } => {
13436                            head.semantic_bytes.clone()
13437                        }
13438                        _ => unreachable!("matched Merge preparation"),
13439                    },
13440                    object: candidate.head_prepared.reference().clone(),
13441                    prepared: candidate.head_prepared,
13442                },
13443            }))
13444        })
13445        .await
13446    }
13447
13448    #[doc(hidden)]
13449    pub async fn merge_candidate_abandonment_required(
13450        &self,
13451        write_id: &WriteId,
13452    ) -> Result<bool, DbError> {
13453        if !matches!(
13454            self.merge_abandonment_state(write_id).await?,
13455            MergeAbandonmentState::None
13456        ) {
13457            return Ok(true);
13458        }
13459        Ok(self
13460            .blocked_merge_candidate(write_id.clone())
13461            .await?
13462            .is_some())
13463    }
13464
13465    pub(crate) async fn merge_abandonment_state(
13466        &self,
13467        write_id: &WriteId,
13468    ) -> Result<MergeAbandonmentState, DbError> {
13469        let write_id = write_id.clone();
13470        self.call(move |conn| {
13471            let raw_prepared: Option<String> = conn
13472                .query_row(
13473                    "SELECT prepared FROM store_writes WHERE write_id = ?1",
13474                    [write_id.as_str()],
13475                    |row| row.get(0),
13476                )
13477                .map_err(DbError::from)?;
13478            let Some(raw_prepared) = raw_prepared else {
13479                return Ok(MergeAbandonmentState::None);
13480            };
13481            let prepared: PreparedStoreWriteState =
13482                serde_json::from_str(&raw_prepared).map_err(|error| {
13483                    DbError::Message(format!("prepared Merge abandonment: {error}"))
13484                })?;
13485            let PreparedStoreWriteState::MergeAbandonment {
13486                candidate_commit,
13487                candidate_head,
13488                outcome,
13489                ..
13490            } = &prepared
13491            else {
13492                return Ok(MergeAbandonmentState::None);
13493            };
13494            let candidate =
13495                parse_prepared_merge_candidate_parts_on(conn, candidate_commit, candidate_head)?;
13496            Ok(match outcome {
13497                MergeAbandonmentOutcome::Prepared => MergeAbandonmentState::Prepared,
13498                MergeAbandonmentOutcome::Accepted { .. } => MergeAbandonmentState::Accepted,
13499                MergeAbandonmentOutcome::Lost { winner_commit, .. }
13500                    if winner_commit == &candidate.reference =>
13501                {
13502                    MergeAbandonmentState::CandidateWon
13503                }
13504                MergeAbandonmentOutcome::Lost { .. } => MergeAbandonmentState::OtherWon,
13505            })
13506        })
13507        .await
13508    }
13509
13510    pub(crate) async fn resume_winning_merge_candidate(
13511        &self,
13512        write_id: WriteId,
13513    ) -> Result<(), DbError> {
13514        let statuses = self.state.write_statuses.clone();
13515        let notified_write_id = write_id.clone();
13516        self.call(move |conn| {
13517            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
13518            let (raw_status, raw_prepared): (String, String) = tx
13519                .query_row(
13520                    "SELECT status, prepared FROM store_writes WHERE write_id = ?1",
13521                    [write_id.as_str()],
13522                    |row| Ok((row.get(0)?, row.get(1)?)),
13523                )
13524                .map_err(DbError::from)?;
13525            let status: WriteStatus = serde_json::from_str(&raw_status).map_err(|error| {
13526                DbError::Message(format!("winning Merge candidate status: {error}"))
13527            })?;
13528            if !matches!(status, WriteStatus::Blocked(_)) {
13529                return Err(DbError::Message(
13530                    "winning Merge candidate is not blocked".to_string(),
13531                ));
13532            }
13533            let prepared: PreparedStoreWriteState =
13534                serde_json::from_str(&raw_prepared).map_err(|error| {
13535                    DbError::Message(format!("winning Merge candidate preparation: {error}"))
13536                })?;
13537            let PreparedStoreWriteState::MergeAbandonment {
13538                candidate_commit,
13539                candidate_head,
13540                authority_commit,
13541                authority_head,
13542                outcome: MergeAbandonmentOutcome::Lost { winner_commit, .. },
13543                local_cleanup,
13544                completion,
13545            } = prepared
13546            else {
13547                return Err(DbError::Message(
13548                    "Merge abandonment did not lose to its prepared candidate".to_string(),
13549                ));
13550            };
13551            let candidate =
13552                parse_prepared_merge_candidate_parts_on(&tx, &candidate_commit, &candidate_head)?;
13553            if winner_commit != candidate.reference {
13554                return Err(DbError::Message(
13555                    "Merge abandonment winner is another candidate".to_string(),
13556                ));
13557            }
13558            let authority =
13559                parse_prepared_merge_candidate_parts_on(&tx, &authority_commit, &authority_head)?;
13560            remove_cleaned_merge_authority_on(&tx, &authority)?;
13561            let replacement = PreparedStoreWriteState::MergeConcurrent {
13562                commit: candidate_commit,
13563                head: candidate_head,
13564                local_cleanup,
13565                completion,
13566            };
13567            let replacement = serde_json::to_string(&replacement).map_err(|error| {
13568                DbError::Message(format!("serialize winning Merge candidate: {error}"))
13569            })?;
13570            let publishing = serde_json::to_string(&WriteStatus::Publishing).map_err(|error| {
13571                DbError::Message(format!("serialize winning Merge status: {error}"))
13572            })?;
13573            let updated = tx
13574                .execute(
13575                    "UPDATE store_writes SET prepared = ?2, status = ?3
13576                     WHERE write_id = ?1 AND prepared = ?4",
13577                    rusqlite::params![write_id.as_str(), replacement, publishing, raw_prepared],
13578                )
13579                .map_err(DbError::from)?;
13580            if updated != 1 {
13581                return Err(DbError::Message(
13582                    "Merge abandonment changed while restoring its winner".to_string(),
13583                ));
13584            }
13585            tx.commit().map_err(DbError::from)?;
13586            Self::notify_write_status_in(&statuses, &notified_write_id, WriteStatus::Publishing);
13587            Ok(())
13588        })
13589        .await
13590    }
13591
13592    pub(crate) async fn finish_lost_merge_abandonment(
13593        &self,
13594        write_id: WriteId,
13595    ) -> Result<(), DbError> {
13596        self.call(move |conn| {
13597            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
13598            let (raw_status, raw_prepared): (String, String) = tx
13599                .query_row(
13600                    "SELECT status, prepared FROM store_writes WHERE write_id = ?1",
13601                    [write_id.as_str()],
13602                    |row| Ok((row.get(0)?, row.get(1)?)),
13603                )
13604                .map_err(DbError::from)?;
13605            let status: WriteStatus = serde_json::from_str(&raw_status).map_err(|error| {
13606                DbError::Message(format!("lost Merge abandonment status: {error}"))
13607            })?;
13608            if !matches!(status, WriteStatus::Blocked(_)) {
13609                return Err(DbError::Message(
13610                    "lost Merge abandonment is not blocked".to_string(),
13611                ));
13612            }
13613            let prepared: PreparedStoreWriteState =
13614                serde_json::from_str(&raw_prepared).map_err(|error| {
13615                    DbError::Message(format!("lost Merge abandonment preparation: {error}"))
13616                })?;
13617            let PreparedStoreWriteState::MergeAbandonment {
13618                candidate_commit,
13619                candidate_head,
13620                authority_commit,
13621                authority_head,
13622                outcome: MergeAbandonmentOutcome::Lost { winner_commit, .. },
13623                local_cleanup,
13624                completion,
13625            } = prepared
13626            else {
13627                return Err(DbError::Message(
13628                    "Merge abandonment has no third-candidate winner".to_string(),
13629                ));
13630            };
13631            let candidate =
13632                parse_prepared_merge_candidate_parts_on(&tx, &candidate_commit, &candidate_head)?;
13633            if winner_commit == candidate.reference {
13634                return Err(DbError::Message(
13635                    "original candidate won the Merge abandonment race".to_string(),
13636                ));
13637            }
13638            let authority =
13639                parse_prepared_merge_candidate_parts_on(&tx, &authority_commit, &authority_head)?;
13640            remove_cleaned_merge_authority_on(&tx, &authority)?;
13641            let replacement = PreparedStoreWriteState::MergeConcurrent {
13642                commit: candidate_commit,
13643                head: candidate_head,
13644                local_cleanup,
13645                completion,
13646            };
13647            let replacement = serde_json::to_string(&replacement).map_err(|error| {
13648                DbError::Message(format!("serialize lost Merge candidate: {error}"))
13649            })?;
13650            let updated = tx
13651                .execute(
13652                    "UPDATE store_writes SET prepared = ?2
13653                     WHERE write_id = ?1 AND status = ?3 AND prepared = ?4",
13654                    rusqlite::params![write_id.as_str(), replacement, raw_status, raw_prepared],
13655                )
13656                .map_err(DbError::from)?;
13657            if updated != 1 {
13658                return Err(DbError::Message(
13659                    "Merge abandonment changed while removing its losing authority".to_string(),
13660                ));
13661            }
13662            tx.commit().map_err(DbError::from)
13663        })
13664        .await
13665    }
13666
13667    #[doc(hidden)]
13668    pub async fn merge_candidate_cleanup_pending(
13669        &self,
13670        write_id: &WriteId,
13671    ) -> Result<bool, DbError> {
13672        let write_id = write_id.clone();
13673        self.call(move |conn| {
13674            let raw_prepared: Option<String> = conn
13675                .query_row(
13676                    "SELECT prepared FROM store_writes WHERE write_id = ?1",
13677                    [write_id.as_str()],
13678                    |row| row.get(0),
13679                )
13680                .map_err(DbError::from)?;
13681            let Some(raw_prepared) = raw_prepared else {
13682                return Ok(false);
13683            };
13684            let prepared: PreparedStoreWriteState = serde_json::from_str(&raw_prepared)
13685                .map_err(|error| DbError::Message(format!("prepared Merge cleanup: {error}")))?;
13686            let Some(candidate) = parse_prepared_merge_candidate_on(conn, &prepared)? else {
13687                return Ok(false);
13688            };
13689            let cleanup_pending = |candidate: &PreparedMergeCandidate| -> Result<bool, DbError> {
13690                let remote = load_remote_object_on(
13691                    conn,
13692                    remote_object_id(&candidate.reference.object),
13693                )?;
13694                Ok(matches!(
13695                    remote,
13696                    RemoteObjectRecord::CandidateCommit(
13697                        crate::sync::remote_object::CandidateCommitRecord {
13698                            state:
13699                                crate::sync::remote_object::CandidateCommitState::CleanupPending {
13700                                    proof: crate::sync::remote_object::CandidateNonactivationProof::MergeWinner { .. }
13701                                },
13702                            ..
13703                        }
13704                    )
13705                ))
13706            };
13707            match &prepared {
13708                PreparedStoreWriteState::MergeConcurrent { .. } => cleanup_pending(&candidate),
13709                PreparedStoreWriteState::MergeAbandonment {
13710                    outcome,
13711                    authority_commit,
13712                    authority_head,
13713                    ..
13714                } => {
13715                    let authority = parse_prepared_merge_candidate_parts_on(
13716                        conn,
13717                        authority_commit,
13718                        authority_head,
13719                    )?;
13720                    match outcome {
13721                        MergeAbandonmentOutcome::Prepared => Ok(false),
13722                        MergeAbandonmentOutcome::Accepted { .. } => cleanup_pending(&candidate),
13723                        MergeAbandonmentOutcome::Lost { winner_commit, .. } => Ok(
13724                            (winner_commit != &candidate.reference && cleanup_pending(&candidate)?)
13725                                || cleanup_pending(&authority)?,
13726                        ),
13727                    }
13728                }
13729                PreparedStoreWriteState::SerialPreparing
13730                | PreparedStoreWriteState::Serial { .. } => Ok(false),
13731            }
13732        })
13733        .await
13734    }
13735
13736    pub(crate) async fn merge_candidate_cleanup_targets(
13737        &self,
13738        write_id: WriteId,
13739    ) -> Result<Vec<CandidateCleanupObject>, DbError> {
13740        self.call(move |conn| {
13741            let (raw_status, raw_prepared): (String, String) = conn
13742                .query_row(
13743                    "SELECT status, prepared FROM store_writes WHERE write_id = ?1",
13744                    [write_id.as_str()],
13745                    |row| Ok((row.get(0)?, row.get(1)?)),
13746                )
13747                .map_err(DbError::from)?;
13748            let status: WriteStatus = serde_json::from_str(&raw_status)
13749                .map_err(|error| DbError::Message(format!("Merge cleanup status: {error}")))?;
13750            if !matches!(status, WriteStatus::Blocked(_)) {
13751                return Err(DbError::Message(format!(
13752                    "Merge cleanup write {write_id} is not blocked"
13753                )));
13754            }
13755            let prepared: PreparedStoreWriteState = serde_json::from_str(&raw_prepared)
13756                .map_err(|error| DbError::Message(format!("prepared Merge cleanup: {error}")))?;
13757            let Some(candidate) = parse_prepared_merge_candidate_on(conn, &prepared)? else {
13758                return Err(DbError::Message(
13759                    "Serial state reached Merge candidate cleanup".to_string(),
13760                ));
13761            };
13762            match &prepared {
13763                PreparedStoreWriteState::MergeConcurrent { .. } => {
13764                    merge_candidate_cleanup_targets_on(conn, &write_id, &candidate, true)
13765                }
13766                PreparedStoreWriteState::MergeAbandonment {
13767                    outcome,
13768                    authority_commit,
13769                    authority_head,
13770                    ..
13771                } => {
13772                    let authority = parse_prepared_merge_candidate_parts_on(
13773                        conn,
13774                        authority_commit,
13775                        authority_head,
13776                    )?;
13777                    let mut targets = Vec::new();
13778                    match outcome {
13779                        MergeAbandonmentOutcome::Prepared => {
13780                            return Err(DbError::Message(
13781                                "Merge abandonment has no accepted winner".to_string(),
13782                            ));
13783                        }
13784                        MergeAbandonmentOutcome::Accepted { .. } => {
13785                            targets.extend(merge_candidate_cleanup_targets_on(
13786                                conn, &write_id, &candidate, true,
13787                            )?);
13788                        }
13789                        MergeAbandonmentOutcome::Lost { winner_commit, .. } => {
13790                            if winner_commit != &candidate.reference {
13791                                targets.extend(merge_candidate_cleanup_targets_on(
13792                                    conn, &write_id, &candidate, true,
13793                                )?);
13794                            }
13795                            targets.extend(merge_candidate_cleanup_targets_on(
13796                                conn, &write_id, &authority, false,
13797                            )?);
13798                        }
13799                    }
13800                    Ok(targets)
13801                }
13802                PreparedStoreWriteState::SerialPreparing
13803                | PreparedStoreWriteState::Serial { .. } => unreachable!("parsed Merge cleanup"),
13804            }
13805        })
13806        .await
13807    }
13808
13809    pub(crate) async fn adopt_alternate_merge_head(
13810        &self,
13811        write_id: WriteId,
13812        winner: StoreDeviceHead,
13813        winner_prepared: PreparedExactObject,
13814    ) -> Result<(), DbError> {
13815        self.call(move |conn| {
13816            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
13817            let (raw_status, raw_prepared): (String, String) = tx
13818                .query_row(
13819                    "SELECT status, prepared FROM store_writes WHERE write_id = ?1",
13820                    [write_id.as_str()],
13821                    |row| Ok((row.get(0)?, row.get(1)?)),
13822                )
13823                .map_err(DbError::from)?;
13824            let status: WriteStatus = serde_json::from_str(&raw_status).map_err(|error| {
13825                DbError::Message(format!("alternate Merge head status: {error}"))
13826            })?;
13827            if !matches!(status, WriteStatus::Publishing) {
13828                return Err(DbError::Message(format!(
13829                    "Merge candidate {write_id} is not publishing"
13830                )));
13831            }
13832            let prepared: PreparedStoreWriteState = serde_json::from_str(&raw_prepared)
13833                .map_err(|error| DbError::Message(format!("prepared Merge candidate: {error}")))?;
13834            let publication = parse_prepared_merge_publication_on(&tx, &prepared)?
13835                .ok_or_else(|| {
13836                    DbError::Message(
13837                        "Serial branch reached alternate Merge head adoption".to_string(),
13838                    )
13839                })?;
13840            if winner_prepared.reference().slot()
13841                != publication.head_prepared.reference().slot()
13842                || winner_prepared.reference() == publication.head_prepared.reference()
13843            {
13844                return Err(DbError::Message(
13845                    "alternate Merge head does not replace the prepared exact slot".to_string(),
13846                ));
13847            }
13848            let root = required_store_root_authority_on(&tx)?;
13849            let registration = load_activated_registration_on(
13850                &tx,
13851                &root,
13852                &publication.commit.author_registration,
13853            )?;
13854            let candidate = publication.reference;
13855            let verified_winner = StoreDeviceHead::parse_at(
13856                &winner.to_bytes(),
13857                root.store_root_hash,
13858                &registration,
13859                &candidate,
13860            )
13861            .map_err(|error| DbError::Message(format!("verify alternate Merge head: {error}")))?;
13862            if verified_winner != winner || winner.commit != candidate {
13863                return Err(DbError::Message(
13864                    "alternate Merge head does not activate the prepared commit".to_string(),
13865                ));
13866            }
13867            let old_object_id = remote_object_id(publication.head_prepared.reference());
13868            let old_remote = load_remote_object_on(&tx, old_object_id)?;
13869            if !matches!(
13870                &old_remote,
13871                RemoteObjectRecord::RetainedAuthority(record)
13872                    if matches!(
13873                        &record.identity.domain,
13874                        crate::sync::remote_object::RetainedAuthorityObjectDomain::StoreDeviceHead { .. }
13875                    ) && matches!(
13876                        &record.state,
13877                        crate::sync::remote_object::RetainedAuthorityObjectState::Prepared {
13878                            ownership
13879                        } if ownership.pending == BTreeSet::from([candidate.clone()])
13880                    )
13881            ) {
13882                return Err(DbError::Message(
13883                    "prepared Merge head lost its candidate ownership".to_string(),
13884                ));
13885            }
13886            let removed = tx
13887                .execute(
13888                    "DELETE FROM remote_objects WHERE object_id = ?1",
13889                    [old_object_id.to_string()],
13890                )
13891                .map_err(DbError::from)?;
13892            if removed != 1 {
13893                return Err(DbError::Message(
13894                    "prepared Merge head disappeared during replacement".to_string(),
13895                ));
13896            }
13897            let winner_ref = crate::sync::store_commit::StoreDeviceHeadRef {
13898                head_hash: winner.head_hash(),
13899                object: winner_prepared.reference().clone(),
13900            };
13901            let mut winner_remote = RemoteObjectRecord::candidate_activated_store_head(
13902                winner_ref,
13903                winner.to_bytes(),
13904                winner_prepared.stored_bytes().to_vec(),
13905                candidate,
13906            )
13907            .map_err(|error| DbError::Message(format!("alternate Merge head: {error}")))?;
13908            winner_remote.mark_uploaded_verified().map_err(|error| {
13909                DbError::Message(format!("mark alternate Merge head uploaded: {error}"))
13910            })?;
13911            persist_exact_remote_object_on(&tx, &winner_remote, "alternate Merge head")?;
13912            let replacement_head = DurablePreparedProtocolObject {
13913                semantic_bytes: winner.to_bytes(),
13914                prepared: winner_prepared,
13915            };
13916            let replacement = match prepared {
13917                PreparedStoreWriteState::MergeConcurrent {
13918                    commit,
13919                    local_cleanup,
13920                    completion,
13921                    ..
13922                } => PreparedStoreWriteState::MergeConcurrent {
13923                    commit,
13924                    head: replacement_head,
13925                    local_cleanup,
13926                    completion,
13927                },
13928                PreparedStoreWriteState::MergeAbandonment {
13929                    candidate_commit,
13930                    candidate_head,
13931                    authority_commit,
13932                    outcome,
13933                    local_cleanup,
13934                    completion,
13935                    ..
13936                } => PreparedStoreWriteState::MergeAbandonment {
13937                    candidate_commit,
13938                    candidate_head,
13939                    authority_commit,
13940                    authority_head: replacement_head,
13941                    outcome,
13942                    local_cleanup,
13943                    completion,
13944                },
13945                PreparedStoreWriteState::SerialPreparing
13946                | PreparedStoreWriteState::Serial { .. } => {
13947                    unreachable!("parsed Merge publication")
13948                }
13949            };
13950            let replacement = serde_json::to_string(&replacement).map_err(|error| {
13951                DbError::Message(format!("serialize alternate Merge preparation: {error}"))
13952            })?;
13953            let updated = tx
13954                .execute(
13955                    "UPDATE store_writes SET prepared = ?2
13956                     WHERE write_id = ?1 AND status = '\"publishing\"' AND prepared = ?3",
13957                    rusqlite::params![write_id.as_str(), replacement, raw_prepared],
13958                )
13959                .map_err(DbError::from)?;
13960            if updated != 1 {
13961                return Err(DbError::Message(
13962                    "prepared Merge write changed during head replacement".to_string(),
13963                ));
13964            }
13965            tx.commit().map_err(DbError::from)
13966        })
13967        .await
13968    }
13969
13970    pub(crate) async fn prepare_serial_candidate_cleanup(
13971        &self,
13972        branch_id: PendingBranchId,
13973        plan: &crate::sync::store_pull::SerialResolutionPlan,
13974    ) -> Result<Vec<CandidateCleanupObject>, DbError> {
13975        let accepted_refs = plan
13976            .commits
13977            .iter()
13978            .map(|commit| commit.commit_ref.clone())
13979            .collect::<Vec<_>>();
13980        let accepted_commits = plan
13981            .commits
13982            .iter()
13983            .map(|commit| commit.commit.clone())
13984            .collect::<Vec<_>>();
13985        let head = plan.head.clone();
13986        let head_bytes = plan.head_object.bytes.clone();
13987        let observed_version_hash =
13988            ObjectHash::digest(plan.head_object.version.cloud().as_provider().as_bytes());
13989        self.call(move |conn| {
13990            if head.to_bytes() != head_bytes {
13991                return Err(DbError::Message(
13992                    "Serial cleanup head bytes differ from the verified head".to_string(),
13993                ));
13994            }
13995            let mut statement = conn
13996                .prepare(
13997                    "SELECT write_id, base, status, prepared FROM store_writes
13998                     WHERE status != '\"local_only\"'
13999                       AND json_extract(status, '$.published') IS NULL
14000                       AND json_extract(status, '$.resolved') IS NULL
14001                       AND json_type(base, '$.serial') IS NOT NULL
14002                     ORDER BY ordinal",
14003                )
14004                .map_err(DbError::from)?;
14005            let rows = statement
14006                .query_map([], |row| {
14007                    Ok((
14008                        row.get::<_, String>(0)?,
14009                        row.get::<_, String>(1)?,
14010                        row.get::<_, String>(2)?,
14011                        row.get::<_, Option<String>>(3)?,
14012                    ))
14013                })
14014                .map_err(DbError::from)?
14015                .collect::<Result<Vec<_>, _>>()
14016                .map_err(DbError::from)?;
14017            drop(statement);
14018            let mut branch_base = None;
14019            let mut prepared = Vec::new();
14020            for (write_id, raw_base, raw_status, raw_prepared) in rows {
14021                let base: StoreWriteBase = serde_json::from_str(&raw_base).map_err(|error| {
14022                    DbError::Message(format!("Serial cleanup branch base: {error}"))
14023                })?;
14024                let StoreWriteBase::Serial {
14025                    branch_id: stored_branch_id,
14026                    base,
14027                } = base
14028                else {
14029                    return Err(DbError::Message(
14030                        "MergeConcurrent base reached Serial candidate cleanup".to_string(),
14031                    ));
14032                };
14033                if stored_branch_id != branch_id {
14034                    return Err(DbError::Message(
14035                        "Serial database contains more than one unresolved branch".to_string(),
14036                    ));
14037                }
14038                let status: WriteStatus = serde_json::from_str(&raw_status).map_err(|error| {
14039                    DbError::Message(format!("Serial cleanup branch status: {error}"))
14040                })?;
14041                if !matches!(
14042                    status,
14043                    WriteStatus::Conflict(ref conflict) if conflict.branch_id == branch_id
14044                ) {
14045                    return Err(DbError::Message(format!(
14046                        "Serial cleanup write {write_id} is not conflicted"
14047                    )));
14048                }
14049                if branch_base.as_ref().is_some_and(|stored| stored != &base) {
14050                    return Err(DbError::Message(
14051                        "Serial cleanup branch has inconsistent bases".to_string(),
14052                    ));
14053                }
14054                branch_base.get_or_insert(base);
14055                let Some(raw_prepared) = raw_prepared else {
14056                    continue;
14057                };
14058                if let Some(candidate) = parse_prepared_serial_candidate(&raw_prepared)? {
14059                    prepared.push((
14060                        WriteId::from_generated(write_id),
14061                        candidate.commit,
14062                        candidate.reference,
14063                        candidate.canonical_signed_bytes,
14064                    ));
14065                }
14066            }
14067            if prepared.is_empty() {
14068                return Ok(Vec::new());
14069            }
14070            let branch_base = branch_base.expect("prepared branch has a base");
14071            if accepted_refs.is_empty() || accepted_refs.len() != accepted_commits.len() {
14072                return Err(DbError::Message(
14073                    "Serial candidate cleanup requires a nonempty accepted suffix".to_string(),
14074                ));
14075            }
14076            let mut accepted_predecessor = branch_base.clone();
14077            for (reference, commit) in accepted_refs.iter().zip(&accepted_commits) {
14078                reference
14079                    .verify_commit(commit)
14080                    .map_err(|error| DbError::Message(error.to_string()))?;
14081                if commit.order.predecessor() != accepted_predecessor.as_ref() {
14082                    return Err(DbError::Message(
14083                        "accepted Serial cleanup suffix has a broken exact predecessor"
14084                            .to_string(),
14085                    ));
14086                }
14087                accepted_predecessor = Some(reference.clone());
14088            }
14089            if !matches!(
14090                head.state,
14091                StoreSerialHeadState::Commit { ref commit, .. }
14092                    if Some(commit) == accepted_refs.last()
14093            ) {
14094                return Err(DbError::Message(
14095                    "Serial cleanup head does not name the accepted suffix tip".to_string(),
14096                ));
14097            }
14098            let mut losing_predecessor = branch_base.clone();
14099            let mut losing_targets = Vec::with_capacity(prepared.len());
14100            for (_, commit, reference, bytes) in &prepared {
14101                if commit.order.predecessor() != losing_predecessor.as_ref() {
14102                    return Err(DbError::Message(
14103                        "losing Serial candidates have a broken exact predecessor chain"
14104                            .to_string(),
14105                    ));
14106                }
14107                losing_targets.push(
14108                    crate::sync::remote_object::StoreBatchCommitDeletionTarget {
14109                        coord: reference.coord.clone(),
14110                        object: reference.object.clone(),
14111                        canonical_signed_bytes: bytes.clone(),
14112                    },
14113                );
14114                losing_predecessor = Some(reference.clone());
14115            }
14116            if accepted_refs.first() == prepared.first().map(|(_, _, reference, _)| reference) {
14117                return Err(DbError::Message(
14118                    "accepted Serial suffix begins with the losing candidate".to_string(),
14119                ));
14120            }
14121            let suffix = crate::sync::remote_object::SerialAcceptedSuffix {
14122                predecessor: branch_base,
14123                commits: accepted_refs,
14124                canonical_signed_head_bytes: head_bytes,
14125                observed_version_hash,
14126            };
14127            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
14128            let mut exclusive_cleanup = Vec::new();
14129            let mut commit_cleanup = Vec::new();
14130            for (index, (write_id, commit, reference, _)) in prepared.iter().enumerate() {
14131                let proof = crate::sync::remote_object::CandidateNonactivationProof::SerialImmediateSuccessor {
14132                    accepted_suffix: suffix.clone(),
14133                    losing_prefix: losing_targets[..=index].to_vec(),
14134                };
14135                let nonactivation = crate::sync::remote_object::CandidateNonactivation {
14136                    candidate: losing_targets[index].clone(),
14137                    proof,
14138                };
14139                let mut object_statement = tx
14140                    .prepare(
14141                        "SELECT remote_object_id FROM store_write_packages WHERE write_id = ?1
14142                         UNION
14143                         SELECT remote_object_id FROM store_write_blobs WHERE write_id = ?1
14144                         ORDER BY remote_object_id",
14145                    )
14146                    .map_err(DbError::from)?;
14147                let object_ids = object_statement
14148                    .query_map([write_id.as_str()], |row| row.get::<_, String>(0))
14149                    .map_err(DbError::from)?
14150                    .collect::<Result<Vec<_>, _>>()
14151                    .map_err(DbError::from)?;
14152                drop(object_statement);
14153                let mut manifest_cleanup = BTreeSet::new();
14154                for encoded in object_ids {
14155                    let object_id: ObjectHash = encoded.parse().map_err(|error| {
14156                        DbError::Message(format!("Serial cleanup remote object id: {error}"))
14157                    })?;
14158                    let mut remote = load_remote_object_on(&tx, object_id)?;
14159                    remote
14160                        .begin_candidate_nonactivation(nonactivation.clone())
14161                        .map_err(|error| {
14162                            DbError::Message(format!(
14163                                "record candidate nonactivation for {object_id}: {error}"
14164                            ))
14165                        })?;
14166                    if let Some(object) = remote.cleanup_target() {
14167                        manifest_cleanup.insert(object.clone());
14168                    }
14169                    update_remote_object_on(&tx, object_id, &remote)?;
14170                }
14171                for object in candidate_manifest_exact_objects(commit) {
14172                    if manifest_cleanup.remove(object) {
14173                        exclusive_cleanup.push(CandidateCleanupObject {
14174                            object: object.clone(),
14175                        });
14176                    }
14177                }
14178                if !manifest_cleanup.is_empty() {
14179                    return Err(DbError::Message(format!(
14180                        "candidate cleanup for write {write_id} contains an object outside its signed manifest"
14181                    )));
14182                }
14183                let commit_object_id = remote_object_id(&reference.object);
14184                let mut remote = load_remote_object_on(&tx, commit_object_id)?;
14185                remote
14186                    .begin_candidate_nonactivation(nonactivation)
14187                    .map_err(|error| {
14188                        DbError::Message(format!(
14189                            "record candidate commit nonactivation for {commit_object_id}: {error}"
14190                        ))
14191                    })?;
14192                if let Some(object) = remote.cleanup_target() {
14193                    commit_cleanup.push(CandidateCleanupObject {
14194                        object: object.clone(),
14195                    });
14196                }
14197                update_remote_object_on(&tx, commit_object_id, &remote)?;
14198            }
14199            tx.commit().map_err(DbError::from)?;
14200            exclusive_cleanup.extend(commit_cleanup);
14201            Ok(exclusive_cleanup)
14202        })
14203        .await
14204    }
14205
14206    pub(crate) async fn prepare_serial_abandonment_authority_cleanup(
14207        &self,
14208        plan: &crate::sync::store_pull::SerialResolutionPlan,
14209    ) -> Result<Option<CandidateCleanupObject>, DbError> {
14210        let prepared = self
14211            .prepared_serial_candidate_abandonment()
14212            .await?
14213            .ok_or_else(|| {
14214                DbError::Message("Serial candidate abandonment is not prepared".to_string())
14215            })?;
14216        let accepted_refs = plan
14217            .commits
14218            .iter()
14219            .map(|commit| commit.commit_ref.clone())
14220            .collect::<Vec<_>>();
14221        if accepted_refs.is_empty() {
14222            return Err(DbError::Message(
14223                "Serial abandonment cleanup requires an accepted successor".to_string(),
14224            ));
14225        }
14226        let authority_ref = StoreBatchCommitRef::from_commit(
14227            &prepared.authority.value,
14228            StoreCommitCoord::Serial {
14229                sequence: prepared.authority.value.seq(),
14230            },
14231            prepared.authority.object.clone(),
14232        )
14233        .map_err(|error| DbError::Message(error.to_string()))?;
14234        if accepted_refs.first() == Some(&authority_ref) {
14235            return Ok(None);
14236        }
14237        let head_bytes = plan.head_object.bytes.clone();
14238        if plan.head.to_bytes() != head_bytes
14239            || !matches!(
14240                &plan.head.state,
14241                StoreSerialHeadState::Commit { commit, .. }
14242                    if Some(commit) == accepted_refs.last()
14243            )
14244        {
14245            return Err(DbError::Message(
14246                "Serial abandonment cleanup head differs from its accepted suffix".to_string(),
14247            ));
14248        }
14249        let mut predecessor = prepared.base.clone();
14250        for commit in &plan.commits {
14251            commit
14252                .commit_ref
14253                .verify_commit(&commit.commit)
14254                .map_err(|error| DbError::Message(error.to_string()))?;
14255            if commit.commit.order.predecessor() != predecessor.as_ref() {
14256                return Err(DbError::Message(
14257                    "Serial abandonment accepted suffix has a broken predecessor".to_string(),
14258                ));
14259            }
14260            predecessor = Some(commit.commit_ref.clone());
14261        }
14262        let authority_target = crate::sync::store_commit::StoreBatchCommitDeletionTarget {
14263            coord: authority_ref.coord.clone(),
14264            object: authority_ref.object.clone(),
14265            canonical_signed_bytes: prepared.authority.bytes.clone(),
14266        };
14267        let suffix = crate::sync::remote_object::SerialAcceptedSuffix {
14268            predecessor: prepared.base,
14269            commits: accepted_refs,
14270            canonical_signed_head_bytes: head_bytes,
14271            observed_version_hash: ObjectHash::digest(
14272                plan.head_object.version.cloud().as_provider().as_bytes(),
14273            ),
14274        };
14275        let expected_state = prepared.durable_state;
14276        self.call(move |conn| {
14277            let state_matches: bool = conn
14278                .query_row(
14279                    "SELECT EXISTS(
14280                         SELECT 1 FROM protocol_state WHERE key = ?1 AND value = ?2
14281                     )",
14282                    rusqlite::params![SERIAL_CANDIDATE_ABANDONMENT_STATE_KEY, expected_state],
14283                    |row| row.get(0),
14284                )
14285                .map_err(DbError::from)?;
14286            if !state_matches {
14287                return Err(DbError::Message(
14288                    "Serial abandonment durable state changed before authority cleanup"
14289                        .to_string(),
14290                ));
14291            }
14292            let object_id = remote_object_id(&authority_ref.object);
14293            let mut remote = load_remote_object_on(conn, object_id)?;
14294            remote
14295                .begin_candidate_nonactivation(
14296                    crate::sync::remote_object::CandidateNonactivation {
14297                        candidate: authority_target.clone(),
14298                        proof: crate::sync::remote_object::CandidateNonactivationProof::SerialImmediateSuccessor {
14299                            accepted_suffix: suffix,
14300                            losing_prefix: vec![authority_target],
14301                        },
14302                    },
14303                )
14304                .map_err(|error| {
14305                    DbError::Message(format!(
14306                        "record Serial abandonment nonactivation for {object_id}: {error}"
14307                    ))
14308                })?;
14309            let target = remote.cleanup_target().cloned();
14310            update_remote_object_on(conn, object_id, &remote)?;
14311            Ok(target.map(|object| CandidateCleanupObject { object }))
14312        })
14313        .await
14314    }
14315
14316    pub(crate) async fn discard_serial_branch_after_abandonment(
14317        &self,
14318        branch_id: PendingBranchId,
14319        plan: crate::sync::store_pull::SerialResolutionPlan,
14320    ) -> Result<(), DbError> {
14321        let prepared = self
14322            .prepared_serial_candidate_abandonment()
14323            .await?
14324            .ok_or_else(|| {
14325                DbError::Message("Serial candidate abandonment is not prepared".to_string())
14326            })?;
14327        if prepared.branch_id != branch_id {
14328            return Err(DbError::Message(
14329                "Serial abandonment belongs to another branch".to_string(),
14330            ));
14331        }
14332        let authority_ref = StoreBatchCommitRef::from_commit(
14333            &prepared.authority.value,
14334            StoreCommitCoord::Serial {
14335                sequence: prepared.authority.value.seq(),
14336            },
14337            prepared.authority.object.clone(),
14338        )
14339        .map_err(|error| DbError::Message(error.to_string()))?;
14340        let authority_accepted = plan
14341            .commits
14342            .first()
14343            .is_some_and(|commit| commit.commit_ref == authority_ref);
14344        let expected_state = prepared.durable_state;
14345        let synced_tables = self.synced_tables().to_vec();
14346        let statuses = self.state.write_statuses.clone();
14347        self.call(move |conn| {
14348            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
14349            let write_ids =
14350                Self::apply_serial_resolution_on(&tx, &synced_tables, &branch_id, plan)?;
14351            let object_id = remote_object_id(&authority_ref.object);
14352            let remote = load_remote_object_on(&tx, object_id)?;
14353            if authority_accepted {
14354                let retained = remote.into_activated(&authority_ref).map_err(|error| {
14355                    DbError::Message(format!("activate Serial abandonment authority: {error}"))
14356                })?;
14357                update_remote_object_on(&tx, object_id, &retained)?;
14358            } else {
14359                if !remote
14360                    .candidate_cleanup_complete(&authority_ref)
14361                    .map_err(|error| {
14362                        DbError::Message(format!(
14363                            "validate Serial abandonment authority cleanup: {error}"
14364                        ))
14365                    })?
14366                {
14367                    return Err(DbError::Message(
14368                        "Serial abandonment authority cleanup is incomplete".to_string(),
14369                    ));
14370                }
14371                let removed = tx
14372                    .execute(
14373                        "DELETE FROM remote_objects WHERE object_id = ?1",
14374                        [object_id.to_string()],
14375                    )
14376                    .map_err(DbError::from)?;
14377                if removed != 1 {
14378                    return Err(DbError::Message(
14379                        "Serial abandonment authority disappeared during removal".to_string(),
14380                    ));
14381                }
14382            }
14383            let removed = tx
14384                .execute(
14385                    "DELETE FROM protocol_state WHERE key = ?1 AND value = ?2",
14386                    rusqlite::params![SERIAL_CANDIDATE_ABANDONMENT_STATE_KEY, expected_state],
14387                )
14388                .map_err(DbError::from)?;
14389            if removed != 1 {
14390                return Err(DbError::Message(
14391                    "Serial abandonment durable state disappeared".to_string(),
14392                ));
14393            }
14394            let resolution = WriteResolution::Discarded;
14395            Self::resolve_unpublished_writes_on(&tx, &write_ids, &resolution)?;
14396            tx.commit().map_err(DbError::from)?;
14397            let status = WriteStatus::Resolved(resolution);
14398            for write_id in write_ids {
14399                Self::notify_write_status_in(&statuses, &write_id, status.clone());
14400            }
14401            Ok(())
14402        })
14403        .await
14404    }
14405
14406    pub(crate) async fn remove_losing_serial_abandonment_authority(&self) -> Result<(), DbError> {
14407        let prepared = self
14408            .prepared_serial_candidate_abandonment()
14409            .await?
14410            .ok_or_else(|| {
14411                DbError::Message("Serial candidate abandonment is not prepared".to_string())
14412            })?;
14413        let authority_ref = StoreBatchCommitRef::from_commit(
14414            &prepared.authority.value,
14415            StoreCommitCoord::Serial {
14416                sequence: prepared.authority.value.seq(),
14417            },
14418            prepared.authority.object.clone(),
14419        )
14420        .map_err(|error| DbError::Message(error.to_string()))?;
14421        let expected_state = prepared.durable_state;
14422        self.call(move |conn| {
14423            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
14424            let object_id = remote_object_id(&authority_ref.object);
14425            let remote = load_remote_object_on(&tx, object_id)?;
14426            if !remote
14427                .candidate_cleanup_complete(&authority_ref)
14428                .map_err(|error| {
14429                    DbError::Message(format!(
14430                        "validate losing Serial abandonment cleanup: {error}"
14431                    ))
14432                })?
14433            {
14434                return Err(DbError::Message(
14435                    "losing Serial abandonment cleanup is incomplete".to_string(),
14436                ));
14437            }
14438            let removed = tx
14439                .execute(
14440                    "DELETE FROM remote_objects WHERE object_id = ?1",
14441                    [object_id.to_string()],
14442                )
14443                .map_err(DbError::from)?;
14444            if removed != 1 {
14445                return Err(DbError::Message(
14446                    "losing Serial abandonment authority disappeared".to_string(),
14447                ));
14448            }
14449            let removed = tx
14450                .execute(
14451                    "DELETE FROM protocol_state WHERE key = ?1 AND value = ?2",
14452                    rusqlite::params![SERIAL_CANDIDATE_ABANDONMENT_STATE_KEY, expected_state],
14453                )
14454                .map_err(DbError::from)?;
14455            if removed != 1 {
14456                return Err(DbError::Message(
14457                    "Serial abandonment durable state disappeared".to_string(),
14458                ));
14459            }
14460            tx.commit().map_err(DbError::from)
14461        })
14462        .await
14463    }
14464
14465    pub(crate) async fn mark_candidate_cleanup_absent(
14466        &self,
14467        object: ExactObjectRef,
14468    ) -> Result<(), DbError> {
14469        self.call(move |conn| {
14470            let object_id = remote_object_id(&object);
14471            let mut remote = load_remote_object_on(conn, object_id)?;
14472            if remote.cleanup_target() != Some(&object) {
14473                return Err(DbError::Message(format!(
14474                    "remote object {object_id} is not awaiting exact cleanup"
14475                )));
14476            }
14477            remote.mark_absent_verified().map_err(|error| {
14478                DbError::Message(format!("mark candidate {object_id} absent: {error}"))
14479            })?;
14480            update_remote_object_on(conn, object_id, &remote)
14481        })
14482        .await
14483    }
14484
14485    /// Install only blob bindings whose exact row stamp won the enclosing
14486    /// changeset. The caller owns the transaction, so app rows, locator facts,
14487    /// and the materialized commit position either all commit or all roll back.
14488    pub(crate) fn install_winning_blob_bindings_on(
14489        conn: &Connection,
14490        gates: &Gates,
14491        synced_tables: &[SyncedTable],
14492        package: &AudiencePackage,
14493        activation: &BlobActivation,
14494        winning_rows: &[crate::sync::apply::WinningRow],
14495    ) -> Result<usize, DbError> {
14496        if package.commit_coord() != &activation.coord {
14497            return Err(DbError::Message(format!(
14498                "blob activation {:?} does not match audience package {:?}",
14499                activation.coord,
14500                package.commit_coord()
14501            )));
14502        }
14503
14504        let package_audience = package.audience().remote_audience();
14505        for winner in winning_rows {
14506            let Some(table) = synced_tables
14507                .iter()
14508                .find(|table| table.name() == winner.table)
14509            else {
14510                return Err(DbError::Message(format!(
14511                    "winning changeset row names undeclared table {:?}",
14512                    winner.table
14513                )));
14514            };
14515            let Some(declaration) = table.blob() else {
14516                continue;
14517            };
14518            match winner.row_stamp.as_deref() {
14519                Some(row_stamp) => conn.execute(
14520                    "DELETE FROM row_blob_locators
14521                     WHERE table_name = ?1 AND row_id = ?2 AND column_name = ?3
14522                       AND row_stamp <> ?4",
14523                    rusqlite::params![
14524                        winner.table,
14525                        winner.row_id,
14526                        declaration.id_column,
14527                        row_stamp,
14528                    ],
14529                ),
14530                None => conn.execute(
14531                    "DELETE FROM row_blob_locators
14532                     WHERE table_name = ?1 AND row_id = ?2 AND column_name = ?3",
14533                    rusqlite::params![winner.table, winner.row_id, declaration.id_column],
14534                ),
14535            }
14536            .map_err(DbError::from)?;
14537        }
14538        let mut installed = 0;
14539        for binding in package.blob_bindings() {
14540            let Some(table) = synced_tables
14541                .iter()
14542                .find(|table| table.name() == binding.table())
14543            else {
14544                return Err(DbError::Message(format!(
14545                    "blob binding names undeclared table {:?}",
14546                    binding.table()
14547                )));
14548            };
14549            let declaration = table.blob().ok_or_else(|| {
14550                DbError::Message(format!(
14551                    "blob binding names table {:?}, which has no blob declaration",
14552                    binding.table()
14553                ))
14554            })?;
14555            if binding.column() != declaration.id_column {
14556                return Err(DbError::Message(format!(
14557                    "blob binding column {:?} does not match declared blob-id column {:?} on table {:?}",
14558                    binding.column(), declaration.id_column, binding.table()
14559                )));
14560            }
14561
14562            let Some(row) = live_blob_row(conn, binding.table(), binding.row_id(), declaration)?
14563            else {
14564                continue;
14565            };
14566            if row.stamp != binding.row_stamp() {
14567                continue;
14568            }
14569
14570            let live_audience =
14571                gate::live_row_audience(conn, gates, binding.table(), binding.row_id()).map_err(
14572                    |error| {
14573                        DbError::Message(format!(
14574                            "resolve winning blob row audience for {:?}/{:?}: {error}",
14575                            binding.table(),
14576                            binding.row_id()
14577                        ))
14578                    },
14579                )?;
14580            let live_audience = RemoteAudience::try_from(live_audience).map_err(|error| {
14581                DbError::Message(format!(
14582                    "winning blob row {:?}/{:?} is not remote: {error}",
14583                    binding.table(),
14584                    binding.row_id()
14585                ))
14586            })?;
14587            if live_audience != package_audience {
14588                return Err(DbError::Message(format!(
14589                    "winning blob row {:?}/{:?} belongs to {:?}, but its package belongs to {:?}",
14590                    binding.table(),
14591                    binding.row_id(),
14592                    live_audience,
14593                    package_audience
14594                )));
14595            }
14596            validate_live_blob_row(binding, declaration, &row, &live_audience)?;
14597
14598            let locator = binding.blob().locator();
14599            let locator_hash = locator.locator_hash();
14600            let remote_object_id = remote_object_id(binding.blob().object());
14601            let remote = load_remote_object_on(conn, remote_object_id)?;
14602            if !remote.is_activated_stored_blob() {
14603                return Err(DbError::Message(format!(
14604                    "blob locator {locator_hash} does not reference an activated uploaded blob"
14605                )));
14606            }
14607            validate_remote_object_on(
14608                conn,
14609                remote_object_id,
14610                binding.blob().object(),
14611                &locator.to_bytes(),
14612                RemoteStoredRepresentationRef::Blob,
14613            )?;
14614            conn.execute(
14615                "INSERT INTO blob_locators
14616                 (remote_object_id, locator_hash)
14617                 VALUES (?1, ?2)
14618                 ON CONFLICT(remote_object_id) DO NOTHING",
14619                rusqlite::params![remote_object_id.to_string(), locator_hash.to_string(),],
14620            )
14621            .map_err(DbError::from)?;
14622            validate_stored_locator_on(conn, binding.blob())?;
14623
14624            let audience_authority =
14625                serde_json::to_string(package.audience()).map_err(|error| {
14626                    DbError::Message(format!("serialize row blob audience authority: {error}"))
14627                })?;
14628            conn.execute(
14629                "INSERT INTO row_blob_locators
14630                 (table_name, row_id, column_name, row_stamp, audience_authority, remote_object_id)
14631                 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
14632                 ON CONFLICT(table_name, row_id, column_name, row_stamp) DO NOTHING",
14633                rusqlite::params![
14634                    binding.table(),
14635                    binding.row_id(),
14636                    binding.column(),
14637                    binding.row_stamp(),
14638                    audience_authority,
14639                    remote_object_id.to_string(),
14640                ],
14641            )
14642            .map_err(DbError::from)?;
14643            validate_stored_row_binding_on(conn, binding, package.audience(), remote_object_id)?;
14644            installed += 1;
14645        }
14646        Ok(installed)
14647    }
14648
14649    pub(crate) fn install_pulled_blob_activations_on(
14650        conn: &Connection,
14651        package: &AudiencePackage,
14652        owner: &StoreBatchCommitRef,
14653    ) -> Result<(), DbError> {
14654        if package.commit_coord() != &owner.coord {
14655            return Err(DbError::Message(
14656                "pulled blob package coordinate differs from its activating commit".to_string(),
14657            ));
14658        }
14659        for binding in package.blob_bindings() {
14660            let stored = binding.blob();
14661            let object_id = remote_object_id(stored.object());
14662            let exists: bool = conn
14663                .query_row(
14664                    "SELECT EXISTS(SELECT 1 FROM remote_objects WHERE object_id = ?1)",
14665                    [object_id.to_string()],
14666                    |row| row.get(0),
14667                )
14668                .map_err(DbError::from)?;
14669            let remote = if exists {
14670                let mut remote = load_remote_object_on(conn, object_id)?;
14671                remote
14672                    .merge_blob_activation(stored, owner)
14673                    .map_err(|error| {
14674                        DbError::Message(format!(
14675                            "merge pulled blob activation {object_id}: {error}"
14676                        ))
14677                    })?;
14678                remote
14679            } else {
14680                RemoteObjectRecord::activated_blob(stored, owner.clone()).map_err(|error| {
14681                    DbError::Message(format!(
14682                        "construct pulled blob activation {object_id}: {error}"
14683                    ))
14684                })?
14685            };
14686            let state = serde_json::to_string(&remote).map_err(|error| {
14687                DbError::Message(format!("serialize pulled blob activation: {error}"))
14688            })?;
14689            conn.execute(
14690                "INSERT INTO remote_objects (object_id, state) VALUES (?1, ?2) \
14691                 ON CONFLICT(object_id) DO UPDATE SET state = excluded.state",
14692                rusqlite::params![object_id.to_string(), state],
14693            )
14694            .map_err(DbError::from)?;
14695        }
14696        Ok(())
14697    }
14698
14699    pub async fn row_blob_ref(&self, table: &str, row_id: &str) -> Result<RowBlobRef, DbError> {
14700        let table = self
14701            .synced_tables()
14702            .iter()
14703            .find(|candidate| candidate.name() == table)
14704            .cloned()
14705            .ok_or_else(|| DbError::Message(format!("undeclared synced table {table:?}")))?;
14706        if table.blob().is_none() {
14707            return Err(DbError::Message(format!(
14708                "synced table {:?} has no blob declaration",
14709                table.name()
14710            )));
14711        }
14712        let row_id = row_id.to_string();
14713        let gates = self.gates();
14714        self.call(move |conn| Self::row_blob_ref_on(conn, &gates, &table, &row_id))
14715            .await
14716    }
14717
14718    pub async fn validate_row_blob_ref(&self, reference: &RowBlobRef) -> Result<(), DbError> {
14719        let current = self
14720            .row_blob_ref(reference.table(), reference.row_id())
14721            .await?;
14722        if &current != reference {
14723            return Err(DbError::Message(format!(
14724                "row blob reference {:?}/{:?}/{:?} at {:?} is stale",
14725                reference.table(),
14726                reference.row_id(),
14727                reference.column(),
14728                reference.row_stamp()
14729            )));
14730        }
14731        Ok(())
14732    }
14733
14734    pub async fn row_blob_refs_for_root(
14735        &self,
14736        root_table: &str,
14737        root_id: &str,
14738    ) -> Result<Vec<RowBlobRef>, DbError> {
14739        let root_table = root_table.to_string();
14740        let root_id = root_id.to_string();
14741        let gates = self.gates();
14742        let tables = self.synced_tables().to_vec();
14743        self.call(move |conn| {
14744            Self::row_blob_refs_for_root_on(conn, &gates, &tables, &root_table, &root_id)
14745        })
14746        .await
14747    }
14748
14749    pub(crate) fn row_blob_refs_for_root_on(
14750        conn: &Connection,
14751        gates: &Gates,
14752        tables: &[SyncedTable],
14753        root_table: &str,
14754        root_id: &str,
14755    ) -> Result<Vec<RowBlobRef>, DbError> {
14756        let mut rows = gates
14757            .subtree_rows(conn, root_table, root_id)
14758            .map_err(|error| DbError::Message(error.to_string()))?
14759            .into_iter()
14760            .collect::<Vec<_>>();
14761        rows.sort();
14762        let tables = tables
14763            .iter()
14764            .map(|table| (table.name(), table))
14765            .collect::<BTreeMap<_, _>>();
14766        rows.into_iter()
14767            .filter_map(|(table_name, row_id)| {
14768                tables
14769                    .get(table_name.as_str())
14770                    .filter(|table| table.blob().is_some())
14771                    .map(|table| Self::row_blob_ref_on(conn, gates, table, &row_id))
14772            })
14773            .collect()
14774    }
14775
14776    pub(crate) fn upload_entries_for_rows_on(
14777        conn: &Connection,
14778        rows: &[RowBlobRef],
14779    ) -> Result<Vec<OutboxEntry>, DbError> {
14780        rows.iter()
14781            .filter_map(|row| {
14782                match upload_entry_for_identity_on(
14783                    conn,
14784                    row.table(),
14785                    row.row_id(),
14786                    row.column(),
14787                    row.row_stamp(),
14788                ) {
14789                    Ok(Some(entry)) => Some(Ok(entry)),
14790                    Ok(None) => None,
14791                    Err(error) => Some(Err(error)),
14792                }
14793            })
14794            .collect()
14795    }
14796
14797    pub(crate) fn upload_entries_for_root_on(
14798        conn: &Connection,
14799        gates: &Gates,
14800        tables: &[SyncedTable],
14801        root_table: &str,
14802        root_id: &str,
14803    ) -> Result<Vec<OutboxEntry>, DbError> {
14804        let rows = Self::row_blob_refs_for_root_on(conn, gates, tables, root_table, root_id)?;
14805        Self::upload_entries_for_rows_on(conn, &rows)
14806    }
14807
14808    pub(crate) fn stored_blob_reference_state_on(
14809        conn: &Connection,
14810        gates: &Gates,
14811        tables: &[SyncedTable],
14812        stored: &StoredBlobRef,
14813    ) -> Result<StoredBlobReferenceState, DbError> {
14814        let exact_object_id = remote_object_id(stored.object()).to_string();
14815        let mut statement = conn
14816            .prepare(
14817                "SELECT table_name, row_id, row_stamp FROM row_blob_locators
14818                 WHERE remote_object_id = ?1",
14819            )
14820            .map_err(DbError::from)?;
14821        let bindings = statement
14822            .query_map([exact_object_id], |row| {
14823                Ok((
14824                    row.get::<_, String>(0)?,
14825                    row.get::<_, String>(1)?,
14826                    row.get::<_, String>(2)?,
14827                ))
14828            })
14829            .map_err(DbError::from)?
14830            .collect::<Result<Vec<_>, _>>()
14831            .map_err(DbError::from)?;
14832        drop(statement);
14833        let mut unresolved = false;
14834        for (table_name, row_id, row_stamp) in bindings {
14835            let table = tables
14836                .iter()
14837                .find(|candidate| candidate.name() == table_name)
14838                .ok_or_else(|| {
14839                    DbError::Message(format!(
14840                        "stored blob binding names undeclared table {table_name:?}"
14841                    ))
14842                })?;
14843            let declaration = table.blob().ok_or_else(|| {
14844                DbError::Message(format!(
14845                    "stored blob binding names table {table_name:?} without a blob declaration"
14846                ))
14847            })?;
14848            let Some(live) = live_blob_row(conn, &table_name, &row_id, declaration)? else {
14849                continue;
14850            };
14851            if live.stamp != row_stamp {
14852                continue;
14853            }
14854            match gates
14855                .root_kept_of(conn, &table_name, &row_id)
14856                .map_err(|error| DbError::Message(error.to_string()))?
14857            {
14858                Some(false) => continue,
14859                None => {
14860                    unresolved = true;
14861                    continue;
14862                }
14863                Some(true) => {}
14864            }
14865            let reference = Self::row_blob_ref_on(conn, gates, table, &row_id)?;
14866            if matches!(reference.authority(), RowBlobAuthority::Remote(_))
14867                && reference.stored() == Some(stored)
14868            {
14869                return Ok(StoredBlobReferenceState::LiveRemote);
14870            }
14871        }
14872        Ok(if unresolved {
14873            StoredBlobReferenceState::Unresolved
14874        } else {
14875            StoredBlobReferenceState::NotLiveRemote
14876        })
14877    }
14878
14879    pub(crate) fn row_blob_ref_on(
14880        conn: &Connection,
14881        gates: &Gates,
14882        table: &SyncedTable,
14883        row_id: &str,
14884    ) -> Result<RowBlobRef, DbError> {
14885        let declaration = table.blob().ok_or_else(|| {
14886            DbError::Message(format!(
14887                "synced table {:?} has no blob declaration",
14888                table.name()
14889            ))
14890        })?;
14891        let row = live_blob_row(conn, table.name(), row_id, declaration)?.ok_or_else(|| {
14892            DbError::Message(format!(
14893                "blob-bearing row {:?}/{row_id:?} does not exist",
14894                table.name()
14895            ))
14896        })?;
14897        let audience =
14898            gate::live_row_audience(conn, gates, table.name(), row_id).map_err(|error| {
14899                DbError::Message(format!(
14900                    "resolve blob row audience for {:?}/{row_id:?}: {error}",
14901                    table.name()
14902                ))
14903            })?;
14904        let (authority, stored) = match RemoteAudience::try_from(audience.clone()) {
14905            Err(_) if audience == Audience::Local => (RowBlobAuthority::Local, None),
14906            Err(error) => {
14907                return Err(DbError::Message(format!(
14908                    "blob row {:?}/{row_id:?} has invalid audience: {error}",
14909                    table.name()
14910                )));
14911            }
14912            Ok(remote_audience) => {
14913                let installed: Option<(String, String)> = conn
14914                    .query_row(
14915                        "SELECT binding.audience_authority, locator.remote_object_id
14916                         FROM row_blob_locators AS binding
14917                         JOIN blob_locators AS locator
14918                           ON locator.remote_object_id = binding.remote_object_id
14919                         WHERE binding.table_name = ?1
14920                           AND binding.row_id = ?2
14921                           AND binding.column_name = ?3
14922                           AND binding.row_stamp = ?4",
14923                        rusqlite::params![table.name(), row_id, declaration.id_column, row.stamp,],
14924                        |row| Ok((row.get(0)?, row.get(1)?)),
14925                    )
14926                    .optional()
14927                    .map_err(DbError::from)?;
14928                let exact = if let Some((authority_json, remote_object_id)) = installed {
14929                    let package_authority: crate::sync::audience_package::PackageAudience =
14930                        serde_json::from_str(&authority_json).map_err(|error| {
14931                            DbError::Message(format!(
14932                                "remote blob row {:?}/{row_id:?} has invalid audience authority: {error}",
14933                                table.name()
14934                            ))
14935                        })?;
14936                    let remote_object_id = remote_object_id.parse().map_err(|error| {
14937                        DbError::Message(format!(
14938                            "remote blob row {:?}/{row_id:?} has invalid prepared object id: {error}",
14939                            table.name()
14940                        ))
14941                    })?;
14942                    let remote = load_remote_object_on(conn, remote_object_id)?;
14943                    if !remote.is_activated_stored_blob() {
14944                        return Err(DbError::Message(format!(
14945                            "remote blob row {:?}/{row_id:?} references a blob without activated ownership",
14946                            table.name()
14947                        )));
14948                    }
14949                    let locator = BlobLocator::parse(remote.bytes().canonical_semantic_bytes())
14950                        .map_err(|error| {
14951                            DbError::Message(format!(
14952                                "remote blob row {:?}/{row_id:?} has invalid locator: {error}",
14953                                table.name()
14954                            ))
14955                        })?;
14956                    let stored = StoredBlobRef::new(locator, remote.object().clone()).map_err(
14957                        |error| {
14958                            DbError::Message(format!(
14959                                "remote blob row {:?}/{row_id:?} has invalid stored blob reference: {error}",
14960                                table.name()
14961                            ))
14962                        },
14963                    )?;
14964                    Some((package_authority, stored))
14965                } else {
14966                    created_upload_handoff_on(
14967                        conn,
14968                        table.name(),
14969                        row_id,
14970                        &declaration.id_column,
14971                        &row.stamp,
14972                    )?
14973                    .map(|handoff| (handoff.authority, handoff.stored))
14974                };
14975                let Some((package_authority, stored)) = exact else {
14976                    return RowBlobRef::new(
14977                        table.name().to_string(),
14978                        row_id.to_string(),
14979                        row.stamp,
14980                        declaration.id_column.clone(),
14981                        BlobRef {
14982                            namespace: declaration.namespace.clone(),
14983                            id: row.blob_id,
14984                            scope: declaration.scope.clone(),
14985                            cloud_path: row.cloud_path,
14986                            provenance: declaration.provenance,
14987                            fill: declaration.fill,
14988                        },
14989                        row.plaintext_size,
14990                        row.plaintext_hash,
14991                        RowBlobAuthority::PendingRemote(remote_audience),
14992                        None,
14993                    )
14994                    .map_err(DbError::Message);
14995                };
14996                if package_authority.remote_audience() != remote_audience {
14997                    return Err(DbError::Message(format!(
14998                        "remote blob row {:?}/{row_id:?} has audience authority {:?}, expected {remote_audience:?}",
14999                        table.name(), package_authority
15000                    )));
15001                }
15002                validate_live_blob_locator(
15003                    table.name(),
15004                    row_id,
15005                    &declaration.id_column,
15006                    &row.stamp,
15007                    &stored,
15008                    declaration,
15009                    &row,
15010                    &remote_audience,
15011                )?;
15012                (RowBlobAuthority::Remote(package_authority), Some(stored))
15013            }
15014        };
15015        let blob = BlobRef {
15016            namespace: declaration.namespace.clone(),
15017            id: row.blob_id.clone(),
15018            scope: declaration.scope.clone(),
15019            cloud_path: row.cloud_path.clone(),
15020            provenance: declaration.provenance,
15021            fill: declaration.fill,
15022        };
15023        RowBlobRef::new(
15024            table.name().to_string(),
15025            row_id.to_string(),
15026            row.stamp,
15027            declaration.id_column.clone(),
15028            blob,
15029            row.plaintext_size,
15030            row.plaintext_hash,
15031            authority,
15032            stored,
15033        )
15034        .map_err(DbError::Message)
15035    }
15036
15037    // ---- Row-bound blob outbox ----
15038
15039    pub fn enqueue_upload_on(
15040        conn: &Connection,
15041        root_table: &str,
15042        root_id: &str,
15043        row: &RowBlobRef,
15044        source_path: &Path,
15045        retain_pinned: bool,
15046        created_at: &str,
15047    ) -> Result<(), DbError> {
15048        if row.authority() != &RowBlobAuthority::Local || row.stored().is_some() {
15049            return Err(DbError::Message(
15050                "cloud upload requires an exact Local row blob reference".to_string(),
15051            ));
15052        }
15053        let source_path = source_path.to_str().ok_or_else(|| {
15054            DbError::Message(format!(
15055                "blob source path for {}/{}/{} is not UTF-8: {source_path:?}",
15056                row.table(),
15057                row.row_id(),
15058                row.column()
15059            ))
15060        })?;
15061        let encoded = serde_json::to_string(row)
15062            .map_err(|error| DbError::Message(format!("serialize row blob ref: {error}")))?;
15063        let pending = serde_json::to_string(&OutboxUploadState::Pending).map_err(|error| {
15064            DbError::Message(format!("serialize pending blob upload state: {error}"))
15065        })?;
15066        conn.execute(
15067            "INSERT INTO cloud_outbox
15068             (operation, table_name, row_id, column_name, row_stamp, root_table, root_id,
15069              row_ref, upload_state, source_path, retain_pinned, created_at)
15070             VALUES ('upload', ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)
15071             ON CONFLICT(operation, table_name, row_id, column_name, row_stamp) DO UPDATE SET
15072               root_table = excluded.root_table,
15073               root_id = excluded.root_id,
15074               source_path = excluded.source_path,
15075               retain_pinned = excluded.retain_pinned,
15076               attempt_count = 0,
15077               last_error = NULL,
15078               last_attempt_at = NULL
15079             WHERE cloud_outbox.row_ref = excluded.row_ref
15080               AND cloud_outbox.root_table = excluded.root_table
15081               AND cloud_outbox.root_id = excluded.root_id",
15082            rusqlite::params![
15083                row.table(),
15084                row.row_id(),
15085                row.column(),
15086                row.row_stamp(),
15087                root_table,
15088                root_id,
15089                encoded,
15090                pending,
15091                source_path,
15092                retain_pinned,
15093                created_at,
15094            ],
15095        )
15096        .map_err(DbError::from)
15097        .and_then(|changed| {
15098            if changed == 1 {
15099                Ok(())
15100            } else {
15101                Err(DbError::Message(format!(
15102                    "upload outbox identity {}/{}/{}/{} carries different row facts",
15103                    row.table(),
15104                    row.row_id(),
15105                    row.column(),
15106                    row.row_stamp()
15107                )))
15108            }
15109        })
15110    }
15111
15112    pub async fn mark_cloud_upload_prepared(
15113        &self,
15114        entry: &OutboxEntry,
15115        authority: crate::sync::audience_package::PackageAudience,
15116        stored: StoredBlobRef,
15117        spool_path: PathBuf,
15118    ) -> Result<(), DbError> {
15119        let OutboxOperation::Upload { row, state, .. } = &entry.operation else {
15120            return Err(DbError::Message(
15121                "only an upload outbox entry can own a prepared blob".to_string(),
15122            ));
15123        };
15124        if state != &OutboxUploadState::Pending {
15125            return Err(DbError::Message(
15126                "blob upload is already prepared".to_string(),
15127            ));
15128        }
15129        let locator = stored.locator();
15130        if locator.namespace() != row.blob().namespace
15131            || locator.blob_id() != row.blob().id
15132            || locator.plaintext_size() != row.plaintext_size()
15133            || locator.plaintext_hash() != row.plaintext_hash()
15134            || locator
15135                .scope()
15136                .is_some_and(|scope| scope != &row.blob().scope)
15137        {
15138            return Err(DbError::Message(
15139                "prepared blob differs from its exact Local row version".to_string(),
15140            ));
15141        }
15142        if locator.audience() != authority.remote_audience() {
15143            return Err(DbError::Message(
15144                "prepared blob audience differs from its package authority".to_string(),
15145            ));
15146        }
15147        let prepared = OutboxUploadState::Prepared {
15148            authority,
15149            stored,
15150            spool_path,
15151        };
15152        let prepared_json = serde_json::to_string(&prepared).map_err(|error| {
15153            DbError::Message(format!("serialize prepared blob upload: {error}"))
15154        })?;
15155        let pending_json = serde_json::to_string(&OutboxUploadState::Pending)
15156            .map_err(|error| DbError::Message(format!("serialize pending blob upload: {error}")))?;
15157        let id = entry.id;
15158        let table = row.table().to_string();
15159        let row_id = row.row_id().to_string();
15160        let column = row.column().to_string();
15161        let row_stamp = row.row_stamp().to_string();
15162        self.call(move |conn| {
15163            let updated = conn
15164                .execute(
15165                    "UPDATE cloud_outbox SET upload_state = ?1
15166                     WHERE id = ?2 AND operation = 'upload' AND table_name = ?3
15167                       AND row_id = ?4 AND column_name = ?5 AND row_stamp = ?6
15168                       AND upload_state = ?7",
15169                    rusqlite::params![
15170                        prepared_json,
15171                        id,
15172                        table,
15173                        row_id,
15174                        column,
15175                        row_stamp,
15176                        pending_json,
15177                    ],
15178                )
15179                .map_err(DbError::from)?;
15180            if updated != 1 {
15181                return Err(DbError::Message(
15182                    "upload outbox entry changed before prepared-object handoff".to_string(),
15183                ));
15184            }
15185            Ok(())
15186        })
15187        .await
15188    }
15189
15190    pub async fn mark_cloud_upload_created(&self, entry: &OutboxEntry) -> Result<(), DbError> {
15191        let OutboxOperation::Upload { row, state, .. } = &entry.operation else {
15192            return Err(DbError::Message(
15193                "only a prepared upload outbox entry can record cloud creation".to_string(),
15194            ));
15195        };
15196        let OutboxUploadState::Prepared {
15197            authority,
15198            stored,
15199            spool_path,
15200        } = state
15201        else {
15202            return Err(DbError::Message(
15203                "cloud creation requires a prepared upload object".to_string(),
15204            ));
15205        };
15206        let created_json = serde_json::to_string(&OutboxUploadState::Created {
15207            authority: authority.clone(),
15208            stored: stored.clone(),
15209            spool_path: spool_path.clone(),
15210        })
15211        .map_err(|error| DbError::Message(format!("serialize created blob upload: {error}")))?;
15212        let prepared_json = serde_json::to_string(state).map_err(|error| {
15213            DbError::Message(format!("serialize prepared blob upload identity: {error}"))
15214        })?;
15215        let id = entry.id;
15216        let table = row.table().to_string();
15217        let row_id = row.row_id().to_string();
15218        let column = row.column().to_string();
15219        let row_stamp = row.row_stamp().to_string();
15220        self.call(move |conn| {
15221            let updated = conn
15222                .execute(
15223                    "UPDATE cloud_outbox SET upload_state = ?1
15224                     WHERE id = ?2 AND operation = 'upload' AND table_name = ?3
15225                       AND row_id = ?4 AND column_name = ?5 AND row_stamp = ?6
15226                       AND upload_state = ?7",
15227                    rusqlite::params![
15228                        created_json,
15229                        id,
15230                        table,
15231                        row_id,
15232                        column,
15233                        row_stamp,
15234                        prepared_json,
15235                    ],
15236                )
15237                .map_err(DbError::from)?;
15238            if updated != 1 {
15239                return Err(DbError::Message(
15240                    "upload outbox entry changed before cloud-created handoff".to_string(),
15241                ));
15242            }
15243            Ok(())
15244        })
15245        .await
15246    }
15247
15248    pub fn enqueue_delete_on(
15249        conn: &Connection,
15250        stored: &StoredBlobRef,
15251        created_at: &str,
15252    ) -> Result<(), DbError> {
15253        let encoded = serde_json::to_string(stored)
15254            .map_err(|error| DbError::Message(format!("serialize stored blob ref: {error}")))?;
15255        conn.execute(
15256            "INSERT INTO cloud_outbox (operation, stored_ref, created_at)
15257             VALUES ('delete', ?1, ?2)
15258             ON CONFLICT(stored_ref) DO UPDATE SET
15259               created_at = excluded.created_at,
15260               attempt_count = 0,
15261               last_error = NULL,
15262               last_attempt_at = NULL",
15263            (encoded, created_at),
15264        )
15265        .map(|_| ())
15266        .map_err(DbError::from)
15267    }
15268
15269    pub async fn get_pending_cloud_uploads(&self) -> Result<Vec<OutboxEntry>, DbError> {
15270        self.pending_outbox("upload").await
15271    }
15272
15273    pub async fn get_pending_cloud_deletes(&self) -> Result<Vec<OutboxEntry>, DbError> {
15274        self.pending_outbox("delete").await
15275    }
15276
15277    async fn pending_outbox(&self, operation: &'static str) -> Result<Vec<OutboxEntry>, DbError> {
15278        self.call(move |conn| {
15279            let mut statement = conn
15280                .prepare(
15281                    "SELECT id, operation, row_ref, stored_ref, source_path, retain_pinned,
15282                            upload_state, attempt_count, last_attempt_at, root_table, root_id
15283                     FROM cloud_outbox WHERE operation = ?1 ORDER BY id",
15284                )
15285                .map_err(DbError::from)?;
15286            let entries = statement
15287                .query_map([operation], row_to_outbox_entry)
15288                .map_err(DbError::from)?
15289                .collect::<Result<Vec<_>, _>>()
15290                .map_err(DbError::from)?;
15291            Ok(entries)
15292        })
15293        .await
15294    }
15295
15296    pub async fn remove_cloud_outbox_entry(&self, entry: &OutboxEntry) -> Result<(), DbError> {
15297        let entry = entry.clone();
15298        self.call(move |conn| Self::remove_cloud_outbox_entry_on(conn, &entry))
15299            .await
15300    }
15301
15302    pub(crate) fn remove_cloud_outbox_entry_on(
15303        conn: &Connection,
15304        entry: &OutboxEntry,
15305    ) -> Result<(), DbError> {
15306        let identity = outbox_identity(&entry.operation)?;
15307        let removed = match identity {
15308            OutboxIdentity::Upload {
15309                table,
15310                row_id,
15311                column,
15312                row_stamp,
15313            } => conn.execute(
15314                "DELETE FROM cloud_outbox WHERE id = ?1 AND operation = 'upload'
15315                 AND table_name = ?2 AND row_id = ?3 AND column_name = ?4 AND row_stamp = ?5",
15316                rusqlite::params![entry.id, table, row_id, column, row_stamp],
15317            ),
15318            OutboxIdentity::Stored { operation, stored } => conn.execute(
15319                "DELETE FROM cloud_outbox WHERE id = ?1 AND operation = ?2 AND stored_ref = ?3",
15320                rusqlite::params![entry.id, operation, stored],
15321            ),
15322        }
15323        .map_err(DbError::from)?;
15324        if removed != 1 {
15325            return Err(DbError::Message(
15326                "cloud outbox entry changed before exact dequeue".to_string(),
15327            ));
15328        }
15329        Ok(())
15330    }
15331
15332    pub(crate) fn finish_cancelled_upload_on(
15333        conn: &Connection,
15334        entry: &OutboxEntry,
15335    ) -> Result<bool, DbError> {
15336        let OutboxOperation::Upload {
15337            root_table,
15338            root_id,
15339            ..
15340        } = &entry.operation
15341        else {
15342            return Err(DbError::Message(
15343                "make_remote cleanup requires an upload entry".to_string(),
15344            ));
15345        };
15346        if !matches!(
15347            Self::make_remote_intent_state(conn, root_table, root_id)?,
15348            Some(MakeRemoteIntentState::Cancelling)
15349        ) {
15350            return Err(DbError::Message(format!(
15351                "make_remote cleanup for {root_table:?}/{root_id:?} lost cancellation ownership"
15352            )));
15353        }
15354        Self::remove_cloud_outbox_entry_on(conn, entry)?;
15355        let remaining: i64 = conn
15356            .query_row(
15357                "SELECT COUNT(*) FROM cloud_outbox
15358                 WHERE operation = 'upload' AND root_table = ?1 AND root_id = ?2",
15359                (root_table, root_id),
15360                |row| row.get(0),
15361            )
15362            .map_err(DbError::from)?;
15363        if remaining != 0 {
15364            return Ok(false);
15365        }
15366        let removed = conn
15367            .execute(
15368                "DELETE FROM blob_make_remote_intents
15369                 WHERE root_table = ?1 AND root_id = ?2 AND state = 'cancelling'",
15370                (root_table, root_id),
15371            )
15372            .map_err(DbError::from)?;
15373        if removed != 1 {
15374            return Err(DbError::Message(format!(
15375                "make_remote cancellation {root_table:?}/{root_id:?} changed before completion"
15376            )));
15377        }
15378        Ok(true)
15379    }
15380
15381    pub async fn record_cloud_outbox_failure(
15382        &self,
15383        entry: &OutboxEntry,
15384        error: &str,
15385        attempted_at: &str,
15386    ) -> Result<(), DbError> {
15387        let id = entry.id;
15388        let identity = outbox_identity(&entry.operation)?;
15389        let error = error.to_string();
15390        let attempted_at = attempted_at.to_string();
15391        self.call(move |conn| {
15392            let updated = match identity {
15393                OutboxIdentity::Upload {
15394                    table,
15395                    row_id,
15396                    column,
15397                    row_stamp,
15398                } => conn.execute(
15399                    "UPDATE cloud_outbox SET attempt_count = attempt_count + 1,
15400                     last_error = ?1, last_attempt_at = ?2
15401                     WHERE id = ?3 AND operation = 'upload' AND table_name = ?4
15402                       AND row_id = ?5 AND column_name = ?6 AND row_stamp = ?7",
15403                    rusqlite::params![error, attempted_at, id, table, row_id, column, row_stamp],
15404                ),
15405                OutboxIdentity::Stored { operation, stored } => conn.execute(
15406                    "UPDATE cloud_outbox SET attempt_count = attempt_count + 1,
15407                     last_error = ?1, last_attempt_at = ?2
15408                     WHERE id = ?3 AND operation = ?4 AND stored_ref = ?5",
15409                    rusqlite::params![error, attempted_at, id, operation, stored],
15410                ),
15411            }
15412            .map_err(DbError::from)?;
15413            if updated != 1 {
15414                return Err(DbError::Message(
15415                    "cloud outbox entry changed before failure recording".to_string(),
15416                ));
15417            }
15418            Ok(())
15419        })
15420        .await
15421    }
15422
15423    pub async fn reset_cloud_outbox_backoff(&self) -> Result<(), DbError> {
15424        self.call(|conn| {
15425            conn.execute(
15426                "UPDATE cloud_outbox SET last_attempt_at = NULL WHERE attempt_count > 0",
15427                [],
15428            )
15429            .map(|_| ())
15430            .map_err(DbError::from)
15431        })
15432        .await
15433    }
15434
15435    // ---- Local blob refs (external user files) ----
15436
15437    pub fn register_external_blob_on(
15438        conn: &Connection,
15439        reference: &RowBlobRef,
15440        path: &Path,
15441    ) -> Result<(), DbError> {
15442        if reference.authority() != &RowBlobAuthority::Local || reference.stored().is_some() {
15443            return Err(DbError::Message(
15444                "external file requires an exact Local row blob reference".to_string(),
15445            ));
15446        }
15447        let path = path.to_str().ok_or_else(|| {
15448            DbError::Message(format!("external blob path is not UTF-8: {path:?}"))
15449        })?;
15450        let size = i64::try_from(reference.plaintext_size()).map_err(|_| {
15451            DbError::Message("external blob plaintext size exceeds SQLite INTEGER".to_string())
15452        })?;
15453        conn.execute(
15454            "INSERT INTO local_blob_refs
15455             (table_name, row_id, column_name, row_stamp, namespace, blob_id,
15456              path, plaintext_size, plaintext_hash)
15457             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
15458             ON CONFLICT(table_name, row_id, column_name, row_stamp) DO UPDATE SET
15459               namespace = excluded.namespace,
15460               blob_id = excluded.blob_id,
15461               path = excluded.path,
15462               plaintext_size = excluded.plaintext_size,
15463               plaintext_hash = excluded.plaintext_hash",
15464            rusqlite::params![
15465                reference.table(),
15466                reference.row_id(),
15467                reference.column(),
15468                reference.row_stamp(),
15469                &reference.blob().namespace,
15470                &reference.blob().id,
15471                path,
15472                size,
15473                reference.plaintext_hash().to_string(),
15474            ],
15475        )
15476        .map(|_| ())
15477        .map_err(DbError::from)
15478    }
15479
15480    pub fn clear_external_blob_on(
15481        conn: &Connection,
15482        reference: &RowBlobRef,
15483    ) -> Result<(), DbError> {
15484        conn.execute(
15485            "DELETE FROM local_blob_refs WHERE table_name = ?1 AND row_id = ?2
15486             AND column_name = ?3 AND row_stamp = ?4",
15487            rusqlite::params![
15488                reference.table(),
15489                reference.row_id(),
15490                reference.column(),
15491                reference.row_stamp(),
15492            ],
15493        )
15494        .map(|_| ())
15495        .map_err(DbError::from)
15496    }
15497
15498    pub fn insert_make_remote_intent_on(
15499        conn: &Connection,
15500        root_table: &str,
15501        root_id: &str,
15502        retain_pinned: bool,
15503    ) -> Result<(), DbError> {
15504        conn.execute(
15505            "INSERT INTO blob_make_remote_intents
15506             (root_table, root_id, retain_pinned, state, write_id)
15507             VALUES (?1, ?2, ?3, 'uploading', NULL)
15508             ON CONFLICT(root_table, root_id) DO UPDATE SET
15509               retain_pinned = excluded.retain_pinned
15510             WHERE blob_make_remote_intents.state = 'uploading'",
15511            (root_table, root_id, retain_pinned),
15512        )
15513        .map_err(DbError::from)
15514        .and_then(|changed| {
15515            if changed == 1 {
15516                Ok(())
15517            } else {
15518                Err(DbError::Message(format!(
15519                    "make_remote for {root_table:?}/{root_id:?} is already publishing"
15520                )))
15521            }
15522        })
15523    }
15524
15525    #[cfg(test)]
15526    pub(crate) fn make_remote_intent_exists(
15527        conn: &Connection,
15528        root_table: &str,
15529        root_id: &str,
15530    ) -> Result<bool, DbError> {
15531        conn.query_row(
15532            "SELECT 1 FROM blob_make_remote_intents WHERE root_table = ?1 AND root_id = ?2",
15533            (root_table, root_id),
15534            |_| Ok(()),
15535        )
15536        .optional()
15537        .map(|value| value.is_some())
15538        .map_err(DbError::from)
15539    }
15540
15541    pub(crate) fn make_remote_intent_state(
15542        conn: &Connection,
15543        root_table: &str,
15544        root_id: &str,
15545    ) -> Result<Option<MakeRemoteIntentState>, DbError> {
15546        let encoded = conn
15547            .query_row(
15548                "SELECT state, write_id FROM blob_make_remote_intents
15549                 WHERE root_table = ?1 AND root_id = ?2",
15550                (root_table, root_id),
15551                |row| Ok((row.get::<_, String>(0)?, row.get::<_, Option<String>>(1)?)),
15552            )
15553            .optional()
15554            .map_err(DbError::from)?;
15555        encoded
15556            .map(|(state, write_id)| match (state.as_str(), write_id) {
15557                ("uploading", None) => Ok(MakeRemoteIntentState::Uploading),
15558                ("cancelling", None) => Ok(MakeRemoteIntentState::Cancelling),
15559                ("publishing", Some(write_id)) => Ok(MakeRemoteIntentState::Publishing(
15560                    WriteId::from_generated(write_id),
15561                )),
15562                _ => Err(DbError::Message(format!(
15563                    "make_remote intent {root_table:?}/{root_id:?} has invalid state {state:?}"
15564                ))),
15565            })
15566            .transpose()
15567    }
15568
15569    pub(crate) fn mark_make_remote_cancelling_on(
15570        conn: &Connection,
15571        root_table: &str,
15572        root_id: &str,
15573    ) -> Result<(), DbError> {
15574        let updated = conn
15575            .execute(
15576                "UPDATE blob_make_remote_intents SET state = 'cancelling'
15577                 WHERE root_table = ?1 AND root_id = ?2 AND state = 'uploading'",
15578                (root_table, root_id),
15579            )
15580            .map_err(DbError::from)?;
15581        if updated == 1 {
15582            conn.execute(
15583                "UPDATE cloud_outbox
15584                 SET attempt_count = 0, last_error = NULL, last_attempt_at = NULL
15585                 WHERE operation = 'upload' AND root_table = ?1 AND root_id = ?2",
15586                (root_table, root_id),
15587            )
15588            .map_err(DbError::from)?;
15589            return Ok(());
15590        }
15591        if matches!(
15592            Self::make_remote_intent_state(conn, root_table, root_id)?,
15593            Some(MakeRemoteIntentState::Cancelling)
15594        ) {
15595            return Ok(());
15596        }
15597        Err(DbError::Message(format!(
15598            "make_remote intent {root_table:?}/{root_id:?} cannot enter cancellation"
15599        )))
15600    }
15601
15602    pub fn mark_make_remote_publishing_on(
15603        conn: &Connection,
15604        root_table: &str,
15605        root_id: &str,
15606        write_id: &WriteId,
15607    ) -> Result<(), DbError> {
15608        let updated = conn
15609            .execute(
15610                "UPDATE blob_make_remote_intents SET state = 'publishing', write_id = ?3
15611                 WHERE root_table = ?1 AND root_id = ?2 AND state = 'uploading'",
15612                (root_table, root_id, write_id.as_str()),
15613            )
15614            .map_err(DbError::from)?;
15615        if updated != 1 {
15616            return Err(DbError::Message(format!(
15617                "make_remote intent {root_table:?}/{root_id:?} changed before publication"
15618            )));
15619        }
15620        Ok(())
15621    }
15622
15623    pub fn complete_make_remote_publication_on(
15624        conn: &Connection,
15625        write_id: &WriteId,
15626    ) -> Result<(), DbError> {
15627        let removed = conn
15628            .execute(
15629                "DELETE FROM blob_make_remote_intents
15630                 WHERE write_id = ?1 AND state = 'publishing'",
15631                [write_id.as_str()],
15632            )
15633            .map_err(DbError::from)?;
15634        if removed != 1 {
15635            return Err(DbError::Message(format!(
15636                "make_remote publication {write_id} changed before activation"
15637            )));
15638        }
15639        Ok(())
15640    }
15641
15642    pub(crate) fn make_remote_publication_root_on(
15643        conn: &Connection,
15644        write_id: &WriteId,
15645    ) -> Result<Option<(String, String)>, DbError> {
15646        conn.query_row(
15647            "SELECT root_table, root_id FROM blob_make_remote_intents
15648             WHERE write_id = ?1 AND state = 'publishing'",
15649            [write_id.as_str()],
15650            |row| Ok((row.get(0)?, row.get(1)?)),
15651        )
15652        .optional()
15653        .map_err(DbError::from)
15654    }
15655
15656    pub fn make_remote_intent_retain_pinned(
15657        conn: &Connection,
15658        root_table: &str,
15659        root_id: &str,
15660    ) -> Result<Option<bool>, DbError> {
15661        conn.query_row(
15662            "SELECT retain_pinned FROM blob_make_remote_intents
15663             WHERE root_table = ?1 AND root_id = ?2",
15664            (root_table, root_id),
15665            |row| row.get(0),
15666        )
15667        .optional()
15668        .map_err(DbError::from)
15669    }
15670
15671    pub(crate) async fn external_blob_for_row(
15672        &self,
15673        reference: &RowBlobRef,
15674    ) -> Result<Option<ExternalBlob>, DbError> {
15675        let table = reference.table().to_string();
15676        let row_id = reference.row_id().to_string();
15677        let column = reference.column().to_string();
15678        let row_stamp = reference.row_stamp().to_string();
15679        let namespace = reference.blob().namespace.clone();
15680        let blob_id = reference.blob().id.clone();
15681        let expected_size = reference.plaintext_size();
15682        let expected_hash = reference.plaintext_hash();
15683        self.call(move |conn| {
15684            let row = conn
15685                .query_row(
15686                    "SELECT path, plaintext_size, plaintext_hash, namespace, blob_id
15687                     FROM local_blob_refs
15688                     WHERE table_name = ?1 AND row_id = ?2 AND column_name = ?3
15689                       AND row_stamp = ?4",
15690                    rusqlite::params![table, row_id, column, row_stamp],
15691                    |row| {
15692                        Ok((
15693                            row.get::<_, String>(0)?,
15694                            row.get::<_, i64>(1)?,
15695                            row.get::<_, String>(2)?,
15696                            row.get::<_, String>(3)?,
15697                            row.get::<_, String>(4)?,
15698                        ))
15699                    },
15700                )
15701                .optional()
15702                .map_err(DbError::from)?;
15703            let Some((path, size, hash, stored_namespace, stored_blob_id)) = row else {
15704                return Ok(None);
15705            };
15706            let size = u64::try_from(size).map_err(|_| {
15707                DbError::Message(format!("external blob {blob_id} has negative size"))
15708            })?;
15709            let hash: ObjectHash = hash.parse().map_err(|error| {
15710                DbError::Message(format!("external blob {blob_id} hash: {error}"))
15711            })?;
15712            if size != expected_size
15713                || hash != expected_hash
15714                || stored_namespace != namespace
15715                || stored_blob_id != blob_id
15716            {
15717                return Err(DbError::Message(format!(
15718                    "external blob row {table}/{row_id}/{column} differs from its row reference"
15719                )));
15720            }
15721            Ok(Some(ExternalBlob {
15722                path: PathBuf::from(path),
15723                size,
15724            }))
15725        })
15726        .await
15727    }
15728}
15729
15730fn validate_snapshot_object_owners_on(
15731    conn: &Connection,
15732    root: &crate::sync::store_commit::StoreRootRef,
15733    meta: &SnapshotMeta,
15734) -> Result<(), DbError> {
15735    let expected = crate::sync::remote_object::SnapshotObjectOwner {
15736        activation: StreamActivationId::store_snapshots(root, &meta.author_registration),
15737        sequence: meta.sequence,
15738    };
15739    if meta.successor.activation != expected.activation {
15740        return Err(DbError::Message(
15741            "verified snapshot successor differs from its author stream activation".to_string(),
15742        ));
15743    }
15744    validate_snapshot_object_owner_records_on(conn, &expected)
15745}
15746
15747fn validate_snapshot_object_owner_records_on(
15748    conn: &Connection,
15749    expected: &crate::sync::remote_object::SnapshotObjectOwner,
15750) -> Result<(), DbError> {
15751    let mut statement = conn
15752        .prepare("SELECT object_id FROM remote_objects ORDER BY object_id")
15753        .map_err(DbError::from)?;
15754    let object_ids = statement
15755        .query_map([], |row| row.get::<_, String>(0))
15756        .map_err(DbError::from)?
15757        .collect::<Result<Vec<_>, _>>()
15758        .map_err(DbError::from)?;
15759    drop(statement);
15760    for object_id in object_ids {
15761        let parsed = object_id.parse().map_err(|error| {
15762            DbError::Message(format!("snapshot remote object id {object_id:?}: {error}"))
15763        })?;
15764        let remote = load_remote_object_on(conn, parsed)?;
15765        for owner in remote.snapshot_owners() {
15766            if owner != expected {
15767                return Err(DbError::Message(format!(
15768                    "snapshot remote object {object_id} belongs to a different snapshot stream activation or sequence"
15769                )));
15770            }
15771        }
15772    }
15773    Ok(())
15774}
15775
15776fn validate_snapshot_blob_plan_on(
15777    conn: &Connection,
15778    gates: &Gates,
15779    synced_tables: &[SyncedTable],
15780    meta: &SnapshotMeta,
15781    blob: &PreparedSnapshotBlob,
15782) -> Result<(), DbError> {
15783    blob.remote
15784        .validate()
15785        .map_err(|error| DbError::Message(format!("snapshot remote blob: {error}")))?;
15786    let expected_owner = crate::sync::remote_object::SnapshotObjectOwner {
15787        activation: meta.successor.activation,
15788        sequence: meta.sequence,
15789    };
15790    let owners = blob.remote.snapshot_owners().collect::<Vec<_>>();
15791    if owners != [&expected_owner] {
15792        return Err(DbError::Message(
15793            "snapshot blob owner differs from the verified snapshot stream activation".to_string(),
15794        ));
15795    }
15796    if blob.bindings.is_empty()
15797        || blob.bindings.iter().any(|binding| {
15798            binding.blob().object() != blob.remote.object()
15799                || binding.blob().locator().audience() != blob.authority.remote_audience()
15800        })
15801        || blob
15802            .spool_path
15803            .as_ref()
15804            .is_some_and(|path| !path.is_absolute())
15805    {
15806        return Err(DbError::Message(
15807            "snapshot blob plan has inconsistent exact references".to_string(),
15808        ));
15809    }
15810    for binding in &blob.bindings {
15811        let table = synced_tables
15812            .iter()
15813            .find(|table| table.name() == binding.table())
15814            .ok_or_else(|| {
15815                DbError::Message(format!(
15816                    "snapshot blob names undeclared table {:?}",
15817                    binding.table()
15818                ))
15819            })?;
15820        let declaration = table.blob().ok_or_else(|| {
15821            DbError::Message(format!(
15822                "snapshot blob names table {:?} without a blob declaration",
15823                table.name()
15824            ))
15825        })?;
15826        let row =
15827            live_blob_row(conn, table.name(), binding.row_id(), declaration)?.ok_or_else(|| {
15828                DbError::Message(format!(
15829                    "snapshot blob row {:?}/{:?} is absent",
15830                    table.name(),
15831                    binding.row_id()
15832                ))
15833            })?;
15834        let audience = gate::live_row_audience(conn, gates, table.name(), binding.row_id())
15835            .map_err(|error| DbError::Message(error.to_string()))?;
15836        let audience = RemoteAudience::try_from(audience)
15837            .map_err(|error| DbError::Message(error.to_string()))?;
15838        validate_live_blob_locator(
15839            binding.table(),
15840            binding.row_id(),
15841            binding.column(),
15842            binding.row_stamp(),
15843            binding.blob(),
15844            declaration,
15845            &row,
15846            &audience,
15847        )?;
15848    }
15849    Ok(())
15850}
15851
15852fn install_snapshot_blob_plan_on(
15853    conn: &Connection,
15854    blob: &PreparedSnapshotBlob,
15855) -> Result<(), DbError> {
15856    let object_id = blob.remote.object_id();
15857    let exists: bool = conn
15858        .query_row(
15859            "SELECT EXISTS(SELECT 1 FROM remote_objects WHERE object_id = ?1)",
15860            [object_id.to_string()],
15861            |row| row.get(0),
15862        )
15863        .map_err(DbError::from)?;
15864    let merged = if exists {
15865        let mut existing = load_remote_object_on(conn, object_id)?;
15866        for owner in blob.remote.snapshot_owners() {
15867            existing
15868                .merge_snapshot_owner(blob.bindings[0].blob(), owner.clone())
15869                .map_err(|error| DbError::Message(format!("merge snapshot blob owner: {error}")))?;
15870        }
15871        existing
15872    } else {
15873        blob.remote.clone()
15874    };
15875    let encoded = serde_json::to_string(&merged)
15876        .map_err(|error| DbError::Message(format!("serialize snapshot blob: {error}")))?;
15877    conn.execute(
15878        "INSERT INTO remote_objects (object_id, state) VALUES (?1, ?2)
15879         ON CONFLICT(object_id) DO UPDATE SET state = excluded.state",
15880        rusqlite::params![object_id.to_string(), encoded],
15881    )
15882    .map_err(DbError::from)?;
15883    conn.execute(
15884        "INSERT INTO blob_locators (remote_object_id, locator_hash) VALUES (?1, ?2)
15885         ON CONFLICT(remote_object_id) DO NOTHING",
15886        rusqlite::params![
15887            object_id.to_string(),
15888            blob.bindings[0].blob().locator().locator_hash().to_string(),
15889        ],
15890    )
15891    .map_err(DbError::from)?;
15892    validate_stored_locator_on(conn, blob.bindings[0].blob())?;
15893    let authority = serde_json::to_string(&blob.authority)
15894        .map_err(|error| DbError::Message(format!("serialize snapshot blob authority: {error}")))?;
15895    for binding in &blob.bindings {
15896        conn.execute(
15897            "INSERT INTO row_blob_locators
15898         (table_name, row_id, column_name, row_stamp, audience_authority, remote_object_id)
15899         VALUES (?1, ?2, ?3, ?4, ?5, ?6)
15900         ON CONFLICT(table_name, row_id, column_name, row_stamp) DO NOTHING",
15901            rusqlite::params![
15902                binding.table(),
15903                binding.row_id(),
15904                binding.column(),
15905                binding.row_stamp(),
15906                authority,
15907                object_id.to_string(),
15908            ],
15909        )
15910        .map_err(DbError::from)?;
15911        validate_stored_row_binding_on(conn, binding, &blob.authority, object_id)?;
15912    }
15913    Ok(())
15914}
15915
15916enum OutboxIdentity {
15917    Upload {
15918        table: String,
15919        row_id: String,
15920        column: String,
15921        row_stamp: String,
15922    },
15923    Stored {
15924        operation: &'static str,
15925        stored: String,
15926    },
15927}
15928
15929fn upload_entry_for_identity_on(
15930    conn: &Connection,
15931    table: &str,
15932    row_id: &str,
15933    column: &str,
15934    row_stamp: &str,
15935) -> Result<Option<OutboxEntry>, DbError> {
15936    conn.query_row(
15937        "SELECT id, operation, row_ref, stored_ref, source_path, retain_pinned,
15938                upload_state, attempt_count, last_attempt_at, root_table, root_id
15939         FROM cloud_outbox
15940         WHERE operation = 'upload' AND table_name = ?1 AND row_id = ?2
15941           AND column_name = ?3 AND row_stamp = ?4",
15942        rusqlite::params![table, row_id, column, row_stamp],
15943        row_to_outbox_entry,
15944    )
15945    .optional()
15946    .map_err(DbError::from)
15947}
15948
15949fn consume_created_upload_handoff_on(
15950    conn: &Connection,
15951    package: &AudiencePackage,
15952    binding: &RowBlobLocatorBinding,
15953) -> Result<bool, DbError> {
15954    let Some(entry) = upload_entry_for_identity_on(
15955        conn,
15956        binding.table(),
15957        binding.row_id(),
15958        binding.column(),
15959        binding.row_stamp(),
15960    )?
15961    else {
15962        return Ok(false);
15963    };
15964    let OutboxOperation::Upload {
15965        row,
15966        state: OutboxUploadState::Created {
15967            authority, stored, ..
15968        },
15969        ..
15970    } = &entry.operation
15971    else {
15972        return Err(DbError::Message(format!(
15973            "activated blob binding {}/{}/{} at {} has an upload that is not Created",
15974            binding.table(),
15975            binding.row_id(),
15976            binding.column(),
15977            binding.row_stamp()
15978        )));
15979    };
15980    if row.table() != binding.table()
15981        || row.row_id() != binding.row_id()
15982        || row.column() != binding.column()
15983        || row.row_stamp() != binding.row_stamp()
15984        || authority != package.audience()
15985        || stored != binding.blob()
15986    {
15987        return Err(DbError::Message(format!(
15988            "activated blob binding {}/{}/{} at {} differs from its Created upload handoff",
15989            binding.table(),
15990            binding.row_id(),
15991            binding.column(),
15992            binding.row_stamp()
15993        )));
15994    }
15995    Database::remove_cloud_outbox_entry_on(conn, &entry)?;
15996    Ok(true)
15997}
15998
15999fn created_upload_handoff_on(
16000    conn: &Connection,
16001    table: &str,
16002    row_id: &str,
16003    column: &str,
16004    row_stamp: &str,
16005) -> Result<Option<StoreWriteRemoteBlob>, DbError> {
16006    let Some(entry) = upload_entry_for_identity_on(conn, table, row_id, column, row_stamp)? else {
16007        return Ok(None);
16008    };
16009    let OutboxOperation::Upload { row, state, .. } = entry.operation else {
16010        return Err(DbError::Message(
16011            "upload identity query returned a non-upload operation".to_string(),
16012        ));
16013    };
16014    if row.table() != table
16015        || row.row_id() != row_id
16016        || row.column() != column
16017        || row.row_stamp() != row_stamp
16018    {
16019        return Err(DbError::Message(format!(
16020            "upload outbox row facts differ from identity {table}/{row_id}/{column} at {row_stamp}"
16021        )));
16022    }
16023    match state {
16024        OutboxUploadState::Created {
16025            authority, stored, ..
16026        } => Ok(Some(StoreWriteRemoteBlob { authority, stored })),
16027        OutboxUploadState::Pending | OutboxUploadState::Prepared { .. } => Ok(None),
16028    }
16029}
16030
16031fn outbox_identity(operation: &OutboxOperation) -> Result<OutboxIdentity, DbError> {
16032    match operation {
16033        OutboxOperation::Upload { row, .. } => Ok(OutboxIdentity::Upload {
16034            table: row.table().to_string(),
16035            row_id: row.row_id().to_string(),
16036            column: row.column().to_string(),
16037            row_stamp: row.row_stamp().to_string(),
16038        }),
16039        OutboxOperation::Delete { stored } => Ok(OutboxIdentity::Stored {
16040            operation: "delete",
16041            stored: serde_json::to_string(stored).map_err(|error| {
16042                DbError::Message(format!("serialize stored blob outbox identity: {error}"))
16043            })?,
16044        }),
16045    }
16046}
16047
16048fn row_to_outbox_entry(row: &rusqlite::Row<'_>) -> rusqlite::Result<OutboxEntry> {
16049    fn invalid(index: usize, message: String) -> rusqlite::Error {
16050        rusqlite::Error::FromSqlConversionFailure(
16051            index,
16052            rusqlite::types::Type::Text,
16053            Box::new(std::io::Error::new(
16054                std::io::ErrorKind::InvalidData,
16055                message,
16056            )),
16057        )
16058    }
16059
16060    let tag: String = row.get(1)?;
16061    let operation = match tag.as_str() {
16062        "upload" => {
16063            let encoded: String = row.get(2)?;
16064            let reference: RowBlobRef =
16065                serde_json::from_str(&encoded).map_err(|error| invalid(2, error.to_string()))?;
16066            let source_path: String = row.get(4)?;
16067            let state_json: String = row.get(6)?;
16068            let state: OutboxUploadState =
16069                serde_json::from_str(&state_json).map_err(|error| invalid(6, error.to_string()))?;
16070            if let OutboxUploadState::Prepared {
16071                authority, stored, ..
16072            }
16073            | OutboxUploadState::Created {
16074                authority, stored, ..
16075            } = &state
16076            {
16077                let locator = stored.locator();
16078                if locator.namespace() != reference.blob().namespace
16079                    || locator.blob_id() != reference.blob().id
16080                    || locator.plaintext_size() != reference.plaintext_size()
16081                    || locator.plaintext_hash() != reference.plaintext_hash()
16082                    || locator
16083                        .scope()
16084                        .is_some_and(|scope| scope != &reference.blob().scope)
16085                {
16086                    return Err(invalid(
16087                        6,
16088                        "prepared upload differs from its exact row version".to_string(),
16089                    ));
16090                }
16091                if locator.audience() != authority.remote_audience() {
16092                    return Err(invalid(
16093                        6,
16094                        "upload package authority differs from its stored locator".to_string(),
16095                    ));
16096                }
16097            }
16098            OutboxOperation::Upload {
16099                root_table: row.get(9)?,
16100                root_id: row.get(10)?,
16101                row: reference,
16102                source_path: PathBuf::from(source_path),
16103                retain_pinned: row.get(5)?,
16104                state,
16105            }
16106        }
16107        "delete" => {
16108            let encoded: String = row.get(3)?;
16109            let stored: StoredBlobRef =
16110                serde_json::from_str(&encoded).map_err(|error| invalid(3, error.to_string()))?;
16111            OutboxOperation::Delete { stored }
16112        }
16113        _ => {
16114            return Err(invalid(
16115                1,
16116                format!("invalid cloud outbox operation {tag:?}"),
16117            ))
16118        }
16119    };
16120    Ok(OutboxEntry {
16121        id: row.get(0)?,
16122        attempt_count: row.get(7)?,
16123        last_attempt_at: row.get(8)?,
16124        operation,
16125    })
16126}
16127
16128fn local_store_authority_on(
16129    conn: &Connection,
16130) -> Result<
16131    (
16132        crate::sync::store_commit::StoreRootRef,
16133        StoreDeviceRegistrationRef,
16134        StoreDeviceRegistration,
16135    ),
16136    DbError,
16137> {
16138    let root = required_store_root_authority_on(conn)?;
16139    let device_id: String = conn
16140        .query_row(
16141            "SELECT value FROM protocol_state WHERE key = ?1",
16142            [LOCAL_DEVICE_ID_STATE_KEY],
16143            |row| row.get(0),
16144        )
16145        .map_err(DbError::from)?;
16146    let (bytes, encoded): (Vec<u8>, String) = conn
16147        .query_row(
16148            "SELECT registration_bytes, registration_object \
16149             FROM store_device_registration_activations WHERE device_id = ?1",
16150            [&device_id],
16151            |row| Ok((row.get(0)?, row.get(1)?)),
16152        )
16153        .map_err(DbError::from)?;
16154    let reference: StoreDeviceRegistrationRef =
16155        serde_json::from_str(&encoded).map_err(|error| {
16156            DbError::Message(format!("local Store registration exact ref: {error}"))
16157        })?;
16158    if reference.device_id.to_string() != device_id {
16159        return Err(DbError::Message(
16160            "local Store device state differs from its activated registration".to_string(),
16161        ));
16162    }
16163    let registration = StoreDeviceRegistration::parse_at(&bytes, &root, reference.device_id)
16164        .map_err(|error| DbError::Message(format!("local Store registration: {error}")))?;
16165    reference
16166        .verify_registration(&registration)
16167        .map_err(|error| DbError::Message(error.to_string()))?;
16168    Ok((root, reference, registration))
16169}
16170
16171fn local_merge_stream_id_on(conn: &Connection) -> Result<Option<String>, DbError> {
16172    let local_device_id = conn
16173        .query_row(
16174            "SELECT value FROM protocol_state WHERE key = ?1",
16175            [LOCAL_DEVICE_ID_STATE_KEY],
16176            |row| row.get::<_, String>(0),
16177        )
16178        .optional()
16179        .map_err(DbError::from)?;
16180    let Some(_) = local_device_id else {
16181        return Ok(None);
16182    };
16183    let (root, reference, _) = local_store_authority_on(conn)?;
16184    Ok(Some(
16185        AuthorStreamId::store_announcements(&root, &reference).to_string(),
16186    ))
16187}
16188
16189fn pin_host_device_id_on(
16190    conn: &Connection,
16191    expected: &str,
16192    initialized: bool,
16193) -> Result<(), DbError> {
16194    if initialized {
16195        return validate_host_device_id_on(conn, expected);
16196    }
16197    conn.execute(
16198        "INSERT INTO protocol_state (key, value) VALUES (?1, ?2)",
16199        (HOST_DEVICE_ID_STATE_KEY, expected),
16200    )
16201    .map(|_| ())
16202    .map_err(DbError::from)
16203}
16204
16205fn validate_host_device_id_on(conn: &Connection, expected: &str) -> Result<(), DbError> {
16206    let stored = conn
16207        .query_row(
16208            "SELECT value FROM protocol_state WHERE key = ?1",
16209            [HOST_DEVICE_ID_STATE_KEY],
16210            |row| row.get::<_, String>(0),
16211        )
16212        .optional()
16213        .map_err(DbError::from)?
16214        .ok_or_else(|| DbError::Message("Coven host device identity is absent".to_string()))?;
16215    if stored != expected {
16216        return Err(DbError::Message(format!(
16217            "Coven host device identity is {stored:?}, not configured identity {expected:?}"
16218        )));
16219    }
16220    Ok(())
16221}
16222
16223fn store_ack_first_slot(
16224    registration: &StoreDeviceRegistration,
16225) -> Result<&crate::storage::cloud::ObjectSlot, DbError> {
16226    match &registration.acknowledgements {
16227        crate::sync::store_commit::DeviceStreamAnchor::StoreAcknowledgements { first_slot } => {
16228            Ok(first_slot)
16229        }
16230        _ => Err(DbError::Message(
16231            "local Store registration has no acknowledgement stream anchor".to_string(),
16232        )),
16233    }
16234}
16235
16236fn store_snapshot_first_slot(
16237    registration: &StoreDeviceRegistration,
16238) -> Result<&crate::storage::cloud::ObjectSlot, DbError> {
16239    match &registration.snapshots {
16240        crate::sync::store_commit::DeviceStreamAnchor::StoreSnapshots { first_slot } => {
16241            Ok(first_slot)
16242        }
16243        _ => Err(DbError::Message(
16244            "local Store registration has no snapshot stream anchor".to_string(),
16245        )),
16246    }
16247}
16248
16249fn load_published_store_ack_on(conn: &Connection) -> Result<Option<PublishedStoreAck>, DbError> {
16250    conn.query_row(
16251        "SELECT ack_ref, successor_slot FROM published_store_acks WHERE singleton = 1",
16252        [],
16253        |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
16254    )
16255    .optional()
16256    .map_err(DbError::from)?
16257    .map(|(reference, successor_slot)| {
16258        let reference: StoreAckRef = serde_json::from_str(&reference).map_err(|error| {
16259            DbError::Message(format!("published Store acknowledgement ref: {error}"))
16260        })?;
16261        if reference.revision == 0 {
16262            return Err(DbError::Message(
16263                "published Store acknowledgement uses revision zero".to_string(),
16264            ));
16265        }
16266        Ok(PublishedStoreAck {
16267            reference,
16268            successor_slot: serde_json::from_str(&successor_slot).map_err(|error| {
16269                DbError::Message(format!(
16270                    "published Store acknowledgement successor slot: {error}"
16271                ))
16272            })?,
16273        })
16274    })
16275    .transpose()
16276}
16277
16278fn load_outbound_store_ack_on(conn: &Connection) -> Result<Option<OutboundStoreAck>, DbError> {
16279    conn.query_row(
16280        "SELECT ack_ref, ack_bytes, prepared_object \
16281         FROM outbound_store_acks WHERE singleton = 1",
16282        [],
16283        |row| {
16284            Ok((
16285                row.get::<_, String>(0)?,
16286                row.get::<_, Vec<u8>>(1)?,
16287                row.get::<_, String>(2)?,
16288            ))
16289        },
16290    )
16291    .optional()
16292    .map_err(DbError::from)?
16293    .map(|(reference, bytes, prepared)| {
16294        let reference: StoreAckRef = serde_json::from_str(&reference).map_err(|error| {
16295            DbError::Message(format!("outbound Store acknowledgement ref: {error}"))
16296        })?;
16297        let prepared: PreparedExactObject = serde_json::from_str(&prepared).map_err(|error| {
16298            DbError::Message(format!("outbound prepared Store acknowledgement: {error}"))
16299        })?;
16300        if prepared.reference() != &reference.object {
16301            return Err(DbError::Message(
16302                "outbound Store acknowledgement ref differs from its prepared object".to_string(),
16303            ));
16304        }
16305        let (root, author_ref, author) = local_store_authority_on(conn)?;
16306        let value = StoreAck::parse_at(&bytes, root.store_root_hash, &reference, &author).map_err(
16307            |error| DbError::Message(format!("outbound Store acknowledgement: {error}")),
16308        )?;
16309        if value.author_registration != author_ref {
16310            return Err(DbError::Message(
16311                "outbound Store acknowledgement author differs from local activation".to_string(),
16312            ));
16313        }
16314        Ok(OutboundStoreAck {
16315            reference,
16316            ack: ExactProtocolObject {
16317                value,
16318                bytes,
16319                object: prepared.reference().clone(),
16320                prepared,
16321            },
16322        })
16323    })
16324    .transpose()
16325}
16326
16327fn load_published_store_snapshot_on(
16328    conn: &Connection,
16329) -> Result<Option<PublishedStoreSnapshot>, DbError> {
16330    conn.query_row(
16331        "SELECT snapshot_ref, successor_slot, meta_bytes \
16332         FROM published_store_snapshot WHERE singleton = 1",
16333        [],
16334        |row| {
16335            Ok((
16336                row.get::<_, String>(0)?,
16337                row.get::<_, String>(1)?,
16338                row.get::<_, Vec<u8>>(2)?,
16339            ))
16340        },
16341    )
16342    .optional()
16343    .map_err(DbError::from)?
16344    .map(|(reference, successor_slot, bytes)| {
16345        let reference: StoreSnapshotRef = serde_json::from_str(&reference)
16346            .map_err(|error| DbError::Message(format!("published Store snapshot ref: {error}")))?;
16347        let successor_slot = serde_json::from_str(&successor_slot).map_err(|error| {
16348            DbError::Message(format!("published Store snapshot successor slot: {error}"))
16349        })?;
16350        let (root, author_ref, author) = local_store_authority_on(conn)?;
16351        let meta = SnapshotMeta::parse_at(&bytes, root.store_root_hash, &reference, &author)
16352            .map_err(|error| DbError::Message(format!("published Store snapshot: {error}")))?;
16353        if meta.author_registration != author_ref || meta.successor.next_slot != successor_slot {
16354            return Err(DbError::Message(
16355                "published Store snapshot differs from its local stream state".to_string(),
16356            ));
16357        }
16358        Ok(PublishedStoreSnapshot {
16359            reference,
16360            successor_slot,
16361            meta,
16362        })
16363    })
16364    .transpose()
16365}
16366
16367fn load_outbound_store_snapshot_on(
16368    conn: &Connection,
16369) -> Result<Option<DurableSnapshotPublication>, DbError> {
16370    conn.query_row(
16371        "SELECT snapshot_ref, meta_prepared, image_ref, image_prepared, image_bytes, meta_bytes, blobs \
16372         FROM outbound_store_snapshot WHERE singleton = 1",
16373        [],
16374        |row| {
16375            Ok((
16376                row.get::<_, String>(0)?,
16377                row.get::<_, String>(1)?,
16378                row.get::<_, String>(2)?,
16379                row.get::<_, String>(3)?,
16380                row.get::<_, Vec<u8>>(4)?,
16381                row.get::<_, Vec<u8>>(5)?,
16382                row.get::<_, String>(6)?,
16383            ))
16384        },
16385    )
16386    .optional()
16387    .map_err(DbError::from)?
16388    .map(
16389        |(reference, meta_prepared, image_reference, image_prepared, image_bytes, meta_bytes, blobs)| {
16390            let reference: StoreSnapshotRef =
16391                serde_json::from_str(&reference).map_err(|error| {
16392                    DbError::Message(format!("outbound Store snapshot ref: {error}"))
16393                })?;
16394            let meta_prepared: PreparedExactObject =
16395                serde_json::from_str(&meta_prepared).map_err(|error| {
16396                    DbError::Message(format!(
16397                        "outbound prepared Store snapshot metadata: {error}"
16398                    ))
16399                })?;
16400            let image_reference: SnapshotImageRef = serde_json::from_str(&image_reference)
16401                .map_err(|error| {
16402                    DbError::Message(format!("outbound Store snapshot image ref: {error}"))
16403                })?;
16404            let image_prepared: PreparedExactObject = serde_json::from_str(&image_prepared)
16405                .map_err(|error| {
16406                    DbError::Message(format!("outbound prepared Store snapshot image: {error}"))
16407                })?;
16408            let blobs: Vec<PreparedSnapshotBlob> = serde_json::from_str(&blobs).map_err(|error| {
16409                DbError::Message(format!("outbound prepared Store snapshot blobs: {error}"))
16410            })?;
16411            if meta_prepared.reference() != &reference.object
16412                || image_prepared.reference() != &image_reference.object
16413                || ObjectHash::digest(&image_bytes) != image_reference.image_hash
16414            {
16415                return Err(DbError::Message(
16416                    "outbound Store snapshot exact references differ from prepared bytes"
16417                        .to_string(),
16418                ));
16419            }
16420            let (root, author_ref, author) = local_store_authority_on(conn)?;
16421            let meta =
16422                SnapshotMeta::parse_at(&meta_bytes, root.store_root_hash, &reference, &author)
16423                    .map_err(|error| {
16424                        DbError::Message(format!("outbound Store snapshot: {error}"))
16425                    })?;
16426            if meta.author_registration != author_ref || meta.image != image_reference {
16427                return Err(DbError::Message(
16428                    "outbound Store snapshot metadata differs from its exact image".to_string(),
16429                ));
16430            }
16431            Ok(DurableSnapshotPublication {
16432                reference,
16433                meta: ExactProtocolObject {
16434                    value: meta,
16435                    bytes: meta_bytes,
16436                    object: meta_prepared.reference().clone(),
16437                    prepared: meta_prepared,
16438                },
16439                image: ExactProtocolObject {
16440                    value: image_bytes.clone(),
16441                    bytes: image_bytes,
16442                    object: image_prepared.reference().clone(),
16443                    prepared: image_prepared,
16444                },
16445                blobs,
16446            })
16447        },
16448    )
16449    .transpose()
16450}
16451
16452struct LiveBlobRow {
16453    stamp: String,
16454    blob_id: String,
16455    plaintext_size: u64,
16456    plaintext_hash: ObjectHash,
16457    cloud_path: Option<String>,
16458}
16459
16460fn live_blob_row(
16461    conn: &Connection,
16462    table: &str,
16463    row_id: &str,
16464    declaration: &crate::sync::session::BlobDecl,
16465) -> Result<Option<LiveBlobRow>, DbError> {
16466    let cloud_path = declaration
16467        .cloud_path_column
16468        .as_deref()
16469        .map(quote_ident)
16470        .unwrap_or_else(|| "NULL".to_string());
16471    let sql = format!(
16472        "SELECT {}, {}, {}, {}, {} FROM {} WHERE {} = ?1",
16473        quote_ident(&declaration.id_column),
16474        quote_ident(&declaration.size_column),
16475        quote_ident(&declaration.hash_column),
16476        cloud_path,
16477        quote_ident("_updated_at"),
16478        quote_ident(table),
16479        quote_ident("id"),
16480    );
16481    let raw = conn
16482        .query_row(&sql, [row_id], |row| {
16483            Ok((
16484                row.get::<_, String>(0)?,
16485                row.get::<_, i64>(1)?,
16486                row.get::<_, String>(2)?,
16487                row.get::<_, Option<String>>(3)?,
16488                row.get::<_, String>(4)?,
16489            ))
16490        })
16491        .optional()
16492        .map_err(DbError::from)?;
16493    let Some((blob_id, plaintext_size, plaintext_hash, cloud_path, stamp)) = raw else {
16494        return Ok(None);
16495    };
16496    let plaintext_size = u64::try_from(plaintext_size).map_err(|_| {
16497        DbError::Message(format!(
16498            "winning blob row {:?}/{:?} has negative plaintext size {plaintext_size}",
16499            table, row_id
16500        ))
16501    })?;
16502    let plaintext_hash = plaintext_hash.parse().map_err(|error| {
16503        DbError::Message(format!(
16504            "winning blob row {:?}/{:?} has invalid plaintext hash: {error}",
16505            table, row_id
16506        ))
16507    })?;
16508    Ok(Some(LiveBlobRow {
16509        stamp,
16510        blob_id,
16511        plaintext_size,
16512        plaintext_hash,
16513        cloud_path,
16514    }))
16515}
16516
16517fn validate_live_blob_row(
16518    binding: &RowBlobLocatorBinding,
16519    declaration: &crate::sync::session::BlobDecl,
16520    row: &LiveBlobRow,
16521    live_audience: &RemoteAudience,
16522) -> Result<(), DbError> {
16523    validate_live_blob_locator(
16524        binding.table(),
16525        binding.row_id(),
16526        binding.column(),
16527        binding.row_stamp(),
16528        binding.blob(),
16529        declaration,
16530        row,
16531        live_audience,
16532    )
16533}
16534
16535#[allow(clippy::too_many_arguments)]
16536fn validate_live_blob_locator(
16537    table: &str,
16538    row_id: &str,
16539    column: &str,
16540    row_stamp: &str,
16541    stored: &StoredBlobRef,
16542    declaration: &crate::sync::session::BlobDecl,
16543    row: &LiveBlobRow,
16544    live_audience: &RemoteAudience,
16545) -> Result<(), DbError> {
16546    let locator = stored.locator();
16547    let invalid = locator.namespace() != declaration.namespace
16548        || locator.blob_id() != row.blob_id
16549        || locator.plaintext_size() != row.plaintext_size
16550        || locator.plaintext_hash() != row.plaintext_hash
16551        || &locator.audience() != live_audience
16552        || locator
16553            .scope()
16554            .is_some_and(|scope| scope != &declaration.scope)
16555        || locator
16556            .cloud_path()
16557            .is_some_and(|path| row.cloud_path.as_deref() != Some(path));
16558    if invalid {
16559        return Err(DbError::Message(format!(
16560            "blob locator does not match winning row values for {:?}/{:?}/{:?} at {:?}",
16561            table, row_id, column, row_stamp
16562        )));
16563    }
16564    Ok(())
16565}
16566
16567fn validate_stored_locator_on(conn: &Connection, expected: &StoredBlobRef) -> Result<(), DbError> {
16568    let locator_hash = expected.locator().locator_hash().to_string();
16569    let expected_remote_object_id = remote_object_id(expected.object());
16570    let stored_locator_hash: String = conn
16571        .query_row(
16572            "SELECT locator_hash FROM blob_locators WHERE remote_object_id = ?1",
16573            [expected_remote_object_id.to_string()],
16574            |row| row.get(0),
16575        )
16576        .map_err(DbError::from)?;
16577    if stored_locator_hash != locator_hash {
16578        return Err(DbError::Message(format!(
16579            "stored blob object {expected_remote_object_id} is indexed under locator {stored_locator_hash}, expected {locator_hash}"
16580        )));
16581    }
16582    let remote = load_remote_object_on(conn, expected_remote_object_id)?;
16583    if !remote.is_activated_stored_blob() {
16584        return Err(DbError::Message(format!(
16585            "stored blob locator {locator_hash} does not reference activated ownership"
16586        )));
16587    }
16588    let locator =
16589        BlobLocator::parse(remote.bytes().canonical_semantic_bytes()).map_err(|error| {
16590            DbError::Message(format!(
16591                "stored blob locator {locator_hash} is invalid: {error}"
16592            ))
16593        })?;
16594    let actual = StoredBlobRef::new(locator, remote.object().clone()).map_err(|error| {
16595        DbError::Message(format!(
16596            "stored blob reference {locator_hash} is invalid: {error}"
16597        ))
16598    })?;
16599    if &actual != expected {
16600        return Err(DbError::Message(format!(
16601            "blob object {expected_remote_object_id} differs from its exact stored reference"
16602        )));
16603    }
16604    Ok(())
16605}
16606
16607fn validate_stored_row_binding_on(
16608    conn: &Connection,
16609    binding: &RowBlobLocatorBinding,
16610    expected_authority: &crate::sync::audience_package::PackageAudience,
16611    expected_remote_object_id: ObjectHash,
16612) -> Result<(), DbError> {
16613    let (audience_authority, remote_object_id): (String, String) = conn
16614        .query_row(
16615            "SELECT audience_authority, remote_object_id FROM row_blob_locators
16616             WHERE table_name = ?1 AND row_id = ?2 AND column_name = ?3 AND row_stamp = ?4",
16617            rusqlite::params![
16618                binding.table(),
16619                binding.row_id(),
16620                binding.column(),
16621                binding.row_stamp(),
16622            ],
16623            |row| Ok((row.get(0)?, row.get(1)?)),
16624        )
16625        .map_err(DbError::from)?;
16626    let actual_authority: crate::sync::audience_package::PackageAudience =
16627        serde_json::from_str(&audience_authority).map_err(|error| {
16628            DbError::Message(format!("parse stored row blob audience authority: {error}"))
16629        })?;
16630    if &actual_authority != expected_authority
16631        || remote_object_id != expected_remote_object_id.to_string()
16632    {
16633        return Err(DbError::Message(format!(
16634            "row blob binding {:?}/{:?}/{:?} at {:?} is already bound to different exact content",
16635            binding.table(),
16636            binding.row_id(),
16637            binding.column(),
16638            binding.row_stamp()
16639        )));
16640    }
16641    Ok(())
16642}
16643
16644fn load_prepared_audience_objects_on(
16645    conn: &Connection,
16646    write_id: &WriteId,
16647) -> Result<PreparedAudienceObjects, DbError> {
16648    let mut package_statement = conn
16649        .prepare(
16650            "SELECT remote_object_id FROM store_write_packages
16651             WHERE write_id = ?1 ORDER BY audience",
16652        )
16653        .map_err(DbError::from)?;
16654    let package_ids = package_statement
16655        .query_map([write_id.as_str()], |row| row.get::<_, String>(0))
16656        .map_err(DbError::from)?
16657        .collect::<Result<Vec<_>, _>>()
16658        .map_err(DbError::from)?;
16659    let mut blob_statement = conn
16660        .prepare(
16661            "SELECT remote_object_id, audience, locator_hash, spool_path FROM store_write_blobs
16662             WHERE write_id = ?1 ORDER BY audience, remote_object_id",
16663        )
16664        .map_err(DbError::from)?;
16665    let blob_rows = blob_statement
16666        .query_map([write_id.as_str()], |row| {
16667            Ok((
16668                row.get::<_, String>(0)?,
16669                row.get::<_, String>(1)?,
16670                row.get::<_, String>(2)?,
16671                row.get::<_, Option<String>>(3)?,
16672            ))
16673        })
16674        .map_err(DbError::from)?
16675        .collect::<Result<Vec<_>, _>>()
16676        .map_err(DbError::from)?;
16677    let packages = package_ids
16678        .into_iter()
16679        .map(|encoded| {
16680            let object_id = encoded
16681                .parse()
16682                .map_err(|error| DbError::Message(format!("stored remote object id: {error}")))?;
16683            PreparedAudiencePackage::from_remote(load_remote_object_on(conn, object_id)?)
16684        })
16685        .collect::<Result<Vec<_>, DbError>>()?;
16686    let blobs = blob_rows
16687        .into_iter()
16688        .map(|(encoded, audience, locator_hash, spool_path)| {
16689            let object_id = encoded
16690                .parse()
16691                .map_err(|error| DbError::Message(format!("stored remote object id: {error}")))?;
16692            PreparedAudienceBlob::from_remote(
16693                parse_remote_audience_db(&audience)?,
16694                &locator_hash,
16695                load_remote_object_on(conn, object_id)?,
16696                spool_path.map(PathBuf::from),
16697            )
16698        })
16699        .collect::<Result<Vec<_>, DbError>>()?;
16700    Ok(PreparedAudienceObjects { packages, blobs })
16701}
16702
16703fn load_activated_registration_on(
16704    conn: &Connection,
16705    root: &crate::sync::store_commit::StoreRootRef,
16706    reference: &StoreDeviceRegistrationRef,
16707) -> Result<StoreDeviceRegistration, DbError> {
16708    let (bytes, encoded): (Vec<u8>, String) = conn
16709        .query_row(
16710            "SELECT registration_bytes, registration_object \
16711             FROM store_device_registration_activations \
16712             WHERE device_id = ?1 AND registration_hash = ?2",
16713            (
16714                reference.device_id.to_string(),
16715                reference.registration_hash.to_string(),
16716            ),
16717            |row| Ok((row.get(0)?, row.get(1)?)),
16718        )
16719        .map_err(DbError::from)?;
16720    let stored: StoreDeviceRegistrationRef = serde_json::from_str(&encoded)
16721        .map_err(|error| DbError::Message(format!("activated Store registration ref: {error}")))?;
16722    if stored != *reference {
16723        return Err(DbError::Message(
16724            "activated Store registration differs from its exact reference".to_string(),
16725        ));
16726    }
16727    let registration = StoreDeviceRegistration::parse_at(&bytes, root, reference.device_id)
16728        .map_err(|error| DbError::Message(format!("activated Store registration: {error}")))?;
16729    reference
16730        .verify_registration(&registration)
16731        .map_err(|error| DbError::Message(error.to_string()))?;
16732    Ok(registration)
16733}
16734
16735#[allow(clippy::too_many_arguments)]
16736pub(crate) fn previous_row_blob_for_write_on(
16737    conn: &Connection,
16738    table: &str,
16739    row_id: &str,
16740    row_stamp: &str,
16741    column: &str,
16742    blob: &BlobRef,
16743    plaintext_size: u64,
16744    plaintext_hash: ObjectHash,
16745) -> Result<Option<StoreWriteRemoteBlob>, DbError> {
16746    if let Some(handoff) = created_upload_handoff_on(conn, table, row_id, column, row_stamp)? {
16747        let locator = handoff.stored.locator();
16748        if locator.namespace() != blob.namespace
16749            || locator.blob_id() != blob.id
16750            || locator.plaintext_size() != plaintext_size
16751            || locator.plaintext_hash() != plaintext_hash
16752            || locator.scope().is_some_and(|scope| scope != &blob.scope)
16753        {
16754            return Err(DbError::Message(format!(
16755                "created upload {table}/{row_id}/{column} at {row_stamp} differs from its captured row"
16756            )));
16757        }
16758        return Ok(Some(handoff));
16759    }
16760    let raw = conn
16761        .query_row(
16762            "SELECT row_blob_locators.audience_authority, blob_locators.remote_object_id
16763             FROM row_blob_locators
16764             JOIN blob_locators USING (remote_object_id)
16765             WHERE table_name = ?1 AND row_id = ?2 AND column_name = ?3
16766             ORDER BY row_stamp DESC LIMIT 1",
16767            rusqlite::params![table, row_id, column],
16768            |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
16769        )
16770        .optional()
16771        .map_err(DbError::from)?;
16772    let Some((authority, object_id)) = raw else {
16773        return Ok(None);
16774    };
16775    let authority: crate::sync::audience_package::PackageAudience =
16776        serde_json::from_str(&authority)
16777            .map_err(|error| DbError::Message(format!("prior row blob authority: {error}")))?;
16778    let object_id = object_id
16779        .parse()
16780        .map_err(|error| DbError::Message(format!("prior row blob object id: {error}")))?;
16781    let remote = load_remote_object_on(conn, object_id)?;
16782    if !remote.is_activated_stored_blob() {
16783        return Err(DbError::Message(format!(
16784            "prior row blob {table}/{row_id}/{column} is not activated"
16785        )));
16786    }
16787    let locator = BlobLocator::parse(remote.bytes().canonical_semantic_bytes())
16788        .map_err(|error| DbError::Message(format!("prior row blob locator: {error}")))?;
16789    if locator.namespace() != blob.namespace
16790        || locator.blob_id() != blob.id
16791        || locator.plaintext_size() != plaintext_size
16792        || locator.plaintext_hash() != plaintext_hash
16793        || locator.scope().is_some_and(|scope| scope != &blob.scope)
16794    {
16795        return Ok(None);
16796    }
16797    if locator.audience() != authority.remote_audience() {
16798        return Err(DbError::Message(format!(
16799            "prior row blob {table}/{row_id}/{column} authority differs from its locator"
16800        )));
16801    }
16802    let stored = StoredBlobRef::new(locator, remote.object().clone())
16803        .map_err(|error| DbError::Message(format!("prior row blob reference: {error}")))?;
16804    Ok(Some(StoreWriteRemoteBlob { authority, stored }))
16805}
16806
16807fn remote_audience_to_db(audience: &RemoteAudience) -> String {
16808    match audience {
16809        RemoteAudience::Store => "store".to_string(),
16810        RemoteAudience::Circle(circle_id) => circle_id.to_string(),
16811    }
16812}
16813
16814fn parse_remote_audience_db(value: &str) -> Result<RemoteAudience, DbError> {
16815    if value == "store" {
16816        return Ok(RemoteAudience::Store);
16817    }
16818    value.parse().map(RemoteAudience::Circle).map_err(|error| {
16819        DbError::Message(format!("invalid stored blob audience {value:?}: {error}"))
16820    })
16821}
16822
16823enum RemoteStoredRepresentationRef<'a> {
16824    Inline(&'a [u8]),
16825    Blob,
16826}
16827
16828fn candidate_manifest_exact_objects(commit: &StoreBatchCommit) -> Vec<&ExactObjectRef> {
16829    let mut objects = Vec::new();
16830    for candidate in &commit.candidate_objects.objects {
16831        match candidate {
16832            crate::sync::store_commit::CandidateExclusiveObjectRef::StorePackage(reference) => {
16833                objects.push(&reference.object);
16834            }
16835            crate::sync::store_commit::CandidateExclusiveObjectRef::CirclePackage(reference) => {
16836                objects.push(&reference.package.object);
16837            }
16838            crate::sync::store_commit::CandidateExclusiveObjectRef::CircleAccess {
16839                access, ..
16840            } => {
16841                objects.push(&access.leaf.object);
16842                objects.push(&access.envelope.object);
16843            }
16844            crate::sync::store_commit::CandidateExclusiveObjectRef::SelfRetirement(reference) => {
16845                objects.push(&reference.object);
16846            }
16847        }
16848    }
16849    objects
16850}
16851
16852fn validate_remote_object_on(
16853    conn: &Connection,
16854    object_id: ObjectHash,
16855    expected_object: &ExactObjectRef,
16856    expected_semantic_bytes: &[u8],
16857    expected_stored: RemoteStoredRepresentationRef<'_>,
16858) -> Result<(), DbError> {
16859    let remote = load_remote_object_on(conn, object_id)?;
16860    let stored = remote.bytes().stored();
16861    let stored_matches = match expected_stored {
16862        RemoteStoredRepresentationRef::Inline(expected) => stored.inline_bytes() == Some(expected),
16863        RemoteStoredRepresentationRef::Blob => stored.inline_bytes().is_none(),
16864    };
16865    if remote.object() != expected_object
16866        || remote.bytes().canonical_semantic_bytes() != expected_semantic_bytes
16867        || !stored_matches
16868    {
16869        return Err(DbError::Message(format!(
16870            "prepared remote object {object_id} differs from its semantic index"
16871        )));
16872    }
16873    Ok(())
16874}
16875
16876fn load_remote_object_on(
16877    conn: &Connection,
16878    object_id: ObjectHash,
16879) -> Result<RemoteObjectRecord, DbError> {
16880    let state: String = conn
16881        .query_row(
16882            "SELECT state FROM remote_objects WHERE object_id = ?1",
16883            [object_id.to_string()],
16884            |row| row.get(0),
16885        )
16886        .map_err(DbError::from)?;
16887    let remote: RemoteObjectRecord = serde_json::from_str(&state).map_err(|error| {
16888        DbError::Message(format!(
16889            "prepared remote object {object_id} has invalid closed state: {error}"
16890        ))
16891    })?;
16892    remote.validate().map_err(|error| {
16893        DbError::Message(format!("prepared remote object {object_id}: {error}"))
16894    })?;
16895    let actual = remote_object_id(remote.object());
16896    if actual != object_id {
16897        return Err(DbError::Message(format!(
16898            "prepared remote object key is {object_id}, exact reference hashes to {actual}"
16899        )));
16900    }
16901    Ok(remote)
16902}
16903
16904fn persist_exact_remote_object_on(
16905    conn: &Connection,
16906    remote: &RemoteObjectRecord,
16907    domain: &str,
16908) -> Result<(), DbError> {
16909    remote
16910        .validate()
16911        .map_err(|error| DbError::Message(format!("prepared {domain}: {error}")))?;
16912    let object_id = remote.object_id();
16913    let existing = conn
16914        .query_row(
16915            "SELECT state FROM remote_objects WHERE object_id = ?1",
16916            [object_id.to_string()],
16917            |row| row.get::<_, String>(0),
16918        )
16919        .optional()
16920        .map_err(DbError::from)?;
16921    if let Some(existing) = existing {
16922        let existing: RemoteObjectRecord = serde_json::from_str(&existing).map_err(|error| {
16923            DbError::Message(format!(
16924                "prepared {domain} {object_id} has invalid closed state: {error}"
16925            ))
16926        })?;
16927        if existing != *remote {
16928            return Err(DbError::Message(format!(
16929                "prepared {domain} {object_id} already has different closed state"
16930            )));
16931        }
16932        return Ok(());
16933    }
16934    let state = serde_json::to_string(remote)
16935        .map_err(|error| DbError::Message(format!("serialize prepared {domain}: {error}")))?;
16936    conn.execute(
16937        "INSERT INTO remote_objects (object_id, state) VALUES (?1, ?2)",
16938        (object_id.to_string(), state),
16939    )
16940    .map_err(DbError::from)?;
16941    Ok(())
16942}
16943
16944fn update_remote_object_on(
16945    conn: &Connection,
16946    object_id: ObjectHash,
16947    remote: &RemoteObjectRecord,
16948) -> Result<(), DbError> {
16949    remote
16950        .validate()
16951        .map_err(|error| DbError::Message(format!("remote object {object_id}: {error}")))?;
16952    if remote.object_id() != object_id {
16953        return Err(DbError::Message(format!(
16954            "remote object {object_id} changed its exact identity"
16955        )));
16956    }
16957    let state = serde_json::to_string(remote)
16958        .map_err(|error| DbError::Message(format!("serialize remote object: {error}")))?;
16959    let updated = conn
16960        .execute(
16961            "UPDATE remote_objects SET state = ?2 WHERE object_id = ?1",
16962            (object_id.to_string(), state),
16963        )
16964        .map_err(DbError::from)?;
16965    if updated != 1 {
16966        return Err(DbError::Message(format!(
16967            "remote object {object_id} disappeared during state transition"
16968        )));
16969    }
16970    Ok(())
16971}
16972
16973fn mark_remote_object_uploaded_on(
16974    conn: &Connection,
16975    expected: RemoteObjectRecord,
16976) -> Result<RemoteObjectRecord, DbError> {
16977    let object_id = expected.object_id();
16978    let current = load_remote_object_on(conn, object_id)?;
16979    if current != expected {
16980        return Err(DbError::Message(format!(
16981            "remote object {object_id} changed before upload completion"
16982        )));
16983    }
16984    let expected_json = serde_json::to_string(&expected)
16985        .map_err(|error| DbError::Message(format!("serialize expected remote object: {error}")))?;
16986    let mut uploaded = current;
16987    uploaded.mark_uploaded_verified().map_err(|error| {
16988        DbError::Message(format!("mark remote object {object_id} uploaded: {error}"))
16989    })?;
16990    let uploaded_json = serde_json::to_string(&uploaded)
16991        .map_err(|error| DbError::Message(format!("serialize uploaded remote object: {error}")))?;
16992    let updated = conn
16993        .execute(
16994            "UPDATE remote_objects SET state = ?3
16995             WHERE object_id = ?1 AND state = ?2",
16996            (object_id.to_string(), expected_json, uploaded_json),
16997        )
16998        .map_err(DbError::from)?;
16999    if updated != 1 {
17000        return Err(DbError::Message(format!(
17001            "remote object {object_id} lost upload ownership"
17002        )));
17003    }
17004    Ok(uploaded)
17005}
17006
17007fn merge_prepared_remote_object(
17008    existing: RemoteObjectRecord,
17009    proposed: &RemoteObjectRecord,
17010    owner: &StoreBatchCommitRef,
17011) -> Result<RemoteObjectRecord, DbError> {
17012    use crate::sync::remote_object::{OwnedObjectState, SharedLiveSetObjectDomain};
17013
17014    if &existing == proposed {
17015        return Ok(existing);
17016    }
17017    let (
17018        RemoteObjectRecord::SharedLiveSet(mut existing),
17019        RemoteObjectRecord::SharedLiveSet(proposed),
17020    ) = (existing, proposed)
17021    else {
17022        return Err(DbError::Message(format!(
17023            "remote object {} already has different closed state",
17024            proposed.object_id()
17025        )));
17026    };
17027    if existing.identity.domain != SharedLiveSetObjectDomain::StoredBlob
17028        || proposed.identity.domain != SharedLiveSetObjectDomain::StoredBlob
17029        || existing.identity != proposed.identity
17030        || existing.bytes != proposed.bytes
17031    {
17032        return Err(DbError::Message(format!(
17033            "stored blob object {} already has different identity or bytes",
17034            remote_object_id(&proposed.identity.object)
17035        )));
17036    }
17037    let proposed_has_owner = match &proposed.state {
17038        OwnedObjectState::Prepared { ownership } => ownership.pending.contains(owner),
17039        OwnedObjectState::UploadedVerified { ownership } => ownership.pending.contains(owner),
17040        OwnedObjectState::RetirementPending { .. } => false,
17041    };
17042    if !proposed_has_owner {
17043        return Err(DbError::Message(format!(
17044            "stored blob object {} does not name its preparing commit",
17045            remote_object_id(&proposed.identity.object)
17046        )));
17047    }
17048    let proposed_uploaded = matches!(&proposed.state, OwnedObjectState::UploadedVerified { .. });
17049    match &mut existing.state {
17050        OwnedObjectState::Prepared { ownership } => {
17051            ownership.pending.insert(owner.clone());
17052            if proposed_uploaded {
17053                existing.state = OwnedObjectState::UploadedVerified {
17054                    ownership: crate::sync::remote_object::SharedObjectOwnership {
17055                        pending: ownership.pending.clone(),
17056                        activated: std::collections::BTreeSet::new(),
17057                        nonactivated: ownership.nonactivated.clone(),
17058                    },
17059                };
17060            }
17061        }
17062        OwnedObjectState::UploadedVerified { ownership } => {
17063            ownership.pending.insert(owner.clone());
17064        }
17065        OwnedObjectState::RetirementPending { former_candidates } => {
17066            let ownership = crate::sync::remote_object::PendingCandidateOwnership {
17067                pending: std::collections::BTreeSet::from([owner.clone()]),
17068                nonactivated: former_candidates.clone(),
17069            };
17070            existing.state = if proposed_uploaded {
17071                OwnedObjectState::UploadedVerified {
17072                    ownership: crate::sync::remote_object::SharedObjectOwnership {
17073                        pending: ownership.pending,
17074                        activated: std::collections::BTreeSet::new(),
17075                        nonactivated: ownership.nonactivated,
17076                    },
17077                }
17078            } else {
17079                OwnedObjectState::Prepared { ownership }
17080            };
17081        }
17082    }
17083    let merged = RemoteObjectRecord::SharedLiveSet(existing);
17084    merged.validate().map_err(|error| {
17085        DbError::Message(format!(
17086            "merged stored blob object {}: {error}",
17087            merged.object_id()
17088        ))
17089    })?;
17090    Ok(merged)
17091}
17092
17093fn validate_prepared_package_on(
17094    conn: &Connection,
17095    write_id: &WriteId,
17096    expected: &PreparedAudiencePackage,
17097) -> Result<(), DbError> {
17098    let audience = expected.package().audience().remote_audience();
17099    let remote_object_id: String = conn
17100        .query_row(
17101            "SELECT remote_object_id
17102             FROM store_write_packages
17103             WHERE write_id = ?1 AND audience = ?2",
17104            rusqlite::params![write_id.as_str(), remote_audience_to_db(&audience)],
17105            |row| row.get(0),
17106        )
17107        .map_err(DbError::from)?;
17108    let remote_object_id = remote_object_id.parse().map_err(|error| {
17109        DbError::Message(format!(
17110            "stored prepared remote object id is invalid: {error}"
17111        ))
17112    })?;
17113    let actual =
17114        PreparedAudiencePackage::from_remote(load_remote_object_on(conn, remote_object_id)?)?;
17115    if actual.package != expected.package
17116        || actual.semantic_bytes != expected.semantic_bytes
17117        || actual.stored_bytes != expected.stored_bytes
17118        || actual.object != expected.object
17119        || actual.remote_object_id != expected.remote_object_id
17120    {
17121        return Err(DbError::Message(format!(
17122            "write {write_id} audience {audience:?} already has different prepared package bytes"
17123        )));
17124    }
17125    Ok(())
17126}
17127
17128fn validate_prepared_blob_on(
17129    conn: &Connection,
17130    write_id: &WriteId,
17131    expected: &PreparedAudienceBlob,
17132) -> Result<(), DbError> {
17133    let locator_hash = expected.blob().locator().locator_hash();
17134    let remote_object_id = expected.remote_object_id();
17135    let (stored_locator_hash, spool_path): (String, Option<String>) = conn
17136        .query_row(
17137            "SELECT locator_hash, spool_path
17138             FROM store_write_blobs
17139             WHERE write_id = ?1 AND audience = ?2 AND remote_object_id = ?3",
17140            rusqlite::params![
17141                write_id.as_str(),
17142                remote_audience_to_db(expected.audience()),
17143                remote_object_id.to_string(),
17144            ],
17145            |row| Ok((row.get(0)?, row.get(1)?)),
17146        )
17147        .map_err(DbError::from)?;
17148    if stored_locator_hash != locator_hash.to_string() {
17149        return Err(DbError::Message(format!(
17150            "write {write_id} audience {:?} exact object {remote_object_id} is indexed under locator {stored_locator_hash}, expected {locator_hash}",
17151            expected.audience()
17152        )));
17153    }
17154    let actual = PreparedAudienceBlob::from_remote(
17155        expected.audience().clone(),
17156        &locator_hash.to_string(),
17157        load_remote_object_on(conn, remote_object_id)?,
17158        spool_path.map(PathBuf::from),
17159    )?;
17160    if actual.blob() != expected.blob()
17161        || actual.spool_path() != expected.spool_path()
17162        || actual.remote_object_id() != expected.remote_object_id()
17163    {
17164        return Err(DbError::Message(format!(
17165            "write {write_id} audience {:?} exact object {remote_object_id} already has different prepared blob bytes",
17166            expected.audience()
17167        )));
17168    }
17169    Ok(())
17170}
17171
17172fn seed_from(hlc: &Hlc, value: Option<String>, context: &str) -> Result<(), DbError> {
17173    if let Some(stamp) = value {
17174        let floor = Timestamp::parse(&stamp)
17175            .ok_or_else(|| DbError::Message(format!("corrupt {context}: {stamp:?}")))?;
17176        hlc.seed(&floor);
17177    }
17178    Ok(())
17179}
17180
17181/// Greatest `_updated_at` within the restart seed's honest future bound, scanned
17182/// across every synced table. A registered table that does not exist is a host
17183/// integration error and surfaces as `Err`.
17184fn scan_max_updated_at(
17185    conn: &Connection,
17186    synced_tables: &[SyncedTable],
17187    seed_bound_ms: u64,
17188) -> Result<Option<String>, DbError> {
17189    let mut overall: Option<String> = None;
17190    let seed_bound = format!("{seed_bound_ms:013}");
17191    for t in synced_tables {
17192        let sql = format!(
17193            "SELECT MAX(_updated_at) FROM {} WHERE substr(_updated_at, 1, 13) <= ?1",
17194            crate::sync::session::quote_ident(t.name())
17195        );
17196        let value: Option<String> = conn
17197            .query_row(&sql, [&seed_bound], |r| r.get::<_, Option<String>>(0))
17198            .map_err(|e| {
17199                DbError::Message(format!(
17200                    "register-floor scan over synced table {}: {e}",
17201                    t.name()
17202                ))
17203            })?;
17204        if let Some(v) = value {
17205            overall = Some(match overall {
17206                Some(cur) if cur >= v => cur,
17207                _ => v,
17208            });
17209        }
17210    }
17211    Ok(overall)
17212}
17213
17214/// Create a capture session and attach every synced table, so a journaled
17215/// transaction records changes to exactly those tables.
17216fn attach_session<'c>(
17217    conn: &'c Connection,
17218    synced_tables: &[SyncedTable],
17219) -> Result<rusqlite::session::Session<'c>, DbError> {
17220    let mut session = rusqlite::session::Session::new(conn)
17221        .map_err(|e| DbError::Message(format!("failed to create capture session: {e}")))?;
17222    for t in synced_tables {
17223        session.attach(Some(t.name())).map_err(|e| {
17224            DbError::Message(format!(
17225                "failed to attach synced table {} to session: {e}",
17226                t.name()
17227            ))
17228        })?;
17229    }
17230    Ok(session)
17231}
17232
17233/// Drain a journal session's recorded changes into a changeset. The caller drops
17234/// the session right after (it lives only for the span of one journaled
17235/// transaction), so there is nothing to reset.
17236fn capture_changeset(session: &mut rusqlite::session::Session<'_>) -> Result<Vec<u8>, DbError> {
17237    let mut buf = Vec::new();
17238    session
17239        .changeset_strm(&mut buf)
17240        .map(|()| buf)
17241        .map_err(DbError::from)
17242}
17243
17244fn open_connection(path: &Path) -> Result<Connection, DbError> {
17245    let conn = Connection::open(path).map_err(DbError::from)?;
17246    // WAL so a read-only connection in another process can read committed rows while
17247    // this writer commits. The mode is stored in the db header and persists, so a
17248    // later read-only open finds the db already in WAL.
17249    conn.query_row("PRAGMA journal_mode = WAL", [], |_| Ok(()))
17250        .map_err(DbError::from)?;
17251    Ok(conn)
17252}
17253
17254/// Open a `SQLITE_OPEN_READONLY` connection for [`Database::open_read_only`].
17255/// `NO_MUTEX` because coven serializes every access
17256/// on its one connection thread; the connection sets no journal mode (a read-only
17257/// connection cannot, and the writer already put the db in WAL).
17258fn open_connection_read_only(path: &Path) -> Result<Connection, DbError> {
17259    use rusqlite::OpenFlags;
17260    let flags = OpenFlags::SQLITE_OPEN_READ_ONLY
17261        | OpenFlags::SQLITE_OPEN_NO_MUTEX
17262        | OpenFlags::SQLITE_OPEN_URI;
17263    Connection::open_with_flags(path, flags).map_err(DbError::from)
17264}
17265
17266#[cfg(test)]
17267mod tests {
17268    use super::*;
17269    use crate::blob::BLOB_TOMBSTONE_GRACE;
17270
17271    fn notes_migration() -> Migration {
17272        Migration::sql(
17273            1,
17274            "notes",
17275            "CREATE TABLE notes (
17276                id TEXT PRIMARY KEY,
17277                body TEXT NOT NULL,
17278                _updated_at TEXT NOT NULL
17279            ) STRICT;",
17280        )
17281    }
17282
17283    fn things_migration() -> Migration {
17284        Migration::sql(
17285            1,
17286            "things",
17287            "CREATE TABLE things (
17288                id TEXT PRIMARY KEY,
17289                body TEXT NOT NULL,
17290                _updated_at TEXT NOT NULL
17291            ) STRICT;",
17292        )
17293    }
17294
17295    fn things_table(identity: crate::sync::session::RowIdentity) -> SyncedTable {
17296        SyncedTable::new("things", identity)
17297    }
17298
17299    fn scoped_things_migration() -> Migration {
17300        Migration::sql(
17301            1,
17302            "scoped things",
17303            "CREATE TABLE things (
17304                id TEXT PRIMARY KEY,
17305                audience TEXT,
17306                body TEXT NOT NULL,
17307                _updated_at TEXT NOT NULL
17308            ) STRICT;",
17309        )
17310    }
17311
17312    fn scoped_things_table() -> SyncedTable {
17313        SyncedTable::new("things", crate::sync::session::RowIdentity::SharedKey)
17314            .scoped_by("audience")
17315    }
17316
17317    fn exact_blob_binding(row_id: &str, stamp: &str, bytes: &[u8]) -> RowBlobLocatorBinding {
17318        let plaintext_hash = ObjectHash::digest(bytes);
17319        let uploader_bytes = b"database test uploader registration";
17320        let uploader = crate::sync::store_commit::StoreDeviceRegistrationRef {
17321            device_id: "aa".repeat(32).parse().expect("valid test device id"),
17322            registration_hash: ObjectHash::digest(uploader_bytes),
17323            object: ExactObjectRef::new(
17324                crate::storage::cloud::ObjectSlot::logical(
17325                    "store-v1/devices/database-test-uploader.json".to_string(),
17326                )
17327                .expect("valid uploader registration slot"),
17328                uploader_bytes.len() as u64,
17329                ObjectHash::digest(uploader_bytes),
17330            ),
17331        };
17332        let locator = BlobLocator::browsable(
17333            "images",
17334            row_id,
17335            uploader,
17336            format!("photos/{row_id}.bin"),
17337            bytes.len() as u64,
17338            plaintext_hash,
17339        )
17340        .expect("valid locator");
17341        let stored = b"stored representation".to_vec();
17342        let slot = crate::storage::cloud::ObjectSlot::logical(locator.semantic_key())
17343            .expect("valid exact slot");
17344        let object = ExactObjectRef::new(slot, stored.len() as u64, ObjectHash::digest(&stored));
17345        RowBlobLocatorBinding::new(
17346            "photos",
17347            row_id,
17348            stamp,
17349            "id",
17350            StoredBlobRef::new(locator, object).expect("valid stored blob"),
17351        )
17352        .expect("valid row binding")
17353    }
17354
17355    #[test]
17356    fn snapshot_blob_owner_rejects_other_activation_and_sequence() {
17357        let conn = Connection::open_in_memory().expect("open snapshot owner database");
17358        apply_coven_schema(&conn).expect("apply snapshot owner schema");
17359        let binding = exact_blob_binding(
17360            "snapshot-owner-photo",
17361            "0000000001000-0000-owner",
17362            b"snapshot owner bytes",
17363        );
17364        let expected = crate::sync::remote_object::SnapshotObjectOwner {
17365            activation: StreamActivationId::from_hash(ObjectHash::digest(
17366                b"verified snapshot activation",
17367            )),
17368            sequence: 7,
17369        };
17370        let install = |owner: crate::sync::remote_object::SnapshotObjectOwner| {
17371            conn.execute("DELETE FROM remote_objects", [])
17372                .expect("clear snapshot owner");
17373            let remote = RemoteObjectRecord::snapshot_activated_blob(binding.blob(), owner)
17374                .expect("build snapshot-owned blob");
17375            conn.execute(
17376                "INSERT INTO remote_objects (object_id, state) VALUES (?1, ?2)",
17377                rusqlite::params![
17378                    remote.object_id().to_string(),
17379                    serde_json::to_string(&remote).expect("serialize snapshot-owned blob"),
17380                ],
17381            )
17382            .expect("install snapshot-owned blob");
17383        };
17384
17385        install(expected.clone());
17386        validate_snapshot_object_owner_records_on(&conn, &expected)
17387            .expect("verified snapshot owner matches");
17388
17389        install(crate::sync::remote_object::SnapshotObjectOwner {
17390            activation: StreamActivationId::from_hash(ObjectHash::digest(
17391                b"other snapshot activation",
17392            )),
17393            sequence: expected.sequence,
17394        });
17395        assert!(validate_snapshot_object_owner_records_on(&conn, &expected).is_err());
17396
17397        install(crate::sync::remote_object::SnapshotObjectOwner {
17398            activation: expected.activation,
17399            sequence: expected.sequence + 1,
17400        });
17401        assert!(validate_snapshot_object_owner_records_on(&conn, &expected).is_err());
17402    }
17403
17404    fn local_row_blob(row_id: &str, stamp: &str, bytes: &[u8]) -> RowBlobRef {
17405        let binding = exact_blob_binding(row_id, stamp, bytes);
17406        let locator = binding.blob().locator();
17407        RowBlobRef::new(
17408            "photos".to_string(),
17409            row_id.to_string(),
17410            stamp.to_string(),
17411            "id".to_string(),
17412            BlobRef {
17413                namespace: locator.namespace().to_string(),
17414                id: locator.blob_id().to_string(),
17415                scope: crate::blob::BlobScope::Master,
17416                cloud_path: locator.cloud_path().map(str::to_string),
17417                provenance: Provenance::HostProvided,
17418                fill: crate::blob::CacheFill::CacheLazy,
17419            },
17420            locator.plaintext_size(),
17421            locator.plaintext_hash(),
17422            RowBlobAuthority::Local,
17423            None,
17424        )
17425        .expect("valid Local row blob")
17426    }
17427
17428    fn open_outbox_database(device_id: &str) -> Database {
17429        Database::open(
17430            Path::new(":memory:"),
17431            Vec::new(),
17432            BLOB_TOMBSTONE_GRACE,
17433            crate::blob::TransferLimits::serial(),
17434            crate::WritePolicy::MergeConcurrent,
17435            device_id.to_string(),
17436            &[],
17437        )
17438        .expect("open outbox database")
17439        .0
17440    }
17441
17442    fn test_candidate_family() -> crate::sync::store_commit::CandidateFamilyId {
17443        crate::sync::store_commit::CandidateFamilyId::from_hash(ObjectHash::digest(
17444            b"database test candidate family",
17445        ))
17446    }
17447
17448    fn test_commit_coord() -> StoreCommitCoord {
17449        StoreCommitCoord::MergeConcurrent {
17450            stream_id: crate::sync::membership::AuthorStreamId::from_bytes([7; 32]),
17451            sequence: 1,
17452        }
17453    }
17454
17455    fn test_commit_ref() -> StoreBatchCommitRef {
17456        let coord = test_commit_coord();
17457        let commit_hash = ObjectHash::digest(b"database test commit");
17458        let StoreCommitCoord::MergeConcurrent { stream_id, .. } = &coord else {
17459            unreachable!("database test coordinate is MergeConcurrent")
17460        };
17461        let slot = crate::storage::cloud::ObjectSlot::logical(format!(
17462            "{}.json",
17463            commit_semantic_prefix(
17464                test_candidate_family(),
17465                &stream_id.to_string(),
17466                coord.sequence(),
17467                commit_hash,
17468            )
17469        ))
17470        .expect("valid database test commit slot");
17471        StoreBatchCommitRef {
17472            coord,
17473            commit_hash,
17474            object: ExactObjectRef::new(slot, 1, ObjectHash::digest(b"x")),
17475        }
17476    }
17477
17478    #[tokio::test]
17479    async fn upload_retry_preserves_prepared_object_handoff() {
17480        let db = open_outbox_database("prepared-upload-retry");
17481        let row = local_row_blob("photo", "0000000001000-0000-a", b"photo bytes");
17482        let initial_row = row.clone();
17483        db.call(move |conn| {
17484            Database::enqueue_upload_on(
17485                conn,
17486                "photos",
17487                "photo",
17488                &initial_row,
17489                Path::new("/source/first"),
17490                false,
17491                "2026-07-16T10:00:00Z",
17492            )
17493        })
17494        .await
17495        .expect("enqueue upload");
17496
17497        let entry = db
17498            .get_pending_cloud_uploads()
17499            .await
17500            .expect("read upload")
17501            .pop()
17502            .expect("upload entry");
17503        let stored = exact_blob_binding("photo", "0000000001000-0000-a", b"photo bytes")
17504            .blob()
17505            .clone();
17506        let spool_path = PathBuf::from("/spool/prepared");
17507        db.mark_cloud_upload_prepared(
17508            &entry,
17509            crate::sync::audience_package::PackageAudience::Store,
17510            stored.clone(),
17511            spool_path.clone(),
17512        )
17513        .await
17514        .expect("record prepared object");
17515
17516        db.call(move |conn| {
17517            Database::enqueue_upload_on(
17518                conn,
17519                "photos",
17520                "photo",
17521                &row,
17522                Path::new("/source/retried"),
17523                true,
17524                "2026-07-16T10:01:00Z",
17525            )
17526        })
17527        .await
17528        .expect("retry upload command");
17529
17530        let entries = db
17531            .get_pending_cloud_uploads()
17532            .await
17533            .expect("read retried upload");
17534        assert_eq!(entries.len(), 1);
17535        assert_eq!(
17536            entries[0].operation,
17537            OutboxOperation::Upload {
17538                root_table: "photos".to_string(),
17539                root_id: "photo".to_string(),
17540                row: local_row_blob("photo", "0000000001000-0000-a", b"photo bytes"),
17541                source_path: PathBuf::from("/source/retried"),
17542                retain_pinned: true,
17543                state: OutboxUploadState::Prepared {
17544                    authority: crate::sync::audience_package::PackageAudience::Store,
17545                    stored,
17546                    spool_path,
17547                },
17548            }
17549        );
17550        db.mark_cloud_upload_created(&entries[0])
17551            .await
17552            .expect("record exact cloud creation");
17553        let created = db
17554            .get_pending_cloud_uploads()
17555            .await
17556            .expect("read Created upload");
17557        let OutboxOperation::Upload { state, .. } = &created[0].operation else {
17558            panic!("upload query returned a non-upload operation");
17559        };
17560        assert!(matches!(
17561            state,
17562            OutboxUploadState::Created {
17563                authority: crate::sync::audience_package::PackageAudience::Store,
17564                ..
17565            }
17566        ));
17567    }
17568
17569    #[tokio::test]
17570    async fn repeated_exact_delete_resets_retry_state_without_duplication() {
17571        let db = open_outbox_database("repeat-delete");
17572        let stored = exact_blob_binding("photo", "0000000001000-0000-a", b"photo bytes")
17573            .blob()
17574            .clone();
17575        let delete_stored = stored.clone();
17576        db.call(move |conn| {
17577            Database::enqueue_delete_on(conn, &delete_stored, "2026-07-16T10:00:00Z")
17578        })
17579        .await
17580        .expect("enqueue delete");
17581        let failed = db
17582            .get_pending_cloud_deletes()
17583            .await
17584            .expect("read pending delete")
17585            .pop()
17586            .expect("delete entry");
17587        db.record_cloud_outbox_failure(&failed, "provider unavailable", "2026-07-16T10:00:30Z")
17588            .await
17589            .expect("record delete failure");
17590        let retry_stored = stored.clone();
17591        db.call(move |conn| {
17592            Database::enqueue_delete_on(conn, &retry_stored, "2026-07-16T10:01:00Z")
17593        })
17594        .await
17595        .expect("repeat exact delete");
17596
17597        let deletes = db.get_pending_cloud_deletes().await.expect("read deletes");
17598        assert_eq!(deletes.len(), 1);
17599        assert_eq!(deletes[0].attempt_count, 0);
17600        assert_eq!(deletes[0].last_attempt_at, None);
17601        assert_eq!(deletes[0].operation, OutboxOperation::Delete { stored });
17602    }
17603
17604    #[tokio::test]
17605    async fn exact_deletes_for_distinct_objects_remain_distinct() {
17606        let db = open_outbox_database("distinct-deletes");
17607        let first = exact_blob_binding("photo-a", "0000000001000-0000-a", b"photo a")
17608            .blob()
17609            .clone();
17610        let second = exact_blob_binding("photo-b", "0000000001000-0000-a", b"photo b")
17611            .blob()
17612            .clone();
17613        db.call(move |conn| Database::enqueue_delete_on(conn, &first, "2026-07-16T10:00:00Z"))
17614            .await
17615            .expect("enqueue first delete");
17616        db.call(move |conn| Database::enqueue_delete_on(conn, &second, "2026-07-16T10:01:00Z"))
17617            .await
17618            .expect("enqueue second delete");
17619
17620        let deletes = db.get_pending_cloud_deletes().await.expect("read deletes");
17621        assert_eq!(deletes.len(), 2);
17622        assert_ne!(deletes[0].operation, deletes[1].operation);
17623    }
17624
17625    fn blob_binding_table() -> SyncedTable {
17626        SyncedTable::new("photos", crate::sync::session::RowIdentity::SharedKey).carries_blob(
17627            crate::sync::session::BlobDecl::new(
17628                "images",
17629                Provenance::HostProvided,
17630                crate::blob::CacheFill::CacheLazy,
17631            )
17632            .with_cloud_path_column("cloud_path"),
17633        )
17634    }
17635
17636    fn insert_blob_row(conn: &Connection, row_id: &str, stamp: &str, bytes: &[u8]) {
17637        conn.execute(
17638            "INSERT INTO photos (id, size, hash, cloud_path, _updated_at)
17639             VALUES (?1, ?2, ?3, ?4, ?5)",
17640            rusqlite::params![
17641                row_id,
17642                bytes.len() as i64,
17643                ObjectHash::digest(bytes).to_string(),
17644                format!("photos/{row_id}.bin"),
17645                stamp,
17646            ],
17647        )
17648        .expect("insert blob row");
17649    }
17650
17651    #[test]
17652    fn blob_bindings_install_only_for_exact_winning_row_stamps() {
17653        let mut conn = Connection::open_in_memory().expect("open");
17654        apply_coven_schema(&conn).expect("apply coven schema");
17655        conn.execute_batch(
17656            "CREATE TABLE photos (
17657                id TEXT PRIMARY KEY,
17658                size INTEGER NOT NULL,
17659                hash TEXT NOT NULL,
17660                cloud_path TEXT NOT NULL,
17661                _updated_at TEXT NOT NULL
17662            ) STRICT;",
17663        )
17664        .expect("create photos");
17665        let table = blob_binding_table();
17666        let tables = vec![table];
17667        let gates = Gates::from_tables(&conn, &tables).expect("build gates");
17668        insert_blob_row(&conn, "winner", "0000000001000-0000-a", b"winner bytes");
17669        insert_blob_row(&conn, "loser", "0000000002000-0000-b", b"loser bytes");
17670
17671        let package = AudiencePackage::store(
17672            ObjectHash::digest(b"root"),
17673            test_candidate_family(),
17674            WriteId::from_generated("write-1".to_string()),
17675            test_commit_coord(),
17676            1,
17677            Vec::new(),
17678            vec![
17679                exact_blob_binding("winner", "0000000001000-0000-a", b"winner bytes"),
17680                exact_blob_binding("loser", "0000000001000-0000-b", b"loser bytes"),
17681            ],
17682        )
17683        .expect("build package");
17684        let activation = BlobActivation {
17685            coord: test_commit_coord(),
17686        };
17687
17688        let tx = conn.transaction().expect("begin");
17689        Database::install_pulled_blob_activations_on(&tx, &package, &test_commit_ref())
17690            .expect("install pulled blob activation");
17691        let winning_rows = [crate::sync::apply::WinningRow {
17692            table: "photos".to_string(),
17693            row_id: "winner".to_string(),
17694            row_stamp: Some("0000000001000-0000-a".to_string()),
17695        }];
17696        assert_eq!(
17697            Database::install_winning_blob_bindings_on(
17698                &tx,
17699                &gates,
17700                &tables,
17701                &package,
17702                &activation,
17703                &winning_rows,
17704            )
17705            .expect("install winning binding"),
17706            1
17707        );
17708        tx.commit().expect("commit");
17709
17710        assert_eq!(
17711            conn.query_row("SELECT count(*) FROM blob_locators", [], |row| row
17712                .get::<_, i64>(0))
17713                .expect("count locators"),
17714            1
17715        );
17716        assert_eq!(
17717            conn.query_row("SELECT row_id FROM row_blob_locators", [], |row| row
17718                .get::<_, String>(0))
17719                .expect("read binding"),
17720            "winner"
17721        );
17722        let resolved = Database::row_blob_ref_on(&conn, &gates, &tables[0], "winner")
17723            .expect("resolve exact row blob reference");
17724        assert_eq!(resolved.row_stamp(), "0000000001000-0000-a");
17725        assert_eq!(
17726            resolved.plaintext_hash(),
17727            ObjectHash::digest(b"winner bytes")
17728        );
17729        assert_eq!(
17730            resolved
17731                .stored()
17732                .expect("remote row carries exact locator")
17733                .locator()
17734                .blob_id(),
17735            "winner"
17736        );
17737    }
17738
17739    #[test]
17740    fn mismatched_blob_values_roll_back_locator_installation_with_rows() {
17741        let mut conn = Connection::open_in_memory().expect("open");
17742        apply_coven_schema(&conn).expect("apply coven schema");
17743        conn.execute_batch(
17744            "CREATE TABLE photos (
17745                id TEXT PRIMARY KEY,
17746                size INTEGER NOT NULL,
17747                hash TEXT NOT NULL,
17748                cloud_path TEXT NOT NULL,
17749                _updated_at TEXT NOT NULL
17750            ) STRICT;",
17751        )
17752        .expect("create photos");
17753        let tables = vec![blob_binding_table()];
17754        let gates = Gates::from_tables(&conn, &tables).expect("build gates");
17755        insert_blob_row(&conn, "photo", "0000000001000-0000-a", b"actual bytes");
17756        let package = AudiencePackage::store(
17757            ObjectHash::digest(b"root"),
17758            test_candidate_family(),
17759            WriteId::from_generated("write-1".to_string()),
17760            test_commit_coord(),
17761            1,
17762            Vec::new(),
17763            vec![exact_blob_binding(
17764                "photo",
17765                "0000000001000-0000-a",
17766                b"different bytes",
17767            )],
17768        )
17769        .expect("build package");
17770        let activation = BlobActivation {
17771            coord: test_commit_coord(),
17772        };
17773
17774        let tx = conn.transaction().expect("begin");
17775        Database::install_pulled_blob_activations_on(&tx, &package, &test_commit_ref())
17776            .expect("install pulled blob activation");
17777        let winning_rows = [crate::sync::apply::WinningRow {
17778            table: "photos".to_string(),
17779            row_id: "photo".to_string(),
17780            row_stamp: Some("0000000001000-0000-a".to_string()),
17781        }];
17782        let error = Database::install_winning_blob_bindings_on(
17783            &tx,
17784            &gates,
17785            &tables,
17786            &package,
17787            &activation,
17788            &winning_rows,
17789        )
17790        .expect_err("mismatched locator must fail");
17791        assert!(error
17792            .to_string()
17793            .contains("does not match winning row values"));
17794        tx.rollback().expect("roll back");
17795        assert_eq!(
17796            conn.query_row("SELECT count(*) FROM blob_locators", [], |row| row
17797                .get::<_, i64>(0))
17798                .expect("count locators"),
17799            0
17800        );
17801    }
17802
17803    fn assert_coven_schema_mutation_is_rejected(
17804        name: &str,
17805        tables: Vec<SyncedTable>,
17806        write_policy: crate::WritePolicy,
17807        migrations: Vec<Migration>,
17808        mutate: impl FnOnce(&Connection),
17809    ) {
17810        let directory = tempfile::tempdir().expect("temp dir");
17811        let path = directory.path().join(format!("{name}.sqlite"));
17812        let (database, _) = Database::open(
17813            &path,
17814            tables.clone(),
17815            BLOB_TOMBSTONE_GRACE,
17816            crate::blob::TransferLimits::serial(),
17817            write_policy,
17818            "schema-seed".to_string(),
17819            &migrations,
17820        )
17821        .expect("seed database");
17822        drop(database);
17823
17824        let conn = Connection::open(&path).expect("open database for schema mutation");
17825        mutate(&conn);
17826        drop(conn);
17827
17828        let writer_error = match Database::open(
17829            &path,
17830            tables.clone(),
17831            BLOB_TOMBSTONE_GRACE,
17832            crate::blob::TransferLimits::serial(),
17833            write_policy,
17834            "schema-writer".to_string(),
17835            &migrations,
17836        ) {
17837            Ok(_) => panic!("writer must reject Coven schema mutation {name}"),
17838            Err(error) => error.to_string(),
17839        };
17840        assert!(
17841            writer_error.contains("Coven schema"),
17842            "{name}: {writer_error}"
17843        );
17844
17845        let reader_error = match Database::open_read_only(
17846            &path,
17847            tables,
17848            BLOB_TOMBSTONE_GRACE,
17849            crate::blob::TransferLimits::serial(),
17850            write_policy,
17851            "schema-reader".to_string(),
17852            &migrations,
17853        ) {
17854            Ok(_) => panic!("read-only open must reject Coven schema mutation {name}"),
17855            Err(error) => error.to_string(),
17856        };
17857        assert!(
17858            reader_error.contains("Coven schema"),
17859            "{name}: {reader_error}"
17860        );
17861
17862        let conn = Connection::open(&path).expect("inspect rejected database");
17863        let host_device_id: String = conn
17864            .query_row(
17865                "SELECT value FROM protocol_state WHERE key = ?1",
17866                [HOST_DEVICE_ID_STATE_KEY],
17867                |row| row.get(0),
17868            )
17869            .expect("host device id");
17870        assert_eq!(host_device_id, "schema-seed", "{name} was rewritten");
17871    }
17872
17873    #[tokio::test]
17874    async fn required_store_root_hash_rejects_missing_and_malformed_exact_authority() {
17875        let (db, _) = Database::open(
17876            Path::new(":memory:"),
17877            vec![SyncedTable::new(
17878                "notes",
17879                crate::sync::session::RowIdentity::SharedKey,
17880            )],
17881            BLOB_TOMBSTONE_GRACE,
17882            crate::blob::TransferLimits::serial(),
17883            crate::WritePolicy::MergeConcurrent,
17884            "required-store-root".to_string(),
17885            &[notes_migration()],
17886        )
17887        .expect("open database");
17888
17889        let missing = db
17890            .required_store_root_hash()
17891            .await
17892            .expect_err("missing Store root must fail");
17893        assert!(matches!(missing, DbError::StoreRootHashMissing));
17894
17895        db.call(|conn| {
17896            conn.execute(
17897                "INSERT INTO store_protocol_root_authority
17898                 (singleton, store_root_hash, store_protocol_root_bytes, store_root_object)
17899                 VALUES (1, ?1, X'00', '{}')",
17900                ["00".repeat(32)],
17901            )
17902            .map(|_| ())
17903            .map_err(DbError::from)
17904        })
17905        .await
17906        .expect("write malformed exact Store root authority");
17907        let malformed = db
17908            .required_store_root_hash()
17909            .await
17910            .expect_err("malformed Store root must fail");
17911        assert!(matches!(
17912            malformed,
17913            DbError::Message(reason) if !reason.is_empty()
17914        ));
17915
17916        db.call(|conn| {
17917            conn.execute("DELETE FROM store_protocol_root_authority", [])
17918                .map(|_| ())
17919                .map_err(DbError::from)
17920        })
17921        .await
17922        .expect("remove malformed authority");
17923        let signer = crate::keys::UserKeypair::generate();
17924        let founder_provider_admin =
17925            crate::sync::test_helpers::test_founder_provider_admin("required-store-root");
17926        let descriptor = crate::sync::store_commit::StoreCreationDescriptor {
17927            version: crate::sync::store_commit::STORE_PROTOCOL_VERSION,
17928            creation_id: crate::sync::store_commit::StoreCreationId::from_nonce(
17929                "required-store-root",
17930            ),
17931            provider: crate::sync::storage::StoreProviderBinding::S3 {
17932                endpoint: crate::sync::storage::S3EndpointBinding::Custom {
17933                    origin: "https://test.invalid".to_string(),
17934                },
17935                region: "test-region".to_string(),
17936                bucket: "required-store-root-bucket".to_string(),
17937                key_prefix: None,
17938            },
17939            schema_version: db.schema_version(),
17940            sync_routing_hash: db.sync_routing_hash(),
17941            write_policy: crate::WritePolicy::MergeConcurrent,
17942            founder_pubkey: crate::keys::public_key_hex(&signer),
17943            founder_grant: crate::sync::test_helpers::test_membership_grant_id(
17944                "required-store-root founder",
17945            ),
17946            root_slot: crate::storage::cloud::ObjectSlot::logical(
17947                crate::sync::store_commit::STORE_PROTOCOL_ROOT_LOGICAL_KEY.to_string(),
17948            )
17949            .expect("valid Store root slot"),
17950            founder_registration: crate::storage::cloud::ObjectSlot::logical(
17951                "store-v1/test/required-store-root/registration.json".to_string(),
17952            )
17953            .expect("valid founder registration slot"),
17954            founder_provider_admin,
17955            membership: crate::sync::store_commit::StoreMembershipGenesis::MergeConcurrent {
17956                founder_membership: crate::sync::store_commit::GrantStreamAnchor::StoreMembership {
17957                    first_slot: crate::storage::cloud::ObjectSlot::logical(
17958                        "store-v1/test/required-store-root/membership/1.json".to_string(),
17959                    )
17960                    .expect("valid membership slot"),
17961                },
17962            },
17963            founder_recovery: crate::sync::store_commit::GrantStreamAnchor::OwnerRecovery {
17964                first_slot: crate::storage::cloud::ObjectSlot::logical(
17965                    "store-v1/test/required-store-root/recovery/1.json".to_string(),
17966                )
17967                .expect("valid recovery slot"),
17968            },
17969        };
17970        let root = crate::sync::store_commit::StoreProtocolRoot::signed(descriptor, &signer)
17971            .expect("sign Store root authority");
17972        let bytes = root.to_bytes();
17973        let expected = root.object_hash();
17974        let reference = crate::sync::store_commit::StoreRootRef {
17975            store_root_id: root.descriptor.store_root_id(),
17976            store_root_hash: expected,
17977            object: ExactObjectRef::new(
17978                crate::storage::cloud::ObjectSlot::logical(
17979                    "store-v1/store-protocol-root/required-store-root.json".to_string(),
17980                )
17981                .expect("valid Store root slot"),
17982                bytes.len() as u64,
17983                ObjectHash::digest(&bytes),
17984            ),
17985        };
17986        db.install_store_root_authority(reference, bytes)
17987            .await
17988            .expect("install exact Store root authority");
17989        assert_eq!(db.required_store_root_hash().await.unwrap(), expected);
17990    }
17991
17992    #[test]
17993    fn fresh_open_rolls_back_host_schema_and_coven_metadata_when_routing_is_invalid() {
17994        let directory = tempfile::tempdir().expect("temp dir");
17995        let path = directory.path().join("fresh-routing-failure.sqlite");
17996        let result = Database::open(
17997            &path,
17998            vec![things_table(crate::sync::session::RowIdentity::SharedKey)],
17999            BLOB_TOMBSTONE_GRACE,
18000            crate::blob::TransferLimits::serial(),
18001            crate::WritePolicy::MergeConcurrent,
18002            "fresh-routing-failure".to_string(),
18003            &[Migration::sql(
18004                1,
18005                "invalid routing",
18006                "CREATE TABLE local_parents (id TEXT PRIMARY KEY) STRICT;
18007                 CREATE TABLE things (
18008                    id TEXT PRIMARY KEY,
18009                    local_parent_id TEXT NOT NULL REFERENCES local_parents(id),
18010                    _updated_at TEXT NOT NULL
18011                 ) STRICT;",
18012            )],
18013        );
18014        let error = match result {
18015            Ok(_) => panic!("fresh open must reject a synced-to-local foreign key"),
18016            Err(error) => error.to_string(),
18017        };
18018        assert!(error.contains("local_parents"), "{error}");
18019
18020        let conn = Connection::open(&path).expect("inspect rolled-back database");
18021        let user_version: i64 = conn
18022            .query_row("PRAGMA user_version", [], |row| row.get(0))
18023            .expect("user version");
18024        let durable_tables: i64 = conn
18025            .query_row(
18026                "SELECT COUNT(*) FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%'",
18027                [],
18028                |row| row.get(0),
18029            )
18030            .expect("durable tables");
18031        assert_eq!(user_version, 0);
18032        assert_eq!(durable_tables, 0);
18033    }
18034
18035    #[test]
18036    fn initialized_open_commits_ordinary_migration_without_changing_routing_contract() {
18037        let directory = tempfile::tempdir().expect("temp dir");
18038        let path = directory.path().join("ordinary-migration.sqlite");
18039        let migrations = [things_migration()];
18040        let (database, _) = Database::open(
18041            &path,
18042            vec![things_table(crate::sync::session::RowIdentity::SharedKey)],
18043            BLOB_TOMBSTONE_GRACE,
18044            crate::blob::TransferLimits::serial(),
18045            crate::WritePolicy::MergeConcurrent,
18046            "ordinary-first-open".to_string(),
18047            &migrations,
18048        )
18049        .expect("initial open");
18050        let pinned_hash = database.sync_routing_hash();
18051        drop(database);
18052
18053        let migrations = [
18054            things_migration(),
18055            Migration::sql(
18056                2,
18057                "ordinary column and index",
18058                "ALTER TABLE things ADD COLUMN ordinary TEXT DEFAULT 'ordinary';
18059                 CREATE INDEX things_ordinary ON things(ordinary);",
18060            ),
18061        ];
18062        let (database, _) = Database::open(
18063            &path,
18064            vec![things_table(crate::sync::session::RowIdentity::SharedKey)],
18065            BLOB_TOMBSTONE_GRACE,
18066            crate::blob::TransferLimits::serial(),
18067            crate::WritePolicy::MergeConcurrent,
18068            "ordinary-first-open".to_string(),
18069            &migrations,
18070        )
18071        .expect("ordinary migration open");
18072        assert_eq!(database.schema_version(), 2);
18073        assert_eq!(database.sync_routing_hash(), pinned_hash);
18074        drop(database);
18075
18076        let conn = Connection::open(&path).expect("inspect migrated database");
18077        let ordinary_ordinal: i64 = conn
18078            .query_row(
18079                "SELECT cid FROM pragma_table_info('things') WHERE name = 'ordinary'",
18080                [],
18081                |row| row.get(0),
18082            )
18083            .expect("ordinary column");
18084        assert_eq!(ordinary_ordinal, 3);
18085    }
18086
18087    #[test]
18088    fn initialized_open_rolls_back_routing_migration_and_user_version() {
18089        let directory = tempfile::tempdir().expect("temp dir");
18090        let path = directory.path().join("routing-migration.sqlite");
18091        let v1 = || {
18092            Migration::sql(
18093                1,
18094                "gated things",
18095                "CREATE TABLE things (
18096                    id TEXT PRIMARY KEY,
18097                    audience TEXT COLLATE BINARY NOT NULL,
18098                    _updated_at TEXT NOT NULL
18099                 ) STRICT;",
18100            )
18101        };
18102        let table = || {
18103            SyncedTable::new("things", crate::sync::session::RowIdentity::SharedKey)
18104                .gated_by("audience")
18105        };
18106        let (database, _) = Database::open(
18107            &path,
18108            vec![table()],
18109            BLOB_TOMBSTONE_GRACE,
18110            crate::blob::TransferLimits::serial(),
18111            crate::WritePolicy::MergeConcurrent,
18112            "routing-first-open".to_string(),
18113            &[v1()],
18114        )
18115        .expect("initial open");
18116        drop(database);
18117
18118        let v2 = Migration::sql(
18119            2,
18120            "change audience collation",
18121            "CREATE TABLE things_next (
18122                id TEXT PRIMARY KEY,
18123                audience TEXT COLLATE NOCASE NOT NULL,
18124                _updated_at TEXT NOT NULL
18125             ) STRICT;
18126             INSERT INTO things_next SELECT * FROM things;
18127             DROP TABLE things;
18128             ALTER TABLE things_next RENAME TO things;",
18129        );
18130        let result = Database::open(
18131            &path,
18132            vec![table()],
18133            BLOB_TOMBSTONE_GRACE,
18134            crate::blob::TransferLimits::serial(),
18135            crate::WritePolicy::MergeConcurrent,
18136            "routing-first-open".to_string(),
18137            &[v1(), v2],
18138        );
18139        let error = match result {
18140            Ok(_) => panic!("routing migration must not commit"),
18141            Err(error) => error.to_string(),
18142        };
18143        assert!(error.contains("sync-routing hash"), "{error}");
18144
18145        let conn = Connection::open(&path).expect("inspect rolled-back migration");
18146        let user_version: i64 = conn
18147            .query_row("PRAGMA user_version", [], |row| row.get(0))
18148            .expect("user version");
18149        let things_next: i64 = conn
18150            .query_row(
18151                "SELECT COUNT(*) FROM sqlite_schema WHERE type = 'table' AND name = 'things_next'",
18152                [],
18153                |row| row.get(0),
18154            )
18155            .expect("things_next presence");
18156        let (_, collation, _, _, _) = conn
18157            .column_metadata(None::<&str>, "things", "audience")
18158            .expect("audience metadata");
18159        assert_eq!(user_version, 1);
18160        assert_eq!(things_next, 0);
18161        assert_eq!(collation.unwrap().to_bytes(), b"BINARY");
18162    }
18163
18164    #[test]
18165    fn writer_and_read_only_open_reject_every_coven_schema_shape_change_without_rewriting() {
18166        let cases = [
18167            ("missing-table", "DROP TABLE published_store_acks;"),
18168            (
18169                "changed-primary-key-and-constraints",
18170                "DROP TABLE published_store_acks;
18171                 CREATE TABLE published_store_acks (
18172                    revision INTEGER,
18173                    ack_hash TEXT NOT NULL,
18174                    PRIMARY KEY (ack_hash)
18175                 ) STRICT;",
18176            ),
18177            (
18178                "unexpected-index",
18179                "CREATE INDEX coven_unexpected_store_write_status ON store_writes(status);",
18180            ),
18181            (
18182                "missing-strict",
18183                "DROP TABLE local_cleanup_intents;
18184                 CREATE TABLE local_cleanup_intents (
18185                    namespace TEXT NOT NULL,
18186                    blob_id TEXT NOT NULL,
18187                    PRIMARY KEY (namespace, blob_id)
18188                 );",
18189            ),
18190            (
18191                "missing-without-rowid",
18192                "DROP TABLE _coven_audience;
18193                 CREATE TABLE _coven_audience (
18194                    routing_id TEXT PRIMARY KEY,
18195                    circle_id TEXT,
18196                    _updated_at TEXT NOT NULL
18197                 ) STRICT;",
18198            ),
18199        ];
18200
18201        for (name, mutation) in cases {
18202            assert_coven_schema_mutation_is_rejected(
18203                name,
18204                vec![scoped_things_table()],
18205                crate::WritePolicy::MergeConcurrent,
18206                vec![scoped_things_migration()],
18207                |conn| {
18208                    conn.execute_batch(mutation).expect("mutate Coven schema");
18209                },
18210            );
18211        }
18212    }
18213
18214    #[test]
18215    fn writer_and_read_only_open_reject_unexpected_policy_specific_coven_tables() {
18216        assert_coven_schema_mutation_is_rejected(
18217            "unexpected-routing",
18218            vec![things_table(crate::sync::session::RowIdentity::SharedKey)],
18219            crate::WritePolicy::Serial,
18220            vec![things_migration()],
18221            |conn| {
18222                crate::db::apply_coven_routing_schema(conn)
18223                    .expect("add inapplicable routing tables");
18224            },
18225        );
18226    }
18227
18228    #[test]
18229    fn first_open_rolls_back_host_migration_when_gate_model_is_invalid() {
18230        let directory = tempfile::tempdir().expect("temp dir");
18231        let path = directory.path().join("invalid-gate-migration.sqlite");
18232        let migration = Migration::sql(
18233            1,
18234            "composite gate relation",
18235            "CREATE TABLE parents (
18236                id TEXT PRIMARY KEY,
18237                code TEXT NOT NULL,
18238                shared INTEGER NOT NULL,
18239                _updated_at TEXT NOT NULL,
18240                UNIQUE (id, code)
18241             ) STRICT;
18242             CREATE TABLE children (
18243                id TEXT PRIMARY KEY,
18244                parent_id TEXT NOT NULL,
18245                parent_code TEXT NOT NULL,
18246                _updated_at TEXT NOT NULL,
18247                FOREIGN KEY (parent_id, parent_code) REFERENCES parents(id, code)
18248             ) STRICT;",
18249        );
18250        let tables = vec![
18251            SyncedTable::new("parents", crate::sync::session::RowIdentity::SharedKey)
18252                .gated_by("shared"),
18253            SyncedTable::new("children", crate::sync::session::RowIdentity::SharedKey),
18254        ];
18255
18256        let error = match Database::open(
18257            &path,
18258            tables,
18259            BLOB_TOMBSTONE_GRACE,
18260            crate::blob::TransferLimits::serial(),
18261            crate::WritePolicy::MergeConcurrent,
18262            "invalid-gate-open".to_string(),
18263            &[migration],
18264        ) {
18265            Ok(_) => panic!("an invalid gate model must reject the open"),
18266            Err(error) => error,
18267        };
18268        assert!(
18269            error.to_string().contains("composite foreign key"),
18270            "{error}"
18271        );
18272
18273        let conn = Connection::open(&path).expect("inspect rejected database");
18274        let user_version: i64 = conn
18275            .query_row("PRAGMA user_version", [], |row| row.get(0))
18276            .expect("user version");
18277        assert_eq!(user_version, 0);
18278        let host_tables: i64 = conn
18279            .query_row(
18280                "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name IN ('parents', 'children')",
18281                [],
18282                |row| row.get(0),
18283            )
18284            .expect("host table count");
18285        assert_eq!(host_tables, 0);
18286    }
18287
18288    #[test]
18289    fn sqlite_session_representation_preserves_upsert_but_loses_primary_key_update_intent() {
18290        let conn = Connection::open_in_memory().expect("open");
18291        conn.execute_batch(
18292            "CREATE TABLE things (
18293                id TEXT PRIMARY KEY,
18294                body TEXT NOT NULL,
18295                _updated_at TEXT NOT NULL
18296             ) STRICT;
18297             INSERT INTO things VALUES ('old', 'base', '0000000001000-0000-writer');",
18298        )
18299        .expect("schema and seed");
18300        let tables = vec![things_table(crate::sync::session::RowIdentity::SharedKey)];
18301
18302        let mut primary_key_session = attach_session(&conn, &tables).expect("attach session");
18303        let primary_key_tx = conn.unchecked_transaction().expect("transaction");
18304        primary_key_tx
18305            .execute(
18306                "UPDATE things SET id = 'new', _updated_at = '0000000002000-0000-writer' WHERE id = 'old'",
18307                [],
18308            )
18309            .expect("update primary key");
18310        let primary_key_changes =
18311            capture_changeset(&mut primary_key_session).expect("capture primary-key update");
18312        drop(primary_key_tx);
18313        drop(primary_key_session);
18314        let primary_key_changes =
18315            crate::changeset::walk(&primary_key_changes).expect("walk primary-key update");
18316        assert_eq!(
18317            primary_key_changes
18318                .iter()
18319                .map(|change| (change.op, change.pk()))
18320                .collect::<Vec<_>>(),
18321            vec![
18322                (crate::changeset::ChangeOp::Insert, Some("new")),
18323                (crate::changeset::ChangeOp::Delete, Some("old")),
18324            ]
18325        );
18326
18327        let mut upsert_session = attach_session(&conn, &tables).expect("attach session");
18328        let upsert_tx = conn.unchecked_transaction().expect("transaction");
18329        upsert_tx
18330            .execute(
18331                "INSERT INTO things VALUES ('old', 'upserted', '0000000003000-0000-writer')
18332                 ON CONFLICT(id) DO UPDATE SET
18333                    body = excluded.body,
18334                    _updated_at = excluded._updated_at",
18335                [],
18336            )
18337            .expect("same-id upsert");
18338        let upsert_changes = capture_changeset(&mut upsert_session).expect("capture upsert");
18339        drop(upsert_tx);
18340        let upsert_changes = crate::changeset::walk(&upsert_changes).expect("walk upsert");
18341        assert_eq!(upsert_changes.len(), 1);
18342        assert_eq!(upsert_changes[0].op, crate::changeset::ChangeOp::Update);
18343        assert_eq!(upsert_changes[0].pk(), Some("old"));
18344    }
18345
18346    #[test]
18347    fn writer_and_read_only_open_reject_existing_invalid_independent_uuid() {
18348        let writer_error = match Database::open(
18349            Path::new(":memory:"),
18350            vec![things_table(
18351                crate::sync::session::RowIdentity::IndependentUuid,
18352            )],
18353            BLOB_TOMBSTONE_GRACE,
18354            crate::blob::TransferLimits::serial(),
18355            crate::WritePolicy::MergeConcurrent,
18356            "invalid-uuid-writer".to_string(),
18357            &[Migration::sql(
18358                1,
18359                "things",
18360                "CREATE TABLE things (
18361                    id TEXT PRIMARY KEY,
18362                    body TEXT NOT NULL,
18363                    _updated_at TEXT NOT NULL
18364                 ) STRICT;
18365                 INSERT INTO things VALUES ('2', 'invalid', '0000000001000-0000-seed');",
18366            )],
18367        ) {
18368            Ok(_) => panic!("writer open must reject an existing non-UUID id"),
18369            Err(error) => error.to_string(),
18370        };
18371        assert!(
18372            writer_error.contains("things") && writer_error.contains("\"2\""),
18373            "writer error identifies the table and value: {writer_error}",
18374        );
18375
18376        let dir = tempfile::tempdir().expect("temp dir");
18377        let path = dir.path().join("read-only-invalid.sqlite");
18378        let (writer, _) = Database::open(
18379            &path,
18380            vec![things_table(crate::sync::session::RowIdentity::SharedKey)],
18381            BLOB_TOMBSTONE_GRACE,
18382            crate::blob::TransferLimits::serial(),
18383            crate::WritePolicy::MergeConcurrent,
18384            "invalid-uuid-seed".to_string(),
18385            &[Migration::sql(
18386                1,
18387                "things",
18388                "CREATE TABLE things (
18389                    id TEXT PRIMARY KEY,
18390                    body TEXT NOT NULL,
18391                    _updated_at TEXT NOT NULL
18392                 ) STRICT;
18393                 INSERT INTO things VALUES ('2', 'invalid', '0000000001000-0000-seed');",
18394            )],
18395        )
18396        .expect("seed database under its declared SharedKey contract");
18397        drop(writer);
18398
18399        let reader_error = match Database::open_read_only(
18400            &path,
18401            vec![things_table(
18402                crate::sync::session::RowIdentity::IndependentUuid,
18403            )],
18404            BLOB_TOMBSTONE_GRACE,
18405            crate::blob::TransferLimits::serial(),
18406            crate::WritePolicy::MergeConcurrent,
18407            "invalid-uuid-reader".to_string(),
18408            &[things_migration()],
18409        ) {
18410            Ok(_) => panic!("read-only open must reject an existing non-UUID id"),
18411            Err(error) => error.to_string(),
18412        };
18413        assert!(
18414            reader_error.contains("things") && reader_error.contains("\"2\""),
18415            "reader error identifies the table and value: {reader_error}",
18416        );
18417    }
18418
18419    #[test]
18420    fn database_open_rejects_duplicate_synced_table_declarations() {
18421        let error = match Database::open(
18422            Path::new(":memory:"),
18423            vec![
18424                things_table(crate::sync::session::RowIdentity::SharedKey),
18425                things_table(crate::sync::session::RowIdentity::IndependentUuid),
18426            ],
18427            BLOB_TOMBSTONE_GRACE,
18428            crate::blob::TransferLimits::serial(),
18429            crate::WritePolicy::MergeConcurrent,
18430            "duplicate-things".to_string(),
18431            &[things_migration()],
18432        ) {
18433            Ok(_) => panic!("one table cannot have two identity declarations"),
18434            Err(error) => error.to_string(),
18435        };
18436        assert!(
18437            error.contains("things") && error.contains("declared"),
18438            "{error}"
18439        );
18440    }
18441
18442    #[tokio::test]
18443    async fn invalid_host_identity_rolls_back_rows_and_preserves_existing_write() {
18444        let tables = vec![things_table(
18445            crate::sync::session::RowIdentity::IndependentUuid,
18446        )];
18447        let (db, _) = Database::open(
18448            Path::new(":memory:"),
18449            tables.clone(),
18450            BLOB_TOMBSTONE_GRACE,
18451            crate::blob::TransferLimits::serial(),
18452            crate::WritePolicy::MergeConcurrent,
18453            "invalid-host-identity".to_string(),
18454            &[things_migration()],
18455        )
18456        .expect("open");
18457        let existing_changeset = vec![0x45, 0x58, 0x41, 0x43, 0x54];
18458        let existing_for_insert = existing_changeset.clone();
18459        db.call(move |conn| {
18460            conn.execute(
18461                "INSERT INTO store_writes
18462                 (write_id, status, affected_rows, changeset, inverse_changeset, base, blob_facts)
18463                 VALUES (
18464                    'existing-write', '\"pending\"', '[]', ?1, ?1,
18465                    '{\"merge_concurrent\":{\"dependencies\":{}}}',
18466                    '{\"blobs\":[]}'
18467                 )",
18468                [existing_for_insert],
18469            )
18470            .map(|_| ())
18471            .map_err(DbError::from)
18472        })
18473        .await
18474        .expect("seed existing write records");
18475
18476        let write_id = db.new_write_id();
18477        let result = db
18478            .call(move |conn| {
18479                Database::run_internal_store_write_transaction_on(
18480                    conn,
18481                    &tables,
18482                    crate::WritePolicy::MergeConcurrent,
18483                    None,
18484                    write_id,
18485                    |tx| {
18486                        tx.execute(
18487                            "INSERT INTO things VALUES (?1, 'valid', '0000000002000-0000-writer')",
18488                            ["f47ac10b-58cc-4372-a567-0e02b2c3d479"],
18489                        )?;
18490                        tx.execute(
18491                        "INSERT INTO things VALUES ('2', 'invalid', '0000000002001-0000-writer')",
18492                        [],
18493                    )?;
18494                        Ok::<_, DbError>(())
18495                    },
18496                )
18497            })
18498            .await;
18499        let error = result.expect_err("invalid UUID must reject the host transaction");
18500        assert!(error.to_string().contains("things") && error.to_string().contains("2"));
18501
18502        db.call(move |conn| {
18503            let row_count: i64 = conn
18504                .query_row("SELECT COUNT(*) FROM things", [], |row| row.get(0))
18505                .map_err(DbError::from)?;
18506            let pending = conn
18507                .prepare("SELECT changeset FROM store_writes ORDER BY ordinal")
18508                .and_then(|mut statement| {
18509                    statement
18510                        .query_map([], |row| row.get::<_, Vec<u8>>(0))?
18511                        .collect::<rusqlite::Result<Vec<_>>>()
18512                })
18513                .map_err(DbError::from)?;
18514            assert_eq!(row_count, 0);
18515            assert_eq!(pending, vec![existing_changeset]);
18516            Ok(())
18517        })
18518        .await
18519        .expect("inspect rollback");
18520    }
18521
18522    #[tokio::test]
18523    async fn valid_identity_changes_updates_and_upserts_succeed_but_invalid_new_uuid_rolls_back() {
18524        let tables = vec![things_table(
18525            crate::sync::session::RowIdentity::IndependentUuid,
18526        )];
18527        let (db, _) = Database::open(
18528            Path::new(":memory:"),
18529            tables.clone(),
18530            BLOB_TOMBSTONE_GRACE,
18531            crate::blob::TransferLimits::serial(),
18532            crate::WritePolicy::MergeConcurrent,
18533            "host-identity-changes".to_string(),
18534            &[things_migration()],
18535        )
18536        .expect("open");
18537        let original = "f47ac10b-58cc-4372-a567-0e02b2c3d479";
18538        db.call(move |conn| {
18539            conn.execute(
18540                "INSERT INTO things VALUES (?1, 'base', '0000000001000-0000-writer')",
18541                [original],
18542            )
18543            .map(|_| ())
18544            .map_err(DbError::from)
18545        })
18546        .await
18547        .expect("seed row");
18548
18549        let update_tables = tables.clone();
18550        let renamed = "01890a5d-ac96-774b-bcce-b302099c3f74";
18551        let update_write_id = db.new_write_id();
18552        db.call(move |conn| {
18553            Database::run_internal_store_write_transaction_on(
18554                conn,
18555                &update_tables,
18556                crate::WritePolicy::MergeConcurrent,
18557                None,
18558                update_write_id,
18559                |tx| {
18560                tx.execute(
18561                    "UPDATE things SET id = ?1, _updated_at = '0000000002000-0000-writer' WHERE id = ?2",
18562                    [renamed, original],
18563                )?;
18564                Ok::<_, DbError>(())
18565                },
18566            )
18567        })
18568        .await
18569        .expect("valid primary-key change succeeds");
18570
18571        let replace_tables = tables.clone();
18572        let replaced = "8b1a9953-c461-4e20-8c66-826115d53552";
18573        let replace_write_id = db.new_write_id();
18574        db.call(move |conn| {
18575            Database::run_internal_store_write_transaction_on(
18576                conn,
18577                &replace_tables,
18578                crate::WritePolicy::MergeConcurrent,
18579                None,
18580                replace_write_id,
18581                |tx| {
18582                    tx.execute("DELETE FROM things WHERE id = ?1", [renamed])?;
18583                    tx.execute(
18584                        "INSERT INTO things VALUES (?1, 'replaced', '0000000003000-0000-writer')",
18585                        [replaced],
18586                    )?;
18587                    Ok::<_, DbError>(())
18588                },
18589            )
18590        })
18591        .await
18592        .expect("explicit delete and insert succeeds");
18593
18594        let ordinary_tables = tables.clone();
18595        let ordinary_write_id = db.new_write_id();
18596        db.call(move |conn| {
18597            Database::run_internal_store_write_transaction_on(
18598                conn,
18599                &ordinary_tables,
18600                crate::WritePolicy::MergeConcurrent,
18601                None,
18602                ordinary_write_id,
18603                |tx| {
18604                tx.execute(
18605                    "UPDATE things SET body = 'ordinary', _updated_at = '0000000004000-0000-writer' WHERE id = ?1",
18606                    [replaced],
18607                )?;
18608                tx.execute(
18609                    "INSERT INTO things VALUES (?1, 'upserted', '0000000005000-0000-writer')
18610                     ON CONFLICT(id) DO UPDATE SET body = excluded.body, _updated_at = excluded._updated_at",
18611                    [replaced],
18612                )?;
18613                Ok::<_, DbError>(())
18614                },
18615            )
18616        })
18617        .await
18618        .expect("ordinary update and same-id upsert succeed");
18619
18620        let pending_before = db
18621            .call(|conn| {
18622                conn.prepare("SELECT changeset FROM store_writes ORDER BY ordinal")
18623                    .and_then(|mut statement| {
18624                        statement
18625                            .query_map([], |row| row.get::<_, Vec<u8>>(0))?
18626                            .collect::<rusqlite::Result<Vec<_>>>()
18627                    })
18628                    .map_err(DbError::from)
18629            })
18630            .await
18631            .expect("read existing write records");
18632        assert_eq!(pending_before.len(), 3);
18633
18634        let invalid_tables = tables;
18635        let invalid_write_id = db.new_write_id();
18636        let invalid = db
18637            .call(move |conn| {
18638                Database::run_internal_store_write_transaction_on(
18639                    conn,
18640                    &invalid_tables,
18641                    crate::WritePolicy::MergeConcurrent,
18642                    None,
18643                    invalid_write_id,
18644                    |tx| {
18645                        tx.execute(
18646                            "UPDATE things SET id = 'not-a-uuid', _updated_at = '0000000006000-0000-writer' WHERE id = ?1",
18647                            [replaced],
18648                        )?;
18649                        Ok::<_, DbError>(())
18650                    },
18651                )
18652            })
18653            .await;
18654        let error = invalid.expect_err("invalid new UUID rejects the primary-key change");
18655        assert!(error.to_string().contains("not-a-uuid"));
18656
18657        db.call(move |conn| {
18658            let row = conn
18659                .query_row("SELECT id, body FROM things", [], |row| {
18660                    Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
18661                })
18662                .map_err(DbError::from)?;
18663            let pending_after = conn
18664                .prepare("SELECT changeset FROM store_writes ORDER BY ordinal")
18665                .and_then(|mut statement| {
18666                    statement
18667                        .query_map([], |row| row.get::<_, Vec<u8>>(0))?
18668                        .collect::<rusqlite::Result<Vec<_>>>()
18669                })
18670                .map_err(DbError::from)?;
18671            assert_eq!(row, (replaced.to_string(), "upserted".to_string()));
18672            assert_eq!(pending_after, pending_before);
18673            Ok(())
18674        })
18675        .await
18676        .expect("invalid identity change rolls back row and write records");
18677    }
18678
18679    /// A SQL closure that blocks for a while must not stall other tasks on the
18680    /// same runtime, because jobs run on the dedicated connection thread rather
18681    /// than the async executor. On a current-thread runtime the scheduler has to
18682    /// poll the spawned DB call before it can resume us; if that call ran its
18683    /// blocking closure inline on the executor thread, this single `yield_now`
18684    /// would not return until the closure finished. With the closure on its own
18685    /// thread we resume immediately, long before it completes.
18686    #[tokio::test]
18687    async fn slow_db_call_does_not_block_the_executor() {
18688        use std::time::{Duration, Instant};
18689
18690        let (db, _stamper) = Database::open(
18691            Path::new(":memory:"),
18692            Vec::new(),
18693            BLOB_TOMBSTONE_GRACE,
18694            crate::blob::TransferLimits::serial(),
18695            crate::WritePolicy::MergeConcurrent,
18696            "liveness".to_string(),
18697            &[],
18698        )
18699        .expect("open database");
18700
18701        let slow_db = db.clone();
18702        let slow = tokio::spawn(async move {
18703            slow_db
18704                .call(|conn| {
18705                    std::thread::sleep(Duration::from_millis(500));
18706                    conn.query_row("SELECT 1", [], |r| r.get::<_, i64>(0))
18707                        .map_err(DbError::from)
18708                })
18709                .await
18710        });
18711
18712        let start = Instant::now();
18713        tokio::task::yield_now().await;
18714        let stalled = start.elapsed();
18715
18716        assert!(
18717            stalled < Duration::from_millis(250),
18718            "unrelated task stalled {stalled:?} behind the slow DB call — the executor was blocked",
18719        );
18720
18721        let value = slow
18722            .await
18723            .expect("slow DB task joins")
18724            .expect("slow DB call succeeds");
18725        assert_eq!(value, 1, "the slow DB call still returns its result");
18726    }
18727
18728    #[tokio::test]
18729    async fn prepared_audience_objects_reload_the_same_verified_bytes_and_spool() {
18730        let (db, _stamper) = Database::open(
18731            Path::new(":memory:"),
18732            Vec::new(),
18733            BLOB_TOMBSTONE_GRACE,
18734            crate::blob::TransferLimits::serial(),
18735            crate::WritePolicy::MergeConcurrent,
18736            "prepared-audience-objects".to_string(),
18737            &[],
18738        )
18739        .expect("open database");
18740        let write_id = WriteId::from_generated("write-1".to_string());
18741        let stored_write_id = write_id.clone();
18742        db.call(move |conn| {
18743            let base = serde_json::to_string(&StoreWriteBase::MergeConcurrent {
18744                dependencies: BTreeMap::new(),
18745            })
18746            .expect("serialize base");
18747            conn.execute(
18748                "INSERT INTO store_writes
18749                 (write_id, status, affected_rows, changeset, inverse_changeset, base, blob_facts)
18750                 VALUES (?1, '\"pending\"', '[]', X'', X'', ?2, '{\"blobs\":[]}')",
18751                rusqlite::params![stored_write_id.as_str(), base],
18752            )
18753            .map(|_| ())
18754            .map_err(DbError::from)
18755        })
18756        .await
18757        .expect("seed write");
18758
18759        let binding = exact_blob_binding("photo", "0000000001000-0000-a", b"photo bytes");
18760        let second_object = ExactObjectRef::new(
18761            crate::storage::cloud::ObjectSlot::opaque(
18762                binding.blob().locator().semantic_key(),
18763                "database-test-second-physical-object".to_string(),
18764            )
18765            .expect("second blob slot"),
18766            binding.blob().object().stored_size(),
18767            binding.blob().object().stored_hash(),
18768        );
18769        let second_blob = StoredBlobRef::new(binding.blob().locator().clone(), second_object)
18770            .expect("second exact object for the same locator");
18771        let second_binding = RowBlobLocatorBinding::new(
18772            "photos",
18773            "photo-copy",
18774            "0000000001000-0000-a",
18775            "id",
18776            second_blob.clone(),
18777        )
18778        .expect("second row binding");
18779        let package = AudiencePackage::store(
18780            ObjectHash::digest(b"root"),
18781            test_candidate_family(),
18782            write_id.clone(),
18783            test_commit_coord(),
18784            1,
18785            b"changeset".to_vec(),
18786            vec![binding.clone(), second_binding],
18787        )
18788        .expect("build package");
18789        let semantic = package.to_bytes();
18790        let stored_package = b"stored package representation".to_vec();
18791        let StoreCommitCoord::MergeConcurrent { stream_id, .. } = test_commit_coord() else {
18792            unreachable!("database test commit is MergeConcurrent")
18793        };
18794        let package_slot = crate::storage::cloud::ObjectSlot::logical(format!(
18795            "{}.pkg",
18796            crate::sync::store_commit::package_semantic_prefix(
18797                test_candidate_family(),
18798                &stream_id.to_string(),
18799                1,
18800                ObjectHash::digest(&semantic),
18801            )
18802        ))
18803        .expect("package slot");
18804        let package_object = ExactObjectRef::new(
18805            package_slot,
18806            stored_package.len() as u64,
18807            ObjectHash::digest(&stored_package),
18808        );
18809        let owner_commit_hash = ObjectHash::digest(b"owner commit semantic bytes");
18810        let owner_object = ExactObjectRef::new(
18811            crate::storage::cloud::ObjectSlot::logical(format!(
18812                "{}.json",
18813                commit_semantic_prefix(
18814                    test_candidate_family(),
18815                    &stream_id.to_string(),
18816                    1,
18817                    owner_commit_hash,
18818                )
18819            ))
18820            .expect("owner commit slot"),
18821            1,
18822            ObjectHash::digest(b"owner commit"),
18823        );
18824        let owner = StoreBatchCommitRef {
18825            coord: test_commit_coord(),
18826            commit_hash: owner_commit_hash,
18827            object: owner_object,
18828        };
18829        let package_remote = crate::sync::remote_object::RemoteObjectRecord::CandidateExclusive(
18830            crate::sync::remote_object::CandidateObjectRecord {
18831                identity: crate::sync::remote_object::CandidateExclusiveTarget {
18832                    family: package.candidate_family(),
18833                    domain:
18834                        crate::sync::remote_object::CandidateExclusiveObjectDomain::StorePackage,
18835                    semantic_hash: ObjectHash::digest(&semantic),
18836                    object: package_object.clone(),
18837                },
18838                bytes: crate::sync::remote_object::RemoteObjectBytes::inline(
18839                    semantic.clone(),
18840                    stored_package.clone(),
18841                    package_object.clone(),
18842                )
18843                .expect("package remote bytes"),
18844                state: crate::sync::remote_object::CandidateObjectState::Prepared {
18845                    ownership: crate::sync::remote_object::PendingCandidateOwnership {
18846                        pending: std::collections::BTreeSet::from([owner.clone()]),
18847                        nonactivated: Vec::new(),
18848                    },
18849                },
18850            },
18851        );
18852        let package_remote_id = package_remote.object_id();
18853        let prepared_package = PreparedAudiencePackage::new(
18854            package_remote_id,
18855            semantic,
18856            stored_package.clone(),
18857            package_object.clone(),
18858        )
18859        .expect("prepare package");
18860
18861        let directory = tempfile::tempdir().expect("temp dir");
18862        let spool = directory.path().join("blob.spool");
18863        crate::local_blob::write_atomic_durable(&spool, b"stored representation")
18864            .await
18865            .expect("write spool");
18866        let blob_remote = crate::sync::remote_object::RemoteObjectRecord::SharedLiveSet(
18867            crate::sync::remote_object::SharedObjectRecord {
18868                identity: crate::sync::remote_object::SharedLiveSetObjectRef {
18869                    domain: crate::sync::remote_object::SharedLiveSetObjectDomain::StoredBlob,
18870                    semantic_hash: ObjectHash::digest(&binding.blob().locator().to_bytes()),
18871                    object: binding.blob().object().clone(),
18872                },
18873                bytes: crate::sync::remote_object::RemoteObjectBytes::blob(
18874                    binding.blob().locator().to_bytes(),
18875                    binding.blob().object().clone(),
18876                )
18877                .expect("blob remote bytes"),
18878                state: crate::sync::remote_object::OwnedObjectState::Prepared {
18879                    ownership: crate::sync::remote_object::PendingCandidateOwnership {
18880                        pending: std::collections::BTreeSet::from([owner.clone()]),
18881                        nonactivated: Vec::new(),
18882                    },
18883                },
18884            },
18885        );
18886        let blob_remote_id = blob_remote.object_id();
18887        let prepared_blob = PreparedAudienceBlob {
18888            remote_object_id: blob_remote_id,
18889            audience: RemoteAudience::Store,
18890            blob: binding.blob().clone(),
18891            spool_path: Some(spool.clone()),
18892        };
18893        let second_blob_remote = crate::sync::remote_object::RemoteObjectRecord::SharedLiveSet(
18894            crate::sync::remote_object::SharedObjectRecord {
18895                identity: crate::sync::remote_object::SharedLiveSetObjectRef {
18896                    domain: crate::sync::remote_object::SharedLiveSetObjectDomain::StoredBlob,
18897                    semantic_hash: ObjectHash::digest(&second_blob.locator().to_bytes()),
18898                    object: second_blob.object().clone(),
18899                },
18900                bytes: crate::sync::remote_object::RemoteObjectBytes::blob(
18901                    second_blob.locator().to_bytes(),
18902                    second_blob.object().clone(),
18903                )
18904                .expect("second blob remote bytes"),
18905                state: crate::sync::remote_object::OwnedObjectState::Prepared {
18906                    ownership: crate::sync::remote_object::PendingCandidateOwnership {
18907                        pending: std::collections::BTreeSet::from([owner]),
18908                        nonactivated: Vec::new(),
18909                    },
18910                },
18911            },
18912        );
18913        let second_blob_remote_id = second_blob_remote.object_id();
18914        let second_prepared_blob = PreparedAudienceBlob {
18915            remote_object_id: second_blob_remote_id,
18916            audience: RemoteAudience::Store,
18917            blob: second_blob,
18918            spool_path: Some(spool.clone()),
18919        };
18920        let persisted_write_id = write_id.clone();
18921        let package_state = serde_json::to_string(&package_remote).expect("package remote state");
18922        let blob_state = serde_json::to_string(&blob_remote).expect("blob remote state");
18923        let second_blob_state =
18924            serde_json::to_string(&second_blob_remote).expect("second blob remote state");
18925        db.call(move |conn| {
18926            let tx = conn.unchecked_transaction().map_err(DbError::from)?;
18927            tx.execute(
18928                "INSERT INTO remote_objects (object_id, state) VALUES (?1, ?2)",
18929                rusqlite::params![package_remote_id.to_string(), package_state],
18930            )
18931            .map_err(DbError::from)?;
18932            tx.execute(
18933                "INSERT INTO remote_objects (object_id, state) VALUES (?1, ?2)",
18934                rusqlite::params![blob_remote_id.to_string(), blob_state],
18935            )
18936            .map_err(DbError::from)?;
18937            tx.execute(
18938                "INSERT INTO remote_objects (object_id, state) VALUES (?1, ?2)",
18939                rusqlite::params![second_blob_remote_id.to_string(), second_blob_state],
18940            )
18941            .map_err(DbError::from)?;
18942            Database::persist_prepared_audience_objects_on(
18943                &tx,
18944                &persisted_write_id,
18945                &[prepared_package],
18946                &[prepared_blob, second_prepared_blob],
18947            )?;
18948            tx.commit().map_err(DbError::from)
18949        })
18950        .await
18951        .expect("persist prepared objects");
18952
18953        let reloaded = db
18954            .prepared_audience_objects(&write_id)
18955            .await
18956            .expect("reload prepared objects");
18957        assert_eq!(reloaded.packages.len(), 1);
18958        assert_eq!(reloaded.packages[0].package(), &package);
18959        assert_eq!(reloaded.packages[0].remote_object_id(), package_remote_id);
18960        assert_eq!(reloaded.blobs.len(), 2);
18961        let reloaded_ids = reloaded
18962            .blobs
18963            .iter()
18964            .map(PreparedAudienceBlob::remote_object_id)
18965            .collect::<std::collections::BTreeSet<_>>();
18966        assert_eq!(
18967            reloaded_ids,
18968            std::collections::BTreeSet::from([blob_remote_id, second_blob_remote_id])
18969        );
18970        assert!(reloaded
18971            .blobs
18972            .iter()
18973            .all(|blob| blob.spool_path() == Some(spool.as_path())));
18974        assert_eq!(
18975            reloaded
18976                .blobs
18977                .iter()
18978                .map(|blob| blob.blob().locator().locator_hash())
18979                .collect::<std::collections::BTreeSet<_>>()
18980                .len(),
18981            1,
18982            "same-locator exact objects survive persistence independently",
18983        );
18984    }
18985
18986    /// Dropping the last handle from inside a runtime task must not block that
18987    /// task on the connection thread's queue, and a job already dispatched must
18988    /// still run to completion. The drop detaches the thread in async context, so
18989    /// it returns at once; the detached thread finishes the queued job (its effect
18990    /// is durable) and exits on its own.
18991    #[tokio::test]
18992    async fn dropping_last_handle_in_async_context_does_not_stall_but_job_still_lands() {
18993        use std::time::{Duration, Instant};
18994
18995        let dir = tempfile::tempdir().expect("temp dir");
18996        let db_path = dir.path().join("db.sqlite");
18997        let marker = dir.path().join("marker");
18998
18999        let (db, _stamper) = Database::open(
19000            &db_path,
19001            Vec::new(),
19002            BLOB_TOMBSTONE_GRACE,
19003            crate::blob::TransferLimits::serial(),
19004            crate::WritePolicy::MergeConcurrent,
19005            "drop-async".to_string(),
19006            &[],
19007        )
19008        .expect("open");
19009
19010        // Dispatch a slow job to the connection thread, then release the clone
19011        // tied to it so the job is in flight with no handle awaiting it: spawn the
19012        // call, let it dispatch, then abort the task so its `Database` clone drops
19013        // while the job still runs. The job writes a marker file when it finishes.
19014        let job_db = db.clone();
19015        let job_marker = marker.clone();
19016        let task = tokio::spawn(async move {
19017            let _ = job_db
19018                .call(move |_conn| {
19019                    std::thread::sleep(Duration::from_millis(300));
19020                    std::fs::write(&job_marker, b"landed")
19021                        .map_err(|e| DbError::Message(e.to_string()))
19022                })
19023                .await;
19024        });
19025        tokio::task::yield_now().await;
19026        task.abort();
19027        let _ = task.await;
19028
19029        // `db` is now the last clone; dropping it inside this runtime task must
19030        // detach (not join) so it returns without waiting out the queued job.
19031        let drop_start = Instant::now();
19032        drop(db);
19033        let drop_elapsed = drop_start.elapsed();
19034        assert!(
19035            drop_elapsed < Duration::from_millis(200),
19036            "dropping the last handle stalled {drop_elapsed:?} — it joined the connection thread \
19037             instead of detaching",
19038        );
19039
19040        // The detached thread still runs the already-dispatched job to completion.
19041        let deadline = Instant::now() + Duration::from_secs(5);
19042        while !marker.exists() {
19043            assert!(
19044                Instant::now() < deadline,
19045                "the dispatched job's effect never landed after the last handle dropped",
19046            );
19047            tokio::time::sleep(Duration::from_millis(10)).await;
19048        }
19049    }
19050
19051    #[tokio::test]
19052    async fn database_open_rejects_empty_device_id() {
19053        let result = Database::open(
19054            Path::new(":memory:"),
19055            Vec::new(),
19056            BLOB_TOMBSTONE_GRACE,
19057            crate::blob::TransferLimits::serial(),
19058            crate::WritePolicy::MergeConcurrent,
19059            String::new(),
19060            &[],
19061        );
19062        let error = match result {
19063            Ok(_) => panic!("empty device_id must be rejected"),
19064            Err(error) => error.to_string(),
19065        };
19066
19067        assert!(
19068            error.contains("device_id") && error.contains("empty"),
19069            "error names the empty device id: {error}",
19070        );
19071    }
19072
19073    #[test]
19074    fn host_sql_authorizer_is_removed_after_success_error_and_panic() {
19075        let conn = Connection::open_in_memory().expect("open");
19076        conn.execute_batch(
19077            "ATTACH ':memory:' AS coven_gate_empty; \
19078             CREATE TABLE coven_gate_empty.baseline (id TEXT PRIMARY KEY) STRICT; \
19079             INSERT INTO coven_gate_empty.baseline VALUES ('guarded');",
19080        )
19081        .expect("attach guarded schema");
19082        let assert_guard_removed = || {
19083            let id: String = conn
19084                .query_row("SELECT id FROM coven_gate_empty.baseline", [], |row| {
19085                    row.get(0)
19086                })
19087                .expect("internal SQL can address the baseline after host SQL");
19088            assert_eq!(id, "guarded");
19089        };
19090
19091        Database::run_host_sql_on(&conn, || Ok::<_, DbError>(())).expect("successful host SQL");
19092        assert_guard_removed();
19093
19094        let error =
19095            Database::run_host_sql_on(&conn, || Err::<(), _>(DbError::Message("host".into())));
19096        assert!(error.is_err());
19097        assert_guard_removed();
19098
19099        let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
19100            Database::run_host_sql_on(&conn, || -> Result<(), DbError> { panic!("host panic") })
19101                .expect("panicking host SQL closure never returns");
19102        }));
19103        assert!(panic.is_err());
19104        assert_guard_removed();
19105    }
19106
19107    #[tokio::test]
19108    async fn database_open_rejects_host_declared_reserved_tables() {
19109        for table_name in ["cloud_outbox", "protocol_state"] {
19110            let result = Database::open(
19111                Path::new(":memory:"),
19112                vec![SyncedTable::new(
19113                    table_name,
19114                    crate::sync::session::RowIdentity::SharedKey,
19115                )],
19116                BLOB_TOMBSTONE_GRACE,
19117                crate::blob::TransferLimits::serial(),
19118                crate::WritePolicy::MergeConcurrent,
19119                format!("reserved-{table_name}"),
19120                &[notes_migration()],
19121            );
19122            let error = match result {
19123                Ok(_) => panic!("reserved table {table_name} must be rejected"),
19124                Err(error) => error.to_string(),
19125            };
19126
19127            assert!(
19128                error.contains(table_name),
19129                "error names reserved table {table_name}: {error}",
19130            );
19131        }
19132    }
19133
19134    #[tokio::test]
19135    async fn database_open_rejects_empty_synced_table_name() {
19136        let result = Database::open(
19137            Path::new(":memory:"),
19138            vec![SyncedTable::new(
19139                "",
19140                crate::sync::session::RowIdentity::SharedKey,
19141            )],
19142            BLOB_TOMBSTONE_GRACE,
19143            crate::blob::TransferLimits::serial(),
19144            crate::WritePolicy::MergeConcurrent,
19145            "empty-synced-table".to_string(),
19146            &[notes_migration()],
19147        );
19148        let error = match result {
19149            Ok(_) => panic!("empty synced table name must be rejected"),
19150            Err(error) => error.to_string(),
19151        };
19152
19153        assert!(
19154            error.contains("empty"),
19155            "error names empty synced table: {error}",
19156        );
19157    }
19158
19159    #[tokio::test]
19160    async fn database_open_accepts_normal_host_synced_table() {
19161        Database::open(
19162            Path::new(":memory:"),
19163            vec![SyncedTable::new(
19164                "notes",
19165                crate::sync::session::RowIdentity::SharedKey,
19166            )],
19167            BLOB_TOMBSTONE_GRACE,
19168            crate::blob::TransferLimits::serial(),
19169            crate::WritePolicy::MergeConcurrent,
19170            "normal-synced-table".to_string(),
19171            &[notes_migration()],
19172        )
19173        .expect("normal host table opens");
19174    }
19175
19176    /// Open with a single host migration and the given synced-table set, expecting
19177    /// the contract validation to refuse the open. Returns the error text so a test
19178    /// asserts it names the offending table and the violated requirement.
19179    fn open_contract_error(
19180        migration_sql: &'static str,
19181        tables: Vec<SyncedTable>,
19182        device_id: &str,
19183    ) -> String {
19184        let result = Database::open(
19185            Path::new(":memory:"),
19186            tables,
19187            BLOB_TOMBSTONE_GRACE,
19188            crate::blob::TransferLimits::serial(),
19189            crate::WritePolicy::MergeConcurrent,
19190            device_id.to_string(),
19191            &[Migration::sql(1, "contract", migration_sql)],
19192        );
19193        match result {
19194            Ok(_) => panic!("open must reject the synced-table contract violation"),
19195            Err(error) => error.to_string(),
19196        }
19197    }
19198
19199    #[tokio::test]
19200    async fn database_open_rejects_integer_primary_key() {
19201        let error = open_contract_error(
19202            "CREATE TABLE things (id INTEGER PRIMARY KEY, _updated_at TEXT NOT NULL) STRICT;",
19203            vec![SyncedTable::new(
19204                "things",
19205                crate::sync::session::RowIdentity::SharedKey,
19206            )],
19207            "integer-pk",
19208        );
19209        assert!(
19210            error.contains("things") && error.contains("TEXT"),
19211            "error names the table and the TEXT requirement: {error}",
19212        );
19213    }
19214
19215    #[tokio::test]
19216    async fn database_open_rejects_primary_key_not_at_column_zero() {
19217        let error = open_contract_error(
19218            "CREATE TABLE things (body TEXT NOT NULL, id TEXT PRIMARY KEY, \
19219             _updated_at TEXT NOT NULL) STRICT;",
19220            vec![SyncedTable::new(
19221                "things",
19222                crate::sync::session::RowIdentity::SharedKey,
19223            )],
19224            "pk-not-first",
19225        );
19226        assert!(
19227            error.contains("things") && error.contains("column 0"),
19228            "error names the table and the column-0 requirement: {error}",
19229        );
19230    }
19231
19232    #[tokio::test]
19233    async fn database_open_rejects_primary_key_named_other_than_id() {
19234        let error = open_contract_error(
19235            "CREATE TABLE things (thing_id TEXT PRIMARY KEY, _updated_at TEXT NOT NULL) STRICT;",
19236            vec![SyncedTable::new(
19237                "things",
19238                crate::sync::session::RowIdentity::SharedKey,
19239            )],
19240            "pk-misnamed",
19241        );
19242        assert!(
19243            error.contains("things") && error.contains("`id`"),
19244            "error names the table and the `id` requirement: {error}",
19245        );
19246    }
19247
19248    #[tokio::test]
19249    async fn database_open_rejects_composite_primary_key() {
19250        let error = open_contract_error(
19251            "CREATE TABLE things (id TEXT NOT NULL, part TEXT NOT NULL, \
19252             _updated_at TEXT NOT NULL, PRIMARY KEY (id, part)) STRICT;",
19253            vec![SyncedTable::new(
19254                "things",
19255                crate::sync::session::RowIdentity::SharedKey,
19256            )],
19257            "composite-pk",
19258        );
19259        assert!(
19260            error.contains("things") && error.contains("composite"),
19261            "error names the table and the single-primary-key requirement: {error}",
19262        );
19263    }
19264
19265    #[tokio::test]
19266    async fn database_open_rejects_nullable_updated_at() {
19267        let error = open_contract_error(
19268            "CREATE TABLE things (id TEXT PRIMARY KEY, _updated_at TEXT) STRICT;",
19269            vec![SyncedTable::new(
19270                "things",
19271                crate::sync::session::RowIdentity::SharedKey,
19272            )],
19273            "nullable-updated-at",
19274        );
19275        assert!(
19276            error.contains("things") && error.contains("_updated_at"),
19277            "error names the table and the `_updated_at` requirement: {error}",
19278        );
19279    }
19280
19281    #[tokio::test]
19282    async fn database_open_rejects_non_strict_synced_table() {
19283        let error = open_contract_error(
19284            "CREATE TABLE things (id TEXT PRIMARY KEY, _updated_at TEXT NOT NULL);",
19285            vec![SyncedTable::new(
19286                "things",
19287                crate::sync::session::RowIdentity::SharedKey,
19288            )],
19289            "non-strict",
19290        );
19291        assert!(
19292            error.contains("things") && error.contains("STRICT"),
19293            "error names the table and the STRICT requirement: {error}",
19294        );
19295    }
19296
19297    #[tokio::test]
19298    async fn database_open_rejects_declared_table_no_migration_creates() {
19299        let error = open_contract_error(
19300            "CREATE TABLE other (id TEXT PRIMARY KEY, _updated_at TEXT NOT NULL) STRICT;",
19301            vec![SyncedTable::new(
19302                "things",
19303                crate::sync::session::RowIdentity::SharedKey,
19304            )],
19305            "declared-never-created",
19306        );
19307        assert!(
19308            error.contains("things") && error.contains("no migration creates it"),
19309            "error says the declared table was never created: {error}",
19310        );
19311    }
19312
19313    #[tokio::test]
19314    async fn database_open_rejects_synced_table_spelling_that_differs_from_live_schema() {
19315        let error = open_contract_error(
19316            "CREATE TABLE things (id TEXT PRIMARY KEY, _updated_at TEXT NOT NULL) STRICT;",
19317            vec![SyncedTable::new(
19318                "Things",
19319                crate::sync::session::RowIdentity::SharedKey,
19320            )],
19321            "case-variant-table-name",
19322        );
19323        assert!(
19324            error.contains("Things") && error.contains("things") && error.contains("exact"),
19325            "error names both spellings and requires the live spelling: {error}",
19326        );
19327    }
19328
19329    #[tokio::test]
19330    async fn database_open_rejects_case_variant_duplicate_synced_tables() {
19331        let error = open_contract_error(
19332            "CREATE TABLE things (id TEXT PRIMARY KEY, _updated_at TEXT NOT NULL) STRICT;",
19333            vec![
19334                SyncedTable::new("things", crate::sync::session::RowIdentity::SharedKey),
19335                SyncedTable::new("THINGS", crate::sync::session::RowIdentity::IndependentUuid),
19336            ],
19337            "case-variant-duplicate-table",
19338        );
19339        assert!(
19340            error.contains("things")
19341                && error.contains("THINGS")
19342                && error.contains("more than once"),
19343            "error names both duplicate declarations: {error}",
19344        );
19345    }
19346
19347    #[tokio::test]
19348    async fn database_open_accepts_strict_synced_table() {
19349        Database::open(
19350            Path::new(":memory:"),
19351            vec![SyncedTable::new(
19352                "things",
19353                crate::sync::session::RowIdentity::SharedKey,
19354            )],
19355            BLOB_TOMBSTONE_GRACE,
19356            crate::blob::TransferLimits::serial(),
19357            crate::WritePolicy::MergeConcurrent,
19358            "strict-synced-table".to_string(),
19359            &[Migration::sql(
19360                1,
19361                "contract",
19362                "CREATE TABLE things (id TEXT PRIMARY KEY, _updated_at TEXT NOT NULL) STRICT;",
19363            )],
19364        )
19365        .expect("a STRICT synced table satisfying the rest of the contract opens");
19366    }
19367
19368    #[tokio::test]
19369    async fn database_open_ignores_undeclared_non_strict_local_table() {
19370        // `things` is declared and STRICT; `scratch` is a host-local table never
19371        // passed to `synced_tables` — its own business, not coven's, so it stays
19372        // non-strict with no open error.
19373        Database::open(
19374            Path::new(":memory:"),
19375            vec![SyncedTable::new(
19376                "things",
19377                crate::sync::session::RowIdentity::SharedKey,
19378            )],
19379            BLOB_TOMBSTONE_GRACE,
19380            crate::blob::TransferLimits::serial(),
19381            crate::WritePolicy::MergeConcurrent,
19382            "undeclared-local-table".to_string(),
19383            &[Migration::sql(
19384                1,
19385                "contract",
19386                "CREATE TABLE things (id TEXT PRIMARY KEY, _updated_at TEXT NOT NULL) STRICT; \
19387                 CREATE TABLE scratch (id INTEGER PRIMARY KEY, note TEXT);",
19388            )],
19389        )
19390        .expect("an undeclared non-strict local table is not coven's business");
19391    }
19392
19393    #[tokio::test]
19394    async fn database_open_rejects_duplicate_blob_namespace() {
19395        let blob = |namespace| {
19396            crate::sync::session::BlobDecl::new(
19397                namespace,
19398                crate::blob::Provenance::HostProvided,
19399                crate::blob::CacheFill::CacheLazy,
19400            )
19401        };
19402        let error = open_contract_error(
19403            "CREATE TABLE covers (id TEXT PRIMARY KEY, size INTEGER NOT NULL, \
19404             hash TEXT, _updated_at TEXT NOT NULL) STRICT;\
19405             CREATE TABLE thumbs (id TEXT PRIMARY KEY, size INTEGER NOT NULL, \
19406             hash TEXT, _updated_at TEXT NOT NULL) STRICT;",
19407            vec![
19408                SyncedTable::new("covers", crate::sync::session::RowIdentity::SharedKey)
19409                    .carries_blob(blob("images")),
19410                SyncedTable::new("thumbs", crate::sync::session::RowIdentity::SharedKey)
19411                    .carries_blob(blob("images")),
19412            ],
19413            "dup-namespace",
19414        );
19415        assert!(
19416            error.contains("covers") && error.contains("thumbs") && error.contains("images"),
19417            "error names both tables and the shared blob namespace: {error}",
19418        );
19419    }
19420
19421    #[tokio::test]
19422    async fn fresh_open_requires_each_make_remote_intent_to_name_retain_pinned() {
19423        let (db, _stamper) = Database::open(
19424            Path::new(":memory:"),
19425            Vec::new(),
19426            BLOB_TOMBSTONE_GRACE,
19427            crate::blob::TransferLimits::serial(),
19428            crate::WritePolicy::MergeConcurrent,
19429            "test-device".to_string(),
19430            &[],
19431        )
19432        .expect("open database");
19433
19434        let column = db
19435            .call(|conn| {
19436                let mut stmt = conn
19437                    .prepare("PRAGMA table_info(blob_make_remote_intents)")
19438                    .map_err(DbError::from)?;
19439                let rows = stmt
19440                    .query_map([], |row| {
19441                        Ok((
19442                            row.get::<_, String>(1)?,
19443                            row.get::<_, i64>(3)?,
19444                            row.get::<_, Option<String>>(4)?,
19445                        ))
19446                    })
19447                    .map_err(DbError::from)?;
19448                for row in rows {
19449                    let (name, notnull, default_value) = row.map_err(DbError::from)?;
19450                    if name == "retain_pinned" {
19451                        return Ok(Some((notnull, default_value)));
19452                    }
19453                }
19454                Ok(None)
19455            })
19456            .await
19457            .expect("read make_remote intent schema")
19458            .expect("retain_pinned column exists");
19459
19460        assert_eq!(column.0, 1, "retain_pinned must be NOT NULL");
19461        assert_eq!(
19462            column.1, None,
19463            "retain_pinned must be supplied by every make_remote intent",
19464        );
19465    }
19466
19467    #[tokio::test]
19468    async fn serial_pending_branch_survives_reopen_with_exact_base_and_inverses() {
19469        let temp = tempfile::tempdir().expect("temporary Store");
19470        let path = temp.path().join("serial.db");
19471        let tables = vec![SyncedTable::new(
19472            "notes",
19473            crate::sync::session::RowIdentity::SharedKey,
19474        )];
19475        let migrations = vec![notes_migration()];
19476        let (db, _) = Database::open(
19477            &path,
19478            tables.clone(),
19479            BLOB_TOMBSTONE_GRACE,
19480            crate::blob::TransferLimits::serial(),
19481            crate::WritePolicy::Serial,
19482            "serial-device".to_string(),
19483            &migrations,
19484        )
19485        .expect("open serial Store");
19486        for (write_id, sql) in [
19487            (
19488                "serial-write-1",
19489                "INSERT INTO notes VALUES ('n1', 'first', '0000000001000-0000-serial')",
19490            ),
19491            (
19492                "serial-write-2",
19493                "UPDATE notes SET body = 'second', _updated_at = '0000000002000-0000-serial' WHERE id = 'n1'",
19494            ),
19495        ] {
19496            let tables = tables.clone();
19497            let write_id = WriteId::from_generated(write_id.to_string());
19498            db.call(move |conn| {
19499                Database::run_internal_store_write_transaction_on(
19500                    conn,
19501                    &tables,
19502                    crate::WritePolicy::Serial,
19503                    None,
19504                    write_id,
19505                    |tx| tx.execute_batch(sql).map_err(DbError::from),
19506                )
19507            })
19508            .await
19509            .expect("commit provisional serial write");
19510        }
19511        drop(db);
19512
19513        let (reopened, _) = Database::open(
19514            &path,
19515            tables,
19516            BLOB_TOMBSTONE_GRACE,
19517            crate::blob::TransferLimits::serial(),
19518            crate::WritePolicy::Serial,
19519            "serial-device".to_string(),
19520            &migrations,
19521        )
19522        .expect("reopen serial Store");
19523        let rows = reopened
19524            .call(|conn| {
19525                let mut statement = conn
19526                    .prepare(
19527                        "SELECT write_id, inverse_changeset, base
19528                         FROM store_writes ORDER BY ordinal",
19529                    )
19530                    .map_err(DbError::from)?;
19531                let rows = statement
19532                    .query_map([], |row| {
19533                        Ok((
19534                            row.get::<_, String>(0)?,
19535                            row.get::<_, Vec<u8>>(1)?,
19536                            row.get::<_, String>(2)?,
19537                        ))
19538                    })
19539                    .map_err(DbError::from)?
19540                    .collect::<Result<Vec<_>, _>>()
19541                    .map_err(DbError::from)?;
19542                Ok(rows)
19543            })
19544            .await
19545            .expect("read reopened branch");
19546        assert_eq!(rows.len(), 2);
19547        for (write_id, inverse, base) in rows {
19548            assert!(!inverse.is_empty(), "{write_id} retains its inverse");
19549            let base: StoreWriteBase = serde_json::from_str(&base).expect("serial base");
19550            assert_eq!(
19551                base,
19552                StoreWriteBase::Serial {
19553                    branch_id: PendingBranchId::from_first_write(WriteId::from_generated(
19554                        "serial-write-1".to_string(),
19555                    )),
19556                    base: None,
19557                }
19558            );
19559        }
19560    }
19561
19562    const RESTART_CIRCLE_ID: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaa";
19563
19564    fn restart_circle_coord(policy: WritePolicy) -> String {
19565        match policy {
19566            WritePolicy::MergeConcurrent => serde_json::json!({
19567                "merge_concurrent": {
19568                    "device_id": "restart-control-device",
19569                    "stream_id": "55".repeat(32),
19570                    "author_pubkey": "restart-owner",
19571                    "author_owner_grant": "11".repeat(32),
19572                    "seq": 1,
19573                    "control_hash": "22".repeat(32)
19574                }
19575            }),
19576            WritePolicy::Serial => serde_json::json!({
19577                "serial": {
19578                    "author_pubkey": "restart-owner",
19579                    "generation": 1,
19580                    "control_hash": "33".repeat(32)
19581                }
19582            }),
19583        }
19584        .to_string()
19585    }
19586
19587    async fn capture_scoped_write_then_reopen(
19588        policy: WritePolicy,
19589        name: &str,
19590    ) -> (
19591        tempfile::TempDir,
19592        Database,
19593        Vec<(String, Option<String>, Vec<u8>)>,
19594    ) {
19595        let temp = tempfile::tempdir().expect("temporary scoped Store");
19596        let path = temp.path().join(format!("{name}.db"));
19597        let tables =
19598            vec![
19599                SyncedTable::new("accounts", crate::sync::session::RowIdentity::SharedKey)
19600                    .scoped_by("audience"),
19601            ];
19602        let migrations = vec![Migration::sql(
19603            1,
19604            "accounts",
19605            "CREATE TABLE accounts (
19606                id TEXT PRIMARY KEY,
19607                audience TEXT,
19608                _updated_at TEXT NOT NULL
19609             ) STRICT;",
19610        )];
19611        let (db, _) = Database::open(
19612            &path,
19613            tables.clone(),
19614            BLOB_TOMBSTONE_GRACE,
19615            crate::blob::TransferLimits::serial(),
19616            policy,
19617            format!("{name}-device"),
19618            &migrations,
19619        )
19620        .expect("open scoped Store");
19621        crate::sync::test_helpers::TestStore::create(
19622            &db,
19623            name,
19624            crate::keys::UserKeypair::generate(),
19625        )
19626        .await
19627        .expect("install exact scoped Store authority");
19628        let control = restart_circle_coord(policy);
19629        db.call(move |conn| {
19630            conn.execute(
19631                "INSERT INTO circle_control_activations
19632                 (circle_id, control_coord, stream_id, seq, commit_hash, control_bytes)
19633                 VALUES (?1, ?2, 'restart-control-device', 1, ?3, X'01')",
19634                (RESTART_CIRCLE_ID, &control, "44".repeat(32)),
19635            )
19636            .map_err(DbError::from)?;
19637            conn.execute(
19638                "INSERT INTO circle_access_cache
19639                 (circle_id, control_coord, owner_pubkey, disposition, access_bytes)
19640                 VALUES (?1, ?2, 'restart-owner', 'active', X'02')",
19641                (RESTART_CIRCLE_ID, &control),
19642            )
19643            .map(|_| ())
19644            .map_err(DbError::from)
19645        })
19646        .await
19647        .expect("seed active Circle authority");
19648
19649        let gates = db.gates();
19650        let blob_decls = db.blob_decls();
19651        let write_id = db.new_write_id();
19652        let routing =
19653            (policy == WritePolicy::MergeConcurrent).then(|| EncryptionService::from_key([7; 32]));
19654        let capture_tables = tables.clone();
19655        db.call(move |conn| {
19656            Database::run_store_write_transaction_on(
19657                conn,
19658                &capture_tables,
19659                &gates,
19660                &blob_decls,
19661                policy,
19662                routing.as_ref(),
19663                write_id,
19664                |tx| {
19665                    tx.execute(
19666                        "INSERT INTO accounts (id, audience, _updated_at)
19667                         VALUES ('store-account', NULL, '0000000001000-0000-restart')",
19668                        [],
19669                    )?;
19670                    tx.execute(
19671                        "INSERT INTO accounts (id, audience, _updated_at)
19672                         VALUES ('circle-account', ?1, '0000000001001-0000-restart')",
19673                        [RESTART_CIRCLE_ID],
19674                    )?;
19675                    Ok::<_, DbError>(())
19676                },
19677            )
19678        })
19679        .await
19680        .expect("capture Store and Circle partitions");
19681        let expected = db
19682            .call(|conn| {
19683                conn.prepare(
19684                    "SELECT audience, control_coord, changeset
19685                     FROM store_write_partitions
19686                     ORDER BY CASE audience WHEN 'store' THEN 0 ELSE 1 END,
19687                              audience, control_coord",
19688                )
19689                .and_then(|mut statement| {
19690                    statement
19691                        .query_map([], |row| {
19692                            Ok((
19693                                row.get::<_, String>(0)?,
19694                                row.get::<_, Option<String>>(1)?,
19695                                row.get::<_, Vec<u8>>(2)?,
19696                            ))
19697                        })?
19698                        .collect::<rusqlite::Result<Vec<_>>>()
19699                })
19700                .map_err(DbError::from)
19701            })
19702            .await
19703            .expect("read exact persisted audience partitions");
19704        assert_eq!(expected.len(), 2);
19705        drop(db);
19706
19707        let (reopened, _) = Database::open(
19708            &path,
19709            tables,
19710            BLOB_TOMBSTONE_GRACE,
19711            crate::blob::TransferLimits::serial(),
19712            policy,
19713            format!("{name}-device"),
19714            &migrations,
19715        )
19716        .expect("reopen scoped Store");
19717        (temp, reopened, expected)
19718    }
19719
19720    fn assert_prepared_partitions(
19721        actual: &PreparedStoreWritePartitions,
19722        expected: &[(String, Option<String>, Vec<u8>)],
19723    ) {
19724        let actual = actual
19725            .iter()
19726            .map(|partition| {
19727                let audience = match partition.audience {
19728                    crate::sync::circle::Audience::Store => "store".to_string(),
19729                    crate::sync::circle::Audience::Circle(circle) => circle.to_string(),
19730                    crate::sync::circle::Audience::Local => {
19731                        panic!("Local audience entered Store preparation")
19732                    }
19733                };
19734                (
19735                    audience,
19736                    partition.control.as_ref().map(|control| {
19737                        let parsed = serde_json::from_str(control.stored_json())
19738                            .expect("parse stored control");
19739                        assert_eq!(control.coordinate(), &parsed);
19740                        control.stored_json().to_string()
19741                    }),
19742                    partition.changeset.clone(),
19743                )
19744            })
19745            .collect::<Vec<_>>();
19746        assert_eq!(actual, expected);
19747    }
19748
19749    #[tokio::test]
19750    async fn merge_preparation_reloads_exact_scoped_partitions_after_restart() {
19751        let (_temp, reopened, expected) =
19752            capture_scoped_write_then_reopen(WritePolicy::MergeConcurrent, "merge-restart").await;
19753        let prepared = reopened
19754            .prepare_store_write()
19755            .await
19756            .expect("prepare restarted Merge write")
19757            .expect("pending Merge write");
19758
19759        assert_prepared_partitions(&prepared.partitions, &expected);
19760    }
19761
19762    #[tokio::test]
19763    async fn serial_preparation_reloads_exact_scoped_partitions_after_restart() {
19764        let (_temp, reopened, expected) =
19765            capture_scoped_write_then_reopen(WritePolicy::Serial, "serial-restart").await;
19766        let branch = reopened
19767            .reserve_serial_store_branch()
19768            .await
19769            .expect("reserve restarted Serial branch")
19770            .expect("pending Serial branch");
19771        assert_eq!(branch.writes.len(), 1);
19772
19773        assert_prepared_partitions(&branch.writes[0].partitions, &expected);
19774    }
19775}