use crate::schema::Schema;
use crate::types::Type;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use thiserror::Error;
use uuid::Uuid;
#[derive(Debug, Error, Clone)]
#[error(
"Type-value mismatch: expected {expected_value} for type {sync_type:?}, got {actual_value}"
)]
pub struct TypedValueError {
pub sync_type: Type,
pub expected_value: String,
pub actual_value: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", content = "value")]
pub enum Value {
Bool(bool),
Int8 {
value: i8,
width: u8,
},
Int16(i16),
Int32(i32),
Int64(i64),
Float32(f32),
Float64(f64),
Decimal {
value: String,
precision: u8,
scale: u8,
},
Char {
value: String,
length: u16,
},
VarChar {
value: String,
length: u16,
},
Text(String),
Blob(Vec<u8>),
Bytes(Vec<u8>),
Date(DateTime<Utc>),
Time(DateTime<Utc>),
LocalDateTime(DateTime<Utc>),
LocalDateTimeNano(DateTime<Utc>),
ZonedDateTime(DateTime<Utc>),
TimeTz(String),
Uuid(Uuid),
Ulid(ulid::Ulid),
Json(Box<serde_json::Value>),
Jsonb(Box<serde_json::Value>),
Array {
elements: Vec<Value>,
element_type: Box<Type>,
},
Set {
elements: Vec<String>,
allowed_values: Vec<String>,
},
Enum {
value: String,
allowed_values: Vec<String>,
},
Geometry {
data: GeometryData,
geometry_type: crate::types::GeometryType,
},
Duration(std::time::Duration),
Thing {
table: String,
id: Box<Value>,
},
Object(HashMap<String, Value>),
Null,
ZeroTemporal {
intended_type: Type,
source: Option<String>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ZeroTemporalPolicy {
#[default]
None,
Null,
String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GeometryData(pub serde_json::Value);
impl Value {
pub fn tinyint(value: i8, width: u8) -> Self {
Self::Int8 { value, width }
}
pub fn decimal(value: impl Into<String>, precision: u8, scale: u8) -> Self {
Self::Decimal {
value: value.into(),
precision,
scale,
}
}
pub fn char(value: impl Into<String>, length: u16) -> Self {
Self::Char {
value: value.into(),
length,
}
}
pub fn varchar(value: impl Into<String>, length: u16) -> Self {
Self::VarChar {
value: value.into(),
length,
}
}
pub fn array(elements: Vec<Value>, element_type: Type) -> Self {
Self::Array {
elements,
element_type: Box::new(element_type),
}
}
pub fn set(elements: Vec<String>, allowed_values: Vec<String>) -> Self {
Self::Set {
elements,
allowed_values,
}
}
pub fn enum_value(value: impl Into<String>, allowed_values: Vec<String>) -> Self {
Self::Enum {
value: value.into(),
allowed_values,
}
}
pub fn geometry_geojson(
data: serde_json::Value,
geometry_type: crate::types::GeometryType,
) -> Self {
Self::Geometry {
data: GeometryData(data),
geometry_type,
}
}
pub fn json(value: serde_json::Value) -> Self {
Self::Json(Box::new(value))
}
pub fn jsonb(value: serde_json::Value) -> Self {
Self::Jsonb(Box::new(value))
}
pub fn zero_temporal(intended_type: Type, source: Option<String>) -> Self {
Self::ZeroTemporal {
intended_type,
source,
}
}
pub fn canonical_zero_literal(intended_type: &Type) -> &'static str {
match intended_type {
Type::Date => "0000-00-00",
Type::Time | Type::TimeTz => "00:00:00",
Type::LocalDateTime | Type::LocalDateTimeNano | Type::ZonedDateTime => {
"0000-00-00 00:00:00"
}
_ => "0000-00-00 00:00:00",
}
}
pub fn is_mysql_zero_temporal_literal(s: &str) -> bool {
let s = s.trim();
if s == "0000-00-00" {
return true;
}
s.starts_with("0000-00-00 00:00:00")
}
pub fn is_mysql_zero_date_ymd(year: u16, month: u8, day: u8) -> bool {
year == 0 && month == 0 && day == 0
}
pub fn is_zero_temporal_type(intended_type: &Type) -> bool {
matches!(
intended_type,
Type::Date
| Type::Time
| Type::LocalDateTime
| Type::LocalDateTimeNano
| Type::ZonedDateTime
| Type::TimeTz
)
}
pub fn is_null(&self) -> bool {
matches!(self, Self::Null)
}
pub fn is_zero_temporal(&self) -> bool {
matches!(self, Self::ZeroTemporal { .. })
}
pub fn as_bool(&self) -> Option<bool> {
match self {
Self::Bool(b) => Some(*b),
_ => None,
}
}
pub fn as_i32(&self) -> Option<i32> {
match self {
Self::Int32(i) => Some(*i),
Self::Int8 { value, .. } => Some(*value as i32),
Self::Int16(i) => Some(*i as i32),
_ => None,
}
}
pub fn as_i64(&self) -> Option<i64> {
match self {
Self::Int64(i) => Some(*i),
Self::Int32(i) => Some(*i as i64),
Self::Int16(i) => Some(*i as i64),
Self::Int8 { value, .. } => Some(*value as i64),
_ => None,
}
}
pub fn as_f64(&self) -> Option<f64> {
match self {
Self::Float64(f) => Some(*f),
Self::Float32(f) => Some(*f as f64),
_ => None,
}
}
pub fn as_str(&self) -> Option<&str> {
match self {
Self::Text(s) => Some(s),
Self::Char { value, .. } => Some(value),
Self::VarChar { value, .. } => Some(value),
Self::Enum { value, .. } => Some(value),
_ => None,
}
}
pub fn as_bytes(&self) -> Option<&[u8]> {
match self {
Self::Bytes(b) => Some(b),
Self::Blob(b) => Some(b),
_ => None,
}
}
pub fn as_uuid(&self) -> Option<&Uuid> {
match self {
Self::Uuid(u) => Some(u),
_ => None,
}
}
pub fn as_ulid(&self) -> Option<&ulid::Ulid> {
match self {
Self::Ulid(u) => Some(u),
_ => None,
}
}
pub fn as_datetime(&self) -> Option<&DateTime<Utc>> {
match self {
Self::LocalDateTime(dt) => Some(dt),
Self::Date(dt) => Some(dt),
Self::Time(dt) => Some(dt),
Self::LocalDateTimeNano(dt) => Some(dt),
Self::ZonedDateTime(dt) => Some(dt),
_ => None,
}
}
pub fn as_array(&self) -> Option<&Vec<Value>> {
match self {
Self::Array { elements, .. } => Some(elements),
_ => None,
}
}
pub fn variant_name(&self) -> &'static str {
match self {
Self::Bool(_) => "Bool",
Self::Int8 { .. } => "Int8",
Self::Int16(_) => "Int16",
Self::Int32(_) => "Int32",
Self::Int64(_) => "Int64",
Self::Float32(_) => "Float32",
Self::Float64(_) => "Float64",
Self::Decimal { .. } => "Decimal",
Self::Char { .. } => "Char",
Self::VarChar { .. } => "VarChar",
Self::Text(_) => "Text",
Self::Blob(_) => "Blob",
Self::Bytes(_) => "Bytes",
Self::Date(_) => "Date",
Self::Time(_) => "Time",
Self::LocalDateTime(_) => "LocalDateTime",
Self::LocalDateTimeNano(_) => "LocalDateTimeNano",
Self::ZonedDateTime(_) => "ZonedDateTime",
Self::TimeTz(_) => "TimeTz",
Self::Uuid(_) => "Uuid",
Self::Ulid(_) => "Ulid",
Self::Json(_) => "Json",
Self::Jsonb(_) => "Jsonb",
Self::Array { .. } => "Array",
Self::Set { .. } => "Set",
Self::Enum { .. } => "Enum",
Self::Geometry { .. } => "Geometry",
Self::Duration(_) => "Duration",
Self::Thing { .. } => "Thing",
Self::Object(_) => "Object",
Self::Null => "Null",
Self::ZeroTemporal { .. } => "ZeroTemporal",
}
}
pub fn to_typed_value(self) -> TypedValue {
let sync_type = self.to_type();
TypedValue::new(sync_type, self)
}
pub fn to_type(&self) -> Type {
match self {
Self::Bool(_) => Type::Bool,
Self::Int8 { width, .. } => Type::Int8 { width: *width },
Self::Int16(_) => Type::Int16,
Self::Int32(_) => Type::Int32,
Self::Int64(_) => Type::Int64,
Self::Float32(_) => Type::Float32,
Self::Float64(_) => Type::Float64,
Self::Decimal {
precision, scale, ..
} => Type::Decimal {
precision: *precision,
scale: *scale,
},
Self::Char { length, .. } => Type::Char { length: *length },
Self::VarChar { length, .. } => Type::VarChar { length: *length },
Self::Text(_) => Type::Text,
Self::Blob(_) => Type::Blob,
Self::Bytes(_) => Type::Bytes,
Self::Date(_) => Type::Date,
Self::Time(_) => Type::Time,
Self::LocalDateTime(_) => Type::LocalDateTime,
Self::LocalDateTimeNano(_) => Type::LocalDateTimeNano,
Self::ZonedDateTime(_) => Type::ZonedDateTime,
Self::TimeTz(_) => Type::TimeTz,
Self::Uuid(_) => Type::Uuid,
Self::Ulid(_) => Type::Ulid,
Self::Json(_) => Type::Json,
Self::Jsonb(_) => Type::Jsonb,
Self::Array { element_type, .. } => Type::Array {
element_type: element_type.clone(),
},
Self::Set { allowed_values, .. } => Type::Set {
values: allowed_values.clone(),
},
Self::Enum { allowed_values, .. } => Type::Enum {
values: allowed_values.clone(),
},
Self::Geometry { geometry_type, .. } => Type::Geometry {
geometry_type: geometry_type.clone(),
},
Self::Duration(_) => Type::Duration,
Self::Thing { .. } => Type::Thing,
Self::Object(_) => Type::Object,
Self::Null => Type::Text,
Self::ZeroTemporal { intended_type, .. } => intended_type.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct TypedValue {
pub sync_type: Type,
pub value: Value,
}
impl TypedValue {
fn new(sync_type: Type, value: Value) -> Self {
Self { sync_type, value }
}
pub fn try_with_type(sync_type: Type, value: Value) -> Result<Self, TypedValueError> {
if matches!(value, Value::Null) {
return Ok(Self::new(sync_type, value));
}
if let Value::ZeroTemporal { intended_type, .. } = &value {
if sync_type == *intended_type && Value::is_zero_temporal_type(&sync_type) {
return Ok(Self::new(sync_type, value));
}
return Err(TypedValueError {
expected_value: Self::expected_value_description(&sync_type),
actual_value: value.variant_name().to_string(),
sync_type,
});
}
let is_valid = match (&sync_type, &value) {
(Type::Bool, Value::Bool(_)) => true,
(Type::Int8 { .. }, Value::Int8 { .. }) => true,
(Type::Int16, Value::Int16(_)) => true,
(Type::Int32, Value::Int32(_)) => true,
(Type::Int64, Value::Int64(_)) => true,
(Type::Float32, Value::Float32(_)) => true,
(Type::Float64, Value::Float64(_)) => true,
(Type::Decimal { .. }, Value::Decimal { .. }) => true,
(Type::Char { .. }, Value::Char { .. }) => true,
(Type::VarChar { .. }, Value::VarChar { .. }) => true,
(Type::Text, Value::Text(_)) => true,
(Type::Blob, Value::Blob(_)) => true,
(Type::Bytes, Value::Bytes(_)) => true,
(Type::Date, Value::Date(_)) => true,
(Type::Time, Value::Time(_)) => true,
(Type::LocalDateTime, Value::LocalDateTime(_)) => true,
(Type::LocalDateTimeNano, Value::LocalDateTimeNano(_)) => true,
(Type::ZonedDateTime, Value::ZonedDateTime(_)) => true,
(Type::TimeTz, Value::TimeTz(_)) => true,
(Type::Uuid, Value::Uuid(_)) => true,
(Type::Json, Value::Json(_)) => true,
(Type::Jsonb, Value::Jsonb(_)) => true,
(Type::Array { .. }, Value::Array { .. }) => true,
(Type::Set { .. }, Value::Set { .. }) => true,
(Type::Enum { .. }, Value::Enum { .. }) => true,
(Type::Geometry { .. }, Value::Geometry { .. }) => true,
(Type::Duration, Value::Duration(_)) => true,
(Type::Object, Value::Object(_)) => true,
_ => false,
};
if is_valid {
Ok(Self::new(sync_type, value))
} else {
Err(TypedValueError {
expected_value: Self::expected_value_description(&sync_type),
actual_value: value.variant_name().to_string(),
sync_type,
})
}
}
fn expected_value_description(sync_type: &Type) -> String {
match sync_type {
Type::Bool => "Bool".to_string(),
Type::Int8 { .. } => "Int8".to_string(),
Type::Int16 => "Int16".to_string(),
Type::Int32 => "Int32".to_string(),
Type::Int64 => "Int64".to_string(),
Type::Float32 => "Float32".to_string(),
Type::Float64 => "Float64".to_string(),
Type::Decimal { .. } => "Decimal".to_string(),
Type::Char { .. } => "Char".to_string(),
Type::VarChar { .. } => "VarChar".to_string(),
Type::Text => "Text".to_string(),
Type::Blob => "Blob".to_string(),
Type::Bytes => "Bytes".to_string(),
Type::Date => "Date".to_string(),
Type::Time => "Time".to_string(),
Type::LocalDateTime => "LocalDateTime".to_string(),
Type::LocalDateTimeNano => "LocalDateTimeNano".to_string(),
Type::ZonedDateTime => "ZonedDateTime".to_string(),
Type::TimeTz => "TimeTz".to_string(),
Type::Uuid => "Uuid".to_string(),
Type::Ulid => "Ulid".to_string(),
Type::Json => "Json".to_string(),
Type::Jsonb => "Jsonb".to_string(),
Type::Array { .. } => "Array".to_string(),
Type::Set { .. } => "Set".to_string(),
Type::Enum { .. } => "Enum".to_string(),
Type::Geometry { .. } => "Geometry".to_string(),
Type::Duration => "Duration".to_string(),
Type::Thing => "Thing".to_string(),
Type::Object => "Object".to_string(),
}
}
#[inline]
pub fn with_type_unchecked(sync_type: Type, value: Value) -> Self {
Self::new(sync_type, value)
}
pub fn bool(value: bool) -> Self {
Self::new(Type::Bool, Value::Bool(value))
}
pub fn int16(value: i16) -> Self {
Self::new(Type::Int16, Value::Int16(value))
}
pub fn int32(value: i32) -> Self {
Self::new(Type::Int32, Value::Int32(value))
}
pub fn int64(value: i64) -> Self {
Self::new(Type::Int64, Value::Int64(value))
}
pub fn float64(value: f64) -> Self {
Self::new(Type::Float64, Value::Float64(value))
}
pub fn text(value: impl Into<String>) -> Self {
Self::new(Type::Text, Value::Text(value.into()))
}
pub fn bytes(value: Vec<u8>) -> Self {
Self::new(Type::Bytes, Value::Bytes(value))
}
pub fn uuid(value: Uuid) -> Self {
Self::new(Type::Uuid, Value::Uuid(value))
}
pub fn ulid(value: ulid::Ulid) -> Self {
Self::new(Type::Ulid, Value::Ulid(value))
}
pub fn datetime(value: DateTime<Utc>) -> Self {
Self::new(Type::LocalDateTime, Value::LocalDateTime(value))
}
pub fn float32(value: f32) -> Self {
Self::new(Type::Float32, Value::Float32(value))
}
pub fn null(sync_type: Type) -> Self {
Self::new(sync_type, Value::Null)
}
pub fn zero_temporal(intended_type: Type, source: Option<String>) -> Self {
assert!(
Value::is_zero_temporal_type(&intended_type),
"zero_temporal requires a temporal Type, got {intended_type:?}"
);
Self::new(
intended_type.clone(),
Value::ZeroTemporal {
intended_type,
source,
},
)
}
pub fn decimal(value: impl Into<String>, precision: u8, scale: u8) -> Self {
Self::new(
Type::Decimal { precision, scale },
Value::Decimal {
value: value.into(),
precision,
scale,
},
)
}
pub fn array(elements: Vec<Value>, element_type: Type) -> Self {
Self::new(
Type::Array {
element_type: Box::new(element_type.clone()),
},
Value::Array {
elements,
element_type: Box::new(element_type),
},
)
}
pub fn json(value: serde_json::Value) -> Self {
Self::new(Type::Json, Value::Json(Box::new(value)))
}
pub fn jsonb(value: serde_json::Value) -> Self {
Self::new(Type::Jsonb, Value::Jsonb(Box::new(value)))
}
pub fn int8(value: i8, width: u8) -> Self {
Self::new(Type::Int8 { width }, Value::Int8 { value, width })
}
pub fn char_type(value: impl Into<String>, length: u16) -> Self {
Self::new(
Type::Char { length },
Value::Char {
value: value.into(),
length,
},
)
}
pub fn varchar(value: impl Into<String>, length: u16) -> Self {
Self::new(
Type::VarChar { length },
Value::VarChar {
value: value.into(),
length,
},
)
}
pub fn blob(value: Vec<u8>) -> Self {
Self::new(Type::Blob, Value::Blob(value))
}
pub fn date(value: DateTime<Utc>) -> Self {
Self::new(Type::Date, Value::Date(value))
}
pub fn time(value: DateTime<Utc>) -> Self {
Self::new(Type::Time, Value::Time(value))
}
pub fn timestamptz(value: DateTime<Utc>) -> Self {
Self::new(Type::ZonedDateTime, Value::ZonedDateTime(value))
}
pub fn datetime_nano(value: DateTime<Utc>) -> Self {
Self::new(Type::LocalDateTimeNano, Value::LocalDateTimeNano(value))
}
pub fn enum_type(value: impl Into<String>, variants: Vec<String>) -> Self {
Self::new(
Type::Enum {
values: variants.clone(),
},
Value::Enum {
value: value.into(),
allowed_values: variants,
},
)
}
pub fn set(elements: Vec<String>, variants: Vec<String>) -> Self {
Self::new(
Type::Set {
values: variants.clone(),
},
Value::Set {
elements,
allowed_values: variants,
},
)
}
pub fn geometry_geojson(
value: serde_json::Value,
geometry_type: crate::types::GeometryType,
) -> Self {
Self::new(
Type::Geometry {
geometry_type: geometry_type.clone(),
},
Value::Geometry {
data: GeometryData(value),
geometry_type,
},
)
}
pub fn is_null(&self) -> bool {
self.value.is_null()
}
pub fn duration(value: std::time::Duration) -> Self {
Self::new(Type::Duration, Value::Duration(value))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Row {
pub table: String,
pub index: u64,
pub id: Value,
pub fields: HashMap<String, Value>,
}
impl Row {
pub fn new(
table: impl Into<String>,
index: u64,
id: Value,
fields: HashMap<String, Value>,
) -> Self {
Self {
table: table.into(),
index,
id,
fields,
}
}
pub fn builder(table: impl Into<String>, index: u64, id: Value) -> RowBuilder {
RowBuilder {
table: table.into(),
index,
id,
fields: HashMap::new(),
}
}
pub fn get_field(&self, name: &str) -> Option<&Value> {
self.fields.get(name)
}
pub fn field_count(&self) -> usize {
self.fields.len()
}
}
pub struct RowBuilder {
table: String,
index: u64,
id: Value,
fields: HashMap<String, Value>,
}
impl RowBuilder {
pub fn field(mut self, name: impl Into<String>, value: Value) -> Self {
self.fields.insert(name.into(), value);
self
}
pub fn build(self) -> Row {
Row {
table: self.table,
index: self.index,
id: self.id,
fields: self.fields,
}
}
}
pub struct RowConverter<'a> {
pub row: Row,
pub schema: &'a Schema,
}
impl<'a> RowConverter<'a> {
pub fn new(row: Row, schema: &'a Schema) -> Self {
Self { row, schema }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ChangeOp {
Create,
Update,
Delete,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Change {
pub operation: ChangeOp,
pub table: String,
pub id: Value,
pub fields: Option<HashMap<String, Value>>,
}
impl Change {
pub fn new(
operation: ChangeOp,
table: impl Into<String>,
id: Value,
fields: Option<HashMap<String, Value>>,
) -> Self {
Self {
operation,
table: table.into(),
id,
fields,
}
}
pub fn create(table: impl Into<String>, id: Value, fields: HashMap<String, Value>) -> Self {
Self::new(ChangeOp::Create, table, id, Some(fields))
}
pub fn update(table: impl Into<String>, id: Value, fields: HashMap<String, Value>) -> Self {
Self::new(ChangeOp::Update, table, id, Some(fields))
}
pub fn delete(table: impl Into<String>, id: Value) -> Self {
Self::new(ChangeOp::Delete, table, id, None)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Relation {
pub relation_type: String,
pub id: Value,
pub input: ThingRef,
pub output: ThingRef,
pub data: HashMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThingRef {
pub table: String,
pub id: Value,
}
impl ThingRef {
pub fn new(table: impl Into<String>, id: Value) -> Self {
Self {
table: table.into(),
id,
}
}
}
impl Relation {
pub fn new(
relation_type: impl Into<String>,
id: Value,
input: ThingRef,
output: ThingRef,
data: HashMap<String, Value>,
) -> Self {
Self {
relation_type: relation_type.into(),
id,
input,
output,
data,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generated_value_accessors() {
assert_eq!(Value::Bool(true).as_bool(), Some(true));
assert_eq!(Value::Int32(42).as_i32(), Some(42));
assert_eq!(Value::Int64(100).as_i64(), Some(100));
assert_eq!(Value::Float64(3.15).as_f64(), Some(3.15));
assert_eq!(Value::Text("test".to_string()).as_str(), Some("test"));
assert_eq!(Value::Int32(42).as_i64(), Some(42));
assert_eq!(Value::Bool(true).as_i32(), None);
}
#[test]
fn test_typed_value_constructors() {
let tv = TypedValue::bool(true);
assert_eq!(tv.sync_type, Type::Bool);
assert_eq!(tv.value, Value::Bool(true));
let tv = TypedValue::int32(42);
assert_eq!(tv.sync_type, Type::Int32);
assert_eq!(tv.value, Value::Int32(42));
}
#[test]
fn test_internal_row_builder() {
let row = Row::builder("users", 0, Value::Int64(1))
.field("name", Value::Text("Alice".to_string()))
.field("age", Value::Int32(30))
.build();
assert_eq!(row.table, "users");
assert_eq!(row.index, 0);
assert_eq!(row.id, Value::Int64(1));
assert_eq!(row.field_count(), 2);
assert_eq!(
row.get_field("name"),
Some(&Value::Text("Alice".to_string()))
);
assert_eq!(row.get_field("age"), Some(&Value::Int32(30)));
}
#[test]
fn test_try_with_type_valid_combinations() {
assert!(TypedValue::try_with_type(Type::Bool, Value::Bool(true)).is_ok());
assert!(TypedValue::try_with_type(Type::Int32, Value::Int32(42)).is_ok());
assert!(TypedValue::try_with_type(Type::Int64, Value::Int64(100)).is_ok());
assert!(TypedValue::try_with_type(Type::Text, Value::Text("hello".to_string())).is_ok());
assert!(TypedValue::try_with_type(
Type::LocalDateTime,
Value::LocalDateTime(chrono::Utc::now())
)
.is_ok());
assert!(TypedValue::try_with_type(Type::Date, Value::Date(chrono::Utc::now())).is_ok());
assert!(TypedValue::try_with_type(Type::Bool, Value::Null).is_ok());
assert!(TypedValue::try_with_type(Type::Int32, Value::Null).is_ok());
assert!(TypedValue::try_with_type(Type::Text, Value::Null).is_ok());
assert!(TypedValue::try_with_type(
Type::Date,
Value::zero_temporal(Type::Date, Some("0000-00-00".into()))
)
.is_ok());
assert!(TypedValue::try_with_type(
Type::LocalDateTime,
Value::zero_temporal(Type::LocalDateTime, Some("0000-00-00 00:00:00".into()))
)
.is_ok());
assert!(TypedValue::try_with_type(
Type::Date,
Value::zero_temporal(Type::LocalDateTime, None)
)
.is_err());
assert!(
TypedValue::try_with_type(Type::Text, Value::zero_temporal(Type::Text, None)).is_err()
);
assert!(TypedValue::try_with_type(
Type::Json,
Value::Json(Box::new(serde_json::Value::Bool(true)))
)
.is_ok());
}
#[test]
fn test_try_with_type_invalid_combinations() {
let err = TypedValue::try_with_type(Type::Bool, Value::Int32(1)).unwrap_err();
assert_eq!(err.expected_value, "Bool");
assert_eq!(err.actual_value, "Int32");
let err =
TypedValue::try_with_type(Type::Int32, Value::Text("42".to_string())).unwrap_err();
assert_eq!(err.expected_value, "Int32");
assert_eq!(err.actual_value, "Text");
let err = TypedValue::try_with_type(Type::Int32, Value::Int64(42)).unwrap_err();
assert_eq!(err.expected_value, "Int32");
assert_eq!(err.actual_value, "Int64");
let err = TypedValue::try_with_type(Type::Int64, Value::Int32(42)).unwrap_err();
assert_eq!(err.expected_value, "Int64");
assert_eq!(err.actual_value, "Int32");
let err = TypedValue::try_with_type(Type::Text, Value::Int32(42)).unwrap_err();
assert_eq!(err.expected_value, "Text");
assert_eq!(err.actual_value, "Int32");
let err = TypedValue::try_with_type(Type::Uuid, Value::Text("not-a-uuid".to_string()))
.unwrap_err();
assert_eq!(err.expected_value, "Uuid");
assert_eq!(err.actual_value, "Text");
}
#[test]
fn test_try_with_type_error_message() {
let err =
TypedValue::try_with_type(Type::Bool, Value::Text("true".to_string())).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("Type-value mismatch"));
assert!(msg.contains("Bool"));
assert!(msg.contains("Text"));
}
#[test]
fn test_variant_name() {
assert_eq!(Value::Bool(true).variant_name(), "Bool");
assert_eq!(Value::Int32(42).variant_name(), "Int32");
assert_eq!(Value::Int64(100).variant_name(), "Int64");
assert_eq!(Value::Float64(1.5).variant_name(), "Float64");
assert_eq!(Value::Text("test".to_string()).variant_name(), "Text");
assert_eq!(Value::Bytes(vec![1, 2, 3]).variant_name(), "Bytes");
assert_eq!(Value::Null.variant_name(), "Null");
}
#[test]
fn test_to_typed_value_deterministic() {
let value = Value::Int32(42);
let typed = value.to_typed_value();
assert_eq!(typed.sync_type, Type::Int32);
let value = Value::Text("hello".to_string());
let typed = value.to_typed_value();
assert_eq!(typed.sync_type, Type::Text);
let value = Value::Int64(100);
let typed = value.to_typed_value();
assert_eq!(typed.sync_type, Type::Int64);
let value = Value::Float64(3.15);
let typed = value.to_typed_value();
assert_eq!(typed.sync_type, Type::Float64);
let value = Value::Float32(1.5);
let typed = value.to_typed_value();
assert_eq!(typed.sync_type, Type::Float32);
let value = Value::Int16(100);
let typed = value.to_typed_value();
assert_eq!(typed.sync_type, Type::Int16);
let value = Value::Int8 { value: 1, width: 1 };
let typed = value.to_typed_value();
assert!(matches!(typed.sync_type, Type::Int8 { width: 1 }));
}
#[test]
fn universal_row_serde_roundtrip() {
let row = Row::builder("users", 7, Value::Int64(42))
.field("name", Value::Text("Alice".to_string()))
.field("active", Value::Bool(true))
.build();
let json = serde_json::to_string(&row).expect("serialize row");
let back: Row = serde_json::from_str(&json).expect("deserialize row");
assert_eq!(back, row);
}
#[test]
fn universal_change_serde_roundtrip_and_golden() {
let mut data = HashMap::new();
data.insert("name".to_string(), Value::Text("Bob".to_string()));
let change = Change::create("users", Value::Int64(9), data);
let json = serde_json::to_string(&change).expect("serialize change");
let back: Change = serde_json::from_str(&json).expect("deserialize change");
assert_eq!(back, change);
let golden = r#"{"operation":"Create","table":"users","id":{"type":"Int64","value":9},"fields":{"name":{"type":"Text","value":"Bob"}}}"#;
let from_golden: Change = serde_json::from_str(golden).expect("deserialize golden");
assert_eq!(from_golden, change);
let delete = Change::delete("users", Value::Int64(9));
let delete_json = serde_json::to_string(&delete).unwrap();
let delete_back: Change = serde_json::from_str(&delete_json).unwrap();
assert_eq!(delete_back, delete);
assert!(delete_back.fields.is_none());
}
}