use crate::StoreLineage;
use crate::{
StoreError,
error::database_error,
schema::{self, Layout},
};
use dpm_engine::{Operation, apply_command};
use dpm_model::{LineageId, Plan};
use rusqlite::{Connection, OpenFlags, TransactionBehavior};
use std::{
path::{Path, PathBuf},
time::Duration,
};
mod history;
mod integrity;
mod lineage;
mod recovery;
use recovery::files;
mod replay;
mod runs;
pub(crate) use crate::{HistoryEntry, RecordedOperation};
pub use recovery::{IntegrityReport, restore_store_blocking, verify_store_blocking};
pub use runs::{
RUN_ACTIVITY_LIMIT, RUN_STORE_VERSION, RunBinding, RunStore, RunStoreReport, Written,
};
mod snapshot;
mod snapshot_cache;
use snapshot::load_blocking;
const BUSY_TIMEOUT: Duration = Duration::from_secs(5);
pub struct SqliteStore {
connection: Connection,
cache: snapshot_cache::SnapshotCache,
path: PathBuf,
}
impl SqliteStore {
pub fn open_blocking(path: impl AsRef<Path>) -> Result<Self, StoreError> {
let path = path.as_ref().to_path_buf();
check_existing_read_only_blocking(&path)?;
let connection = Connection::open(&path).map_err(database_error(&path, "open database"))?;
Self::prepare_blocking(connection, path)
}
pub fn open_existing_blocking(path: impl AsRef<Path>) -> Result<Self, StoreError> {
let path = path.as_ref().to_path_buf();
check_existing_read_only_blocking(&path)?;
let connection = Connection::open_with_flags(&path, OpenFlags::SQLITE_OPEN_READ_WRITE)
.map_err(database_error(&path, "open existing database"))?;
Self::prepare_blocking(connection, path)
}
pub fn in_memory_blocking() -> Result<Self, StoreError> {
let path = PathBuf::from(":memory:");
let connection =
Connection::open_in_memory().map_err(database_error(&path, "open database"))?;
Self::prepare_blocking(connection, path)
}
fn prepare_blocking(connection: Connection, path: PathBuf) -> Result<Self, StoreError> {
connection
.busy_timeout(BUSY_TIMEOUT)
.map_err(database_error(&path, "set busy timeout"))?;
schema::check_blocking(&connection, &path)?;
connection
.execute_batch("PRAGMA foreign_keys = ON;")
.map_err(database_error(&path, "configure database"))?;
Ok(Self {
connection,
path,
cache: Default::default(),
})
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn set_busy_timeout_blocking(&self, timeout: Duration) -> Result<(), StoreError> {
self.connection
.busy_timeout(timeout)
.map_err(database_error(&self.path, "set busy timeout"))
}
pub fn lineage_blocking(&self) -> Result<Option<StoreLineage>, StoreError> {
if schema::check_blocking(&self.connection, &self.path)? == Layout::Empty {
return Ok(None);
}
lineage::read_blocking(&self.connection, &self.path).map(Some)
}
pub fn revision_blocking(&self) -> Result<Option<crate::StoreRevision>, StoreError> {
read_revision_blocking(&self.connection, &self.path)
}
pub fn plan_before_blocking(&self, sequence: u64) -> Result<Plan, StoreError> {
let transaction = self
.connection
.unchecked_transaction()
.map_err(database_error(&self.path, "begin replay"))?;
let genesis = snapshot::genesis_blocking(&transaction, &self.path)?.ok_or_else(|| {
StoreError::MissingGenesis {
path: self.path.clone(),
}
})?;
let plan = replay::replay_before_blocking(&transaction, &self.path, genesis, sequence)?;
transaction
.commit()
.map_err(database_error(&self.path, "finish replay"))?;
Ok(plan)
}
pub fn load_blocking(&self) -> Result<Option<Plan>, StoreError> {
if schema::check_blocking(&self.connection, &self.path)? == Layout::Empty {
return Ok(None);
}
snapshot::load_cached_blocking(&self.connection, &self.path, &self.cache)
}
pub fn initialize_blocking(&mut self, plan: &Plan) -> Result<(), StoreError> {
plan.validate()?;
if occupied_blocking(&self.connection, &self.path)? {
return Err(StoreError::AlreadyInitialized(self.path.clone()));
}
self.connection
.execute_batch("PRAGMA journal_mode = WAL;")
.map_err(database_error(&self.path, "configure database"))?;
let transaction = self
.connection
.transaction_with_behavior(TransactionBehavior::Immediate)
.map_err(database_error(&self.path, "begin initialization"))?;
prepare_write_blocking(&transaction, &self.path)?;
if occupied_blocking(&transaction, &self.path)? {
return Err(StoreError::AlreadyInitialized(self.path.clone()));
}
snapshot::write_initial_blocking(&transaction, &self.path, plan)?;
transaction
.commit()
.map_err(database_error(&self.path, "commit initialization"))?;
Ok(())
}
pub fn persist_blocking(
&mut self,
plan: &Plan,
operation: &Operation,
expected_lineage: Option<LineageId>,
) -> Result<HistoryEntry, StoreError> {
if operation.base_revision.wrapping_add(1) != operation.resulting_revision
|| plan.revision != operation.resulting_revision
{
return Err(StoreError::InvalidOperationRevision {
base: operation.base_revision,
result: operation.resulting_revision,
plan: plan.revision,
});
}
plan.validate()?;
let transaction = self
.connection
.transaction_with_behavior(TransactionBehavior::Immediate)
.map_err(database_error(&self.path, "begin operation"))?;
if prepare_write_blocking(&transaction, &self.path)? == Layout::Empty {
return Err(StoreError::NotInitialized(self.path.clone()));
}
if let Some(recorded) = history::recorded_blocking(&transaction, &self.path, operation.id)?
{
return Err(StoreError::DuplicateOperation {
recorded: Box::new(recorded),
});
}
let found = lineage::read_blocking(&transaction, &self.path)?;
found.check_writable(expected_lineage, &self.path)?;
let mut expected = snapshot::load_cached_blocking(&transaction, &self.path, &self.cache)?
.ok_or_else(|| StoreError::NotInitialized(self.path.clone()))?;
if expected.revision != operation.base_revision {
return Err(StoreError::RevisionConflict {
expected: operation.base_revision,
actual: expected.revision,
});
}
apply_command(
&mut expected,
operation.actor.clone(),
operation.command.clone(),
operation.timestamp,
operation.id,
)?;
if expected != *plan {
return Err(StoreError::SnapshotMismatch(operation.id));
}
let sequence = snapshot::write_operation_blocking(
&transaction,
&self.path,
plan,
operation,
found.lineage_id,
)?;
transaction
.commit()
.map_err(database_error(&self.path, "commit operation"))?;
Ok(HistoryEntry {
sequence,
operation: RecordedOperation {
operation: operation.clone(),
workspace_id: plan.workspace.id,
lineage_id: found.lineage_id,
},
})
}
pub fn operation_count_blocking(&self) -> Result<u64, StoreError> {
if schema::check_blocking(&self.connection, &self.path)? == Layout::Empty {
return Ok(0);
}
let count: u64 = self
.connection
.query_row("SELECT COUNT(*) FROM operations", [], |row| row.get(0))
.map_err(database_error(&self.path, "count operations"))?;
Ok(count)
}
}
pub fn store_revision_blocking(path: &Path) -> Result<Option<crate::StoreRevision>, StoreError> {
let mode = files::read_mode_blocking(path)?;
let connection = files::open_read_only_blocking(path, mode)?;
read_revision_blocking(&connection, path)
}
fn read_revision_blocking(
connection: &Connection,
path: &Path,
) -> Result<Option<crate::StoreRevision>, StoreError> {
if schema::check_blocking(connection, path)? == Layout::Empty {
return Ok(None);
}
let transaction = connection
.unchecked_transaction()
.map_err(database_error(path, "begin revision query"))?;
let revision = snapshot::revision_blocking(&transaction, path)?;
let lineage = lineage::read_blocking(&transaction, path)?;
let history_head: u64 = transaction
.query_row(
"SELECT COALESCE(MAX(sequence), 0) FROM operations",
[],
|row| row.get(0),
)
.map_err(database_error(path, "read history head"))?;
transaction
.commit()
.map_err(database_error(path, "finish revision query"))?;
Ok(revision.map(|revision| crate::StoreRevision {
revision,
lineage,
history_head,
}))
}
fn check_existing_read_only_blocking(path: &Path) -> Result<(), StoreError> {
let exists = path.try_exists().map_err(|source| StoreError::Io {
action: "inspect",
path: path.to_path_buf(),
source,
})?;
if !exists {
return Ok(());
}
let mode = files::read_mode_blocking(path)?;
let connection = files::open_read_only_blocking(path, mode)?;
if schema::check_blocking(&connection, path)? == Layout::Current {
snapshot::load_blocking(&connection, path)?;
snapshot::genesis_blocking(&connection, path)?;
lineage::read_blocking(&connection, path)?;
}
Ok(())
}
fn occupied_blocking(connection: &Connection, path: &Path) -> Result<bool, StoreError> {
if schema::check_blocking(connection, path)? == Layout::Empty {
return Ok(false);
}
connection
.query_row(
"SELECT EXISTS(SELECT 1 FROM plan_state) OR EXISTS(SELECT 1 FROM operations)",
[],
|row| row.get(0),
)
.map_err(database_error(path, "check initialization"))
}
fn prepare_write_blocking(connection: &Connection, path: &Path) -> Result<Layout, StoreError> {
let found = schema::check_blocking(connection, path)?;
if found == Layout::Empty {
schema::create_blocking(connection, path)?;
}
Ok(found)
}
#[cfg(test)]
mod tests;