use anyhow::{Result, anyhow, ensure};
use rusqlite::{Connection, OptionalExtension, Transaction, params};
use serde_json::Value;
use crate::ReducerEvent;
pub(super) const MAX_RECEIPT_JSON_BYTES: usize = 32 * 1024;
pub(super) fn initialize_schema(connection: &Connection) -> Result<()> {
connection.execute_batch(
"CREATE TABLE IF NOT EXISTS strategy_command_receipts(
run_id TEXT NOT NULL REFERENCES strategy_runs(run_id) ON DELETE CASCADE,
command_id TEXT NOT NULL,
attempt_token TEXT NOT NULL,
receipt_json TEXT NOT NULL,
PRIMARY KEY(run_id, command_id, attempt_token)
);",
)?;
Ok(())
}
pub(super) fn event_identity(event: &ReducerEvent) -> Result<(String, String)> {
match event {
ReducerEvent::CommandSucceeded {
command_id,
attempt_token,
..
}
| ReducerEvent::CommandFailed {
command_id,
attempt_token,
..
}
| ReducerEvent::CancellationAcknowledged {
command_id,
attempt_token,
}
| ReducerEvent::CancellationUnknown {
command_id,
attempt_token,
} => Ok((command_id.clone(), attempt_token.clone())),
_ => Err(anyhow!("strategy_receipt_event_invalid")),
}
}
pub(super) fn lookup(
connection: &Connection,
run_id: &str,
command_id: &str,
attempt_token: &str,
) -> Result<Option<Value>> {
let receipt: Option<String> = connection
.query_row(
"SELECT receipt_json FROM strategy_command_receipts
WHERE run_id=?1 AND command_id=?2 AND attempt_token=?3",
params![run_id, command_id, attempt_token],
|row| row.get(0),
)
.optional()?;
Ok(receipt
.map(|json| serde_json::from_str(&json))
.transpose()?)
}
pub(super) fn persist(
transaction: &Transaction<'_>,
run_id: &str,
command_id: &str,
attempt_token: &str,
receipt: &Value,
applied: bool,
) -> Result<()> {
let encoded = serde_json::to_string(receipt)?;
ensure!(
encoded.len() <= MAX_RECEIPT_JSON_BYTES,
"strategy_receipt_too_large"
);
if applied {
let inserted = transaction.execute(
"INSERT INTO strategy_command_receipts(run_id, command_id, attempt_token, receipt_json)
VALUES (?1, ?2, ?3, ?4)
ON CONFLICT(run_id, command_id, attempt_token) DO NOTHING",
params![run_id, command_id, attempt_token, encoded],
)?;
if inserted == 1 {
return Ok(());
}
}
let existing = lookup(transaction, run_id, command_id, attempt_token)?
.ok_or_else(|| anyhow!("strategy_receipt_missing"))?;
ensure!(existing == *receipt, "strategy_receipt_conflict");
Ok(())
}