use super::{ Database, Transaction, models::{ DocumentId, DocumentVersionWithoutContent, EventBatch, EventRecord, FileManifest, StoredDocumentVersion, VaultEvent, VaultId, VaultSnapshot, VaultUpdateId, }, }; use anyhow::{Context as _, Result, ensure}; use crate::consts::MAX_EVENTS_PAGE_BYTES; const EMPTY_HISTORY_CHECKPOINT: &str = "0:empty"; impl Database { pub async fn get_missing_document( tx: &mut Transaction<'_>, ids: &[DocumentId], ) -> Result> { Ok(sqlx::query_scalar( "SELECT value FROM json_each(?) AS requested WHERE NOT EXISTS (SELECT 1 FROM documents WHERE document_id = requested.value) LIMIT 1", ) .bind(serde_json::to_string(ids)?) .fetch_optional(&mut **tx) .await?) } pub async fn get_request_event( tx: &mut Transaction<'_>, request_id: uuid::Uuid, ) -> Result, VaultEvent)>> { let row: Option<(Vec, VaultUpdateId)> = sqlx::query_as("SELECT request_fingerprint, event_id FROM events WHERE request_id = ?") .bind(request_id.to_string()) .fetch_optional(&mut **tx) .await?; match row { Some((fingerprint, event_id)) => { Ok(Some((fingerprint, Self::event_by_id(tx, event_id).await?))) } None => Ok(None), } } pub async fn get_latest_document_version( &self, vault: &VaultId, id: &DocumentId, transaction: Option<&mut Transaction<'_>>, ) -> Result> { let query = sqlx::query_as::<_, StoredDocumentVersion>( "SELECT * FROM documents WHERE document_id = ? ORDER BY vault_update_id DESC LIMIT 1", ) .bind(id.hyphenated()); Ok(if let Some(tx) = transaction { query.fetch_optional(&mut **tx).await? } else { query .fetch_optional(&self.get_connection_pool(vault).await?) .await? }) } pub async fn get_latest_document_metadata( tx: &mut Transaction<'_>, id: &DocumentId, ) -> Result> { Ok(sqlx::query_as::<_, DocumentVersionWithoutContent>( "SELECT vault_update_id, document_id, updated_date, user_id, device_id, length(content) AS content_size FROM documents WHERE document_id = ? ORDER BY vault_update_id DESC LIMIT 1", ) .bind(id.hyphenated()) .fetch_optional(&mut **tx) .await?) } pub async fn get_document_version( &self, vault: &VaultId, version: VaultUpdateId, transaction: Option<&mut Transaction<'_>>, ) -> Result> { let query = sqlx::query_as::<_, StoredDocumentVersion>( "SELECT * FROM documents WHERE vault_update_id = ?", ) .bind(version); Ok(if let Some(tx) = transaction { query.fetch_optional(&mut **tx).await? } else { query .fetch_optional(&self.get_connection_pool(vault).await?) .await? }) } pub async fn current_file_manifest(tx: &mut Transaction<'_>) -> Result { let Some(file_manifest_id) = sqlx::query_scalar::<_, VaultUpdateId>( "SELECT file_manifest_id FROM file_manifests ORDER BY file_manifest_id DESC LIMIT 1", ) .fetch_optional(&mut **tx) .await? else { return Ok(FileManifest::default()); }; Self::file_manifest_by_id(tx, file_manifest_id) .await? .context("Latest file manifest is missing") } async fn file_manifest_by_id( tx: &mut Transaction<'_>, file_manifest_id: VaultUpdateId, ) -> Result> { let exists = sqlx::query_scalar::<_, VaultUpdateId>( "SELECT file_manifest_id FROM file_manifests WHERE file_manifest_id = ?", ) .bind(file_manifest_id) .fetch_optional(&mut **tx) .await?; if exists.is_none() { return Ok(None); } let entries = sqlx::query_as::<_, (uuid::fmt::Hyphenated, String)>( "SELECT document_id, path FROM file_manifest_entries WHERE file_manifest_id = ?", ) .bind(file_manifest_id) .fetch_all(&mut **tx) .await? .into_iter() .map(|(document_id, path)| (document_id.into_uuid(), path)) .collect(); Ok(Some(FileManifest { file_manifest_id, entries, })) } async fn event_by_id(tx: &mut Transaction<'_>, event_id: VaultUpdateId) -> Result { let document = sqlx::query_as::<_, DocumentVersionWithoutContent>( "SELECT vault_update_id, document_id, updated_date, user_id, device_id, length(content) AS content_size FROM documents WHERE vault_update_id = ?", ) .bind(event_id) .fetch_optional(&mut **tx) .await?; if let Some(document) = document { return Ok(VaultEvent::Content { document }); } let file_manifest = Self::file_manifest_by_id(tx, event_id) .await? .context("Event has no normalized content or file manifest")?; Ok(VaultEvent::FileManifest { file_manifest }) } pub async fn latest_event_id(tx: &mut Transaction<'_>) -> Result { Ok( sqlx::query_scalar("SELECT COALESCE(MAX(event_id), 0) FROM events") .fetch_one(&mut **tx) .await?, ) } pub async fn vault_snapshot(&self, vault: &VaultId) -> Result { let mut tx = self.create_readonly_transaction(vault).await?; let head_event_id = Self::latest_event_id(&mut tx).await?; let file_manifest = Self::current_file_manifest(&mut tx).await?; let documents = sqlx::query_as::<_, DocumentVersionWithoutContent>( "SELECT d.vault_update_id, d.document_id, d.updated_date, d.user_id, d.device_id, length(d.content) AS content_size FROM latest_document_versions d JOIN file_manifest_entries m ON m.document_id = d.document_id WHERE m.file_manifest_id = ? ORDER BY d.vault_update_id", ) .bind(file_manifest.file_manifest_id) .fetch_all(&mut *tx) .await?; Ok(VaultSnapshot { head_event_id, file_manifest, documents, }) } pub async fn events_after( &self, vault: &VaultId, after: VaultUpdateId, page_size: usize, ) -> Result { let mut tx = self.create_readonly_transaction(vault).await?; let head_event_id = Self::latest_event_id(&mut tx).await?; ensure!( (0..=head_event_id).contains(&after), "Event cursor is outside this vault's history" ); let rows: Vec<(VaultUpdateId, String)> = sqlx::query_as( "SELECT event_id, request_id FROM events WHERE event_id > ? ORDER BY event_id LIMIT ?", ) .bind(after) .bind(i64::try_from(page_size)?) .fetch_all(&mut *tx) .await?; let mut events = Vec::with_capacity(rows.len()); let mut bytes = 0; let mut end_event_id = after; for (event_id, request_id) in rows { let event = EventRecord { event_id, request_id: request_id.parse()?, event: Self::event_by_id(&mut tx, event_id).await?, }; let size = serde_json::to_vec(&event)?.len(); // One manifest is atomic and may exceed this budget. Always allow // one event so even such a page advances; never accumulate history. if !events.is_empty() && bytes + size > MAX_EVENTS_PAGE_BYTES { break; } bytes += size; end_event_id = event_id; events.push(event); } Ok(EventBatch { head_event_id, end_event_id, events, }) } } impl Database { pub async fn history_checkpoint(&self, vault: &VaultId) -> Result { let mut tx = self.create_readonly_transaction(vault).await?; let row: Option<(i64, String)> = sqlx::query_as( "SELECT event_id, event_token FROM events ORDER BY event_id DESC LIMIT 1", ) .fetch_optional(&mut *tx) .await?; Ok(row.map_or_else( || EMPTY_HISTORY_CHECKPOINT.to_owned(), |(id, token)| format!("{id}:{token}"), )) } pub async fn contains_checkpoint(&self, vault: &VaultId, checkpoint: &str) -> Result { if checkpoint == EMPTY_HISTORY_CHECKPOINT { return Ok(true); } let Some((id, token)) = checkpoint.split_once(':') else { return Ok(false); }; let Ok(id) = id.parse::() else { return Ok(false); }; let mut tx = self.create_readonly_transaction(vault).await?; let exists: bool = sqlx::query_scalar( "SELECT EXISTS(SELECT 1 FROM events WHERE event_id = ? AND event_token = ?)", ) .bind(id) .bind(token) .fetch_one(&mut *tx) .await?; Ok(exists) } }