Skip to main content

cynos_database/
convert.rs

1//! Type conversion utilities between JavaScript and Rust types.
2//!
3//! This module provides functions to convert between JS values and Cynos's
4//! internal types (Value, Row, etc.).
5
6use alloc::rc::Rc;
7use alloc::string::String;
8use alloc::vec::Vec;
9use cynos_core::schema::Table;
10use cynos_core::{DataType, Row, Value};
11use wasm_bindgen::prelude::*;
12
13/// Converts a JavaScript value to an Cynos Value.
14///
15/// The conversion is based on the expected data type:
16/// - Boolean: JS boolean
17/// - Int32/Int64: JS number (truncated to integer)
18/// - Float64: JS number
19/// - String: JS string
20/// - DateTime: JS number (Unix timestamp in ms) or Date object
21/// - Bytes: JS Uint8Array
22/// - Jsonb: Any JS value (serialized to JSON)
23pub fn js_to_value(js: &JsValue, expected_type: DataType) -> Result<Value, JsValue> {
24    if js.is_null() || js.is_undefined() {
25        return Ok(Value::Null);
26    }
27
28    match expected_type {
29        DataType::Boolean => {
30            if let Some(b) = js.as_bool() {
31                Ok(Value::Boolean(b))
32            } else {
33                Err(JsValue::from_str("Expected boolean value"))
34            }
35        }
36        DataType::Int32 => {
37            if let Some(n) = js.as_f64() {
38                Ok(Value::Int32(n as i32))
39            } else {
40                Err(JsValue::from_str("Expected number value"))
41            }
42        }
43        DataType::Int64 => {
44            if let Some(n) = js.as_f64() {
45                Ok(Value::Int64(n as i64))
46            } else if js.is_bigint() {
47                // Handle BigInt
48                let s = js_sys::BigInt::from(js.clone())
49                    .to_string(10)
50                    .map_err(|_| JsValue::from_str("Failed to convert BigInt"))?;
51                let n: i64 = String::from(s)
52                    .parse()
53                    .map_err(|_| JsValue::from_str("BigInt out of i64 range"))?;
54                Ok(Value::Int64(n))
55            } else {
56                Err(JsValue::from_str("Expected number or BigInt value"))
57            }
58        }
59        DataType::Float64 => {
60            if let Some(n) = js.as_f64() {
61                Ok(Value::Float64(n))
62            } else {
63                Err(JsValue::from_str("Expected number value"))
64            }
65        }
66        DataType::String => {
67            if let Some(s) = js.as_string() {
68                Ok(Value::String(s))
69            } else {
70                Err(JsValue::from_str("Expected string value"))
71            }
72        }
73        DataType::DateTime => {
74            if let Some(n) = js.as_f64() {
75                Ok(Value::DateTime(n as i64))
76            } else if js.is_object() {
77                // Try to get time from Date object
78                let date = js_sys::Date::from(js.clone());
79                Ok(Value::DateTime(date.get_time() as i64))
80            } else {
81                Err(JsValue::from_str("Expected number or Date value"))
82            }
83        }
84        DataType::Bytes => {
85            if js.is_object() {
86                let arr = js_sys::Uint8Array::new(js);
87                Ok(Value::Bytes(arr.to_vec()))
88            } else {
89                Err(JsValue::from_str("Expected Uint8Array value"))
90            }
91        }
92        DataType::Jsonb => {
93            // Serialize any JS value to JSON bytes
94            let json_str = js_sys::JSON::stringify(js)
95                .map_err(|_| JsValue::from_str("Failed to stringify JSON"))?;
96            let bytes = String::from(json_str).into_bytes();
97            Ok(Value::Jsonb(cynos_core::JsonbValue::new(bytes)))
98        }
99    }
100}
101
102/// Converts an Cynos Value to a JavaScript value.
103pub fn value_to_js(value: &Value) -> JsValue {
104    match value {
105        Value::Null => JsValue::NULL,
106        Value::Boolean(b) => JsValue::from_bool(*b),
107        Value::Int32(n) => JsValue::from_f64(*n as f64),
108        Value::Int64(n) => JsValue::from_f64(*n as f64),
109        Value::Float64(n) => JsValue::from_f64(*n),
110        Value::String(s) => JsValue::from_str(s),
111        Value::DateTime(ts) => {
112            // Return as Date object
113            js_sys::Date::new(&JsValue::from_f64(*ts as f64)).into()
114        }
115        Value::Bytes(b) => {
116            let arr = js_sys::Uint8Array::new_with_length(b.len() as u32);
117            arr.copy_from(b);
118            arr.into()
119        }
120        Value::Jsonb(j) => {
121            // Parse JSON bytes back to JS value
122            if let Ok(s) = core::str::from_utf8(&j.0) {
123                js_sys::JSON::parse(s).unwrap_or(JsValue::NULL)
124            } else {
125                JsValue::NULL
126            }
127        }
128    }
129}
130
131/// Converts an Cynos Row to a JavaScript object.
132///
133/// The returned object has properties named after the table columns.
134pub fn row_to_js(row: &Row, schema: &Table) -> JsValue {
135    let obj = js_sys::Object::new();
136    let columns = schema.columns();
137
138    for (i, col) in columns.iter().enumerate() {
139        if let Some(value) = row.get(i) {
140            let js_val = value_to_js(value);
141            js_sys::Reflect::set(&obj, &JsValue::from_str(col.name()), &js_val).ok();
142        }
143    }
144
145    obj.into()
146}
147
148/// Converts a JavaScript object to an Cynos Row.
149///
150/// The object properties are matched against the table schema columns.
151pub fn js_to_row(js: &JsValue, schema: &Table, row_id: u64) -> Result<Row, JsValue> {
152    if !js.is_object() {
153        return Err(JsValue::from_str("Expected object value"));
154    }
155
156    let columns = schema.columns();
157    let mut values = Vec::with_capacity(columns.len());
158
159    for col in columns {
160        let prop = js_sys::Reflect::get(js, &JsValue::from_str(col.name()))
161            .map_err(|_| JsValue::from_str(&alloc::format!("Missing column: {}", col.name())))?;
162
163        let value = if prop.is_undefined() || prop.is_null() {
164            if col.is_nullable() {
165                Value::Null
166            } else {
167                return Err(JsValue::from_str(&alloc::format!(
168                    "Column {} is not nullable",
169                    col.name()
170                )));
171            }
172        } else {
173            js_to_value(&prop, col.data_type())?
174        };
175
176        values.push(value);
177    }
178
179    Ok(Row::new(row_id, values))
180}
181
182/// Converts a JavaScript array of objects to a vector of Rows.
183pub fn js_array_to_rows(
184    js: &JsValue,
185    schema: &Table,
186    start_row_id: u64,
187) -> Result<Vec<Row>, JsValue> {
188    if !js_sys::Array::is_array(js) {
189        return Err(JsValue::from_str("Expected array value"));
190    }
191
192    let arr = js_sys::Array::from(js);
193    let mut rows = Vec::with_capacity(arr.length() as usize);
194
195    for (i, item) in arr.iter().enumerate() {
196        let row = js_to_row(&item, schema, start_row_id + i as u64)?;
197        rows.push(row);
198    }
199
200    Ok(rows)
201}
202
203/// Converts a vector of Rows to a JavaScript array of objects.
204pub fn rows_to_js_array(rows: &[Rc<Row>], schema: &Table) -> JsValue {
205    let arr = js_sys::Array::new_with_length(rows.len() as u32);
206
207    for (i, row) in rows.iter().enumerate() {
208        let obj = row_to_js(row, schema);
209        arr.set(i as u32, obj);
210    }
211
212    arr.into()
213}
214
215/// Converts a vector of projected Rows to a JavaScript array of objects.
216///
217/// This function is used when only specific columns are selected (projection).
218/// The `column_names` parameter specifies the names of the projected columns
219/// in the order they appear in the row.
220pub fn projected_rows_to_js_array(rows: &[Rc<Row>], column_names: &[String]) -> JsValue {
221    let arr = js_sys::Array::new_with_length(rows.len() as u32);
222
223    // Extract just the column part from qualified names and count occurrences
224    let mut name_counts: hashbrown::HashMap<&str, usize> = hashbrown::HashMap::new();
225    for col_name in column_names {
226        let simple_name = if let Some(dot_pos) = col_name.find('.') {
227            &col_name[dot_pos + 1..]
228        } else {
229            col_name.as_str()
230        };
231        *name_counts.entry(simple_name).or_insert(0) += 1;
232    }
233
234    // Build the final column names - use simple names when unique, qualified when duplicate
235    let final_names: Vec<&str> = column_names
236        .iter()
237        .map(|col_name| {
238            if let Some(dot_pos) = col_name.find('.') {
239                let simple_name = &col_name[dot_pos + 1..];
240                if name_counts.get(simple_name).copied().unwrap_or(0) > 1 {
241                    // Duplicate - keep qualified name
242                    col_name.as_str()
243                } else {
244                    // Unique - use simple name
245                    simple_name
246                }
247            } else {
248                col_name.as_str()
249            }
250        })
251        .collect();
252
253    for (i, row) in rows.iter().enumerate() {
254        let obj = js_sys::Object::new();
255        for (col_idx, col_name) in final_names.iter().enumerate() {
256            if let Some(value) = row.get(col_idx) {
257                let js_val = value_to_js(value);
258                js_sys::Reflect::set(&obj, &JsValue::from_str(col_name), &js_val).ok();
259            }
260        }
261        arr.set(i as u32, obj.into());
262    }
263
264    arr.into()
265}
266
267/// Converts a vector of Rows to a JavaScript array of objects using multiple schemas.
268///
269/// This function is used for JOIN queries where the result contains columns from multiple tables.
270/// The `schemas` parameter specifies the schemas of all joined tables in order.
271/// For duplicate column names across tables, we use `table.column` format to distinguish them.
272pub fn joined_rows_to_js_array(rows: &[Rc<Row>], schemas: &[&Table]) -> JsValue {
273    let arr = js_sys::Array::new_with_length(rows.len() as u32);
274
275    // First pass: count occurrences of each column name
276    let mut name_counts: hashbrown::HashMap<&str, usize> = hashbrown::HashMap::new();
277    for schema in schemas {
278        for col in schema.columns() {
279            *name_counts.entry(col.name()).or_insert(0) += 1;
280        }
281    }
282
283    // Second pass: build column mapping with qualified names for duplicates
284    let mut column_names: Vec<String> = Vec::new();
285    for schema in schemas {
286        let table_name = schema.name();
287        for col in schema.columns() {
288            let col_name = col.name();
289            if name_counts.get(col_name).copied().unwrap_or(0) > 1 {
290                // Duplicate column name - use table.column format
291                column_names.push(alloc::format!("{}.{}", table_name, col_name));
292            } else {
293                // Unique column name - use as-is
294                column_names.push(col_name.to_string());
295            }
296        }
297    }
298
299    for (i, row) in rows.iter().enumerate() {
300        let obj = js_sys::Object::new();
301        for (col_idx, col_name) in column_names.iter().enumerate() {
302            if let Some(value) = row.get(col_idx) {
303                let js_val = value_to_js(value);
304                js_sys::Reflect::set(&obj, &JsValue::from_str(col_name), &js_val).ok();
305            }
306        }
307        arr.set(i as u32, obj.into());
308    }
309
310    arr.into()
311}
312
313/// Infers the data type from a JavaScript value.
314pub fn infer_type(js: &JsValue) -> Option<DataType> {
315    if js.is_null() || js.is_undefined() {
316        None
317    } else if js.as_bool().is_some() {
318        Some(DataType::Boolean)
319    } else if js.is_bigint() {
320        Some(DataType::Int64)
321    } else if js.as_f64().is_some() {
322        // Check if it's an integer
323        let n = js.as_f64().unwrap();
324        if n.fract() == 0.0 && n >= i32::MIN as f64 && n <= i32::MAX as f64 {
325            Some(DataType::Int32)
326        } else {
327            Some(DataType::Float64)
328        }
329    } else if js.as_string().is_some() {
330        Some(DataType::String)
331    } else if js.is_object() {
332        // Could be Date, Uint8Array, or generic object (JSONB)
333        if js.is_instance_of::<js_sys::Date>() {
334            Some(DataType::DateTime)
335        } else if js.is_instance_of::<js_sys::Uint8Array>() {
336            Some(DataType::Bytes)
337        } else {
338            Some(DataType::Jsonb)
339        }
340    } else {
341        None
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348    use wasm_bindgen_test::*;
349
350    wasm_bindgen_test_configure!(run_in_browser);
351
352    #[wasm_bindgen_test]
353    fn test_js_to_value_boolean() {
354        let js = JsValue::from_bool(true);
355        let result = js_to_value(&js, DataType::Boolean).unwrap();
356        assert_eq!(result, Value::Boolean(true));
357    }
358
359    #[wasm_bindgen_test]
360    fn test_js_to_value_int32() {
361        let js = JsValue::from_f64(42.0);
362        let result = js_to_value(&js, DataType::Int32).unwrap();
363        assert_eq!(result, Value::Int32(42));
364    }
365
366    #[wasm_bindgen_test]
367    fn test_js_to_value_int64() {
368        let js = JsValue::from_f64(1234567890.0);
369        let result = js_to_value(&js, DataType::Int64).unwrap();
370        assert_eq!(result, Value::Int64(1234567890));
371    }
372
373    #[wasm_bindgen_test]
374    fn test_js_to_value_float64() {
375        let js = JsValue::from_f64(3.14159);
376        let result = js_to_value(&js, DataType::Float64).unwrap();
377        assert_eq!(result, Value::Float64(3.14159));
378    }
379
380    #[wasm_bindgen_test]
381    fn test_js_to_value_string() {
382        let js = JsValue::from_str("hello");
383        let result = js_to_value(&js, DataType::String).unwrap();
384        assert_eq!(result, Value::String("hello".to_string()));
385    }
386
387    #[wasm_bindgen_test]
388    fn test_js_to_value_null() {
389        let js = JsValue::NULL;
390        let result = js_to_value(&js, DataType::String).unwrap();
391        assert_eq!(result, Value::Null);
392    }
393
394    #[wasm_bindgen_test]
395    fn test_value_to_js_boolean() {
396        let value = Value::Boolean(true);
397        let js = value_to_js(&value);
398        assert_eq!(js.as_bool(), Some(true));
399    }
400
401    #[wasm_bindgen_test]
402    fn test_value_to_js_int32() {
403        let value = Value::Int32(42);
404        let js = value_to_js(&value);
405        assert_eq!(js.as_f64(), Some(42.0));
406    }
407
408    #[wasm_bindgen_test]
409    fn test_value_to_js_string() {
410        let value = Value::String("hello".to_string());
411        let js = value_to_js(&value);
412        assert_eq!(js.as_string(), Some("hello".to_string()));
413    }
414
415    #[wasm_bindgen_test]
416    fn test_value_to_js_null() {
417        let value = Value::Null;
418        let js = value_to_js(&value);
419        assert!(js.is_null());
420    }
421
422    #[wasm_bindgen_test]
423    fn test_infer_type_boolean() {
424        let js = JsValue::from_bool(true);
425        assert_eq!(infer_type(&js), Some(DataType::Boolean));
426    }
427
428    #[wasm_bindgen_test]
429    fn test_infer_type_number() {
430        let js = JsValue::from_f64(42.0);
431        assert_eq!(infer_type(&js), Some(DataType::Int32));
432
433        let js = JsValue::from_f64(3.14);
434        assert_eq!(infer_type(&js), Some(DataType::Float64));
435    }
436
437    #[wasm_bindgen_test]
438    fn test_infer_type_string() {
439        let js = JsValue::from_str("hello");
440        assert_eq!(infer_type(&js), Some(DataType::String));
441    }
442
443    #[wasm_bindgen_test]
444    fn test_infer_type_null() {
445        let js = JsValue::NULL;
446        assert_eq!(infer_type(&js), None);
447    }
448}