Add new data model
This commit is contained in:
parent
945984adfa
commit
ce5f3bbd7d
4 changed files with 316 additions and 70 deletions
|
|
@ -1,30 +1,34 @@
|
||||||
CREATE TABLE IF NOT EXISTS documents (
|
CREATE TABLE events (
|
||||||
vault_update_id INTEGER NOT NULL PRIMARY KEY,
|
event_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(event_id <= 9007199254740991), -- JavaScript’s Number.MAX_SAFE_INTEGER
|
||||||
|
request_id TEXT NOT NULL UNIQUE,
|
||||||
|
request_fingerprint BLOB NOT NULL,
|
||||||
|
event_json TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE documents (
|
||||||
|
vault_update_id INTEGER PRIMARY KEY REFERENCES events(event_id),
|
||||||
document_id TEXT NOT NULL,
|
document_id TEXT NOT NULL,
|
||||||
relative_path TEXT NOT NULL,
|
updated_date TEXT NOT NULL,
|
||||||
updated_date TIMESTAMP NOT NULL,
|
|
||||||
content BLOB NOT NULL,
|
content BLOB NOT NULL,
|
||||||
is_deleted BOOLEAN NOT NULL,
|
|
||||||
user_id TEXT NOT NULL,
|
user_id TEXT NOT NULL,
|
||||||
device_id TEXT NOT NULL
|
device_id TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE VIEW IF NOT EXISTS latest_document_versions AS
|
CREATE INDEX documents_by_id ON documents(document_id);
|
||||||
SELECT d.*
|
|
||||||
FROM documents d
|
|
||||||
INNER JOIN (
|
|
||||||
SELECT MAX(vault_update_id) AS max_version_id
|
|
||||||
FROM documents
|
|
||||||
GROUP BY document_id
|
|
||||||
) max_versions
|
|
||||||
ON d.vault_update_id = max_versions.max_version_id;
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_documents_vault_id_relative_path
|
CREATE VIEW latest_document_versions AS
|
||||||
ON documents (relative_path);
|
SELECT d.* FROM documents d JOIN (
|
||||||
|
SELECT document_id, MAX(vault_update_id) AS version FROM documents GROUP BY document_id
|
||||||
|
) heads ON d.vault_update_id = heads.version;
|
||||||
|
|
||||||
-- The immutable version and its acknowledgement commit in one transaction.
|
CREATE TABLE file_manifests (
|
||||||
CREATE TABLE push_acknowledgements (
|
file_manifest_id INTEGER PRIMARY KEY REFERENCES events(event_id)
|
||||||
request_id TEXT NOT NULL PRIMARY KEY,
|
);
|
||||||
request_fingerprint BLOB NOT NULL,
|
|
||||||
vault_update_id INTEGER NOT NULL REFERENCES documents(vault_update_id)
|
CREATE TABLE file_manifest_entries (
|
||||||
|
file_manifest_id INTEGER NOT NULL REFERENCES file_manifests(file_manifest_id),
|
||||||
|
document_id TEXT NOT NULL,
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (file_manifest_id, document_id),
|
||||||
|
UNIQUE (file_manifest_id, path)
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,91 +1,125 @@
|
||||||
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
pub type VaultId = String;
|
pub type VaultId = String;
|
||||||
pub type VaultUpdateId = i64;
|
pub type VaultUpdateId = i64;
|
||||||
|
|
||||||
pub type DocumentId = uuid::Uuid;
|
pub type DocumentId = uuid::Uuid;
|
||||||
pub type UserId = String;
|
pub type UserId = String;
|
||||||
pub type DeviceId = String;
|
pub type DeviceId = String;
|
||||||
|
pub type FileManifestEntries = BTreeMap<DocumentId, String>;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, sqlx::FromRow)]
|
||||||
pub struct StoredDocumentVersion {
|
pub struct StoredDocumentVersion {
|
||||||
pub vault_update_id: VaultUpdateId,
|
pub vault_update_id: VaultUpdateId,
|
||||||
|
#[sqlx(try_from = "uuid::fmt::Hyphenated")]
|
||||||
pub document_id: DocumentId,
|
pub document_id: DocumentId,
|
||||||
pub relative_path: String,
|
|
||||||
pub updated_date: DateTime<Utc>,
|
pub updated_date: DateTime<Utc>,
|
||||||
pub content: Vec<u8>,
|
pub content: Vec<u8>,
|
||||||
pub is_deleted: bool,
|
|
||||||
pub user_id: UserId,
|
pub user_id: UserId,
|
||||||
pub device_id: DeviceId,
|
pub device_id: DeviceId,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq<Self> for StoredDocumentVersion {
|
#[derive(TS, Debug, Clone, Serialize, Deserialize)]
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.vault_update_id == other.vault_update_id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(TS, Debug, Clone, Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
pub struct DocumentVersionWithoutContent {
|
pub struct DocumentVersionWithoutContent {
|
||||||
#[ts(as = "i32")]
|
#[ts(as = "f64")]
|
||||||
pub vault_update_id: VaultUpdateId,
|
pub vault_update_id: VaultUpdateId,
|
||||||
|
|
||||||
pub document_id: DocumentId,
|
pub document_id: DocumentId,
|
||||||
pub relative_path: String,
|
|
||||||
pub updated_date: DateTime<Utc>,
|
pub updated_date: DateTime<Utc>,
|
||||||
pub is_deleted: bool,
|
|
||||||
pub user_id: UserId,
|
pub user_id: UserId,
|
||||||
pub device_id: DeviceId,
|
pub device_id: DeviceId,
|
||||||
|
pub content_size: usize,
|
||||||
#[ts(as = "i32")]
|
|
||||||
pub content_size: u64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<StoredDocumentVersion> for DocumentVersionWithoutContent {
|
impl From<&StoredDocumentVersion> for DocumentVersionWithoutContent {
|
||||||
fn from(value: StoredDocumentVersion) -> Self {
|
fn from(v: &StoredDocumentVersion) -> Self {
|
||||||
Self {
|
Self {
|
||||||
vault_update_id: value.vault_update_id,
|
vault_update_id: v.vault_update_id,
|
||||||
document_id: value.document_id,
|
document_id: v.document_id,
|
||||||
relative_path: value.relative_path,
|
updated_date: v.updated_date,
|
||||||
updated_date: value.updated_date,
|
user_id: v.user_id.clone(),
|
||||||
is_deleted: value.is_deleted,
|
device_id: v.device_id.clone(),
|
||||||
user_id: value.user_id,
|
content_size: v.content.len(),
|
||||||
device_id: value.device_id,
|
|
||||||
content_size: value.content.len() as u64,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl From<StoredDocumentVersion> for DocumentVersionWithoutContent {
|
||||||
|
fn from(v: StoredDocumentVersion) -> Self {
|
||||||
|
Self::from(&v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TS, Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct DocumentVersion {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub metadata: DocumentVersionWithoutContent,
|
||||||
|
pub content_base64: String,
|
||||||
|
}
|
||||||
|
impl From<StoredDocumentVersion> for DocumentVersion {
|
||||||
|
fn from(v: StoredDocumentVersion) -> Self {
|
||||||
|
Self {
|
||||||
|
metadata: (&v).into(),
|
||||||
|
content_base64: STANDARD.encode(&v.content),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TS, Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct FileManifest {
|
||||||
|
#[ts(as = "f64")]
|
||||||
|
pub file_manifest_id: VaultUpdateId,
|
||||||
|
#[ts(type = "Record<string, string>")]
|
||||||
|
pub entries: FileManifestEntries,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TS, Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type", rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
|
pub enum VaultEvent {
|
||||||
|
Content {
|
||||||
|
document: DocumentVersionWithoutContent,
|
||||||
|
},
|
||||||
|
FileManifest {
|
||||||
|
#[serde(rename = "fileManifest")]
|
||||||
|
#[ts(rename = "fileManifest")]
|
||||||
|
file_manifest: FileManifest,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TS, Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct EventRecord {
|
||||||
|
#[ts(as = "f64")]
|
||||||
|
pub event_id: VaultUpdateId,
|
||||||
|
pub request_id: uuid::Uuid,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub event: VaultEvent,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(TS, Debug, Clone, Serialize)]
|
#[derive(TS, Debug, Clone, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct DocumentVersion {
|
#[ts(export)]
|
||||||
#[ts(as = "i32")]
|
pub struct VaultSnapshot {
|
||||||
pub vault_update_id: VaultUpdateId,
|
#[ts(as = "f64")]
|
||||||
|
pub head_event_id: VaultUpdateId,
|
||||||
pub document_id: DocumentId,
|
pub file_manifest: FileManifest,
|
||||||
pub relative_path: String,
|
pub documents: Vec<DocumentVersionWithoutContent>,
|
||||||
pub updated_date: DateTime<Utc>,
|
|
||||||
pub content_base64: String,
|
|
||||||
pub is_deleted: bool,
|
|
||||||
pub user_id: UserId,
|
|
||||||
pub device_id: DeviceId,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<StoredDocumentVersion> for DocumentVersion {
|
#[derive(TS, Debug, Clone, Serialize)]
|
||||||
fn from(value: StoredDocumentVersion) -> Self {
|
#[serde(rename_all = "camelCase")]
|
||||||
Self {
|
#[ts(export)]
|
||||||
vault_update_id: value.vault_update_id,
|
pub struct EventBatch {
|
||||||
document_id: value.document_id,
|
#[ts(as = "f64")]
|
||||||
relative_path: value.relative_path,
|
pub head_event_id: VaultUpdateId,
|
||||||
updated_date: value.updated_date,
|
pub events: Vec<EventRecord>,
|
||||||
content_base64: STANDARD.encode(&value.content),
|
|
||||||
is_deleted: value.is_deleted,
|
|
||||||
user_id: value.user_id,
|
|
||||||
device_id: value.device_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
68
sync-server/src/app_state/database/mutations.rs
Normal file
68
sync-server/src/app_state/database/mutations.rs
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
use super::{Database, Transaction, models::*};
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
impl Database {
|
||||||
|
pub async fn allocate_event(
|
||||||
|
tx: &mut Transaction<'_>,
|
||||||
|
request_id: uuid::Uuid,
|
||||||
|
fingerprint: &[u8],
|
||||||
|
) -> Result<VaultUpdateId> {
|
||||||
|
Ok(sqlx::query(
|
||||||
|
"INSERT INTO events(request_id, request_fingerprint, event_json) VALUES (?, ?, '')",
|
||||||
|
)
|
||||||
|
.bind(request_id.to_string())
|
||||||
|
.bind(fingerprint)
|
||||||
|
.execute(&mut **tx)
|
||||||
|
.await?
|
||||||
|
.last_insert_rowid())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn insert_document_version(
|
||||||
|
tx: &mut Transaction<'_>,
|
||||||
|
version: &StoredDocumentVersion,
|
||||||
|
) -> Result<()> {
|
||||||
|
sqlx::query("INSERT INTO documents(vault_update_id, document_id, updated_date, content, user_id, device_id) VALUES (?, ?, ?, ?, ?, ?)")
|
||||||
|
.bind(version.vault_update_id)
|
||||||
|
.bind(version.document_id.hyphenated())
|
||||||
|
.bind(version.updated_date)
|
||||||
|
.bind(&version.content)
|
||||||
|
.bind(&version.user_id)
|
||||||
|
.bind(&version.device_id)
|
||||||
|
.execute(&mut **tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn insert_file_manifest(
|
||||||
|
tx: &mut Transaction<'_>,
|
||||||
|
file_manifest: &FileManifest,
|
||||||
|
) -> Result<()> {
|
||||||
|
sqlx::query("INSERT INTO file_manifests(file_manifest_id) VALUES (?)")
|
||||||
|
.bind(file_manifest.file_manifest_id)
|
||||||
|
.execute(&mut **tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
for (document_id, path) in &file_manifest.entries {
|
||||||
|
sqlx::query(
|
||||||
|
"INSERT INTO file_manifest_entries(file_manifest_id, document_id, path) VALUES (?, ?, ?)",
|
||||||
|
)
|
||||||
|
.bind(file_manifest.file_manifest_id)
|
||||||
|
.bind(document_id.hyphenated())
|
||||||
|
.bind(path)
|
||||||
|
.execute(&mut **tx)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn write_event(tx: &mut Transaction<'_>, event: &EventRecord) -> Result<()> {
|
||||||
|
sqlx::query("UPDATE events SET event_json = ? WHERE event_id = ?")
|
||||||
|
.bind(serde_json::to_string(event)?)
|
||||||
|
.bind(event.event_id)
|
||||||
|
.execute(&mut **tx)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
140
sync-server/src/app_state/database/queries.rs
Normal file
140
sync-server/src/app_state/database/queries.rs
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
use super::{Database, Transaction, models::*};
|
||||||
|
use anyhow::{Result, ensure};
|
||||||
|
|
||||||
|
impl Database {
|
||||||
|
pub async fn get_request_event(
|
||||||
|
tx: &mut Transaction<'_>,
|
||||||
|
request_id: uuid::Uuid,
|
||||||
|
) -> Result<Option<(Vec<u8>, String)>> {
|
||||||
|
Ok(sqlx::query_as(
|
||||||
|
"SELECT request_fingerprint, event_json FROM events WHERE request_id = ?",
|
||||||
|
)
|
||||||
|
.bind(request_id.to_string())
|
||||||
|
.fetch_optional(&mut **tx)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_latest_document_version(
|
||||||
|
&self,
|
||||||
|
vault: &VaultId,
|
||||||
|
id: &DocumentId,
|
||||||
|
transaction: Option<&mut Transaction<'_>>,
|
||||||
|
) -> Result<Option<StoredDocumentVersion>> {
|
||||||
|
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_document_version(
|
||||||
|
&self,
|
||||||
|
vault: &VaultId,
|
||||||
|
version: VaultUpdateId,
|
||||||
|
transaction: Option<&mut Transaction<'_>>,
|
||||||
|
) -> Result<Option<StoredDocumentVersion>> {
|
||||||
|
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<FileManifest> {
|
||||||
|
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());
|
||||||
|
};
|
||||||
|
|
||||||
|
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(FileManifest {
|
||||||
|
file_manifest_id,
|
||||||
|
entries,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn latest_event_id(tx: &mut Transaction<'_>) -> Result<VaultUpdateId> {
|
||||||
|
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<VaultSnapshot> {
|
||||||
|
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 versions = sqlx::query_as::<_, StoredDocumentVersion>(
|
||||||
|
"SELECT d.* 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?;
|
||||||
|
|
||||||
|
let documents = versions.into_iter().map(Into::into).collect();
|
||||||
|
|
||||||
|
Ok(VaultSnapshot {
|
||||||
|
head_event_id,
|
||||||
|
file_manifest,
|
||||||
|
documents,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn events_after(&self, vault: &VaultId, after: VaultUpdateId) -> Result<EventBatch> {
|
||||||
|
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<String> = sqlx::query_scalar(
|
||||||
|
"SELECT event_json FROM events WHERE event_id > ? ORDER BY event_id",
|
||||||
|
)
|
||||||
|
.bind(after)
|
||||||
|
.fetch_all(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let events = rows
|
||||||
|
.iter()
|
||||||
|
.map(|json| serde_json::from_str(json))
|
||||||
|
.collect::<Result<_, _>>()?;
|
||||||
|
|
||||||
|
Ok(EventBatch {
|
||||||
|
head_event_id,
|
||||||
|
events,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue