Skip to main content

surreal_sync_json/types/
reverse.rs

1//! Reverse conversion: JSON value → TypedValue.
2//!
3//! This module provides conversion from JSON values to sync-core's `TypedValue`.
4
5use base64::Engine;
6use chrono::{DateTime, NaiveDateTime, TimeZone, Utc};
7use serde_json;
8use std::collections::HashMap;
9use surreal_sync_core::{Type, TypedValue, Value};
10
11/// Parse an ISO 8601 duration string (PTxS or PTx.xxxxxxxxxS format).
12///
13/// Supports:
14/// - Simple seconds: "PT181S" (181 seconds)
15/// - Seconds with nanoseconds: "PT60.123456789S" (60 seconds + 123456789 nanoseconds)
16fn parse_iso8601_duration(s: &str) -> Option<std::time::Duration> {
17    let trimmed = s.trim();
18    // Only accept "PTxS" or "PTx.xxxxxxxxxS" format
19    if let Some(secs_str) = trimmed.strip_prefix("PT").and_then(|s| s.strip_suffix('S')) {
20        if let Some(dot_pos) = secs_str.find('.') {
21            // Has fractional seconds
22            let secs: u64 = secs_str[..dot_pos].parse().ok()?;
23            let nanos_str = &secs_str[dot_pos + 1..];
24            let nanos: u32 = nanos_str.parse().ok()?;
25            Some(std::time::Duration::new(secs, nanos))
26        } else {
27            let secs: u64 = secs_str.parse().ok()?;
28            Some(std::time::Duration::from_secs(secs))
29        }
30    } else {
31        None
32    }
33}
34
35/// Parse a datetime string in various formats.
36///
37/// Supports:
38/// - RFC 3339: "2024-01-01T12:00:00Z"
39/// - MySQL timestamp: "2024-01-01 12:00:00"
40/// - MySQL timestamp with microseconds: "2024-01-01 12:00:00.123456"
41fn parse_datetime_string(s: &str) -> Option<DateTime<Utc>> {
42    // Try RFC 3339 first (ISO 8601 with timezone)
43    if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
44        return Some(dt.with_timezone(&Utc));
45    }
46
47    // Try PostgreSQL to_jsonb() format for TIMESTAMP columns (ISO 8601 without timezone)
48    // Format: "2024-11-13T20:15:33" (T separator, no timezone suffix)
49    if let Ok(naive) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S") {
50        return Some(Utc.from_utc_datetime(&naive));
51    }
52
53    // Try PostgreSQL to_jsonb() format with microseconds
54    // Format: "2024-11-13T20:15:33.123456"
55    if let Ok(naive) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f") {
56        return Some(Utc.from_utc_datetime(&naive));
57    }
58
59    // Try MySQL timestamp format without microseconds
60    if let Ok(naive) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
61        return Some(Utc.from_utc_datetime(&naive));
62    }
63
64    // Try MySQL timestamp format with microseconds
65    if let Ok(naive) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f") {
66        return Some(Utc.from_utc_datetime(&naive));
67    }
68
69    // Try PostgreSQL timestamp format with timezone offset
70    if let Ok(dt) = DateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%#z") {
71        return Some(dt.with_timezone(&Utc));
72    }
73
74    None
75}
76
77/// JSON value paired with schema information for type-aware conversion.
78#[derive(Debug, Clone)]
79pub struct JsonValueWithSchema {
80    /// The JSON value.
81    pub value: serde_json::Value,
82    /// The expected sync type for conversion.
83    pub sync_type: Type,
84}
85
86impl JsonValueWithSchema {
87    /// Create a new JsonValueWithSchema.
88    pub fn new(value: serde_json::Value, sync_type: Type) -> Self {
89        Self { value, sync_type }
90    }
91
92    /// Convert to TypedValue.
93    pub fn to_typed_value(&self) -> TypedValue {
94        TypedValue::from(self.clone())
95    }
96}
97
98impl From<JsonValueWithSchema> for TypedValue {
99    fn from(jv: JsonValueWithSchema) -> Self {
100        match (&jv.sync_type, &jv.value) {
101            // Null
102            (sync_type, serde_json::Value::Null) => TypedValue::null(sync_type.clone()),
103
104            // Boolean
105            (Type::Bool, serde_json::Value::Bool(b)) => TypedValue::bool(*b),
106            // MySQL stores TINYINT(1) booleans as 0/1 in JSON_OBJECT
107            (Type::Bool, serde_json::Value::Number(n)) => {
108                if let Some(i) = n.as_i64() {
109                    TypedValue::bool(i != 0)
110                } else {
111                    TypedValue::null(Type::Bool)
112                }
113            }
114
115            // Integer types
116            (Type::Int8 { width }, serde_json::Value::Number(n)) => {
117                if let Some(i) = n.as_i64() {
118                    TypedValue::int8(i as i8, *width)
119                } else {
120                    TypedValue::null(Type::Int8 { width: *width })
121                }
122            }
123            (Type::Int16, serde_json::Value::Number(n)) => {
124                if let Some(i) = n.as_i64() {
125                    TypedValue::int16(i as i16)
126                } else {
127                    TypedValue::null(Type::Int16)
128                }
129            }
130            (Type::Int32, serde_json::Value::Number(n)) => {
131                if let Some(i) = n.as_i64() {
132                    TypedValue::int32(i as i32)
133                } else {
134                    TypedValue::null(Type::Int32)
135                }
136            }
137            (Type::Int64, serde_json::Value::Number(n)) => {
138                if let Some(i) = n.as_i64() {
139                    TypedValue::int64(i)
140                } else {
141                    TypedValue::null(Type::Int64)
142                }
143            }
144
145            // Floating point
146            (Type::Float32, serde_json::Value::Number(n)) => {
147                if let Some(f) = n.as_f64() {
148                    TypedValue::float32(f as f32)
149                } else {
150                    TypedValue::null(Type::Float32)
151                }
152            }
153            (Type::Float64, serde_json::Value::Number(n)) => {
154                if let Some(f) = n.as_f64() {
155                    TypedValue::float64(f)
156                } else {
157                    TypedValue::null(Type::Float64)
158                }
159            }
160
161            // Decimal - stored as string in JSON
162            (Type::Decimal { precision, scale }, serde_json::Value::String(s)) => {
163                TypedValue::decimal(s, *precision, *scale)
164            }
165            (Type::Decimal { precision, scale }, serde_json::Value::Number(n)) => {
166                TypedValue::decimal(n.to_string(), *precision, *scale)
167            }
168
169            // String types
170            (Type::Char { length }, serde_json::Value::String(s)) => {
171                TypedValue::char_type(s, *length)
172            }
173            (Type::VarChar { length }, serde_json::Value::String(s)) => {
174                TypedValue::varchar(s, *length)
175            }
176            (Type::Text, serde_json::Value::String(s)) => TypedValue::text(s),
177
178            // Binary types - base64 encoded in JSON
179            (Type::Blob, serde_json::Value::String(s)) => {
180                match base64::engine::general_purpose::STANDARD.decode(s) {
181                    Ok(bytes) => TypedValue::blob(bytes),
182                    Err(_) => TypedValue::null(Type::Blob),
183                }
184            }
185            (Type::Bytes, serde_json::Value::String(s)) => {
186                match base64::engine::general_purpose::STANDARD.decode(s) {
187                    Ok(bytes) => TypedValue::bytes(bytes),
188                    Err(_) => TypedValue::null(Type::Bytes),
189                }
190            }
191
192            // UUID
193            (Type::Uuid, serde_json::Value::String(s)) => {
194                if let Ok(uuid) = uuid::Uuid::parse_str(s) {
195                    TypedValue::uuid(uuid)
196                } else {
197                    TypedValue::null(Type::Uuid)
198                }
199            }
200
201            // Date/time types - multiple formats supported
202            (Type::LocalDateTime, serde_json::Value::String(s)) => {
203                if Value::is_mysql_zero_temporal_literal(s) {
204                    TypedValue::zero_temporal(Type::LocalDateTime, Some(s.clone()))
205                } else if let Some(dt) = parse_datetime_string(s) {
206                    TypedValue::datetime(dt)
207                } else {
208                    TypedValue::null(Type::LocalDateTime)
209                }
210            }
211            (Type::LocalDateTimeNano, serde_json::Value::String(s)) => {
212                if Value::is_mysql_zero_temporal_literal(s) {
213                    TypedValue::zero_temporal(Type::LocalDateTimeNano, Some(s.clone()))
214                } else if let Some(dt) = parse_datetime_string(s) {
215                    TypedValue::datetime_nano(dt)
216                } else {
217                    TypedValue::null(Type::LocalDateTimeNano)
218                }
219            }
220            (Type::ZonedDateTime, serde_json::Value::String(s)) => {
221                if Value::is_mysql_zero_temporal_literal(s) {
222                    TypedValue::zero_temporal(Type::ZonedDateTime, Some(s.clone()))
223                } else if let Some(dt) = parse_datetime_string(s) {
224                    TypedValue::timestamptz(dt)
225                } else {
226                    TypedValue::null(Type::ZonedDateTime)
227                }
228            }
229
230            // Date stored as string
231            (Type::Date, serde_json::Value::String(s)) => {
232                if Value::is_mysql_zero_temporal_literal(s) {
233                    TypedValue::zero_temporal(Type::Date, Some(s.clone()))
234                } else if let Ok(dt) = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") {
235                    let datetime = dt.and_hms_opt(0, 0, 0).unwrap();
236                    let utc_dt = DateTime::<Utc>::from_naive_utc_and_offset(datetime, Utc);
237                    TypedValue::date(utc_dt)
238                } else {
239                    TypedValue::null(Type::Date)
240                }
241            }
242
243            // Time stored as string
244            (Type::Time, serde_json::Value::String(s)) => {
245                if let Ok(time) = chrono::NaiveTime::parse_from_str(s, "%H:%M:%S") {
246                    let datetime = chrono::NaiveDate::from_ymd_opt(1970, 1, 1)
247                        .unwrap()
248                        .and_time(time);
249                    let utc_dt = DateTime::<Utc>::from_naive_utc_and_offset(datetime, Utc);
250                    TypedValue::time(utc_dt)
251                } else {
252                    TypedValue::null(Type::Time)
253                }
254            }
255
256            // JSON types - can be objects or arrays
257            (Type::Json, serde_json::Value::Object(obj)) => {
258                let value = json_object_to_universal(obj);
259                TypedValue::json(value)
260            }
261            (Type::Json, serde_json::Value::Array(arr)) => {
262                let value = json_array_to_universal(arr);
263                TypedValue::json(value)
264            }
265            (Type::Jsonb, serde_json::Value::Object(obj)) => {
266                let value = json_object_to_universal(obj);
267                TypedValue::jsonb(value)
268            }
269            (Type::Jsonb, serde_json::Value::Array(arr)) => {
270                let value = json_array_to_universal(arr);
271                TypedValue::jsonb(value)
272            }
273
274            // Array types
275            (Type::Array { element_type }, serde_json::Value::Array(arr)) => {
276                let values: Vec<Value> = arr
277                    .iter()
278                    .map(|v| {
279                        let jv = JsonValueWithSchema::new(v.clone(), (**element_type).clone());
280                        TypedValue::from(jv).value
281                    })
282                    .collect();
283                TypedValue::array(values, (**element_type).clone())
284            }
285
286            // Set - stored as array
287            (Type::Set { values: set_values }, serde_json::Value::Array(arr)) => {
288                let elements: Vec<String> = arr
289                    .iter()
290                    .filter_map(|v| {
291                        if let serde_json::Value::String(s) = v {
292                            Some(s.clone())
293                        } else {
294                            None
295                        }
296                    })
297                    .collect();
298                TypedValue::set(elements, set_values.clone())
299            }
300
301            // Enum - stored as string
302            (
303                Type::Enum {
304                    values: enum_values,
305                },
306                serde_json::Value::String(s),
307            ) => TypedValue::enum_type(s.clone(), enum_values.clone()),
308
309            // Geometry types - GeoJSON format
310            (Type::Geometry { geometry_type }, serde_json::Value::Object(obj)) => {
311                TypedValue::geometry_geojson(
312                    serde_json::Value::Object(obj.clone()),
313                    geometry_type.clone(),
314                )
315            }
316
317            // Duration - parse ISO 8601 duration string (PTxS or PTx.xxxxxxxxxS format)
318            (Type::Duration, serde_json::Value::String(s)) => {
319                if let Some(duration) = parse_iso8601_duration(s) {
320                    TypedValue::duration(duration)
321                } else {
322                    TypedValue::null(Type::Duration)
323                }
324            }
325
326            // Fallback
327            (sync_type, _) => TypedValue::null(sync_type.clone()),
328        }
329    }
330}
331
332/// Convert a JSON object to a Value.
333#[allow(dead_code)]
334fn json_object_to_universal(obj: &serde_json::Map<String, serde_json::Value>) -> serde_json::Value {
335    serde_json::Value::Object(obj.clone())
336}
337
338/// Convert a JSON array to a Value.
339#[allow(dead_code)]
340fn json_array_to_universal(arr: &[serde_json::Value]) -> serde_json::Value {
341    serde_json::Value::Array(arr.to_vec())
342}
343
344/// Convert a JSON object to a HashMap of Value (for GeoJSON geometry).
345#[allow(dead_code)]
346fn json_object_to_geojson_hashmap(
347    obj: &serde_json::Map<String, serde_json::Value>,
348) -> HashMap<String, Value> {
349    let mut map = HashMap::new();
350    for (key, value) in obj {
351        map.insert(key.clone(), json_value_to_universal(value));
352    }
353    map
354}
355
356/// Convert a JSON value to Value (without schema information).
357///
358/// This performs a generic conversion without type hints, inferring types from the JSON values.
359pub fn json_value_to_universal(value: &serde_json::Value) -> Value {
360    match value {
361        serde_json::Value::Null => Value::Null,
362        serde_json::Value::Bool(b) => Value::Bool(*b),
363        serde_json::Value::Number(n) => {
364            if let Some(i) = n.as_i64() {
365                Value::Int64(i)
366            } else if let Some(f) = n.as_f64() {
367                Value::Float64(f)
368            } else {
369                Value::Text(n.to_string())
370            }
371        }
372        serde_json::Value::String(s) => Value::Text(s.clone()),
373        serde_json::Value::Array(arr) => Value::Array {
374            elements: arr.iter().map(json_value_to_universal).collect(),
375            element_type: Box::new(Type::Text),
376        },
377        serde_json::Value::Object(obj) => {
378            let mut map = HashMap::new();
379            for (key, val) in obj {
380                map.insert(key.clone(), json_value_to_universal(val));
381            }
382            Value::Json(Box::new(serde_json::Value::Object(obj.clone())))
383        }
384    }
385}
386
387/// Convert a JSON value to Value (without type context).
388#[allow(dead_code)]
389fn json_value_to_generated(value: &serde_json::Value) -> Value {
390    match value {
391        serde_json::Value::Null => Value::Null,
392        serde_json::Value::Bool(b) => Value::Bool(*b),
393        serde_json::Value::Number(n) => {
394            if let Some(i) = n.as_i64() {
395                Value::Int64(i)
396            } else if let Some(f) = n.as_f64() {
397                Value::Float64(f)
398            } else {
399                Value::Null
400            }
401        }
402        serde_json::Value::String(s) => Value::Text(s.clone()),
403        serde_json::Value::Array(arr) => Value::Array {
404            elements: arr.iter().map(json_value_to_generated).collect(),
405            element_type: Box::new(surreal_sync_core::Type::Text),
406        },
407        serde_json::Value::Object(_obj) => Value::Json(Box::new(value.clone())),
408    }
409}
410
411/// Configuration for JSON field conversions.
412///
413/// Some databases (MySQL, PostgreSQL) store boolean values as 0/1 in JSON fields.
414/// This struct allows specifying which JSON paths should be converted to boolean values
415/// or treated as SET columns (comma-separated arrays).
416///
417/// # Example
418///
419/// ```
420/// use surreal_sync_json::types::JsonConversionConfig;
421///
422/// let config = JsonConversionConfig::new()
423///     .with_boolean_path("settings.enabled")
424///     .with_boolean_path("flags.is_active")
425///     .with_set_path("permissions");
426/// ```
427#[derive(Debug, Clone, Default)]
428pub struct JsonConversionConfig {
429    /// JSON paths that should convert 0/1 to boolean.
430    /// Paths use dot notation, e.g., "settings.enabled" or "flags.is_active".
431    pub boolean_paths: Vec<String>,
432    /// JSON paths that should be treated as SET columns (comma-separated arrays).
433    pub set_paths: Vec<String>,
434}
435
436impl JsonConversionConfig {
437    /// Create a new empty configuration.
438    pub fn new() -> Self {
439        Self::default()
440    }
441
442    /// Add a boolean path.
443    pub fn with_boolean_path(mut self, path: &str) -> Self {
444        self.boolean_paths.push(path.to_string());
445        self
446    }
447
448    /// Add multiple boolean paths.
449    pub fn with_boolean_paths(mut self, paths: &[&str]) -> Self {
450        self.boolean_paths
451            .extend(paths.iter().map(|s| s.to_string()));
452        self
453    }
454
455    /// Add a SET path.
456    pub fn with_set_path(mut self, path: &str) -> Self {
457        self.set_paths.push(path.to_string());
458        self
459    }
460
461    /// Add multiple SET paths.
462    pub fn with_set_paths(mut self, paths: &[&str]) -> Self {
463        self.set_paths.extend(paths.iter().map(|s| s.to_string()));
464        self
465    }
466}
467
468/// Convert a JSON value to TypedValue with path-based configuration.
469///
470/// This handles database-specific quirks like storing booleans as 0/1 in JSON fields.
471///
472/// # Arguments
473/// * `value` - The JSON value to convert
474/// * `current_path` - The current path in the JSON tree (for nested objects), typically ""
475/// * `config` - Configuration specifying which paths should be treated specially
476///
477/// # Example
478/// ```
479/// use surreal_sync_json::types::{JsonConversionConfig, json_to_typed_value_with_config};
480/// use surreal_sync_core::Value;
481///
482/// let config = JsonConversionConfig::new()
483///     .with_boolean_path("settings.enabled")
484///     .with_boolean_path("flags.is_active");
485///
486/// let json = serde_json::json!({"settings": {"enabled": 1}});
487/// let tv = json_to_typed_value_with_config(json, "", &config);
488/// // tv.value will have {"settings": {"enabled": true}}
489/// ```
490pub fn json_to_typed_value_with_config(
491    value: serde_json::Value,
492    current_path: &str,
493    config: &JsonConversionConfig,
494) -> TypedValue {
495    let gv = json_to_generated_value_with_config(value, current_path, config);
496    // Convert Value back to serde_json::Value for TypedValue::json
497    let json_value = if let Value::Json(json_val) = gv {
498        *json_val
499    } else {
500        // For other types, convert to JSON
501        universal_value_to_json(&gv)
502    };
503    TypedValue::json(json_value)
504}
505
506/// Convert JSON to Value with path-based configuration.
507///
508/// This is the internal implementation that handles the recursive conversion.
509pub fn json_to_generated_value_with_config(
510    value: serde_json::Value,
511    current_path: &str,
512    config: &JsonConversionConfig,
513) -> Value {
514    match value {
515        serde_json::Value::Null => Value::Null,
516        serde_json::Value::Bool(b) => Value::Bool(b),
517        serde_json::Value::Number(n) => {
518            // Check if this path should be treated as boolean
519            let is_boolean_path = config.boolean_paths.iter().any(|p| p == current_path);
520
521            if let Some(i) = n.as_i64() {
522                if is_boolean_path && (i == 0 || i == 1) {
523                    // Convert 0/1 to boolean for specified paths
524                    Value::Bool(i == 1)
525                } else {
526                    Value::Int64(i)
527                }
528            } else if let Some(f) = n.as_f64() {
529                Value::Float64(f)
530            } else {
531                Value::Text(n.to_string())
532            }
533        }
534        serde_json::Value::String(s) => {
535            // Check if this path should be treated as a SET column
536            let is_set_path = config.set_paths.iter().any(|p| p == current_path);
537
538            if is_set_path {
539                // Convert comma-separated SET values to array
540                if s.is_empty() {
541                    Value::Array {
542                        elements: Vec::new(),
543                        element_type: Box::new(surreal_sync_core::Type::Text),
544                    }
545                } else {
546                    let values: Vec<Value> =
547                        s.split(',').map(|v| Value::Text(v.to_string())).collect();
548                    Value::Array {
549                        elements: values,
550                        element_type: Box::new(surreal_sync_core::Type::Text),
551                    }
552                }
553            } else {
554                Value::Text(s)
555            }
556        }
557        serde_json::Value::Array(arr) => {
558            let values: Vec<Value> = arr
559                .into_iter()
560                .enumerate()
561                .map(|(idx, item)| {
562                    let item_path = format!("{current_path}[{idx}]");
563                    json_to_generated_value_with_config(item, &item_path, config)
564                })
565                .collect();
566            Value::Array {
567                elements: values,
568                element_type: Box::new(surreal_sync_core::Type::Text),
569            }
570        }
571        serde_json::Value::Object(obj) => {
572            let map: HashMap<String, Value> = obj
573                .into_iter()
574                .map(|(key, val)| {
575                    // Build the nested path for this field
576                    let nested_path = if current_path.is_empty() {
577                        key.clone()
578                    } else {
579                        format!("{current_path}.{key}")
580                    };
581                    // Convert using the config
582                    let converted = json_to_generated_value_with_config(val, &nested_path, config);
583                    (key, converted)
584                })
585                .collect();
586            // Convert the HashMap back to a JSON object
587            let json_obj: serde_json::Map<String, serde_json::Value> = map
588                .iter()
589                .map(|(k, v)| (k.clone(), universal_value_to_json(v)))
590                .collect();
591            Value::Json(Box::new(serde_json::Value::Object(json_obj)))
592        }
593    }
594}
595
596/// Convert an Value to serde_json::Value (helper for config-based conversion).
597fn universal_value_to_json(value: &Value) -> serde_json::Value {
598    match value {
599        Value::Null => serde_json::Value::Null,
600        Value::Bool(b) => serde_json::Value::Bool(*b),
601        Value::Int64(i) => serde_json::json!(*i),
602        Value::Float64(f) => serde_json::json!(*f),
603        Value::Text(s) => serde_json::Value::String(s.clone()),
604        Value::Array { elements, .. } => {
605            serde_json::Value::Array(elements.iter().map(universal_value_to_json).collect())
606        }
607        Value::Json(json_val) => (**json_val).clone(),
608        _ => serde_json::Value::Null,
609    }
610}
611
612/// Extract a typed value from a JSON object field.
613pub fn extract_field(
614    obj: &serde_json::Map<String, serde_json::Value>,
615    field: &str,
616    sync_type: &Type,
617) -> TypedValue {
618    match obj.get(field) {
619        Some(value) => JsonValueWithSchema::new(value.clone(), sync_type.clone()).to_typed_value(),
620        None => TypedValue::null(sync_type.clone()),
621    }
622}
623
624/// Convert a complete JSON object to a map of TypedValues using schema.
625pub fn json_object_to_typed_values(
626    obj: &serde_json::Map<String, serde_json::Value>,
627    schema: &[(String, Type)],
628) -> HashMap<String, TypedValue> {
629    let mut result = HashMap::new();
630    for (field_name, sync_type) in schema {
631        let tv = extract_field(obj, field_name, sync_type);
632        result.insert(field_name.clone(), tv);
633    }
634    result
635}
636
637/// Parse a JSONL line and convert to typed values.
638pub fn parse_jsonl_line(
639    line: &str,
640    schema: &[(String, Type)],
641) -> Result<HashMap<String, TypedValue>, serde_json::Error> {
642    let obj: serde_json::Map<String, serde_json::Value> = serde_json::from_str(line)?;
643    Ok(json_object_to_typed_values(&obj, schema))
644}
645
646// ============================================================================
647// Schema-aware conversion functions for source crates
648// These replace surreal_sync_surreal::v2::types functions to enable decoupling from SurrealDB
649// ============================================================================
650
651/// Convert JSON value to Value using table schema for type lookup.
652///
653/// This is the universal equivalent of `surreal_sync_surreal::v2::types::json_to_surreal_with_table_schema`.
654/// Returns Value instead of surrealdb::sql::Value.
655pub fn json_to_universal_with_table_schema(
656    value: serde_json::Value,
657    field_name: &str,
658    schema: &surreal_sync_core::TableDefinition,
659) -> anyhow::Result<Value> {
660    // Look up the column type for this field
661    let column_type = schema.get_column_type(field_name);
662
663    match column_type {
664        Some(sync_type) => {
665            let jv = JsonValueWithSchema::new(value, sync_type.clone());
666            let tv = TypedValue::from(jv);
667            Ok(tv.value)
668        }
669        None => {
670            // No schema info for this field, use generic conversion
671            Ok(json_value_to_universal(&value))
672        }
673    }
674}
675
676/// Convert a string ID value to Value based on schema-defined type.
677///
678/// This is the universal equivalent of `surreal_sync_surreal::v2::types::convert_id_with_database_schema`.
679/// Returns Value instead of surrealdb::sql::Id.
680pub fn convert_id_with_database_schema(
681    id_str: &str,
682    table_name: &str,
683    id_column: &str,
684    schema: &surreal_sync_core::DatabaseSchema,
685) -> anyhow::Result<Value> {
686    // Look up the table schema
687    let table_schema = schema
688        .get_table(table_name)
689        .ok_or_else(|| anyhow::anyhow!("Table '{table_name}' not found in schema"))?;
690
691    // Look up the ID column type
692    let id_type = table_schema.get_column_type(id_column).ok_or_else(|| {
693        anyhow::anyhow!("Column '{id_column}' not found in table '{table_name}' schema")
694    })?;
695
696    // Convert based on the schema-defined type
697    convert_id_to_value(id_str, table_name, id_type)
698}
699
700/// Convert a string ID value to Value using Type.
701pub fn convert_id_to_value(
702    id_str: &str,
703    table_name: &str,
704    id_type: &Type,
705) -> anyhow::Result<Value> {
706    match id_type {
707        Type::Int8 { .. } | Type::Int16 | Type::Int32 | Type::Int64 => {
708            let id_int: i64 = id_str.parse().map_err(|e| {
709                anyhow::anyhow!(
710                    "Failed to parse ID '{id_str}' as integer for table '{table_name}': {e}"
711                )
712            })?;
713            Ok(Value::Int64(id_int))
714        }
715        Type::Uuid => {
716            let uuid = uuid::Uuid::parse_str(id_str).map_err(|e| {
717                anyhow::anyhow!(
718                    "Failed to parse ID '{id_str}' as UUID for table '{table_name}': {e}"
719                )
720            })?;
721            Ok(Value::Uuid(uuid))
722        }
723        Type::Text | Type::VarChar { .. } | Type::Char { .. } => {
724            Ok(Value::Text(id_str.to_string()))
725        }
726        other => {
727            anyhow::bail!(
728                "Unsupported ID type {other:?} for table '{table_name}'. Supported types: Int8-64, Uuid, Text, VarChar, Char."
729            );
730        }
731    }
732}
733
734#[cfg(test)]
735mod tests {
736    use super::*;
737    use chrono::{Datelike, TimeZone, Timelike, Utc};
738    use serde_json::json;
739    use surreal_sync_core::GeometryType;
740
741    #[test]
742    fn test_null_conversion() {
743        let jv = JsonValueWithSchema::new(serde_json::Value::Null, Type::Text);
744        let tv = TypedValue::from(jv);
745        assert!(matches!(tv.value, Value::Null));
746    }
747
748    #[test]
749    fn test_bool_conversion() {
750        let jv = JsonValueWithSchema::new(json!(true), Type::Bool);
751        let tv = TypedValue::from(jv);
752        assert!(matches!(tv.value, Value::Bool(true)));
753    }
754
755    #[test]
756    fn test_bool_from_number_zero() {
757        // MySQL stores TINYINT(1) booleans as 0/1 in JSON_OBJECT
758        let jv = JsonValueWithSchema::new(json!(0), Type::Bool);
759        let tv = TypedValue::from(jv);
760        assert!(matches!(tv.value, Value::Bool(false)));
761    }
762
763    #[test]
764    fn test_bool_from_number_one() {
765        // MySQL stores TINYINT(1) booleans as 0/1 in JSON_OBJECT
766        let jv = JsonValueWithSchema::new(json!(1), Type::Bool);
767        let tv = TypedValue::from(jv);
768        assert!(matches!(tv.value, Value::Bool(true)));
769    }
770
771    #[test]
772    fn test_bool_from_nonzero_number() {
773        // Non-zero numbers should be true (like MySQL's boolean semantics)
774        let jv = JsonValueWithSchema::new(json!(42), Type::Bool);
775        let tv = TypedValue::from(jv);
776        assert!(matches!(tv.value, Value::Bool(true)));
777    }
778
779    #[test]
780    fn test_int_conversion() {
781        let jv = JsonValueWithSchema::new(json!(42), Type::Int32);
782        let tv = TypedValue::from(jv);
783        assert!(matches!(tv.value, Value::Int32(42)));
784    }
785
786    #[test]
787    fn test_bigint_conversion() {
788        let jv = JsonValueWithSchema::new(json!(9876543210i64), Type::Int64);
789        let tv = TypedValue::from(jv);
790        assert!(matches!(tv.value, Value::Int64(9876543210)));
791    }
792
793    #[test]
794    fn test_float_conversion() {
795        let jv = JsonValueWithSchema::new(json!(1.23456), Type::Float64);
796        let tv = TypedValue::from(jv);
797        if let Value::Float64(f) = tv.value {
798            assert!((f - 1.23456).abs() < 0.00001);
799        } else {
800            panic!("Expected Float64");
801        }
802    }
803
804    #[test]
805    fn test_decimal_from_string() {
806        let jv = JsonValueWithSchema::new(
807            json!("123.456"),
808            Type::Decimal {
809                precision: 10,
810                scale: 3,
811            },
812        );
813        let tv = TypedValue::from(jv);
814        if let Value::Decimal {
815            value,
816            precision,
817            scale,
818        } = tv.value
819        {
820            assert_eq!(value, "123.456");
821            assert_eq!(precision, 10);
822            assert_eq!(scale, 3);
823        } else {
824            panic!("Expected Decimal");
825        }
826    }
827
828    #[test]
829    fn test_string_conversion() {
830        let jv = JsonValueWithSchema::new(json!("hello world"), Type::Text);
831        let tv = TypedValue::from(jv);
832        assert!(matches!(tv.value, Value::Text(ref s) if s == "hello world"));
833    }
834
835    #[test]
836    fn test_varchar_conversion() {
837        let jv = JsonValueWithSchema::new(json!("test"), Type::VarChar { length: 100 });
838        let tv = TypedValue::from(jv);
839        assert!(matches!(tv.sync_type, Type::VarChar { length: 100 }));
840        if let Value::VarChar { value, length } = tv.value {
841            assert_eq!(value, "test");
842            assert_eq!(length, 100);
843        } else {
844            panic!("Expected VarChar, got {:?}", tv.value);
845        }
846    }
847
848    #[test]
849    fn test_bytes_from_base64() {
850        let encoded = base64::engine::general_purpose::STANDARD.encode(vec![0x01, 0x02, 0x03]);
851        let jv = JsonValueWithSchema::new(json!(encoded), Type::Bytes);
852        let tv = TypedValue::from(jv);
853        assert!(matches!(tv.value, Value::Bytes(ref b) if *b == vec![0x01, 0x02, 0x03]));
854    }
855
856    #[test]
857    fn test_uuid_conversion() {
858        let jv =
859            JsonValueWithSchema::new(json!("550e8400-e29b-41d4-a716-446655440000"), Type::Uuid);
860        let tv = TypedValue::from(jv);
861        if let Value::Uuid(u) = tv.value {
862            assert_eq!(u.to_string(), "550e8400-e29b-41d4-a716-446655440000");
863        } else {
864            panic!("Expected Uuid");
865        }
866    }
867
868    #[test]
869    fn test_datetime_conversion() {
870        let dt = Utc.with_ymd_and_hms(2024, 6, 15, 10, 30, 0).unwrap();
871        let jv = JsonValueWithSchema::new(json!(dt.to_rfc3339()), Type::LocalDateTime);
872        let tv = TypedValue::from(jv);
873        if let Value::LocalDateTime(result_dt) = tv.value {
874            assert_eq!(result_dt.year(), 2024);
875            assert_eq!(result_dt.month(), 6);
876            assert_eq!(result_dt.day(), 15);
877        } else {
878            panic!("Expected DateTime");
879        }
880    }
881
882    /// Test that PostgreSQL's to_jsonb() timestamp format is correctly parsed.
883    /// PostgreSQL produces timestamps WITHOUT timezone like "2024-11-13T20:15:33"
884    /// when using `to_jsonb(row)` on TIMESTAMP (without time zone) columns.
885    #[test]
886    fn test_datetime_postgresql_to_jsonb_format() {
887        // This is the exact format PostgreSQL's to_jsonb() produces for TIMESTAMP columns
888        let json_str = "2024-11-13T20:15:33";
889        let jv = JsonValueWithSchema::new(json!(json_str), Type::LocalDateTime);
890        let tv = TypedValue::from(jv);
891
892        // This MUST NOT return null - PostgreSQL timestamps must be parseable
893        match &tv.value {
894            Value::LocalDateTime(dt) => {
895                assert_eq!(dt.year(), 2024);
896                assert_eq!(dt.month(), 11);
897                assert_eq!(dt.day(), 13);
898                assert_eq!(dt.hour(), 20);
899                assert_eq!(dt.minute(), 15);
900                assert_eq!(dt.second(), 33);
901            }
902            Value::Null => {
903                panic!(
904                    "PostgreSQL timestamp format '{json_str}' was not parsed! parse_datetime_string failed."
905                );
906            }
907            other => {
908                panic!("Expected LocalDateTime, got {other:?}");
909            }
910        }
911    }
912
913    /// Test parse_datetime_string directly with PostgreSQL format
914    #[test]
915    fn test_parse_datetime_string_postgresql_format() {
916        // PostgreSQL to_jsonb format for TIMESTAMP columns
917        let result = parse_datetime_string("2024-11-13T20:15:33");
918        assert!(
919            result.is_some(),
920            "parse_datetime_string must handle PostgreSQL to_jsonb format '2024-11-13T20:15:33'"
921        );
922    }
923
924    #[test]
925    fn test_date_from_string() {
926        let jv = JsonValueWithSchema::new(json!("2024-06-15"), Type::Date);
927        let tv = TypedValue::from(jv);
928        if let Value::Date(dt) = tv.value {
929            assert_eq!(dt.format("%Y-%m-%d").to_string(), "2024-06-15");
930        } else {
931            panic!("Expected Date, got {:?}", tv.value);
932        }
933    }
934
935    #[test]
936    fn test_zero_date_literal_emits_zero_temporal() {
937        let jv = JsonValueWithSchema::new(json!("0000-00-00"), Type::Date);
938        let tv = TypedValue::from(jv);
939        assert!(matches!(
940            tv.value,
941            Value::ZeroTemporal {
942                intended_type: Type::Date,
943                ..
944            }
945        ));
946    }
947
948    #[test]
949    fn test_zero_datetime_literal_emits_zero_temporal() {
950        let jv = JsonValueWithSchema::new(json!("0000-00-00 00:00:00"), Type::LocalDateTime);
951        let tv = TypedValue::from(jv);
952        assert!(matches!(
953            tv.value,
954            Value::ZeroTemporal {
955                intended_type: Type::LocalDateTime,
956                ..
957            }
958        ));
959    }
960
961    #[test]
962    fn test_time_from_string() {
963        let jv = JsonValueWithSchema::new(json!("14:30:45"), Type::Time);
964        let tv = TypedValue::from(jv);
965        if let Value::Time(dt) = tv.value {
966            assert_eq!(dt.format("%H:%M:%S").to_string(), "14:30:45");
967        } else {
968            panic!("Expected Time, got {:?}", tv.value);
969        }
970    }
971
972    #[test]
973    fn test_json_object_conversion() {
974        let jv = JsonValueWithSchema::new(json!({"name": "test", "count": 42}), Type::Json);
975        let tv = TypedValue::from(jv);
976        if let Value::Json(json_val) = tv.value {
977            if let serde_json::Value::Object(map) = json_val.as_ref() {
978                assert!(
979                    matches!(map.get("name"), Some(serde_json::Value::String(s)) if s == "test")
980                );
981                assert!(
982                    matches!(map.get("count"), Some(serde_json::Value::Number(n)) if n.as_i64() == Some(42))
983                );
984            } else {
985                panic!("Expected Object");
986            }
987        } else {
988            panic!("Expected Json");
989        }
990    }
991
992    #[test]
993    fn test_json_array_conversion() {
994        // JSON columns in MySQL can contain arrays - stored as Json with array content
995        let jv = JsonValueWithSchema::new(json!([1, 2, 3]), Type::Json);
996        let tv = TypedValue::from(jv);
997        if let Value::Json(json_val) = tv.value {
998            if let serde_json::Value::Array(arr) = json_val.as_ref() {
999                assert_eq!(arr.len(), 3);
1000                assert!(
1001                    matches!(arr[0], serde_json::Value::Number(ref n) if n.as_i64() == Some(1))
1002                );
1003                assert!(
1004                    matches!(arr[1], serde_json::Value::Number(ref n) if n.as_i64() == Some(2))
1005                );
1006                assert!(
1007                    matches!(arr[2], serde_json::Value::Number(ref n) if n.as_i64() == Some(3))
1008                );
1009            } else {
1010                panic!("Expected Array inside Json");
1011            }
1012        } else {
1013            panic!("Expected Json, got {:?}", tv.value);
1014        }
1015    }
1016
1017    #[test]
1018    fn test_json_array_of_strings_conversion() {
1019        // JSON columns can contain arrays of strings (e.g., tags field) - stored as Json
1020        let jv = JsonValueWithSchema::new(json!(["tag1", "tag2", "tag3"]), Type::Json);
1021        let tv = TypedValue::from(jv);
1022        if let Value::Json(json_val) = tv.value {
1023            if let serde_json::Value::Array(arr) = json_val.as_ref() {
1024                assert_eq!(arr.len(), 3);
1025                assert!(matches!(arr[0], serde_json::Value::String(ref s) if s == "tag1"));
1026                assert!(matches!(arr[1], serde_json::Value::String(ref s) if s == "tag2"));
1027                assert!(matches!(arr[2], serde_json::Value::String(ref s) if s == "tag3"));
1028            } else {
1029                panic!("Expected Array inside Json");
1030            }
1031        } else {
1032            panic!("Expected Json, got {:?}", tv.value);
1033        }
1034    }
1035
1036    #[test]
1037    fn test_jsonb_array_conversion() {
1038        // JSONB columns can also contain arrays - stored as Jsonb
1039        let jv = JsonValueWithSchema::new(json!([1, 2, 3]), Type::Jsonb);
1040        let tv = TypedValue::from(jv);
1041        if let Value::Jsonb(json_val) = tv.value {
1042            if let serde_json::Value::Array(arr) = json_val.as_ref() {
1043                assert_eq!(arr.len(), 3);
1044                assert!(
1045                    matches!(arr[0], serde_json::Value::Number(ref n) if n.as_i64() == Some(1))
1046                );
1047            } else {
1048                panic!("Expected Array inside Jsonb");
1049            }
1050        } else {
1051            panic!("Expected Jsonb, got {:?}", tv.value);
1052        }
1053    }
1054
1055    #[test]
1056    fn test_array_int_conversion() {
1057        let jv = JsonValueWithSchema::new(
1058            json!([1, 2, 3]),
1059            Type::Array {
1060                element_type: Box::new(Type::Int32),
1061            },
1062        );
1063        let tv = TypedValue::from(jv);
1064        if let Value::Array { elements, .. } = tv.value {
1065            assert_eq!(elements.len(), 3);
1066            assert!(matches!(elements[0], Value::Int32(1)));
1067        } else {
1068            panic!("Expected Array");
1069        }
1070    }
1071
1072    #[test]
1073    fn test_set_conversion() {
1074        let jv = JsonValueWithSchema::new(
1075            json!(["a", "b"]),
1076            Type::Set {
1077                values: vec!["a".to_string(), "b".to_string(), "c".to_string()],
1078            },
1079        );
1080        let tv = TypedValue::from(jv);
1081        if let Value::Set { elements, .. } = tv.value {
1082            assert_eq!(elements.len(), 2);
1083            assert!(elements.contains(&"a".to_string()));
1084            assert!(elements.contains(&"b".to_string()));
1085        } else {
1086            panic!("Expected Set, got {:?}", tv.value);
1087        }
1088    }
1089
1090    #[test]
1091    fn test_enum_conversion() {
1092        let jv = JsonValueWithSchema::new(
1093            json!("active"),
1094            Type::Enum {
1095                values: vec!["active".to_string(), "inactive".to_string()],
1096            },
1097        );
1098        let tv = TypedValue::from(jv);
1099        if let Value::Enum { value, .. } = tv.value {
1100            assert_eq!(value, "active");
1101        } else {
1102            panic!("Expected Enum, got {:?}", tv.value);
1103        }
1104    }
1105
1106    #[test]
1107    fn test_geometry_conversion() {
1108        let jv = JsonValueWithSchema::new(
1109            json!({"type": "Point", "coordinates": [-73.97, 40.77]}),
1110            Type::Geometry {
1111                geometry_type: GeometryType::Point,
1112            },
1113        );
1114        let tv = TypedValue::from(jv);
1115        if let Value::Geometry { data, .. } = tv.value {
1116            use surreal_sync_core::values::GeometryData;
1117            let GeometryData(ref geo_json) = data;
1118            if let serde_json::Value::Object(map) = geo_json {
1119                assert!(
1120                    matches!(map.get("type"), Some(serde_json::Value::String(s)) if s == "Point")
1121                );
1122            } else {
1123                panic!("Expected Object inside GeometryData");
1124            }
1125        } else {
1126            panic!("Expected Geometry, got {:?}", tv.value);
1127        }
1128    }
1129
1130    #[test]
1131    fn test_extract_field() {
1132        let obj = json!({"name": "Alice", "age": 30})
1133            .as_object()
1134            .unwrap()
1135            .clone();
1136
1137        let name = extract_field(&obj, "name", &Type::Text);
1138        assert!(matches!(name.value, Value::Text(ref s) if s == "Alice"));
1139
1140        let age = extract_field(&obj, "age", &Type::Int32);
1141        assert!(matches!(age.value, Value::Int32(30)));
1142
1143        let missing = extract_field(&obj, "missing", &Type::Text);
1144        assert!(matches!(missing.value, Value::Null));
1145    }
1146
1147    #[test]
1148    fn test_parse_jsonl_line() {
1149        let line = r#"{"name": "Bob", "active": true, "score": 95.5}"#;
1150        let schema = vec![
1151            ("name".to_string(), Type::Text),
1152            ("active".to_string(), Type::Bool),
1153            ("score".to_string(), Type::Float64),
1154        ];
1155
1156        let values = parse_jsonl_line(line, &schema).unwrap();
1157        assert!(matches!(
1158            values.get("name").unwrap().value,
1159            Value::Text(ref s) if s == "Bob"
1160        ));
1161        assert!(matches!(
1162            values.get("active").unwrap().value,
1163            Value::Bool(true)
1164        ));
1165    }
1166
1167    #[test]
1168    fn test_duration_conversion() {
1169        // Test parsing ISO 8601 duration string "PT181S" (181 seconds)
1170        let jv = JsonValueWithSchema::new(json!("PT181S"), Type::Duration);
1171        let tv = TypedValue::from(jv);
1172        if let Value::Duration(d) = tv.value {
1173            assert_eq!(d.as_secs(), 181);
1174            assert_eq!(d.subsec_nanos(), 0);
1175        } else {
1176            panic!("Expected Duration, got {:?}", tv.value);
1177        }
1178    }
1179
1180    #[test]
1181    fn test_duration_with_nanos_conversion() {
1182        // Test parsing ISO 8601 duration string "PT60.123456789S" (60 seconds + 123456789 nanoseconds)
1183        let jv = JsonValueWithSchema::new(json!("PT60.123456789S"), Type::Duration);
1184        let tv = TypedValue::from(jv);
1185        if let Value::Duration(d) = tv.value {
1186            assert_eq!(d.as_secs(), 60);
1187            assert_eq!(d.subsec_nanos(), 123456789);
1188        } else {
1189            panic!("Expected Duration, got {:?}", tv.value);
1190        }
1191    }
1192
1193    #[test]
1194    fn test_duration_invalid_format() {
1195        // Test that invalid duration strings return null
1196        let jv = JsonValueWithSchema::new(json!("not a duration"), Type::Duration);
1197        let tv = TypedValue::from(jv);
1198        assert!(matches!(tv.value, Value::Null));
1199    }
1200
1201    #[test]
1202    fn test_json_to_universal_with_table_schema_array() {
1203        // Test that json_to_universal_with_table_schema correctly converts JSON arrays
1204        // when the schema defines the field as Array<Text>
1205        use surreal_sync_core::{ColumnDefinition, TableDefinition};
1206
1207        // Create a table schema with an array column
1208        let pk = ColumnDefinition::new("id", Type::Text);
1209        let columns = vec![ColumnDefinition::new(
1210            "tags",
1211            Type::Array {
1212                element_type: Box::new(Type::Text),
1213            },
1214        )];
1215        let table_schema = TableDefinition::new("test_table", pk, columns);
1216
1217        // Test converting a JSON array to Value::Array
1218        let json_array = json!(["tag1", "tag2", "tag3"]);
1219        let result =
1220            json_to_universal_with_table_schema(json_array, "tags", &table_schema).unwrap();
1221
1222        match result {
1223            Value::Array { elements, .. } => {
1224                assert_eq!(elements.len(), 3);
1225                assert!(matches!(&elements[0], Value::Text(s) if s == "tag1"));
1226                assert!(matches!(&elements[1], Value::Text(s) if s == "tag2"));
1227                assert!(matches!(&elements[2], Value::Text(s) if s == "tag3"));
1228            }
1229            other => panic!("Expected Array, got {other:?}"),
1230        }
1231    }
1232
1233    #[test]
1234    fn test_json_to_universal_with_table_schema_null_array() {
1235        // Test that null JSON values become Value::Null even for array columns
1236        use surreal_sync_core::{ColumnDefinition, TableDefinition};
1237
1238        let pk = ColumnDefinition::new("id", Type::Text);
1239        let columns = vec![ColumnDefinition::new(
1240            "tags",
1241            Type::Array {
1242                element_type: Box::new(Type::Text),
1243            },
1244        )];
1245        let table_schema = TableDefinition::new("test_table", pk, columns);
1246
1247        let json_null = json!(null);
1248        let result = json_to_universal_with_table_schema(json_null, "tags", &table_schema).unwrap();
1249
1250        assert!(
1251            matches!(result, Value::Null),
1252            "Expected Null, got {result:?}"
1253        );
1254    }
1255
1256    #[test]
1257    fn test_json_to_universal_with_table_schema_unknown_field() {
1258        // Test that unknown fields fall back to generic conversion
1259        use surreal_sync_core::{ColumnDefinition, TableDefinition};
1260
1261        let pk = ColumnDefinition::new("id", Type::Text);
1262        let columns = vec![];
1263        let table_schema = TableDefinition::new("test_table", pk, columns);
1264
1265        // Unknown field with JSON array should still convert using generic conversion
1266        let json_array = json!(["a", "b"]);
1267        let result =
1268            json_to_universal_with_table_schema(json_array, "unknown_field", &table_schema)
1269                .unwrap();
1270
1271        match result {
1272            Value::Array { elements, .. } => {
1273                assert_eq!(elements.len(), 2);
1274            }
1275            other => panic!("Expected Array from generic conversion, got {other:?}"),
1276        }
1277    }
1278}