Skip to main content

sz_orm_query/
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_query::json_query::JsonQuery;
18//! use sz_orm_model::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 sz_orm_model::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)]
565#[allow(deprecated)] // 测试 deprecated 的 where_cond 方法仍正常工作
566mod tests {
567    use super::*;
568
569    // ===== JsonQuery::build_extract 三方言测试 =====
570
571    #[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    // ===== 比较操作测试 =====
608
609    #[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    // ===== 转义测试 =====
672
673    #[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    // ===== contains 三方言测试 =====
682
683    #[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        // SQLite json_each().value 返回已解码的值(rust),不需要再包裹双引号
705        assert_eq!(
706            cond,
707            "EXISTS (SELECT 1 FROM json_each(json_extract(`tags`, '$.category')) WHERE value = 'rust')"
708        );
709    }
710
711    // ===== array_length 三方言测试 =====
712
713    #[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    // ===== JsonUpdate 测试 =====
741
742    #[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        // PG: 顶层包裹 "col" = jsonb_set(...)
769        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    // ===== 整合测试:JSON 查询 + 主查询 =====
787
788    #[test]
789    fn json_query_integrate_with_quick_query() {
790        use crate::quick_query::Db;
791        use sz_orm_model::get_dialect;
792
793        let dialect = get_dialect(DbType::MySQL).expect("MySQL");
794        let json_cond = JsonQuery::new(DbType::MySQL, "prefs")
795            .path("theme")
796            .eq_string("dark");
797        let sql = Db::new(dialect)
798            .name("users")
799            .where_cond(json_cond)
800            .build_select();
801        assert_eq!(
802            sql,
803            "SELECT * FROM `users` WHERE `prefs`->'$.theme' = 'dark'"
804        );
805    }
806
807    #[test]
808    fn json_update_integrate_with_quick_query() {
809        use sz_orm_model::get_dialect;
810
811        let _dialect = get_dialect(DbType::MySQL).expect("MySQL");
812        let set_clause = JsonUpdate::new(DbType::MySQL, "prefs")
813            .set_str("theme", "light")
814            .build_set();
815        // 验证 SET 子句正确性,可直接拼到 UPDATE ... SET <此处>
816        assert!(set_clause.contains("JSON_SET(`prefs`"));
817        assert!(set_clause.contains("'$.theme', 'light'"));
818
819        // 拼接 UPDATE SQL:UPDATE `users` SET <set_clause> WHERE id = 1
820        let sql = format!("UPDATE `users` SET {} WHERE id = 1", set_clause);
821        assert!(sql.starts_with("UPDATE `users` SET `prefs` = JSON_SET(`prefs`"));
822        assert!(sql.contains("WHERE id = 1"));
823    }
824
825    // ===== 边界/极端测试 =====
826
827    #[test]
828    fn empty_path_extracts_root() {
829        // 空路径应直接引用列自身,避免 MySQL 的 `'$.'` 非法路径表达式
830        let q = JsonQuery::new(DbType::MySQL, "data").build_extract();
831        assert_eq!(q, "`data`");
832    }
833
834    #[test]
835    fn empty_path_extracts_root_pg() {
836        // PostgreSQL 空路径也应直接引用列自身
837        let q = JsonQuery::new(DbType::PostgreSQL, "data").build_extract();
838        assert_eq!(q, "\"data\"");
839    }
840
841    #[test]
842    fn empty_path_extracts_root_sqlite() {
843        // SQLite 空路径也应直接引用列自身
844        let q = JsonQuery::new(DbType::Sqlite, "data").build_extract();
845        assert_eq!(q, "`data`");
846    }
847
848    #[test]
849    fn unsupported_db_falls_back_to_mysql() {
850        let q = JsonQuery::new(DbType::Redis, "data").path("x");
851        // Redis 不支持 JSON,应回退到 MySQL 语法
852        assert_eq!(q.build_extract(), "`data`->'$.x'");
853    }
854
855    #[test]
856    fn special_chars_in_value_escaped() {
857        // 反斜杠不转义(SQL 标准只要求转义单引号),但单引号必须转义
858        let cond = JsonQuery::new(DbType::MySQL, "d")
859            .path("k")
860            .eq_string("a'b'c");
861        assert_eq!(cond, "`d`->'$.k' = 'a''b''c'");
862    }
863
864    // ===== v0.2.0+ 增强:>= / <= / IN / BETWEEN / has_key / json_type_eq 测试 =====
865
866    #[test]
867    fn mysql_ge_le_string() {
868        let ge = JsonQuery::new(DbType::MySQL, "d").path("k").ge_string("m");
869        assert_eq!(ge, "`d`->'$.k' >= 'm'");
870        let le = JsonQuery::new(DbType::MySQL, "d").path("k").le_string("m");
871        assert_eq!(le, "`d`->'$.k' <= 'm'");
872    }
873
874    #[test]
875    fn mysql_ge_le_i64() {
876        let ge = JsonQuery::new(DbType::MySQL, "d").path("k").ge_i64(10);
877        assert_eq!(ge, "`d`->'$.k' >= 10");
878        let le = JsonQuery::new(DbType::MySQL, "d").path("k").le_i64(99);
879        assert_eq!(le, "`d`->'$.k' <= 99");
880        let gt = JsonQuery::new(DbType::MySQL, "d").path("k").gt_i64(5);
881        assert_eq!(gt, "`d`->'$.k' > 5");
882        let lt = JsonQuery::new(DbType::MySQL, "d").path("k").lt_i64(8);
883        assert_eq!(lt, "`d`->'$.k' < 8");
884    }
885
886    #[test]
887    fn mysql_between_i64() {
888        let cond = JsonQuery::new(DbType::MySQL, "stats")
889            .path("visits")
890            .between_i64(10, 100);
891        assert_eq!(cond, "`stats`->'$.visits' BETWEEN 10 AND 100");
892    }
893
894    #[test]
895    fn mysql_in_strs() {
896        let cond = JsonQuery::new(DbType::MySQL, "prefs")
897            .path("theme")
898            .in_strs(&["dark", "light"]);
899        assert_eq!(cond, "`prefs`->'$.theme' IN ('dark', 'light')");
900    }
901
902    #[test]
903    fn mysql_in_i64s() {
904        let cond = JsonQuery::new(DbType::MySQL, "stats")
905            .path("level")
906            .in_i64s(&[1, 2, 3]);
907        assert_eq!(cond, "`stats`->'$.level' IN (1, 2, 3)");
908    }
909
910    #[test]
911    fn mysql_in_strs_with_quote_escape() {
912        let cond = JsonQuery::new(DbType::MySQL, "d")
913            .path("k")
914            .in_strs(&["a'b", "c"]);
915        assert_eq!(cond, "`d`->'$.k' IN ('a''b', 'c')");
916    }
917
918    #[test]
919    fn mysql_in_empty_list() {
920        // 空列表生成 IN (),语义上等价于 false(标准 SQL 行为)
921        let cond = JsonQuery::new(DbType::MySQL, "d").path("k").in_strs(&[]);
922        assert_eq!(cond, "`d`->'$.k' IN ()");
923    }
924
925    #[test]
926    fn mysql_has_key() {
927        let cond = JsonQuery::new(DbType::MySQL, "prefs")
928            .path("theme")
929            .has_key();
930        assert_eq!(cond, "JSON_CONTAINS_PATH(`prefs`, 'one', '$.theme')");
931    }
932
933    #[test]
934    fn pg_has_key_single_level() {
935        let cond = JsonQuery::new(DbType::PostgreSQL, "prefs")
936            .path("theme")
937            .has_key();
938        assert_eq!(cond, "\"prefs\" ? 'theme'");
939    }
940
941    #[test]
942    fn pg_has_key_multi_level() {
943        let cond = JsonQuery::new(DbType::PostgreSQL, "prefs")
944            .path("a.b.c")
945            .has_key();
946        // 多层路径用 #> + IS NOT NULL
947        assert!(cond.contains("#>"));
948        assert!(cond.contains("IS NOT NULL"));
949    }
950
951    #[test]
952    fn sqlite_has_key() {
953        let cond = JsonQuery::new(DbType::Sqlite, "prefs")
954            .path("theme")
955            .has_key();
956        assert_eq!(cond, "json_type(`prefs`, '$.theme') IS NOT NULL");
957    }
958
959    #[test]
960    fn mysql_json_type_eq_integer() {
961        let cond = JsonQuery::new(DbType::MySQL, "stats")
962            .path("visits")
963            .json_type_eq("integer");
964        // MySQL JSON_TYPE 返回大写
965        assert_eq!(cond, "JSON_TYPE(`stats`->'$.visits') = 'INTEGER'");
966    }
967
968    #[test]
969    fn mysql_json_type_eq_array() {
970        let cond = JsonQuery::new(DbType::MySQL, "data")
971            .path("tags")
972            .json_type_eq("array");
973        assert_eq!(cond, "JSON_TYPE(`data`->'$.tags') = 'ARRAY'");
974    }
975
976    #[test]
977    fn pg_json_type_eq() {
978        let cond = JsonQuery::new(DbType::PostgreSQL, "stats")
979            .path("visits")
980            .json_type_eq("integer");
981        assert_eq!(cond, "json_typeof(\"stats\"#>>'{visits}') = 'integer'");
982    }
983
984    #[test]
985    fn sqlite_json_type_eq() {
986        let cond = JsonQuery::new(DbType::Sqlite, "data")
987            .path("tags")
988            .json_type_eq("array");
989        assert_eq!(cond, "json_type(`data`, '$.tags') = 'array'");
990    }
991
992    // ===== JsonUpdate 增强:array_append / remove_key 测试 =====
993
994    #[test]
995    fn mysql_array_append_str_single() {
996        let set = JsonUpdate::new(DbType::MySQL, "tags")
997            .array_append_str("list", "rust")
998            .build_set();
999        assert_eq!(set, "`tags` = JSON_ARRAY_APPEND(`tags`, '$.list', 'rust')");
1000    }
1001
1002    #[test]
1003    fn mysql_array_append_i64_single() {
1004        let set = JsonUpdate::new(DbType::MySQL, "nums")
1005            .array_append_i64("list", 42)
1006            .build_set();
1007        assert_eq!(set, "`nums` = JSON_ARRAY_APPEND(`nums`, '$.list', 42)");
1008    }
1009
1010    #[test]
1011    fn mysql_array_append_multiple() {
1012        let set = JsonUpdate::new(DbType::MySQL, "tags")
1013            .array_append_str("list", "rust")
1014            .array_append_str("list", "orm")
1015            .build_set();
1016        // 多次追加应嵌套
1017        assert!(set.contains("JSON_ARRAY_APPEND(JSON_ARRAY_APPEND"));
1018        assert!(set.contains("'rust'"));
1019        assert!(set.contains("'orm'"));
1020    }
1021
1022    #[test]
1023    fn mysql_remove_key_single() {
1024        let set = JsonUpdate::new(DbType::MySQL, "prefs")
1025            .remove_key("deprecated_field")
1026            .build_set();
1027        assert_eq!(set, "`prefs` = JSON_REMOVE(`prefs`, '$.deprecated_field')");
1028    }
1029
1030    #[test]
1031    fn mysql_remove_key_multiple() {
1032        let set = JsonUpdate::new(DbType::MySQL, "prefs")
1033            .remove_key("a")
1034            .remove_key("b")
1035            .build_set();
1036        assert_eq!(set, "`prefs` = JSON_REMOVE(`prefs`, '$.a', '$.b')");
1037    }
1038
1039    #[test]
1040    fn mysql_combined_set_append_remove() {
1041        let set = JsonUpdate::new(DbType::MySQL, "prefs")
1042            .set_str("theme", "dark")
1043            .array_append_str("tags", "new")
1044            .remove_key("old_field")
1045            .build_set();
1046        // SET 在最内层,REMOVE 在最外层(链式嵌套)
1047        assert!(set.starts_with("`prefs` = JSON_REMOVE(JSON_ARRAY_APPEND(JSON_SET("));
1048        assert!(set.contains("'$.theme', 'dark'"));
1049        assert!(set.contains("'$.tags', 'new'"));
1050        assert!(set.contains("'$.old_field'"));
1051    }
1052
1053    #[test]
1054    fn sqlite_remove_key() {
1055        let set = JsonUpdate::new(DbType::Sqlite, "prefs")
1056            .remove_key("old")
1057            .build_set();
1058        assert_eq!(set, "`prefs` = json_remove(`prefs`, '$.old')");
1059    }
1060
1061    #[test]
1062    fn sqlite_array_append() {
1063        let set = JsonUpdate::new(DbType::Sqlite, "tags")
1064            .array_append_str("list", "rust")
1065            .build_set();
1066        assert!(set.contains("json_set"));
1067        assert!(set.contains("json_insert"));
1068        assert!(set.contains("'rust'"));
1069    }
1070
1071    #[test]
1072    fn pg_remove_key() {
1073        let set = JsonUpdate::new(DbType::PostgreSQL, "prefs")
1074            .remove_key("old")
1075            .build_set();
1076        assert_eq!(set, "\"prefs\" = (\"prefs\" - 'old')");
1077    }
1078
1079    #[test]
1080    fn pg_combined_set_remove() {
1081        let set = JsonUpdate::new(DbType::PostgreSQL, "prefs")
1082            .set_str("theme", "dark")
1083            .remove_key("old")
1084            .build_set();
1085        assert!(set.contains("jsonb_set"));
1086        // PG 删除用 `- 'key'` 操作符
1087        assert!(set.contains("- 'old'"));
1088    }
1089}