cynos_database/
convert.rs1use 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
13pub 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 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 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 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
102pub 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 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 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
131pub 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
148pub 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
182pub 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
203pub 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
215pub 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 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 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 col_name.as_str()
243 } else {
244 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
267pub 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 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 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 column_names.push(alloc::format!("{}.{}", table_name, col_name));
292 } else {
293 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
313pub 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 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 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}