khive_runtime/
entity_write.rs1use khive_storage::{SqlStatement, SqlValue, SqlWriter, StorageError};
4use khive_types::{Details, KhiveError};
5use uuid::Uuid;
6
7use crate::{RuntimeError, RuntimeResult};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct EntityVersionConflict {
12 pub expected: i64,
13 pub current: i64,
14}
15
16impl EntityVersionConflict {
17 pub fn into_error(self) -> KhiveError {
18 KhiveError::conflict("entity version precondition failed").with_details(Details::new_owned(
19 vec![
20 ("reason", "version_conflict".to_owned()),
21 ("expected_version", self.expected.to_string()),
22 ("current_version", self.current.to_string()),
23 ],
24 ))
25 }
26}
27
28#[derive(Debug, Clone)]
29pub(crate) struct EntityWriteGuard {
30 pub(crate) id: Uuid,
31 pub(crate) expected_version: i64,
32}
33
34pub(crate) fn validate_expected_version(expected: Option<i64>) -> RuntimeResult<()> {
35 if expected.is_some_and(|value| value < 1) {
36 return Err(RuntimeError::InvalidInput(
37 "expected_version must be a positive integer".into(),
38 ));
39 }
40 Ok(())
41}
42
43impl EntityWriteGuard {
44 pub(crate) async fn check(
45 &self,
46 writer: &mut dyn SqlWriter,
47 ) -> Result<Option<EntityVersionConflict>, StorageError> {
48 let current = writer
49 .query_scalar(SqlStatement {
50 sql: "SELECT version FROM entities WHERE id=?1 AND deleted_at IS NULL".into(),
51 params: vec![SqlValue::Text(self.id.to_string())],
52 label: Some("entity-version-precondition".into()),
53 })
54 .await?;
55 Ok(match current {
58 Some(SqlValue::Integer(current)) if current != self.expected_version => {
59 Some(EntityVersionConflict {
60 expected: self.expected_version,
61 current,
62 })
63 }
64 _ => None,
65 })
66 }
67}
68
69#[cfg(test)]
70#[path = "entity_write_tests.rs"]
71mod tests;