1use crate::convert::js_to_value;
7use alloc::boxed::Box;
8use alloc::string::{String, ToString};
9use alloc::vec::Vec;
10use cynos_core::{DataType, Value};
11use cynos_query::ast::Expr as AstExpr;
12use wasm_bindgen::prelude::*;
13
14#[wasm_bindgen]
16#[derive(Clone, Debug)]
17pub struct Column {
18 table: Option<String>,
19 name: String,
20 index: Option<usize>,
21}
22
23#[wasm_bindgen]
24impl Column {
25 #[wasm_bindgen(constructor)]
27 pub fn new(table: &str, name: &str) -> Self {
28 Self {
29 table: Some(table.to_string()),
30 name: name.to_string(),
31 index: None,
32 }
33 }
34
35 pub fn new_simple(name: &str) -> Self {
39 if let Some(dot_pos) = name.find('.') {
40 let table = &name[..dot_pos];
41 let col = &name[dot_pos + 1..];
42 Self {
43 table: Some(table.to_string()),
44 name: col.to_string(),
45 index: None,
46 }
47 } else {
48 Self {
49 table: None,
50 name: name.to_string(),
51 index: None,
52 }
53 }
54 }
55
56 pub fn with_index(mut self, index: usize) -> Self {
58 self.index = Some(index);
59 self
60 }
61
62 #[wasm_bindgen(getter)]
64 pub fn name(&self) -> String {
65 self.name.clone()
66 }
67
68 #[wasm_bindgen(getter, js_name = tableName)]
70 pub fn table_name(&self) -> Option<String> {
71 self.table.clone()
72 }
73
74 pub fn eq(&self, value: &JsValue) -> Expr {
76 Expr::comparison(self.clone(), ComparisonOp::Eq, value.clone())
77 }
78
79 pub fn ne(&self, value: &JsValue) -> Expr {
81 Expr::comparison(self.clone(), ComparisonOp::Ne, value.clone())
82 }
83
84 pub fn gt(&self, value: &JsValue) -> Expr {
86 Expr::comparison(self.clone(), ComparisonOp::Gt, value.clone())
87 }
88
89 pub fn gte(&self, value: &JsValue) -> Expr {
91 Expr::comparison(self.clone(), ComparisonOp::Gte, value.clone())
92 }
93
94 pub fn lt(&self, value: &JsValue) -> Expr {
96 Expr::comparison(self.clone(), ComparisonOp::Lt, value.clone())
97 }
98
99 pub fn lte(&self, value: &JsValue) -> Expr {
101 Expr::comparison(self.clone(), ComparisonOp::Lte, value.clone())
102 }
103
104 pub fn between(&self, low: &JsValue, high: &JsValue) -> Expr {
106 Expr::between(self.clone(), low.clone(), high.clone())
107 }
108
109 #[wasm_bindgen(js_name = notBetween)]
111 pub fn not_between(&self, low: &JsValue, high: &JsValue) -> Expr {
112 Expr::not_between(self.clone(), low.clone(), high.clone())
113 }
114
115 #[wasm_bindgen(js_name = "in")]
117 pub fn in_(&self, values: &JsValue) -> Expr {
118 Expr::in_list(self.clone(), values.clone())
119 }
120
121 #[wasm_bindgen(js_name = notIn)]
123 pub fn not_in(&self, values: &JsValue) -> Expr {
124 Expr::not_in_list(self.clone(), values.clone())
125 }
126
127 pub fn like(&self, pattern: &str) -> Expr {
129 Expr::like(self.clone(), pattern)
130 }
131
132 #[wasm_bindgen(js_name = notLike)]
134 pub fn not_like(&self, pattern: &str) -> Expr {
135 Expr::not_like(self.clone(), pattern)
136 }
137
138 #[wasm_bindgen(js_name = "match")]
140 pub fn regex_match(&self, pattern: &str) -> Expr {
141 Expr::regex_match(self.clone(), pattern)
142 }
143
144 #[wasm_bindgen(js_name = notMatch)]
146 pub fn not_regex_match(&self, pattern: &str) -> Expr {
147 Expr::not_regex_match(self.clone(), pattern)
148 }
149
150 #[wasm_bindgen(js_name = isNull)]
152 pub fn is_null(&self) -> Expr {
153 Expr::is_null(self.clone())
154 }
155
156 #[wasm_bindgen(js_name = isNotNull)]
158 pub fn is_not_null(&self) -> Expr {
159 Expr::is_not_null(self.clone())
160 }
161
162 pub fn get(&self, path: &str) -> JsonbColumn {
164 JsonbColumn {
165 column: self.clone(),
166 path: path.to_string(),
167 }
168 }
169
170 pub(crate) fn to_ast(&self) -> AstExpr {
172 AstExpr::column(
173 self.table.as_deref().unwrap_or(""),
174 &self.name,
175 self.index.unwrap_or(0),
176 )
177 }
178}
179
180#[wasm_bindgen]
182#[derive(Clone, Debug)]
183pub struct JsonbColumn {
184 column: Column,
185 path: String,
186}
187
188#[wasm_bindgen]
189impl JsonbColumn {
190 pub fn eq(&self, value: &JsValue) -> Expr {
192 Expr::jsonb_eq(self.column.clone(), &self.path, value.clone())
193 }
194
195 pub fn contains(&self, value: &JsValue) -> Expr {
197 Expr::jsonb_contains(self.column.clone(), &self.path, value.clone())
198 }
199
200 pub fn exists(&self) -> Expr {
202 Expr::jsonb_exists(self.column.clone(), &self.path)
203 }
204}
205
206#[derive(Clone, Copy, Debug, PartialEq, Eq)]
208pub enum ComparisonOp {
209 Eq,
210 Ne,
211 Gt,
212 Gte,
213 Lt,
214 Lte,
215}
216
217#[wasm_bindgen]
219#[derive(Clone, Debug)]
220pub struct Expr {
221 inner: ExprInner,
222}
223
224#[derive(Clone, Debug)]
225#[allow(dead_code)]
226pub(crate) enum ExprInner {
227 Comparison {
228 column: Column,
229 op: ComparisonOp,
230 value: JsValue,
231 },
232 Between {
233 column: Column,
234 low: JsValue,
235 high: JsValue,
236 },
237 NotBetween {
238 column: Column,
239 low: JsValue,
240 high: JsValue,
241 },
242 InList {
243 column: Column,
244 values: JsValue,
245 },
246 NotInList {
247 column: Column,
248 values: JsValue,
249 },
250 Like {
251 column: Column,
252 pattern: String,
253 },
254 NotLike {
255 column: Column,
256 pattern: String,
257 },
258 Match {
259 column: Column,
260 pattern: String,
261 },
262 NotMatch {
263 column: Column,
264 pattern: String,
265 },
266 IsNull {
267 column: Column,
268 },
269 IsNotNull {
270 column: Column,
271 },
272 JsonbEq {
273 column: Column,
274 path: String,
275 value: JsValue,
276 },
277 JsonbContains {
278 column: Column,
279 path: String,
280 value: JsValue,
281 },
282 JsonbExists {
283 column: Column,
284 path: String,
285 },
286 And {
287 left: Box<Expr>,
288 right: Box<Expr>,
289 },
290 Or {
291 left: Box<Expr>,
292 right: Box<Expr>,
293 },
294 Not {
295 inner: Box<Expr>,
296 },
297 ColumnRef {
298 column: Column,
299 },
300 Literal {
301 value: JsValue,
302 },
303 True,
304}
305
306impl Expr {
307 pub(crate) fn comparison(column: Column, op: ComparisonOp, value: JsValue) -> Self {
308 Self {
309 inner: ExprInner::Comparison { column, op, value },
310 }
311 }
312
313 pub(crate) fn between(column: Column, low: JsValue, high: JsValue) -> Self {
314 Self {
315 inner: ExprInner::Between { column, low, high },
316 }
317 }
318
319 pub(crate) fn not_between(column: Column, low: JsValue, high: JsValue) -> Self {
320 Self {
321 inner: ExprInner::NotBetween { column, low, high },
322 }
323 }
324
325 pub(crate) fn in_list(column: Column, values: JsValue) -> Self {
326 Self {
327 inner: ExprInner::InList { column, values },
328 }
329 }
330
331 pub(crate) fn not_in_list(column: Column, values: JsValue) -> Self {
332 Self {
333 inner: ExprInner::NotInList { column, values },
334 }
335 }
336
337 pub(crate) fn like(column: Column, pattern: &str) -> Self {
338 Self {
339 inner: ExprInner::Like {
340 column,
341 pattern: pattern.to_string(),
342 },
343 }
344 }
345
346 pub(crate) fn not_like(column: Column, pattern: &str) -> Self {
347 Self {
348 inner: ExprInner::NotLike {
349 column,
350 pattern: pattern.to_string(),
351 },
352 }
353 }
354
355 pub(crate) fn regex_match(column: Column, pattern: &str) -> Self {
356 Self {
357 inner: ExprInner::Match {
358 column,
359 pattern: pattern.to_string(),
360 },
361 }
362 }
363
364 pub(crate) fn not_regex_match(column: Column, pattern: &str) -> Self {
365 Self {
366 inner: ExprInner::NotMatch {
367 column,
368 pattern: pattern.to_string(),
369 },
370 }
371 }
372
373 pub(crate) fn is_null(column: Column) -> Self {
374 Self {
375 inner: ExprInner::IsNull { column },
376 }
377 }
378
379 pub(crate) fn is_not_null(column: Column) -> Self {
380 Self {
381 inner: ExprInner::IsNotNull { column },
382 }
383 }
384
385 pub(crate) fn jsonb_eq(column: Column, path: &str, value: JsValue) -> Self {
386 Self {
387 inner: ExprInner::JsonbEq {
388 column,
389 path: path.to_string(),
390 value,
391 },
392 }
393 }
394
395 pub(crate) fn jsonb_contains(column: Column, path: &str, value: JsValue) -> Self {
396 Self {
397 inner: ExprInner::JsonbContains {
398 column,
399 path: path.to_string(),
400 value,
401 },
402 }
403 }
404
405 pub(crate) fn jsonb_exists(column: Column, path: &str) -> Self {
406 Self {
407 inner: ExprInner::JsonbExists {
408 column,
409 path: path.to_string(),
410 },
411 }
412 }
413
414 #[allow(dead_code)]
415 pub(crate) fn column_ref(column: Column) -> Self {
416 Self {
417 inner: ExprInner::ColumnRef { column },
418 }
419 }
420
421 #[allow(dead_code)]
422 pub(crate) fn literal(value: JsValue) -> Self {
423 Self {
424 inner: ExprInner::Literal { value },
425 }
426 }
427
428 #[allow(dead_code)]
429 pub(crate) fn true_expr() -> Self {
430 Self {
431 inner: ExprInner::True,
432 }
433 }
434
435 pub(crate) fn inner(&self) -> &ExprInner {
437 &self.inner
438 }
439}
440
441#[wasm_bindgen]
442impl Expr {
443 pub fn and(&self, other: &Expr) -> Expr {
445 Expr {
446 inner: ExprInner::And {
447 left: Box::new(self.clone()),
448 right: Box::new(other.clone()),
449 },
450 }
451 }
452
453 pub fn or(&self, other: &Expr) -> Expr {
455 Expr {
456 inner: ExprInner::Or {
457 left: Box::new(self.clone()),
458 right: Box::new(other.clone()),
459 },
460 }
461 }
462
463 pub fn not(&self) -> Expr {
465 Expr {
466 inner: ExprInner::Not {
467 inner: Box::new(self.clone()),
468 },
469 }
470 }
471}
472
473impl Expr {
474 pub(crate) fn to_ast_with_table(&self, get_column_info: &impl Fn(&str) -> Option<(String, usize, DataType)>) -> AstExpr {
476 match &self.inner {
477 ExprInner::Comparison { column, op, value } => {
478 let lookup_key = if let Some(ref table) = column.table {
480 alloc::format!("{}.{}", table, column.name)
481 } else {
482 column.name.clone()
483 };
484
485 let col_expr = if let Some((table, idx, _dt)) = get_column_info(&lookup_key) {
486 let table_name = if table.is_empty() {
487 column.table.as_deref().unwrap_or("")
488 } else {
489 &table
490 };
491 AstExpr::column(table_name, &column.name, idx)
492 } else {
493 column.to_ast()
494 };
495
496 let right_expr = if let Some(s) = value.as_string() {
499 if let Some((table, idx, _dt)) = get_column_info(&s) {
500 let col_name = if let Some(dot_pos) = s.find('.') {
502 &s[dot_pos + 1..]
503 } else {
504 &s
505 };
506 AstExpr::column(&table, col_name, idx)
507 } else {
508 let val = if let Some((_, _, dt)) = get_column_info(&lookup_key) {
510 js_to_value(value, dt).unwrap_or(Value::String(s))
511 } else {
512 Value::String(s)
513 };
514 AstExpr::literal(val)
515 }
516 } else if value.is_object() {
517 if let Ok(name_val) = js_sys::Reflect::get(value, &JsValue::from_str("name")) {
519 if let Some(col_name) = name_val.as_string() {
520 let table_name = js_sys::Reflect::get(value, &JsValue::from_str("tableName"))
522 .ok()
523 .and_then(|v| v.as_string());
524
525 let col_lookup = if let Some(ref tbl) = table_name {
527 alloc::format!("{}.{}", tbl, col_name)
528 } else {
529 col_name.clone()
530 };
531
532 if let Some((table, idx, _dt)) = get_column_info(&col_lookup) {
533 AstExpr::column(&table, &col_name, idx)
534 } else {
535 AstExpr::column(table_name.as_deref().unwrap_or(""), &col_name, 0)
537 }
538 } else {
539 AstExpr::literal(Value::Null)
541 }
542 } else {
543 AstExpr::literal(Value::Null)
545 }
546 } else {
547 let val = if let Some((_, _, dt)) = get_column_info(&lookup_key) {
548 js_to_value(value, dt).unwrap_or(Value::Null)
549 } else {
550 if let Some(n) = value.as_f64() {
552 if n.fract() == 0.0 {
553 Value::Int64(n as i64)
554 } else {
555 Value::Float64(n)
556 }
557 } else if let Some(b) = value.as_bool() {
558 Value::Boolean(b)
559 } else {
560 Value::Null
561 }
562 };
563 AstExpr::literal(val)
564 };
565
566 match op {
567 ComparisonOp::Eq => AstExpr::eq(col_expr, right_expr),
568 ComparisonOp::Ne => AstExpr::ne(col_expr, right_expr),
569 ComparisonOp::Gt => AstExpr::gt(col_expr, right_expr),
570 ComparisonOp::Gte => AstExpr::gte(col_expr, right_expr),
571 ComparisonOp::Lt => AstExpr::lt(col_expr, right_expr),
572 ComparisonOp::Lte => AstExpr::lte(col_expr, right_expr),
573 }
574 }
575 ExprInner::Between { column, low, high } => {
576 let (table, idx, dt) = get_column_info(&column.name).unwrap_or((String::new(), 0, DataType::Float64));
577 let table_name = if table.is_empty() {
578 column.table.as_deref().unwrap_or("")
579 } else {
580 &table
581 };
582 let col_expr = AstExpr::column(table_name, &column.name, idx);
583 let low_val = js_to_value(low, dt).unwrap_or(Value::Null);
584 let high_val = js_to_value(high, dt).unwrap_or(Value::Null);
585 AstExpr::between(col_expr, AstExpr::literal(low_val), AstExpr::literal(high_val))
586 }
587 ExprInner::NotBetween { column, low, high } => {
588 let (_, idx, dt) = get_column_info(&column.name).unwrap_or((String::new(), 0, DataType::Float64));
589 let col_expr = AstExpr::column(column.table.as_deref().unwrap_or(""), &column.name, idx);
590 let low_val = js_to_value(low, dt).unwrap_or(Value::Null);
591 let high_val = js_to_value(high, dt).unwrap_or(Value::Null);
592 AstExpr::not_between(col_expr, AstExpr::literal(low_val), AstExpr::literal(high_val))
593 }
594 ExprInner::InList { column, values } => {
595 let (_, idx, dt) = get_column_info(&column.name).unwrap_or((String::new(), 0, DataType::String));
596 let col_expr = AstExpr::column(column.table.as_deref().unwrap_or(""), &column.name, idx);
597
598 let arr = js_sys::Array::from(values);
599 let vals: Vec<Value> = arr
600 .iter()
601 .filter_map(|v| js_to_value(&v, dt).ok())
602 .collect();
603
604 AstExpr::in_list(col_expr, vals)
605 }
606 ExprInner::NotInList { column, values } => {
607 let (_, idx, dt) = get_column_info(&column.name).unwrap_or((String::new(), 0, DataType::String));
608 let col_expr = AstExpr::column(column.table.as_deref().unwrap_or(""), &column.name, idx);
609
610 let arr = js_sys::Array::from(values);
611 let vals: Vec<Value> = arr
612 .iter()
613 .filter_map(|v| js_to_value(&v, dt).ok())
614 .collect();
615
616 AstExpr::not_in_list(col_expr, vals)
617 }
618 ExprInner::Like { column, pattern } => {
619 let idx = get_column_info(&column.name).map(|(_, i, _)| i).unwrap_or(0);
620 let col_expr = AstExpr::column(column.table.as_deref().unwrap_or(""), &column.name, idx);
621 AstExpr::like(col_expr, pattern)
622 }
623 ExprInner::NotLike { column, pattern } => {
624 let idx = get_column_info(&column.name).map(|(_, i, _)| i).unwrap_or(0);
625 let col_expr = AstExpr::column(column.table.as_deref().unwrap_or(""), &column.name, idx);
626 AstExpr::not_like(col_expr, pattern)
627 }
628 ExprInner::Match { column, pattern } => {
629 let idx = get_column_info(&column.name).map(|(_, i, _)| i).unwrap_or(0);
630 let col_expr = AstExpr::column(column.table.as_deref().unwrap_or(""), &column.name, idx);
631 AstExpr::regex_match(col_expr, pattern)
632 }
633 ExprInner::NotMatch { column, pattern } => {
634 let idx = get_column_info(&column.name).map(|(_, i, _)| i).unwrap_or(0);
635 let col_expr = AstExpr::column(column.table.as_deref().unwrap_or(""), &column.name, idx);
636 AstExpr::not_regex_match(col_expr, pattern)
637 }
638 ExprInner::IsNull { column } => {
639 let idx = get_column_info(&column.name).map(|(_, i, _)| i).unwrap_or(0);
640 let col_expr = AstExpr::column(column.table.as_deref().unwrap_or(""), &column.name, idx);
641 AstExpr::is_null(col_expr)
642 }
643 ExprInner::IsNotNull { column } => {
644 let idx = get_column_info(&column.name).map(|(_, i, _)| i).unwrap_or(0);
645 let col_expr = AstExpr::column(column.table.as_deref().unwrap_or(""), &column.name, idx);
646 AstExpr::is_not_null(col_expr)
647 }
648 ExprInner::JsonbEq { column, path, value } => {
649 let col_expr = if let Some((_, idx, _)) = get_column_info(&column.name) {
651 AstExpr::column(column.table.as_deref().unwrap_or(""), &column.name, idx)
652 } else {
653 column.to_ast()
654 };
655 let val = if let Some(s) = value.as_string() {
656 Value::String(s)
657 } else if let Some(n) = value.as_f64() {
658 Value::Float64(n)
659 } else {
660 Value::Null
661 };
662 AstExpr::jsonb_path_eq(col_expr, path, val)
663 }
664 ExprInner::JsonbContains { column, path, value: _ } => {
665 let col_expr = if let Some((_, idx, _)) = get_column_info(&column.name) {
666 AstExpr::column(column.table.as_deref().unwrap_or(""), &column.name, idx)
667 } else {
668 column.to_ast()
669 };
670 AstExpr::jsonb_contains(col_expr, path)
671 }
672 ExprInner::JsonbExists { column, path } => {
673 let col_expr = if let Some((_, idx, _)) = get_column_info(&column.name) {
674 AstExpr::column(column.table.as_deref().unwrap_or(""), &column.name, idx)
675 } else {
676 column.to_ast()
677 };
678 AstExpr::jsonb_exists(col_expr, path)
679 }
680 ExprInner::And { left, right } => {
681 let left_ast = left.to_ast_with_table(get_column_info);
682 let right_ast = right.to_ast_with_table(get_column_info);
683 AstExpr::and(left_ast, right_ast)
684 }
685 ExprInner::Or { left, right } => {
686 let left_ast = left.to_ast_with_table(get_column_info);
687 let right_ast = right.to_ast_with_table(get_column_info);
688 AstExpr::or(left_ast, right_ast)
689 }
690 ExprInner::Not { inner } => {
691 let inner_ast = inner.to_ast_with_table(get_column_info);
692 AstExpr::not(inner_ast)
693 }
694 ExprInner::ColumnRef { column } => column.to_ast(),
695 ExprInner::Literal { value } => {
696 let val = if let Some(n) = value.as_f64() {
697 if n.fract() == 0.0 {
698 Value::Int64(n as i64)
699 } else {
700 Value::Float64(n)
701 }
702 } else if let Some(s) = value.as_string() {
703 Value::String(s)
704 } else if let Some(b) = value.as_bool() {
705 Value::Boolean(b)
706 } else {
707 Value::Null
708 };
709 AstExpr::literal(val)
710 }
711 ExprInner::True => AstExpr::literal(Value::Boolean(true)),
712 }
713 }
714}
715
716#[cfg(test)]
717mod tests {
718 use super::*;
719 use wasm_bindgen_test::*;
720
721 wasm_bindgen_test_configure!(run_in_browser);
722
723 #[wasm_bindgen_test]
724 fn test_column_eq() {
725 let col = Column::new_simple("age");
726 let expr = col.eq(&JsValue::from_f64(25.0));
727
728 match &expr.inner {
729 ExprInner::Comparison { column, op, .. } => {
730 assert_eq!(column.name, "age");
731 assert_eq!(*op, ComparisonOp::Eq);
732 }
733 _ => panic!("Expected Comparison"),
734 }
735 }
736
737 #[wasm_bindgen_test]
738 fn test_column_gt() {
739 let col = Column::new_simple("age");
740 let expr = col.gt(&JsValue::from_f64(18.0));
741
742 match &expr.inner {
743 ExprInner::Comparison { op, .. } => {
744 assert_eq!(*op, ComparisonOp::Gt);
745 }
746 _ => panic!("Expected Comparison"),
747 }
748 }
749
750 #[wasm_bindgen_test]
751 fn test_expr_and() {
752 let col = Column::new_simple("age");
753 let expr1 = col.gt(&JsValue::from_f64(18.0));
754 let expr2 = col.lt(&JsValue::from_f64(65.0));
755 let combined = expr1.and(&expr2);
756
757 match &combined.inner {
758 ExprInner::And { .. } => {}
759 _ => panic!("Expected And"),
760 }
761 }
762
763 #[wasm_bindgen_test]
764 fn test_expr_or() {
765 let col = Column::new_simple("status");
766 let expr1 = col.eq(&JsValue::from_str("active"));
767 let expr2 = col.eq(&JsValue::from_str("pending"));
768 let combined = expr1.or(&expr2);
769
770 match &combined.inner {
771 ExprInner::Or { .. } => {}
772 _ => panic!("Expected Or"),
773 }
774 }
775
776 #[wasm_bindgen_test]
777 fn test_expr_not() {
778 let col = Column::new_simple("deleted");
779 let expr = col.eq(&JsValue::from_bool(true)).not();
780
781 match &expr.inner {
782 ExprInner::Not { .. } => {}
783 _ => panic!("Expected Not"),
784 }
785 }
786
787 #[wasm_bindgen_test]
788 fn test_column_is_null() {
789 let col = Column::new_simple("email");
790 let expr = col.is_null();
791
792 match &expr.inner {
793 ExprInner::IsNull { column } => {
794 assert_eq!(column.name, "email");
795 }
796 _ => panic!("Expected IsNull"),
797 }
798 }
799
800 #[wasm_bindgen_test]
801 fn test_column_between() {
802 let col = Column::new_simple("age");
803 let expr = col.between(&JsValue::from_f64(18.0), &JsValue::from_f64(65.0));
804
805 match &expr.inner {
806 ExprInner::Between { column, .. } => {
807 assert_eq!(column.name, "age");
808 }
809 _ => panic!("Expected Between"),
810 }
811 }
812
813 #[wasm_bindgen_test]
814 fn test_column_like() {
815 let col = Column::new_simple("name");
816 let expr = col.like("Alice%");
817
818 match &expr.inner {
819 ExprInner::Like { column, pattern } => {
820 assert_eq!(column.name, "name");
821 assert_eq!(pattern, "Alice%");
822 }
823 _ => panic!("Expected Like"),
824 }
825 }
826}