use anyhow::Result;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use surreal_sync_core::{Change, Row, Value};
use uuid::Uuid;
pub trait ReconciliationPos:
Clone + Ord + Serialize + DeserializeOwned + Send + Sync + 'static
{
}
impl<T> ReconciliationPos for T where
T: Clone + Ord + Serialize + DeserializeOwned + Send + Sync + 'static
{
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TableSpec {
pub table: String,
pub pk_columns: Vec<String>,
}
impl TableSpec {
pub fn new(table: impl Into<String>, pk_columns: Vec<String>) -> Self {
Self {
table: table.into(),
pk_columns,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PkTuple(pub Vec<Value>);
impl PkTuple {
pub fn new(values: Vec<Value>) -> Self {
Self(values)
}
pub fn key(&self) -> String {
let canonical: Vec<Value> = self.0.iter().map(canonicalize_pk_value).collect();
serde_json::to_string(&canonical).unwrap_or_default()
}
pub fn single_uuid(&self) -> Option<Uuid> {
match self.0.as_slice() {
[Value::Uuid(u)] => Some(*u),
_ => None,
}
}
pub fn from_row(row: &Row, pk_columns: &[String]) -> Result<Self> {
let mut values = Vec::with_capacity(pk_columns.len());
for col in pk_columns {
if let Some(v) = row.fields.get(col) {
values.push(canonicalize_pk_value(v));
} else if pk_columns.len() == 1 {
values.push(canonicalize_pk_value(&row.id));
} else {
anyhow::bail!(
"primary key column '{col}' not found in row for table '{}'",
row.table
);
}
}
Ok(Self(values))
}
}
fn canonicalize_pk_value(value: &Value) -> Value {
match value {
Value::Int8 { value, .. } => Value::Int64(*value as i64),
Value::Int16(v) => Value::Int64(*v as i64),
Value::Int32(v) => Value::Int64(*v as i64),
Value::Int64(v) => Value::Int64(*v),
Value::Uuid(u) => Value::Uuid(*u),
Value::Char { value, .. } => Value::Text(value.clone()),
Value::VarChar { value, .. } => Value::Text(value.clone()),
Value::Text(s) => Value::Text(s.clone()),
other => other.clone(),
}
}
#[cfg(test)]
mod pk_key_tests {
use super::*;
#[test]
fn key_equates_int_widths() {
let i32 = PkTuple::new(vec![Value::Int32(42)]);
let i64 = PkTuple::new(vec![Value::Int64(42)]);
assert_eq!(i32.key(), i64.key());
}
#[test]
fn key_equates_string_kinds() {
let text = PkTuple::new(vec![Value::Text("acct-001".into())]);
let varchar = PkTuple::new(vec![Value::VarChar {
value: "acct-001".into(),
length: 32,
}]);
let char_v = PkTuple::new(vec![Value::Char {
value: "acct-001".into(),
length: 32,
}]);
assert_eq!(text.key(), varchar.key());
assert_eq!(text.key(), char_v.key());
}
#[test]
fn from_row_canonicalizes_field_pk() {
let mut fields = std::collections::HashMap::new();
fields.insert("id".to_string(), Value::Int32(7));
let row = Row::new("users", 0, Value::Int32(7), fields);
let pk = PkTuple::from_row(&row, &["id".to_string()]).unwrap();
assert_eq!(pk.0, vec![Value::Int64(7)]);
assert_eq!(pk.key(), PkTuple::new(vec![Value::Int64(7)]).key());
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum WatermarkKind {
Low,
High,
}
#[derive(Debug, Clone)]
pub struct ReconciliationEvent<P: ReconciliationPos> {
pub position: P,
pub table: String,
pub pk: PkTuple,
pub change: Change,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SnapshotSignal {
pub id: String,
pub tables: Vec<String>,
}