Skip to main content

sz_orm_core/
dynamic_sql.rs

1//! XML/py_sql 动态 SQL 构造器(rbatis 风格)
2//!
3//! rbatis 的核心特性是 XML 风格的动态 SQL 模板:
4//! - `<select>` / `<insert>` / `<update>` / `<delete>` 顶层语句
5//! - `<if test="...">` 条件块
6//! - `<where>` 自动处理首个 AND/OR
7//! - `<set>` UPDATE 语句的 SET 子句
8//! - `<foreach>` 循环展开(用于 IN 子句)
9//! - `<choose><when><otherwise>` 多分支选择
10//! - `<trim>` 通用前后缀修剪
11//! - `#{name}` 命名参数绑定
12//!
13//! # 用法
14//!
15//! ```ignore
16//! use sz_orm_core::dynamic_sql::{DynamicSqlParser, SqlParams};
17//! use std::collections::HashMap;
18//!
19//! let xml = r#"
20//! <select id="find_users">
21//!     SELECT * FROM users
22//!     <where>
23//!         <if test="name != null">AND name = #{name}</if>
24//!         <if test="age != null">AND age &gt; #{age}</if>
25//!     </where>
26//! </select>
27//! "#;
28//!
29//! let parser = DynamicSqlParser::from_xml(xml);
30//! let mut params = SqlParams::new();
31//! params.set("name", "Alice");
32//! // params.set("age", 18);  // 不设置则 if 不生效
33//!
34//! let sql = parser.build("find_users", &params).unwrap();
35//! // SELECT * FROM users WHERE name = ?
36//! ```
37//!
38//! # 支持的标签
39//!
40//! | 标签 | 作用 |
41//! |------|------|
42//! | `<select id>` | SELECT 语句容器 |
43//! | `<insert id>` | INSERT 语句容器 |
44//! | `<update id>` | UPDATE 语句容器 |
45//! | `<delete id>` | DELETE 语句容器 |
46//! | `<if test="expr">` | 条件包含 |
47//! | `<where>` | WHERE 子句(自动处理首个 AND/OR) |
48//! | `<set>` | SET 子句(自动处理末尾逗号) |
49//! | `<foreach collection="x" item="i" separator=",">` | 循环展开 |
50//! | `<choose>` / `<when>` / `<otherwise>` | 多分支选择 |
51//! | `<trim prefix="..." suffix="..." prefixOverrides="AND">` | 通用修剪 |
52//! | `#{name}` | 命名参数绑定 |
53//! | `${name}` | 字符串插值(注意 SQL 注入风险) |
54//!
55//! # 安全警告
56//!
57//! `${name}` 字符串插值存在 SQL 注入风险,即使已添加基础转义。
58//! **强烈推荐使用 `#{name}` 命名参数绑定**,仅在 SQL 结构动态(如表名/列名)时
59//! 才使用 `${name}`,且必须确保输入来源可信。
60
61use std::collections::HashMap;
62
63/// 动态 SQL 错误
64#[derive(Debug, Clone, PartialEq)]
65pub enum DynamicSqlError {
66    /// XML 解析错误
67    ParseError(String),
68    /// 找不到指定的语句 ID
69    StatementNotFound(String),
70    /// 表达式求值错误
71    EvalError(String),
72    /// 参数缺失
73    MissingParam(String),
74}
75
76impl std::fmt::Display for DynamicSqlError {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        match self {
79            DynamicSqlError::ParseError(msg) => write!(f, "XML 解析错误: {}", msg),
80            DynamicSqlError::StatementNotFound(id) => {
81                write!(f, "找不到语句 ID: {}", id)
82            }
83            DynamicSqlError::EvalError(msg) => write!(f, "表达式求值错误: {}", msg),
84            DynamicSqlError::MissingParam(name) => write!(f, "缺少参数: {}", name),
85        }
86    }
87}
88
89impl std::error::Error for DynamicSqlError {}
90
91/// SQL 参数容器
92///
93/// 支持命名参数,按 `#{name}` 引用。
94#[derive(Debug, Clone, Default)]
95pub struct SqlParams {
96    params: HashMap<String, ParamValue>,
97}
98
99/// 参数值
100#[derive(Debug, Clone)]
101pub enum ParamValue {
102    /// Null
103    Null,
104    /// 字符串
105    String(String),
106    /// 整数
107    Int(i64),
108    /// 浮点
109    Float(f64),
110    /// 布尔
111    Bool(bool),
112    /// 数组(用于 foreach)
113    Array(Vec<ParamValue>),
114}
115
116impl SqlParams {
117    /// 创建空参数集
118    pub fn new() -> Self {
119        Self::default()
120    }
121
122    /// 设置字符串参数
123    pub fn set(&mut self, name: &str, value: &str) {
124        self.params
125            .insert(name.to_string(), ParamValue::String(value.to_string()));
126    }
127
128    /// 设置整数参数
129    pub fn set_int(&mut self, name: &str, value: i64) {
130        self.params.insert(name.to_string(), ParamValue::Int(value));
131    }
132
133    /// 设置浮点参数
134    pub fn set_float(&mut self, name: &str, value: f64) {
135        self.params
136            .insert(name.to_string(), ParamValue::Float(value));
137    }
138
139    /// 设置布尔参数
140    pub fn set_bool(&mut self, name: &str, value: bool) {
141        self.params
142            .insert(name.to_string(), ParamValue::Bool(value));
143    }
144
145    /// 设置 null
146    pub fn set_null(&mut self, name: &str) {
147        self.params.insert(name.to_string(), ParamValue::Null);
148    }
149
150    /// 设置数组参数
151    pub fn set_array(&mut self, name: &str, values: Vec<ParamValue>) {
152        self.params
153            .insert(name.to_string(), ParamValue::Array(values));
154    }
155
156    /// 获取参数值
157    pub fn get(&self, name: &str) -> Option<&ParamValue> {
158        self.params.get(name)
159    }
160
161    /// 是否存在参数
162    pub fn contains(&self, name: &str) -> bool {
163        self.params.contains_key(name)
164    }
165
166    /// 判断参数是否为 null(或不存在)
167    pub fn is_null(&self, name: &str) -> bool {
168        matches!(self.params.get(name), None | Some(ParamValue::Null))
169    }
170
171    /// 判断参数是否不为 null
172    pub fn is_not_null(&self, name: &str) -> bool {
173        !self.is_null(name)
174    }
175
176    /// 获取所有参数名
177    pub fn names(&self) -> Vec<String> {
178        self.params.keys().cloned().collect()
179    }
180}
181
182/// 动态 SQL 解析器
183#[derive(Debug, Clone)]
184pub struct DynamicSqlParser {
185    /// 已解析的语句:id → 语句节点
186    statements: HashMap<String, XmlNode>,
187}
188
189impl DynamicSqlParser {
190    /// 创建空解析器
191    pub fn new() -> Self {
192        Self {
193            statements: HashMap::new(),
194        }
195    }
196
197    /// 从 XML 字符串解析
198    pub fn from_xml(xml: &str) -> Result<Self, DynamicSqlError> {
199        let mut parser = Self::new();
200        parser.parse_xml(xml)?;
201        Ok(parser)
202    }
203
204    /// 解析 XML 字符串
205    fn parse_xml(&mut self, xml: &str) -> Result<(), DynamicSqlError> {
206        let root = XmlParser::parse(xml)?;
207        for child in &root.children {
208            if let XmlNodeType::Element { name, attrs } = &child.node_type {
209                let id = attrs
210                    .get("id")
211                    .ok_or_else(|| DynamicSqlError::ParseError(format!("<{}> 缺少 id 属性", name)))?
212                    .clone();
213                self.statements.insert(id, child.clone());
214            }
215        }
216        Ok(())
217    }
218
219    /// 构建指定 ID 的 SQL 语句
220    pub fn build(&self, id: &str, params: &SqlParams) -> Result<String, DynamicSqlError> {
221        let node = self
222            .statements
223            .get(id)
224            .ok_or_else(|| DynamicSqlError::StatementNotFound(id.to_string()))?;
225        let mut ctx = BuildContext::new(params);
226        self.build_node(node, &mut ctx)?;
227        Ok(self.cleanup_sql(&ctx.buffer))
228    }
229
230    /// 构建并返回 SQL + 绑定参数(按出现顺序)
231    pub fn build_with_binds(
232        &self,
233        id: &str,
234        params: &SqlParams,
235    ) -> Result<(String, Vec<ParamValue>), DynamicSqlError> {
236        let node = self
237            .statements
238            .get(id)
239            .ok_or_else(|| DynamicSqlError::StatementNotFound(id.to_string()))?;
240        let mut ctx = BuildContext::new(params);
241        self.build_node(node, &mut ctx)?;
242        Ok((self.cleanup_sql(&ctx.buffer), ctx.binds))
243    }
244
245    /// 列出所有已注册的语句 ID
246    pub fn statement_ids(&self) -> Vec<String> {
247        let mut ids: Vec<String> = self.statements.keys().cloned().collect();
248        ids.sort();
249        ids
250    }
251
252    // ---- 内部:节点构建 ----
253
254    fn build_node(&self, node: &XmlNode, ctx: &mut BuildContext) -> Result<(), DynamicSqlError> {
255        match &node.node_type {
256            XmlNodeType::Text(text) => {
257                self.append_text(text, ctx)?;
258            }
259            XmlNodeType::Element { name, attrs } => {
260                match name.as_str() {
261                    "select" | "insert" | "update" | "delete" => {
262                        for child in &node.children {
263                            self.build_node(child, ctx)?;
264                        }
265                    }
266                    "if" => {
267                        let test = attrs.get("test").ok_or_else(|| {
268                            DynamicSqlError::ParseError("<if> 缺少 test 属性".into())
269                        })?;
270                        if eval_test(test, ctx.params)? {
271                            for child in &node.children {
272                                self.build_node(child, ctx)?;
273                            }
274                        }
275                    }
276                    "where" => {
277                        let mut sub_ctx = BuildContext::new(ctx.params);
278                        for child in &node.children {
279                            self.build_node(child, &mut sub_ctx)?;
280                        }
281                        let content = sub_ctx.buffer.trim();
282                        if !content.is_empty() {
283                            // 去除开头的 AND/OR
284                            let cleaned = strip_leading_and_or(content);
285                            ctx.buffer.push_str(" WHERE ");
286                            ctx.buffer.push_str(cleaned.trim());
287                            // 把子上下文收集的绑定参数传回父上下文
288                            ctx.binds.extend(sub_ctx.binds);
289                        }
290                    }
291                    "set" => {
292                        let mut sub_ctx = BuildContext::new(ctx.params);
293                        for child in &node.children {
294                            self.build_node(child, &mut sub_ctx)?;
295                        }
296                        let content = sub_ctx.buffer.trim();
297                        if !content.is_empty() {
298                            // 去除末尾的逗号
299                            let cleaned = content.trim_end_matches(',').trim();
300                            // 规范化逗号:每个逗号后保留一个空格
301                            let normalized = normalize_set_commas(cleaned);
302                            ctx.buffer.push_str(" SET ");
303                            ctx.buffer.push_str(&normalized);
304                            ctx.binds.extend(sub_ctx.binds);
305                        }
306                    }
307                    "foreach" => {
308                        self.build_foreach(node, attrs, ctx)?;
309                    }
310                    "choose" => {
311                        self.build_choose(node, ctx)?;
312                    }
313                    "trim" => {
314                        self.build_trim(node, attrs, ctx)?;
315                    }
316                    _ => {
317                        // 未知标签,递归处理子节点
318                        for child in &node.children {
319                            self.build_node(child, ctx)?;
320                        }
321                    }
322                }
323            }
324        }
325        Ok(())
326    }
327
328    fn build_foreach(
329        &self,
330        node: &XmlNode,
331        attrs: &HashMap<String, String>,
332        ctx: &mut BuildContext,
333    ) -> Result<(), DynamicSqlError> {
334        let collection = attrs
335            .get("collection")
336            .ok_or_else(|| DynamicSqlError::ParseError("<foreach> 缺少 collection 属性".into()))?;
337        let item = attrs.get("item").map(|s| s.as_str()).unwrap_or("item");
338        let separator = attrs.get("separator").map(|s| s.as_str()).unwrap_or(",");
339        let open = attrs.get("open").cloned().unwrap_or_default();
340        let close = attrs.get("close").cloned().unwrap_or_default();
341
342        let arr = match ctx.params.get(collection) {
343            Some(ParamValue::Array(arr)) => arr.clone(),
344            _ => return Ok(()),
345        };
346
347        // 仅克隆一次参数集,循环内复用(避免每次迭代克隆整个 SqlParams)
348        let mut sub_params = ctx.params.clone();
349        let mut parts: Vec<String> = Vec::new();
350        for v in &arr {
351            // 临时设置 item 变量(覆盖上一次迭代的值,无需还原,因为 sub_params 不会被外部观察)
352            match v {
353                ParamValue::String(s) => sub_params.set(item, s),
354                ParamValue::Int(i) => sub_params.set_int(item, *i),
355                ParamValue::Float(f) => sub_params.set_float(item, *f),
356                ParamValue::Bool(b) => sub_params.set_bool(item, *b),
357                ParamValue::Null => sub_params.set_null(item),
358                ParamValue::Array(_) => {} // 嵌套数组不支持
359            }
360            let mut sub_ctx = BuildContext::new(&sub_params);
361            for child in &node.children {
362                self.build_node(child, &mut sub_ctx)?;
363            }
364            parts.push(sub_ctx.buffer.trim().to_string());
365            // 把子上下文收集的绑定参数传回父上下文(按出现顺序)
366            ctx.binds.extend(sub_ctx.binds);
367        }
368        if !parts.is_empty() {
369            let joined = parts.join(separator);
370            ctx.buffer.push(' ');
371            if !open.is_empty() {
372                ctx.buffer.push_str(&open);
373            }
374            ctx.buffer.push_str(&joined);
375            if !close.is_empty() {
376                ctx.buffer.push_str(&close);
377            }
378        }
379        Ok(())
380    }
381
382    fn build_choose(&self, node: &XmlNode, ctx: &mut BuildContext) -> Result<(), DynamicSqlError> {
383        for child in &node.children {
384            if let XmlNodeType::Element { name, attrs } = &child.node_type {
385                match name.as_str() {
386                    "when" => {
387                        let test = attrs.get("test").ok_or_else(|| {
388                            DynamicSqlError::ParseError("<when> 缺少 test 属性".into())
389                        })?;
390                        if eval_test(test, ctx.params)? {
391                            for c in &child.children {
392                                self.build_node(c, ctx)?;
393                            }
394                            return Ok(());
395                        }
396                    }
397                    "otherwise" => {
398                        for c in &child.children {
399                            self.build_node(c, ctx)?;
400                        }
401                        return Ok(());
402                    }
403                    _ => {}
404                }
405            }
406        }
407        Ok(())
408    }
409
410    fn build_trim(
411        &self,
412        node: &XmlNode,
413        attrs: &HashMap<String, String>,
414        ctx: &mut BuildContext,
415    ) -> Result<(), DynamicSqlError> {
416        let prefix = attrs.get("prefix").cloned().unwrap_or_default();
417        let suffix = attrs.get("suffix").cloned().unwrap_or_default();
418        let prefix_overrides = attrs.get("prefixOverrides").cloned().unwrap_or_default();
419        let suffix_overrides = attrs.get("suffixOverrides").cloned().unwrap_or_default();
420
421        let mut sub_ctx = BuildContext::new(ctx.params);
422        for child in &node.children {
423            self.build_node(child, &mut sub_ctx)?;
424        }
425        let mut content = sub_ctx.buffer.trim().to_string();
426
427        // 处理 prefixOverrides
428        if !prefix_overrides.is_empty() {
429            for ov in prefix_overrides.split('|') {
430                if content.starts_with(ov) {
431                    content = content[ov.len()..].trim_start().to_string();
432                    break;
433                }
434            }
435        }
436        // 处理 suffixOverrides
437        if !suffix_overrides.is_empty() {
438            for ov in suffix_overrides.split('|') {
439                if content.ends_with(ov) {
440                    content = content[..content.len() - ov.len()].trim_end().to_string();
441                    break;
442                }
443            }
444        }
445
446        if !content.is_empty() {
447            ctx.buffer.push(' ');
448            if !prefix.is_empty() {
449                ctx.buffer.push_str(&prefix);
450                ctx.buffer.push(' ');
451            }
452            ctx.buffer.push_str(&content);
453            if !suffix.is_empty() {
454                ctx.buffer.push(' ');
455                ctx.buffer.push_str(&suffix);
456            }
457            // 把子上下文收集的绑定参数传回父上下文
458            ctx.binds.extend(sub_ctx.binds);
459        }
460        Ok(())
461    }
462
463    fn append_text(&self, text: &str, ctx: &mut BuildContext) -> Result<(), DynamicSqlError> {
464        let mut i = 0;
465        let bytes = text.as_bytes();
466        while i < bytes.len() {
467            if i + 1 < bytes.len() && bytes[i] == b'#' && bytes[i + 1] == b'{' {
468                // #{name} 参数绑定
469                let end = text[i + 2..].find('}').ok_or_else(|| {
470                    DynamicSqlError::ParseError(format!("未闭合的 #{{}}: {}", &text[i..]))
471                })?;
472                let name = &text[i + 2..i + 2 + end];
473                let value = ctx
474                    .params
475                    .get(name)
476                    .ok_or_else(|| DynamicSqlError::MissingParam(name.to_string()))?
477                    .clone();
478                ctx.buffer.push('?');
479                ctx.binds.push(value);
480                i += 2 + end + 1; // 跳过 #{name}
481            } else if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'{' {
482                // ${name} 字符串插值
483                let end = text[i + 2..].find('}').ok_or_else(|| {
484                    DynamicSqlError::ParseError(format!("未闭合的 ${{}}: {}", &text[i..]))
485                })?;
486                let name = &text[i + 2..i + 2 + end];
487                let value = ctx
488                    .params
489                    .get(name)
490                    .ok_or_else(|| DynamicSqlError::MissingParam(name.to_string()))?;
491                let s = param_to_string(value);
492                ctx.buffer.push_str(&s);
493                i += 2 + end + 1;
494            } else {
495                ctx.buffer.push(bytes[i] as char);
496                i += 1;
497            }
498        }
499        Ok(())
500    }
501
502    /// 清理 SQL 中的多余空白
503    fn cleanup_sql(&self, sql: &str) -> String {
504        let mut result = String::with_capacity(sql.len());
505        let mut prev_space = false;
506        for c in sql.chars() {
507            if c.is_whitespace() {
508                if !prev_space {
509                    result.push(' ');
510                    prev_space = true;
511                }
512            } else {
513                result.push(c);
514                prev_space = false;
515            }
516        }
517        result.trim().to_string()
518    }
519}
520
521impl Default for DynamicSqlParser {
522    fn default() -> Self {
523        Self::new()
524    }
525}
526
527/// 构建上下文
528struct BuildContext<'a> {
529    buffer: String,
530    binds: Vec<ParamValue>,
531    params: &'a SqlParams,
532}
533
534impl<'a> BuildContext<'a> {
535    fn new(params: &'a SqlParams) -> Self {
536        Self {
537            buffer: String::new(),
538            binds: Vec::new(),
539            params,
540        }
541    }
542}
543
544/// 评估 `<if test="...">` 表达式
545///
546/// 支持的表达式语法:
547/// - `name != null` — 参数存在且非 Null
548/// - `name == null` — 参数不存在或为 Null
549/// - `name != null and age != null` — AND(不区分大小写)
550/// - `name != null or age != null` — OR
551/// - `name == 'Alice'` — 字符串相等
552/// - `name == "Alice"` — 字符串相等(双引号)
553/// - `age > 18` — 数值比较(仅 >=, >, <, <=)
554fn eval_test(expr: &str, params: &SqlParams) -> Result<bool, DynamicSqlError> {
555    let expr = expr.trim();
556
557    // 处理 OR
558    if let Some(idx) = find_keyword(expr, " or ") {
559        let left = &expr[..idx];
560        let right = &expr[idx + 4..];
561        return Ok(eval_test(left, params)? || eval_test(right, params)?);
562    }
563
564    // 处理 AND
565    if let Some(idx) = find_keyword(expr, " and ") {
566        let left = &expr[..idx];
567        let right = &expr[idx + 5..];
568        return Ok(eval_test(left, params)? && eval_test(right, params)?);
569    }
570
571    // 处理 != null / == null
572    if let Some(stripped) = expr.strip_suffix("!= null") {
573        let name = stripped.trim();
574        return Ok(params.is_not_null(name));
575    }
576    if let Some(stripped) = expr.strip_suffix("== null") {
577        let name = stripped.trim();
578        return Ok(params.is_null(name));
579    }
580
581    // 处理 == '字符串' / == "字符串"
582    if let Some(idx) = expr.find("==") {
583        let left = expr[..idx].trim();
584        let right = expr[idx + 2..].trim();
585        let actual = params.get(left);
586        let expected = right.trim_matches('\'').trim_matches('"');
587        return Ok(match actual {
588            Some(ParamValue::String(s)) => s == expected,
589            _ => false,
590        });
591    }
592    if let Some(idx) = expr.find("!=") {
593        let left = expr[..idx].trim();
594        let right = expr[idx + 2..].trim();
595        let actual = params.get(left);
596        let expected = right.trim_matches('\'').trim_matches('"');
597        return Ok(match actual {
598            Some(ParamValue::String(s)) => s != expected,
599            _ => true,
600        });
601    }
602
603    // 处理 >, <, >=, <=
604    type CmpFn = fn(i64, i64) -> bool;
605    let cmps: [(&str, CmpFn); 4] = [
606        (">=", |a, b| a >= b),
607        ("<=", |a, b| a <= b),
608        (">", |a, b| a > b),
609        ("<", |a, b| a < b),
610    ];
611    for (op, cmp) in cmps {
612        if let Some(idx) = expr.find(op) {
613            let left = expr[..idx].trim();
614            let right_str = expr[idx + op.len()..].trim();
615            if let (Some(ParamValue::Int(a)), Ok(b)) = (params.get(left), right_str.parse::<i64>())
616            {
617                return Ok(cmp(*a, b));
618            }
619            return Ok(false);
620        }
621    }
622
623    Err(DynamicSqlError::EvalError(format!(
624        "无法解析表达式: {}",
625        expr
626    )))
627}
628
629/// 在表达式中查找关键字(不区分大小写,但需要前后是空格)
630fn find_keyword(expr: &str, keyword: &str) -> Option<usize> {
631    let lower = expr.to_lowercase();
632    lower.find(keyword)
633}
634
635/// 去除开头的 AND 或 OR
636fn strip_leading_and_or(s: &str) -> &str {
637    let trimmed = s.trim_start();
638    let lower = trimmed.to_lowercase();
639    if lower.starts_with("and ") {
640        trimmed[4..].trim_start()
641    } else if lower.starts_with("or ") {
642        trimmed[3..].trim_start()
643    } else {
644        trimmed
645    }
646}
647
648/// 参数值转字符串(用于 ${name} 插值)
649fn param_to_string(v: &ParamValue) -> String {
650    match v {
651        ParamValue::Null => "NULL".to_string(),
652        ParamValue::String(s) => escape_sql_string(s),
653        ParamValue::Int(i) => i.to_string(),
654        ParamValue::Float(f) => f.to_string(),
655        ParamValue::Bool(b) => {
656            if *b {
657                "TRUE".to_string()
658            } else {
659                "FALSE".to_string()
660            }
661        }
662        ParamValue::Array(_) => "[]".to_string(),
663    }
664}
665
666/// 转义 SQL 字符串字面量中的特殊字符
667/// 用于 ${name} 插值场景(不推荐使用 ${},应优先使用 #{})
668fn escape_sql_string(s: &str) -> String {
669    let mut out = String::with_capacity(s.len() + 2);
670    for ch in s.chars() {
671        match ch {
672            '\'' => out.push_str("''"),
673            '\\' => out.push_str("\\\\"),
674            '\n' => out.push_str("\\n"),
675            '\r' => out.push_str("\\r"),
676            '\0' => out.push_str("\\0"),
677            _ => out.push(ch),
678        }
679    }
680    out
681}
682
683/// 规范化 SET 子句中的逗号:确保每个逗号后跟一个空格
684/// 用于 `<set>` 标签的输出清理
685fn normalize_set_commas(s: &str) -> String {
686    let mut out = String::with_capacity(s.len());
687    let mut chars = s.chars().peekable();
688    while let Some(ch) = chars.next() {
689        if ch == ',' {
690            out.push(',');
691            // 跳过逗号后已有的空白
692            while matches!(chars.peek(), Some(c) if c.is_whitespace()) {
693                chars.next();
694            }
695            out.push(' ');
696        } else {
697            out.push(ch);
698        }
699    }
700    out
701}
702
703// =====================================================
704// 简易 XML 解析器
705// =====================================================
706
707/// XML 节点(简易解析器内部表示)
708#[derive(Debug, Clone)]
709pub struct XmlNode {
710    /// 节点类型(文本或元素)
711    pub node_type: XmlNodeType,
712    /// 子节点列表
713    pub children: Vec<XmlNode>,
714}
715
716/// XML 节点类型
717#[derive(Debug, Clone)]
718pub enum XmlNodeType {
719    /// 文本节点
720    Text(String),
721    /// 元素节点(含标签名与属性映射)
722    Element {
723        /// 标签名
724        name: String,
725        /// 属性键值对
726        attrs: HashMap<String, String>,
727    },
728}
729
730struct XmlParser<'a> {
731    input: &'a str,
732    pos: usize,
733}
734
735impl<'a> XmlParser<'a> {
736    fn parse(input: &'a str) -> Result<XmlNode, DynamicSqlError> {
737        let mut parser = Self { input, pos: 0 };
738        let mut root = XmlNode {
739            node_type: XmlNodeType::Element {
740                name: "root".to_string(),
741                attrs: HashMap::new(),
742            },
743            children: Vec::new(),
744        };
745        parser.parse_children(&mut root)?;
746        Ok(root)
747    }
748
749    fn parse_children(&mut self, parent: &mut XmlNode) -> Result<(), DynamicSqlError> {
750        loop {
751            if self.pos >= self.input.len() {
752                break;
753            }
754            // 检查是否是结束标签
755            if self.starts_with("</") {
756                break;
757            }
758            // 检查是否是注释
759            if self.starts_with("<!--") {
760                self.skip_comment()?;
761                continue;
762            }
763            // 检查是否是开始标签
764            if self.starts_with("<") {
765                let element = self.parse_element()?;
766                parent.children.push(element);
767            } else {
768                // 文本内容(保留空白,由后续 cleanup_sql 统一规范化)
769                let text = self.parse_text();
770                parent.children.push(XmlNode {
771                    node_type: XmlNodeType::Text(text),
772                    children: Vec::new(),
773                });
774            }
775        }
776        Ok(())
777    }
778
779    fn parse_element(&mut self) -> Result<XmlNode, DynamicSqlError> {
780        // 跳过 '<'
781        self.pos += 1;
782        // 读取标签名
783        let name = self.read_name();
784        // 读取属性
785        let mut attrs = HashMap::new();
786        loop {
787            self.skip_whitespace();
788            if self.pos >= self.input.len() {
789                return Err(DynamicSqlError::ParseError("未闭合的标签".into()));
790            }
791            let c = self.input.as_bytes()[self.pos] as char;
792            if c == '>' {
793                self.pos += 1;
794                break;
795            }
796            if c == '/' {
797                // 自闭合标签
798                if self.pos + 1 < self.input.len() && self.input.as_bytes()[self.pos + 1] == b'>' {
799                    self.pos += 2;
800                    return Ok(XmlNode {
801                        node_type: XmlNodeType::Element { name, attrs },
802                        children: Vec::new(),
803                    });
804                }
805            }
806            // 读取属性名
807            let attr_name = self.read_name();
808            self.skip_whitespace();
809            if self.pos < self.input.len() && self.input.as_bytes()[self.pos] == b'=' {
810                self.pos += 1;
811                self.skip_whitespace();
812                let attr_value = self.read_attr_value()?;
813                attrs.insert(attr_name, attr_value);
814            }
815        }
816        // 解析子节点
817        let mut node = XmlNode {
818            node_type: XmlNodeType::Element {
819                name: name.clone(),
820                attrs,
821            },
822            children: Vec::new(),
823        };
824        self.parse_children(&mut node)?;
825        // 跳过结束标签 </name>
826        // P4-1 修复:未闭合标签必须报错(之前 EOF 时静默返回 Ok)
827        if self.starts_with("</") {
828            self.pos += 2;
829            let end_name = self.read_name();
830            self.skip_whitespace();
831            if self.pos < self.input.len() && self.input.as_bytes()[self.pos] == b'>' {
832                self.pos += 1;
833            }
834            // 验证标签名匹配
835            if let XmlNodeType::Element { name: n, .. } = &node.node_type {
836                if n != &end_name {
837                    return Err(DynamicSqlError::ParseError(format!(
838                        "标签不匹配: <{}> vs </{}>",
839                        n, end_name
840                    )));
841                }
842            }
843        } else {
844            // 非 self-closing 标签必须有 </name> 结束标签
845            // 否则视为未闭合标签错误
846            return Err(DynamicSqlError::ParseError(format!(
847                "未闭合的标签: <{}>(缺少 </{}>)",
848                name, name
849            )));
850        }
851        Ok(node)
852    }
853
854    fn parse_text(&mut self) -> String {
855        let start = self.pos;
856        while self.pos < self.input.len() {
857            let b = self.input.as_bytes()[self.pos];
858            if b == b'<' {
859                break;
860            }
861            self.pos += 1;
862        }
863        // 反转义 XML 实体
864        self.input[start..self.pos]
865            .replace("&gt;", ">")
866            .replace("&lt;", "<")
867            .replace("&amp;", "&")
868            .replace("&quot;", "\"")
869            .replace("&apos;", "'")
870    }
871
872    fn read_name(&mut self) -> String {
873        let start = self.pos;
874        while self.pos < self.input.len() {
875            let b = self.input.as_bytes()[self.pos];
876            if b.is_ascii_alphanumeric() || b == b'_' || b == b'-' || b == b':' {
877                self.pos += 1;
878            } else {
879                break;
880            }
881        }
882        self.input[start..self.pos].to_string()
883    }
884
885    fn read_attr_value(&mut self) -> Result<String, DynamicSqlError> {
886        if self.pos >= self.input.len() {
887            return Err(DynamicSqlError::ParseError("属性值缺失".into()));
888        }
889        let quote = self.input.as_bytes()[self.pos];
890        if quote != b'"' && quote != b'\'' {
891            return Err(DynamicSqlError::ParseError(format!(
892                "属性值应以引号开头, 实际: {}",
893                quote as char
894            )));
895        }
896        self.pos += 1;
897        let start = self.pos;
898        while self.pos < self.input.len() {
899            if self.input.as_bytes()[self.pos] == quote {
900                let raw = &self.input[start..self.pos];
901                // 反转义 XML 实体(属性值中允许出现 &gt; &lt; &amp; &quot; &apos;)
902                let value = raw
903                    .replace("&gt;", ">")
904                    .replace("&lt;", "<")
905                    .replace("&amp;", "&")
906                    .replace("&quot;", "\"")
907                    .replace("&apos;", "'");
908                self.pos += 1;
909                return Ok(value);
910            }
911            self.pos += 1;
912        }
913        Err(DynamicSqlError::ParseError("属性值未闭合".into()))
914    }
915
916    fn skip_whitespace(&mut self) {
917        while self.pos < self.input.len() {
918            if !self.input.as_bytes()[self.pos].is_ascii_whitespace() {
919                break;
920            }
921            self.pos += 1;
922        }
923    }
924
925    fn skip_comment(&mut self) -> Result<(), DynamicSqlError> {
926        // 跳过 <!--
927        self.pos += 4;
928        while self.pos + 2 < self.input.len() {
929            if &self.input[self.pos..self.pos + 3] == "-->" {
930                self.pos += 3;
931                return Ok(());
932            }
933            self.pos += 1;
934        }
935        Err(DynamicSqlError::ParseError("注释未闭合".into()))
936    }
937
938    fn starts_with(&self, s: &str) -> bool {
939        self.input[self.pos..].starts_with(s)
940    }
941}
942
943#[cfg(test)]
944mod tests {
945    use super::*;
946
947    // ---- SqlParams 测试 ----
948
949    #[test]
950    fn test_sql_params_set_get() {
951        let mut p = SqlParams::new();
952        p.set("name", "Alice");
953        p.set_int("age", 30);
954        p.set_bool("active", true);
955        p.set_null("deleted");
956
957        assert!(matches!(p.get("name"), Some(ParamValue::String(_))));
958        assert!(matches!(p.get("age"), Some(ParamValue::Int(30))));
959        assert!(matches!(p.get("active"), Some(ParamValue::Bool(true))));
960        assert!(matches!(p.get("deleted"), Some(ParamValue::Null)));
961        assert!(p.get("missing").is_none());
962    }
963
964    #[test]
965    fn test_sql_params_is_null() {
966        let mut p = SqlParams::new();
967        assert!(p.is_null("missing"));
968        p.set_null("x");
969        assert!(p.is_null("x"));
970        p.set("y", "val");
971        assert!(!p.is_null("y"));
972        assert!(p.is_not_null("y"));
973    }
974
975    #[test]
976    fn test_sql_params_contains() {
977        let mut p = SqlParams::new();
978        p.set("a", "1");
979        assert!(p.contains("a"));
980        assert!(!p.contains("b"));
981    }
982
983    // ---- 简单 SELECT 测试 ----
984
985    #[test]
986    fn test_simple_select_no_params() {
987        let xml = r#"<select id="all">SELECT * FROM users</select>"#;
988        let parser = DynamicSqlParser::from_xml(xml).unwrap();
989        let params = SqlParams::new();
990        let sql = parser.build("all", &params).unwrap();
991        assert_eq!(sql, "SELECT * FROM users");
992    }
993
994    #[test]
995    fn test_select_with_param_binding() {
996        let xml = r#"<select id="by_id">SELECT * FROM users WHERE id = #{id}</select>"#;
997        let parser = DynamicSqlParser::from_xml(xml).unwrap();
998        let mut params = SqlParams::new();
999        params.set_int("id", 42);
1000        let sql = parser.build("by_id", &params).unwrap();
1001        assert_eq!(sql, "SELECT * FROM users WHERE id = ?");
1002    }
1003
1004    #[test]
1005    fn test_select_with_string_interpolation() {
1006        let xml = r#"<select id="by_table">SELECT * FROM ${table}</select>"#;
1007        let parser = DynamicSqlParser::from_xml(xml).unwrap();
1008        let mut params = SqlParams::new();
1009        params.set("table", "users");
1010        let sql = parser.build("by_table", &params).unwrap();
1011        assert_eq!(sql, "SELECT * FROM users");
1012    }
1013
1014    // ---- <if> 测试 ----
1015
1016    #[test]
1017    fn test_if_true() {
1018        let xml = r#"<select id="q">SELECT * FROM users WHERE 1=1 <if test="name != null">AND name = #{name}</if></select>"#;
1019        let parser = DynamicSqlParser::from_xml(xml).unwrap();
1020        let mut params = SqlParams::new();
1021        params.set("name", "Alice");
1022        let sql = parser.build("q", &params).unwrap();
1023        assert!(sql.contains("AND name = ?"));
1024    }
1025
1026    #[test]
1027    fn test_if_false() {
1028        let xml = r#"<select id="q">SELECT * FROM users WHERE 1=1 <if test="name != null">AND name = #{name}</if></select>"#;
1029        let parser = DynamicSqlParser::from_xml(xml).unwrap();
1030        let params = SqlParams::new();
1031        let sql = parser.build("q", &params).unwrap();
1032        assert!(!sql.contains("AND name"));
1033    }
1034
1035    #[test]
1036    fn test_if_null_check() {
1037        let xml = r#"<select id="q">SELECT * FROM users <if test="name == null">WHERE name IS NULL</if></select>"#;
1038        let parser = DynamicSqlParser::from_xml(xml).unwrap();
1039
1040        let params = SqlParams::new();
1041        let sql = parser.build("q", &params).unwrap();
1042        assert!(sql.contains("WHERE name IS NULL"));
1043
1044        let mut params = SqlParams::new();
1045        params.set("name", "Alice");
1046        let sql = parser.build("q", &params).unwrap();
1047        assert!(!sql.contains("WHERE name IS NULL"));
1048    }
1049
1050    #[test]
1051    fn test_if_string_equals() {
1052        let xml = r#"<select id="q">SELECT * FROM users <if test="role == 'admin'">WHERE is_admin = 1</if></select>"#;
1053        let parser = DynamicSqlParser::from_xml(xml).unwrap();
1054
1055        let mut params = SqlParams::new();
1056        params.set("role", "admin");
1057        let sql = parser.build("q", &params).unwrap();
1058        assert!(sql.contains("WHERE is_admin = 1"));
1059
1060        let mut params = SqlParams::new();
1061        params.set("role", "user");
1062        let sql = parser.build("q", &params).unwrap();
1063        assert!(!sql.contains("WHERE is_admin"));
1064    }
1065
1066    #[test]
1067    fn test_if_numeric_comparison() {
1068        let xml = r#"<select id="q">SELECT * FROM users <if test="age &gt; 18">WHERE age &gt; 18</if></select>"#;
1069        let parser = DynamicSqlParser::from_xml(xml).unwrap();
1070
1071        let mut params = SqlParams::new();
1072        params.set_int("age", 25);
1073        let sql = parser.build("q", &params).unwrap();
1074        assert!(sql.contains("WHERE age > 18"));
1075
1076        let mut params = SqlParams::new();
1077        params.set_int("age", 15);
1078        let sql = parser.build("q", &params).unwrap();
1079        assert!(!sql.contains("WHERE age"));
1080    }
1081
1082    #[test]
1083    fn test_if_and_or() {
1084        let xml = r#"<select id="q">SELECT * FROM users <if test="name != null and age != null">WHERE name = #{name} AND age = #{age}</if></select>"#;
1085        let parser = DynamicSqlParser::from_xml(xml).unwrap();
1086
1087        let mut params = SqlParams::new();
1088        params.set("name", "Alice");
1089        params.set_int("age", 30);
1090        let sql = parser.build("q", &params).unwrap();
1091        assert!(sql.contains("WHERE name = ? AND age = ?"));
1092
1093        let mut params = SqlParams::new();
1094        params.set("name", "Alice");
1095        let sql = parser.build("q", &params).unwrap();
1096        assert!(!sql.contains("WHERE"));
1097    }
1098
1099    // ---- <where> 测试 ----
1100
1101    #[test]
1102    fn test_where_strips_leading_and() {
1103        let xml = r#"<select id="q">SELECT * FROM users <where><if test="name != null">AND name = #{name}</if></where></select>"#;
1104        let parser = DynamicSqlParser::from_xml(xml).unwrap();
1105        let mut params = SqlParams::new();
1106        params.set("name", "Alice");
1107        let sql = parser.build("q", &params).unwrap();
1108        assert!(sql.contains("WHERE name = ?"));
1109        assert!(!sql.contains("WHERE AND"));
1110    }
1111
1112    #[test]
1113    fn test_where_empty_no_clause() {
1114        let xml = r#"<select id="q">SELECT * FROM users <where><if test="name != null">AND name = #{name}</if></where></select>"#;
1115        let parser = DynamicSqlParser::from_xml(xml).unwrap();
1116        let params = SqlParams::new();
1117        let sql = parser.build("q", &params).unwrap();
1118        assert_eq!(sql, "SELECT * FROM users");
1119    }
1120
1121    #[test]
1122    fn test_where_multiple_conditions() {
1123        let xml = r#"<select id="q">SELECT * FROM users <where>
1124            <if test="name != null">AND name = #{name}</if>
1125            <if test="age != null">AND age = #{age}</if>
1126        </where></select>"#;
1127        let parser = DynamicSqlParser::from_xml(xml).unwrap();
1128        let mut params = SqlParams::new();
1129        params.set("name", "Alice");
1130        params.set_int("age", 30);
1131        let sql = parser.build("q", &params).unwrap();
1132        assert!(sql.contains("WHERE name = ? AND age = ?"));
1133    }
1134
1135    // ---- <set> 测试 ----
1136
1137    #[test]
1138    fn test_set_strips_trailing_comma() {
1139        let xml = r#"<update id="u">UPDATE users <set><if test="name != null">name = #{name},</if><if test="age != null">age = #{age},</if></set> WHERE id = #{id}</update>"#;
1140        let parser = DynamicSqlParser::from_xml(xml).unwrap();
1141        let mut params = SqlParams::new();
1142        params.set("name", "Alice");
1143        params.set_int("age", 30);
1144        params.set_int("id", 1);
1145        let sql = parser.build("u", &params).unwrap();
1146        // <set> 语义:trim 末尾逗号;逗号后规范化为单个空格
1147        assert!(sql.contains("SET name = ?, age = ? WHERE id = ?"));
1148    }
1149
1150    #[test]
1151    fn test_set_single_field() {
1152        let xml = r#"<update id="u">UPDATE users <set><if test="name != null">name = #{name},</if></set> WHERE id = #{id}</update>"#;
1153        let parser = DynamicSqlParser::from_xml(xml).unwrap();
1154        let mut params = SqlParams::new();
1155        params.set("name", "Alice");
1156        params.set_int("id", 1);
1157        let sql = parser.build("u", &params).unwrap();
1158        assert!(sql.contains("SET name = ? WHERE id = ?"));
1159    }
1160
1161    // ---- <foreach> 测试 ----
1162
1163    #[test]
1164    fn test_foreach_basic() {
1165        let xml = r#"<select id="q">SELECT * FROM users WHERE id IN (<foreach collection="ids" item="id" separator=",">#{id}</foreach>)</select>"#;
1166        let parser = DynamicSqlParser::from_xml(xml).unwrap();
1167        let mut params = SqlParams::new();
1168        params.set_array(
1169            "ids",
1170            vec![ParamValue::Int(1), ParamValue::Int(2), ParamValue::Int(3)],
1171        );
1172        let sql = parser.build("q", &params).unwrap();
1173        // separator="," 不带空格,foreach 前会自动加一个空格
1174        assert!(sql.contains("WHERE id IN ( ?,?,?)"));
1175    }
1176
1177    #[test]
1178    fn test_foreach_empty() {
1179        let xml = r#"<select id="q">SELECT * FROM users WHERE id IN (<foreach collection="ids" item="id" separator=",">#{id}</foreach>)</select>"#;
1180        let parser = DynamicSqlParser::from_xml(xml).unwrap();
1181        let params = SqlParams::new();
1182        let sql = parser.build("q", &params).unwrap();
1183        assert!(sql.contains("WHERE id IN ( )") || sql.contains("WHERE id IN ()"));
1184    }
1185
1186    #[test]
1187    fn test_foreach_with_strings() {
1188        let xml = r#"<select id="q">SELECT * FROM users WHERE name IN (<foreach collection="names" item="n" separator=",">#{n}</foreach>)</select>"#;
1189        let parser = DynamicSqlParser::from_xml(xml).unwrap();
1190        let mut params = SqlParams::new();
1191        params.set_array(
1192            "names",
1193            vec![
1194                ParamValue::String("Alice".into()),
1195                ParamValue::String("Bob".into()),
1196            ],
1197        );
1198        let (sql, binds) = parser.build_with_binds("q", &params).unwrap();
1199        assert_eq!(binds.len(), 2);
1200        assert!(sql.contains("?"));
1201    }
1202
1203    // ---- <choose> 测试 ----
1204
1205    #[test]
1206    fn test_choose_when_matches() {
1207        let xml = r#"<select id="q">SELECT * FROM users WHERE 1=1 <choose>
1208            <when test="role == 'admin'">AND is_admin = 1</when>
1209            <otherwise>AND is_admin = 0</otherwise>
1210        </choose></select>"#;
1211        let parser = DynamicSqlParser::from_xml(xml).unwrap();
1212        let mut params = SqlParams::new();
1213        params.set("role", "admin");
1214        let sql = parser.build("q", &params).unwrap();
1215        assert!(sql.contains("AND is_admin = 1"));
1216        assert!(!sql.contains("AND is_admin = 0"));
1217    }
1218
1219    #[test]
1220    fn test_choose_otherwise() {
1221        let xml = r#"<select id="q">SELECT * FROM users WHERE 1=1 <choose>
1222            <when test="role == 'admin'">AND is_admin = 1</when>
1223            <otherwise>AND is_admin = 0</otherwise>
1224        </choose></select>"#;
1225        let parser = DynamicSqlParser::from_xml(xml).unwrap();
1226        let mut params = SqlParams::new();
1227        params.set("role", "user");
1228        let sql = parser.build("q", &params).unwrap();
1229        assert!(sql.contains("AND is_admin = 0"));
1230    }
1231
1232    // ---- <trim> 测试 ----
1233
1234    #[test]
1235    fn test_trim_prefix_suffix() {
1236        let xml = r#"<select id="q">SELECT * FROM users <trim prefix="WHERE" prefixOverrides="AND |OR "><if test="name != null">AND name = #{name}</if></trim></select>"#;
1237        let parser = DynamicSqlParser::from_xml(xml).unwrap();
1238        let mut params = SqlParams::new();
1239        params.set("name", "Alice");
1240        let sql = parser.build("q", &params).unwrap();
1241        assert!(sql.contains("WHERE name = ?"));
1242    }
1243
1244    #[test]
1245    fn test_trim_empty() {
1246        let xml = r#"<select id="q">SELECT * FROM users <trim prefix="WHERE" prefixOverrides="AND"><if test="name != null">AND name = #{name}</if></trim></select>"#;
1247        let parser = DynamicSqlParser::from_xml(xml).unwrap();
1248        let params = SqlParams::new();
1249        let sql = parser.build("q", &params).unwrap();
1250        assert_eq!(sql, "SELECT * FROM users");
1251    }
1252
1253    // ---- 错误处理测试 ----
1254
1255    #[test]
1256    fn test_statement_not_found() {
1257        let xml = r#"<select id="a">SELECT 1</select>"#;
1258        let parser = DynamicSqlParser::from_xml(xml).unwrap();
1259        let params = SqlParams::new();
1260        let result = parser.build("missing", &params);
1261        assert!(matches!(result, Err(DynamicSqlError::StatementNotFound(_))));
1262    }
1263
1264    #[test]
1265    fn test_missing_param() {
1266        let xml = r#"<select id="q">SELECT * FROM users WHERE id = #{id}</select>"#;
1267        let parser = DynamicSqlParser::from_xml(xml).unwrap();
1268        let params = SqlParams::new();
1269        let result = parser.build("q", &params);
1270        assert!(matches!(result, Err(DynamicSqlError::MissingParam(_))));
1271    }
1272
1273    #[test]
1274    fn test_parse_error_unclosed_tag() {
1275        // P4-1 修复:未闭合标签必须返回 ParseError,而非"接受任何结果"
1276        let xml = r#"<select id="q">SELECT 1"#;
1277        let result = DynamicSqlParser::from_xml(xml);
1278        assert!(
1279            matches!(result, Err(DynamicSqlError::ParseError(_))),
1280            "未闭合标签应返回 ParseError,实际: {:?}",
1281            result
1282        );
1283        if let Err(DynamicSqlError::ParseError(msg)) = result {
1284            assert!(
1285                msg.contains("未闭合") || msg.contains("标签") || msg.contains("EOF"),
1286                "错误信息应提及未闭合/标签/EOF: {}",
1287                msg
1288            );
1289        }
1290    }
1291
1292    // ---- 多语句管理测试 ----
1293
1294    #[test]
1295    fn test_multiple_statements() {
1296        let xml = r#"
1297        <select id="find_all">SELECT * FROM users</select>
1298        <select id="find_by_id">SELECT * FROM users WHERE id = #{id}</select>
1299        <insert id="insert">INSERT INTO users (name) VALUES (#{name})</insert>
1300        <update id="update">UPDATE users SET name = #{name} WHERE id = #{id}</update>
1301        <delete id="delete">DELETE FROM users WHERE id = #{id}</delete>
1302        "#;
1303        let parser = DynamicSqlParser::from_xml(xml).unwrap();
1304        let mut ids = parser.statement_ids();
1305        ids.sort();
1306        assert_eq!(
1307            ids,
1308            vec!["delete", "find_all", "find_by_id", "insert", "update"]
1309        );
1310    }
1311
1312    // ---- XML 实体测试 ----
1313
1314    #[test]
1315    fn test_xml_entities() {
1316        let xml =
1317            r#"<select id="q">SELECT * FROM users WHERE age &gt; 18 AND age &lt; 65</select>"#;
1318        let parser = DynamicSqlParser::from_xml(xml).unwrap();
1319        let params = SqlParams::new();
1320        let sql = parser.build("q", &params).unwrap();
1321        assert!(sql.contains("age > 18 AND age < 65"));
1322    }
1323
1324    // ---- 完整流程测试 ----
1325
1326    #[test]
1327    fn test_full_dynamic_query() {
1328        let xml = r#"<select id="search">
1329            SELECT u.id, u.name, o.total
1330            FROM users u
1331            LEFT JOIN orders o ON u.id = o.user_id
1332            <where>
1333                <if test="name != null">AND u.name LIKE #{name}</if>
1334                <if test="min_age != null">AND u.age &gt; #{min_age}</if>
1335                <if test="status != null">AND u.status = #{status}</if>
1336            </where>
1337            ORDER BY u.id
1338        </select>"#;
1339        let parser = DynamicSqlParser::from_xml(xml).unwrap();
1340        let mut params = SqlParams::new();
1341        params.set("name", "%Alice%");
1342        params.set_int("min_age", 18);
1343        // status 不设置
1344
1345        let (sql, binds) = parser.build_with_binds("search", &params).unwrap();
1346        assert!(sql.contains("SELECT u.id, u.name, o.total"));
1347        assert!(sql.contains("FROM users u"));
1348        assert!(sql.contains("LEFT JOIN orders o ON u.id = o.user_id"));
1349        assert!(sql.contains("WHERE u.name LIKE ? AND u.age > ?"));
1350        assert!(!sql.contains("u.status"));
1351        assert!(sql.contains("ORDER BY u.id"));
1352        assert_eq!(binds.len(), 2);
1353    }
1354}