Skip to main content

sz_orm_core/
json_query.rs

1//! JSON 字段查询增强
2//!
3//! 对应 think-orm 的 JSON 字段查询语法,支持 MySQL JSON 函数 + PostgreSQL jsonb 函数 + SQLite json_extract。
4//!
5//! # 三种方言差异
6//!
7//! | 操作 | MySQL | PostgreSQL | SQLite |
8//! |------|-------|-----------|--------|
9//! | 取字段 | `->'$.field'` | `->>'field'` | `json_extract(col, '$.field')` |
10//! | 取路径 | `->'$.a.b'` | `#>>'{a,b}'` | `json_extract(col, '$.a.b')` |
11//! | 包含键 | `JSON_CONTAINS(col, '"v"', '$.k')` | `col @> '{"k":"v"}'` | `json_extract(col,'$.k')='v'` |
12//! | 数组长度 | `JSON_LENGTH(col)` | `jsonb_array_length(col)` | `json_array_length(col)` |
13//!
14//! # 用法
15//!
16//! ```no_run
17//! use sz_orm_core::json_query::JsonQuery;
18//! use sz_orm_core::DbType;
19//!
20//! // MySQL: WHERE `prefs`->'$.theme' = 'dark'
21//! let cond = JsonQuery::new(DbType::MySQL, "prefs")
22//!     .path("theme")
23//!     .eq_string("dark");
24//! ```
25
26use crate::db_type::DbType;
27
28/// JSON 字段查询构造器
29///
30/// 提供 think-orm 风格的链式 JSON 字段查询 API,支持 MySQL/PostgreSQL/SQLite 三种方言。
31pub struct JsonQuery {
32    db_type: DbType,
33    column: String,
34    /// JSON 路径表达式(如 `theme` 或 `a.b.c`)
35    path: Option<String>,
36}
37
38impl JsonQuery {
39    /// 创建 JSON 查询构造器
40    ///
41    /// - `db_type`:目标数据库类型
42    /// - `column`:JSON 列名
43    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    /// 指定 JSON 路径(如 `theme` 或 `a.b.c`)
52    #[must_use]
53    pub fn path(mut self, path: impl Into<String>) -> Self {
54        self.path = Some(path.into());
55        self
56    }
57
58    /// 构建取字段表达式(左侧值,不含操作符与右侧值)
59    ///
60    /// - MySQL: `col->'$.field'`
61    /// - PostgreSQL: `col->>'field'`
62    /// - SQLite: `json_extract(col, '$.field')`
63    ///
64    /// 当路径为空时(未调用 `path()` 或传入空字符串),直接引用列自身,
65    /// 避免生成 `'$.'`(MySQL 非法路径)或 `''` 等非法语法。
66    pub fn build_extract(&self) -> String {
67        let path = self.path.as_deref().unwrap_or("");
68        if path.is_empty() {
69            // 空路径:直接引用列自身,等价于取整个 JSON 文档
70            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                // MySQL: `col`->'$.a.b.c'
78                format!("`{}`->'$.{}'", self.column, path)
79            }
80            DbType::PostgreSQL => {
81                // PG: "col"->>'a'->>'b'->>'c'  或  "col"->>'a'(单层)
82                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                // SQLite: json_extract(col, '$.a.b.c')
91                format!("json_extract(`{}`, '$.{}')", self.column, path)
92            }
93            _ => {
94                // 不支持的方言回退到 MySQL 语法
95                format!("`{}`->'$.{}'", self.column, path)
96            }
97        }
98    }
99
100    /// `=` 字符串值
101    pub fn eq_string(self, value: &str) -> String {
102        format!("{} = '{}'", self.build_extract(), escape_sql_str(value))
103    }
104
105    /// `=` 整数值
106    pub fn eq_i64(self, value: i64) -> String {
107        format!("{} = {}", self.build_extract(), value)
108    }
109
110    /// `=` 浮点值
111    pub fn eq_f64(self, value: f64) -> String {
112        format!("{} = {}", self.build_extract(), value)
113    }
114
115    /// `!=` 字符串值
116    pub fn ne_string(self, value: &str) -> String {
117        format!("{} != '{}'", self.build_extract(), escape_sql_str(value))
118    }
119
120    /// `>` 字符串值
121    pub fn gt_string(self, value: &str) -> String {
122        format!("{} > '{}'", self.build_extract(), escape_sql_str(value))
123    }
124
125    /// `<` 字符串值
126    pub fn lt_string(self, value: &str) -> String {
127        format!("{} < '{}'", self.build_extract(), escape_sql_str(value))
128    }
129
130    /// `>=` 字符串值
131    pub fn ge_string(self, value: &str) -> String {
132        format!("{} >= '{}'", self.build_extract(), escape_sql_str(value))
133    }
134
135    /// `<=` 字符串值
136    pub fn le_string(self, value: &str) -> String {
137        format!("{} <= '{}'", self.build_extract(), escape_sql_str(value))
138    }
139
140    /// `>=` 整数值
141    pub fn ge_i64(self, value: i64) -> String {
142        format!("{} >= {}", self.build_extract(), value)
143    }
144
145    /// `<=` 整数值
146    pub fn le_i64(self, value: i64) -> String {
147        format!("{} <= {}", self.build_extract(), value)
148    }
149
150    /// `>` 整数值
151    pub fn gt_i64(self, value: i64) -> String {
152        format!("{} > {}", self.build_extract(), value)
153    }
154
155    /// `<` 整数值
156    pub fn lt_i64(self, value: i64) -> String {
157        format!("{} < {}", self.build_extract(), value)
158    }
159
160    /// `BETWEEN` 整数范围(包含两端)
161    pub fn between_i64(self, low: i64, high: i64) -> String {
162        format!("{} BETWEEN {} AND {}", self.build_extract(), low, high)
163    }
164
165    /// `IN (字符串列表)`
166    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    /// `IN (整数列表)`
175    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    /// `LIKE` 字符串值
181    pub fn like(self, value: &str) -> String {
182        format!(
183            "{} LIKE '%{}%'",
184            self.build_extract(),
185            escape_sql_str(value)
186        )
187    }
188
189    /// `IS NULL`
190    pub fn is_null(self) -> String {
191        format!("{} IS NULL", self.build_extract())
192    }
193
194    /// `IS NOT NULL`
195    pub fn is_not_null(self) -> String {
196        format!("{} IS NOT NULL", self.build_extract())
197    }
198
199    /// 键存在性检查(路径下有键)
200    ///
201    /// - MySQL: `JSON_CONTAINS_PATH(col, 'one', '$.path')`
202    /// - PostgreSQL: `col ? 'path'`(顶层键)/ `col #? '{path}'`(路径)
203    /// - SQLite: `json_type(col, '$.path') IS NOT NULL`
204    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                // PG: 顶层用 ?,多层路径用 #>
210                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    /// JSON 类型检查(判断 JSON 值类型)
227    ///
228    /// - MySQL: `JSON_TYPE(col->'$.path') = 'INTEGER'`
229    /// - PostgreSQL: `json_typeof(col#>>'{path}') = 'integer'`
230    /// - SQLite: `json_type(col, '$.path') = 'integer'`
231    ///
232    /// `expected_type` 应为小写('integer'/'string'/'boolean'/'array'/'object'/'null')。
233    /// 在 MySQL 中会被自动转为大写。
234    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    /// 数组包含某元素(JSON_CONTAINS / @> / json_extract LIKE)
261    ///
262    /// - MySQL: `JSON_CONTAINS(col, '"v"', '$.path')`
263    /// - PostgreSQL: `col @> '{"path":"v"}'`(简化:用 path 拼接)
264    /// - SQLite: `EXISTS (SELECT 1 FROM json_each(json_extract(col, '$.path')) WHERE value = 'v')`
265    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                // PG: col @> '{"key":"value"}' 形式(path 为单层时直接用)
278                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                // SQLite: json_each().value 返回的是已解码的 JSON 值(如 `rust`),而非 JSON 编码的 `"rust"`。
288                // 因此 WHERE value = 'rust',不需要在两侧再包裹双引号。
289                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    /// 数组长度比较
310    ///
311    /// - MySQL: `JSON_LENGTH(col->'$.path') = N`
312    /// - PG: `jsonb_array_length(col#>>'{a,b}') = N`
313    /// - SQLite: `json_array_length(json_extract(col, '$.path')) = N`
314    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    /// 返回列名(不带方言处理)
341    pub fn column(&self) -> &str {
342        &self.column
343    }
344
345    /// 返回数据库类型
346    pub fn db_type(&self) -> DbType {
347        self.db_type
348    }
349}
350
351/// JSON 字段更新构造器
352///
353/// 提供 think-orm 风格的 JSON 字段 SET 子句构造。
354///
355/// - MySQL: `JSON_SET(col, '$.key', 'value')`
356/// - PG: `jsonb_set(col, '{key}', '"value"')`
357/// - SQLite: `json_set(col, '$.key', 'value')`
358pub struct JsonUpdate {
359    db_type: DbType,
360    column: String,
361    sets: Vec<(String, String)>,
362    /// 数组追加操作(key, value 的 SQL 字面量表达式)
363    array_appends: Vec<(String, String)>,
364    /// 需要删除的 JSON 路径
365    removes: Vec<String>,
366}
367
368impl JsonUpdate {
369    /// 创建 JSON 更新构造器
370    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    /// 添加一个 SET 项(key → value,value 为字符串)
381    #[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    /// 添加一个 SET 项(key → value,value 为 i64)
389    #[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    /// 添加一个 SET 项(key → value,value 为 bool)
396    #[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    /// 数组追加元素(字符串):将 value 追加到 col.path 指向的数组末尾
410    ///
411    /// - MySQL: `col = JSON_ARRAY_APPEND(col, '$.path', 'v')`
412    /// - PG: `col = jsonb_set(col, '{path}', (col#>'{path}') || to_jsonb('v'::text))`
413    /// - SQLite: `col = json_set(col, '$.path', json_insert(col->'$.path', '$[#]', 'v'))`
414    ///
415    /// 注:追加多个元素请多次调用。
416    #[must_use]
417    pub fn array_append_str(mut self, key: impl Into<String>, value: &str) -> Self {
418        // 复用 sets,但用特殊标记区分;这里直接构建为完整 SQL 表达式存入
419        // 改为:在 build_set 时用专门的处理逻辑
420        let k = key.into();
421        let v = format!("'{}'", escape_sql_str(value));
422        self.array_appends.push((k, v));
423        self
424    }
425
426    /// 数组追加元素(整数)
427    #[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    /// 删除指定 JSON 路径的字段
436    ///
437    /// - MySQL: `col = JSON_REMOVE(col, '$.key')`
438    /// - PG: `col = col - 'key'`
439    /// - SQLite: `col = json_remove(col, '$.key')`
440    #[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    /// 构建 SET 子句片段(不含 `SET` 关键字)
447    ///
448    /// 返回可直接拼到 `UPDATE ... SET <此处>` 的字符串。
449    /// 若同时存在 set / array_append / remove 操作,将按 SET → APPEND → REMOVE 顺序合并到同一列。
450    pub fn build_set(&self) -> String {
451        // 三类操作均为空:返回恒等赋值
452        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        // MySQL: 链式嵌套 col = JSON_REMOVE(JSON_ARRAY_APPEND(JSON_SET(col, ...), ...), ...)
471        let mut expr = format!("`{}`", self.column);
472
473        // 1. SET
474        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        // 2. ARRAY_APPEND
484        for (k, v) in &self.array_appends {
485            expr = format!("JSON_ARRAY_APPEND({}, '$.{}', {})", expr, k, v);
486        }
487
488        // 3. REMOVE
489        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        // PG: 链式嵌套 "col" = (col - 'rm' || jsonb_build_array(...)...) 等
499        // 简化:SET 用 jsonb_set 链式,APPEND 用 ||,REMOVE 用 -
500        let mut expr = format!("\"{}\"", self.column);
501
502        // 1. SET (链式 jsonb_set)
503        for (k, v) in &self.sets {
504            expr = format!("jsonb_set({}, '{{{}}}', {})", expr, k, v);
505        }
506
507        // 2. ARRAY_APPEND (用 || 拼接单元素数组)
508        for (k, v) in &self.array_appends {
509            // 把 v 包装为 to_jsonb 形式后追加到 #>'{k}' 数组
510            // v 可能是 'value' 或 100,统一用 to_jsonb 处理
511            // 由于 expr 是 String,需 clone 才能在 format! 中使用两次
512            let current = expr.clone();
513            expr = format!(
514                "jsonb_set({}, '{{{}}}', ({}#>'{{{}}}') || to_jsonb({}::text))",
515                current, k, current, k, v
516            );
517        }
518
519        // 3. REMOVE (用 -)
520        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        // 1. SET
531        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        // 2. ARRAY_APPEND(用 json_insert 在 '$[#]' 位置追加)
541        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        // 3. REMOVE
550        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
559/// 转义 SQL 字符串中的单引号
560fn escape_sql_str(s: &str) -> String {
561    s.replace('\'', "''")
562}
563
564#[cfg(test)]
565mod tests {
566    use super::*;
567
568    // ===== JsonQuery::build_extract 三方言测试 =====
569
570    #[test]
571    fn mysql_extract_single_field() {
572        let q = JsonQuery::new(DbType::MySQL, "prefs").path("theme");
573        assert_eq!(q.build_extract(), "`prefs`->'$.theme'");
574    }
575
576    #[test]
577    fn mysql_extract_nested_path() {
578        let q = JsonQuery::new(DbType::MySQL, "prefs").path("a.b.c");
579        assert_eq!(q.build_extract(), "`prefs`->'$.a.b.c'");
580    }
581
582    #[test]
583    fn pg_extract_single_field() {
584        let q = JsonQuery::new(DbType::PostgreSQL, "prefs").path("theme");
585        assert_eq!(q.build_extract(), "\"prefs\"->>'theme'");
586    }
587
588    #[test]
589    fn pg_extract_nested_path() {
590        let q = JsonQuery::new(DbType::PostgreSQL, "prefs").path("a.b.c");
591        assert_eq!(q.build_extract(), "\"prefs\"->>'a'->>'b'->>'c'");
592    }
593
594    #[test]
595    fn sqlite_extract_single_field() {
596        let q = JsonQuery::new(DbType::Sqlite, "prefs").path("theme");
597        assert_eq!(q.build_extract(), "json_extract(`prefs`, '$.theme')");
598    }
599
600    #[test]
601    fn sqlite_extract_nested_path() {
602        let q = JsonQuery::new(DbType::Sqlite, "prefs").path("a.b.c");
603        assert_eq!(q.build_extract(), "json_extract(`prefs`, '$.a.b.c')");
604    }
605
606    // ===== 比较操作测试 =====
607
608    #[test]
609    fn mysql_eq_string() {
610        let cond = JsonQuery::new(DbType::MySQL, "prefs")
611            .path("theme")
612            .eq_string("dark");
613        assert_eq!(cond, "`prefs`->'$.theme' = 'dark'");
614    }
615
616    #[test]
617    fn mysql_eq_i64() {
618        let cond = JsonQuery::new(DbType::MySQL, "stats")
619            .path("visits")
620            .eq_i64(100);
621        assert_eq!(cond, "`stats`->'$.visits' = 100");
622    }
623
624    #[test]
625    fn mysql_eq_f64() {
626        let cond = JsonQuery::new(DbType::MySQL, "stats")
627            .path("rate")
628            .eq_f64(0.95);
629        assert!(cond.starts_with("`stats`->'$.rate' = 0.95"));
630    }
631
632    #[test]
633    fn mysql_ne_string() {
634        let cond = JsonQuery::new(DbType::MySQL, "prefs")
635            .path("theme")
636            .ne_string("dark");
637        assert_eq!(cond, "`prefs`->'$.theme' != 'dark'");
638    }
639
640    #[test]
641    fn mysql_gt_lt_string() {
642        let gt = JsonQuery::new(DbType::MySQL, "prefs")
643            .path("name")
644            .gt_string("m");
645        assert_eq!(gt, "`prefs`->'$.name' > 'm'");
646        let lt = JsonQuery::new(DbType::MySQL, "prefs")
647            .path("name")
648            .lt_string("n");
649        assert_eq!(lt, "`prefs`->'$.name' < 'n'");
650    }
651
652    #[test]
653    fn mysql_like() {
654        let cond = JsonQuery::new(DbType::MySQL, "prefs")
655            .path("bio")
656            .like("engineer");
657        assert_eq!(cond, "`prefs`->'$.bio' LIKE '%engineer%'");
658    }
659
660    #[test]
661    fn mysql_is_null_and_not_null() {
662        let n = JsonQuery::new(DbType::MySQL, "prefs").path("opt").is_null();
663        assert_eq!(n, "`prefs`->'$.opt' IS NULL");
664        let nn = JsonQuery::new(DbType::MySQL, "prefs")
665            .path("opt")
666            .is_not_null();
667        assert_eq!(nn, "`prefs`->'$.opt' IS NOT NULL");
668    }
669
670    // ===== 转义测试 =====
671
672    #[test]
673    fn escape_single_quote_in_value() {
674        let cond = JsonQuery::new(DbType::MySQL, "prefs")
675            .path("name")
676            .eq_string("O'Brien");
677        assert_eq!(cond, "`prefs`->'$.name' = 'O''Brien'");
678    }
679
680    // ===== contains 三方言测试 =====
681
682    #[test]
683    fn mysql_contains() {
684        let cond = JsonQuery::new(DbType::MySQL, "tags")
685            .path("category")
686            .contains("rust");
687        assert_eq!(cond, "JSON_CONTAINS(`tags`, '\"rust\"', '$.category')");
688    }
689
690    #[test]
691    fn pg_contains() {
692        let cond = JsonQuery::new(DbType::PostgreSQL, "tags")
693            .path("category")
694            .contains("rust");
695        assert_eq!(cond, "\"tags\" @> '{\"category\":\"rust\"}'");
696    }
697
698    #[test]
699    fn sqlite_contains() {
700        let cond = JsonQuery::new(DbType::Sqlite, "tags")
701            .path("category")
702            .contains("rust");
703        // SQLite json_each().value 返回已解码的值(rust),不需要再包裹双引号
704        assert_eq!(
705            cond,
706            "EXISTS (SELECT 1 FROM json_each(json_extract(`tags`, '$.category')) WHERE value = 'rust')"
707        );
708    }
709
710    // ===== array_length 三方言测试 =====
711
712    #[test]
713    fn mysql_array_length() {
714        let cond = JsonQuery::new(DbType::MySQL, "items")
715            .path("list")
716            .array_length_eq(3);
717        assert_eq!(cond, "JSON_LENGTH(`items`->'$.list') = 3");
718    }
719
720    #[test]
721    fn pg_array_length() {
722        let cond = JsonQuery::new(DbType::PostgreSQL, "items")
723            .path("a.b")
724            .array_length_eq(3);
725        assert_eq!(cond, "jsonb_array_length(\"items\"#>>'{a,b}') = 3");
726    }
727
728    #[test]
729    fn sqlite_array_length() {
730        let cond = JsonQuery::new(DbType::Sqlite, "items")
731            .path("list")
732            .array_length_eq(3);
733        assert_eq!(
734            cond,
735            "json_array_length(json_extract(`items`, '$.list')) = 3"
736        );
737    }
738
739    // ===== JsonUpdate 测试 =====
740
741    #[test]
742    fn mysql_json_set_single() {
743        let set = JsonUpdate::new(DbType::MySQL, "prefs")
744            .set_str("theme", "dark")
745            .build_set();
746        assert_eq!(set, "`prefs` = JSON_SET(`prefs`, '$.theme', 'dark')");
747    }
748
749    #[test]
750    fn mysql_json_set_multi() {
751        let set = JsonUpdate::new(DbType::MySQL, "prefs")
752            .set_str("theme", "dark")
753            .set_i64("volume", 80)
754            .set_bool("autoplay", true)
755            .build_set();
756        assert!(set.contains("JSON_SET(`prefs`"));
757        assert!(set.contains("'$.theme', 'dark'"));
758        assert!(set.contains("'$.volume', 80"));
759        assert!(set.contains("'$.autoplay', true"));
760    }
761
762    #[test]
763    fn pg_json_set_single() {
764        let set = JsonUpdate::new(DbType::PostgreSQL, "prefs")
765            .set_str("theme", "dark")
766            .build_set();
767        // PG: 顶层包裹 "col" = jsonb_set(...)
768        assert_eq!(set, "\"prefs\" = jsonb_set(\"prefs\", '{theme}', 'dark')");
769    }
770
771    #[test]
772    fn sqlite_json_set_single() {
773        let set = JsonUpdate::new(DbType::Sqlite, "prefs")
774            .set_str("theme", "dark")
775            .build_set();
776        assert_eq!(set, "`prefs` = json_set(`prefs`, '$.theme', 'dark')");
777    }
778
779    #[test]
780    fn json_update_empty_set() {
781        let set = JsonUpdate::new(DbType::MySQL, "prefs").build_set();
782        assert_eq!(set, "`prefs` = `prefs`");
783    }
784
785    // ===== 整合测试:JSON 查询 + 主查询 =====
786
787    #[test]
788    fn json_query_integrate_with_quick_query() {
789        use crate::dialect::get_dialect;
790        use crate::quick_query::Db;
791
792        let dialect = get_dialect(DbType::MySQL).expect("MySQL");
793        let json_cond = JsonQuery::new(DbType::MySQL, "prefs")
794            .path("theme")
795            .eq_string("dark");
796        let sql = Db::new(dialect)
797            .name("users")
798            .where_cond(json_cond)
799            .build_select();
800        assert_eq!(
801            sql,
802            "SELECT * FROM `users` WHERE `prefs`->'$.theme' = 'dark'"
803        );
804    }
805
806    #[test]
807    fn json_update_integrate_with_quick_query() {
808        use crate::dialect::get_dialect;
809
810        let _dialect = get_dialect(DbType::MySQL).expect("MySQL");
811        let set_clause = JsonUpdate::new(DbType::MySQL, "prefs")
812            .set_str("theme", "light")
813            .build_set();
814        // 验证 SET 子句正确性,可直接拼到 UPDATE ... SET <此处>
815        assert!(set_clause.contains("JSON_SET(`prefs`"));
816        assert!(set_clause.contains("'$.theme', 'light'"));
817
818        // 拼接 UPDATE SQL:UPDATE `users` SET <set_clause> WHERE id = 1
819        let sql = format!("UPDATE `users` SET {} WHERE id = 1", set_clause);
820        assert!(sql.starts_with("UPDATE `users` SET `prefs` = JSON_SET(`prefs`"));
821        assert!(sql.contains("WHERE id = 1"));
822    }
823
824    // ===== 边界/极端测试 =====
825
826    #[test]
827    fn empty_path_extracts_root() {
828        // 空路径应直接引用列自身,避免 MySQL 的 `'$.'` 非法路径表达式
829        let q = JsonQuery::new(DbType::MySQL, "data").build_extract();
830        assert_eq!(q, "`data`");
831    }
832
833    #[test]
834    fn empty_path_extracts_root_pg() {
835        // PostgreSQL 空路径也应直接引用列自身
836        let q = JsonQuery::new(DbType::PostgreSQL, "data").build_extract();
837        assert_eq!(q, "\"data\"");
838    }
839
840    #[test]
841    fn empty_path_extracts_root_sqlite() {
842        // SQLite 空路径也应直接引用列自身
843        let q = JsonQuery::new(DbType::Sqlite, "data").build_extract();
844        assert_eq!(q, "`data`");
845    }
846
847    #[test]
848    fn unsupported_db_falls_back_to_mysql() {
849        let q = JsonQuery::new(DbType::Redis, "data").path("x");
850        // Redis 不支持 JSON,应回退到 MySQL 语法
851        assert_eq!(q.build_extract(), "`data`->'$.x'");
852    }
853
854    #[test]
855    fn special_chars_in_value_escaped() {
856        // 反斜杠不转义(SQL 标准只要求转义单引号),但单引号必须转义
857        let cond = JsonQuery::new(DbType::MySQL, "d")
858            .path("k")
859            .eq_string("a'b'c");
860        assert_eq!(cond, "`d`->'$.k' = 'a''b''c'");
861    }
862
863    // ===== v0.2.0+ 增强:>= / <= / IN / BETWEEN / has_key / json_type_eq 测试 =====
864
865    #[test]
866    fn mysql_ge_le_string() {
867        let ge = JsonQuery::new(DbType::MySQL, "d").path("k").ge_string("m");
868        assert_eq!(ge, "`d`->'$.k' >= 'm'");
869        let le = JsonQuery::new(DbType::MySQL, "d").path("k").le_string("m");
870        assert_eq!(le, "`d`->'$.k' <= 'm'");
871    }
872
873    #[test]
874    fn mysql_ge_le_i64() {
875        let ge = JsonQuery::new(DbType::MySQL, "d").path("k").ge_i64(10);
876        assert_eq!(ge, "`d`->'$.k' >= 10");
877        let le = JsonQuery::new(DbType::MySQL, "d").path("k").le_i64(99);
878        assert_eq!(le, "`d`->'$.k' <= 99");
879        let gt = JsonQuery::new(DbType::MySQL, "d").path("k").gt_i64(5);
880        assert_eq!(gt, "`d`->'$.k' > 5");
881        let lt = JsonQuery::new(DbType::MySQL, "d").path("k").lt_i64(8);
882        assert_eq!(lt, "`d`->'$.k' < 8");
883    }
884
885    #[test]
886    fn mysql_between_i64() {
887        let cond = JsonQuery::new(DbType::MySQL, "stats")
888            .path("visits")
889            .between_i64(10, 100);
890        assert_eq!(cond, "`stats`->'$.visits' BETWEEN 10 AND 100");
891    }
892
893    #[test]
894    fn mysql_in_strs() {
895        let cond = JsonQuery::new(DbType::MySQL, "prefs")
896            .path("theme")
897            .in_strs(&["dark", "light"]);
898        assert_eq!(cond, "`prefs`->'$.theme' IN ('dark', 'light')");
899    }
900
901    #[test]
902    fn mysql_in_i64s() {
903        let cond = JsonQuery::new(DbType::MySQL, "stats")
904            .path("level")
905            .in_i64s(&[1, 2, 3]);
906        assert_eq!(cond, "`stats`->'$.level' IN (1, 2, 3)");
907    }
908
909    #[test]
910    fn mysql_in_strs_with_quote_escape() {
911        let cond = JsonQuery::new(DbType::MySQL, "d")
912            .path("k")
913            .in_strs(&["a'b", "c"]);
914        assert_eq!(cond, "`d`->'$.k' IN ('a''b', 'c')");
915    }
916
917    #[test]
918    fn mysql_in_empty_list() {
919        // 空列表生成 IN (),语义上等价于 false(标准 SQL 行为)
920        let cond = JsonQuery::new(DbType::MySQL, "d").path("k").in_strs(&[]);
921        assert_eq!(cond, "`d`->'$.k' IN ()");
922    }
923
924    #[test]
925    fn mysql_has_key() {
926        let cond = JsonQuery::new(DbType::MySQL, "prefs")
927            .path("theme")
928            .has_key();
929        assert_eq!(cond, "JSON_CONTAINS_PATH(`prefs`, 'one', '$.theme')");
930    }
931
932    #[test]
933    fn pg_has_key_single_level() {
934        let cond = JsonQuery::new(DbType::PostgreSQL, "prefs")
935            .path("theme")
936            .has_key();
937        assert_eq!(cond, "\"prefs\" ? 'theme'");
938    }
939
940    #[test]
941    fn pg_has_key_multi_level() {
942        let cond = JsonQuery::new(DbType::PostgreSQL, "prefs")
943            .path("a.b.c")
944            .has_key();
945        // 多层路径用 #> + IS NOT NULL
946        assert!(cond.contains("#>"));
947        assert!(cond.contains("IS NOT NULL"));
948    }
949
950    #[test]
951    fn sqlite_has_key() {
952        let cond = JsonQuery::new(DbType::Sqlite, "prefs")
953            .path("theme")
954            .has_key();
955        assert_eq!(cond, "json_type(`prefs`, '$.theme') IS NOT NULL");
956    }
957
958    #[test]
959    fn mysql_json_type_eq_integer() {
960        let cond = JsonQuery::new(DbType::MySQL, "stats")
961            .path("visits")
962            .json_type_eq("integer");
963        // MySQL JSON_TYPE 返回大写
964        assert_eq!(cond, "JSON_TYPE(`stats`->'$.visits') = 'INTEGER'");
965    }
966
967    #[test]
968    fn mysql_json_type_eq_array() {
969        let cond = JsonQuery::new(DbType::MySQL, "data")
970            .path("tags")
971            .json_type_eq("array");
972        assert_eq!(cond, "JSON_TYPE(`data`->'$.tags') = 'ARRAY'");
973    }
974
975    #[test]
976    fn pg_json_type_eq() {
977        let cond = JsonQuery::new(DbType::PostgreSQL, "stats")
978            .path("visits")
979            .json_type_eq("integer");
980        assert_eq!(cond, "json_typeof(\"stats\"#>>'{visits}') = 'integer'");
981    }
982
983    #[test]
984    fn sqlite_json_type_eq() {
985        let cond = JsonQuery::new(DbType::Sqlite, "data")
986            .path("tags")
987            .json_type_eq("array");
988        assert_eq!(cond, "json_type(`data`, '$.tags') = 'array'");
989    }
990
991    // ===== JsonUpdate 增强:array_append / remove_key 测试 =====
992
993    #[test]
994    fn mysql_array_append_str_single() {
995        let set = JsonUpdate::new(DbType::MySQL, "tags")
996            .array_append_str("list", "rust")
997            .build_set();
998        assert_eq!(set, "`tags` = JSON_ARRAY_APPEND(`tags`, '$.list', 'rust')");
999    }
1000
1001    #[test]
1002    fn mysql_array_append_i64_single() {
1003        let set = JsonUpdate::new(DbType::MySQL, "nums")
1004            .array_append_i64("list", 42)
1005            .build_set();
1006        assert_eq!(set, "`nums` = JSON_ARRAY_APPEND(`nums`, '$.list', 42)");
1007    }
1008
1009    #[test]
1010    fn mysql_array_append_multiple() {
1011        let set = JsonUpdate::new(DbType::MySQL, "tags")
1012            .array_append_str("list", "rust")
1013            .array_append_str("list", "orm")
1014            .build_set();
1015        // 多次追加应嵌套
1016        assert!(set.contains("JSON_ARRAY_APPEND(JSON_ARRAY_APPEND"));
1017        assert!(set.contains("'rust'"));
1018        assert!(set.contains("'orm'"));
1019    }
1020
1021    #[test]
1022    fn mysql_remove_key_single() {
1023        let set = JsonUpdate::new(DbType::MySQL, "prefs")
1024            .remove_key("deprecated_field")
1025            .build_set();
1026        assert_eq!(set, "`prefs` = JSON_REMOVE(`prefs`, '$.deprecated_field')");
1027    }
1028
1029    #[test]
1030    fn mysql_remove_key_multiple() {
1031        let set = JsonUpdate::new(DbType::MySQL, "prefs")
1032            .remove_key("a")
1033            .remove_key("b")
1034            .build_set();
1035        assert_eq!(set, "`prefs` = JSON_REMOVE(`prefs`, '$.a', '$.b')");
1036    }
1037
1038    #[test]
1039    fn mysql_combined_set_append_remove() {
1040        let set = JsonUpdate::new(DbType::MySQL, "prefs")
1041            .set_str("theme", "dark")
1042            .array_append_str("tags", "new")
1043            .remove_key("old_field")
1044            .build_set();
1045        // SET 在最内层,REMOVE 在最外层(链式嵌套)
1046        assert!(set.starts_with("`prefs` = JSON_REMOVE(JSON_ARRAY_APPEND(JSON_SET("));
1047        assert!(set.contains("'$.theme', 'dark'"));
1048        assert!(set.contains("'$.tags', 'new'"));
1049        assert!(set.contains("'$.old_field'"));
1050    }
1051
1052    #[test]
1053    fn sqlite_remove_key() {
1054        let set = JsonUpdate::new(DbType::Sqlite, "prefs")
1055            .remove_key("old")
1056            .build_set();
1057        assert_eq!(set, "`prefs` = json_remove(`prefs`, '$.old')");
1058    }
1059
1060    #[test]
1061    fn sqlite_array_append() {
1062        let set = JsonUpdate::new(DbType::Sqlite, "tags")
1063            .array_append_str("list", "rust")
1064            .build_set();
1065        assert!(set.contains("json_set"));
1066        assert!(set.contains("json_insert"));
1067        assert!(set.contains("'rust'"));
1068    }
1069
1070    #[test]
1071    fn pg_remove_key() {
1072        let set = JsonUpdate::new(DbType::PostgreSQL, "prefs")
1073            .remove_key("old")
1074            .build_set();
1075        assert_eq!(set, "\"prefs\" = (\"prefs\" - 'old')");
1076    }
1077
1078    #[test]
1079    fn pg_combined_set_remove() {
1080        let set = JsonUpdate::new(DbType::PostgreSQL, "prefs")
1081            .set_str("theme", "dark")
1082            .remove_key("old")
1083            .build_set();
1084        assert!(set.contains("jsonb_set"));
1085        // PG 删除用 `- 'key'` 操作符
1086        assert!(set.contains("- 'old'"));
1087    }
1088}