use super::history::{OPERATION_COLUMNS, read_entry};
use crate::{StoreError, error::database_error};
use dpm_engine::apply_command;
use dpm_model::Plan;
use rusqlite::Connection;
use std::path::Path;
pub(super) fn replay_blocking(
connection: &Connection,
path: &Path,
genesis: Plan,
snapshot: &Plan,
) -> Result<(), StoreError> {
let plan = replay_before_blocking(connection, path, genesis, u64::MAX)?;
if plan == *snapshot {
return Ok(());
}
Err(StoreError::ReplayDiverged {
path: path.to_path_buf(),
revision: snapshot.revision,
differing: differing_fields(&plan, snapshot)?,
})
}
pub(super) fn replay_before_blocking(
connection: &Connection,
path: &Path,
genesis: Plan,
before: u64,
) -> Result<Plan, StoreError> {
let bound = i64::try_from(before).unwrap_or(i64::MAX);
let mut statement = connection
.prepare(&format!(
"{OPERATION_COLUMNS} WHERE sequence < ?1 ORDER BY sequence"
))
.map_err(database_error(path, "prepare history replay"))?;
let mut rows = statement
.query([bound])
.map_err(database_error(path, "query history"))?;
let mut plan = genesis;
while let Some(row) = rows.next().map_err(database_error(path, "read history"))? {
let entry = read_entry(row, path)?;
let operation = entry.operation.operation;
apply_command(
&mut plan,
operation.actor,
operation.command,
operation.timestamp,
operation.id,
)
.map_err(|source| StoreError::ReplayRefused {
path: path.to_path_buf(),
sequence: entry.sequence,
source,
})?;
}
Ok(plan)
}
fn differing_fields(replayed: &Plan, snapshot: &Plan) -> Result<Vec<String>, StoreError> {
let replayed = serde_json::to_value(replayed)?;
let snapshot = serde_json::to_value(snapshot)?;
let empty = serde_json::Map::new();
let (left, right) = (
replayed.as_object().unwrap_or(&empty),
snapshot.as_object().unwrap_or(&empty),
);
let mut fields: Vec<String> = left
.keys()
.chain(right.keys())
.filter(|key| left.get(*key) != right.get(*key))
.cloned()
.collect();
fields.sort();
fields.dedup();
Ok(fields)
}