use super::{
SqliteStore, lineage,
snapshot::{column, decode, load_cached_blocking, revision_from_sql},
};
use crate::{
HistoryEntry, HistoryPage, RecordedOperation, StoreError, StoredRecord, error::database_error,
};
use dpm_engine::Operation;
use dpm_model::OperationId;
use rusqlite::{Connection, OptionalExtension, params};
use std::path::Path;
impl SqliteStore {
pub fn history_blocking(
&self,
after_sequence: u64,
limit: u16,
) -> Result<HistoryPage, StoreError> {
let transaction = self
.connection
.unchecked_transaction()
.map_err(database_error(&self.path, "begin history query"))?;
let plan = load_cached_blocking(&transaction, &self.path, &self.cache)?
.ok_or_else(|| StoreError::NotInitialized(self.path.clone()))?;
let lineage = lineage::read_blocking(&transaction, &self.path)?;
let mut entries = Vec::new();
if let Ok(cursor) = i64::try_from(after_sequence) {
let mut statement = transaction
.prepare(&format!(
"{OPERATION_COLUMNS} WHERE sequence > ?1 ORDER BY sequence LIMIT ?2"
))
.map_err(database_error(&self.path, "prepare history query"))?;
let mut rows = statement
.query(params![cursor, limit.min(1000)])
.map_err(database_error(&self.path, "query history"))?;
while let Some(row) = rows
.next()
.map_err(database_error(&self.path, "read history"))?
{
entries.push(read_entry(row, &self.path)?);
}
}
transaction
.commit()
.map_err(database_error(&self.path, "finish history query"))?;
Ok(HistoryPage {
revision: plan.revision,
lineage_id: Some(lineage.lineage_id),
next_after_sequence: entries
.last()
.map_or(after_sequence, |entry| entry.sequence),
entries,
})
}
pub fn recorded_operation_blocking(
&self,
id: OperationId,
) -> Result<Option<HistoryEntry>, StoreError> {
recorded_blocking(&self.connection, &self.path, id)
}
}
pub(super) fn recorded_blocking(
connection: &Connection,
path: &Path,
id: OperationId,
) -> Result<Option<HistoryEntry>, StoreError> {
let mut statement = connection
.prepare(&format!("{OPERATION_COLUMNS} WHERE operation_id = ?1"))
.map_err(database_error(path, "prepare operation lookup"))?;
statement
.query_row(params![id.to_string()], |row| Ok(read_entry(row, path)))
.optional()
.map_err(database_error(path, "look up operation"))?
.transpose()
}
pub(crate) const OPERATION_COLUMNS: &str = "SELECT sequence, operation_id, base_revision, \
resulting_revision, actor_json, timestamp, command_json, workspace_id, lineage_id \
FROM operations";
pub(crate) fn read_entry(row: &rusqlite::Row<'_>, path: &Path) -> Result<HistoryEntry, StoreError> {
let sequence: u64 = row
.get(0)
.map_err(database_error(path, "read history cursor"))?;
let record = StoredRecord::Operation { sequence };
let text = |index, field| column::<String>(row, index, path, record, field);
let revision =
|index, field| column::<i64>(row, index, path, record, field).map(revision_from_sql);
let scalar =
|index, field| text(index, field).map(|value| serde_json::Value::String(value).to_string());
Ok(HistoryEntry {
sequence,
operation: RecordedOperation {
operation: Operation {
id: decode(&scalar(1, "operation_id")?, path, record, "operation_id")?,
base_revision: revision(2, "base_revision")?,
resulting_revision: revision(3, "resulting_revision")?,
actor: decode(&text(4, "actor_json")?, path, record, "actor_json")?,
timestamp: decode(&scalar(5, "timestamp")?, path, record, "timestamp")?,
command: decode(&text(6, "command_json")?, path, record, "command_json")?,
},
workspace_id: decode(&scalar(7, "workspace_id")?, path, record, "workspace_id")?,
lineage_id: decode(&scalar(8, "lineage_id")?, path, record, "lineage_id")?,
},
})
}
#[cfg(test)]
mod tests;