Skip to main content

surreal_sync_core/
values.rs

1//! Value representations for the surreal-sync load testing framework.
2//!
3//! This module defines the intermediate value types used for data generation
4//! and type conversion between different database systems.
5
6use crate::schema::Schema;
7use crate::types::Type;
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use thiserror::Error;
12use uuid::Uuid;
13
14/// Error when creating a TypedValue with mismatched type and value.
15#[derive(Debug, Error, Clone)]
16#[error(
17    "Type-value mismatch: expected {expected_value} for type {sync_type:?}, got {actual_value}"
18)]
19pub struct TypedValueError {
20    /// The Type that was specified
21    pub sync_type: Type,
22    /// Description of the expected value kind
23    pub expected_value: String,
24    /// Description of the actual value kind
25    pub actual_value: String,
26}
27
28/// Universal value representation with 1:1 correspondence to `Type`.
29///
30/// Each variant of `Value` corresponds exactly to one variant of `Type`,
31/// enabling deterministic conversion via `to_typed_value()` without inference or fallback.
32///
33/// # Design Principles
34///
35/// 1. **Exact correspondence**: Every `Type` variant has exactly one matching `Value` variant
36/// 2. **No inference**: `to_typed_value()` is deterministic - no guessing or fallback
37/// 3. **Type metadata included**: Variants like `Char`, `VarChar`, `Decimal` include their type parameters
38/// 4. **Self-describing**: Each value knows its exact type without external context
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40#[serde(tag = "type", content = "value")]
41pub enum Value {
42    // === Boolean ===
43    /// Boolean value → `Type::Bool`
44    Bool(bool),
45
46    // === Integer types ===
47    /// Tiny integer with display width → `Type::Int8 { width }`
48    Int8 {
49        /// The integer value
50        value: i8,
51        /// Display width
52        width: u8,
53    },
54
55    /// 16-bit signed integer → `Type::Int16`
56    Int16(i16),
57
58    /// 32-bit signed integer → `Type::Int32`
59    Int32(i32),
60
61    /// 64-bit signed integer → `Type::Int64`
62    Int64(i64),
63
64    // === Floating point types ===
65    /// 32-bit IEEE 754 floating point → `Type::Float32`
66    Float32(f32),
67
68    /// 64-bit IEEE 754 floating point → `Type::Float64`
69    Float64(f64),
70
71    // === Exact numeric ===
72    /// Decimal value with precision/scale → `Type::Decimal { precision, scale }`
73    Decimal {
74        /// String representation of the decimal value
75        value: String,
76        /// Total number of digits
77        precision: u8,
78        /// Number of digits after decimal point
79        scale: u8,
80    },
81
82    // === String types ===
83    /// Fixed-length character string → `Type::Char { length }`
84    Char {
85        /// The string value
86        value: String,
87        /// Maximum length
88        length: u16,
89    },
90
91    /// Variable-length character string → `Type::VarChar { length }`
92    VarChar {
93        /// The string value
94        value: String,
95        /// Maximum length
96        length: u16,
97    },
98
99    /// Unlimited text → `Type::Text`
100    Text(String),
101
102    // === Binary types ===
103    /// Binary large object → `Type::Blob`
104    Blob(Vec<u8>),
105
106    /// Binary data → `Type::Bytes`
107    Bytes(Vec<u8>),
108
109    // === Temporal types ===
110    /// Date only (YYYY-MM-DD) → `Type::Date`
111    Date(DateTime<Utc>),
112
113    /// Time only (HH:MM:SS) → `Type::Time`
114    Time(DateTime<Utc>),
115
116    /// Timestamp without timezone (microsecond precision) → `Type::LocalDateTime`
117    LocalDateTime(DateTime<Utc>),
118
119    /// Timestamp with nanosecond precision → `Type::LocalDateTimeNano`
120    LocalDateTimeNano(DateTime<Utc>),
121
122    /// Timestamp with timezone → `Type::ZonedDateTime`
123    ZonedDateTime(DateTime<Utc>),
124
125    /// Time with timezone (stored as string to preserve original format)
126    /// → `Type::TimeTz`
127    ///
128    /// Note: We intentionally use String instead of DateTime because time and datetime
129    /// are fundamentally different types. DateTime implies a specific point in time,
130    /// while time with timezone represents a daily recurring time in a specific timezone.
131    /// Using DateTime to represent time would misrepresent the semantics.
132    TimeTz(String),
133
134    // === Special types ===
135    /// UUID (128-bit) → `Type::Uuid`
136    Uuid(Uuid),
137
138    /// ULID (128-bit sortable identifier) → `Type::Ulid`
139    Ulid(ulid::Ulid),
140
141    /// JSON document → `Type::Json`
142    Json(Box<serde_json::Value>),
143
144    /// Binary JSON (PostgreSQL JSONB) → `Type::Jsonb`
145    Jsonb(Box<serde_json::Value>),
146
147    // === Collection types ===
148    /// Array of a specific type → `Type::Array { element_type }`
149    Array {
150        /// The array elements
151        elements: Vec<Value>,
152        /// Element type
153        element_type: Box<Type>,
154    },
155
156    /// MySQL SET type → `Type::Set { values }`
157    Set {
158        /// Selected values from the set
159        elements: Vec<String>,
160        /// Allowed values in the set definition
161        allowed_values: Vec<String>,
162    },
163
164    // === Enumeration ===
165    /// Enumeration type → `Type::Enum { values }`
166    Enum {
167        /// The selected enum value
168        value: String,
169        /// Allowed enum values
170        allowed_values: Vec<String>,
171    },
172
173    // === Spatial ===
174    /// Spatial/geometry type → `Type::Geometry { geometry_type }`
175    Geometry {
176        /// Geometry data (WKB bytes or GeoJSON object)
177        data: GeometryData,
178        /// Specific geometry variant
179        geometry_type: crate::types::GeometryType,
180    },
181
182    /// Duration type → `Type::Duration`
183    Duration(std::time::Duration),
184
185    /// Record reference/link (e.g., SurrealDB Thing) → `Type::Thing`
186    Thing {
187        /// The target table/collection name
188        table: String,
189        /// The record ID (can be string, int, uuid, etc.)
190        id: Box<Value>,
191    },
192
193    /// Nested object/document (e.g., MongoDB embedded documents, SurrealDB objects)
194    /// → `Type::Object`
195    ///
196    /// This differs from Json/Jsonb which are serialized JSON storage types.
197    /// Object represents a structured nested document with typed fields.
198    Object(HashMap<String, Value>),
199
200    /// Null value (can be any nullable type)
201    Null,
202
203    /// Source emitted a temporal that is not a valid calendar/chrono value
204    /// (e.g. MySQL/MariaDB zero date `0000-00-00`).
205    ///
206    /// Distinct from [`Value::Null`]: SQL NULL stays `Null`; MySQL-style
207    /// zero dates/timestamps use this sentinel so transforms retain the intended
208    /// column type before the SurrealDB sink maps it.
209    ZeroTemporal {
210        /// Intended column type (`Date`, `Time`, `LocalDateTime`, `ZonedDateTime`, …)
211        intended_type: Type,
212        /// Optional source literal for transforms/debugging, e.g. `"0000-00-00 00:00:00"`
213        source: Option<String>,
214    },
215}
216
217/// How the SurrealDB sink represents [`Value::ZeroTemporal`].
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
219#[serde(rename_all = "snake_case")]
220pub enum ZeroTemporalPolicy {
221    /// SurrealDB `NONE` (undefined). Default — safe for typed datetime fields.
222    #[default]
223    None,
224    /// SurrealDB `NULL`.
225    Null,
226    /// Canonical MySQL-style zero literal string (e.g. `"0000-00-00"`).
227    String,
228}
229
230/// Geometry data representation.
231///
232/// Currently only supports GeoJSON format. Native geometry types
233/// (Point, LineString, Polygon, etc.) may be added in the future.
234#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
235pub struct GeometryData(pub serde_json::Value);
236
237impl Value {
238    // === Factory methods ===
239
240    /// Create a TinyInt value.
241    pub fn tinyint(value: i8, width: u8) -> Self {
242        Self::Int8 { value, width }
243    }
244
245    /// Create a Decimal value.
246    pub fn decimal(value: impl Into<String>, precision: u8, scale: u8) -> Self {
247        Self::Decimal {
248            value: value.into(),
249            precision,
250            scale,
251        }
252    }
253
254    /// Create a Char value.
255    pub fn char(value: impl Into<String>, length: u16) -> Self {
256        Self::Char {
257            value: value.into(),
258            length,
259        }
260    }
261
262    /// Create a VarChar value.
263    pub fn varchar(value: impl Into<String>, length: u16) -> Self {
264        Self::VarChar {
265            value: value.into(),
266            length,
267        }
268    }
269
270    /// Create an Array value.
271    pub fn array(elements: Vec<Value>, element_type: Type) -> Self {
272        Self::Array {
273            elements,
274            element_type: Box::new(element_type),
275        }
276    }
277
278    /// Create a Set value.
279    pub fn set(elements: Vec<String>, allowed_values: Vec<String>) -> Self {
280        Self::Set {
281            elements,
282            allowed_values,
283        }
284    }
285
286    /// Create an Enum value.
287    pub fn enum_value(value: impl Into<String>, allowed_values: Vec<String>) -> Self {
288        Self::Enum {
289            value: value.into(),
290            allowed_values,
291        }
292    }
293
294    /// Create a Geometry value from GeoJSON.
295    pub fn geometry_geojson(
296        data: serde_json::Value,
297        geometry_type: crate::types::GeometryType,
298    ) -> Self {
299        Self::Geometry {
300            data: GeometryData(data),
301            geometry_type,
302        }
303    }
304
305    /// Create a JSON value.
306    pub fn json(value: serde_json::Value) -> Self {
307        Self::Json(Box::new(value))
308    }
309
310    /// Create a JSONB value.
311    pub fn jsonb(value: serde_json::Value) -> Self {
312        Self::Jsonb(Box::new(value))
313    }
314
315    /// Create a zero-temporal sentinel with the intended column type.
316    pub fn zero_temporal(intended_type: Type, source: Option<String>) -> Self {
317        Self::ZeroTemporal {
318            intended_type,
319            source,
320        }
321    }
322
323    /// Canonical MySQL-style zero literal for an intended temporal type.
324    pub fn canonical_zero_literal(intended_type: &Type) -> &'static str {
325        match intended_type {
326            Type::Date => "0000-00-00",
327            Type::Time | Type::TimeTz => "00:00:00",
328            Type::LocalDateTime | Type::LocalDateTimeNano | Type::ZonedDateTime => {
329                "0000-00-00 00:00:00"
330            }
331            _ => "0000-00-00 00:00:00",
332        }
333    }
334
335    /// Whether `s` is a MySQL/MariaDB zero date or zero datetime literal.
336    ///
337    /// Matches `0000-00-00` and `0000-00-00 00:00:00` with optional fractional seconds.
338    pub fn is_mysql_zero_temporal_literal(s: &str) -> bool {
339        let s = s.trim();
340        if s == "0000-00-00" {
341            return true;
342        }
343        s.starts_with("0000-00-00 00:00:00")
344    }
345
346    /// Whether year/month/day are the MySQL zero-date triple `(0, 0, 0)`.
347    pub fn is_mysql_zero_date_ymd(year: u16, month: u8, day: u8) -> bool {
348        year == 0 && month == 0 && day == 0
349    }
350
351    /// Whether `intended_type` may be used with [`Value::ZeroTemporal`].
352    pub fn is_zero_temporal_type(intended_type: &Type) -> bool {
353        matches!(
354            intended_type,
355            Type::Date
356                | Type::Time
357                | Type::LocalDateTime
358                | Type::LocalDateTimeNano
359                | Type::ZonedDateTime
360                | Type::TimeTz
361        )
362    }
363
364    // === Predicates ===
365
366    /// Check if this value is null.
367    pub fn is_null(&self) -> bool {
368        matches!(self, Self::Null)
369    }
370
371    /// Check if this value is a zero-temporal sentinel.
372    pub fn is_zero_temporal(&self) -> bool {
373        matches!(self, Self::ZeroTemporal { .. })
374    }
375
376    // === Accessors ===
377
378    /// Try to get this value as a boolean.
379    pub fn as_bool(&self) -> Option<bool> {
380        match self {
381            Self::Bool(b) => Some(*b),
382            _ => None,
383        }
384    }
385
386    /// Try to get this value as an i32.
387    pub fn as_i32(&self) -> Option<i32> {
388        match self {
389            Self::Int32(i) => Some(*i),
390            Self::Int8 { value, .. } => Some(*value as i32),
391            Self::Int16(i) => Some(*i as i32),
392            _ => None,
393        }
394    }
395
396    /// Try to get this value as an i64.
397    pub fn as_i64(&self) -> Option<i64> {
398        match self {
399            Self::Int64(i) => Some(*i),
400            Self::Int32(i) => Some(*i as i64),
401            Self::Int16(i) => Some(*i as i64),
402            Self::Int8 { value, .. } => Some(*value as i64),
403            _ => None,
404        }
405    }
406
407    /// Try to get this value as an f64.
408    pub fn as_f64(&self) -> Option<f64> {
409        match self {
410            Self::Float64(f) => Some(*f),
411            Self::Float32(f) => Some(*f as f64),
412            _ => None,
413        }
414    }
415
416    /// Try to get this value as a string reference.
417    pub fn as_str(&self) -> Option<&str> {
418        match self {
419            Self::Text(s) => Some(s),
420            Self::Char { value, .. } => Some(value),
421            Self::VarChar { value, .. } => Some(value),
422            Self::Enum { value, .. } => Some(value),
423            _ => None,
424        }
425    }
426
427    /// Try to get this value as a byte slice.
428    pub fn as_bytes(&self) -> Option<&[u8]> {
429        match self {
430            Self::Bytes(b) => Some(b),
431            Self::Blob(b) => Some(b),
432            _ => None,
433        }
434    }
435
436    /// Try to get this value as a UUID.
437    pub fn as_uuid(&self) -> Option<&Uuid> {
438        match self {
439            Self::Uuid(u) => Some(u),
440            _ => None,
441        }
442    }
443
444    /// Try to get this value as a ULID.
445    pub fn as_ulid(&self) -> Option<&ulid::Ulid> {
446        match self {
447            Self::Ulid(u) => Some(u),
448            _ => None,
449        }
450    }
451
452    /// Try to get this value as a DateTime.
453    pub fn as_datetime(&self) -> Option<&DateTime<Utc>> {
454        match self {
455            Self::LocalDateTime(dt) => Some(dt),
456            Self::Date(dt) => Some(dt),
457            Self::Time(dt) => Some(dt),
458            Self::LocalDateTimeNano(dt) => Some(dt),
459            Self::ZonedDateTime(dt) => Some(dt),
460            _ => None,
461        }
462    }
463
464    /// Try to get this value as an array of elements.
465    pub fn as_array(&self) -> Option<&Vec<Value>> {
466        match self {
467            Self::Array { elements, .. } => Some(elements),
468            _ => None,
469        }
470    }
471
472    /// Get a human-readable description of this value's variant.
473    pub fn variant_name(&self) -> &'static str {
474        match self {
475            Self::Bool(_) => "Bool",
476            Self::Int8 { .. } => "Int8",
477            Self::Int16(_) => "Int16",
478            Self::Int32(_) => "Int32",
479            Self::Int64(_) => "Int64",
480            Self::Float32(_) => "Float32",
481            Self::Float64(_) => "Float64",
482            Self::Decimal { .. } => "Decimal",
483            Self::Char { .. } => "Char",
484            Self::VarChar { .. } => "VarChar",
485            Self::Text(_) => "Text",
486            Self::Blob(_) => "Blob",
487            Self::Bytes(_) => "Bytes",
488            Self::Date(_) => "Date",
489            Self::Time(_) => "Time",
490            Self::LocalDateTime(_) => "LocalDateTime",
491            Self::LocalDateTimeNano(_) => "LocalDateTimeNano",
492            Self::ZonedDateTime(_) => "ZonedDateTime",
493            Self::TimeTz(_) => "TimeTz",
494            Self::Uuid(_) => "Uuid",
495            Self::Ulid(_) => "Ulid",
496            Self::Json(_) => "Json",
497            Self::Jsonb(_) => "Jsonb",
498            Self::Array { .. } => "Array",
499            Self::Set { .. } => "Set",
500            Self::Enum { .. } => "Enum",
501            Self::Geometry { .. } => "Geometry",
502            Self::Duration(_) => "Duration",
503            Self::Thing { .. } => "Thing",
504            Self::Object(_) => "Object",
505            Self::Null => "Null",
506            Self::ZeroTemporal { .. } => "ZeroTemporal",
507        }
508    }
509
510    /// Convert this value to a TypedValue deterministically.
511    ///
512    /// Each `Value` variant maps to exactly one `Type` variant,
513    /// so there is no inference or fallback - the mapping is deterministic.
514    ///
515    /// # Example
516    ///
517    /// ```rust
518    /// use surreal_sync_core::{Value, Type};
519    ///
520    /// let value = Value::Int32(42);
521    /// let typed = value.to_typed_value();
522    /// assert!(matches!(typed.sync_type, Type::Int32));
523    ///
524    /// let value = Value::Text("hello".to_string());
525    /// let typed = value.to_typed_value();
526    /// assert!(matches!(typed.sync_type, Type::Text));
527    /// ```
528    pub fn to_typed_value(self) -> TypedValue {
529        let sync_type = self.to_type();
530        // Safe to use new() since we're deriving type from the value itself
531        TypedValue::new(sync_type, self)
532    }
533
534    /// Get the corresponding Type for this value.
535    ///
536    /// This is a deterministic 1:1 mapping - no inference or fallback.
537    pub fn to_type(&self) -> Type {
538        match self {
539            Self::Bool(_) => Type::Bool,
540            Self::Int8 { width, .. } => Type::Int8 { width: *width },
541            Self::Int16(_) => Type::Int16,
542            Self::Int32(_) => Type::Int32,
543            Self::Int64(_) => Type::Int64,
544            Self::Float32(_) => Type::Float32,
545            Self::Float64(_) => Type::Float64,
546            Self::Decimal {
547                precision, scale, ..
548            } => Type::Decimal {
549                precision: *precision,
550                scale: *scale,
551            },
552            Self::Char { length, .. } => Type::Char { length: *length },
553            Self::VarChar { length, .. } => Type::VarChar { length: *length },
554            Self::Text(_) => Type::Text,
555            Self::Blob(_) => Type::Blob,
556            Self::Bytes(_) => Type::Bytes,
557            Self::Date(_) => Type::Date,
558            Self::Time(_) => Type::Time,
559            Self::LocalDateTime(_) => Type::LocalDateTime,
560            Self::LocalDateTimeNano(_) => Type::LocalDateTimeNano,
561            Self::ZonedDateTime(_) => Type::ZonedDateTime,
562            Self::TimeTz(_) => Type::TimeTz,
563            Self::Uuid(_) => Type::Uuid,
564            Self::Ulid(_) => Type::Ulid,
565            Self::Json(_) => Type::Json,
566            Self::Jsonb(_) => Type::Jsonb,
567            Self::Array { element_type, .. } => Type::Array {
568                element_type: element_type.clone(),
569            },
570            Self::Set { allowed_values, .. } => Type::Set {
571                values: allowed_values.clone(),
572            },
573            Self::Enum { allowed_values, .. } => Type::Enum {
574                values: allowed_values.clone(),
575            },
576            Self::Geometry { geometry_type, .. } => Type::Geometry {
577                geometry_type: geometry_type.clone(),
578            },
579            Self::Duration(_) => Type::Duration,
580            Self::Thing { .. } => Type::Thing,
581            Self::Object(_) => Type::Object,
582            // Null doesn't have a single type - this is a special case
583            // We use Text as a placeholder, but callers should handle Null explicitly
584            Self::Null => Type::Text,
585            Self::ZeroTemporal { intended_type, .. } => intended_type.clone(),
586        }
587    }
588}
589
590/// Typed value with its Type for conversion.
591///
592/// `TypedValue` combines a `Value` with its corresponding `Type`,
593/// providing the type context needed for `From`/`Into` trait implementations
594/// in the database-specific type crates.
595#[derive(Debug, Clone)]
596pub struct TypedValue {
597    /// The extended type information
598    pub sync_type: Type,
599
600    /// The raw generated value
601    pub value: Value,
602}
603
604impl TypedValue {
605    /// Create a new typed value (internal use - prefer factory methods).
606    fn new(sync_type: Type, value: Value) -> Self {
607        Self { sync_type, value }
608    }
609
610    /// Create a typed value with a dynamically specified type, validating the combination.
611    ///
612    /// This validates that the type and value are compatible using strict 1:1 matching.
613    /// Returns an error if the combination is invalid (e.g., passing a Text value for a Bool type).
614    ///
615    /// This is useful when the type is determined at runtime (e.g., from schema).
616    /// For known types, prefer the specific factory methods like `bool()`, `int()`, etc.
617    ///
618    /// # Strict 1:1 Valid Combinations
619    ///
620    /// - `Null` is valid for any type
621    /// - `ZeroTemporal` is valid when `sync_type` equals `intended_type` and is temporal
622    /// - `Bool` type requires `Bool` value
623    /// - `TinyInt` type requires `TinyInt` value
624    /// - `SmallInt` type requires `SmallInt` value
625    /// - `Int` type requires `Int` value
626    /// - `BigInt` type requires `BigInt` value
627    /// - `Float` type requires `Float` value
628    /// - `Double` type requires `Double` value
629    /// - `Decimal` type requires `Decimal` value
630    /// - `Char` type requires `Char` value
631    /// - `VarChar` type requires `VarChar` value
632    /// - `Text` type requires `Text` value
633    /// - `Blob` type requires `Blob` value
634    /// - `Bytes` type requires `Bytes` value
635    /// - `Date` type requires `Date` value
636    /// - `Time` type requires `Time` value
637    /// - `DateTime` type requires `DateTime` value
638    /// - `DateTimeNano` type requires `DateTimeNano` value
639    /// - `TimestampTz` type requires `TimestampTz` value
640    /// - `Uuid` type requires `Uuid` value
641    /// - `Json` type requires `Json` value
642    /// - `Jsonb` type requires `Jsonb` value
643    /// - `Array` type requires `Array` value
644    /// - `Set` type requires `Set` value
645    /// - `Enum` type requires `Enum` value
646    /// - `Geometry` type requires `Geometry` value
647    pub fn try_with_type(sync_type: Type, value: Value) -> Result<Self, TypedValueError> {
648        // Null is always valid for any type
649        if matches!(value, Value::Null) {
650            return Ok(Self::new(sync_type, value));
651        }
652
653        // ZeroTemporal is valid when sync_type matches intended_type and is temporal
654        if let Value::ZeroTemporal { intended_type, .. } = &value {
655            if sync_type == *intended_type && Value::is_zero_temporal_type(&sync_type) {
656                return Ok(Self::new(sync_type, value));
657            }
658            return Err(TypedValueError {
659                expected_value: Self::expected_value_description(&sync_type),
660                actual_value: value.variant_name().to_string(),
661                sync_type,
662            });
663        }
664
665        // Strict 1:1 validation - each type requires its exact corresponding value variant
666        let is_valid = match (&sync_type, &value) {
667            // Boolean
668            (Type::Bool, Value::Bool(_)) => true,
669
670            // Integer types - strict matching
671            (Type::Int8 { .. }, Value::Int8 { .. }) => true,
672            (Type::Int16, Value::Int16(_)) => true,
673            (Type::Int32, Value::Int32(_)) => true,
674            (Type::Int64, Value::Int64(_)) => true,
675
676            // Floating point types - strict matching
677            (Type::Float32, Value::Float32(_)) => true,
678            (Type::Float64, Value::Float64(_)) => true,
679
680            // Decimal
681            (Type::Decimal { .. }, Value::Decimal { .. }) => true,
682
683            // String types - strict matching
684            (Type::Char { .. }, Value::Char { .. }) => true,
685            (Type::VarChar { .. }, Value::VarChar { .. }) => true,
686            (Type::Text, Value::Text(_)) => true,
687
688            // Binary types - strict matching
689            (Type::Blob, Value::Blob(_)) => true,
690            (Type::Bytes, Value::Bytes(_)) => true,
691
692            // Temporal types - strict matching
693            (Type::Date, Value::Date(_)) => true,
694            (Type::Time, Value::Time(_)) => true,
695            (Type::LocalDateTime, Value::LocalDateTime(_)) => true,
696            (Type::LocalDateTimeNano, Value::LocalDateTimeNano(_)) => true,
697            (Type::ZonedDateTime, Value::ZonedDateTime(_)) => true,
698            (Type::TimeTz, Value::TimeTz(_)) => true,
699
700            // UUID
701            (Type::Uuid, Value::Uuid(_)) => true,
702
703            // JSON types - strict matching
704            (Type::Json, Value::Json(_)) => true,
705            (Type::Jsonb, Value::Jsonb(_)) => true,
706
707            // Collection types - strict matching
708            (Type::Array { .. }, Value::Array { .. }) => true,
709            (Type::Set { .. }, Value::Set { .. }) => true,
710
711            // Enumeration
712            (Type::Enum { .. }, Value::Enum { .. }) => true,
713
714            // Geometry
715            (Type::Geometry { .. }, Value::Geometry { .. }) => true,
716
717            // Duration
718            (Type::Duration, Value::Duration(_)) => true,
719
720            // Object
721            (Type::Object, Value::Object(_)) => true,
722
723            // All other combinations are invalid
724            _ => false,
725        };
726
727        if is_valid {
728            Ok(Self::new(sync_type, value))
729        } else {
730            Err(TypedValueError {
731                expected_value: Self::expected_value_description(&sync_type),
732                actual_value: value.variant_name().to_string(),
733                sync_type,
734            })
735        }
736    }
737
738    /// Get the expected value description for a given type.
739    fn expected_value_description(sync_type: &Type) -> String {
740        match sync_type {
741            Type::Bool => "Bool".to_string(),
742            Type::Int8 { .. } => "Int8".to_string(),
743            Type::Int16 => "Int16".to_string(),
744            Type::Int32 => "Int32".to_string(),
745            Type::Int64 => "Int64".to_string(),
746            Type::Float32 => "Float32".to_string(),
747            Type::Float64 => "Float64".to_string(),
748            Type::Decimal { .. } => "Decimal".to_string(),
749            Type::Char { .. } => "Char".to_string(),
750            Type::VarChar { .. } => "VarChar".to_string(),
751            Type::Text => "Text".to_string(),
752            Type::Blob => "Blob".to_string(),
753            Type::Bytes => "Bytes".to_string(),
754            Type::Date => "Date".to_string(),
755            Type::Time => "Time".to_string(),
756            Type::LocalDateTime => "LocalDateTime".to_string(),
757            Type::LocalDateTimeNano => "LocalDateTimeNano".to_string(),
758            Type::ZonedDateTime => "ZonedDateTime".to_string(),
759            Type::TimeTz => "TimeTz".to_string(),
760            Type::Uuid => "Uuid".to_string(),
761            Type::Ulid => "Ulid".to_string(),
762            Type::Json => "Json".to_string(),
763            Type::Jsonb => "Jsonb".to_string(),
764            Type::Array { .. } => "Array".to_string(),
765            Type::Set { .. } => "Set".to_string(),
766            Type::Enum { .. } => "Enum".to_string(),
767            Type::Geometry { .. } => "Geometry".to_string(),
768            Type::Duration => "Duration".to_string(),
769            Type::Thing => "Thing".to_string(),
770            Type::Object => "Object".to_string(),
771        }
772    }
773
774    /// Create a typed value with a dynamically specified type (unchecked).
775    ///
776    /// **Warning**: This does not validate the type-value combination.
777    /// Prefer `try_with_type` when possible to catch mismatches early.
778    ///
779    /// This is useful when you're certain the combination is valid or when
780    /// performance is critical and validation overhead is unacceptable.
781    #[inline]
782    pub fn with_type_unchecked(sync_type: Type, value: Value) -> Self {
783        Self::new(sync_type, value)
784    }
785
786    /// Create a boolean typed value.
787    pub fn bool(value: bool) -> Self {
788        Self::new(Type::Bool, Value::Bool(value))
789    }
790
791    /// Create a smallint typed value.
792    pub fn int16(value: i16) -> Self {
793        Self::new(Type::Int16, Value::Int16(value))
794    }
795
796    /// Create an integer typed value.
797    pub fn int32(value: i32) -> Self {
798        Self::new(Type::Int32, Value::Int32(value))
799    }
800
801    /// Create a bigint typed value.
802    pub fn int64(value: i64) -> Self {
803        Self::new(Type::Int64, Value::Int64(value))
804    }
805
806    /// Create a double typed value.
807    pub fn float64(value: f64) -> Self {
808        Self::new(Type::Float64, Value::Float64(value))
809    }
810
811    /// Create a text typed value.
812    pub fn text(value: impl Into<String>) -> Self {
813        Self::new(Type::Text, Value::Text(value.into()))
814    }
815
816    /// Create a bytes typed value.
817    pub fn bytes(value: Vec<u8>) -> Self {
818        Self::new(Type::Bytes, Value::Bytes(value))
819    }
820
821    /// Create a UUID typed value.
822    pub fn uuid(value: Uuid) -> Self {
823        Self::new(Type::Uuid, Value::Uuid(value))
824    }
825
826    /// Create a ULID typed value.
827    pub fn ulid(value: ulid::Ulid) -> Self {
828        Self::new(Type::Ulid, Value::Ulid(value))
829    }
830
831    /// Create a datetime typed value.
832    pub fn datetime(value: DateTime<Utc>) -> Self {
833        Self::new(Type::LocalDateTime, Value::LocalDateTime(value))
834    }
835
836    /// Create a float typed value.
837    pub fn float32(value: f32) -> Self {
838        Self::new(Type::Float32, Value::Float32(value))
839    }
840
841    /// Create a null typed value with a specified type.
842    pub fn null(sync_type: Type) -> Self {
843        Self::new(sync_type, Value::Null)
844    }
845
846    /// Create a zero-temporal typed value with the intended column type.
847    ///
848    /// # Panics
849    ///
850    /// Panics if `intended_type` is not a temporal type allowed for zero temporals.
851    pub fn zero_temporal(intended_type: Type, source: Option<String>) -> Self {
852        assert!(
853            Value::is_zero_temporal_type(&intended_type),
854            "zero_temporal requires a temporal Type, got {intended_type:?}"
855        );
856        Self::new(
857            intended_type.clone(),
858            Value::ZeroTemporal {
859                intended_type,
860                source,
861            },
862        )
863    }
864
865    /// Create a decimal typed value.
866    pub fn decimal(value: impl Into<String>, precision: u8, scale: u8) -> Self {
867        Self::new(
868            Type::Decimal { precision, scale },
869            Value::Decimal {
870                value: value.into(),
871                precision,
872                scale,
873            },
874        )
875    }
876
877    /// Create an array typed value.
878    pub fn array(elements: Vec<Value>, element_type: Type) -> Self {
879        Self::new(
880            Type::Array {
881                element_type: Box::new(element_type.clone()),
882            },
883            Value::Array {
884                elements,
885                element_type: Box::new(element_type),
886            },
887        )
888    }
889
890    /// Create a JSON typed value from a serde_json::Value.
891    pub fn json(value: serde_json::Value) -> Self {
892        Self::new(Type::Json, Value::Json(Box::new(value)))
893    }
894
895    /// Create a JSONB typed value from a serde_json::Value.
896    pub fn jsonb(value: serde_json::Value) -> Self {
897        Self::new(Type::Jsonb, Value::Jsonb(Box::new(value)))
898    }
899
900    /// Create a TINYINT typed value with optional width.
901    pub fn int8(value: i8, width: u8) -> Self {
902        Self::new(Type::Int8 { width }, Value::Int8 { value, width })
903    }
904
905    /// Create a CHAR typed value with specified length.
906    pub fn char_type(value: impl Into<String>, length: u16) -> Self {
907        Self::new(
908            Type::Char { length },
909            Value::Char {
910                value: value.into(),
911                length,
912            },
913        )
914    }
915
916    /// Create a VARCHAR typed value with specified length.
917    pub fn varchar(value: impl Into<String>, length: u16) -> Self {
918        Self::new(
919            Type::VarChar { length },
920            Value::VarChar {
921                value: value.into(),
922                length,
923            },
924        )
925    }
926
927    /// Create a BLOB typed value.
928    pub fn blob(value: Vec<u8>) -> Self {
929        Self::new(Type::Blob, Value::Blob(value))
930    }
931
932    /// Create a DATE typed value from a DateTime.
933    pub fn date(value: DateTime<Utc>) -> Self {
934        Self::new(Type::Date, Value::Date(value))
935    }
936
937    /// Create a TIME typed value from a DateTime.
938    pub fn time(value: DateTime<Utc>) -> Self {
939        Self::new(Type::Time, Value::Time(value))
940    }
941
942    /// Create a TIMESTAMPTZ typed value.
943    pub fn timestamptz(value: DateTime<Utc>) -> Self {
944        Self::new(Type::ZonedDateTime, Value::ZonedDateTime(value))
945    }
946
947    /// Create a DATETIME with nanosecond precision typed value.
948    pub fn datetime_nano(value: DateTime<Utc>) -> Self {
949        Self::new(Type::LocalDateTimeNano, Value::LocalDateTimeNano(value))
950    }
951
952    /// Create an ENUM typed value.
953    pub fn enum_type(value: impl Into<String>, variants: Vec<String>) -> Self {
954        Self::new(
955            Type::Enum {
956                values: variants.clone(),
957            },
958            Value::Enum {
959                value: value.into(),
960                allowed_values: variants,
961            },
962        )
963    }
964
965    /// Create a SET typed value.
966    pub fn set(elements: Vec<String>, variants: Vec<String>) -> Self {
967        Self::new(
968            Type::Set {
969                values: variants.clone(),
970            },
971            Value::Set {
972                elements,
973                allowed_values: variants,
974            },
975        )
976    }
977
978    /// Create a GEOMETRY typed value from a GeoJSON object.
979    pub fn geometry_geojson(
980        value: serde_json::Value,
981        geometry_type: crate::types::GeometryType,
982    ) -> Self {
983        Self::new(
984            Type::Geometry {
985                geometry_type: geometry_type.clone(),
986            },
987            Value::Geometry {
988                data: GeometryData(value),
989                geometry_type,
990            },
991        )
992    }
993
994    /// Check if this typed value is null.
995    pub fn is_null(&self) -> bool {
996        self.value.is_null()
997    }
998
999    /// Create a DURATION typed value.
1000    pub fn duration(value: std::time::Duration) -> Self {
1001        Self::new(Type::Duration, Value::Duration(value))
1002    }
1003}
1004
1005/// Internal row representation - the intermediate format.
1006///
1007/// `Row` represents a single row of data in the intermediate format,
1008/// produced by the data generator and consumed by both source populators
1009/// and the streaming verifier.
1010#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1011pub struct Row {
1012    /// Table name
1013    pub table: String,
1014
1015    /// Row index (for incremental support and reproducibility)
1016    pub index: u64,
1017
1018    /// Primary key value
1019    pub id: Value,
1020
1021    /// Field values (column name -> value)
1022    pub fields: HashMap<String, Value>,
1023}
1024
1025impl Row {
1026    /// Create a new internal row.
1027    pub fn new(
1028        table: impl Into<String>,
1029        index: u64,
1030        id: Value,
1031        fields: HashMap<String, Value>,
1032    ) -> Self {
1033        Self {
1034            table: table.into(),
1035            index,
1036            id,
1037            fields,
1038        }
1039    }
1040
1041    /// Create a new internal row with a builder pattern.
1042    pub fn builder(table: impl Into<String>, index: u64, id: Value) -> RowBuilder {
1043        RowBuilder {
1044            table: table.into(),
1045            index,
1046            id,
1047            fields: HashMap::new(),
1048        }
1049    }
1050
1051    /// Get a field value by name.
1052    pub fn get_field(&self, name: &str) -> Option<&Value> {
1053        self.fields.get(name)
1054    }
1055
1056    /// Get the number of fields (excluding the id).
1057    pub fn field_count(&self) -> usize {
1058        self.fields.len()
1059    }
1060}
1061
1062/// Builder for `Row`.
1063pub struct RowBuilder {
1064    table: String,
1065    index: u64,
1066    id: Value,
1067    fields: HashMap<String, Value>,
1068}
1069
1070impl RowBuilder {
1071    /// Add a field to the row.
1072    pub fn field(mut self, name: impl Into<String>, value: Value) -> Self {
1073        self.fields.insert(name.into(), value);
1074        self
1075    }
1076
1077    /// Build the internal row.
1078    pub fn build(self) -> Row {
1079        Row {
1080            table: self.table,
1081            index: self.index,
1082            id: self.id,
1083            fields: self.fields,
1084        }
1085    }
1086}
1087
1088/// Converter that holds schema context for From implementations.
1089///
1090/// `RowConverter` wraps an `Row` along with the schema context,
1091/// enabling database-specific `From` implementations to look up type
1092/// information for each field.
1093pub struct RowConverter<'a> {
1094    /// The internal row to convert
1095    pub row: Row,
1096
1097    /// Schema providing type information for fields
1098    pub schema: &'a Schema,
1099}
1100
1101impl<'a> RowConverter<'a> {
1102    /// Create a new row converter.
1103    pub fn new(row: Row, schema: &'a Schema) -> Self {
1104        Self { row, schema }
1105    }
1106}
1107
1108// ============================================================================
1109// Universal Change Type (for incremental sync)
1110// ============================================================================
1111
1112/// Operation type for incremental sync changes.
1113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1114pub enum ChangeOp {
1115    /// Create a new record
1116    Create,
1117    /// Update an existing record
1118    Update,
1119    /// Delete a record
1120    Delete,
1121}
1122
1123/// A database-agnostic change event for incremental sync.
1124///
1125/// This type represents a change (INSERT/UPDATE/DELETE) from a source database
1126/// in a shared IR that can be converted to any target database format.
1127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1128pub struct Change {
1129    /// The operation type (Create/Update/Delete)
1130    pub operation: ChangeOp,
1131    /// The table name
1132    pub table: String,
1133    /// The record ID
1134    pub id: Value,
1135    /// Field values (None for Delete operations)
1136    pub fields: Option<HashMap<String, Value>>,
1137}
1138
1139impl Change {
1140    /// Create a new change.
1141    pub fn new(
1142        operation: ChangeOp,
1143        table: impl Into<String>,
1144        id: Value,
1145        fields: Option<HashMap<String, Value>>,
1146    ) -> Self {
1147        Self {
1148            operation,
1149            table: table.into(),
1150            id,
1151            fields,
1152        }
1153    }
1154
1155    /// Create a CREATE change.
1156    pub fn create(table: impl Into<String>, id: Value, fields: HashMap<String, Value>) -> Self {
1157        Self::new(ChangeOp::Create, table, id, Some(fields))
1158    }
1159
1160    /// Create an UPDATE change.
1161    pub fn update(table: impl Into<String>, id: Value, fields: HashMap<String, Value>) -> Self {
1162        Self::new(ChangeOp::Update, table, id, Some(fields))
1163    }
1164
1165    /// Create a DELETE change.
1166    pub fn delete(table: impl Into<String>, id: Value) -> Self {
1167        Self::new(ChangeOp::Delete, table, id, None)
1168    }
1169}
1170
1171// ============================================================================
1172// Universal Relation Type (for graph database relationships)
1173// ============================================================================
1174
1175/// A database-agnostic graph relation/edge.
1176///
1177/// This type represents a relationship between two nodes (records) in graph databases
1178/// like Neo4j and SurrealDB. It contains the relation type, IDs for both endpoints,
1179/// and optional properties on the relation itself.
1180#[derive(Debug, Clone, Serialize, Deserialize)]
1181pub struct Relation {
1182    /// The relation type (table name in SurrealDB terms)
1183    pub relation_type: String,
1184    /// The relation's own ID
1185    pub id: Value,
1186    /// The source node reference (table name + id)
1187    pub input: ThingRef,
1188    /// The target node reference (table name + id)
1189    pub output: ThingRef,
1190    /// Properties on the relation itself
1191    pub data: HashMap<String, Value>,
1192}
1193
1194/// A reference to a record/node in a specific table.
1195///
1196/// This is used to identify the endpoints of a relation.
1197#[derive(Debug, Clone, Serialize, Deserialize)]
1198pub struct ThingRef {
1199    /// The table/collection name
1200    pub table: String,
1201    /// The record ID
1202    pub id: Value,
1203}
1204
1205impl ThingRef {
1206    /// Create a new thing reference.
1207    pub fn new(table: impl Into<String>, id: Value) -> Self {
1208        Self {
1209            table: table.into(),
1210            id,
1211        }
1212    }
1213}
1214
1215impl Relation {
1216    /// Create a new relation.
1217    pub fn new(
1218        relation_type: impl Into<String>,
1219        id: Value,
1220        input: ThingRef,
1221        output: ThingRef,
1222        data: HashMap<String, Value>,
1223    ) -> Self {
1224        Self {
1225            relation_type: relation_type.into(),
1226            id,
1227            input,
1228            output,
1229            data,
1230        }
1231    }
1232}
1233
1234#[cfg(test)]
1235mod tests {
1236    use super::*;
1237
1238    #[test]
1239    fn test_generated_value_accessors() {
1240        assert_eq!(Value::Bool(true).as_bool(), Some(true));
1241        assert_eq!(Value::Int32(42).as_i32(), Some(42));
1242        assert_eq!(Value::Int64(100).as_i64(), Some(100));
1243        assert_eq!(Value::Float64(3.15).as_f64(), Some(3.15));
1244        assert_eq!(Value::Text("test".to_string()).as_str(), Some("test"));
1245
1246        // Cross-type conversions
1247        assert_eq!(Value::Int32(42).as_i64(), Some(42));
1248        assert_eq!(Value::Bool(true).as_i32(), None);
1249    }
1250
1251    #[test]
1252    fn test_typed_value_constructors() {
1253        let tv = TypedValue::bool(true);
1254        assert_eq!(tv.sync_type, Type::Bool);
1255        assert_eq!(tv.value, Value::Bool(true));
1256
1257        let tv = TypedValue::int32(42);
1258        assert_eq!(tv.sync_type, Type::Int32);
1259        assert_eq!(tv.value, Value::Int32(42));
1260    }
1261
1262    #[test]
1263    fn test_internal_row_builder() {
1264        let row = Row::builder("users", 0, Value::Int64(1))
1265            .field("name", Value::Text("Alice".to_string()))
1266            .field("age", Value::Int32(30))
1267            .build();
1268
1269        assert_eq!(row.table, "users");
1270        assert_eq!(row.index, 0);
1271        assert_eq!(row.id, Value::Int64(1));
1272        assert_eq!(row.field_count(), 2);
1273        assert_eq!(
1274            row.get_field("name"),
1275            Some(&Value::Text("Alice".to_string()))
1276        );
1277        assert_eq!(row.get_field("age"), Some(&Value::Int32(30)));
1278    }
1279
1280    #[test]
1281    fn test_try_with_type_valid_combinations() {
1282        // Bool type with Bool value
1283        assert!(TypedValue::try_with_type(Type::Bool, Value::Bool(true)).is_ok());
1284
1285        // Int type with Int value (strict 1:1)
1286        assert!(TypedValue::try_with_type(Type::Int32, Value::Int32(42)).is_ok());
1287
1288        // BigInt type with BigInt value (strict 1:1)
1289        assert!(TypedValue::try_with_type(Type::Int64, Value::Int64(100)).is_ok());
1290
1291        // Text type with Text value (strict 1:1)
1292        assert!(TypedValue::try_with_type(Type::Text, Value::Text("hello".to_string())).is_ok());
1293
1294        // DateTime type with DateTime value
1295        assert!(TypedValue::try_with_type(
1296            Type::LocalDateTime,
1297            Value::LocalDateTime(chrono::Utc::now())
1298        )
1299        .is_ok());
1300
1301        // Date type with Date value (strict 1:1)
1302        assert!(TypedValue::try_with_type(Type::Date, Value::Date(chrono::Utc::now())).is_ok());
1303
1304        // Null is always valid
1305        assert!(TypedValue::try_with_type(Type::Bool, Value::Null).is_ok());
1306        assert!(TypedValue::try_with_type(Type::Int32, Value::Null).is_ok());
1307        assert!(TypedValue::try_with_type(Type::Text, Value::Null).is_ok());
1308
1309        // ZeroTemporal is valid when sync_type matches intended_type
1310        assert!(TypedValue::try_with_type(
1311            Type::Date,
1312            Value::zero_temporal(Type::Date, Some("0000-00-00".into()))
1313        )
1314        .is_ok());
1315        assert!(TypedValue::try_with_type(
1316            Type::LocalDateTime,
1317            Value::zero_temporal(Type::LocalDateTime, Some("0000-00-00 00:00:00".into()))
1318        )
1319        .is_ok());
1320        // Mismatched intended_type is invalid
1321        assert!(TypedValue::try_with_type(
1322            Type::Date,
1323            Value::zero_temporal(Type::LocalDateTime, None)
1324        )
1325        .is_err());
1326        // Non-temporal intended_type is invalid
1327        assert!(
1328            TypedValue::try_with_type(Type::Text, Value::zero_temporal(Type::Text, None)).is_err()
1329        );
1330
1331        // JSON type with Json value (strict 1:1)
1332        assert!(TypedValue::try_with_type(
1333            Type::Json,
1334            Value::Json(Box::new(serde_json::Value::Bool(true)))
1335        )
1336        .is_ok());
1337    }
1338
1339    #[test]
1340    fn test_try_with_type_invalid_combinations() {
1341        // Bool type with wrong value types
1342        let err = TypedValue::try_with_type(Type::Bool, Value::Int32(1)).unwrap_err();
1343        assert_eq!(err.expected_value, "Bool");
1344        assert_eq!(err.actual_value, "Int32");
1345
1346        // Int type with wrong value types (strict 1:1 now)
1347        let err =
1348            TypedValue::try_with_type(Type::Int32, Value::Text("42".to_string())).unwrap_err();
1349        assert_eq!(err.expected_value, "Int32");
1350        assert_eq!(err.actual_value, "Text");
1351
1352        // Int type no longer accepts BigInt (strict 1:1)
1353        let err = TypedValue::try_with_type(Type::Int32, Value::Int64(42)).unwrap_err();
1354        assert_eq!(err.expected_value, "Int32");
1355        assert_eq!(err.actual_value, "Int64");
1356
1357        // BigInt type no longer accepts Int (strict 1:1)
1358        let err = TypedValue::try_with_type(Type::Int64, Value::Int32(42)).unwrap_err();
1359        assert_eq!(err.expected_value, "Int64");
1360        assert_eq!(err.actual_value, "Int32");
1361
1362        // Text type with wrong value types
1363        let err = TypedValue::try_with_type(Type::Text, Value::Int32(42)).unwrap_err();
1364        assert_eq!(err.expected_value, "Text");
1365        assert_eq!(err.actual_value, "Int32");
1366
1367        // Uuid type with Text value (strict 1:1)
1368        let err = TypedValue::try_with_type(Type::Uuid, Value::Text("not-a-uuid".to_string()))
1369            .unwrap_err();
1370        assert_eq!(err.expected_value, "Uuid");
1371        assert_eq!(err.actual_value, "Text");
1372    }
1373
1374    #[test]
1375    fn test_try_with_type_error_message() {
1376        let err =
1377            TypedValue::try_with_type(Type::Bool, Value::Text("true".to_string())).unwrap_err();
1378
1379        let msg = err.to_string();
1380        assert!(msg.contains("Type-value mismatch"));
1381        assert!(msg.contains("Bool"));
1382        assert!(msg.contains("Text"));
1383    }
1384
1385    #[test]
1386    fn test_variant_name() {
1387        assert_eq!(Value::Bool(true).variant_name(), "Bool");
1388        assert_eq!(Value::Int32(42).variant_name(), "Int32");
1389        assert_eq!(Value::Int64(100).variant_name(), "Int64");
1390        assert_eq!(Value::Float64(1.5).variant_name(), "Float64");
1391        assert_eq!(Value::Text("test".to_string()).variant_name(), "Text");
1392        assert_eq!(Value::Bytes(vec![1, 2, 3]).variant_name(), "Bytes");
1393        assert_eq!(Value::Null.variant_name(), "Null");
1394    }
1395
1396    #[test]
1397    fn test_to_typed_value_deterministic() {
1398        // Each Value variant should map to exactly one Type
1399        let value = Value::Int32(42);
1400        let typed = value.to_typed_value();
1401        assert_eq!(typed.sync_type, Type::Int32);
1402
1403        let value = Value::Text("hello".to_string());
1404        let typed = value.to_typed_value();
1405        assert_eq!(typed.sync_type, Type::Text);
1406
1407        let value = Value::Int64(100);
1408        let typed = value.to_typed_value();
1409        assert_eq!(typed.sync_type, Type::Int64);
1410
1411        let value = Value::Float64(3.15);
1412        let typed = value.to_typed_value();
1413        assert_eq!(typed.sync_type, Type::Float64);
1414
1415        let value = Value::Float32(1.5);
1416        let typed = value.to_typed_value();
1417        assert_eq!(typed.sync_type, Type::Float32);
1418
1419        let value = Value::Int16(100);
1420        let typed = value.to_typed_value();
1421        assert_eq!(typed.sync_type, Type::Int16);
1422
1423        let value = Value::Int8 { value: 1, width: 1 };
1424        let typed = value.to_typed_value();
1425        assert!(matches!(typed.sync_type, Type::Int8 { width: 1 }));
1426    }
1427
1428    #[test]
1429    fn universal_row_serde_roundtrip() {
1430        let row = Row::builder("users", 7, Value::Int64(42))
1431            .field("name", Value::Text("Alice".to_string()))
1432            .field("active", Value::Bool(true))
1433            .build();
1434        let json = serde_json::to_string(&row).expect("serialize row");
1435        let back: Row = serde_json::from_str(&json).expect("deserialize row");
1436        assert_eq!(back, row);
1437    }
1438
1439    #[test]
1440    fn universal_change_serde_roundtrip_and_golden() {
1441        let mut data = HashMap::new();
1442        data.insert("name".to_string(), Value::Text("Bob".to_string()));
1443        let change = Change::create("users", Value::Int64(9), data);
1444
1445        let json = serde_json::to_string(&change).expect("serialize change");
1446        let back: Change = serde_json::from_str(&json).expect("deserialize change");
1447        assert_eq!(back, change);
1448
1449        // Golden: fixed JSON (single field so HashMap order is irrelevant).
1450        let golden = r#"{"operation":"Create","table":"users","id":{"type":"Int64","value":9},"fields":{"name":{"type":"Text","value":"Bob"}}}"#;
1451        let from_golden: Change = serde_json::from_str(golden).expect("deserialize golden");
1452        assert_eq!(from_golden, change);
1453
1454        let delete = Change::delete("users", Value::Int64(9));
1455        let delete_json = serde_json::to_string(&delete).unwrap();
1456        let delete_back: Change = serde_json::from_str(&delete_json).unwrap();
1457        assert_eq!(delete_back, delete);
1458        assert!(delete_back.fields.is_none());
1459    }
1460}