use anyhow::{Context, Result};
use serde_json::Value;
use sqlx::{Row, SqliteConnection};
use crate::error::CoreError;
use crate::ids::{new_id, now};
use super::{get_meta, set_meta};
async fn next_local_seq(conn: &mut SqliteConnection) -> Result<i64> {
let seq = get_meta(conn, "local_seq")
.await?
.unwrap_or_else(|| "0".to_string())
.parse::<i64>()?
+ 1;
set_meta(conn, "local_seq", &seq.to_string()).await?;
Ok(seq)
}
async fn serialize_local_change(
conn: &mut SqliteConnection,
op_type: &str,
field: Option<&str>,
payload: &Value,
) -> Result<String> {
let protocol = crate::sync::protocol::replica_protocol(conn).await?;
crate::sync::protocol::validate_operation(protocol, op_type, field, payload)?;
crate::sync::wire::serialize_change_payload(payload)
}
pub(crate) async fn insert_change(
conn: &mut SqliteConnection,
entity_type: &str,
entity_id: &str,
field: Option<&str>,
op_type: &str,
payload: Value,
base_version: Option<&str>,
) -> Result<String> {
let payload = serialize_local_change(conn, op_type, field, &payload).await?;
let change_id = new_id();
let client_id = get_meta(conn, "client_id")
.await?
.context("missing client id")?;
let local_seq = next_local_seq(conn).await?;
let created_at = now();
sqlx::query!(
"INSERT INTO changes(change_id, client_id, local_seq, entity_type, entity_id, field,
op_type, payload, base_version, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
change_id,
client_id,
local_seq,
entity_type,
entity_id,
field,
op_type,
payload,
base_version,
created_at,
)
.execute(&mut *conn)
.await?;
Ok(change_id)
}
pub(crate) struct IdentifiedChange<'a> {
pub change_id: &'a str,
pub entity_type: &'a str,
pub entity_id: &'a str,
pub field: Option<&'a str>,
pub op_type: &'a str,
pub payload: Value,
pub base_version: Option<&'a str>,
pub created_at: &'a str,
}
pub(crate) async fn insert_change_with_identity(
conn: &mut SqliteConnection,
change: IdentifiedChange<'_>,
) -> Result<()> {
let IdentifiedChange {
change_id,
entity_type,
entity_id,
field,
op_type,
payload,
base_version,
created_at,
} = change;
let payload = serialize_local_change(conn, op_type, field, &payload).await?;
let existing = sqlx::query(
"SELECT entity_type, entity_id, field, op_type, payload, base_version, created_at
FROM changes WHERE change_id = ?",
)
.bind(change_id)
.fetch_optional(&mut *conn)
.await?;
if let Some(existing) = existing {
let equal = existing.try_get::<String, _>("entity_type")? == entity_type
&& existing.try_get::<String, _>("entity_id")? == entity_id
&& existing.try_get::<Option<String>, _>("field")?.as_deref() == field
&& existing.try_get::<String, _>("op_type")? == op_type
&& existing.try_get::<String, _>("payload")? == payload
&& existing
.try_get::<Option<String>, _>("base_version")?
.as_deref()
== base_version
&& existing.try_get::<String, _>("created_at")? == created_at;
if equal {
return Ok(());
}
return Err(CoreError::generation_conflict(format!(
"error recurrence-generation-conflict change_id={change_id}"
))
.into());
}
let client_id = get_meta(conn, "client_id")
.await?
.context("missing client id")?;
let local_seq = next_local_seq(conn).await?;
sqlx::query(
"INSERT INTO changes(change_id, client_id, local_seq, entity_type, entity_id, field,
op_type, payload, base_version, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
)
.bind(change_id)
.bind(client_id)
.bind(local_seq)
.bind(entity_type)
.bind(entity_id)
.bind(field)
.bind(op_type)
.bind(payload)
.bind(base_version)
.bind(created_at)
.execute(&mut *conn)
.await?;
Ok(())
}