1use crate::db_type::DbType;
27
28pub struct JsonQuery {
32 db_type: DbType,
33 column: String,
34 path: Option<String>,
36}
37
38impl JsonQuery {
39 pub fn new(db_type: DbType, column: impl Into<String>) -> Self {
44 Self {
45 db_type,
46 column: column.into(),
47 path: None,
48 }
49 }
50
51 #[must_use]
53 pub fn path(mut self, path: impl Into<String>) -> Self {
54 self.path = Some(path.into());
55 self
56 }
57
58 pub fn build_extract(&self) -> String {
67 let path = self.path.as_deref().unwrap_or("");
68 if path.is_empty() {
69 return match self.db_type {
71 DbType::PostgreSQL => format!("\"{}\"", self.column),
72 _ => format!("`{}`", self.column),
73 };
74 }
75 match self.db_type {
76 DbType::MySQL => {
77 format!("`{}`->'$.{}'", self.column, path)
79 }
80 DbType::PostgreSQL => {
81 let parts: Vec<&str> = path.split('.').collect();
83 let mut expr = format!("\"{}\"", self.column);
84 for p in parts {
85 expr.push_str(&format!("->>'{}'", p));
86 }
87 expr
88 }
89 DbType::Sqlite => {
90 format!("json_extract(`{}`, '$.{}')", self.column, path)
92 }
93 _ => {
94 format!("`{}`->'$.{}'", self.column, path)
96 }
97 }
98 }
99
100 pub fn eq_string(self, value: &str) -> String {
102 format!("{} = '{}'", self.build_extract(), escape_sql_str(value))
103 }
104
105 pub fn eq_i64(self, value: i64) -> String {
107 format!("{} = {}", self.build_extract(), value)
108 }
109
110 pub fn eq_f64(self, value: f64) -> String {
112 format!("{} = {}", self.build_extract(), value)
113 }
114
115 pub fn ne_string(self, value: &str) -> String {
117 format!("{} != '{}'", self.build_extract(), escape_sql_str(value))
118 }
119
120 pub fn gt_string(self, value: &str) -> String {
122 format!("{} > '{}'", self.build_extract(), escape_sql_str(value))
123 }
124
125 pub fn lt_string(self, value: &str) -> String {
127 format!("{} < '{}'", self.build_extract(), escape_sql_str(value))
128 }
129
130 pub fn ge_string(self, value: &str) -> String {
132 format!("{} >= '{}'", self.build_extract(), escape_sql_str(value))
133 }
134
135 pub fn le_string(self, value: &str) -> String {
137 format!("{} <= '{}'", self.build_extract(), escape_sql_str(value))
138 }
139
140 pub fn ge_i64(self, value: i64) -> String {
142 format!("{} >= {}", self.build_extract(), value)
143 }
144
145 pub fn le_i64(self, value: i64) -> String {
147 format!("{} <= {}", self.build_extract(), value)
148 }
149
150 pub fn gt_i64(self, value: i64) -> String {
152 format!("{} > {}", self.build_extract(), value)
153 }
154
155 pub fn lt_i64(self, value: i64) -> String {
157 format!("{} < {}", self.build_extract(), value)
158 }
159
160 pub fn between_i64(self, low: i64, high: i64) -> String {
162 format!("{} BETWEEN {} AND {}", self.build_extract(), low, high)
163 }
164
165 pub fn in_strs(self, values: &[&str]) -> String {
167 let list: Vec<String> = values
168 .iter()
169 .map(|v| format!("'{}'", escape_sql_str(v)))
170 .collect();
171 format!("{} IN ({})", self.build_extract(), list.join(", "))
172 }
173
174 pub fn in_i64s(self, values: &[i64]) -> String {
176 let list: Vec<String> = values.iter().map(|v| v.to_string()).collect();
177 format!("{} IN ({})", self.build_extract(), list.join(", "))
178 }
179
180 pub fn like(self, value: &str) -> String {
182 format!(
183 "{} LIKE '%{}%'",
184 self.build_extract(),
185 escape_sql_str(value)
186 )
187 }
188
189 pub fn is_null(self) -> String {
191 format!("{} IS NULL", self.build_extract())
192 }
193
194 pub fn is_not_null(self) -> String {
196 format!("{} IS NOT NULL", self.build_extract())
197 }
198
199 pub fn has_key(self) -> String {
205 let path = self.path.as_deref().unwrap_or("");
206 match self.db_type {
207 DbType::MySQL => format!("JSON_CONTAINS_PATH(`{}`, 'one', '$.{}')", self.column, path),
208 DbType::PostgreSQL => {
209 let parts: Vec<&str> = path.split('.').collect();
211 if parts.len() <= 1 {
212 format!("\"{}\" ? '{}'", self.column, path)
213 } else {
214 let path_braced = parts.join(",");
215 format!(
216 "\"{}\" #> '{{{{{}}}}}' IS NOT NULL",
217 self.column, path_braced
218 )
219 }
220 }
221 DbType::Sqlite => format!("json_type(`{}`, '$.{}') IS NOT NULL", self.column, path),
222 _ => format!("JSON_CONTAINS_PATH(`{}`, 'one', '$.{}')", self.column, path),
223 }
224 }
225
226 pub fn json_type_eq(self, expected_type: &str) -> String {
235 let path = self.path.as_deref().unwrap_or("");
236 match self.db_type {
237 DbType::MySQL => {
238 let upper = expected_type.to_uppercase();
239 format!("JSON_TYPE(`{}`->'$.{}') = '{}'", self.column, path, upper)
240 }
241 DbType::PostgreSQL => {
242 let parts: Vec<&str> = path.split('.').collect();
243 let path_braced = parts.join(",");
244 format!(
245 "json_typeof(\"{}\"#>>'{{{}}}') = '{}'",
246 self.column, path_braced, expected_type
247 )
248 }
249 DbType::Sqlite => format!(
250 "json_type(`{}`, '$.{}') = '{}'",
251 self.column, path, expected_type
252 ),
253 _ => {
254 let upper = expected_type.to_uppercase();
255 format!("JSON_TYPE(`{}`->'$.{}') = '{}'", self.column, path, upper)
256 }
257 }
258 }
259
260 pub fn contains(self, value: &str) -> String {
266 match self.db_type {
267 DbType::MySQL => {
268 let path = self.path.as_deref().unwrap_or("");
269 format!(
270 "JSON_CONTAINS(`{}`, '\"{}\"', '$.{}')",
271 self.column,
272 escape_sql_str(value),
273 path
274 )
275 }
276 DbType::PostgreSQL => {
277 let path = self.path.as_deref().unwrap_or("");
279 format!(
280 "\"{}\" @> '{{\"{}\":\"{}\"}}'",
281 self.column,
282 path,
283 escape_sql_str(value)
284 )
285 }
286 DbType::Sqlite => {
287 let path = self.path.as_deref().unwrap_or("");
290 format!(
291 "EXISTS (SELECT 1 FROM json_each(json_extract(`{}`, '$.{}')) WHERE value = '{}')",
292 self.column,
293 path,
294 escape_sql_str(value)
295 )
296 }
297 _ => {
298 let path = self.path.as_deref().unwrap_or("");
299 format!(
300 "JSON_CONTAINS(`{}`, '\"{}\"', '$.{}')",
301 self.column,
302 escape_sql_str(value),
303 path
304 )
305 }
306 }
307 }
308
309 pub fn array_length_eq(self, length: i64) -> String {
315 let path = self.path.as_deref().unwrap_or("");
316 match self.db_type {
317 DbType::MySQL => {
318 format!("JSON_LENGTH(`{}`->'$.{}') = {}", self.column, path, length)
319 }
320 DbType::PostgreSQL => {
321 let parts: Vec<&str> = path.split('.').collect();
322 let path_str = parts.join(",");
323 format!(
324 "jsonb_array_length(\"{}\"#>>'{{{}}}') = {}",
325 self.column, path_str, length
326 )
327 }
328 DbType::Sqlite => {
329 format!(
330 "json_array_length(json_extract(`{}`, '$.{}')) = {}",
331 self.column, path, length
332 )
333 }
334 _ => {
335 format!("JSON_LENGTH(`{}`->'$.{}') = {}", self.column, path, length)
336 }
337 }
338 }
339
340 pub fn column(&self) -> &str {
342 &self.column
343 }
344
345 pub fn db_type(&self) -> DbType {
347 self.db_type
348 }
349}
350
351pub struct JsonUpdate {
359 db_type: DbType,
360 column: String,
361 sets: Vec<(String, String)>,
362 array_appends: Vec<(String, String)>,
364 removes: Vec<String>,
366}
367
368impl JsonUpdate {
369 pub fn new(db_type: DbType, column: impl Into<String>) -> Self {
371 Self {
372 db_type,
373 column: column.into(),
374 sets: Vec::new(),
375 array_appends: Vec::new(),
376 removes: Vec::new(),
377 }
378 }
379
380 #[must_use]
382 pub fn set_str(mut self, key: impl Into<String>, value: &str) -> Self {
383 self.sets
384 .push((key.into(), format!("'{}'", escape_sql_str(value))));
385 self
386 }
387
388 #[must_use]
390 pub fn set_i64(mut self, key: impl Into<String>, value: i64) -> Self {
391 self.sets.push((key.into(), value.to_string()));
392 self
393 }
394
395 #[must_use]
397 pub fn set_bool(mut self, key: impl Into<String>, value: bool) -> Self {
398 self.sets.push((
399 key.into(),
400 if value {
401 "true".to_string()
402 } else {
403 "false".to_string()
404 },
405 ));
406 self
407 }
408
409 #[must_use]
417 pub fn array_append_str(mut self, key: impl Into<String>, value: &str) -> Self {
418 let k = key.into();
421 let v = format!("'{}'", escape_sql_str(value));
422 self.array_appends.push((k, v));
423 self
424 }
425
426 #[must_use]
428 pub fn array_append_i64(mut self, key: impl Into<String>, value: i64) -> Self {
429 let k = key.into();
430 let v = value.to_string();
431 self.array_appends.push((k, v));
432 self
433 }
434
435 #[must_use]
441 pub fn remove_key(mut self, key: impl Into<String>) -> Self {
442 self.removes.push(key.into());
443 self
444 }
445
446 pub fn build_set(&self) -> String {
451 let empty =
453 self.sets.is_empty() && self.array_appends.is_empty() && self.removes.is_empty();
454 if empty {
455 return match self.db_type {
456 DbType::PostgreSQL => format!("\"{}\" = \"{}\"", self.column, self.column),
457 _ => format!("`{}` = `{}`", self.column, self.column),
458 };
459 }
460
461 match self.db_type {
462 DbType::MySQL => self.build_set_mysql(),
463 DbType::PostgreSQL => self.build_set_pg(),
464 DbType::Sqlite => self.build_set_sqlite(),
465 _ => self.build_set_mysql(),
466 }
467 }
468
469 fn build_set_mysql(&self) -> String {
470 let mut expr = format!("`{}`", self.column);
472
473 if !self.sets.is_empty() {
475 let args: Vec<String> = self
476 .sets
477 .iter()
478 .map(|(k, v)| format!("'$.{}', {}", k, v))
479 .collect();
480 expr = format!("JSON_SET({}, {})", expr, args.join(", "));
481 }
482
483 for (k, v) in &self.array_appends {
485 expr = format!("JSON_ARRAY_APPEND({}, '$.{}', {})", expr, k, v);
486 }
487
488 if !self.removes.is_empty() {
490 let args: Vec<String> = self.removes.iter().map(|k| format!("'$.{}'", k)).collect();
491 expr = format!("JSON_REMOVE({}, {})", expr, args.join(", "));
492 }
493
494 format!("`{}` = {}", self.column, expr)
495 }
496
497 fn build_set_pg(&self) -> String {
498 let mut expr = format!("\"{}\"", self.column);
501
502 for (k, v) in &self.sets {
504 expr = format!("jsonb_set({}, '{{{}}}', {})", expr, k, v);
505 }
506
507 for (k, v) in &self.array_appends {
509 let current = expr.clone();
513 expr = format!(
514 "jsonb_set({}, '{{{}}}', ({}#>'{{{}}}') || to_jsonb({}::text))",
515 current, k, current, k, v
516 );
517 }
518
519 for k in &self.removes {
521 expr = format!("({} - '{}')", expr, k);
522 }
523
524 format!("\"{}\" = {}", self.column, expr)
525 }
526
527 fn build_set_sqlite(&self) -> String {
528 let mut expr = format!("`{}`", self.column);
529
530 if !self.sets.is_empty() {
532 let args: Vec<String> = self
533 .sets
534 .iter()
535 .map(|(k, v)| format!("'$.{}', {}", k, v))
536 .collect();
537 expr = format!("json_set({}, {})", expr, args.join(", "));
538 }
539
540 for (k, v) in &self.array_appends {
542 let current = expr.clone();
543 expr = format!(
544 "json_set({}, '$.{}', json_insert({}->'$.{}', '$[#]', {}))",
545 current, k, current, k, v
546 );
547 }
548
549 if !self.removes.is_empty() {
551 let args: Vec<String> = self.removes.iter().map(|k| format!("'$.{}'", k)).collect();
552 expr = format!("json_remove({}, {})", expr, args.join(", "));
553 }
554
555 format!("`{}` = {}", self.column, expr)
556 }
557}
558
559fn escape_sql_str(s: &str) -> String {
561 s.replace('\'', "''")
562}
563
564#[cfg(test)]
565#[allow(deprecated)]
566mod tests {
567 use super::*;
568
569 #[test]
572 fn mysql_extract_single_field() {
573 let q = JsonQuery::new(DbType::MySQL, "prefs").path("theme");
574 assert_eq!(q.build_extract(), "`prefs`->'$.theme'");
575 }
576
577 #[test]
578 fn mysql_extract_nested_path() {
579 let q = JsonQuery::new(DbType::MySQL, "prefs").path("a.b.c");
580 assert_eq!(q.build_extract(), "`prefs`->'$.a.b.c'");
581 }
582
583 #[test]
584 fn pg_extract_single_field() {
585 let q = JsonQuery::new(DbType::PostgreSQL, "prefs").path("theme");
586 assert_eq!(q.build_extract(), "\"prefs\"->>'theme'");
587 }
588
589 #[test]
590 fn pg_extract_nested_path() {
591 let q = JsonQuery::new(DbType::PostgreSQL, "prefs").path("a.b.c");
592 assert_eq!(q.build_extract(), "\"prefs\"->>'a'->>'b'->>'c'");
593 }
594
595 #[test]
596 fn sqlite_extract_single_field() {
597 let q = JsonQuery::new(DbType::Sqlite, "prefs").path("theme");
598 assert_eq!(q.build_extract(), "json_extract(`prefs`, '$.theme')");
599 }
600
601 #[test]
602 fn sqlite_extract_nested_path() {
603 let q = JsonQuery::new(DbType::Sqlite, "prefs").path("a.b.c");
604 assert_eq!(q.build_extract(), "json_extract(`prefs`, '$.a.b.c')");
605 }
606
607 #[test]
610 fn mysql_eq_string() {
611 let cond = JsonQuery::new(DbType::MySQL, "prefs")
612 .path("theme")
613 .eq_string("dark");
614 assert_eq!(cond, "`prefs`->'$.theme' = 'dark'");
615 }
616
617 #[test]
618 fn mysql_eq_i64() {
619 let cond = JsonQuery::new(DbType::MySQL, "stats")
620 .path("visits")
621 .eq_i64(100);
622 assert_eq!(cond, "`stats`->'$.visits' = 100");
623 }
624
625 #[test]
626 fn mysql_eq_f64() {
627 let cond = JsonQuery::new(DbType::MySQL, "stats")
628 .path("rate")
629 .eq_f64(0.95);
630 assert!(cond.starts_with("`stats`->'$.rate' = 0.95"));
631 }
632
633 #[test]
634 fn mysql_ne_string() {
635 let cond = JsonQuery::new(DbType::MySQL, "prefs")
636 .path("theme")
637 .ne_string("dark");
638 assert_eq!(cond, "`prefs`->'$.theme' != 'dark'");
639 }
640
641 #[test]
642 fn mysql_gt_lt_string() {
643 let gt = JsonQuery::new(DbType::MySQL, "prefs")
644 .path("name")
645 .gt_string("m");
646 assert_eq!(gt, "`prefs`->'$.name' > 'm'");
647 let lt = JsonQuery::new(DbType::MySQL, "prefs")
648 .path("name")
649 .lt_string("n");
650 assert_eq!(lt, "`prefs`->'$.name' < 'n'");
651 }
652
653 #[test]
654 fn mysql_like() {
655 let cond = JsonQuery::new(DbType::MySQL, "prefs")
656 .path("bio")
657 .like("engineer");
658 assert_eq!(cond, "`prefs`->'$.bio' LIKE '%engineer%'");
659 }
660
661 #[test]
662 fn mysql_is_null_and_not_null() {
663 let n = JsonQuery::new(DbType::MySQL, "prefs").path("opt").is_null();
664 assert_eq!(n, "`prefs`->'$.opt' IS NULL");
665 let nn = JsonQuery::new(DbType::MySQL, "prefs")
666 .path("opt")
667 .is_not_null();
668 assert_eq!(nn, "`prefs`->'$.opt' IS NOT NULL");
669 }
670
671 #[test]
674 fn escape_single_quote_in_value() {
675 let cond = JsonQuery::new(DbType::MySQL, "prefs")
676 .path("name")
677 .eq_string("O'Brien");
678 assert_eq!(cond, "`prefs`->'$.name' = 'O''Brien'");
679 }
680
681 #[test]
684 fn mysql_contains() {
685 let cond = JsonQuery::new(DbType::MySQL, "tags")
686 .path("category")
687 .contains("rust");
688 assert_eq!(cond, "JSON_CONTAINS(`tags`, '\"rust\"', '$.category')");
689 }
690
691 #[test]
692 fn pg_contains() {
693 let cond = JsonQuery::new(DbType::PostgreSQL, "tags")
694 .path("category")
695 .contains("rust");
696 assert_eq!(cond, "\"tags\" @> '{\"category\":\"rust\"}'");
697 }
698
699 #[test]
700 fn sqlite_contains() {
701 let cond = JsonQuery::new(DbType::Sqlite, "tags")
702 .path("category")
703 .contains("rust");
704 assert_eq!(
706 cond,
707 "EXISTS (SELECT 1 FROM json_each(json_extract(`tags`, '$.category')) WHERE value = 'rust')"
708 );
709 }
710
711 #[test]
714 fn mysql_array_length() {
715 let cond = JsonQuery::new(DbType::MySQL, "items")
716 .path("list")
717 .array_length_eq(3);
718 assert_eq!(cond, "JSON_LENGTH(`items`->'$.list') = 3");
719 }
720
721 #[test]
722 fn pg_array_length() {
723 let cond = JsonQuery::new(DbType::PostgreSQL, "items")
724 .path("a.b")
725 .array_length_eq(3);
726 assert_eq!(cond, "jsonb_array_length(\"items\"#>>'{a,b}') = 3");
727 }
728
729 #[test]
730 fn sqlite_array_length() {
731 let cond = JsonQuery::new(DbType::Sqlite, "items")
732 .path("list")
733 .array_length_eq(3);
734 assert_eq!(
735 cond,
736 "json_array_length(json_extract(`items`, '$.list')) = 3"
737 );
738 }
739
740 #[test]
743 fn mysql_json_set_single() {
744 let set = JsonUpdate::new(DbType::MySQL, "prefs")
745 .set_str("theme", "dark")
746 .build_set();
747 assert_eq!(set, "`prefs` = JSON_SET(`prefs`, '$.theme', 'dark')");
748 }
749
750 #[test]
751 fn mysql_json_set_multi() {
752 let set = JsonUpdate::new(DbType::MySQL, "prefs")
753 .set_str("theme", "dark")
754 .set_i64("volume", 80)
755 .set_bool("autoplay", true)
756 .build_set();
757 assert!(set.contains("JSON_SET(`prefs`"));
758 assert!(set.contains("'$.theme', 'dark'"));
759 assert!(set.contains("'$.volume', 80"));
760 assert!(set.contains("'$.autoplay', true"));
761 }
762
763 #[test]
764 fn pg_json_set_single() {
765 let set = JsonUpdate::new(DbType::PostgreSQL, "prefs")
766 .set_str("theme", "dark")
767 .build_set();
768 assert_eq!(set, "\"prefs\" = jsonb_set(\"prefs\", '{theme}', 'dark')");
770 }
771
772 #[test]
773 fn sqlite_json_set_single() {
774 let set = JsonUpdate::new(DbType::Sqlite, "prefs")
775 .set_str("theme", "dark")
776 .build_set();
777 assert_eq!(set, "`prefs` = json_set(`prefs`, '$.theme', 'dark')");
778 }
779
780 #[test]
781 fn json_update_empty_set() {
782 let set = JsonUpdate::new(DbType::MySQL, "prefs").build_set();
783 assert_eq!(set, "`prefs` = `prefs`");
784 }
785
786 #[test]
789 fn json_query_build_extract() {
790 let json_cond = JsonQuery::new(DbType::MySQL, "prefs")
791 .path("theme")
792 .build_extract();
793 assert_eq!(json_cond, "`prefs`->'$.theme'");
794 }
795
796 #[test]
797 fn json_update_integrate_with_quick_query() {
798 use crate::dialect::get_dialect;
799
800 let _dialect = get_dialect(DbType::MySQL).expect("MySQL");
801 let set_clause = JsonUpdate::new(DbType::MySQL, "prefs")
802 .set_str("theme", "light")
803 .build_set();
804 assert!(set_clause.contains("JSON_SET(`prefs`"));
806 assert!(set_clause.contains("'$.theme', 'light'"));
807
808 let sql = format!("UPDATE `users` SET {} WHERE id = 1", set_clause);
810 assert!(sql.starts_with("UPDATE `users` SET `prefs` = JSON_SET(`prefs`"));
811 assert!(sql.contains("WHERE id = 1"));
812 }
813
814 #[test]
817 fn empty_path_extracts_root() {
818 let q = JsonQuery::new(DbType::MySQL, "data").build_extract();
820 assert_eq!(q, "`data`");
821 }
822
823 #[test]
824 fn empty_path_extracts_root_pg() {
825 let q = JsonQuery::new(DbType::PostgreSQL, "data").build_extract();
827 assert_eq!(q, "\"data\"");
828 }
829
830 #[test]
831 fn empty_path_extracts_root_sqlite() {
832 let q = JsonQuery::new(DbType::Sqlite, "data").build_extract();
834 assert_eq!(q, "`data`");
835 }
836
837 #[test]
838 fn unsupported_db_falls_back_to_mysql() {
839 let q = JsonQuery::new(DbType::Redis, "data").path("x");
840 assert_eq!(q.build_extract(), "`data`->'$.x'");
842 }
843
844 #[test]
845 fn special_chars_in_value_escaped() {
846 let cond = JsonQuery::new(DbType::MySQL, "d")
848 .path("k")
849 .eq_string("a'b'c");
850 assert_eq!(cond, "`d`->'$.k' = 'a''b''c'");
851 }
852
853 #[test]
856 fn mysql_ge_le_string() {
857 let ge = JsonQuery::new(DbType::MySQL, "d").path("k").ge_string("m");
858 assert_eq!(ge, "`d`->'$.k' >= 'm'");
859 let le = JsonQuery::new(DbType::MySQL, "d").path("k").le_string("m");
860 assert_eq!(le, "`d`->'$.k' <= 'm'");
861 }
862
863 #[test]
864 fn mysql_ge_le_i64() {
865 let ge = JsonQuery::new(DbType::MySQL, "d").path("k").ge_i64(10);
866 assert_eq!(ge, "`d`->'$.k' >= 10");
867 let le = JsonQuery::new(DbType::MySQL, "d").path("k").le_i64(99);
868 assert_eq!(le, "`d`->'$.k' <= 99");
869 let gt = JsonQuery::new(DbType::MySQL, "d").path("k").gt_i64(5);
870 assert_eq!(gt, "`d`->'$.k' > 5");
871 let lt = JsonQuery::new(DbType::MySQL, "d").path("k").lt_i64(8);
872 assert_eq!(lt, "`d`->'$.k' < 8");
873 }
874
875 #[test]
876 fn mysql_between_i64() {
877 let cond = JsonQuery::new(DbType::MySQL, "stats")
878 .path("visits")
879 .between_i64(10, 100);
880 assert_eq!(cond, "`stats`->'$.visits' BETWEEN 10 AND 100");
881 }
882
883 #[test]
884 fn mysql_in_strs() {
885 let cond = JsonQuery::new(DbType::MySQL, "prefs")
886 .path("theme")
887 .in_strs(&["dark", "light"]);
888 assert_eq!(cond, "`prefs`->'$.theme' IN ('dark', 'light')");
889 }
890
891 #[test]
892 fn mysql_in_i64s() {
893 let cond = JsonQuery::new(DbType::MySQL, "stats")
894 .path("level")
895 .in_i64s(&[1, 2, 3]);
896 assert_eq!(cond, "`stats`->'$.level' IN (1, 2, 3)");
897 }
898
899 #[test]
900 fn mysql_in_strs_with_quote_escape() {
901 let cond = JsonQuery::new(DbType::MySQL, "d")
902 .path("k")
903 .in_strs(&["a'b", "c"]);
904 assert_eq!(cond, "`d`->'$.k' IN ('a''b', 'c')");
905 }
906
907 #[test]
908 fn mysql_in_empty_list() {
909 let cond = JsonQuery::new(DbType::MySQL, "d").path("k").in_strs(&[]);
911 assert_eq!(cond, "`d`->'$.k' IN ()");
912 }
913
914 #[test]
915 fn mysql_has_key() {
916 let cond = JsonQuery::new(DbType::MySQL, "prefs")
917 .path("theme")
918 .has_key();
919 assert_eq!(cond, "JSON_CONTAINS_PATH(`prefs`, 'one', '$.theme')");
920 }
921
922 #[test]
923 fn pg_has_key_single_level() {
924 let cond = JsonQuery::new(DbType::PostgreSQL, "prefs")
925 .path("theme")
926 .has_key();
927 assert_eq!(cond, "\"prefs\" ? 'theme'");
928 }
929
930 #[test]
931 fn pg_has_key_multi_level() {
932 let cond = JsonQuery::new(DbType::PostgreSQL, "prefs")
933 .path("a.b.c")
934 .has_key();
935 assert!(cond.contains("#>"));
937 assert!(cond.contains("IS NOT NULL"));
938 }
939
940 #[test]
941 fn sqlite_has_key() {
942 let cond = JsonQuery::new(DbType::Sqlite, "prefs")
943 .path("theme")
944 .has_key();
945 assert_eq!(cond, "json_type(`prefs`, '$.theme') IS NOT NULL");
946 }
947
948 #[test]
949 fn mysql_json_type_eq_integer() {
950 let cond = JsonQuery::new(DbType::MySQL, "stats")
951 .path("visits")
952 .json_type_eq("integer");
953 assert_eq!(cond, "JSON_TYPE(`stats`->'$.visits') = 'INTEGER'");
955 }
956
957 #[test]
958 fn mysql_json_type_eq_array() {
959 let cond = JsonQuery::new(DbType::MySQL, "data")
960 .path("tags")
961 .json_type_eq("array");
962 assert_eq!(cond, "JSON_TYPE(`data`->'$.tags') = 'ARRAY'");
963 }
964
965 #[test]
966 fn pg_json_type_eq() {
967 let cond = JsonQuery::new(DbType::PostgreSQL, "stats")
968 .path("visits")
969 .json_type_eq("integer");
970 assert_eq!(cond, "json_typeof(\"stats\"#>>'{visits}') = 'integer'");
971 }
972
973 #[test]
974 fn sqlite_json_type_eq() {
975 let cond = JsonQuery::new(DbType::Sqlite, "data")
976 .path("tags")
977 .json_type_eq("array");
978 assert_eq!(cond, "json_type(`data`, '$.tags') = 'array'");
979 }
980
981 #[test]
984 fn mysql_array_append_str_single() {
985 let set = JsonUpdate::new(DbType::MySQL, "tags")
986 .array_append_str("list", "rust")
987 .build_set();
988 assert_eq!(set, "`tags` = JSON_ARRAY_APPEND(`tags`, '$.list', 'rust')");
989 }
990
991 #[test]
992 fn mysql_array_append_i64_single() {
993 let set = JsonUpdate::new(DbType::MySQL, "nums")
994 .array_append_i64("list", 42)
995 .build_set();
996 assert_eq!(set, "`nums` = JSON_ARRAY_APPEND(`nums`, '$.list', 42)");
997 }
998
999 #[test]
1000 fn mysql_array_append_multiple() {
1001 let set = JsonUpdate::new(DbType::MySQL, "tags")
1002 .array_append_str("list", "rust")
1003 .array_append_str("list", "orm")
1004 .build_set();
1005 assert!(set.contains("JSON_ARRAY_APPEND(JSON_ARRAY_APPEND"));
1007 assert!(set.contains("'rust'"));
1008 assert!(set.contains("'orm'"));
1009 }
1010
1011 #[test]
1012 fn mysql_remove_key_single() {
1013 let set = JsonUpdate::new(DbType::MySQL, "prefs")
1014 .remove_key("deprecated_field")
1015 .build_set();
1016 assert_eq!(set, "`prefs` = JSON_REMOVE(`prefs`, '$.deprecated_field')");
1017 }
1018
1019 #[test]
1020 fn mysql_remove_key_multiple() {
1021 let set = JsonUpdate::new(DbType::MySQL, "prefs")
1022 .remove_key("a")
1023 .remove_key("b")
1024 .build_set();
1025 assert_eq!(set, "`prefs` = JSON_REMOVE(`prefs`, '$.a', '$.b')");
1026 }
1027
1028 #[test]
1029 fn mysql_combined_set_append_remove() {
1030 let set = JsonUpdate::new(DbType::MySQL, "prefs")
1031 .set_str("theme", "dark")
1032 .array_append_str("tags", "new")
1033 .remove_key("old_field")
1034 .build_set();
1035 assert!(set.starts_with("`prefs` = JSON_REMOVE(JSON_ARRAY_APPEND(JSON_SET("));
1037 assert!(set.contains("'$.theme', 'dark'"));
1038 assert!(set.contains("'$.tags', 'new'"));
1039 assert!(set.contains("'$.old_field'"));
1040 }
1041
1042 #[test]
1043 fn sqlite_remove_key() {
1044 let set = JsonUpdate::new(DbType::Sqlite, "prefs")
1045 .remove_key("old")
1046 .build_set();
1047 assert_eq!(set, "`prefs` = json_remove(`prefs`, '$.old')");
1048 }
1049
1050 #[test]
1051 fn sqlite_array_append() {
1052 let set = JsonUpdate::new(DbType::Sqlite, "tags")
1053 .array_append_str("list", "rust")
1054 .build_set();
1055 assert!(set.contains("json_set"));
1056 assert!(set.contains("json_insert"));
1057 assert!(set.contains("'rust'"));
1058 }
1059
1060 #[test]
1061 fn pg_remove_key() {
1062 let set = JsonUpdate::new(DbType::PostgreSQL, "prefs")
1063 .remove_key("old")
1064 .build_set();
1065 assert_eq!(set, "\"prefs\" = (\"prefs\" - 'old')");
1066 }
1067
1068 #[test]
1069 fn pg_combined_set_remove() {
1070 let set = JsonUpdate::new(DbType::PostgreSQL, "prefs")
1071 .set_str("theme", "dark")
1072 .remove_key("old")
1073 .build_set();
1074 assert!(set.contains("jsonb_set"));
1075 assert!(set.contains("- 'old'"));
1077 }
1078}