Skip to main content

sz_orm_core/
query.rs

1//! 查询构造器
2//!
3//! 提供类似 ThinkORM 的链式查询构造 API
4//!
5//! # P0-1 软删除集成(v1.3.0+)
6//!
7//! 当 `M: Model` 实现了 `soft_delete_field()` 返回 `Some(field)` 时,
8//! `QueryBuilder` 会在以下场景自动追加 `WHERE {field} IS NULL`:
9//! - `build_select` / `build_select_with_params`
10//! - `build_count` / `build_exists` / `build_max` / `build_min` / `build_sum` / `build_avg`
11//! - `build_update` / `build_update_with_params`(防止更新已删除记录)
12//! - `build_delete` / `build_delete_with_params`(自动转为 `UPDATE SET {field} = NOW()`)
13//!
14//! 使用 `without_soft_delete()` 可临时禁用软删除过滤(用于查询已删除记录)。
15//!
16//! # P0-2 参数化 WHERE 条件(v1.3.0+)
17//!
18//! 新增类型安全的参数化 WHERE API:
19//! - `where_eq(field, value)` / `where_ne` / `where_gt` / `where_ge` / `where_lt` / `where_le`
20//! - `where_like(field, pattern)`
21//!
22//! 这些方法使用 `?` 占位符 + `Value` 绑定,杜绝 SQL 注入。
23//! 原有 `where_cond(condition: impl Into<String>)` 因字符串拼接存在注入风险,
24//! 保留以兼容复杂表达式(如 `age > 18 AND status = 'active'`),但文档标记为不推荐。
25
26use crate::dialect::Dialect;
27use crate::model::Model;
28use crate::value::Value;
29use std::fmt;
30
31/// 用于构造 SQL 查询的查询构造器
32pub struct QueryBuilder<M: Model> {
33    table: Option<String>,
34    select_columns: Vec<String>,
35    where_conditions: Vec<WhereCondition>,
36    order_by: Vec<OrderClause>,
37    group_by: Vec<String>,
38    having_conditions: Vec<WhereCondition>,
39    limit_value: Option<usize>,
40    offset_value: Option<usize>,
41    joins: Vec<JoinClause>,
42    dialect: Box<dyn Dialect>,
43    /// P0-1:是否禁用软删除过滤(true 表示禁用,查询包含已删除记录)
44    soft_delete_disabled: bool,
45    /// P0-3:当前租户 ID(运行时注入)。设置后自动追加 `WHERE {tenant_field} = ?`
46    tenant_id_value: Option<i64>,
47    /// P0-3:是否禁用租户过滤(true 表示禁用,跨租户查询)
48    tenant_disabled: bool,
49    /// P2-5:Keyset 分页游标条件(field, value, direction)
50    ///
51    /// 设置后,`build_select`/`build_select_with_params` 会追加 `WHERE {field} > ?` 或
52    /// `WHERE {field} < ?` 条件(取决于排序方向),实现基于游标的高效分页。
53    /// 与 OFFSET 分页相比,Keyset 分页在大数据集下性能稳定,不受数据插入/删除影响。
54    keyset_cursor: Option<KeysetCursor>,
55    #[allow(dead_code)]
56    model: std::marker::PhantomData<M>,
57}
58
59/// P2-5:Keyset 分页游标
60///
61/// 表示一个基于排序字段值的分页游标。结合 `ORDER BY {field} {direction}` 和
62/// `WHERE {field} {op} ?` 实现游标分页。
63///
64/// - `After(value)` + `Asc`:查询 `field > value` 的记录(下一页)
65/// - `Before(value)` + `Desc`:查询 `field < value` 的记录(上一页)
66#[derive(Debug, Clone)]
67struct KeysetCursor {
68    /// 排序字段名
69    field: String,
70    /// 游标值(上一页/下一页最后一行的该字段值)
71    value: Value,
72    /// 游标方向:After = 下一页(field > value),Before = 上一页(field < value)
73    direction: KeysetDirection,
74}
75
76/// P2-5:Keyset 游标方向
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78enum KeysetDirection {
79    /// 下一页:`WHERE field > cursor_value`(配合 ASC 排序)
80    After,
81    /// 上一页:`WHERE field < cursor_value`(配合 DESC 排序)
82    Before,
83}
84
85#[derive(Debug, Clone)]
86#[allow(dead_code)]
87enum WhereCondition {
88    /// 原始字符串条件(AND)— **存在注入风险,不推荐使用**
89    ///
90    /// 保留以兼容复杂表达式如 `age > 18 AND status = 'active'`。
91    /// 调用方必须确保字符串来自可信来源。
92    And(String),
93    /// 原始字符串条件(OR)— **存在注入风险,不推荐使用**
94    Or(String),
95    /// P0-2:参数化等值条件 `field = ?`
96    Eq(String, Value),
97    /// P0-2:参数化不等条件 `field != ?`
98    Ne(String, Value),
99    /// P0-2:参数化大于条件 `field > ?`
100    Gt(String, Value),
101    /// P0-2:参数化大于等于条件 `field >= ?`
102    Ge(String, Value),
103    /// P0-2:参数化小于条件 `field < ?`
104    Lt(String, Value),
105    /// P0-2:参数化小于等于条件 `field <= ?`
106    Le(String, Value),
107    /// P0-2:参数化 LIKE 条件 `field LIKE ?`
108    Like(String, Value),
109    /// P0-2:参数化 OR 等值条件 `OR field = ?`
110    OrEq(String, Value),
111    /// P0-2:参数化 OR 不等条件 `OR field != ?`
112    OrNe(String, Value),
113    /// P0-2:参数化 OR 大于条件 `OR field > ?`
114    OrGt(String, Value),
115    /// P0-2:参数化 OR 大于等于条件 `OR field >= ?`
116    OrGe(String, Value),
117    /// P0-2:参数化 OR 小于条件 `OR field < ?`
118    OrLt(String, Value),
119    /// P0-2:参数化 OR 小于等于条件 `OR field <= ?`
120    OrLe(String, Value),
121    /// P0-2:参数化 OR LIKE 条件 `OR field LIKE ?`
122    OrLike(String, Value),
123    In(String, Vec<Value>),
124    NotIn(String, Vec<Value>),
125    Between(String, Value, Value),
126    NotBetween(String, Value, Value),
127    Null(String),
128    NotNull(String),
129    Exists(String),
130    NotExists(String),
131}
132
133#[derive(Debug, Clone)]
134struct OrderClause {
135    field: String,
136    direction: OrderDirection,
137}
138
139#[derive(Debug, Clone)]
140enum OrderDirection {
141    Asc,
142    Desc,
143}
144
145#[derive(Debug, Clone)]
146#[allow(dead_code)]
147enum JoinClause {
148    Inner(String, String, String),
149    Left(String, String, String),
150    Right(String, String, String),
151    Cross(String, String),
152}
153
154impl<M: Model> QueryBuilder<M> {
155    pub fn new(dialect: Box<dyn Dialect>) -> Self {
156        Self {
157            table: None,
158            select_columns: vec!["*".to_string()],
159            where_conditions: Vec::new(),
160            order_by: Vec::new(),
161            group_by: Vec::new(),
162            having_conditions: Vec::new(),
163            limit_value: None,
164            offset_value: None,
165            joins: Vec::new(),
166            dialect,
167            soft_delete_disabled: false,
168            tenant_id_value: None,
169            tenant_disabled: false,
170            keyset_cursor: None,
171            model: std::marker::PhantomData,
172        }
173    }
174
175    pub fn table(mut self, table: impl Into<String>) -> Self {
176        self.table = Some(table.into());
177        self
178    }
179
180    /// P0-1:临时禁用软删除过滤,用于查询已删除的记录。
181    ///
182    /// 等价于 SeaORM 的 `Entity::find().filter(Column::DeletedAt.is_not_null())`
183    /// 或 Laravel Eloquent 的 `Model::withTrashed()`。
184    ///
185    /// # 示例
186    ///
187    /// ```ignore
188    /// use sz_orm_core::query::QueryBuilder;
189    /// use sz_orm_core::dialect::MySqlDialect;
190    ///
191    /// // 查询包含已软删除的用户
192    /// let sql = QueryBuilder::<User>::new(Box::new(MySqlDialect))
193    ///     .table("users")
194    ///     .without_soft_delete()
195    ///     .build_select();
196    /// // 不会自动追加 WHERE deleted_at IS NULL
197    /// ```
198    pub fn without_soft_delete(mut self) -> Self {
199        self.soft_delete_disabled = true;
200        self
201    }
202
203    /// P0-1:返回软删除过滤是否被禁用
204    pub fn is_soft_delete_disabled(&self) -> bool {
205        self.soft_delete_disabled
206    }
207
208    /// P0-1:返回当前 Model 的软删除字段名(若启用)
209    ///
210    /// 内部使用,用于 `build_*` 方法决定是否追加 `WHERE {field} IS NULL`。
211    fn soft_delete_field(&self) -> Option<&'static str> {
212        if self.soft_delete_disabled {
213            return None;
214        }
215        M::soft_delete_field()
216    }
217
218    /// P0-1:构造软删除过滤条件 SQL 片段(不含 `AND` 前缀)
219    ///
220    /// 返回 `None` 表示无需过滤;返回 `Some(sql)` 表示追加 `AND {sql}` 到 WHERE 子句。
221    fn build_soft_delete_condition(&self) -> Option<String> {
222        self.soft_delete_field()
223            .map(|field| format!("{} IS NULL", self.dialect.quote(field)))
224    }
225
226    // ===================== P0-3 多租户过滤 =====================
227
228    /// P0-3:设置当前租户 ID,启用多租户自动过滤。
229    ///
230    /// 当 `M::tenant_field()` 返回 `Some(field)` 时,`QueryBuilder` 会在以下场景
231    /// 自动追加 `WHERE {field} = ?`(参数化,值通过 `params` 绑定):
232    /// - `build_select` / `build_select_with_params`
233    /// - `build_count` / `build_exists` / `build_max` / `build_min` / `build_sum` / `build_avg`
234    /// - `build_update` / `build_update_with_params`(防止跨租户更新)
235    /// - `build_delete` / `build_delete_with_params`(防止跨租户删除)
236    ///
237    /// 使用 `without_tenant()` 可临时禁用租户过滤(用于跨租户管理查询)。
238    ///
239    /// # 示例
240    ///
241    /// ```ignore
242    /// use sz_orm_core::query::QueryBuilder;
243    /// use sz_orm_core::dialect::MySqlDialect;
244    ///
245    /// let (sql, params) = QueryBuilder::<Order>::new(Box::new(MySqlDialect))
246    ///     .table("orders")
247    ///     .with_tenant_id(42)
248    ///     .build_select_with_params();
249    /// // sql => "SELECT * FROM `orders` WHERE `tenant_id` = ?"
250    /// // params => [Value::I64(42)]
251    /// ```
252    pub fn with_tenant_id(mut self, tenant_id: i64) -> Self {
253        self.tenant_id_value = Some(tenant_id);
254        self
255    }
256
257    /// P0-3:临时禁用租户过滤,用于跨租户管理查询。
258    ///
259    /// 等价于 Laravel Eloquent 的全局作用域禁用。
260    pub fn without_tenant(mut self) -> Self {
261        self.tenant_disabled = true;
262        self
263    }
264
265    /// P0-3:返回租户过滤是否被禁用
266    pub fn is_tenant_disabled(&self) -> bool {
267        self.tenant_disabled
268    }
269
270    /// P0-3:返回当前 Model 的租户字段名(若启用且未禁用)
271    ///
272    /// 内部使用,用于 `build_*` 方法决定是否追加租户条件。
273    fn tenant_field(&self) -> Option<&'static str> {
274        if self.tenant_disabled {
275            return None;
276        }
277        M::tenant_field()
278    }
279
280    /// P0-3:返回当前租户 ID(若设置了且未禁用)
281    fn tenant_id_value(&self) -> Option<i64> {
282        if self.tenant_disabled {
283            return None;
284        }
285        self.tenant_id_value
286    }
287
288    /// P0-3:构造租户过滤条件(SQL 片段 + 参数值)
289    ///
290    /// 返回 `None` 表示无需过滤;返回 `Some((sql, value))` 表示追加 `AND {sql}` 到 WHERE 子句,
291    /// 并将 `value` 加入参数列表。
292    fn build_tenant_condition(&self) -> Option<(String, Value)> {
293        let field = self.tenant_field()?;
294        let tid = self.tenant_id_value()?;
295        Some((
296            format!("{} = ?", self.dialect.quote(field)),
297            Value::I64(tid),
298        ))
299    }
300
301    /// 设置 SELECT 列。
302    ///
303    /// **M-3 安全警告**:本方法直接拼接 `columns` 到 SQL,**不**进行标识符校验或 quote。
304    /// 调用方必须确保 `columns` 来自可信来源(硬编码或经 `sql_safety::validate_identifier`
305    /// 校验)。若列名可能来自不可信输入,请使用 [`QueryBuilder::select_quoted`]。
306    ///
307    /// 本方法保留原行为以兼容复杂表达式(如 `COUNT(*)`、`users.id AS uid`)。
308    pub fn select(mut self, columns: Vec<&str>) -> Self {
309        self.select_columns = columns.into_iter().map(|s| s.to_string()).collect();
310        self
311    }
312
313    /// M-3 修复:安全的 SELECT 列设置,自动校验每个列名并 quote。
314    ///
315    /// 每个 `column` 必须通过 `sql_safety::validate_identifier` 校验
316    /// (仅允许 ASCII 字母数字 + 下划线,不以数字开头,长度 1-63)。
317    /// 校验失败时返回 `DbError::InvalidInput`。
318    ///
319    /// 对于复杂表达式(如 `COUNT(*)`、`users.id AS uid`),请使用 [`QueryBuilder::select`]
320    /// 并自行确保安全。
321    pub fn select_quoted(mut self, columns: Vec<&str>) -> Result<Self, crate::DbError> {
322        let mut quoted = Vec::with_capacity(columns.len());
323        for col in columns {
324            crate::sql_safety::validate_identifier(col, "select column")?;
325            quoted.push(self.dialect.quote(col));
326        }
327        self.select_columns = quoted;
328        Ok(self)
329    }
330
331    /// 添加原始字符串 WHERE 条件(AND 关系)。
332    ///
333    /// **⚠️ P0-2 安全警告(v1.3.0+)**:本方法直接拼接 `condition` 到 SQL,
334    /// **存在 SQL 注入风险**。仅在以下场景使用:
335    /// - 条件来自硬编码字符串(如 `where_cond("age > 18")`)
336    /// - 条件含复杂表达式(如 `where_cond("status = 'active' AND role = 'admin'")`)
337    ///
338    /// **禁止**将用户输入拼接到 `condition` 中。若值来自不可信来源,
339    /// 必须使用参数化方法:[`where_eq`](Self::where_eq) / [`where_ne`](Self::where_ne) /
340    /// [`where_gt`](Self::where_gt) / [`where_lt`](Self::where_lt) / [`where_like`](Self::where_like)。
341    ///
342    /// # 推荐迁移
343    ///
344    /// ```ignore
345    /// // ❌ 危险:字符串拼接
346    /// builder.where_cond(format!("name = '{}'", user_input));
347    ///
348    /// // ✅ 安全:参数化绑定
349    /// builder.where_eq("name", Value::String(user_input.to_string()));
350    /// ```
351    #[deprecated(
352        since = "1.3.0",
353        note = "P0-2: 字符串拼接存在 SQL 注入风险,请使用 where_eq/where_ne/where_gt/where_lt/where_like 等参数化方法"
354    )]
355    pub fn where_cond(mut self, condition: impl Into<String>) -> Self {
356        self.where_conditions
357            .push(WhereCondition::And(condition.into()));
358        self
359    }
360
361    /// 添加原始字符串 WHERE 条件(OR 关系)。
362    ///
363    /// **⚠️ P0-2 安全警告**:同 [`where_cond`](Self::where_cond),存在注入风险。
364    #[deprecated(
365        since = "1.3.0",
366        note = "P0-2: 字符串拼接存在 SQL 注入风险,请使用参数化方法"
367    )]
368    pub fn or_where(mut self, condition: impl Into<String>) -> Self {
369        self.where_conditions
370            .push(WhereCondition::Or(condition.into()));
371        self
372    }
373
374    /// P0-2:参数化等值条件 `field = ?`(AND 关系)。
375    ///
376    /// 值通过 `?` 占位符绑定,杜绝 SQL 注入。
377    ///
378    /// # 示例
379    ///
380    /// ```ignore
381    /// use sz_orm_core::Value;
382    ///
383    /// builder
384    ///     .where_eq("status", Value::String("active".into()))
385    ///     .where_eq("tenant_id", Value::I64(42));
386    /// ```
387    pub fn where_eq(mut self, field: impl Into<String>, value: Value) -> Self {
388        self.where_conditions
389            .push(WhereCondition::Eq(field.into(), value));
390        self
391    }
392
393    /// P0-2:参数化不等条件 `field != ?`(AND 关系)。
394    pub fn where_ne(mut self, field: impl Into<String>, value: Value) -> Self {
395        self.where_conditions
396            .push(WhereCondition::Ne(field.into(), value));
397        self
398    }
399
400    /// P0-2:参数化大于条件 `field > ?`(AND 关系)。
401    pub fn where_gt(mut self, field: impl Into<String>, value: Value) -> Self {
402        self.where_conditions
403            .push(WhereCondition::Gt(field.into(), value));
404        self
405    }
406
407    /// P0-2:参数化大于等于条件 `field >= ?`(AND 关系)。
408    pub fn where_ge(mut self, field: impl Into<String>, value: Value) -> Self {
409        self.where_conditions
410            .push(WhereCondition::Ge(field.into(), value));
411        self
412    }
413
414    /// P0-2:参数化小于条件 `field < ?`(AND 关系)。
415    pub fn where_lt(mut self, field: impl Into<String>, value: Value) -> Self {
416        self.where_conditions
417            .push(WhereCondition::Lt(field.into(), value));
418        self
419    }
420
421    /// P0-2:参数化小于等于条件 `field <= ?`(AND 关系)。
422    pub fn where_le(mut self, field: impl Into<String>, value: Value) -> Self {
423        self.where_conditions
424            .push(WhereCondition::Le(field.into(), value));
425        self
426    }
427
428    /// P0-2:参数化 LIKE 条件 `field LIKE ?`(AND 关系)。
429    ///
430    /// 调用方负责在 `pattern` 中包含 `%` 通配符。
431    ///
432    /// # 示例
433    ///
434    /// ```ignore
435    /// use sz_orm_core::Value;
436    ///
437    /// builder.where_like("name", Value::String("%alice%".into()));
438    /// ```
439    pub fn where_like(mut self, field: impl Into<String>, pattern: Value) -> Self {
440        self.where_conditions
441            .push(WhereCondition::Like(field.into(), pattern));
442        self
443    }
444
445    /// P0-2:参数化 OR 等值条件 `OR field = ?`。
446    ///
447    /// 值通过 `?` 占位符绑定,杜绝 SQL 注入。OR 条件会与相邻的 OR 条件组合成 `(cond1 OR cond2)` 形式。
448    pub fn or_where_eq(mut self, field: impl Into<String>, value: Value) -> Self {
449        self.where_conditions
450            .push(WhereCondition::OrEq(field.into(), value));
451        self
452    }
453
454    /// P0-2:参数化 OR 不等条件 `OR field != ?`。
455    pub fn or_where_ne(mut self, field: impl Into<String>, value: Value) -> Self {
456        self.where_conditions
457            .push(WhereCondition::OrNe(field.into(), value));
458        self
459    }
460
461    /// P0-2:参数化 OR 大于条件 `OR field > ?`。
462    pub fn or_where_gt(mut self, field: impl Into<String>, value: Value) -> Self {
463        self.where_conditions
464            .push(WhereCondition::OrGt(field.into(), value));
465        self
466    }
467
468    /// P0-2:参数化 OR 大于等于条件 `OR field >= ?`。
469    pub fn or_where_ge(mut self, field: impl Into<String>, value: Value) -> Self {
470        self.where_conditions
471            .push(WhereCondition::OrGe(field.into(), value));
472        self
473    }
474
475    /// P0-2:参数化 OR 小于条件 `OR field < ?`。
476    pub fn or_where_lt(mut self, field: impl Into<String>, value: Value) -> Self {
477        self.where_conditions
478            .push(WhereCondition::OrLt(field.into(), value));
479        self
480    }
481
482    /// P0-2:参数化 OR 小于等于条件 `OR field <= ?`。
483    pub fn or_where_le(mut self, field: impl Into<String>, value: Value) -> Self {
484        self.where_conditions
485            .push(WhereCondition::OrLe(field.into(), value));
486        self
487    }
488
489    /// P0-2:参数化 OR LIKE 条件 `OR field LIKE ?`。
490    pub fn or_where_like(mut self, field: impl Into<String>, pattern: Value) -> Self {
491        self.where_conditions
492            .push(WhereCondition::OrLike(field.into(), pattern));
493        self
494    }
495
496    pub fn where_in(mut self, field: impl Into<String>, values: Vec<Value>) -> Self {
497        self.where_conditions
498            .push(WhereCondition::In(field.into(), values));
499        self
500    }
501
502    pub fn where_not_in(mut self, field: impl Into<String>, values: Vec<Value>) -> Self {
503        self.where_conditions
504            .push(WhereCondition::NotIn(field.into(), values));
505        self
506    }
507
508    pub fn where_between(mut self, field: impl Into<String>, start: Value, end: Value) -> Self {
509        self.where_conditions
510            .push(WhereCondition::Between(field.into(), start, end));
511        self
512    }
513
514    pub fn where_not_between(mut self, field: impl Into<String>, start: Value, end: Value) -> Self {
515        self.where_conditions
516            .push(WhereCondition::NotBetween(field.into(), start, end));
517        self
518    }
519
520    pub fn where_null(mut self, field: impl Into<String>) -> Self {
521        self.where_conditions
522            .push(WhereCondition::Null(field.into()));
523        self
524    }
525
526    pub fn where_not_null(mut self, field: impl Into<String>) -> Self {
527        self.where_conditions
528            .push(WhereCondition::NotNull(field.into()));
529        self
530    }
531
532    pub fn order_by(mut self, field: impl Into<String>) -> Self {
533        self.order_by.push(OrderClause {
534            field: field.into(),
535            direction: OrderDirection::Asc,
536        });
537        self
538    }
539
540    pub fn order_desc(mut self, field: impl Into<String>) -> Self {
541        self.order_by.push(OrderClause {
542            field: field.into(),
543            direction: OrderDirection::Desc,
544        });
545        self
546    }
547
548    pub fn group_by(mut self, field: impl Into<String>) -> Self {
549        self.group_by.push(field.into());
550        self
551    }
552
553    pub fn having(mut self, condition: impl Into<String>) -> Self {
554        self.having_conditions
555            .push(WhereCondition::And(condition.into()));
556        self
557    }
558
559    pub fn limit(mut self, limit: usize) -> Self {
560        self.limit_value = Some(limit);
561        self
562    }
563
564    pub fn offset(mut self, offset: usize) -> Self {
565        self.offset_value = Some(offset);
566        self
567    }
568
569    pub fn page(mut self, page: usize, page_size: usize) -> Self {
570        self.limit_value = Some(page_size);
571        self.offset_value = Some((page.saturating_sub(1)) * page_size);
572        self
573    }
574
575    /// P2-5:基于游标的 Keyset 分页 — 查询指定字段值之后的记录(下一页)
576    ///
577    /// 生成 `WHERE {field} > ? ORDER BY {field} ASC LIMIT {page_size}`。
578    /// 适用于按主键或时间戳递增遍历大表的场景,性能不受数据插入/删除影响。
579    ///
580    /// # 参数
581    ///
582    /// - `field`:排序字段(通常是主键或索引列,如 `id`、`created_at`)
583    /// - `cursor_value`:当前页最后一条记录的该字段值
584    /// - `page_size`:每页大小
585    ///
586    /// # 示例
587    ///
588    /// ```
589    /// use sz_orm_core::{QueryBuilder, DbType, dialect::get_dialect, Value};
590    /// # use sz_orm_core::{Model, ModelExt};
591    /// # #[derive(Clone, Debug)]
592    /// # struct User { id: i64 }
593    /// # impl Model for User {
594    /// #     type PrimaryKey = i64;
595    /// #     fn table_name() -> &'static str { "users" }
596    /// #     fn pk(&self) -> i64 { self.id }
597    /// #     fn set_pk(&mut self, pk: i64) { self.id = pk; }
598    /// # }
599    /// # impl ModelExt for User {
600    /// #     fn columns() -> Vec<&'static str> { vec!["id"] }
601    /// #     fn fillable() -> Vec<&'static str> { vec![] }
602    /// #     fn guarded() -> Vec<&'static str> { vec!["id"] }
603    /// #     fn hidden() -> Vec<&'static str> { vec![] }
604    /// #     fn relations() -> std::collections::HashMap<&'static str, sz_orm_core::Relation> { Default::default() }
605    /// #     fn fill(&mut self, _: std::collections::HashMap<String, Value>) {}
606    /// #     fn to_json(&self) -> serde_json::Value { serde_json::json!({}) }
607    /// # }
608    /// let dialect = get_dialect(DbType::MySQL).unwrap();
609    /// let builder = QueryBuilder::<User>::new(dialect)
610    ///     .keyset_after("id", Value::I64(100), 20);
611    /// let (sql, params) = builder.build_select_with_params();
612    /// // 方言会引用字段名(如 MySQL 的 `id`),去除引号后检查
613    /// let sql_clean = sql.replace('`', "").replace('"', "");
614    /// assert!(sql_clean.contains("id > ?"));
615    /// assert!(sql.to_uppercase().contains("ORDER BY"));
616    /// assert!(sql.to_uppercase().contains("ASC"));
617    /// assert!(sql.contains("LIMIT 20"));
618    /// assert_eq!(params, vec![Value::I64(100)]);
619    /// ```
620    pub fn keyset_after(
621        mut self,
622        field: impl Into<String>,
623        cursor_value: Value,
624        page_size: usize,
625    ) -> Self {
626        let field_str = field.into();
627        // 自动设置 ORDER BY ASC(若字段已存在则更新方向为 ASC,保证 keyset 语义一致)
628        if let Some(existing) = self.order_by.iter_mut().find(|o| o.field == field_str) {
629            existing.direction = OrderDirection::Asc;
630        } else {
631            self.order_by.push(OrderClause {
632                field: field_str.clone(),
633                direction: OrderDirection::Asc,
634            });
635        }
636        self.limit_value = Some(page_size);
637        // 清除 offset(keyset 与 offset 互斥)
638        self.offset_value = None;
639        self.keyset_cursor = Some(KeysetCursor {
640            field: field_str,
641            value: cursor_value,
642            direction: KeysetDirection::After,
643        });
644        self
645    }
646
647    /// P2-5:基于游标的 Keyset 分页 — 查询指定字段值之前的记录(上一页)
648    ///
649    /// 生成 `WHERE {field} < ? ORDER BY {field} DESC LIMIT {page_size}`。
650    /// 适用于反向遍历场景。
651    ///
652    /// # 参数
653    ///
654    /// - `field`:排序字段
655    /// - `cursor_value`:当前页第一条记录的该字段值
656    /// - `page_size`:每页大小
657    ///
658    /// # 示例
659    ///
660    /// ```
661    /// use sz_orm_core::{QueryBuilder, DbType, dialect::get_dialect, Value};
662    /// # use sz_orm_core::{Model, ModelExt};
663    /// # #[derive(Clone, Debug)]
664    /// # struct User { id: i64 }
665    /// # impl Model for User {
666    /// #     type PrimaryKey = i64;
667    /// #     fn table_name() -> &'static str { "users" }
668    /// #     fn pk(&self) -> i64 { self.id }
669    /// #     fn set_pk(&mut self, pk: i64) { self.id = pk; }
670    /// # }
671    /// # impl ModelExt for User {
672    /// #     fn columns() -> Vec<&'static str> { vec!["id"] }
673    /// #     fn fillable() -> Vec<&'static str> { vec![] }
674    /// #     fn guarded() -> Vec<&'static str> { vec!["id"] }
675    /// #     fn hidden() -> Vec<&'static str> { vec![] }
676    /// #     fn relations() -> std::collections::HashMap<&'static str, sz_orm_core::Relation> { Default::default() }
677    /// #     fn fill(&mut self, _: std::collections::HashMap<String, Value>) {}
678    /// #     fn to_json(&self) -> serde_json::Value { serde_json::json!({}) }
679    /// # }
680    /// let dialect = get_dialect(DbType::MySQL).unwrap();
681    /// let builder = QueryBuilder::<User>::new(dialect)
682    ///     .keyset_before("id", Value::I64(100), 20);
683    /// let (sql, params) = builder.build_select_with_params();
684    /// // 方言会引用字段名(如 MySQL 的 `id`),去除引号后检查
685    /// let sql_clean = sql.replace('`', "").replace('"', "");
686    /// assert!(sql_clean.contains("id < ?"));
687    /// assert!(sql.to_uppercase().contains("ORDER BY"));
688    /// assert!(sql.to_uppercase().contains("DESC"));
689    /// assert!(sql.contains("LIMIT 20"));
690    /// assert_eq!(params, vec![Value::I64(100)]);
691    /// ```
692    pub fn keyset_before(
693        mut self,
694        field: impl Into<String>,
695        cursor_value: Value,
696        page_size: usize,
697    ) -> Self {
698        let field_str = field.into();
699        // 自动设置 ORDER BY DESC(若字段已存在则更新方向为 DESC,保证 keyset 语义一致)
700        if let Some(existing) = self.order_by.iter_mut().find(|o| o.field == field_str) {
701            existing.direction = OrderDirection::Desc;
702        } else {
703            self.order_by.push(OrderClause {
704                field: field_str.clone(),
705                direction: OrderDirection::Desc,
706            });
707        }
708        self.limit_value = Some(page_size);
709        self.offset_value = None;
710        self.keyset_cursor = Some(KeysetCursor {
711            field: field_str,
712            value: cursor_value,
713            direction: KeysetDirection::Before,
714        });
715        self
716    }
717
718    pub fn join_inner(
719        mut self,
720        table: impl Into<String>,
721        on_left: impl Into<String>,
722        on_right: impl Into<String>,
723    ) -> Self {
724        self.joins.push(JoinClause::Inner(
725            table.into(),
726            on_left.into(),
727            on_right.into(),
728        ));
729        self
730    }
731
732    pub fn join_left(
733        mut self,
734        table: impl Into<String>,
735        on_left: impl Into<String>,
736        on_right: impl Into<String>,
737    ) -> Self {
738        self.joins.push(JoinClause::Left(
739            table.into(),
740            on_left.into(),
741            on_right.into(),
742        ));
743        self
744    }
745
746    pub fn join_right(
747        mut self,
748        table: impl Into<String>,
749        on_left: impl Into<String>,
750        on_right: impl Into<String>,
751    ) -> Self {
752        self.joins.push(JoinClause::Right(
753            table.into(),
754            on_left.into(),
755            on_right.into(),
756        ));
757        self
758    }
759
760    /// 构建 SELECT SQL 语句
761    ///
762    /// L-5 修复:补充示例文档
763    ///
764    /// 根据 `table`、`select_columns`、`where_conditions`、`joins`、`order_by`、
765    /// `group_by`、`having`、`limit`、`offset` 等条件拼装最终 SQL。
766    /// 若未通过 `table()` 指定表名,则使用 `M::table_name()`。
767    ///
768    /// # 示例
769    ///
770    /// ```ignore
771    /// use sz_orm_core::query::QueryBuilder;
772    /// use sz_orm_core::dialect::MySqlDialect;
773    /// use sz_orm_core::model::Model;
774    ///
775    /// #[derive(Default)]
776    /// struct User;
777    /// impl Model for User {
778    ///     type PrimaryKey = i64;
779    ///     fn table_name() -> &'static str { "users" }
780    ///     fn pk(&self) -> Self::PrimaryKey { 0 }
781    ///     fn set_pk(&mut self, _: Self::PrimaryKey) {}
782    /// }
783    ///
784    /// let sql = QueryBuilder::<User>::new(Box::new(MySqlDialect))
785    ///     .select(vec!["id", "name"])
786    ///     .where_cond("age > 18")
787    ///     .order_by("id DESC")
788    ///     .limit(10)
789    ///     .build_select();
790    /// // sql => "SELECT id, name FROM `users` WHERE age > 18 ORDER BY id DESC LIMIT 10"
791    /// ```
792    #[tracing::instrument(skip(self), fields(op = "select"))]
793    pub fn build_select(&self) -> String {
794        let table = self
795            .table
796            .clone()
797            .unwrap_or_else(|| M::table_name().to_string());
798
799        let columns = if self.select_columns.is_empty() {
800            "*".to_string()
801        } else {
802            self.select_columns.join(", ")
803        };
804
805        let mut sql = format!("SELECT {} FROM {}", columns, self.dialect.quote(&table));
806
807        for join in &self.joins {
808            match join {
809                JoinClause::Inner(t, l, r) => {
810                    sql.push_str(&format!(
811                        " INNER JOIN {} ON {} = {}",
812                        self.dialect.quote(t),
813                        self.dialect.quote(l),
814                        self.dialect.quote(r)
815                    ));
816                }
817                JoinClause::Left(t, l, r) => {
818                    sql.push_str(&format!(
819                        " LEFT JOIN {} ON {} = {}",
820                        self.dialect.quote(t),
821                        self.dialect.quote(l),
822                        self.dialect.quote(r)
823                    ));
824                }
825                JoinClause::Right(t, l, r) => {
826                    sql.push_str(&format!(
827                        " RIGHT JOIN {} ON {} = {}",
828                        self.dialect.quote(t),
829                        self.dialect.quote(l),
830                        self.dialect.quote(r)
831                    ));
832                }
833                JoinClause::Cross(t, on) => {
834                    sql.push_str(&format!(
835                        " CROSS JOIN {} ON {}",
836                        self.dialect.quote(t),
837                        self.dialect.quote(on)
838                    ));
839                }
840            }
841        }
842
843        // P0-1:build_where_clause 内部已处理软删除条件,即使 where_conditions 为空也可能返回非空
844        let where_clause = self.build_where_clause();
845        if !where_clause.is_empty() {
846            sql.push_str(&where_clause);
847        }
848
849        if !self.group_by.is_empty() {
850            let cols: Vec<String> = self
851                .group_by
852                .iter()
853                .map(|c| self.dialect.quote(c))
854                .collect();
855            sql.push_str(" GROUP BY ");
856            sql.push_str(&cols.join(", "));
857        }
858
859        if !self.having_conditions.is_empty() {
860            sql.push_str(" HAVING ");
861            for (i, cond) in self.having_conditions.iter().enumerate() {
862                if i > 0 {
863                    sql.push_str(" AND ");
864                }
865                if let WhereCondition::And(c) = cond {
866                    sql.push_str(c);
867                }
868            }
869        }
870
871        if !self.order_by.is_empty() {
872            let order_cols: Vec<String> = self
873                .order_by
874                .iter()
875                .map(|o| {
876                    let dir = match o.direction {
877                        OrderDirection::Asc => " ASC",
878                        OrderDirection::Desc => " DESC",
879                    };
880                    format!("{}{}", self.dialect.quote(&o.field), dir)
881                })
882                .collect();
883            sql.push_str(" ORDER BY ");
884            sql.push_str(&order_cols.join(", "));
885        }
886
887        if let Some(limit) = self.limit_value {
888            sql.push_str(&format!(" LIMIT {}", limit));
889        }
890
891        if let Some(offset) = self.offset_value {
892            sql.push_str(&format!(" OFFSET {}", offset));
893        }
894
895        sql
896    }
897
898    /// 构建 WHERE 子句(处理所有条件类型:And/Or/In/NotIn/Between/Null/Eq/Ne/Gt/Lt/Like 等)
899    ///
900    /// P0-1:自动追加软删除过滤条件(`AND {soft_delete_field} IS NULL`)
901    ///
902    /// 返回空字符串表示无 WHERE 子句
903    fn build_where_clause(&self) -> String {
904        self.build_where_clause_with_options(true)
905    }
906
907    /// 构建 WHERE 子句(可控制是否追加软删除过滤)。
908    ///
909    /// `include_soft_delete = true`:追加 `AND {soft_delete_field} IS NULL`(默认行为)
910    /// `include_soft_delete = false`:不追加软删除过滤(用于 `build_force_delete`)
911    ///
912    /// P0-3:租户条件总是追加(若启用),不受 `include_soft_delete` 控制。
913    /// 物理删除也应受租户隔离约束,跨租户操作需显式 `without_tenant()`。
914    fn build_where_clause_with_options(&self, include_soft_delete: bool) -> String {
915        // P0-1:构造软删除条件(若有且启用)
916        let soft_delete_cond = if include_soft_delete {
917            self.build_soft_delete_condition()
918        } else {
919            None
920        };
921
922        // P0-3:构造租户条件(若有且启用)— 无参数版本内嵌转义值
923        let tenant_cond = self.build_tenant_condition().map(|(sql, value)| {
924            // sql 形如 "`tenant_id` = ?",将 ? 替换为内嵌值
925            sql.replacen('?', &value.to_param_with_dialect(&*self.dialect), 1)
926        });
927
928        // 无用户条件且无软删除条件且无租户条件且无 keyset 游标 → 空 WHERE
929        if self.where_conditions.is_empty()
930            && soft_delete_cond.is_none()
931            && tenant_cond.is_none()
932            && self.keyset_cursor.is_none()
933        {
934            return String::new();
935        }
936
937        // 将每个条件转换为字符串,OR 条件标记前缀
938        let mut conditions: Vec<String> = self
939            .where_conditions
940            .iter()
941            .map(|cond| match cond {
942                WhereCondition::And(c) => c.clone(),
943                WhereCondition::Or(c) => format!("OR {}", c),
944                // P0-2:参数化条件在无参数版本中直接 inline 值(用于 build_select 等无参数绑定场景)
945                WhereCondition::Eq(f, v) => format!(
946                    "{} = {}",
947                    self.dialect.quote(f),
948                    v.to_param_with_dialect(&*self.dialect)
949                ),
950                WhereCondition::Ne(f, v) => format!(
951                    "{} != {}",
952                    self.dialect.quote(f),
953                    v.to_param_with_dialect(&*self.dialect)
954                ),
955                WhereCondition::Gt(f, v) => format!(
956                    "{} > {}",
957                    self.dialect.quote(f),
958                    v.to_param_with_dialect(&*self.dialect)
959                ),
960                WhereCondition::Ge(f, v) => format!(
961                    "{} >= {}",
962                    self.dialect.quote(f),
963                    v.to_param_with_dialect(&*self.dialect)
964                ),
965                WhereCondition::Lt(f, v) => format!(
966                    "{} < {}",
967                    self.dialect.quote(f),
968                    v.to_param_with_dialect(&*self.dialect)
969                ),
970                WhereCondition::Le(f, v) => format!(
971                    "{} <= {}",
972                    self.dialect.quote(f),
973                    v.to_param_with_dialect(&*self.dialect)
974                ),
975                WhereCondition::Like(f, v) => format!(
976                    "{} LIKE {}",
977                    self.dialect.quote(f),
978                    v.to_param_with_dialect(&*self.dialect)
979                ),
980                WhereCondition::OrEq(f, v) => format!(
981                    "OR {} = {}",
982                    self.dialect.quote(f),
983                    v.to_param_with_dialect(&*self.dialect)
984                ),
985                WhereCondition::OrNe(f, v) => format!(
986                    "OR {} != {}",
987                    self.dialect.quote(f),
988                    v.to_param_with_dialect(&*self.dialect)
989                ),
990                WhereCondition::OrGt(f, v) => format!(
991                    "OR {} > {}",
992                    self.dialect.quote(f),
993                    v.to_param_with_dialect(&*self.dialect)
994                ),
995                WhereCondition::OrGe(f, v) => format!(
996                    "OR {} >= {}",
997                    self.dialect.quote(f),
998                    v.to_param_with_dialect(&*self.dialect)
999                ),
1000                WhereCondition::OrLt(f, v) => format!(
1001                    "OR {} < {}",
1002                    self.dialect.quote(f),
1003                    v.to_param_with_dialect(&*self.dialect)
1004                ),
1005                WhereCondition::OrLe(f, v) => format!(
1006                    "OR {} <= {}",
1007                    self.dialect.quote(f),
1008                    v.to_param_with_dialect(&*self.dialect)
1009                ),
1010                WhereCondition::OrLike(f, v) => format!(
1011                    "OR {} LIKE {}",
1012                    self.dialect.quote(f),
1013                    v.to_param_with_dialect(&*self.dialect)
1014                ),
1015                WhereCondition::In(f, vals) => {
1016                    // v0.2.2 修复 H-1:使用方言感知的转义
1017                    let vals_str: Vec<String> = vals
1018                        .iter()
1019                        .map(|v| v.to_param_with_dialect(&*self.dialect).to_string())
1020                        .collect();
1021                    format!("{} IN ({})", self.dialect.quote(f), vals_str.join(", "))
1022                }
1023                WhereCondition::NotIn(f, vals) => {
1024                    let vals_str: Vec<String> = vals
1025                        .iter()
1026                        .map(|v| v.to_param_with_dialect(&*self.dialect).to_string())
1027                        .collect();
1028                    format!("{} NOT IN ({})", self.dialect.quote(f), vals_str.join(", "))
1029                }
1030                WhereCondition::Between(f, start, end) => {
1031                    format!(
1032                        "{} BETWEEN {} AND {}",
1033                        self.dialect.quote(f),
1034                        start.to_param_with_dialect(&*self.dialect),
1035                        end.to_param_with_dialect(&*self.dialect)
1036                    )
1037                }
1038                WhereCondition::NotBetween(f, start, end) => {
1039                    format!(
1040                        "{} NOT BETWEEN {} AND {}",
1041                        self.dialect.quote(f),
1042                        start.to_param_with_dialect(&*self.dialect),
1043                        end.to_param_with_dialect(&*self.dialect)
1044                    )
1045                }
1046                WhereCondition::Null(f) => format!("{} IS NULL", self.dialect.quote(f)),
1047                WhereCondition::NotNull(f) => format!("{} IS NOT NULL", self.dialect.quote(f)),
1048                WhereCondition::Exists(s) => format!("EXISTS ({})", s),
1049                WhereCondition::NotExists(s) => format!("NOT EXISTS ({})", s),
1050            })
1051            .collect();
1052
1053        // P0-1:追加软删除条件(作为最后一个 AND 条件)
1054        if let Some(sd_cond) = soft_delete_cond {
1055            conditions.push(sd_cond);
1056        }
1057
1058        // P0-3:追加租户条件(在软删除之后,作为 AND 条件)
1059        if let Some(t_cond) = tenant_cond {
1060            conditions.push(t_cond);
1061        }
1062
1063        // P2-5:追加 keyset 游标条件(在租户条件之后,内嵌转义值)
1064        if let Some(ref cursor) = self.keyset_cursor {
1065            let op = match cursor.direction {
1066                KeysetDirection::After => ">",
1067                KeysetDirection::Before => "<",
1068            };
1069            conditions.push(format!(
1070                "{} {} {}",
1071                self.dialect.quote(&cursor.field),
1072                op,
1073                cursor.value.to_param_with_dialect(&*self.dialect)
1074            ));
1075        }
1076
1077        if conditions.is_empty() {
1078            return String::new();
1079        }
1080
1081        // OR 分组逻辑:将相邻的 OR 条件组合成 (cond1 OR cond2) 形式
1082        // 边界处理:如果第一个条件就是 OR(不合理但需防御),当作 AND 处理
1083        let mut groups: Vec<Vec<String>> = Vec::new();
1084        let mut current_group: Vec<String> = Vec::new();
1085        for cond in conditions.iter() {
1086            if let Some(stripped) = cond.strip_prefix("OR ") {
1087                // OR 条件:无论是否首个,都把 OR 前缀去掉当作普通条件加入当前组
1088                current_group.push(stripped.to_string());
1089            } else {
1090                // AND 条件:如果当前组非空,先保存
1091                if !current_group.is_empty() {
1092                    groups.push(std::mem::take(&mut current_group));
1093                }
1094                current_group.push(cond.clone());
1095            }
1096        }
1097        if !current_group.is_empty() {
1098            groups.push(current_group);
1099        }
1100
1101        let group_strs: Vec<String> = groups
1102            .iter()
1103            .map(|g| {
1104                if g.len() == 1 {
1105                    g[0].clone()
1106                } else {
1107                    format!("({})", g.join(" OR "))
1108                }
1109            })
1110            .collect();
1111
1112        format!(" WHERE {}", group_strs.join(" AND "))
1113    }
1114
1115    #[tracing::instrument(skip(self, data), fields(op = "insert"))]
1116    pub fn build_insert(&self, data: &std::collections::HashMap<String, Value>) -> String {
1117        let table = self
1118            .table
1119            .clone()
1120            .unwrap_or_else(|| M::table_name().to_string());
1121
1122        if data.is_empty() {
1123            return String::new();
1124        }
1125
1126        let columns: Vec<String> = data.keys().map(|k| self.dialect.quote(k)).collect();
1127        // v0.2.2 修复 H-1:使用方言感知的转义
1128        let values: Vec<String> = data
1129            .values()
1130            .map(|v| v.to_param_with_dialect(&*self.dialect).to_string())
1131            .collect();
1132
1133        format!(
1134            "INSERT INTO {} ({}) VALUES ({})",
1135            self.dialect.quote(&table),
1136            columns.join(", "),
1137            values.join(", ")
1138        )
1139    }
1140
1141    #[tracing::instrument(skip(self, data), fields(op = "update"))]
1142    pub fn build_update(&self, data: &std::collections::HashMap<String, Value>) -> String {
1143        let table = self
1144            .table
1145            .clone()
1146            .unwrap_or_else(|| M::table_name().to_string());
1147
1148        if data.is_empty() {
1149            return String::new();
1150        }
1151
1152        let set_clauses: Vec<String> = data
1153            .iter()
1154            .map(|(k, v)| {
1155                format!(
1156                    "{} = {}",
1157                    self.dialect.quote(k),
1158                    v.to_param_with_dialect(&*self.dialect)
1159                )
1160            })
1161            .collect();
1162
1163        let mut sql = format!(
1164            "UPDATE {} SET {}",
1165            self.dialect.quote(&table),
1166            set_clauses.join(", ")
1167        );
1168
1169        sql.push_str(&self.build_where_clause());
1170        sql
1171    }
1172
1173    /// 构建 DELETE SQL 语句。
1174    ///
1175    /// **P0-1 软删除集成(v1.3.0+)**:当 `M: Model` 实现了 `soft_delete_field()`
1176    /// 返回 `Some(field)` 且未调用 `without_soft_delete()` 时,本方法自动生成
1177    /// `UPDATE {table} SET {field} = NOW() WHERE ...` 而非 `DELETE FROM ...`。
1178    ///
1179    /// 这与 SeaORM 的 `ActiveModelBehavior::after_delete` + `ActiveValue::Set`
1180    /// 行为对齐:删除操作实际是软删除 UPDATE。
1181    ///
1182    /// 若需物理删除,请使用 [`build_force_delete`](Self::build_force_delete)。
1183    #[tracing::instrument(skip(self), fields(op = "delete"))]
1184    pub fn build_delete(&self) -> String {
1185        let table = self
1186            .table
1187            .clone()
1188            .unwrap_or_else(|| M::table_name().to_string());
1189
1190        // P0-1:软删除启用时转为 UPDATE SET {field} = NOW()
1191        if let Some(field) = self.soft_delete_field() {
1192            let where_clause = self.build_where_clause();
1193            return format!(
1194                "UPDATE {} SET {} = NOW(){}",
1195                self.dialect.quote(&table),
1196                self.dialect.quote(field),
1197                where_clause
1198            );
1199        }
1200
1201        let mut sql = format!("DELETE FROM {}", self.dialect.quote(&table));
1202        sql.push_str(&self.build_where_clause());
1203        sql
1204    }
1205
1206    /// 构建物理 DELETE SQL 语句(绕过软删除)。
1207    ///
1208    /// 即使 Model 实现了 `soft_delete_field()`,也生成 `DELETE FROM ...`,
1209    /// 且**不追加** `WHERE deleted_at IS NULL` 过滤(保留用户指定的 WHERE 条件)。
1210    /// 用于管理员强制清除场景。
1211    ///
1212    /// # 安全警告
1213    ///
1214    /// 物理删除不可恢复,请谨慎使用。
1215    pub fn build_force_delete(&self) -> String {
1216        let table = self
1217            .table
1218            .clone()
1219            .unwrap_or_else(|| M::table_name().to_string());
1220
1221        let mut sql = format!("DELETE FROM {}", self.dialect.quote(&table));
1222        // P0-1:物理删除不追加软删除过滤(include_soft_delete = false)
1223        sql.push_str(&self.build_where_clause_with_options(false));
1224        sql
1225    }
1226
1227    // ===================== 参数绑定版本(v1.1.0 新增) =====================
1228
1229    /// 构建 WHERE 子句(参数绑定版本)。
1230    ///
1231    /// P0-1:自动追加软删除过滤条件(`AND {soft_delete_field} IS NULL`,无参数)
1232    ///
1233    /// 将 `In`/`NotIn`/`Between`/`NotBetween`/`Eq`/`Ne`/`Gt`/`Ge`/`Lt`/`Le`/`Like`
1234    /// 条件中的值替换为 `?` 占位符,值收集到 `params` 向量中。
1235    /// `And`/`Or`/`Exists` 等原始字符串条件不提取参数(调用方负责安全)。
1236    fn build_where_clause_with_params(&self) -> (String, Vec<Value>) {
1237        // 默认包含软删除条件
1238        self.build_where_clause_with_params_options(true)
1239    }
1240
1241    /// 构建参数化 WHERE 子句(可控是否包含软删除条件)
1242    ///
1243    /// # 参数
1244    ///
1245    /// - `include_soft_delete`:true 时追加软删除条件;false 时跳过(用于物理删除等场景)
1246    ///
1247    /// P0-3:租户条件总是追加(若启用),不受 `include_soft_delete` 控制。
1248    /// 租户值通过 `?` 占位符绑定,加入 `params` 列表末尾。
1249    fn build_where_clause_with_params_options(
1250        &self,
1251        include_soft_delete: bool,
1252    ) -> (String, Vec<Value>) {
1253        // P0-1:构造软删除条件(若有且启用,无参数)
1254        let soft_delete_cond = if include_soft_delete {
1255            self.build_soft_delete_condition()
1256        } else {
1257            None
1258        };
1259
1260        // P0-3:构造租户条件(若有且启用)— 参数化版本保留 (sql, value)
1261        let tenant_cond = self.build_tenant_condition();
1262
1263        // 无用户条件且无软删除条件且无租户条件且无 keyset 游标 → 空 WHERE
1264        if self.where_conditions.is_empty()
1265            && soft_delete_cond.is_none()
1266            && tenant_cond.is_none()
1267            && self.keyset_cursor.is_none()
1268        {
1269            return (String::new(), Vec::new());
1270        }
1271
1272        let mut params = Vec::new();
1273
1274        let mut conditions: Vec<String> = self
1275            .where_conditions
1276            .iter()
1277            .map(|cond| match cond {
1278                WhereCondition::And(c) => c.clone(),
1279                WhereCondition::Or(c) => format!("OR {}", c),
1280                // P0-2:参数化条件使用 `?` 占位符
1281                WhereCondition::Eq(f, v) => {
1282                    params.push(v.clone());
1283                    format!("{} = ?", self.dialect.quote(f))
1284                }
1285                WhereCondition::Ne(f, v) => {
1286                    params.push(v.clone());
1287                    format!("{} != ?", self.dialect.quote(f))
1288                }
1289                WhereCondition::Gt(f, v) => {
1290                    params.push(v.clone());
1291                    format!("{} > ?", self.dialect.quote(f))
1292                }
1293                WhereCondition::Ge(f, v) => {
1294                    params.push(v.clone());
1295                    format!("{} >= ?", self.dialect.quote(f))
1296                }
1297                WhereCondition::Lt(f, v) => {
1298                    params.push(v.clone());
1299                    format!("{} < ?", self.dialect.quote(f))
1300                }
1301                WhereCondition::Le(f, v) => {
1302                    params.push(v.clone());
1303                    format!("{} <= ?", self.dialect.quote(f))
1304                }
1305                WhereCondition::Like(f, v) => {
1306                    params.push(v.clone());
1307                    format!("{} LIKE ?", self.dialect.quote(f))
1308                }
1309                WhereCondition::OrEq(f, v) => {
1310                    params.push(v.clone());
1311                    format!("OR {} = ?", self.dialect.quote(f))
1312                }
1313                WhereCondition::OrNe(f, v) => {
1314                    params.push(v.clone());
1315                    format!("OR {} != ?", self.dialect.quote(f))
1316                }
1317                WhereCondition::OrGt(f, v) => {
1318                    params.push(v.clone());
1319                    format!("OR {} > ?", self.dialect.quote(f))
1320                }
1321                WhereCondition::OrGe(f, v) => {
1322                    params.push(v.clone());
1323                    format!("OR {} >= ?", self.dialect.quote(f))
1324                }
1325                WhereCondition::OrLt(f, v) => {
1326                    params.push(v.clone());
1327                    format!("OR {} < ?", self.dialect.quote(f))
1328                }
1329                WhereCondition::OrLe(f, v) => {
1330                    params.push(v.clone());
1331                    format!("OR {} <= ?", self.dialect.quote(f))
1332                }
1333                WhereCondition::OrLike(f, v) => {
1334                    params.push(v.clone());
1335                    format!("OR {} LIKE ?", self.dialect.quote(f))
1336                }
1337                WhereCondition::In(f, vals) => {
1338                    let placeholders: Vec<&str> = vals.iter().map(|_| "?").collect();
1339                    params.extend(vals.iter().cloned());
1340                    format!("{} IN ({})", self.dialect.quote(f), placeholders.join(", "))
1341                }
1342                WhereCondition::NotIn(f, vals) => {
1343                    let placeholders: Vec<&str> = vals.iter().map(|_| "?").collect();
1344                    params.extend(vals.iter().cloned());
1345                    format!(
1346                        "{} NOT IN ({})",
1347                        self.dialect.quote(f),
1348                        placeholders.join(", ")
1349                    )
1350                }
1351                WhereCondition::Between(f, start, end) => {
1352                    params.push(start.clone());
1353                    params.push(end.clone());
1354                    format!("{} BETWEEN ? AND ?", self.dialect.quote(f))
1355                }
1356                WhereCondition::NotBetween(f, start, end) => {
1357                    params.push(start.clone());
1358                    params.push(end.clone());
1359                    format!("{} NOT BETWEEN ? AND ?", self.dialect.quote(f))
1360                }
1361                WhereCondition::Null(f) => format!("{} IS NULL", self.dialect.quote(f)),
1362                WhereCondition::NotNull(f) => format!("{} IS NOT NULL", self.dialect.quote(f)),
1363                WhereCondition::Exists(s) => format!("EXISTS ({})", s),
1364                WhereCondition::NotExists(s) => format!("NOT EXISTS ({})", s),
1365            })
1366            .collect();
1367
1368        // P0-1:追加软删除条件(作为最后一个 AND 条件,无参数)
1369        if let Some(sd_cond) = soft_delete_cond {
1370            conditions.push(sd_cond);
1371        }
1372
1373        // P0-3:追加租户条件(在软删除之后,参数化绑定)
1374        if let Some((t_sql, t_value)) = tenant_cond {
1375            conditions.push(t_sql);
1376            params.push(t_value);
1377        }
1378
1379        // P2-5:追加 keyset 游标条件(在租户条件之后,参数化绑定)
1380        if let Some(ref cursor) = self.keyset_cursor {
1381            let op = match cursor.direction {
1382                KeysetDirection::After => ">",
1383                KeysetDirection::Before => "<",
1384            };
1385            conditions.push(format!("{} {} ?", self.dialect.quote(&cursor.field), op));
1386            params.push(cursor.value.clone());
1387        }
1388
1389        if conditions.is_empty() {
1390            return (String::new(), params);
1391        }
1392
1393        // OR 分组逻辑:与 build_where_clause 相同
1394        let mut groups: Vec<Vec<String>> = Vec::new();
1395        let mut current_group: Vec<String> = Vec::new();
1396        for cond in conditions.iter() {
1397            if let Some(stripped) = cond.strip_prefix("OR ") {
1398                current_group.push(stripped.to_string());
1399            } else {
1400                if !current_group.is_empty() {
1401                    groups.push(std::mem::take(&mut current_group));
1402                }
1403                current_group.push(cond.clone());
1404            }
1405        }
1406        if !current_group.is_empty() {
1407            groups.push(current_group);
1408        }
1409
1410        let group_strs: Vec<String> = groups
1411            .iter()
1412            .map(|g| {
1413                if g.len() == 1 {
1414                    g[0].clone()
1415                } else {
1416                    format!("({})", g.join(" OR "))
1417                }
1418            })
1419            .collect();
1420
1421        (format!(" WHERE {}", group_strs.join(" AND ")), params)
1422    }
1423
1424    /// 构建 SELECT SQL(参数绑定版本)。
1425    ///
1426    /// WHERE 子句中的值使用 `?` 占位符,值通过 `params` 返回。
1427    /// 适用于 `Connection::query_with_params()`。
1428    pub fn build_select_with_params(&self) -> (String, Vec<Value>) {
1429        let table = self
1430            .table
1431            .clone()
1432            .unwrap_or_else(|| M::table_name().to_string());
1433        let columns = if self.select_columns.is_empty() {
1434            "*".to_string()
1435        } else {
1436            self.select_columns.join(", ")
1437        };
1438
1439        let mut sql = format!("SELECT {} FROM {}", columns, self.dialect.quote(&table));
1440
1441        for join in &self.joins {
1442            match join {
1443                JoinClause::Inner(t, l, r) => {
1444                    sql.push_str(&format!(
1445                        " INNER JOIN {} ON {} = {}",
1446                        self.dialect.quote(t),
1447                        self.dialect.quote(l),
1448                        self.dialect.quote(r)
1449                    ));
1450                }
1451                JoinClause::Left(t, l, r) => {
1452                    sql.push_str(&format!(
1453                        " LEFT JOIN {} ON {} = {}",
1454                        self.dialect.quote(t),
1455                        self.dialect.quote(l),
1456                        self.dialect.quote(r)
1457                    ));
1458                }
1459                JoinClause::Right(t, l, r) => {
1460                    sql.push_str(&format!(
1461                        " RIGHT JOIN {} ON {} = {}",
1462                        self.dialect.quote(t),
1463                        self.dialect.quote(l),
1464                        self.dialect.quote(r)
1465                    ));
1466                }
1467                JoinClause::Cross(t, on) => {
1468                    sql.push_str(&format!(
1469                        " CROSS JOIN {} ON {}",
1470                        self.dialect.quote(t),
1471                        self.dialect.quote(on)
1472                    ));
1473                }
1474            }
1475        }
1476
1477        let mut params = Vec::new();
1478        // P0-1:build_where_clause_with_params 内部已处理软删除条件
1479        let (where_clause, where_params) = self.build_where_clause_with_params();
1480        if !where_clause.is_empty() {
1481            sql.push_str(&where_clause);
1482            params = where_params;
1483        }
1484
1485        if !self.group_by.is_empty() {
1486            let cols: Vec<String> = self
1487                .group_by
1488                .iter()
1489                .map(|c| self.dialect.quote(c))
1490                .collect();
1491            sql.push_str(" GROUP BY ");
1492            sql.push_str(&cols.join(", "));
1493        }
1494
1495        if !self.having_conditions.is_empty() {
1496            sql.push_str(" HAVING ");
1497            for (i, cond) in self.having_conditions.iter().enumerate() {
1498                if i > 0 {
1499                    sql.push_str(" AND ");
1500                }
1501                if let WhereCondition::And(c) = cond {
1502                    sql.push_str(c);
1503                }
1504            }
1505        }
1506
1507        if !self.order_by.is_empty() {
1508            let order_cols: Vec<String> = self
1509                .order_by
1510                .iter()
1511                .map(|o| {
1512                    let dir = match o.direction {
1513                        OrderDirection::Asc => " ASC",
1514                        OrderDirection::Desc => " DESC",
1515                    };
1516                    format!("{}{}", self.dialect.quote(&o.field), dir)
1517                })
1518                .collect();
1519            sql.push_str(" ORDER BY ");
1520            sql.push_str(&order_cols.join(", "));
1521        }
1522
1523        if let Some(limit) = self.limit_value {
1524            sql.push_str(&format!(" LIMIT {}", limit));
1525        }
1526        if let Some(offset) = self.offset_value {
1527            sql.push_str(&format!(" OFFSET {}", offset));
1528        }
1529
1530        (sql, params)
1531    }
1532
1533    /// 构建 INSERT SQL(参数绑定版本)。
1534    pub fn build_insert_with_params(
1535        &self,
1536        data: &std::collections::HashMap<String, Value>,
1537    ) -> (String, Vec<Value>) {
1538        let table = self
1539            .table
1540            .clone()
1541            .unwrap_or_else(|| M::table_name().to_string());
1542        if data.is_empty() {
1543            return (String::new(), Vec::new());
1544        }
1545
1546        let mut columns = Vec::with_capacity(data.len());
1547        let mut params = Vec::with_capacity(data.len());
1548        let placeholders: Vec<&str> = data.iter().map(|_| "?").collect();
1549        for (k, v) in data.iter() {
1550            columns.push(self.dialect.quote(k));
1551            params.push(v.clone());
1552        }
1553
1554        let sql = format!(
1555            "INSERT INTO {} ({}) VALUES ({})",
1556            self.dialect.quote(&table),
1557            columns.join(", "),
1558            placeholders.join(", ")
1559        );
1560        (sql, params)
1561    }
1562
1563    /// P2-6:构建批量 INSERT SQL(参数绑定版本)。
1564    ///
1565    /// 生成 `INSERT INTO t (c1, c2) VALUES (?, ?), (?, ?), ...` 形式的多行插入 SQL。
1566    /// 所有行的列必须一致(取第一行的列顺序);空行列表返回空 SQL。
1567    ///
1568    /// **L3 实现深度**:使用参数化占位符 `?`,所有值通过 `params` 绑定,杜绝 SQL 注入。
1569    pub fn build_batch_insert_with_params(
1570        &self,
1571        rows: &[std::collections::HashMap<String, Value>],
1572    ) -> (String, Vec<Value>) {
1573        let table = self
1574            .table
1575            .clone()
1576            .unwrap_or_else(|| M::table_name().to_string());
1577        if rows.is_empty() {
1578            return (String::new(), Vec::new());
1579        }
1580
1581        // 取第一行的列作为列顺序(所有行必须一致)
1582        let first_row = &rows[0];
1583        let columns: Vec<String> = first_row.keys().cloned().collect();
1584        let quoted_columns: Vec<String> = columns.iter().map(|c| self.dialect.quote(c)).collect();
1585
1586        let mut params = Vec::with_capacity(rows.len() * columns.len());
1587        let mut value_groups: Vec<String> = Vec::with_capacity(rows.len());
1588        for row in rows {
1589            let placeholders: Vec<String> = columns
1590                .iter()
1591                .map(|col| {
1592                    match row.get(col) {
1593                        Some(v) => {
1594                            params.push(v.clone());
1595                            "?".to_string()
1596                        }
1597                        None => "NULL".to_string(),
1598                    }
1599                })
1600                .collect();
1601            value_groups.push(format!("({})", placeholders.join(", ")));
1602        }
1603
1604        let sql = format!(
1605            "INSERT INTO {} ({}) VALUES {}",
1606            self.dialect.quote(&table),
1607            quoted_columns.join(", "),
1608            value_groups.join(", ")
1609        );
1610        (sql, params)
1611    }
1612
1613    /// P2-6:构建批量 Upsert SQL(参数绑定版本)。
1614    ///
1615    /// 在 `build_batch_insert_with_params` 基础上追加冲突处理子句:
1616    /// - MySQL: `ON DUPLICATE KEY UPDATE col=VALUES(col), ...`
1617    /// - PostgreSQL/SQLite: `ON CONFLICT (conflict_cols) DO UPDATE SET col=EXCLUDED.col, ...`
1618    /// - Oracle/SQL Server/ClickHouse/Db2: 返回 `Err(DbError::InvalidInput)`(不支持)
1619    ///
1620    /// # 参数
1621    /// - `rows`: 批量数据行(所有行的列必须一致)
1622    /// - `conflict_columns`: 冲突检测列(主键/唯一键);MySQL 自动检测可传空
1623    /// - `update_columns`: 冲突时更新的列;空切片表示更新所有非冲突列
1624    ///
1625    /// # 返回
1626    /// - `Ok((sql, params))`: 生成的 SQL 和参数列表
1627    /// - `Err(DbError::InvalidInput)`: 方言不支持 upsert 或 rows 为空
1628    ///
1629    /// **L3 实现深度**:
1630    /// 1. SQL 下推:冲突处理由数据库执行,非内存判断
1631    /// 2. 参数化:所有值通过 `?` 占位符绑定,不拼接用户值
1632    /// 3. 实际执行:生成标准 INSERT...ON CONFLICT/ON DUPLICATE KEY SQL
1633    pub fn build_batch_upsert_with_params(
1634        &self,
1635        rows: &[std::collections::HashMap<String, Value>],
1636        conflict_columns: &[&str],
1637        update_columns: &[&str],
1638    ) -> Result<(String, Vec<Value>), crate::DbError> {
1639        if rows.is_empty() {
1640            return Err(crate::DbError::InvalidInput(
1641                "build_batch_upsert_with_params: rows cannot be empty".to_string(),
1642            ));
1643        }
1644
1645        // 构建批量 INSERT 部分
1646        let (insert_sql, params) = self.build_batch_insert_with_params(rows);
1647        if insert_sql.is_empty() {
1648            return Err(crate::DbError::InvalidInput(
1649                "build_batch_upsert_with_params: failed to build INSERT part".to_string(),
1650            ));
1651        }
1652
1653        // 取所有列名(原始未 quote)
1654        let all_columns: Vec<String> = rows[0].keys().cloned().collect();
1655
1656        // 调用方言生成冲突处理子句
1657        let conflict_clause = self
1658            .dialect
1659            .build_upsert_on_conflict(conflict_columns, update_columns, &all_columns)
1660            .ok_or_else(|| {
1661                crate::DbError::InvalidInput(format!(
1662                    "build_batch_upsert_with_params: dialect {:?} does not support upsert (ON CONFLICT / ON DUPLICATE KEY UPDATE). Consider using MERGE statement or individual upserts instead.",
1663                    self.dialect.db_type()
1664                ))
1665            })?;
1666
1667        let sql = format!("{} {}", insert_sql, conflict_clause);
1668        Ok((sql, params))
1669    }
1670
1671    /// 构建 UPDATE SQL(参数绑定版本)。
1672    /// 参数顺序:SET 参数在前,WHERE 参数在后。
1673    pub fn build_update_with_params(
1674        &self,
1675        data: &std::collections::HashMap<String, Value>,
1676    ) -> (String, Vec<Value>) {
1677        let table = self
1678            .table
1679            .clone()
1680            .unwrap_or_else(|| M::table_name().to_string());
1681        if data.is_empty() {
1682            return (String::new(), Vec::new());
1683        }
1684
1685        let mut set_clauses = Vec::with_capacity(data.len());
1686        let mut params = Vec::with_capacity(data.len());
1687        for (k, v) in data.iter() {
1688            set_clauses.push(format!("{} = ?", self.dialect.quote(k)));
1689            params.push(v.clone());
1690        }
1691
1692        let mut sql = format!(
1693            "UPDATE {} SET {}",
1694            self.dialect.quote(&table),
1695            set_clauses.join(", ")
1696        );
1697
1698        // P0-1:build_where_clause_with_params 内部已处理软删除条件
1699        let (where_clause, where_params) = self.build_where_clause_with_params();
1700        if !where_clause.is_empty() {
1701            sql.push_str(&where_clause);
1702            params.extend(where_params);
1703        }
1704
1705        (sql, params)
1706    }
1707
1708    /// 构建 DELETE SQL(参数绑定版本)。
1709    ///
1710    /// **P0-1 软删除集成(v1.3.0+)**:当 Model 启用软删除时,自动生成
1711    /// `UPDATE {table} SET {field} = NOW() WHERE ...` 而非 `DELETE FROM ...`。
1712    /// 参数列表为空(NOW() 由数据库填充)。
1713    pub fn build_delete_with_params(&self) -> (String, Vec<Value>) {
1714        let table = self
1715            .table
1716            .clone()
1717            .unwrap_or_else(|| M::table_name().to_string());
1718
1719        // P0-1:软删除启用时转为 UPDATE SET {field} = NOW()
1720        if let Some(field) = self.soft_delete_field() {
1721            let (where_clause, where_params) = self.build_where_clause_with_params();
1722            let sql = format!(
1723                "UPDATE {} SET {} = NOW(){}",
1724                self.dialect.quote(&table),
1725                self.dialect.quote(field),
1726                where_clause
1727            );
1728            return (sql, where_params);
1729        }
1730
1731        let mut sql = format!("DELETE FROM {}", self.dialect.quote(&table));
1732        let mut params = Vec::new();
1733
1734        let (where_clause, where_params) = self.build_where_clause_with_params();
1735        if !where_clause.is_empty() {
1736            sql.push_str(&where_clause);
1737            params = where_params;
1738        }
1739
1740        (sql, params)
1741    }
1742
1743    /// 构建物理 DELETE SQL(参数绑定版本,绕过软删除)。
1744    ///
1745    /// 即使 Model 启用软删除,也生成 `DELETE FROM ...`,且不追加软删除过滤。
1746    pub fn build_force_delete_with_params(&self) -> (String, Vec<Value>) {
1747        let table = self
1748            .table
1749            .clone()
1750            .unwrap_or_else(|| M::table_name().to_string());
1751
1752        let mut sql = format!("DELETE FROM {}", self.dialect.quote(&table));
1753        let mut params = Vec::new();
1754
1755        // P0-1:物理删除不追加软删除过滤,使用 build_where_clause_with_params_no_soft_delete
1756        let (where_clause, where_params) = self.build_where_clause_with_params_options(false);
1757        if !where_clause.is_empty() {
1758            sql.push_str(&where_clause);
1759            params = where_params;
1760        }
1761
1762        (sql, params)
1763    }
1764
1765    pub fn build_count(&self) -> String {
1766        let table = self
1767            .table
1768            .clone()
1769            .unwrap_or_else(|| M::table_name().to_string());
1770
1771        let mut sql = format!(
1772            "SELECT COUNT(*) as total FROM {}",
1773            self.dialect.quote(&table)
1774        );
1775        sql.push_str(&self.build_where_clause());
1776        sql
1777    }
1778
1779    pub fn build_exists(&self) -> String {
1780        let table = self
1781            .table
1782            .clone()
1783            .unwrap_or_else(|| M::table_name().to_string());
1784
1785        let mut sql = format!("SELECT 1 FROM {}", self.dialect.quote(&table));
1786        sql.push_str(&self.build_where_clause());
1787        sql.push_str(" LIMIT 1");
1788        format!("SELECT EXISTS({})", sql)
1789    }
1790
1791    pub fn build_max(&self, field: &str) -> String {
1792        let table = self
1793            .table
1794            .clone()
1795            .unwrap_or_else(|| M::table_name().to_string());
1796
1797        let mut sql = format!(
1798            "SELECT MAX({}) as max_val FROM {}",
1799            self.dialect.quote(field),
1800            self.dialect.quote(&table)
1801        );
1802        sql.push_str(&self.build_where_clause());
1803        sql
1804    }
1805
1806    pub fn build_min(&self, field: &str) -> String {
1807        let table = self
1808            .table
1809            .clone()
1810            .unwrap_or_else(|| M::table_name().to_string());
1811
1812        let mut sql = format!(
1813            "SELECT MIN({}) as min_val FROM {}",
1814            self.dialect.quote(field),
1815            self.dialect.quote(&table)
1816        );
1817        sql.push_str(&self.build_where_clause());
1818        sql
1819    }
1820
1821    pub fn build_sum(&self, field: &str) -> String {
1822        let table = self
1823            .table
1824            .clone()
1825            .unwrap_or_else(|| M::table_name().to_string());
1826
1827        let mut sql = format!(
1828            "SELECT SUM({}) as sum_val FROM {}",
1829            self.dialect.quote(field),
1830            self.dialect.quote(&table)
1831        );
1832        sql.push_str(&self.build_where_clause());
1833        sql
1834    }
1835
1836    pub fn build_avg(&self, field: &str) -> String {
1837        let table = self
1838            .table
1839            .clone()
1840            .unwrap_or_else(|| M::table_name().to_string());
1841
1842        let mut sql = format!(
1843            "SELECT AVG({}) as avg_val FROM {}",
1844            self.dialect.quote(field),
1845            self.dialect.quote(&table)
1846        );
1847        sql.push_str(&self.build_where_clause());
1848        sql
1849    }
1850
1851    /// 校验生成的 SELECT SQL 语句
1852    /// 检查 SQL 语法、JOIN 列名、表名合法性
1853    pub fn validate(&self) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
1854        let sql = self.build_select();
1855        let mut errors = Vec::new();
1856
1857        if let Err(e) = sz_orm_sql_validator::validate_select(&sql) {
1858            errors.push(e);
1859        }
1860
1861        // 校验 JOIN 子句产生的 SQL 是否合法
1862        if !self.joins.is_empty() {
1863            for join in &self.joins {
1864                match join {
1865                    JoinClause::Inner(_, left, right)
1866                    | JoinClause::Left(_, left, right)
1867                    | JoinClause::Right(_, left, right) => {
1868                        if let Err(e) = sz_orm_sql_validator::validate_column_name(left) {
1869                            errors.push(e);
1870                        }
1871                        if let Err(e) = sz_orm_sql_validator::validate_column_name(right) {
1872                            errors.push(e);
1873                        }
1874                    }
1875                    _ => {}
1876                }
1877            }
1878        }
1879
1880        // 校验表名合法性
1881        let table = self
1882            .table
1883            .clone()
1884            .unwrap_or_else(|| M::table_name().to_string());
1885        if let Err(e) = sz_orm_sql_validator::validate_table_name(&table) {
1886            errors.push(e);
1887        }
1888
1889        if errors.is_empty() {
1890            Ok(())
1891        } else {
1892            Err(errors)
1893        }
1894    }
1895
1896    /// 校验生成的 INSERT SQL 语句
1897    /// 含空数据检测(EmptyInsertData 错误)
1898    pub fn validate_insert(
1899        &self,
1900        data: &std::collections::HashMap<String, Value>,
1901    ) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
1902        let sql = self.build_insert(data);
1903        let mut errors = Vec::new();
1904
1905        if sql.is_empty() {
1906            errors.push(sz_orm_sql_validator::SqlValidationError::EmptyInsertData);
1907            return Err(errors);
1908        }
1909
1910        if let Err(e) = sz_orm_sql_validator::validate_insert(&sql) {
1911            errors.push(e);
1912        }
1913
1914        if errors.is_empty() {
1915            Ok(())
1916        } else {
1917            Err(errors)
1918        }
1919    }
1920
1921    /// 校验生成的 UPDATE SQL 语句
1922    /// 含空数据检测(EmptyUpdateData 错误)
1923    pub fn validate_update(
1924        &self,
1925        data: &std::collections::HashMap<String, Value>,
1926    ) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
1927        let sql = self.build_update(data);
1928        let mut errors = Vec::new();
1929
1930        if sql.is_empty() {
1931            errors.push(sz_orm_sql_validator::SqlValidationError::EmptyUpdateData);
1932            return Err(errors);
1933        }
1934
1935        if let Err(e) = sz_orm_sql_validator::validate_update(&sql) {
1936            errors.push(e);
1937        }
1938
1939        if errors.is_empty() {
1940            Ok(())
1941        } else {
1942            Err(errors)
1943        }
1944    }
1945
1946    /// 校验生成的 DELETE SQL 语句
1947    pub fn validate_delete(&self) -> Result<(), Vec<sz_orm_sql_validator::SqlValidationError>> {
1948        let sql = self.build_delete();
1949        let mut errors = Vec::new();
1950
1951        if let Err(e) = sz_orm_sql_validator::validate_delete(&sql) {
1952            errors.push(e);
1953        }
1954
1955        if errors.is_empty() {
1956            Ok(())
1957        } else {
1958            Err(errors)
1959        }
1960    }
1961}
1962
1963impl<M: Model> fmt::Debug for QueryBuilder<M> {
1964    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1965        f.debug_struct("QueryBuilder")
1966            .field("table", &self.table)
1967            .field("select_columns", &self.select_columns)
1968            .field("where_conditions", &self.where_conditions.len())
1969            .field("limit", &self.limit_value)
1970            .finish()
1971    }
1972}
1973
1974#[cfg(test)]
1975mod tests {
1976    use super::*;
1977    use crate::db_type::DbType;
1978    use crate::dialect::get_dialect;
1979
1980    struct TestModel;
1981    impl Model for TestModel {
1982        type PrimaryKey = i64;
1983
1984        fn table_name() -> &'static str {
1985            "test_models"
1986        }
1987
1988        fn pk(&self) -> Self::PrimaryKey {
1989            1
1990        }
1991
1992        fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
1993    }
1994
1995    #[test]
1996    fn test_query_builder_select() {
1997        let dialect = get_dialect(DbType::MySQL).unwrap();
1998        let builder = QueryBuilder::<TestModel>::new(dialect);
1999
2000        let sql = builder
2001            .table("users")
2002            .select(vec!["id", "name"])
2003            .build_select();
2004        assert!(sql.contains("SELECT id, name FROM"));
2005        assert!(sql.contains("`users`"));
2006    }
2007
2008    #[test]
2009    fn test_query_builder_where() {
2010        let dialect = get_dialect(DbType::MySQL).unwrap();
2011        let builder = QueryBuilder::<TestModel>::new(dialect);
2012
2013        let sql = builder
2014            .table("users")
2015            .where_cond("status = 'active'")
2016            .where_cond("age > 18")
2017            .build_select();
2018
2019        assert!(sql.contains("WHERE"));
2020        assert!(sql.contains("status = 'active'"));
2021        assert!(sql.contains("age > 18"));
2022    }
2023
2024    #[test]
2025    fn test_query_builder_order_by() {
2026        let dialect = get_dialect(DbType::MySQL).unwrap();
2027        let builder = QueryBuilder::<TestModel>::new(dialect);
2028
2029        let sql = builder
2030            .table("users")
2031            .order_by("created_at")
2032            .order_desc("id")
2033            .build_select();
2034
2035        assert!(sql.contains("ORDER BY"));
2036        assert!(sql.contains("`created_at` ASC"));
2037        assert!(sql.contains("`id` DESC"));
2038    }
2039
2040    #[test]
2041    fn test_query_builder_limit_offset() {
2042        let dialect = get_dialect(DbType::MySQL).unwrap();
2043        let builder = QueryBuilder::<TestModel>::new(dialect);
2044
2045        let sql = builder.table("users").limit(10).offset(20).build_select();
2046
2047        assert!(sql.contains("LIMIT 10"));
2048        assert!(sql.contains("OFFSET 20"));
2049    }
2050
2051    #[test]
2052    fn test_query_builder_page() {
2053        let dialect = get_dialect(DbType::MySQL).unwrap();
2054        let builder = QueryBuilder::<TestModel>::new(dialect);
2055
2056        let sql = builder.table("users").page(3, 20).build_select();
2057
2058        assert!(sql.contains("LIMIT 20"));
2059        assert!(sql.contains("OFFSET 40"));
2060    }
2061
2062    #[test]
2063    fn test_query_builder_insert() {
2064        let dialect = get_dialect(DbType::MySQL).unwrap();
2065        let builder = QueryBuilder::<TestModel>::new(dialect);
2066
2067        let mut data = std::collections::HashMap::new();
2068        data.insert("name".to_string(), Value::String("test".to_string()));
2069        data.insert("age".to_string(), Value::I64(25));
2070
2071        let sql = builder.table("users").build_insert(&data);
2072
2073        assert!(sql.contains("INSERT INTO"));
2074        assert!(sql.contains("`name`"));
2075        assert!(sql.contains("'test'"));
2076    }
2077
2078    #[test]
2079    fn test_query_builder_update() {
2080        let dialect = get_dialect(DbType::MySQL).unwrap();
2081        let builder = QueryBuilder::<TestModel>::new(dialect);
2082
2083        let mut data = std::collections::HashMap::new();
2084        data.insert("name".to_string(), Value::String("updated".to_string()));
2085
2086        let sql = builder
2087            .table("users")
2088            .where_cond("id = 1")
2089            .build_update(&data);
2090
2091        assert!(sql.contains("UPDATE"));
2092        assert!(sql.contains("`name` = 'updated'"));
2093        assert!(sql.contains("WHERE"));
2094    }
2095
2096    #[test]
2097    fn test_query_builder_delete() {
2098        let dialect = get_dialect(DbType::MySQL).unwrap();
2099        let builder = QueryBuilder::<TestModel>::new(dialect);
2100
2101        let sql = builder.table("users").where_cond("id = 1").build_delete();
2102
2103        assert!(sql.contains("DELETE FROM"));
2104        assert!(sql.contains("WHERE"));
2105    }
2106
2107    #[test]
2108    fn test_query_builder_count() {
2109        let dialect = get_dialect(DbType::MySQL).unwrap();
2110        let builder = QueryBuilder::<TestModel>::new(dialect);
2111
2112        let sql = builder.table("users").build_count();
2113
2114        assert!(sql.contains("SELECT COUNT(*)"));
2115        assert!(sql.contains("FROM"));
2116    }
2117
2118    #[test]
2119    fn test_query_builder_where_in() {
2120        let dialect = get_dialect(DbType::MySQL).unwrap();
2121        let builder = QueryBuilder::<TestModel>::new(dialect);
2122
2123        let sql = builder
2124            .table("users")
2125            .where_in("id", vec![Value::I64(1), Value::I64(2), Value::I64(3)])
2126            .build_select();
2127
2128        assert!(sql.contains("IN ("));
2129    }
2130
2131    #[test]
2132    fn test_query_builder_where_between() {
2133        let dialect = get_dialect(DbType::MySQL).unwrap();
2134        let builder = QueryBuilder::<TestModel>::new(dialect);
2135
2136        let sql = builder
2137            .table("users")
2138            .where_between("age", Value::I64(18), Value::I64(30))
2139            .build_select();
2140
2141        assert!(sql.contains("BETWEEN"));
2142    }
2143
2144    #[test]
2145    fn test_query_builder_where_null() {
2146        let dialect = get_dialect(DbType::MySQL).unwrap();
2147        let builder = QueryBuilder::<TestModel>::new(dialect);
2148
2149        let sql = builder
2150            .table("users")
2151            .where_null("deleted_at")
2152            .build_select();
2153
2154        assert!(sql.contains("IS NULL"));
2155    }
2156
2157    #[test]
2158    fn test_query_builder_join() {
2159        let dialect = get_dialect(DbType::MySQL).unwrap();
2160        let builder = QueryBuilder::<TestModel>::new(dialect);
2161
2162        let sql = builder
2163            .table("users")
2164            .join_inner("posts", "users.id", "posts.user_id")
2165            .build_select();
2166
2167        assert!(sql.contains("INNER JOIN"));
2168        assert!(sql.contains("`posts`"));
2169    }
2170
2171    #[test]
2172    fn test_query_builder_group_by() {
2173        let dialect = get_dialect(DbType::MySQL).unwrap();
2174        let builder = QueryBuilder::<TestModel>::new(dialect);
2175
2176        let sql = builder.table("users").group_by("status").build_select();
2177
2178        assert!(sql.contains("GROUP BY"));
2179        assert!(sql.contains("`status`"));
2180    }
2181
2182    #[test]
2183    fn test_query_builder_max() {
2184        let dialect = get_dialect(DbType::MySQL).unwrap();
2185        let builder = QueryBuilder::<TestModel>::new(dialect);
2186
2187        let sql = builder.table("users").build_max("score");
2188
2189        assert!(sql.contains("MAX("));
2190        assert!(sql.contains("`score`"));
2191    }
2192
2193    #[test]
2194    fn test_query_builder_min() {
2195        let dialect = get_dialect(DbType::MySQL).unwrap();
2196        let builder = QueryBuilder::<TestModel>::new(dialect);
2197
2198        let sql = builder.table("users").build_min("price");
2199
2200        assert!(sql.contains("MIN("));
2201        assert!(sql.contains("`price`"));
2202    }
2203
2204    #[test]
2205    fn test_query_builder_sum() {
2206        let dialect = get_dialect(DbType::MySQL).unwrap();
2207        let builder = QueryBuilder::<TestModel>::new(dialect);
2208
2209        let sql = builder.table("orders").build_sum("amount");
2210
2211        assert!(sql.contains("SUM("));
2212        assert!(sql.contains("`amount`"));
2213    }
2214
2215    #[test]
2216    fn test_query_builder_avg() {
2217        let dialect = get_dialect(DbType::MySQL).unwrap();
2218        let builder = QueryBuilder::<TestModel>::new(dialect);
2219
2220        let sql = builder.table("scores").build_avg("value");
2221
2222        assert!(sql.contains("AVG("));
2223        assert!(sql.contains("`value`"));
2224    }
2225
2226    #[test]
2227    fn test_validator_select() {
2228        let dialect = get_dialect(DbType::MySQL).unwrap();
2229        let builder = QueryBuilder::<TestModel>::new(dialect);
2230
2231        let result = builder.table("users").select(vec!["id", "name"]).validate();
2232        assert!(result.is_ok());
2233    }
2234
2235    #[test]
2236    fn test_validator_select_with_join() {
2237        let dialect = get_dialect(DbType::MySQL).unwrap();
2238        let builder = QueryBuilder::<TestModel>::new(dialect);
2239
2240        let result = builder
2241            .table("users")
2242            .join_inner("posts", "users.id", "posts.user_id")
2243            .validate();
2244        assert!(result.is_ok());
2245    }
2246
2247    #[test]
2248    fn test_validator_insert() {
2249        let dialect = get_dialect(DbType::MySQL).unwrap();
2250        let builder = QueryBuilder::<TestModel>::new(dialect);
2251
2252        let mut data = std::collections::HashMap::new();
2253        data.insert("name".to_string(), Value::String("test".to_string()));
2254
2255        let result = builder.table("users").validate_insert(&data);
2256        assert!(result.is_ok());
2257    }
2258
2259    #[test]
2260    fn test_validator_insert_empty_data() {
2261        let dialect = get_dialect(DbType::MySQL).unwrap();
2262        let builder = QueryBuilder::<TestModel>::new(dialect);
2263
2264        let data = std::collections::HashMap::new();
2265        let result = builder.table("users").validate_insert(&data);
2266        assert!(result.is_err());
2267    }
2268
2269    #[test]
2270    fn test_validator_update() {
2271        let dialect = get_dialect(DbType::MySQL).unwrap();
2272        let builder = QueryBuilder::<TestModel>::new(dialect);
2273
2274        let mut data = std::collections::HashMap::new();
2275        data.insert("name".to_string(), Value::String("updated".to_string()));
2276
2277        let result = builder.table("users").validate_update(&data);
2278        assert!(result.is_ok());
2279    }
2280
2281    #[test]
2282    fn test_validator_update_empty_data() {
2283        let dialect = get_dialect(DbType::MySQL).unwrap();
2284        let builder = QueryBuilder::<TestModel>::new(dialect);
2285
2286        let data = std::collections::HashMap::new();
2287        let result = builder.table("users").validate_update(&data);
2288        assert!(result.is_err());
2289    }
2290
2291    #[test]
2292    fn test_validator_delete() {
2293        let dialect = get_dialect(DbType::MySQL).unwrap();
2294        let builder = QueryBuilder::<TestModel>::new(dialect);
2295
2296        let result = builder
2297            .table("users")
2298            .where_cond("id = 1")
2299            .validate_delete();
2300        assert!(result.is_ok());
2301    }
2302
2303    #[test]
2304    fn test_validator_delete_no_where() {
2305        let dialect = get_dialect(DbType::MySQL).unwrap();
2306        let builder = QueryBuilder::<TestModel>::new(dialect);
2307
2308        // DELETE without WHERE still produces valid SQL (just no filter)
2309        let result = builder.table("users").validate_delete();
2310        assert!(result.is_ok());
2311    }
2312
2313    // ==================== M-3 select_quoted 测试 ====================
2314
2315    #[test]
2316    fn test_m3_select_quoted_valid_columns() {
2317        let dialect = get_dialect(DbType::MySQL).unwrap();
2318        let builder = QueryBuilder::<TestModel>::new(dialect);
2319        let builder = builder
2320            .table("users")
2321            .select_quoted(vec!["id", "name"])
2322            .expect("valid columns should succeed");
2323        let sql = builder.build_select();
2324        // 应自动 quote 列名
2325        assert!(sql.contains("SELECT `id`, `name` FROM"));
2326        assert!(sql.contains("`users`"));
2327    }
2328
2329    #[test]
2330    fn test_m3_select_quoted_rejects_sql_injection() {
2331        let dialect = get_dialect(DbType::MySQL).unwrap();
2332        let builder = QueryBuilder::<TestModel>::new(dialect);
2333
2334        // SQL 注入尝试:分号 + DROP TABLE
2335        let result = builder
2336            .table("users")
2337            .select_quoted(vec!["id; DROP TABLE users"]);
2338        assert!(result.is_err());
2339
2340        // 含引号
2341        let dialect = get_dialect(DbType::MySQL).unwrap();
2342        let builder = QueryBuilder::<TestModel>::new(dialect);
2343        let result = builder.table("users").select_quoted(vec!["name'"]);
2344        assert!(result.is_err());
2345
2346        // 数字开头
2347        let dialect = get_dialect(DbType::MySQL).unwrap();
2348        let builder = QueryBuilder::<TestModel>::new(dialect);
2349        let result = builder.table("users").select_quoted(vec!["1col"]);
2350        assert!(result.is_err());
2351
2352        // 含空格
2353        let dialect = get_dialect(DbType::MySQL).unwrap();
2354        let builder = QueryBuilder::<TestModel>::new(dialect);
2355        let result = builder.table("users").select_quoted(vec!["col name"]);
2356        assert!(result.is_err());
2357    }
2358
2359    #[test]
2360    fn test_m3_select_quoted_postgresql_dialect() {
2361        let dialect = get_dialect(DbType::PostgreSQL).unwrap();
2362        let builder = QueryBuilder::<TestModel>::new(dialect);
2363        let builder = builder
2364            .table("users")
2365            .select_quoted(vec!["id", "name"])
2366            .expect("valid columns should succeed");
2367        let sql = builder.build_select();
2368        // PostgreSQL 使用双引号
2369        assert!(sql.contains("SELECT \"id\", \"name\" FROM"));
2370        assert!(sql.contains("\"users\""));
2371    }
2372
2373    // ==================== P0-1 软删除集成行为测试 ====================
2374
2375    /// 软删除测试模型:实现 soft_delete_field() 返回 "deleted_at"
2376    struct SoftDeleteModel;
2377    impl Model for SoftDeleteModel {
2378        type PrimaryKey = i64;
2379
2380        fn table_name() -> &'static str {
2381            "soft_users"
2382        }
2383
2384        fn pk(&self) -> Self::PrimaryKey {
2385            1
2386        }
2387
2388        fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
2389
2390        fn soft_delete_field() -> Option<&'static str> {
2391            Some("deleted_at")
2392        }
2393    }
2394
2395    /// 行为级测试 L3-1:软删除模型 build_select 自动追加 `WHERE deleted_at IS NULL`
2396    ///
2397    /// 用户视角:查询软删除模型时,自动过滤已删除记录,无需手动写条件。
2398    #[test]
2399    fn test_p01_soft_delete_select_auto_filter() {
2400        let dialect = get_dialect(DbType::MySQL).unwrap();
2401        let builder = QueryBuilder::<SoftDeleteModel>::new(dialect);
2402        let sql = builder.table("soft_users").build_select();
2403        // 必须自动追加软删除过滤
2404        assert!(
2405            sql.contains("`deleted_at` IS NULL"),
2406            "软删除模型 SELECT 必须自动追加 `deleted_at` IS NULL,实际: {}",
2407            sql
2408        );
2409    }
2410
2411    /// 行为级测试 L3-2:软删除模型 + 用户 WHERE 条件,软删除条件以 AND 追加
2412    #[test]
2413    fn test_p01_soft_delete_select_with_user_where() {
2414        let dialect = get_dialect(DbType::MySQL).unwrap();
2415        let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
2416            .table("soft_users")
2417            .where_eq("status", Value::String("active".into()))
2418            .build_select();
2419        // 用户条件 + 软删除条件 同时存在
2420        assert!(sql.contains("`status` = "), "用户条件应保留: {}", sql);
2421        assert!(
2422            sql.contains("`deleted_at` IS NULL"),
2423            "软删除条件应自动追加: {}",
2424            sql
2425        );
2426    }
2427
2428    /// 行为级测试 L3-3:without_soft_delete() 临时禁用软删除过滤
2429    ///
2430    /// 用户视角:管理员查询已删除记录时,可禁用自动过滤。
2431    #[test]
2432    fn test_p01_soft_delete_without_soft_delete() {
2433        let dialect = get_dialect(DbType::MySQL).unwrap();
2434        let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
2435            .table("soft_users")
2436            .without_soft_delete()
2437            .build_select();
2438        // 不应包含软删除过滤
2439        assert!(
2440            !sql.contains("`deleted_at` IS NULL"),
2441            "without_soft_delete 应禁用过滤,实际: {}",
2442            sql
2443        );
2444        // 也应无 WHERE 子句(因为用户未提供任何条件)
2445        assert!(
2446            !sql.contains("WHERE"),
2447            "无用户条件 + 禁用软删除应无 WHERE 子句: {}",
2448            sql
2449        );
2450    }
2451
2452    /// 行为级测试 L3-4:软删除模型 build_delete 自动转为 UPDATE
2453    ///
2454    /// 用户视角:调用 delete 实际是软删除 UPDATE,不是物理 DELETE。
2455    #[test]
2456    fn test_p01_soft_delete_delete_becomes_update() {
2457        let dialect = get_dialect(DbType::MySQL).unwrap();
2458        let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
2459            .table("soft_users")
2460            .where_eq("id", Value::I64(42))
2461            .build_delete();
2462        // 应生成 UPDATE 而非 DELETE
2463        assert!(
2464            sql.starts_with("UPDATE"),
2465            "软删除模型的 build_delete 应生成 UPDATE,实际: {}",
2466            sql
2467        );
2468        assert!(
2469            !sql.contains("DELETE FROM"),
2470            "不应生成 DELETE FROM: {}",
2471            sql
2472        );
2473        assert!(
2474            sql.contains("`deleted_at` = NOW()"),
2475            "应设置 deleted_at = NOW(): {}",
2476            sql
2477        );
2478        // 软删除条件应自动追加,防止更新已删除记录
2479        assert!(
2480            sql.contains("`deleted_at` IS NULL"),
2481            "软删除 UPDATE 应追加 deleted_at IS NULL 防止重复删除: {}",
2482            sql
2483        );
2484    }
2485
2486    /// 行为级测试 L3-5:build_force_delete 物理删除,不追加软删除过滤
2487    ///
2488    /// 用户视角:管理员强制清除时使用 build_force_delete。
2489    #[test]
2490    fn test_p01_soft_delete_force_delete() {
2491        let dialect = get_dialect(DbType::MySQL).unwrap();
2492        let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
2493            .table("soft_users")
2494            .where_eq("id", Value::I64(99))
2495            .build_force_delete();
2496        // 应生成 DELETE FROM
2497        assert!(
2498            sql.starts_with("DELETE FROM"),
2499            "build_force_delete 应生成 DELETE FROM,实际: {}",
2500            sql
2501        );
2502        // 不应追加软删除过滤
2503        assert!(
2504            !sql.contains("`deleted_at` IS NULL"),
2505            "物理删除不应追加软删除过滤: {}",
2506            sql
2507        );
2508    }
2509
2510    /// 行为级测试 L3-6:build_select_with_params 自动追加软删除条件(参数化版本)
2511    #[test]
2512    fn test_p01_soft_delete_select_with_params() {
2513        let dialect = get_dialect(DbType::MySQL).unwrap();
2514        let (sql, params) = QueryBuilder::<SoftDeleteModel>::new(dialect)
2515            .table("soft_users")
2516            .where_eq("id", Value::I64(1))
2517            .build_select_with_params();
2518        assert!(
2519            sql.contains("`deleted_at` IS NULL"),
2520            "参数化版本也应自动追加软删除: {}",
2521            sql
2522        );
2523        assert_eq!(params.len(), 1, "参数应为 1 个(用户 where_eq 的值)");
2524        assert_eq!(params[0], Value::I64(1));
2525    }
2526
2527    /// 行为级测试 L3-7:build_delete_with_params 自动转为 UPDATE
2528    #[test]
2529    fn test_p01_soft_delete_delete_with_params_becomes_update() {
2530        let dialect = get_dialect(DbType::MySQL).unwrap();
2531        let (sql, params) = QueryBuilder::<SoftDeleteModel>::new(dialect)
2532            .table("soft_users")
2533            .where_eq("id", Value::I64(7))
2534            .build_delete_with_params();
2535        assert!(sql.starts_with("UPDATE"), "应生成 UPDATE: {}", sql);
2536        assert!(sql.contains("`deleted_at` = NOW()"), "应设置 NOW(): {}", sql);
2537        assert_eq!(params.len(), 1, "参数应为 1 个(WHERE 的值)");
2538    }
2539
2540    /// 行为级测试 L3-8:build_force_delete_with_params 物理删除(参数化版本)
2541    #[test]
2542    fn test_p01_soft_delete_force_delete_with_params() {
2543        let dialect = get_dialect(DbType::MySQL).unwrap();
2544        let (sql, params) = QueryBuilder::<SoftDeleteModel>::new(dialect)
2545            .table("soft_users")
2546            .where_eq("id", Value::I64(11))
2547            .build_force_delete_with_params();
2548        assert!(sql.starts_with("DELETE FROM"), "应生成 DELETE: {}", sql);
2549        assert!(
2550            !sql.contains("`deleted_at` IS NULL"),
2551            "不应追加软删除过滤: {}",
2552            sql
2553        );
2554        assert_eq!(params.len(), 1);
2555    }
2556
2557    /// 行为级测试 L3-9:非软删除模型 TestModel 不追加软删除条件
2558    ///
2559    /// 用户视角:未启用软删除的模型行为不变。
2560    #[test]
2561    fn test_p01_non_soft_delete_model_unchanged() {
2562        let dialect = get_dialect(DbType::MySQL).unwrap();
2563        let sql = QueryBuilder::<TestModel>::new(dialect)
2564            .table("users")
2565            .where_eq("id", Value::I64(1))
2566            .build_select();
2567        assert!(
2568            !sql.contains("deleted_at"),
2569            "非软删除模型不应追加 deleted_at: {}",
2570            sql
2571        );
2572        // build_delete 仍生成 DELETE FROM
2573        let dialect = get_dialect(DbType::MySQL).unwrap();
2574        let del_sql = QueryBuilder::<TestModel>::new(dialect)
2575            .table("users")
2576            .where_eq("id", Value::I64(1))
2577            .build_delete();
2578        assert!(
2579            del_sql.starts_with("DELETE FROM"),
2580            "非软删除模型 build_delete 应生成 DELETE: {}",
2581            del_sql
2582        );
2583    }
2584
2585    /// 行为级测试 L3-10:build_count 也应自动追加软删除条件
2586    #[test]
2587    fn test_p01_soft_delete_count_auto_filter() {
2588        let dialect = get_dialect(DbType::MySQL).unwrap();
2589        let sql = QueryBuilder::<SoftDeleteModel>::new(dialect)
2590            .table("soft_users")
2591            .build_count();
2592        assert!(
2593            sql.contains("`deleted_at` IS NULL"),
2594            "build_count 也应追加软删除过滤: {}",
2595            sql
2596        );
2597    }
2598
2599    // ==================== P0-2 参数化查询注入防护测试 ====================
2600
2601    /// 行为级测试 L3-11:where_eq 使用 `?` 占位符,值收集到 params
2602    ///
2603    /// 用户视角:参数化查询杜绝 SQL 注入。
2604    #[test]
2605    fn test_p02_where_eq_uses_placeholder() {
2606        let dialect = get_dialect(DbType::MySQL).unwrap();
2607        let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
2608            .table("users")
2609            .where_eq("name", Value::String("alice".into()))
2610            .build_select_with_params();
2611        // SQL 中应含 `?` 占位符,不应内嵌值
2612        assert!(
2613            sql.contains("`name` = ?"),
2614            "应使用 ? 占位符: {}",
2615            sql
2616        );
2617        assert!(
2618            !sql.contains("'alice'"),
2619            "不应内嵌值到 SQL: {}",
2620            sql
2621        );
2622        assert_eq!(params.len(), 1);
2623        assert_eq!(params[0], Value::String("alice".into()));
2624    }
2625
2626    /// 行为级测试 L3-12:where_like 使用 `?` 占位符
2627    #[test]
2628    fn test_p02_where_like_uses_placeholder() {
2629        let dialect = get_dialect(DbType::MySQL).unwrap();
2630        let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
2631            .table("users")
2632            .where_like("name", Value::String("%alice%".into()))
2633            .build_select_with_params();
2634        assert!(sql.contains("`name` LIKE ?"), "应使用 LIKE ?: {}", sql);
2635        assert!(!sql.contains("%alice%"), "不应内嵌 pattern: {}", sql);
2636        assert_eq!(params.len(), 1);
2637    }
2638
2639    /// 行为级测试 L3-12a:where_ne 使用 `?` 占位符
2640    ///
2641    /// 验证 P0-2 参数化 API where_ne 生成 `field != ?` 且值不内嵌。
2642    #[test]
2643    fn test_p02_where_ne_uses_placeholder() {
2644        let dialect = get_dialect(DbType::MySQL).unwrap();
2645        let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
2646            .table("users")
2647            .where_ne("status", Value::I64(0))
2648            .build_select_with_params();
2649        assert!(
2650            sql.contains("`status` != ?"),
2651            "应使用 != ?: {}",
2652            sql
2653        );
2654        assert!(!sql.contains("!= 0"), "不应内嵌值: {}", sql);
2655        assert_eq!(params.len(), 1);
2656        assert_eq!(params[0], Value::I64(0));
2657    }
2658
2659    /// 行为级测试 L3-12b:where_ge 使用 `?` 占位符
2660    ///
2661    /// 验证 P0-2 参数化 API where_ge 生成 `field >= ?` 且值不内嵌。
2662    #[test]
2663    fn test_p02_where_ge_uses_placeholder() {
2664        let dialect = get_dialect(DbType::MySQL).unwrap();
2665        let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
2666            .table("users")
2667            .where_ge("age", Value::I64(18))
2668            .build_select_with_params();
2669        assert!(
2670            sql.contains("`age` >= ?"),
2671            "应使用 >= ?: {}",
2672            sql
2673        );
2674        assert!(!sql.contains(">= 18"), "不应内嵌值: {}", sql);
2675        assert_eq!(params.len(), 1);
2676        assert_eq!(params[0], Value::I64(18));
2677    }
2678
2679    /// 行为级测试 L3-12c:where_lt 使用 `?` 占位符
2680    ///
2681    /// 验证 P0-2 参数化 API where_lt 生成 `field < ?` 且值不内嵌。
2682    #[test]
2683    fn test_p02_where_lt_uses_placeholder() {
2684        let dialect = get_dialect(DbType::MySQL).unwrap();
2685        let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
2686            .table("users")
2687            .where_lt("score", Value::F64(60.0))
2688            .build_select_with_params();
2689        assert!(
2690            sql.contains("`score` < ?"),
2691            "应使用 < ?: {}",
2692            sql
2693        );
2694        assert!(!sql.contains("< 60"), "不应内嵌值: {}", sql);
2695        assert_eq!(params.len(), 1);
2696        assert_eq!(params[0], Value::F64(60.0));
2697    }
2698
2699    /// 行为级测试 L3-13:注入攻击防护 - 值含 SQL 关键字也不会被解释执行
2700    ///
2701    /// 用户视角:即使用户输入 `'; DROP TABLE users; --`,也不会造成注入。
2702    #[test]
2703    fn test_p02_injection_protection_drop_table() {
2704        let dialect = get_dialect(DbType::MySQL).unwrap();
2705        let evil_input = "'; DROP TABLE users; --".to_string();
2706        let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
2707            .table("users")
2708            .where_eq("name", Value::String(evil_input.clone()))
2709            .build_select_with_params();
2710        // SQL 中不应出现 DROP TABLE
2711        assert!(
2712            !sql.contains("DROP TABLE"),
2713            "SQL 注入未防护: {}",
2714            sql
2715        );
2716        // 整个恶意字符串应作为单一参数传递
2717        assert_eq!(params.len(), 1);
2718        assert_eq!(params[0], Value::String(evil_input));
2719        // SQL 中只有 1 个 `?`
2720        assert_eq!(sql.matches('?').count(), 1);
2721    }
2722
2723    /// 行为级测试 L3-14:注入攻击防护 - OR 1=1 经典攻击
2724    #[test]
2725    fn test_p02_injection_protection_or_one_equals_one() {
2726        let dialect = get_dialect(DbType::MySQL).unwrap();
2727        let evil = "' OR '1'='1".to_string();
2728        let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
2729            .table("users")
2730            .where_eq("name", Value::String(evil.clone()))
2731            .build_select_with_params();
2732        assert!(
2733            !sql.contains("OR '1'='1'"),
2734            "OR 1=1 注入未防护: {}",
2735            sql
2736        );
2737        assert_eq!(params.len(), 1);
2738        assert_eq!(params[0], Value::String(evil));
2739    }
2740
2741    /// 行为级测试 L3-15:多参数顺序正确(WHERE a = ? AND b = ?)
2742    #[test]
2743    fn test_p02_multiple_params_order() {
2744        let dialect = get_dialect(DbType::MySQL).unwrap();
2745        let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
2746            .table("users")
2747            .where_eq("name", Value::String("alice".into()))
2748            .where_gt("age", Value::I64(18))
2749            .where_le("score", Value::F64(99.5))
2750            .build_select_with_params();
2751        assert_eq!(
2752            sql.matches('?').count(),
2753            3,
2754            "应有 3 个占位符: {}",
2755            sql
2756        );
2757        assert_eq!(params.len(), 3);
2758        // 参数顺序应与 WHERE 子句出现顺序一致
2759        assert_eq!(params[0], Value::String("alice".into()));
2760        assert_eq!(params[1], Value::I64(18));
2761        assert_eq!(params[2], Value::F64(99.5));
2762    }
2763
2764    /// 行为级测试 L3-16:where_in 参数化
2765    #[test]
2766    fn test_p02_where_in_uses_placeholders() {
2767        let dialect = get_dialect(DbType::MySQL).unwrap();
2768        let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
2769            .table("users")
2770            .where_in("id", vec![Value::I64(1), Value::I64(2), Value::I64(3)])
2771            .build_select_with_params();
2772        assert!(
2773            sql.contains("`id` IN (?, ?, ?)"),
2774            "应使用 3 个占位符: {}",
2775            sql
2776        );
2777        assert_eq!(params.len(), 3);
2778    }
2779
2780    /// 行为级测试 L3-17:where_between 参数化
2781    #[test]
2782    fn test_p02_where_between_uses_placeholders() {
2783        let dialect = get_dialect(DbType::MySQL).unwrap();
2784        let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
2785            .table("users")
2786            .where_between("age", Value::I64(18), Value::I64(65))
2787            .build_select_with_params();
2788        assert!(
2789            sql.contains("`age` BETWEEN ? AND ?"),
2790            "应使用 2 个占位符: {}",
2791            sql
2792        );
2793        assert_eq!(params.len(), 2);
2794        assert_eq!(params[0], Value::I64(18));
2795        assert_eq!(params[1], Value::I64(65));
2796    }
2797
2798    /// 行为级测试 L3-18:UPDATE 参数化版本 - SET 参数在前,WHERE 参数在后
2799    #[test]
2800    fn test_p02_update_params_order_set_before_where() {
2801        let dialect = get_dialect(DbType::MySQL).unwrap();
2802        let mut data = std::collections::HashMap::new();
2803        data.insert("name".to_string(), Value::String("bob".into()));
2804        data.insert("age".to_string(), Value::I64(30));
2805        let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
2806            .table("users")
2807            .where_eq("id", Value::I64(99))
2808            .build_update_with_params(&data);
2809        // SET 子句应有 2 个占位符,WHERE 子句 1 个,共 3 个
2810        assert_eq!(sql.matches('?').count(), 3, "应有 3 个 ?: {}", sql);
2811        assert_eq!(params.len(), 3);
2812        // 前 2 个为 SET 参数,最后 1 个为 WHERE 参数
2813        // 注意:HashMap 迭代顺序未指定,仅校验 WHERE 参数在最后
2814        assert_eq!(params[2], Value::I64(99));
2815    }
2816
2817    /// 行为级测试 L3-19:build_where_clause(无参数版本)参数化条件内嵌值
2818    ///
2819    /// 验证无参数版本(build_select)对参数化条件的处理:直接内嵌转义值。
2820    #[test]
2821    fn test_p02_build_where_clause_inlines_value() {
2822        let dialect = get_dialect(DbType::MySQL).unwrap();
2823        let sql = QueryBuilder::<TestModel>::new(dialect)
2824            .table("users")
2825            .where_eq("name", Value::String("alice".into()))
2826            .build_select();
2827        // 无参数版本应内嵌值(依赖 to_param_with_dialect 转义)
2828        assert!(
2829            sql.contains("`name` = "),
2830            "无参数版本应含 WHERE 条件: {}",
2831            sql
2832        );
2833        // 不应含 `?`(无参数版本)
2834        assert!(
2835            !sql.contains("`name` = ?"),
2836            "无参数版本不应使用 ? 占位符: {}",
2837            sql
2838        );
2839    }
2840
2841    /// 行为级测试 L3-20:is_soft_delete_disabled 反映状态
2842    #[test]
2843    fn test_p01_is_soft_delete_disabled_flag() {
2844        let dialect = get_dialect(DbType::MySQL).unwrap();
2845        let builder = QueryBuilder::<SoftDeleteModel>::new(dialect);
2846        assert!(
2847            !builder.is_soft_delete_disabled(),
2848            "默认应启用软删除过滤"
2849        );
2850        let builder = QueryBuilder::<SoftDeleteModel>::new(get_dialect(DbType::MySQL).unwrap())
2851            .without_soft_delete();
2852        assert!(
2853            builder.is_soft_delete_disabled(),
2854            "without_soft_delete 后应反映禁用状态"
2855        );
2856    }
2857
2858    // ==================== P0-3 多租户自动过滤行为测试 ====================
2859
2860    /// 多租户测试模型:实现 tenant_field() 返回 "tenant_id"
2861    struct TenantModel;
2862    impl Model for TenantModel {
2863        type PrimaryKey = i64;
2864
2865        fn table_name() -> &'static str {
2866            "orders"
2867        }
2868
2869        fn pk(&self) -> Self::PrimaryKey {
2870            1
2871        }
2872
2873        fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
2874
2875        fn tenant_field() -> Option<&'static str> {
2876            Some("tenant_id")
2877        }
2878    }
2879
2880    /// 同时实现软删除 + 多租户的模型
2881    struct SoftDeleteAndTenantModel;
2882    impl Model for SoftDeleteAndTenantModel {
2883        type PrimaryKey = i64;
2884
2885        fn table_name() -> &'static str {
2886            "documents"
2887        }
2888
2889        fn pk(&self) -> Self::PrimaryKey {
2890            1
2891        }
2892
2893        fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
2894
2895        fn soft_delete_field() -> Option<&'static str> {
2896            Some("deleted_at")
2897        }
2898
2899        fn tenant_field() -> Option<&'static str> {
2900            Some("tenant_id")
2901        }
2902    }
2903
2904    /// 行为级测试 L3-21:多租户模型 + with_tenant_id 自动追加 WHERE tenant_id = ?
2905    ///
2906    /// 用户视角:设置租户 ID 后,查询自动过滤当前租户数据。
2907    #[test]
2908    fn test_p03_tenant_select_auto_filter() {
2909        let dialect = get_dialect(DbType::MySQL).unwrap();
2910        let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
2911            .table("orders")
2912            .with_tenant_id(42)
2913            .build_select_with_params();
2914        assert!(
2915            sql.contains("`tenant_id` = ?"),
2916            "多租户模型应自动追加 tenant_id = ?: {}",
2917            sql
2918        );
2919        assert_eq!(params.len(), 1, "应有 1 个参数(tenant_id 值)");
2920        assert_eq!(params[0], Value::I64(42));
2921    }
2922
2923    /// 行为级测试 L3-22:多租户模型 + 用户 WHERE 条件 + 租户条件
2924    #[test]
2925    fn test_p03_tenant_select_with_user_where() {
2926        let dialect = get_dialect(DbType::MySQL).unwrap();
2927        let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
2928            .table("orders")
2929            .with_tenant_id(7)
2930            .where_eq("status", Value::String("active".into()))
2931            .build_select_with_params();
2932        assert!(sql.contains("`status` = ?"), "用户条件应保留: {}", sql);
2933        assert!(
2934            sql.contains("`tenant_id` = ?"),
2935            "租户条件应自动追加: {}",
2936            sql
2937        );
2938        assert_eq!(params.len(), 2, "应有 2 个参数");
2939        // 第 1 个为用户 where_eq 的值,第 2 个为 tenant_id
2940        assert_eq!(params[0], Value::String("active".into()));
2941        assert_eq!(params[1], Value::I64(7));
2942    }
2943
2944    /// 行为级测试 L3-23:without_tenant() 临时禁用租户过滤
2945    ///
2946    /// 用户视角:管理员跨租户查询时禁用自动过滤。
2947    #[test]
2948    fn test_p03_tenant_without_tenant() {
2949        let dialect = get_dialect(DbType::MySQL).unwrap();
2950        let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
2951            .table("orders")
2952            .with_tenant_id(42)
2953            .without_tenant()
2954            .build_select_with_params();
2955        assert!(
2956            !sql.contains("`tenant_id` = ?"),
2957            "without_tenant 应禁用过滤: {}",
2958            sql
2959        );
2960        assert_eq!(params.len(), 0, "不应有租户参数");
2961    }
2962
2963    /// 行为级测试 L3-24:多租户模型 build_delete 自动追加租户条件
2964    ///
2965    /// 用户视角:删除操作自动限定在当前租户,防止跨租户删除。
2966    #[test]
2967    fn test_p03_tenant_delete_auto_filter() {
2968        let dialect = get_dialect(DbType::MySQL).unwrap();
2969        let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
2970            .table("orders")
2971            .with_tenant_id(99)
2972            .where_eq("id", Value::I64(1))
2973            .build_delete_with_params();
2974        assert!(
2975            sql.contains("`tenant_id` = ?"),
2976            "删除应自动追加租户条件: {}",
2977            sql
2978        );
2979        // 2 个参数:where_eq(id=1) + tenant_id=99
2980        assert_eq!(params.len(), 2);
2981        assert_eq!(params[0], Value::I64(1));
2982        assert_eq!(params[1], Value::I64(99));
2983    }
2984
2985    /// 行为级测试 L3-25:多租户模型 build_update 自动追加租户条件
2986    #[test]
2987    fn test_p03_tenant_update_auto_filter() {
2988        let dialect = get_dialect(DbType::MySQL).unwrap();
2989        let mut data = std::collections::HashMap::new();
2990        data.insert("status".to_string(), Value::String("shipped".into()));
2991        let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
2992            .table("orders")
2993            .with_tenant_id(5)
2994            .where_eq("id", Value::I64(10))
2995            .build_update_with_params(&data);
2996        assert!(
2997            sql.contains("`tenant_id` = ?"),
2998            "更新应自动追加租户条件: {}",
2999            sql
3000        );
3001        // 3 个参数:SET status + WHERE id + tenant_id
3002        assert_eq!(params.len(), 3);
3003        // 最后一个应为 tenant_id
3004        assert_eq!(params[2], Value::I64(5));
3005    }
3006
3007    /// 行为级测试 L3-26:多租户模型 build_count 自动追加租户条件
3008    #[test]
3009    fn test_p03_tenant_count_auto_filter() {
3010        let dialect = get_dialect(DbType::MySQL).unwrap();
3011        let sql = QueryBuilder::<TenantModel>::new(dialect)
3012            .table("orders")
3013            .with_tenant_id(42)
3014            .build_count();
3015        assert!(
3016            sql.contains("`tenant_id` = 42"),
3017            "build_count 应追加租户条件(无参数版本内嵌值): {}",
3018            sql
3019        );
3020    }
3021
3022    /// 行为级测试 L3-27:非多租户模型 TestModel 不追加租户条件
3023    ///
3024    /// 用户视角:未启用多租户的模型行为不变。
3025    #[test]
3026    fn test_p03_non_tenant_model_unchanged() {
3027        let dialect = get_dialect(DbType::MySQL).unwrap();
3028        // 即使设置了 with_tenant_id,非多租户模型也不应追加
3029        let (sql, params) = QueryBuilder::<TestModel>::new(dialect)
3030            .table("users")
3031            .with_tenant_id(42)
3032            .build_select_with_params();
3033        assert!(
3034            !sql.contains("tenant_id"),
3035            "非多租户模型不应追加 tenant_id: {}",
3036            sql
3037        );
3038        assert_eq!(params.len(), 0);
3039    }
3040
3041    /// 行为级测试 L3-28:多租户模型未设置 tenant_id 时不追加条件
3042    ///
3043    /// 用户视角:未设置租户 ID 时,查询不追加租户过滤(允许跨租户,需调用方保证安全)。
3044    #[test]
3045    fn test_p03_tenant_no_id_no_filter() {
3046        let dialect = get_dialect(DbType::MySQL).unwrap();
3047        let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3048            .table("orders")
3049            .build_select_with_params();
3050        assert!(
3051            !sql.contains("tenant_id"),
3052            "未设置 tenant_id 时不应追加过滤: {}",
3053            sql
3054        );
3055        assert_eq!(params.len(), 0);
3056    }
3057
3058    /// 行为级测试 L3-29:软删除 + 多租户组合,两个条件同时追加
3059    ///
3060    /// 用户视角:同时启用软删除和多租户时,查询自动追加两个条件。
3061    #[test]
3062    fn test_p03_soft_delete_and_tenant_combined() {
3063        let dialect = get_dialect(DbType::MySQL).unwrap();
3064        let (sql, params) = QueryBuilder::<SoftDeleteAndTenantModel>::new(dialect)
3065            .table("documents")
3066            .with_tenant_id(100)
3067            .where_eq("title", Value::String("report".into()))
3068            .build_select_with_params();
3069        // 软删除条件
3070        assert!(
3071            sql.contains("`deleted_at` IS NULL"),
3072            "应追加软删除条件: {}",
3073            sql
3074        );
3075        // 租户条件
3076        assert!(
3077            sql.contains("`tenant_id` = ?"),
3078            "应追加租户条件: {}",
3079            sql
3080        );
3081        // 用户条件
3082        assert!(
3083            sql.contains("`title` = ?"),
3084            "用户条件应保留: {}",
3085            sql
3086        );
3087        // 2 个参数:where_eq(title) + tenant_id(软删除 IS NULL 无参数)
3088        assert_eq!(params.len(), 2);
3089        assert_eq!(params[0], Value::String("report".into()));
3090        assert_eq!(params[1], Value::I64(100));
3091    }
3092
3093    /// 行为级测试 L3-30:without_tenant + without_soft_delete 同时禁用
3094    #[test]
3095    fn test_p03_without_tenant_and_soft_delete() {
3096        let dialect = get_dialect(DbType::MySQL).unwrap();
3097        let (sql, params) = QueryBuilder::<SoftDeleteAndTenantModel>::new(dialect)
3098            .table("documents")
3099            .with_tenant_id(100)
3100            .without_tenant()
3101            .without_soft_delete()
3102            .build_select_with_params();
3103        assert!(
3104            !sql.contains("`deleted_at` IS NULL"),
3105            "应禁用软删除: {}",
3106            sql
3107        );
3108        assert!(
3109            !sql.contains("`tenant_id` = ?"),
3110            "应禁用租户: {}",
3111            sql
3112        );
3113        assert_eq!(params.len(), 0);
3114    }
3115
3116    /// 行为级测试 L3-31:is_tenant_disabled 反映状态
3117    #[test]
3118    fn test_p03_is_tenant_disabled_flag() {
3119        let dialect = get_dialect(DbType::MySQL).unwrap();
3120        let builder = QueryBuilder::<TenantModel>::new(dialect);
3121        assert!(
3122            !builder.is_tenant_disabled(),
3123            "默认应启用租户过滤"
3124        );
3125        let builder = QueryBuilder::<TenantModel>::new(get_dialect(DbType::MySQL).unwrap())
3126            .with_tenant_id(1)
3127            .without_tenant();
3128        assert!(
3129            builder.is_tenant_disabled(),
3130            "without_tenant 后应反映禁用状态"
3131        );
3132    }
3133
3134    /// 行为级测试 L3-32:build_force_delete 保留租户条件(防止跨租户物理删除)
3135    ///
3136    /// 用户视角:物理删除也应受租户隔离约束,跨租户操作需显式 without_tenant()。
3137    #[test]
3138    fn test_p03_tenant_force_delete_keeps_tenant_filter() {
3139        let dialect = get_dialect(DbType::MySQL).unwrap();
3140        let (sql, params) = QueryBuilder::<TenantModel>::new(dialect)
3141            .table("orders")
3142            .with_tenant_id(42)
3143            .where_eq("id", Value::I64(999))
3144            .build_force_delete_with_params();
3145        // 物理删除不应追加软删除(TenantModel 未实现软删除,无影响)
3146        // 但应保留租户条件
3147        assert!(
3148            sql.contains("`tenant_id` = ?"),
3149            "物理删除应保留租户条件: {}",
3150            sql
3151        );
3152        assert_eq!(params.len(), 2);
3153        assert_eq!(params[0], Value::I64(999));
3154        assert_eq!(params[1], Value::I64(42));
3155    }
3156}