Skip to main content

sz_orm_query_builder/
lib.rs

1//! # SZ-ORM QueryBuilder — Standalone SQL Builder (sea-query style)
2//!
3//! A pure SQL builder that does not bind to Model, can be compiled independently and published to crates.io.
4//!
5//! Design inspired by [sea-query](https://crates.io/crates/sea-query):
6//! - Decoupled from ORM: does not depend on `Model` trait, pure SQL construction
7//! - Multi-dialect support: adapts MySQL/PostgreSQL/SQLite/Oracle via [`DbType`]
8//! - Chainable API: all methods return `Self`
9//! - Zero runtime overhead: construction process requires zero database connections
10//!
11//! # Quick Start
12//!
13//! ```rust
14//! use sz_orm_core::DbType;
15//! use sz_orm_query_builder::{Query, SelectQuery};
16//!
17//! // SELECT
18//! let sql = Query::select()
19//!     .column("id")
20//!     .column("name")
21//!     .from("users")
22//!     .where_clause("age > 18")
23//!     .order_by("id", true)
24//!     .limit(10)
25//!     .build(DbType::MySQL);
26//! assert!(sql.contains("SELECT"));
27//! assert!(sql.contains("FROM `users`"));
28//!
29//! // INSERT
30//! let sql = Query::insert()
31//!     .into_table("users")
32//!     .value("name", "'Alice'")
33//!     .value("age", "30")
34//!     .build();
35//! assert!(sql.contains("INSERT INTO `users`"));
36//!
37//! // UPDATE
38//! let sql = Query::update()
39//!     .table("users")
40//!     .set("name", "'Bob'")
41//!     .where_clause("id = 1")
42//!     .build();
43//! assert!(sql.contains("UPDATE `users`"));
44//!
45//! // DELETE
46//! let sql = Query::delete()
47//!     .from_table("users")
48//!     .where_clause("id = 1")
49//!     .build();
50//! assert!(sql.contains("DELETE FROM"));
51//! ```
52//!
53//! # Differences from sz-orm-core::QueryBuilder
54//!
55//! | Feature | `sz-orm-core::QueryBuilder<M>` | sz-orm-query-builder::Query |
56//! |---------|------------------------------|----------------------------|
57//! | Binds Model | Yes (`<M: Model>`) | No |
58//! | Type safety | Compile-time table/column validation | Runtime strings |
59//! | Applicable scenarios | Full ORM workflow | Pure SQL construction, dynamic queries |
60//! | Dependencies | All of sz-orm-core | Only dialect module |
61//! | Independent publish | No | Yes |
62
63use sz_orm_core::{DbType, Value};
64
65// ============================================================================
66// BuiltQuery — 参数化查询构造结果
67// ============================================================================
68
69/// Parameterized query construction result
70///
71/// Contains a SQL string with `?` placeholders and a list of parameters bound in order.
72///
73/// # Security (P0 fix: SQL injection prevention)
74///
75/// Through parameterized binding, user input is separated from the SQL structure,
76/// eliminating SQL injection at the root. Callers should use `build_with_params()`
77/// instead of string-concatenated `where_clause()`, especially when WHERE condition
78/// values come from untrusted input.
79#[derive(Debug, Clone, Default)]
80pub struct BuiltQuery {
81    /// SQL statement with `?` placeholders
82    pub sql: String,
83    /// List of parameters bound in order of appearance
84    pub params: Vec<Value>,
85}
86
87impl BuiltQuery {
88    /// Extract the SQL and parameters parts
89    pub fn into_parts(self) -> (String, Vec<Value>) {
90        (self.sql, self.params)
91    }
92}
93
94/// Parameterized WHERE condition (internal representation)
95///
96/// Compared to the original string concatenation of `wheres: Vec<String>`, this structure
97/// separates the SQL template (with `?`) from parameter values, fundamentally avoiding SQL injection.
98///
99/// # Dialect-aware quoting (v1.0.1 fix)
100///
101/// `column` stores the unquoted column name. At `build_with_params` time, it is quoted
102/// via `dialect.quote()` for the target dialect (PostgreSQL double quotes, MySQL backticks),
103/// avoiding premature hard-coding with backticks at the call site which would cause
104/// PostgreSQL dialect tests to fail.
105#[derive(Debug, Clone)]
106enum ParamWhere {
107    /// `AND <expr>` condition
108    And {
109        /// Unquoted column name (empty string represents a trivial expression like `1 = 0`)
110        column: String,
111        /// Operator + placeholder part (e.g., ` = ?`, ` IN (?, ?)`, `1 = 0`)
112        op: String,
113        /// Parameter values bound in order of appearance
114        values: Vec<Value>,
115    },
116    /// `OR <expr>` condition
117    Or {
118        column: String,
119        op: String,
120        values: Vec<Value>,
121    },
122}
123
124/// Wrap an identifier with backticks and escape internal backticks (MySQL standard: ` → ``)
125///
126/// # Security (gate 9 fix)
127///
128/// Unescaped backtick wrapping allows malicious identifiers to break out via ` injection.
129/// This function doubles backticks within the identifier (MySQL standard escaping),
130/// ensuring the concatenated SQL cannot be broken by malicious identifiers.
131///
132/// Supports dotted qualified identifiers: `u.id` → `u`.`id`
133fn quote_ident(s: &str) -> String {
134    s.split('.')
135        .map(|part| format!("`{}`", part.replace('`', "``")))
136        .collect::<Vec<_>>()
137        .join(".")
138}
139
140/// Quote a column name for the target dialect, supporting dotted qualified identifiers
141///
142/// Difference from [`sz_orm_core::Dialect::quote`]: this function first splits the qualified
143/// identifier by `.` (e.g., `t.id`), applies dialect quoting to each part, then joins them
144/// with `.`, producing `t`.`id` (MySQL) or `"t"."id"` (PostgreSQL/SQLite). Using
145/// `dialect.quote()` directly would quote the entire `t.id` as a single identifier,
146/// causing test assertions of the `t`.`id` form to fail.
147///
148/// # Security
149///
150/// Each identifier segment is escaped for internal quotes by `dialect.quote()` (MySQL `` ` `` → ` `` ` ``,
151/// PostgreSQL `"` → `""`), preventing identifier escape injection.
152fn quote_column_dialect(dialect: &dyn sz_orm_core::Dialect, column: &str) -> String {
153    column
154        .split('.')
155        .map(|part| dialect.quote(part))
156        .collect::<Vec<_>>()
157        .join(".")
158}
159
160/// Validate a WHERE condition string, rejecting obvious SQL injection patterns
161///
162/// v0.2.2 fix C-6: the public `where_clause(condition: &str)` accepts arbitrary strings,
163/// posing a SQL injection risk. This function detects high-risk patterns (semicolon + SQL
164/// keyword, line comments, block comments) and rejects obviously malicious input.
165///
166/// # Detection patterns
167///
168/// - `;` followed by a SQL keyword (DROP/DELETE/UPDATE/INSERT/ALTER/TRUNCATE/EXEC/CREATE/GRANT/REVOKE)
169/// - `--` line comment sequence
170/// - `/*` block comment start
171/// - `*/` block comment end
172///
173/// # Note
174///
175/// This validation is a baseline defense and cannot replace parameterized queries.
176/// Complex WHERE conditions should use the parameterized API.
177fn check_where_injection(condition: &str) {
178    let upper = condition.to_uppercase();
179    const SQL_KEYWORDS: &[&str] = &[
180        "DROP", "DELETE", "UPDATE", "INSERT", "ALTER", "TRUNCATE", "EXEC", "CREATE", "GRANT",
181        "REVOKE",
182    ];
183    for kw in SQL_KEYWORDS {
184        let pattern1 = format!(";{}", kw);
185        let pattern2 = format!("; {}", kw);
186        if upper.contains(&pattern1) || upper.contains(&pattern2) {
187            panic!(
188                "SQL injection detected in where_clause: semicolon followed by {} keyword: {:?}",
189                kw, condition
190            );
191        }
192    }
193    if condition.contains("--") {
194        panic!(
195            "SQL injection detected in where_clause: line comment '--' not allowed: {:?}",
196            condition
197        );
198    }
199    if condition.contains("/*") || condition.contains("*/") {
200        panic!(
201            "SQL injection detected in where_clause: block comment '/*' or '*/' not allowed: {:?}",
202            condition
203        );
204    }
205}
206
207/// Query builder entry point
208///
209/// # Gradual deprecation notice (v3.5.0)
210///
211/// Since v3.5.0, `sz-orm-query-builder` has entered gradual deprecation.
212/// Please evaluate [docs/query-builder-guide.md](../../../docs/query-builder-guide.md)
213/// to choose an appropriate approach:
214///
215/// - **Standard CRUD**: recommended to migrate to `sz_orm_core::QueryBuilder<M>` (compile-time type safety)
216/// - **Complex SQL (UNION/CTE/window functions)**: may continue using this crate (not removed yet)
217///
218/// This crate is **not immediately removed** in v3.5.0; the API is fully compatible,
219/// only a `#[deprecated]` warning is added. A decision on whether to merge into
220/// `sz-orm-core` or keep it standalone will be evaluated in v4.0.0.
221#[deprecated(
222    since = "3.5.0",
223    note = "v3.5.0: 请评估 docs/query-builder-guide.md 选择合适方案。标准 CRUD 推荐迁移到 sz_orm_core::QueryBuilder<M>"
224)]
225pub struct Query;
226
227#[allow(deprecated)]
228impl Query {
229    /// Create a SELECT query
230    #[deprecated(
231        since = "3.5.0",
232        note = "v3.5.0: 推荐迁移到 sz_orm_core::QueryBuilder<M>::select()。详见 docs/query-builder-guide.md"
233    )]
234    pub fn select() -> SelectQuery {
235        SelectQuery::new()
236    }
237
238    /// Create an INSERT query
239    #[deprecated(
240        since = "3.5.0",
241        note = "v3.5.0: 推荐迁移到 sz_orm_core::QueryBuilder<M>::insert()。详见 docs/query-builder-guide.md"
242    )]
243    pub fn insert() -> InsertQuery {
244        InsertQuery::new()
245    }
246
247    /// Create an UPDATE query
248    #[deprecated(
249        since = "3.5.0",
250        note = "v3.5.0: 推荐迁移到 sz_orm_core::QueryBuilder<M>::update()。详见 docs/query-builder-guide.md"
251    )]
252    pub fn update() -> UpdateQuery {
253        UpdateQuery::new()
254    }
255
256    /// Create a DELETE query
257    #[deprecated(
258        since = "3.5.0",
259        note = "v3.5.0: 推荐迁移到 sz_orm_core::QueryBuilder<M>::delete()。详见 docs/query-builder-guide.md"
260    )]
261    pub fn delete() -> DeleteQuery {
262        DeleteQuery::new()
263    }
264}
265
266/// Parameterized JOIN ON condition (P2 fix #68: JOIN injection risk)
267///
268/// Compared to the original `joins: Vec<String>` strings, this structure separates the
269/// SQL template from parameter values, preventing user input from being injected into
270/// the ON condition via `format!("u.id = {}", user_input)`.
271#[derive(Debug, Clone)]
272enum JoinOn {
273    /// Raw string (backward compatible with `inner_join(table, on_str)`)
274    Raw(String),
275    /// Column-to-column equality join: `left_column = right_column` (no parameters, pure identifiers, already escaped)
276    ColumnEq {
277        left_column: String,
278        right_column: String,
279    },
280    /// Parameterized condition: `left_column op ?` with Value
281    Param {
282        left_column: String,
283        op: String,
284        values: Vec<Value>,
285    },
286}
287
288/// JOIN clause (P2 fix #68)
289#[derive(Debug, Clone)]
290struct JoinClause {
291    /// JOIN type keyword (INNER JOIN / LEFT JOIN / RIGHT JOIN)
292    join_type: &'static str,
293    /// Table name part (already escaped via `quote_join_table`)
294    table: String,
295    /// ON condition list (supports multiple conditions, joined by AND)
296    on: Vec<JoinOn>,
297}
298
299/// SELECT query builder
300#[derive(Debug, Clone, Default)]
301pub struct SelectQuery {
302    columns: Vec<String>,
303    from_table: Option<String>,
304    /// FROM subquery: `(subquery SQL, alias)`. Mutually exclusive with `from_table`; later caller overrides earlier.
305    from_subquery: Option<(String, String)>,
306    /// Structured JOIN clauses (uniformly stores raw strings and parameterized ON conditions, P2 fix #68)
307    ///
308    /// The legacy `inner_join(table, on_str)` API pushes via `JoinOn::Raw(on_str)`,
309    /// sharing the same render path as the new `inner_join_on` / `inner_join_param`,
310    /// avoiding render order and dead code issues from dual-track data structures.
311    join_clauses: Vec<JoinClause>,
312    wheres: Vec<String>,
313    /// Parameterized WHERE conditions (P0 fix: does not interfere with `wheres`; at render time `wheres` first, then `param_wheres`)
314    param_wheres: Vec<ParamWhere>,
315    order_by: Vec<String>,
316    group_by: Vec<String>,
317    having: Vec<String>,
318    limit: Option<u64>,
319    offset: Option<u64>,
320    distinct: bool,
321    /// CTE (Common Table Expression) clause list: (name, subquery SQL, is_recursive)
322    ctes: Vec<(String, String, bool)>,
323    /// Window function columns: raw expressions (e.g., `ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC)`)
324    window_columns: Vec<String>,
325    /// FOR UPDATE lock hint
326    for_update: bool,
327    /// Column-qualified FOR UPDATE options (NOWAIT / SKIP LOCKED, etc.)
328    for_update_options: Option<String>,
329}
330
331impl SelectQuery {
332    /// Create an empty SELECT query
333    pub fn new() -> Self {
334        Self::default()
335    }
336
337    /// Set DISTINCT
338    pub fn distinct(mut self) -> Self {
339        self.distinct = true;
340        self
341    }
342
343    /// Add a column
344    pub fn column(mut self, name: &str) -> Self {
345        self.columns.push(name.to_string());
346        self
347    }
348
349    /// Add multiple columns
350    pub fn columns(mut self, names: &[&str]) -> Self {
351        for n in names {
352            self.columns.push(n.to_string());
353        }
354        self
355    }
356
357    /// Add a `*` column
358    pub fn all_columns(self) -> Self {
359        self.column("*")
360    }
361
362    /// Set FROM table
363    pub fn from(mut self, table: &str) -> Self {
364        self.from_table = Some(table.to_string());
365        // Mutually exclusive with from_subquery; later caller overrides earlier
366        self.from_subquery = None;
367        self
368    }
369
370    /// Set FROM subquery: `FROM (<subquery_sql>) AS <alias>`
371    ///
372    /// Mutually exclusive with [`from`](Self::from); later caller overrides earlier.
373    /// The subquery SQL is constructed by the caller (may be generated by another
374    /// `SelectQuery::build`), and the alias is escaped via `dialect.quote()` to
375    /// prevent identifier escape.
376    ///
377    /// # Example
378    ///
379    /// ```
380    /// use sz_orm_core::DbType;
381    /// use sz_orm_query_builder::Query;
382    ///
383    /// let inner = Query::select()
384    ///     .column("id")
385    ///     .column("amount")
386    ///     .from("orders")
387    ///     .build(DbType::MySQL);
388    /// let sql = Query::select()
389    ///     .column("id")
390    ///     .from_subquery(&inner, "t")
391    ///     .build(DbType::MySQL);
392    /// assert!(sql.contains("FROM (SELECT `id`, `amount` FROM `orders`) AS `t`"));
393    /// ```
394    pub fn from_subquery(mut self, subquery_sql: &str, alias: &str) -> Self {
395        self.from_subquery = Some((subquery_sql.to_string(), alias.to_string()));
396        // Mutually exclusive with from_table; later caller overrides earlier
397        self.from_table = None;
398        self
399    }
400
401    /// Add an INNER JOIN
402    ///
403    /// # Security (gate 9 fix)
404    ///
405    /// The table name is escaped via `quote_ident()`. The `on` condition is an
406    /// expression; the caller should ensure it is not constructed with malicious input.
407    pub fn inner_join(mut self, table: &str, on: &str) -> Self {
408        self.join_clauses.push(JoinClause {
409            join_type: "INNER JOIN",
410            table: Self::quote_join_table(table),
411            on: vec![JoinOn::Raw(on.to_string())],
412        });
413        self
414    }
415
416    /// Add a LEFT JOIN
417    ///
418    /// # Security (gate 9 fix)
419    ///
420    /// Same as `inner_join`; the table name is escaped via `quote_ident()`.
421    pub fn left_join(mut self, table: &str, on: &str) -> Self {
422        self.join_clauses.push(JoinClause {
423            join_type: "LEFT JOIN",
424            table: Self::quote_join_table(table),
425            on: vec![JoinOn::Raw(on.to_string())],
426        });
427        self
428    }
429
430    /// Add a RIGHT JOIN
431    ///
432    /// # Security (gate 9 fix)
433    ///
434    /// Same as `inner_join`; the table name is escaped via `quote_ident()`.
435    pub fn right_join(mut self, table: &str, on: &str) -> Self {
436        self.join_clauses.push(JoinClause {
437            join_type: "RIGHT JOIN",
438            table: Self::quote_join_table(table),
439            on: vec![JoinOn::Raw(on.to_string())],
440        });
441        self
442    }
443
444    // ========================================================================
445    // 参数化 JOIN(P2 修复 #68:JOIN 注入风险)
446    // ========================================================================
447    //
448    // 以下方法分离 JOIN ON 条件的 SQL 模板与参数值,从根源上杜绝 SQL 注入。
449    // 调用方应优先使用这些方法替代 `inner_join(table, &format!(...))`,
450    // 尤其是当 ON 条件值来自不可信输入时。
451    //
452    // 使用示例:
453    // ```
454    // use sz_orm_core::{DbType, Value};
455    // use sz_orm_query_builder::Query;
456    //
457    // // 列对列等值连接(最常见场景,无参数)
458    // let sql = Query::select()
459    //     .column("u.id")
460    //     .from("users u")
461    //     .inner_join_on("orders o", "u.id", "o.user_id")
462    //     .build(DbType::MySQL);
463    // // INNER JOIN `orders` o ON `u`.`id` = `o`.`user_id`
464    //
465    // // 参数化 ON 条件(用户输入作为参数绑定)
466    // let built = Query::select()
467    //     .column("u.id")
468    //     .from("users u")
469    //     .inner_join_param("orders o", "o.status", " = ?", Value::String("paid".into()))
470    //     .build_with_params(DbType::MySQL);
471    // // built.sql: "INNER JOIN `orders` o ON `o`.`status` = ?"
472    // // built.params: [String("paid")]
473    // ```
474
475    /// Add an INNER JOIN with a column-to-column equality ON condition (`left_col = right_col`)
476    ///
477    /// # Security
478    ///
479    /// Column names are escaped per dialect via `quote_column_dialect`, preventing identifier escape.
480    /// No parameter values; pure identifier join, the most common and safest JOIN form.
481    pub fn inner_join_on(mut self, table: &str, left_col: &str, right_col: &str) -> Self {
482        self.join_clauses.push(JoinClause {
483            join_type: "INNER JOIN",
484            table: Self::quote_join_table(table),
485            on: vec![JoinOn::ColumnEq {
486                left_column: left_col.to_string(),
487                right_column: right_col.to_string(),
488            }],
489        });
490        self
491    }
492
493    /// Add a LEFT JOIN with a column-to-column equality ON condition
494    pub fn left_join_on(mut self, table: &str, left_col: &str, right_col: &str) -> Self {
495        self.join_clauses.push(JoinClause {
496            join_type: "LEFT JOIN",
497            table: Self::quote_join_table(table),
498            on: vec![JoinOn::ColumnEq {
499                left_column: left_col.to_string(),
500                right_column: right_col.to_string(),
501            }],
502        });
503        self
504    }
505
506    /// Add a RIGHT JOIN with a column-to-column equality ON condition
507    pub fn right_join_on(mut self, table: &str, left_col: &str, right_col: &str) -> Self {
508        self.join_clauses.push(JoinClause {
509            join_type: "RIGHT JOIN",
510            table: Self::quote_join_table(table),
511            on: vec![JoinOn::ColumnEq {
512                left_column: left_col.to_string(),
513                right_column: right_col.to_string(),
514            }],
515        });
516        self
517    }
518
519    /// Add an INNER JOIN with a parameterized expression ON condition (`left_col op ?`)
520    ///
521    /// # Parameters
522    ///
523    /// - `table`: JOIN table name (supports alias `orders o`)
524    /// - `left_col`: left column name (already escaped)
525    /// - `op_expr`: operator + placeholder part (e.g., ` = ?`, ` > ?`, ` IN (?, ?)`)
526    /// - `value`: single parameter value
527    pub fn inner_join_param(
528        mut self,
529        table: &str,
530        left_col: &str,
531        op_expr: &str,
532        value: Value,
533    ) -> Self {
534        self.join_clauses.push(JoinClause {
535            join_type: "INNER JOIN",
536            table: Self::quote_join_table(table),
537            on: vec![JoinOn::Param {
538                left_column: left_col.to_string(),
539                op: op_expr.to_string(),
540                values: vec![value],
541            }],
542        });
543        self
544    }
545
546    /// Add a LEFT JOIN with a parameterized expression ON condition
547    pub fn left_join_param(
548        mut self,
549        table: &str,
550        left_col: &str,
551        op_expr: &str,
552        value: Value,
553    ) -> Self {
554        self.join_clauses.push(JoinClause {
555            join_type: "LEFT JOIN",
556            table: Self::quote_join_table(table),
557            on: vec![JoinOn::Param {
558                left_column: left_col.to_string(),
559                op: op_expr.to_string(),
560                values: vec![value],
561            }],
562        });
563        self
564    }
565
566    /// Add a RIGHT JOIN with a parameterized expression ON condition
567    pub fn right_join_param(
568        mut self,
569        table: &str,
570        left_col: &str,
571        op_expr: &str,
572        value: Value,
573    ) -> Self {
574        self.join_clauses.push(JoinClause {
575            join_type: "RIGHT JOIN",
576            table: Self::quote_join_table(table),
577            on: vec![JoinOn::Param {
578                left_column: left_col.to_string(),
579                op: op_expr.to_string(),
580                values: vec![value],
581            }],
582        });
583        self
584    }
585
586    /// Render JOIN clauses (including parameterized ON conditions) into a SQL string
587    ///
588    /// Uniformly renders `join_clauses`: includes raw string ON conditions (`JoinOn::Raw`,
589    /// backward compatible) and parameterized ON conditions (`ColumnEq` / `Param`).
590    /// Parameters from parameterized JOINs are appended to `params`.
591    fn render_joins(&self, dialect: &dyn sz_orm_core::Dialect, params: &mut Vec<Value>) -> String {
592        let mut sql = String::new();
593        for clause in &self.join_clauses {
594            sql.push(' ');
595            sql.push_str(clause.join_type);
596            sql.push(' ');
597            sql.push_str(&clause.table);
598            sql.push_str(" ON ");
599            for (i, on) in clause.on.iter().enumerate() {
600                if i > 0 {
601                    sql.push_str(" AND ");
602                }
603                match on {
604                    JoinOn::Raw(raw) => sql.push_str(raw),
605                    JoinOn::ColumnEq {
606                        left_column,
607                        right_column,
608                    } => {
609                        sql.push_str(&quote_column_dialect(dialect, left_column));
610                        sql.push_str(" = ");
611                        sql.push_str(&quote_column_dialect(dialect, right_column));
612                    }
613                    JoinOn::Param {
614                        left_column,
615                        op,
616                        values,
617                    } => {
618                        sql.push_str(&quote_column_dialect(dialect, left_column));
619                        sql.push_str(op);
620                        params.extend(values.iter().cloned());
621                    }
622                }
623            }
624        }
625        sql
626    }
627
628    /// Escape the JOIN table name part (supports alias: `orders o` → `` `orders` o ``)
629    fn quote_join_table(table: &str) -> String {
630        if let Some((tbl, alias)) = table.rsplit_once(' ') {
631            if alias.to_uppercase() == "AS" {
632                // `orders AS o`
633                format!("{} AS {}", quote_ident(tbl), alias)
634            } else {
635                // `orders o`
636                format!("{} {}", quote_ident(tbl), alias)
637            }
638        } else {
639            quote_ident(table)
640        }
641    }
642
643    /// Add a WHERE condition (AND joined)
644    ///
645    /// # Security (v0.2.2 fix C-6)
646    ///
647    /// Calls `check_where_injection` to detect high-risk patterns (semicolon + SQL keyword,
648    /// line comments, block comments). Complex WHERE conditions should use the parameterized
649    /// query API to avoid direct string concatenation.
650    pub fn where_clause(mut self, condition: &str) -> Self {
651        check_where_injection(condition);
652        self.wheres.push(condition.to_string());
653        self
654    }
655
656    // ========================================================================
657    // 参数化 WHERE 条件(P0 修复:SQL 注入防护)
658    // ========================================================================
659    //
660    // 以下方法分离 SQL 模板(含 `?` 占位符)与参数值,从根源上杜绝 SQL 注入。
661    // 调用方应优先使用这些方法替代 `where_clause(&str)`,尤其是当 WHERE 条件值
662    // 来自不可信输入时。
663    //
664    // 使用示例:
665    // ```
666    // use sz_orm_core::DbType;
667    // use sz_orm_query_builder::Query;
668    //
669    // let built = Query::select()
670    //     .column("id")
671    //     .from("users")
672    //     .where_eq("age", Value::I32(18))           // age > 18
673    //     .where_eq("status", Value::String("active".into()))  // AND status = ?
674    //     .or_where_eq("role", Value::String("admin".into()))  // OR role = ?
675    //     .build_with_params(DbType::MySQL);
676    // // built.sql: "SELECT `id` FROM `users` WHERE `age` = ? AND `status` = ? OR `role` = ?"
677    // // built.params: [I32(18), String("active"), String("admin")]
678    // ```
679
680    /// Add a `column = ?` AND condition
681    pub fn where_eq(mut self, column: &str, value: Value) -> Self {
682        self.param_wheres.push(ParamWhere::And {
683            column: column.to_string(),
684            op: " = ?".to_string(),
685            values: vec![value],
686        });
687        self
688    }
689
690    /// Add a `column <> ?` AND condition
691    pub fn where_ne(mut self, column: &str, value: Value) -> Self {
692        self.param_wheres.push(ParamWhere::And {
693            column: column.to_string(),
694            op: " <> ?".to_string(),
695            values: vec![value],
696        });
697        self
698    }
699
700    /// Add a `column > ?` AND condition
701    pub fn where_gt(mut self, column: &str, value: Value) -> Self {
702        self.param_wheres.push(ParamWhere::And {
703            column: column.to_string(),
704            op: " > ?".to_string(),
705            values: vec![value],
706        });
707        self
708    }
709
710    /// Add a `column >= ?` AND condition
711    pub fn where_ge(mut self, column: &str, value: Value) -> Self {
712        self.param_wheres.push(ParamWhere::And {
713            column: column.to_string(),
714            op: " >= ?".to_string(),
715            values: vec![value],
716        });
717        self
718    }
719
720    /// Add a `column < ?` AND condition
721    pub fn where_lt(mut self, column: &str, value: Value) -> Self {
722        self.param_wheres.push(ParamWhere::And {
723            column: column.to_string(),
724            op: " < ?".to_string(),
725            values: vec![value],
726        });
727        self
728    }
729
730    /// Add a `column <= ?` AND condition
731    pub fn where_le(mut self, column: &str, value: Value) -> Self {
732        self.param_wheres.push(ParamWhere::And {
733            column: column.to_string(),
734            op: " <= ?".to_string(),
735            values: vec![value],
736        });
737        self
738    }
739
740    /// Add a `column LIKE ?` AND condition
741    pub fn where_like(mut self, column: &str, pattern: Value) -> Self {
742        self.param_wheres.push(ParamWhere::And {
743            column: column.to_string(),
744            op: " LIKE ?".to_string(),
745            values: vec![pattern],
746        });
747        self
748    }
749
750    /// Add a `column IN (?, ?, ...)` AND condition
751    ///
752    /// An empty list produces `1 = 0` (always false), avoiding an invalid `IN ()`.
753    pub fn where_in(mut self, column: &str, values: Vec<Value>) -> Self {
754        let (column, op) = if values.is_empty() {
755            (String::new(), "1 = 0".to_string())
756        } else {
757            let placeholders: Vec<&str> = std::iter::repeat_n("?", values.len()).collect();
758            (
759                column.to_string(),
760                format!(" IN ({})", placeholders.join(", ")),
761            )
762        };
763        self.param_wheres
764            .push(ParamWhere::And { column, op, values });
765        self
766    }
767
768    /// Add a `column NOT IN (?, ?, ...)` AND condition
769    ///
770    /// An empty list produces `1 = 1` (always true), avoiding an invalid `NOT IN ()`.
771    pub fn where_not_in(mut self, column: &str, values: Vec<Value>) -> Self {
772        let (column, op) = if values.is_empty() {
773            (String::new(), "1 = 1".to_string())
774        } else {
775            let placeholders: Vec<&str> = std::iter::repeat_n("?", values.len()).collect();
776            (
777                column.to_string(),
778                format!(" NOT IN ({})", placeholders.join(", ")),
779            )
780        };
781        self.param_wheres
782            .push(ParamWhere::And { column, op, values });
783        self
784    }
785
786    /// Add a `column BETWEEN ? AND ?` AND condition
787    pub fn where_between(mut self, column: &str, low: Value, high: Value) -> Self {
788        self.param_wheres.push(ParamWhere::And {
789            column: column.to_string(),
790            op: " BETWEEN ? AND ?".to_string(),
791            values: vec![low, high],
792        });
793        self
794    }
795
796    /// Add a `column IS NULL` AND condition
797    pub fn where_null(mut self, column: &str) -> Self {
798        self.param_wheres.push(ParamWhere::And {
799            column: column.to_string(),
800            op: " IS NULL".to_string(),
801            values: vec![],
802        });
803        self
804    }
805
806    /// Add a `column IS NOT NULL` AND condition
807    pub fn where_not_null(mut self, column: &str) -> Self {
808        self.param_wheres.push(ParamWhere::And {
809            column: column.to_string(),
810            op: " IS NOT NULL".to_string(),
811            values: vec![],
812        });
813        self
814    }
815
816    /// Add a `column = ?` OR condition
817    pub fn or_where_eq(mut self, column: &str, value: Value) -> Self {
818        self.param_wheres.push(ParamWhere::Or {
819            column: column.to_string(),
820            op: " = ?".to_string(),
821            values: vec![value],
822        });
823        self
824    }
825
826    /// Add a `column <> ?` OR condition
827    pub fn or_where_ne(mut self, column: &str, value: Value) -> Self {
828        self.param_wheres.push(ParamWhere::Or {
829            column: column.to_string(),
830            op: " <> ?".to_string(),
831            values: vec![value],
832        });
833        self
834    }
835
836    /// Add a `column > ?` OR condition
837    pub fn or_where_gt(mut self, column: &str, value: Value) -> Self {
838        self.param_wheres.push(ParamWhere::Or {
839            column: column.to_string(),
840            op: " > ?".to_string(),
841            values: vec![value],
842        });
843        self
844    }
845
846    /// Add a `column >= ?` OR condition
847    pub fn or_where_ge(mut self, column: &str, value: Value) -> Self {
848        self.param_wheres.push(ParamWhere::Or {
849            column: column.to_string(),
850            op: " >= ?".to_string(),
851            values: vec![value],
852        });
853        self
854    }
855
856    /// Add a `column < ?` OR condition
857    pub fn or_where_lt(mut self, column: &str, value: Value) -> Self {
858        self.param_wheres.push(ParamWhere::Or {
859            column: column.to_string(),
860            op: " < ?".to_string(),
861            values: vec![value],
862        });
863        self
864    }
865
866    /// Add a `column <= ?` OR condition
867    pub fn or_where_le(mut self, column: &str, value: Value) -> Self {
868        self.param_wheres.push(ParamWhere::Or {
869            column: column.to_string(),
870            op: " <= ?".to_string(),
871            values: vec![value],
872        });
873        self
874    }
875
876    /// Add a `column LIKE ?` OR condition
877    pub fn or_where_like(mut self, column: &str, pattern: Value) -> Self {
878        self.param_wheres.push(ParamWhere::Or {
879            column: column.to_string(),
880            op: " LIKE ?".to_string(),
881            values: vec![pattern],
882        });
883        self
884    }
885
886    /// Add a `column IN (?, ?, ...)` OR condition
887    pub fn or_where_in(mut self, column: &str, values: Vec<Value>) -> Self {
888        let (column, op) = if values.is_empty() {
889            (String::new(), "1 = 0".to_string())
890        } else {
891            let placeholders: Vec<&str> = std::iter::repeat_n("?", values.len()).collect();
892            (
893                column.to_string(),
894                format!(" IN ({})", placeholders.join(", ")),
895            )
896        };
897        self.param_wheres
898            .push(ParamWhere::Or { column, op, values });
899        self
900    }
901
902    /// Add a `column BETWEEN ? AND ?` OR condition
903    pub fn or_where_between(mut self, column: &str, low: Value, high: Value) -> Self {
904        self.param_wheres.push(ParamWhere::Or {
905            column: column.to_string(),
906            op: " BETWEEN ? AND ?".to_string(),
907            values: vec![low, high],
908        });
909        self
910    }
911
912    /// Add a `column IS NULL` OR condition
913    pub fn or_where_null(mut self, column: &str) -> Self {
914        self.param_wheres.push(ParamWhere::Or {
915            column: column.to_string(),
916            op: " IS NULL".to_string(),
917            values: vec![],
918        });
919        self
920    }
921
922    /// Add a `column IS NOT NULL` OR condition
923    pub fn or_where_not_null(mut self, column: &str) -> Self {
924        self.param_wheres.push(ParamWhere::Or {
925            column: column.to_string(),
926            op: " IS NOT NULL".to_string(),
927            values: vec![],
928        });
929        self
930    }
931
932    /// Add GROUP BY
933    pub fn group_by(mut self, column: &str) -> Self {
934        self.group_by.push(column.to_string());
935        self
936    }
937
938    /// Add HAVING
939    pub fn having(mut self, condition: &str) -> Self {
940        self.having.push(condition.to_string());
941        self
942    }
943
944    /// Add ORDER BY
945    ///
946    /// # Parameters
947    ///
948    /// - `column`: column name
949    /// - `asc`: true=ASC, false=DESC
950    pub fn order_by(mut self, column: &str, asc: bool) -> Self {
951        let dir = if asc { "ASC" } else { "DESC" };
952        self.order_by.push(format!("{} {}", column, dir));
953        self
954    }
955
956    /// Set LIMIT
957    pub fn limit(mut self, n: u64) -> Self {
958        self.limit = Some(n);
959        self
960    }
961
962    /// Set OFFSET
963    pub fn offset(mut self, n: u64) -> Self {
964        self.offset = Some(n);
965        self
966    }
967
968    /// Generate pagination (sets both LIMIT and OFFSET)
969    ///
970    /// # Parameters
971    ///
972    /// - `page`: page number (1-based)
973    /// - `size`: page size
974    pub fn paginate(self, page: u64, size: u64) -> Self {
975        let offset = (page.saturating_sub(1)) * size;
976        self.limit(size).offset(offset)
977    }
978
979    /// Add a CTE (Common Table Expression / WITH clause).
980    ///
981    /// Generates SQL of the form `WITH name AS (subquery) SELECT ...`.
982    ///
983    /// # Parameters
984    ///
985    /// - `name`: CTE name
986    /// - `subquery`: subquery SQL (a complete SELECT statement)
987    pub fn with_cte(mut self, name: &str, subquery: &str) -> Self {
988        self.ctes
989            .push((name.to_string(), subquery.to_string(), false));
990        self
991    }
992
993    /// Add a recursive CTE (`WITH RECURSIVE name AS (...) SELECT ...`).
994    ///
995    /// # Parameters
996    ///
997    /// - `name`: CTE name
998    /// - `subquery`: recursive subquery SQL
999    pub fn with_recursive_cte(mut self, name: &str, subquery: &str) -> Self {
1000        self.ctes
1001            .push((name.to_string(), subquery.to_string(), true));
1002        self
1003    }
1004
1005    /// Add a window function column (as a raw expression in the SELECT list).
1006    ///
1007    /// The caller is responsible for constructing the complete window function expression, e.g.:
1008    /// - `ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC)`
1009    /// - `RANK() OVER (ORDER BY score DESC)`
1010    /// - `SUM(amount) OVER (PARTITION BY user_id ORDER BY created_at)`
1011    ///
1012    /// # Parameters
1013    ///
1014    /// - `expr`: complete window function expression
1015    pub fn window_function(mut self, expr: &str) -> Self {
1016        self.window_columns.push(expr.to_string());
1017        self
1018    }
1019
1020    /// Add a `ROW_NUMBER()` window function column.
1021    ///
1022    /// # Parameters
1023    ///
1024    /// - `partition_by`: PARTITION BY column (may be empty)
1025    /// - `order_by`: ORDER BY column (e.g., `salary DESC`)
1026    /// - `alias`: result column alias (e.g., `row_num`)
1027    pub fn row_number(self, partition_by: &str, order_by: &str, alias: &str) -> Self {
1028        let partition_clause = if partition_by.is_empty() {
1029            String::new()
1030        } else {
1031            format!("PARTITION BY {} ", partition_by)
1032        };
1033        let expr = format!(
1034            "ROW_NUMBER() OVER ({}ORDER BY {}) AS {}",
1035            partition_clause, order_by, alias
1036        );
1037        self.window_function(&expr)
1038    }
1039
1040    /// Add a `RANK()` window function column.
1041    ///
1042    /// # Parameters
1043    ///
1044    /// - `partition_by`: PARTITION BY column (may be empty)
1045    /// - `order_by`: ORDER BY column
1046    /// - `alias`: result column alias
1047    pub fn rank(self, partition_by: &str, order_by: &str, alias: &str) -> Self {
1048        let partition_clause = if partition_by.is_empty() {
1049            String::new()
1050        } else {
1051            format!("PARTITION BY {} ", partition_by)
1052        };
1053        let expr = format!(
1054            "RANK() OVER ({}ORDER BY {}) AS {}",
1055            partition_clause, order_by, alias
1056        );
1057        self.window_function(&expr)
1058    }
1059
1060    /// Add a `DENSE_RANK()` window function column.
1061    ///
1062    /// # Parameters
1063    ///
1064    /// - `partition_by`: PARTITION BY column (may be empty)
1065    /// - `order_by`: ORDER BY column
1066    /// - `alias`: result column alias
1067    pub fn dense_rank(self, partition_by: &str, order_by: &str, alias: &str) -> Self {
1068        let partition_clause = if partition_by.is_empty() {
1069            String::new()
1070        } else {
1071            format!("PARTITION BY {} ", partition_by)
1072        };
1073        let expr = format!(
1074            "DENSE_RANK() OVER ({}ORDER BY {}) AS {}",
1075            partition_clause, order_by, alias
1076        );
1077        self.window_function(&expr)
1078    }
1079
1080    /// Set FOR UPDATE row lock.
1081    ///
1082    /// Appends `FOR UPDATE` to the end of the generated SQL, used for pessimistic lock.
1083    pub fn for_update(mut self) -> Self {
1084        self.for_update = true;
1085        self.for_update_options = None;
1086        self
1087    }
1088
1089    /// Set FOR UPDATE with options (e.g., `NOWAIT`, `SKIP LOCKED`).
1090    ///
1091    /// # Parameters
1092    ///
1093    /// - `options`: options string, e.g., `"NOWAIT"` or `"SKIP LOCKED"`
1094    pub fn for_update_with_options(mut self, options: &str) -> Self {
1095        self.for_update = true;
1096        self.for_update_options = Some(options.to_string());
1097        self
1098    }
1099
1100    /// Combine the current query with another query using UNION set operation.
1101    ///
1102    /// Returns a [`SetQuery`], which can be turned into the final SQL via `build()`.
1103    pub fn union(self, other: SelectQuery) -> SetQuery {
1104        SetQuery::new(self, SetOperator::Union, other)
1105    }
1106
1107    /// Combine the current query with another query using UNION ALL set operation.
1108    pub fn union_all(self, other: SelectQuery) -> SetQuery {
1109        SetQuery::new(self, SetOperator::UnionAll, other)
1110    }
1111
1112    /// Combine the current query with another query using INTERSECT set operation.
1113    pub fn intersect(self, other: SelectQuery) -> SetQuery {
1114        SetQuery::new(self, SetOperator::Intersect, other)
1115    }
1116
1117    /// Combine the current query with another query using EXCEPT set operation.
1118    pub fn except(self, other: SelectQuery) -> SetQuery {
1119        SetQuery::new(self, SetOperator::Except, other)
1120    }
1121
1122    /// Generate SQL
1123    ///
1124    /// # Parameters
1125    ///
1126    /// - `db_type`: database type, used to select the dialect
1127    pub fn build(self, db_type: DbType) -> String {
1128        let dialect = match sz_orm_core::get_dialect(db_type) {
1129            Ok(d) => d,
1130            Err(_) => return String::new(),
1131        };
1132
1133        let mut sql = String::new();
1134
1135        // CTE(WITH 子句)
1136        if !self.ctes.is_empty() {
1137            let has_recursive = self.ctes.iter().any(|(_, _, r)| *r);
1138            if has_recursive {
1139                sql.push_str("WITH RECURSIVE ");
1140            } else {
1141                sql.push_str("WITH ");
1142            }
1143            let cte_strs: Vec<String> = self
1144                .ctes
1145                .iter()
1146                .map(|(name, subquery, _)| format!("{} AS ({})", name, subquery))
1147                .collect();
1148            sql.push_str(&cte_strs.join(", "));
1149            sql.push(' ');
1150        }
1151
1152        sql.push_str("SELECT ");
1153
1154        if self.distinct {
1155            sql.push_str("DISTINCT ");
1156        }
1157
1158        // 合并普通列与窗口函数列
1159        let mut all_columns: Vec<String> = self
1160            .columns
1161            .iter()
1162            .map(|c| {
1163                if c == "*" {
1164                    c.clone()
1165                } else {
1166                    dialect.quote(c)
1167                }
1168            })
1169            .collect();
1170        all_columns.extend(self.window_columns.iter().cloned());
1171
1172        if all_columns.is_empty() {
1173            sql.push('*');
1174        } else {
1175            sql.push_str(&all_columns.join(", "));
1176        }
1177
1178        if let Some(ref table) = self.from_table {
1179            sql.push_str(" FROM ");
1180            sql.push_str(&dialect.quote(table));
1181        } else if let Some((ref subquery, ref alias)) = self.from_subquery {
1182            // FROM 子查询:`FROM (<subquery>) AS <alias>`
1183            sql.push_str(" FROM (");
1184            sql.push_str(subquery);
1185            sql.push_str(") AS ");
1186            sql.push_str(&dialect.quote(alias));
1187        }
1188
1189        // 渲染 JOIN(含参数化 ON 条件,P2 修复 #68)
1190        // build() 不收集参数(无参数化查询),参数化 JOIN 应通过 build_with_params() 调用
1191        let mut unused_params = Vec::new();
1192        sql.push_str(&self.render_joins(&*dialect, &mut unused_params));
1193
1194        if !self.wheres.is_empty() {
1195            sql.push_str(" WHERE ");
1196            // 第一个条件不加 AND/OR 前缀
1197            sql.push_str(&self.wheres[0]);
1198            for w in &self.wheres[1..] {
1199                if w.starts_with("OR ") {
1200                    sql.push(' ');
1201                    sql.push_str(w);
1202                } else {
1203                    sql.push_str(" AND ");
1204                    sql.push_str(w);
1205                }
1206            }
1207        }
1208
1209        if !self.group_by.is_empty() {
1210            sql.push_str(" GROUP BY ");
1211            sql.push_str(
1212                &self
1213                    .group_by
1214                    .iter()
1215                    .map(|c| quote_ident(c))
1216                    .collect::<Vec<_>>()
1217                    .join(", "),
1218            );
1219        }
1220
1221        if !self.having.is_empty() {
1222            sql.push_str(" HAVING ");
1223            sql.push_str(&self.having.join(" AND "));
1224        }
1225
1226        if !self.order_by.is_empty() {
1227            sql.push_str(" ORDER BY ");
1228            sql.push_str(
1229                &self
1230                    .order_by
1231                    .iter()
1232                    .map(|s| {
1233                        // 格式为 "column ASC" 或 "column DESC"
1234                        if let Some((col, dir)) = s.rsplit_once(' ') {
1235                            format!("{} {}", quote_ident(col), dir)
1236                        } else {
1237                            quote_ident(s)
1238                        }
1239                    })
1240                    .collect::<Vec<_>>()
1241                    .join(", "),
1242            );
1243        }
1244
1245        if let Some(limit) = self.limit {
1246            sql.push_str(&format!(" LIMIT {}", limit));
1247        }
1248        if let Some(offset) = self.offset {
1249            sql.push_str(&format!(" OFFSET {}", offset));
1250        }
1251
1252        // FOR UPDATE 行锁
1253        if self.for_update {
1254            sql.push_str(" FOR UPDATE");
1255            if let Some(ref opts) = self.for_update_options {
1256                sql.push(' ');
1257                sql.push_str(opts);
1258            }
1259        }
1260
1261        sql
1262    }
1263
1264    /// Generate parameterized SQL (parameterized query, P0 fix: SQL injection prevention)
1265    ///
1266    /// Returns a [`BuiltQuery`], containing a SQL string with `?` placeholders and a
1267    /// list of parameters bound in order. Differences from [`build`](Self::build):
1268    /// - WHERE conditions can come from parameterized APIs like `where_eq`/`where_in`/`where_between`
1269    /// - User input is bound as parameters rather than concatenated into the SQL string
1270    ///
1271    /// # Mixed usage rules
1272    ///
1273    /// When using both raw `where_clause(&str)` and parameterized `where_eq(column, value)`:
1274    /// - Raw conditions render first (no parameters)
1275    /// - Parameterized conditions render after (parameters collected in order)
1276    /// - Both preserve AND/OR conjunction semantics in call order
1277    ///
1278    /// # Example
1279    ///
1280    /// ```
1281    /// use sz_orm_core::{DbType, Value};
1282    /// use sz_orm_query_builder::Query;
1283    ///
1284    /// let built = Query::select()
1285    ///     .column("id")
1286    ///     .from("users")
1287    ///     .where_eq("age", Value::I32(18))
1288    ///     .or_where_eq("role", Value::String("admin".into()))
1289    ///     .build_with_params(DbType::MySQL);
1290    /// assert!(built.sql.contains("WHERE `age` = ? OR `role` = ?"));
1291    /// assert_eq!(built.params.len(), 2);
1292    /// ```
1293    pub fn build_with_params(self, db_type: DbType) -> BuiltQuery {
1294        let dialect = match sz_orm_core::get_dialect(db_type) {
1295            Ok(d) => d,
1296            Err(_) => return BuiltQuery::default(),
1297        };
1298
1299        let mut sql = String::new();
1300        let mut params: Vec<Value> = Vec::new();
1301
1302        // CTE(WITH 子句)
1303        if !self.ctes.is_empty() {
1304            let has_recursive = self.ctes.iter().any(|(_, _, r)| *r);
1305            if has_recursive {
1306                sql.push_str("WITH RECURSIVE ");
1307            } else {
1308                sql.push_str("WITH ");
1309            }
1310            let cte_strs: Vec<String> = self
1311                .ctes
1312                .iter()
1313                .map(|(name, subquery, _)| format!("{} AS ({})", name, subquery))
1314                .collect();
1315            sql.push_str(&cte_strs.join(", "));
1316            sql.push(' ');
1317        }
1318
1319        sql.push_str("SELECT ");
1320
1321        if self.distinct {
1322            sql.push_str("DISTINCT ");
1323        }
1324
1325        let mut all_columns: Vec<String> = self
1326            .columns
1327            .iter()
1328            .map(|c| {
1329                if c == "*" {
1330                    c.clone()
1331                } else {
1332                    dialect.quote(c)
1333                }
1334            })
1335            .collect();
1336        all_columns.extend(self.window_columns.iter().cloned());
1337
1338        if all_columns.is_empty() {
1339            sql.push('*');
1340        } else {
1341            sql.push_str(&all_columns.join(", "));
1342        }
1343
1344        if let Some(ref table) = self.from_table {
1345            sql.push_str(" FROM ");
1346            sql.push_str(&dialect.quote(table));
1347        } else if let Some((ref subquery, ref alias)) = self.from_subquery {
1348            // FROM 子查询:`FROM (<subquery>) AS <alias>`
1349            sql.push_str(" FROM (");
1350            sql.push_str(subquery);
1351            sql.push_str(") AS ");
1352            sql.push_str(&dialect.quote(alias));
1353        }
1354
1355        // 渲染 JOIN(含参数化 ON 条件,P2 修复 #68)
1356        // 参数化 JOIN 的参数追加到 params 列表
1357        sql.push_str(&self.render_joins(&*dialect, &mut params));
1358
1359        // WHERE:原始 wheres(无参数)+ 参数化 param_wheres(带 ? 占位符)
1360        let has_raw = !self.wheres.is_empty();
1361        let has_param = !self.param_wheres.is_empty();
1362        if has_raw || has_param {
1363            sql.push_str(" WHERE ");
1364            let mut first = true;
1365            // 原始条件先渲染
1366            for w in &self.wheres {
1367                if first {
1368                    sql.push_str(w);
1369                    first = false;
1370                } else if w.starts_with("OR ") {
1371                    sql.push(' ');
1372                    sql.push_str(w);
1373                } else {
1374                    sql.push_str(" AND ");
1375                    sql.push_str(w);
1376                }
1377            }
1378            // 参数化条件后渲染
1379            for pw in &self.param_wheres {
1380                let (conjunction, column, op, vals) = match pw {
1381                    ParamWhere::And { column, op, values } => ("AND", column, op, values),
1382                    ParamWhere::Or { column, op, values } => ("OR", column, op, values),
1383                };
1384                // 按目标方言引用列名(PostgreSQL 双引号、MySQL 反引号)
1385                let expr = if column.is_empty() {
1386                    op.clone()
1387                } else {
1388                    format!("{}{}", quote_column_dialect(&*dialect, column), op)
1389                };
1390                if first {
1391                    sql.push_str(&expr);
1392                    first = false;
1393                } else {
1394                    sql.push(' ');
1395                    sql.push_str(conjunction);
1396                    sql.push(' ');
1397                    sql.push_str(&expr);
1398                }
1399                params.extend(vals.iter().cloned());
1400            }
1401        }
1402
1403        if !self.group_by.is_empty() {
1404            sql.push_str(" GROUP BY ");
1405            sql.push_str(
1406                &self
1407                    .group_by
1408                    .iter()
1409                    .map(|c| quote_ident(c))
1410                    .collect::<Vec<_>>()
1411                    .join(", "),
1412            );
1413        }
1414
1415        if !self.having.is_empty() {
1416            sql.push_str(" HAVING ");
1417            sql.push_str(&self.having.join(" AND "));
1418        }
1419
1420        if !self.order_by.is_empty() {
1421            sql.push_str(" ORDER BY ");
1422            sql.push_str(
1423                &self
1424                    .order_by
1425                    .iter()
1426                    .map(|s| {
1427                        if let Some((col, dir)) = s.rsplit_once(' ') {
1428                            format!("{} {}", quote_ident(col), dir)
1429                        } else {
1430                            quote_ident(s)
1431                        }
1432                    })
1433                    .collect::<Vec<_>>()
1434                    .join(", "),
1435            );
1436        }
1437
1438        if let Some(limit) = self.limit {
1439            sql.push_str(&format!(" LIMIT {}", limit));
1440        }
1441        if let Some(offset) = self.offset {
1442            sql.push_str(&format!(" OFFSET {}", offset));
1443        }
1444
1445        if self.for_update {
1446            sql.push_str(" FOR UPDATE");
1447            if let Some(ref opts) = self.for_update_options {
1448                sql.push(' ');
1449                sql.push_str(opts);
1450            }
1451        }
1452
1453        BuiltQuery { sql, params }
1454    }
1455}
1456
1457// ============================================================================
1458// 深度扩展:集合运算(UNION / INTERSECT / EXCEPT)
1459// ============================================================================
1460
1461/// SQL set operator type.
1462#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1463pub enum SetOperator {
1464    /// `UNION`: merge with deduplication
1465    Union,
1466    /// `UNION ALL`: merge without deduplication
1467    UnionAll,
1468    /// `INTERSECT`: intersection
1469    Intersect,
1470    /// `EXCEPT`: difference (MySQL 8.0+ calls it `EXCEPT`; some dialects use `MINUS`)
1471    Except,
1472}
1473
1474impl SetOperator {
1475    /// Returns the SQL keyword corresponding to the operator.
1476    pub fn as_sql(&self) -> &'static str {
1477        match self {
1478            SetOperator::Union => "UNION",
1479            SetOperator::UnionAll => "UNION ALL",
1480            SetOperator::Intersect => "INTERSECT",
1481            SetOperator::Except => "EXCEPT",
1482        }
1483    }
1484}
1485
1486/// Set operation query, supports chaining multiple SELECTs joined by UNION/INTERSECT/EXCEPT.
1487///
1488/// # Example
1489///
1490/// ```ignore
1491/// use sz_orm_core::DbType;
1492/// use sz_orm_query_builder::Query;
1493///
1494/// let q1 = Query::select().column("id").from("active_users");
1495/// let q2 = Query::select().column("id").from("pending_users");
1496/// let sql = q1.union(q2).build(DbType::MySQL);
1497/// // SELECT `id` FROM `active_users` UNION SELECT `id` FROM `pending_users`
1498/// ```
1499#[derive(Debug, Clone)]
1500pub struct SetQuery {
1501    /// First SELECT query
1502    first: SelectQuery,
1503    /// Subsequent (operator, query) pairs
1504    rest: Vec<(SetOperator, SelectQuery)>,
1505    /// Global ORDER BY (applies to the entire set operation result)
1506    order_by: Vec<String>,
1507    /// Global LIMIT
1508    limit: Option<u64>,
1509    /// Global OFFSET
1510    offset: Option<u64>,
1511}
1512
1513impl SetQuery {
1514    /// Create a set operation query.
1515    pub fn new(first: SelectQuery, op: SetOperator, second: SelectQuery) -> Self {
1516        Self {
1517            first,
1518            rest: vec![(op, second)],
1519            order_by: Vec::new(),
1520            limit: None,
1521            offset: None,
1522        }
1523    }
1524
1525    /// Append a UNION query.
1526    pub fn union(mut self, other: SelectQuery) -> Self {
1527        self.rest.push((SetOperator::Union, other));
1528        self
1529    }
1530
1531    /// Append a UNION ALL query.
1532    pub fn union_all(mut self, other: SelectQuery) -> Self {
1533        self.rest.push((SetOperator::UnionAll, other));
1534        self
1535    }
1536
1537    /// Append an INTERSECT query.
1538    pub fn intersect(mut self, other: SelectQuery) -> Self {
1539        self.rest.push((SetOperator::Intersect, other));
1540        self
1541    }
1542
1543    /// Append an EXCEPT query.
1544    pub fn except(mut self, other: SelectQuery) -> Self {
1545        self.rest.push((SetOperator::Except, other));
1546        self
1547    }
1548
1549    /// Set global ORDER BY (applies to the entire set operation result).
1550    pub fn order_by(mut self, column: &str, asc: bool) -> Self {
1551        let dir = if asc { "ASC" } else { "DESC" };
1552        self.order_by.push(format!("{} {}", column, dir));
1553        self
1554    }
1555
1556    /// Set global LIMIT.
1557    pub fn limit(mut self, n: u64) -> Self {
1558        self.limit = Some(n);
1559        self
1560    }
1561
1562    /// Set global OFFSET.
1563    pub fn offset(mut self, n: u64) -> Self {
1564        self.offset = Some(n);
1565        self
1566    }
1567
1568    /// Generate SQL.
1569    ///
1570    /// Joins all subqueries with the corresponding set operators, and appends
1571    /// global ORDER BY / LIMIT / OFFSET at the end.
1572    pub fn build(self, db_type: DbType) -> String {
1573        let mut sql = self.first.build(db_type);
1574        for (op, query) in &self.rest {
1575            sql.push(' ');
1576            sql.push_str(op.as_sql());
1577            sql.push(' ');
1578            sql.push_str(&query.clone().build(db_type));
1579        }
1580        if !self.order_by.is_empty() {
1581            sql.push_str(" ORDER BY ");
1582            sql.push_str(
1583                &self
1584                    .order_by
1585                    .iter()
1586                    .map(|s| {
1587                        if let Some((col, dir)) = s.rsplit_once(' ') {
1588                            format!("{} {}", quote_ident(col), dir)
1589                        } else {
1590                            quote_ident(s)
1591                        }
1592                    })
1593                    .collect::<Vec<_>>()
1594                    .join(", "),
1595            );
1596        }
1597        if let Some(limit) = self.limit {
1598            sql.push_str(&format!(" LIMIT {}", limit));
1599        }
1600        if let Some(offset) = self.offset {
1601            sql.push_str(&format!(" OFFSET {}", offset));
1602        }
1603        sql
1604    }
1605}
1606
1607/// Upsert (insert or update) strategy
1608///
1609/// Supports conflict-handling syntax of three mainstream databases:
1610/// - PostgreSQL/SQLite: `ON CONFLICT ... DO NOTHING` / `DO UPDATE`
1611/// - MySQL/MariaDB/TiDB: `ON DUPLICATE KEY UPDATE` / `REPLACE INTO`
1612///
1613/// # Dialect compatibility
1614///
1615/// | Strategy | MySQL | PostgreSQL | SQLite |
1616/// |----------|-------|------------|--------|
1617/// | `OnConflictDoNothing` | ✗ | ✓ | ✓ |
1618/// | `OnConflictDoUpdate` | ✗ | ✓ | ✓ |
1619/// | `OnDuplicateKeyUpdate` | ✓ | ✗ | ✗ |
1620/// | `Replace` | ✓ | ✗ | ✓ (`INSERT OR REPLACE`)|
1621///
1622/// Incompatible combinations are skipped in `build_with_dialect` (no error; the caller
1623/// must ensure dialect match).
1624#[derive(Debug, Clone, Default)]
1625pub enum UpsertStrategy {
1626    /// No upsert (default)
1627    #[default]
1628    None,
1629    /// PostgreSQL/SQLite: `ON CONFLICT (cols) DO NOTHING`
1630    OnConflictDoNothing(Vec<String>),
1631    /// PostgreSQL/SQLite: `ON CONFLICT (cols) DO UPDATE SET col = expr, ...`
1632    /// `(conflict_cols, update_assignments)`
1633    OnConflictDoUpdate(Vec<String>, Vec<(String, String)>),
1634    /// MySQL: `ON DUPLICATE KEY UPDATE col = expr, ...`
1635    OnDuplicateKeyUpdate(Vec<(String, String)>),
1636    /// MySQL: `REPLACE INTO` (delete conflicting rows first, then insert)
1637    Replace,
1638}
1639
1640/// Determine whether DbType is a MySQL-compatible dialect
1641fn is_mysql_family(db_type: DbType) -> bool {
1642    matches!(
1643        db_type,
1644        DbType::MySQL | DbType::MariaDB | DbType::TiDB | DbType::OceanBase | DbType::PolarDB
1645    )
1646}
1647
1648/// Determine whether DbType is a PostgreSQL-compatible dialect
1649fn is_pg_family(db_type: DbType) -> bool {
1650    matches!(
1651        db_type,
1652        DbType::PostgreSQL | DbType::Kingbase | DbType::GaussDB | DbType::PolarDB
1653    ) || db_type == DbType::Sqlite
1654}
1655
1656/// Render the upsert clause (for build_with_dialect)
1657///
1658/// Returns the corresponding upsert SQL fragment per dialect; incompatible combinations
1659/// return `None` (skipped).
1660fn render_upsert_clause(strategy: &UpsertStrategy, db_type: DbType) -> Option<String> {
1661    match strategy {
1662        UpsertStrategy::None => None,
1663        UpsertStrategy::OnConflictDoNothing(cols) => {
1664            if is_pg_family(db_type) {
1665                let cols_str = cols
1666                    .iter()
1667                    .map(|c| quote_ident(c))
1668                    .collect::<Vec<_>>()
1669                    .join(", ");
1670                Some(format!("ON CONFLICT ({}) DO NOTHING", cols_str))
1671            } else {
1672                None
1673            }
1674        }
1675        UpsertStrategy::OnConflictDoUpdate(cols, assignments) => {
1676            if is_pg_family(db_type) {
1677                let cols_str = cols
1678                    .iter()
1679                    .map(|c| quote_ident(c))
1680                    .collect::<Vec<_>>()
1681                    .join(", ");
1682                let sets = assignments
1683                    .iter()
1684                    .map(|(c, v)| format!("{} = {}", quote_ident(c), v))
1685                    .collect::<Vec<_>>()
1686                    .join(", ");
1687                Some(format!("ON CONFLICT ({}) DO UPDATE SET {}", cols_str, sets))
1688            } else {
1689                None
1690            }
1691        }
1692        UpsertStrategy::OnDuplicateKeyUpdate(assignments) => {
1693            if is_mysql_family(db_type) {
1694                let sets = assignments
1695                    .iter()
1696                    .map(|(c, v)| format!("{} = {}", quote_ident(c), v))
1697                    .collect::<Vec<_>>()
1698                    .join(", ");
1699                Some(format!("ON DUPLICATE KEY UPDATE {}", sets))
1700            } else {
1701                None
1702            }
1703        }
1704        UpsertStrategy::Replace => None, // REPLACE 在 build 阶段处理 INSERT 关键字替换
1705    }
1706}
1707
1708/// Render the RETURNING clause (for build_with_dialect)
1709///
1710/// RETURNING is PostgreSQL and SQLite 3.35+ syntax; MySQL does not support it.
1711/// - MySQL family: returns `None` (skipped)
1712/// - PostgreSQL/SQLite: returns `RETURNING col1, col2, ...`
1713/// - A column of `*` is not quoted; other column names are escaped via `dialect.quote()`
1714fn render_returning_clause(columns: &Option<Vec<String>>, db_type: DbType) -> Option<String> {
1715    let cols = columns.as_ref()?;
1716    if cols.is_empty() {
1717        return None;
1718    }
1719    // MySQL 不支持 RETURNING
1720    if is_mysql_family(db_type) {
1721        return None;
1722    }
1723    let dialect = sz_orm_core::get_dialect(db_type).ok()?;
1724    let quoted: Vec<String> = cols
1725        .iter()
1726        .map(|c| {
1727            if c == "*" {
1728                c.clone()
1729            } else {
1730                dialect.quote(c)
1731            }
1732        })
1733        .collect();
1734    Some(format!("RETURNING {}", quoted.join(", ")))
1735}
1736
1737/// INSERT query builder
1738#[derive(Debug, Clone, Default)]
1739pub struct InsertQuery {
1740    table: Option<String>,
1741    columns: Vec<String>,
1742    values: Vec<String>,
1743    /// Upsert strategy (ON CONFLICT / ON DUPLICATE KEY UPDATE / REPLACE)
1744    upsert: UpsertStrategy,
1745    /// RETURNING clause column list (supported by PostgreSQL/SQLite, not MySQL)
1746    returning: Option<Vec<String>>,
1747}
1748
1749impl InsertQuery {
1750    /// Create an empty INSERT query
1751    pub fn new() -> Self {
1752        Self::default()
1753    }
1754
1755    /// Set the target table
1756    pub fn into_table(mut self, table: &str) -> Self {
1757        self.table = Some(table.to_string());
1758        self
1759    }
1760
1761    /// Add a column-value pair (value should be an already-escaped SQL literal)
1762    pub fn value(mut self, column: &str, value: &str) -> Self {
1763        self.columns.push(column.to_string());
1764        self.values.push(value.to_string());
1765        self
1766    }
1767
1768    /// Batch add column-value pairs
1769    pub fn values(mut self, pairs: &[(&str, &str)]) -> Self {
1770        for (c, v) in pairs {
1771            self.columns.push(c.to_string());
1772            self.values.push(v.to_string());
1773        }
1774        self
1775    }
1776
1777    // ========================================================================
1778    // Upsert 策略(ON CONFLICT / ON DUPLICATE KEY UPDATE / REPLACE)
1779    // ========================================================================
1780
1781    /// PostgreSQL/SQLite: `ON CONFLICT (cols) DO NOTHING`
1782    ///
1783    /// Ignore insertion on conflict. Applicable to PostgreSQL and SQLite dialects.
1784    /// Under MySQL dialect this setting is ignored (MySQL does not support ON CONFLICT syntax).
1785    ///
1786    /// # Example
1787    ///
1788    /// ```
1789    /// use sz_orm_core::DbType;
1790    /// use sz_orm_query_builder::Query;
1791    ///
1792    /// let sql = Query::insert()
1793    ///     .into_table("users")
1794    ///     .value("id", "1")
1795    ///     .value("name", "'Alice'")
1796    ///     .on_conflict_do_nothing(&["id"])
1797    ///     .build_with_dialect(DbType::PostgreSQL);
1798    /// // INSERT INTO "users" ("id", "name") VALUES (1, 'Alice') ON CONFLICT ("id") DO NOTHING
1799    /// ```
1800    pub fn on_conflict_do_nothing(mut self, conflict_cols: &[&str]) -> Self {
1801        self.upsert = UpsertStrategy::OnConflictDoNothing(
1802            conflict_cols.iter().map(|s| s.to_string()).collect(),
1803        );
1804        self
1805    }
1806
1807    /// PostgreSQL/SQLite: `ON CONFLICT (cols) DO UPDATE SET col = expr, ...`
1808    ///
1809    /// Update specified columns on conflict. `assignments` is a list of `(column, expression)` pairs.
1810    /// Expressions may use `EXCLUDED.col` to reference the to-be-inserted value (PG/SQLite standard).
1811    ///
1812    /// # Example
1813    ///
1814    /// ```
1815    /// use sz_orm_core::DbType;
1816    /// use sz_orm_query_builder::Query;
1817    ///
1818    /// let sql = Query::insert()
1819    ///     .into_table("users")
1820    ///     .value("id", "1")
1821    ///     .value("name", "'Alice'")
1822    ///     .value("count", "1")
1823    ///     .on_conflict_do_update(
1824    ///         &["id"],
1825    ///         &[("name", "EXCLUDED.name"), ("count", "users.count + 1")],
1826    ///     )
1827    ///     .build_with_dialect(DbType::PostgreSQL);
1828    /// // INSERT INTO "users" (...) VALUES (...) ON CONFLICT ("id") DO UPDATE SET
1829    /// //   "name" = EXCLUDED.name, "count" = users.count + 1
1830    /// ```
1831    pub fn on_conflict_do_update(
1832        mut self,
1833        conflict_cols: &[&str],
1834        assignments: &[(&str, &str)],
1835    ) -> Self {
1836        self.upsert = UpsertStrategy::OnConflictDoUpdate(
1837            conflict_cols.iter().map(|s| s.to_string()).collect(),
1838            assignments
1839                .iter()
1840                .map(|(c, v)| (c.to_string(), v.to_string()))
1841                .collect(),
1842        );
1843        self
1844    }
1845
1846    /// MySQL: `ON DUPLICATE KEY UPDATE col = expr, ...`
1847    ///
1848    /// Update specified columns on primary/unique key conflict. `assignments` is a list of
1849    /// `(column, expression)` pairs. Expressions may use `VALUES(col)` to reference the
1850    /// to-be-inserted value (MySQL syntax).
1851    ///
1852    /// # Example
1853    ///
1854    /// ```
1855    /// use sz_orm_core::DbType;
1856    /// use sz_orm_query_builder::Query;
1857    ///
1858    /// let sql = Query::insert()
1859    ///     .into_table("users")
1860    ///     .value("id", "1")
1861    ///     .value("name", "'Alice'")
1862    ///     .value("count", "1")
1863    ///     .on_duplicate_key_update(&[("name", "VALUES(name)"), ("count", "count + 1")])
1864    ///     .build_with_dialect(DbType::MySQL);
1865    /// // INSERT INTO `users` (`id`, `name`, `count`) VALUES (1, 'Alice', 1)
1866    /// //   ON DUPLICATE KEY UPDATE `name` = VALUES(name), `count` = count + 1
1867    /// ```
1868    pub fn on_duplicate_key_update(mut self, assignments: &[(&str, &str)]) -> Self {
1869        self.upsert = UpsertStrategy::OnDuplicateKeyUpdate(
1870            assignments
1871                .iter()
1872                .map(|(c, v)| (c.to_string(), v.to_string()))
1873                .collect(),
1874        );
1875        self
1876    }
1877
1878    /// MySQL: use `REPLACE INTO` instead of `INSERT INTO`
1879    ///
1880    /// On primary/unique key conflict, delete the old row first then insert the new row.
1881    /// Applicable to MySQL/MariaDB/TiDB dialects.
1882    pub fn replace(mut self) -> Self {
1883        self.upsert = UpsertStrategy::Replace;
1884        self
1885    }
1886
1887    /// Set the RETURNING clause (supported by PostgreSQL/SQLite 3.35+)
1888    ///
1889    /// Returns the values of specified columns after INSERT, commonly used to obtain
1890    /// auto-increment primary keys or default values. MySQL does not support RETURNING;
1891    /// it is ignored in `build()` (MySQL style) and only rendered for PostgreSQL/SQLite
1892    /// dialects in `build_with_dialect()`.
1893    ///
1894    /// # Example
1895    ///
1896    /// ```
1897    /// use sz_orm_core::DbType;
1898    /// use sz_orm_query_builder::Query;
1899    ///
1900    /// let sql = Query::insert()
1901    ///     .into_table("users")
1902    ///     .value("name", "'Alice'")
1903    ///     .returning(&["id", "created_at"])
1904    ///     .build_with_dialect(DbType::PostgreSQL);
1905    /// // INSERT INTO "users" ("name") VALUES ('Alice') RETURNING "id", "created_at"
1906    /// ```
1907    pub fn returning(mut self, columns: &[&str]) -> Self {
1908        self.returning = Some(columns.iter().map(|s| s.to_string()).collect());
1909        self
1910    }
1911
1912    /// Set RETURNING * (return all columns)
1913    pub fn returning_all(mut self) -> Self {
1914        self.returning = Some(vec!["*".to_string()]);
1915        self
1916    }
1917
1918    /// Build INSERT SQL (no dialect, hard-coded backticks, MySQL style)
1919    ///
1920    /// # Security (gate 9 fix)
1921    ///
1922    /// Identifiers are escaped via `quote_ident()` and wrapped in backticks, preventing
1923    /// malicious identifiers containing `` ` `` from breaking out.
1924    ///
1925    /// # Upsert behavior
1926    ///
1927    /// - `Replace`: generates `REPLACE INTO` instead of `INSERT INTO`
1928    /// - `OnDuplicateKeyUpdate`: appends `ON DUPLICATE KEY UPDATE` clause
1929    /// - `OnConflictDoNothing`/`OnConflictDoUpdate`: skipped (MySQL does not support)
1930    pub fn build(self) -> String {
1931        let table = self.table.unwrap_or_default();
1932        if table.is_empty() || self.columns.is_empty() {
1933            return String::new();
1934        }
1935
1936        let cols: Vec<String> = self.columns.iter().map(|c| quote_ident(c)).collect();
1937        let vals: Vec<String> = self.values.iter().map(|v| v.to_string()).collect();
1938
1939        // 根据是否 REPLACE 策略选择关键字
1940        let verb = match &self.upsert {
1941            UpsertStrategy::Replace => "REPLACE INTO",
1942            _ => "INSERT INTO",
1943        };
1944        let mut sql = format!(
1945            "{} {} ({}) VALUES ({})",
1946            verb,
1947            quote_ident(&table),
1948            cols.join(", "),
1949            vals.join(", ")
1950        );
1951
1952        // MySQL 风格下仅支持 ON DUPLICATE KEY UPDATE
1953        if let UpsertStrategy::OnDuplicateKeyUpdate(assignments) = &self.upsert {
1954            let sets = assignments
1955                .iter()
1956                .map(|(c, v)| format!("{} = {}", quote_ident(c), v))
1957                .collect::<Vec<_>>()
1958                .join(", ");
1959            sql.push_str(&format!(" ON DUPLICATE KEY UPDATE {}", sets));
1960        }
1961
1962        sql
1963    }
1964
1965    /// Generate SQL for the specified dialect
1966    ///
1967    /// # Upsert dialect compatibility
1968    ///
1969    /// - MySQL family: supports `OnDuplicateKeyUpdate`, `Replace`
1970    /// - PostgreSQL/SQLite: supports `OnConflictDoNothing`, `OnConflictDoUpdate`
1971    /// - Incompatible combinations skip the upsert clause (no error)
1972    pub fn build_with_dialect(self, db_type: DbType) -> String {
1973        let dialect = match sz_orm_core::get_dialect(db_type) {
1974            Ok(d) => d,
1975            Err(_) => return String::new(),
1976        };
1977
1978        let table = self.table.unwrap_or_default();
1979        if table.is_empty() || self.columns.is_empty() {
1980            return String::new();
1981        }
1982
1983        let cols: Vec<String> = self.columns.iter().map(|c| dialect.quote(c)).collect();
1984
1985        // 根据是否 REPLACE 策略和方言选择关键字
1986        let verb = match (&self.upsert, db_type) {
1987            (UpsertStrategy::Replace, dt) if is_mysql_family(dt) => "REPLACE INTO",
1988            (UpsertStrategy::Replace, DbType::Sqlite) => "INSERT OR REPLACE INTO",
1989            _ => "INSERT INTO",
1990        };
1991        let mut sql = format!(
1992            "{} {} ({}) VALUES ({})",
1993            verb,
1994            dialect.quote(&table),
1995            cols.join(", "),
1996            self.values.join(", ")
1997        );
1998
1999        // 追加 upsert 子句(根据方言)
2000        if let Some(clause) = render_upsert_clause(&self.upsert, db_type) {
2001            sql.push(' ');
2002            sql.push_str(&clause);
2003        }
2004
2005        // 追加 RETURNING 子句(PostgreSQL/SQLite 支持,MySQL 跳过)
2006        if let Some(clause) = render_returning_clause(&self.returning, db_type) {
2007            sql.push(' ');
2008            sql.push_str(&clause);
2009        }
2010
2011        sql
2012    }
2013}
2014
2015/// UPDATE query builder
2016#[derive(Debug, Clone, Default)]
2017pub struct UpdateQuery {
2018    table: Option<String>,
2019    sets: Vec<(String, String)>,
2020    wheres: Vec<String>,
2021    /// Parameterized WHERE conditions (P0 fix: SQL injection prevention)
2022    param_wheres: Vec<ParamWhere>,
2023    /// RETURNING clause column list (supported by PostgreSQL/SQLite, not MySQL)
2024    returning: Option<Vec<String>>,
2025}
2026
2027impl UpdateQuery {
2028    /// Create an empty UPDATE query
2029    pub fn new() -> Self {
2030        Self::default()
2031    }
2032
2033    /// Set the target table
2034    pub fn table(mut self, table: &str) -> Self {
2035        self.table = Some(table.to_string());
2036        self
2037    }
2038
2039    /// Add a SET assignment (value should be an already-escaped SQL literal)
2040    pub fn set(mut self, column: &str, value: &str) -> Self {
2041        self.sets.push((column.to_string(), value.to_string()));
2042        self
2043    }
2044
2045    /// Batch add SET assignments
2046    pub fn sets(mut self, pairs: &[(&str, &str)]) -> Self {
2047        for (c, v) in pairs {
2048            self.sets.push((c.to_string(), v.to_string()));
2049        }
2050        self
2051    }
2052
2053    /// Add a WHERE condition
2054    ///
2055    /// # Security (v0.2.2 fix C-6)
2056    ///
2057    /// Calls `check_where_injection` to detect high-risk patterns.
2058    pub fn where_clause(mut self, condition: &str) -> Self {
2059        check_where_injection(condition);
2060        self.wheres.push(condition.to_string());
2061        self
2062    }
2063
2064    /// Add a `column = ?` AND condition (P0 fix: parameterized binding)
2065    pub fn where_eq(mut self, column: &str, value: Value) -> Self {
2066        self.param_wheres.push(ParamWhere::And {
2067            column: column.to_string(),
2068            op: " = ?".to_string(),
2069            values: vec![value],
2070        });
2071        self
2072    }
2073
2074    /// Add a `column <> ?` AND condition
2075    pub fn where_ne(mut self, column: &str, value: Value) -> Self {
2076        self.param_wheres.push(ParamWhere::And {
2077            column: column.to_string(),
2078            op: " <> ?".to_string(),
2079            values: vec![value],
2080        });
2081        self
2082    }
2083
2084    /// Add a `column > ?` AND condition
2085    pub fn where_gt(mut self, column: &str, value: Value) -> Self {
2086        self.param_wheres.push(ParamWhere::And {
2087            column: column.to_string(),
2088            op: " > ?".to_string(),
2089            values: vec![value],
2090        });
2091        self
2092    }
2093
2094    /// Add a `column >= ?` AND condition
2095    pub fn where_ge(mut self, column: &str, value: Value) -> Self {
2096        self.param_wheres.push(ParamWhere::And {
2097            column: column.to_string(),
2098            op: " >= ?".to_string(),
2099            values: vec![value],
2100        });
2101        self
2102    }
2103
2104    /// Add a `column < ?` AND condition
2105    pub fn where_lt(mut self, column: &str, value: Value) -> Self {
2106        self.param_wheres.push(ParamWhere::And {
2107            column: column.to_string(),
2108            op: " < ?".to_string(),
2109            values: vec![value],
2110        });
2111        self
2112    }
2113
2114    /// Add a `column <= ?` AND condition
2115    pub fn where_le(mut self, column: &str, value: Value) -> Self {
2116        self.param_wheres.push(ParamWhere::And {
2117            column: column.to_string(),
2118            op: " <= ?".to_string(),
2119            values: vec![value],
2120        });
2121        self
2122    }
2123
2124    /// Add a `column LIKE ?` AND condition
2125    pub fn where_like(mut self, column: &str, pattern: Value) -> Self {
2126        self.param_wheres.push(ParamWhere::And {
2127            column: column.to_string(),
2128            op: " LIKE ?".to_string(),
2129            values: vec![pattern],
2130        });
2131        self
2132    }
2133
2134    /// Add a `column IN (?, ?, ...)` AND condition
2135    pub fn where_in(mut self, column: &str, values: Vec<Value>) -> Self {
2136        let (column, op) = if values.is_empty() {
2137            (String::new(), "1 = 0".to_string())
2138        } else {
2139            let placeholders: Vec<&str> = std::iter::repeat_n("?", values.len()).collect();
2140            (
2141                column.to_string(),
2142                format!(" IN ({})", placeholders.join(", ")),
2143            )
2144        };
2145        self.param_wheres
2146            .push(ParamWhere::And { column, op, values });
2147        self
2148    }
2149
2150    /// Add a `column BETWEEN ? AND ?` AND condition
2151    pub fn where_between(mut self, column: &str, low: Value, high: Value) -> Self {
2152        self.param_wheres.push(ParamWhere::And {
2153            column: column.to_string(),
2154            op: " BETWEEN ? AND ?".to_string(),
2155            values: vec![low, high],
2156        });
2157        self
2158    }
2159
2160    /// Add a `column IS NULL` AND condition
2161    pub fn where_null(mut self, column: &str) -> Self {
2162        self.param_wheres.push(ParamWhere::And {
2163            column: column.to_string(),
2164            op: " IS NULL".to_string(),
2165            values: vec![],
2166        });
2167        self
2168    }
2169
2170    /// Add a `column IS NOT NULL` AND condition
2171    pub fn where_not_null(mut self, column: &str) -> Self {
2172        self.param_wheres.push(ParamWhere::And {
2173            column: column.to_string(),
2174            op: " IS NOT NULL".to_string(),
2175            values: vec![],
2176        });
2177        self
2178    }
2179
2180    /// Set the RETURNING clause (supported by PostgreSQL/SQLite 3.35+)
2181    ///
2182    /// Returns the new values of specified columns after UPDATE. MySQL does not support
2183    /// RETURNING; it is ignored in `build()` (MySQL style) and under MySQL dialect in
2184    /// `build_with_dialect()`.
2185    pub fn returning(mut self, columns: &[&str]) -> Self {
2186        self.returning = Some(columns.iter().map(|s| s.to_string()).collect());
2187        self
2188    }
2189
2190    /// Set RETURNING * (return all columns)
2191    pub fn returning_all(mut self) -> Self {
2192        self.returning = Some(vec!["*".to_string()]);
2193        self
2194    }
2195
2196    /// Generate SQL
2197    ///
2198    /// # Security (gate 9 fix)
2199    ///
2200    /// Table and column names are escaped via `quote_ident()` and wrapped in backticks,
2201    /// preventing malicious identifiers containing `` ` `` from breaking out.
2202    pub fn build(self) -> String {
2203        let table = self.table.unwrap_or_default();
2204        if table.is_empty() || self.sets.is_empty() {
2205            return String::new();
2206        }
2207
2208        let set_str: Vec<String> = self
2209            .sets
2210            .iter()
2211            .map(|(c, v)| format!("{} = {}", quote_ident(c), v))
2212            .collect();
2213
2214        let mut sql = format!("UPDATE {} SET {}", quote_ident(&table), set_str.join(", "));
2215
2216        if !self.wheres.is_empty() {
2217            sql.push_str(" WHERE ");
2218            sql.push_str(&self.wheres.join(" AND "));
2219        }
2220
2221        sql
2222    }
2223
2224    /// Generate SQL for the specified dialect
2225    pub fn build_with_dialect(self, db_type: DbType) -> String {
2226        let dialect = match sz_orm_core::get_dialect(db_type) {
2227            Ok(d) => d,
2228            Err(_) => return String::new(),
2229        };
2230
2231        let table = self.table.unwrap_or_default();
2232        if table.is_empty() || self.sets.is_empty() {
2233            return String::new();
2234        }
2235
2236        let set_str: Vec<String> = self
2237            .sets
2238            .iter()
2239            .map(|(c, v)| format!("{} = {}", dialect.quote(c), v))
2240            .collect();
2241
2242        let mut sql = format!(
2243            "UPDATE {} SET {}",
2244            dialect.quote(&table),
2245            set_str.join(", ")
2246        );
2247
2248        if !self.wheres.is_empty() {
2249            sql.push_str(" WHERE ");
2250            sql.push_str(&self.wheres.join(" AND "));
2251        }
2252
2253        // 追加 RETURNING 子句(PostgreSQL/SQLite 支持,MySQL 跳过)
2254        if let Some(clause) = render_returning_clause(&self.returning, db_type) {
2255            sql.push(' ');
2256            sql.push_str(&clause);
2257        }
2258
2259        sql
2260    }
2261
2262    /// Generate parameterized SQL (parameterized query, P0 fix: SQL injection prevention)
2263    ///
2264    /// WHERE conditions can come from parameterized APIs like `where_eq`/`where_in`/`where_between`,
2265    /// with user input bound as parameters rather than string concatenation.
2266    /// Note: SET values are still raw strings; for parameterized SET, use the ORM's `save()` interface.
2267    pub fn build_with_params(self, db_type: DbType) -> BuiltQuery {
2268        let dialect = match sz_orm_core::get_dialect(db_type) {
2269            Ok(d) => d,
2270            Err(_) => return BuiltQuery::default(),
2271        };
2272
2273        let table = self.table.unwrap_or_default();
2274        if table.is_empty() || self.sets.is_empty() {
2275            return BuiltQuery::default();
2276        }
2277
2278        let set_str: Vec<String> = self
2279            .sets
2280            .iter()
2281            .map(|(c, v)| format!("{} = {}", dialect.quote(c), v))
2282            .collect();
2283
2284        let mut sql = format!(
2285            "UPDATE {} SET {}",
2286            dialect.quote(&table),
2287            set_str.join(", ")
2288        );
2289        let mut params: Vec<Value> = Vec::new();
2290
2291        let has_raw = !self.wheres.is_empty();
2292        let has_param = !self.param_wheres.is_empty();
2293        if has_raw || has_param {
2294            sql.push_str(" WHERE ");
2295            let mut first = true;
2296            for w in &self.wheres {
2297                if first {
2298                    sql.push_str(w);
2299                    first = false;
2300                } else {
2301                    sql.push_str(" AND ");
2302                    sql.push_str(w);
2303                }
2304            }
2305            for pw in &self.param_wheres {
2306                let (conjunction, column, op, vals) = match pw {
2307                    ParamWhere::And { column, op, values } => ("AND", column, op, values),
2308                    ParamWhere::Or { column, op, values } => ("OR", column, op, values),
2309                };
2310                // 按目标方言引用列名(PostgreSQL 双引号、MySQL 反引号)
2311                let expr = if column.is_empty() {
2312                    op.clone()
2313                } else {
2314                    format!("{}{}", quote_column_dialect(&*dialect, column), op)
2315                };
2316                if first {
2317                    sql.push_str(&expr);
2318                    first = false;
2319                } else {
2320                    sql.push(' ');
2321                    sql.push_str(conjunction);
2322                    sql.push(' ');
2323                    sql.push_str(&expr);
2324                }
2325                params.extend(vals.iter().cloned());
2326            }
2327        }
2328
2329        // 追加 RETURNING 子句(PostgreSQL/SQLite 支持,MySQL 跳过)
2330        if let Some(clause) = render_returning_clause(&self.returning, db_type) {
2331            sql.push(' ');
2332            sql.push_str(&clause);
2333        }
2334
2335        BuiltQuery { sql, params }
2336    }
2337}
2338
2339/// DELETE query builder
2340#[derive(Debug, Clone, Default)]
2341pub struct DeleteQuery {
2342    table: Option<String>,
2343    wheres: Vec<String>,
2344    /// Parameterized WHERE conditions (P0 fix: SQL injection prevention)
2345    param_wheres: Vec<ParamWhere>,
2346    /// RETURNING clause column list (supported by PostgreSQL/SQLite, not MySQL)
2347    returning: Option<Vec<String>>,
2348}
2349
2350impl DeleteQuery {
2351    /// Create an empty DELETE query
2352    pub fn new() -> Self {
2353        Self::default()
2354    }
2355
2356    /// Set the target table
2357    pub fn from_table(mut self, table: &str) -> Self {
2358        self.table = Some(table.to_string());
2359        self
2360    }
2361
2362    /// Add a WHERE condition
2363    ///
2364    /// # Security (v0.2.2 fix C-6)
2365    ///
2366    /// Calls `check_where_injection` to detect high-risk patterns.
2367    pub fn where_clause(mut self, condition: &str) -> Self {
2368        check_where_injection(condition);
2369        self.wheres.push(condition.to_string());
2370        self
2371    }
2372
2373    /// Add a `column = ?` AND condition (P0 fix: parameterized binding)
2374    pub fn where_eq(mut self, column: &str, value: Value) -> Self {
2375        self.param_wheres.push(ParamWhere::And {
2376            column: column.to_string(),
2377            op: " = ?".to_string(),
2378            values: vec![value],
2379        });
2380        self
2381    }
2382
2383    /// Add a `column <> ?` AND condition
2384    pub fn where_ne(mut self, column: &str, value: Value) -> Self {
2385        self.param_wheres.push(ParamWhere::And {
2386            column: column.to_string(),
2387            op: " <> ?".to_string(),
2388            values: vec![value],
2389        });
2390        self
2391    }
2392
2393    /// Add a `column > ?` AND condition
2394    pub fn where_gt(mut self, column: &str, value: Value) -> Self {
2395        self.param_wheres.push(ParamWhere::And {
2396            column: column.to_string(),
2397            op: " > ?".to_string(),
2398            values: vec![value],
2399        });
2400        self
2401    }
2402
2403    /// Add a `column >= ?` AND condition
2404    pub fn where_ge(mut self, column: &str, value: Value) -> Self {
2405        self.param_wheres.push(ParamWhere::And {
2406            column: column.to_string(),
2407            op: " >= ?".to_string(),
2408            values: vec![value],
2409        });
2410        self
2411    }
2412
2413    /// Add a `column < ?` AND condition
2414    pub fn where_lt(mut self, column: &str, value: Value) -> Self {
2415        self.param_wheres.push(ParamWhere::And {
2416            column: column.to_string(),
2417            op: " < ?".to_string(),
2418            values: vec![value],
2419        });
2420        self
2421    }
2422
2423    /// Add a `column <= ?` AND condition
2424    pub fn where_le(mut self, column: &str, value: Value) -> Self {
2425        self.param_wheres.push(ParamWhere::And {
2426            column: column.to_string(),
2427            op: " <= ?".to_string(),
2428            values: vec![value],
2429        });
2430        self
2431    }
2432
2433    /// Add a `column LIKE ?` AND condition
2434    pub fn where_like(mut self, column: &str, pattern: Value) -> Self {
2435        self.param_wheres.push(ParamWhere::And {
2436            column: column.to_string(),
2437            op: " LIKE ?".to_string(),
2438            values: vec![pattern],
2439        });
2440        self
2441    }
2442
2443    /// Add a `column IN (?, ?, ...)` AND condition
2444    pub fn where_in(mut self, column: &str, values: Vec<Value>) -> Self {
2445        let (column, op) = if values.is_empty() {
2446            (String::new(), "1 = 0".to_string())
2447        } else {
2448            let placeholders: Vec<&str> = std::iter::repeat_n("?", values.len()).collect();
2449            (
2450                column.to_string(),
2451                format!(" IN ({})", placeholders.join(", ")),
2452            )
2453        };
2454        self.param_wheres
2455            .push(ParamWhere::And { column, op, values });
2456        self
2457    }
2458
2459    /// Add a `column BETWEEN ? AND ?` AND condition
2460    pub fn where_between(mut self, column: &str, low: Value, high: Value) -> Self {
2461        self.param_wheres.push(ParamWhere::And {
2462            column: column.to_string(),
2463            op: " BETWEEN ? AND ?".to_string(),
2464            values: vec![low, high],
2465        });
2466        self
2467    }
2468
2469    /// Add a `column IS NULL` AND condition
2470    pub fn where_null(mut self, column: &str) -> Self {
2471        self.param_wheres.push(ParamWhere::And {
2472            column: column.to_string(),
2473            op: " IS NULL".to_string(),
2474            values: vec![],
2475        });
2476        self
2477    }
2478
2479    /// Add a `column IS NOT NULL` AND condition
2480    pub fn where_not_null(mut self, column: &str) -> Self {
2481        self.param_wheres.push(ParamWhere::And {
2482            column: column.to_string(),
2483            op: " IS NOT NULL".to_string(),
2484            values: vec![],
2485        });
2486        self
2487    }
2488
2489    /// Set the RETURNING clause (supported by PostgreSQL/SQLite 3.35+)
2490    ///
2491    /// Returns the specified column values of deleted rows after DELETE. MySQL does not
2492    /// support RETURNING; it is ignored in `build()` (MySQL style) and under MySQL dialect
2493    /// in `build_with_dialect()`.
2494    pub fn returning(mut self, columns: &[&str]) -> Self {
2495        self.returning = Some(columns.iter().map(|s| s.to_string()).collect());
2496        self
2497    }
2498
2499    /// Set RETURNING * (return all columns)
2500    pub fn returning_all(mut self) -> Self {
2501        self.returning = Some(vec!["*".to_string()]);
2502        self
2503    }
2504
2505    /// Generate SQL
2506    ///
2507    /// # Security (gate 9 fix)
2508    ///
2509    /// The table name is escaped via `quote_ident()` and wrapped in backticks, preventing
2510    /// malicious table names containing `` ` `` from breaking out.
2511    pub fn build(self) -> String {
2512        let table = self.table.unwrap_or_default();
2513        if table.is_empty() {
2514            return String::new();
2515        }
2516
2517        let mut sql = format!("DELETE FROM {}", quote_ident(&table));
2518
2519        if !self.wheres.is_empty() {
2520            sql.push_str(" WHERE ");
2521            sql.push_str(&self.wheres.join(" AND "));
2522        }
2523
2524        sql
2525    }
2526
2527    /// Generate SQL for the specified dialect
2528    pub fn build_with_dialect(self, db_type: DbType) -> String {
2529        let dialect = match sz_orm_core::get_dialect(db_type) {
2530            Ok(d) => d,
2531            Err(_) => return String::new(),
2532        };
2533
2534        let table = self.table.unwrap_or_default();
2535        if table.is_empty() {
2536            return String::new();
2537        }
2538
2539        let mut sql = format!("DELETE FROM {}", dialect.quote(&table));
2540
2541        if !self.wheres.is_empty() {
2542            sql.push_str(" WHERE ");
2543            sql.push_str(&self.wheres.join(" AND "));
2544        }
2545
2546        // 追加 RETURNING 子句(PostgreSQL/SQLite 支持,MySQL 跳过)
2547        if let Some(clause) = render_returning_clause(&self.returning, db_type) {
2548            sql.push(' ');
2549            sql.push_str(&clause);
2550        }
2551
2552        sql
2553    }
2554
2555    /// Generate parameterized SQL (parameterized query, P0 fix: SQL injection prevention)
2556    ///
2557    /// WHERE conditions can come from parameterized APIs like `where_eq`/`where_in`/`where_between`,
2558    /// with user input bound as parameters rather than string concatenation.
2559    pub fn build_with_params(self, db_type: DbType) -> BuiltQuery {
2560        let dialect = match sz_orm_core::get_dialect(db_type) {
2561            Ok(d) => d,
2562            Err(_) => return BuiltQuery::default(),
2563        };
2564
2565        let table = self.table.unwrap_or_default();
2566        if table.is_empty() {
2567            return BuiltQuery::default();
2568        }
2569
2570        let mut sql = format!("DELETE FROM {}", dialect.quote(&table));
2571        let mut params: Vec<Value> = Vec::new();
2572
2573        let has_raw = !self.wheres.is_empty();
2574        let has_param = !self.param_wheres.is_empty();
2575        if has_raw || has_param {
2576            sql.push_str(" WHERE ");
2577            let mut first = true;
2578            for w in &self.wheres {
2579                if first {
2580                    sql.push_str(w);
2581                    first = false;
2582                } else {
2583                    sql.push_str(" AND ");
2584                    sql.push_str(w);
2585                }
2586            }
2587            for pw in &self.param_wheres {
2588                let (conjunction, column, op, vals) = match pw {
2589                    ParamWhere::And { column, op, values } => ("AND", column, op, values),
2590                    ParamWhere::Or { column, op, values } => ("OR", column, op, values),
2591                };
2592                // 按目标方言引用列名(PostgreSQL 双引号、MySQL 反引号)
2593                let expr = if column.is_empty() {
2594                    op.clone()
2595                } else {
2596                    format!("{}{}", quote_column_dialect(&*dialect, column), op)
2597                };
2598                if first {
2599                    sql.push_str(&expr);
2600                    first = false;
2601                } else {
2602                    sql.push(' ');
2603                    sql.push_str(conjunction);
2604                    sql.push(' ');
2605                    sql.push_str(&expr);
2606                }
2607                params.extend(vals.iter().cloned());
2608            }
2609        }
2610
2611        // 追加 RETURNING 子句(PostgreSQL/SQLite 支持,MySQL 跳过)
2612        if let Some(clause) = render_returning_clause(&self.returning, db_type) {
2613            sql.push(' ');
2614            sql.push_str(&clause);
2615        }
2616
2617        BuiltQuery { sql, params }
2618    }
2619}
2620
2621#[cfg(test)]
2622#[allow(deprecated)]
2623mod tests {
2624    use super::*;
2625
2626    // ---- Query::select 测试 ----
2627
2628    #[test]
2629    fn test_select_basic() {
2630        let sql = Query::select()
2631            .column("id")
2632            .column("name")
2633            .from("users")
2634            .build(DbType::MySQL);
2635        assert!(sql.starts_with("SELECT "));
2636        assert!(sql.contains("`id`"));
2637        assert!(sql.contains("`name`"));
2638        assert!(sql.contains("FROM `users`"));
2639    }
2640
2641    #[test]
2642    fn test_select_star() {
2643        let sql = Query::select()
2644            .all_columns()
2645            .from("users")
2646            .build(DbType::MySQL);
2647        assert!(sql.contains("SELECT *"));
2648        assert!(sql.contains("FROM `users`"));
2649    }
2650
2651    #[test]
2652    fn test_select_distinct() {
2653        let sql = Query::select()
2654            .distinct()
2655            .column("name")
2656            .from("users")
2657            .build(DbType::MySQL);
2658        assert!(sql.contains("SELECT DISTINCT"));
2659    }
2660
2661    #[test]
2662    fn test_select_with_where() {
2663        let sql = Query::select()
2664            .column("id")
2665            .from("users")
2666            .where_clause("age > 18")
2667            .where_clause("status = 'active'")
2668            .build(DbType::MySQL);
2669        assert!(sql.contains("WHERE age > 18 AND status = 'active'"));
2670    }
2671
2672    #[test]
2673
2674    fn test_select_with_inner_join() {
2675        let sql = Query::select()
2676            .column("u.id")
2677            .from("users u")
2678            .inner_join("orders o", "u.id = o.user_id")
2679            .build(DbType::MySQL);
2680        assert!(sql.contains("INNER JOIN `orders` o ON u.id = o.user_id"));
2681    }
2682
2683    #[test]
2684    fn test_select_with_left_join() {
2685        let sql = Query::select()
2686            .column("u.id")
2687            .from("users u")
2688            .left_join("profiles p", "u.id = p.user_id")
2689            .build(DbType::MySQL);
2690        assert!(sql.contains("LEFT JOIN `profiles` p ON u.id = p.user_id"));
2691    }
2692
2693    #[test]
2694    fn test_select_with_order_by() {
2695        let sql = Query::select()
2696            .column("id")
2697            .from("users")
2698            .order_by("created_at", true)
2699            .order_by("id", false)
2700            .build(DbType::MySQL);
2701        assert!(sql.contains("ORDER BY `created_at` ASC, `id` DESC"));
2702    }
2703
2704    #[test]
2705    fn test_select_with_limit_offset() {
2706        let sql = Query::select()
2707            .column("id")
2708            .from("users")
2709            .limit(10)
2710            .offset(20)
2711            .build(DbType::MySQL);
2712        assert!(sql.contains("LIMIT 10"));
2713        assert!(sql.contains("OFFSET 20"));
2714    }
2715
2716    #[test]
2717    fn test_select_paginate() {
2718        let sql = Query::select()
2719            .column("id")
2720            .from("users")
2721            .paginate(3, 20)
2722            .build(DbType::MySQL);
2723        // page 3, size 20 -> offset = (3-1)*20 = 40
2724        assert!(sql.contains("LIMIT 20"));
2725        assert!(sql.contains("OFFSET 40"));
2726    }
2727
2728    #[test]
2729    fn test_select_with_group_by_having() {
2730        let sql = Query::select()
2731            .column("status")
2732            .from("users")
2733            .group_by("status")
2734            .having("COUNT(*) > 5")
2735            .build(DbType::MySQL);
2736        assert!(sql.contains("GROUP BY `status`"));
2737        assert!(sql.contains("HAVING COUNT(*) > 5"));
2738    }
2739
2740    #[test]
2741    fn test_select_postgres_dialect() {
2742        let sql = Query::select()
2743            .column("id")
2744            .from("users")
2745            .build(DbType::PostgreSQL);
2746        assert!(sql.contains("\"id\""));
2747        assert!(sql.contains("FROM \"users\""));
2748    }
2749
2750    #[test]
2751    fn test_select_sqlite_dialect() {
2752        let sql = Query::select()
2753            .column("id")
2754            .from("users")
2755            .build(DbType::Sqlite);
2756        assert!(sql.contains("\"id\""));
2757    }
2758
2759    #[test]
2760    fn test_select_multiple_joins() {
2761        let sql = Query::select()
2762            .column("u.id")
2763            .from("users u")
2764            .inner_join("orders o", "u.id = o.user_id")
2765            .left_join("profiles p", "u.id = p.user_id")
2766            .build(DbType::MySQL);
2767        assert!(sql.contains("INNER JOIN `orders` o"));
2768        assert!(sql.contains("LEFT JOIN `profiles` p"));
2769    }
2770
2771    #[test]
2772    fn test_select_columns_multiple() {
2773        let sql = Query::select()
2774            .columns(&["id", "name", "email"])
2775            .from("users")
2776            .build(DbType::MySQL);
2777        assert!(sql.contains("`id`, `name`, `email`"));
2778    }
2779
2780    #[test]
2781    fn test_select_no_columns_defaults_star() {
2782        let sql = Query::select().from("users").build(DbType::MySQL);
2783        assert!(sql.contains("SELECT *"));
2784    }
2785
2786    // ---- Query::insert 测试 ----
2787
2788    #[test]
2789    fn test_insert_basic() {
2790        let sql = Query::insert()
2791            .into_table("users")
2792            .value("name", "'Alice'")
2793            .value("age", "30")
2794            .build();
2795        assert!(sql.starts_with("INSERT INTO `users`"));
2796        assert!(sql.contains("`name`, `age`"));
2797        assert!(sql.contains("'Alice', 30"));
2798    }
2799
2800    #[test]
2801    fn test_insert_values_batch() {
2802        let sql = Query::insert()
2803            .into_table("users")
2804            .values(&[("name", "'Bob'"), ("age", "25"), ("email", "'bob@x.com'")])
2805            .build();
2806        assert!(sql.contains("`name`, `age`, `email`"));
2807        assert!(sql.contains("'Bob', 25, 'bob@x.com'"));
2808    }
2809
2810    #[test]
2811    fn test_insert_empty_returns_empty() {
2812        let sql = Query::insert().into_table("users").build();
2813        assert_eq!(sql, "");
2814    }
2815
2816    #[test]
2817    fn test_insert_with_dialect() {
2818        let sql = Query::insert()
2819            .into_table("users")
2820            .value("name", "'Alice'")
2821            .build_with_dialect(DbType::PostgreSQL);
2822        assert!(sql.contains("\"name\""));
2823        assert!(sql.contains("\"users\""));
2824    }
2825
2826    // ---- Query::update 测试 ----
2827
2828    #[test]
2829    fn test_update_basic() {
2830        let sql = Query::update()
2831            .table("users")
2832            .set("name", "'Bob'")
2833            .where_clause("id = 1")
2834            .build();
2835        assert!(sql.starts_with("UPDATE `users` SET"));
2836        assert!(sql.contains("`name` = 'Bob'"));
2837        assert!(sql.contains("WHERE id = 1"));
2838    }
2839
2840    #[test]
2841    fn test_update_multiple_sets() {
2842        let sql = Query::update()
2843            .table("users")
2844            .sets(&[("name", "'Bob'"), ("age", "30")])
2845            .where_clause("id = 1")
2846            .build();
2847        assert!(sql.contains("`name` = 'Bob', `age` = 30"));
2848    }
2849
2850    #[test]
2851    fn test_update_no_where() {
2852        let sql = Query::update()
2853            .table("users")
2854            .set("status", "'active'")
2855            .build();
2856        assert!(sql.contains("UPDATE `users` SET `status` = 'active'"));
2857        assert!(!sql.contains("WHERE"));
2858    }
2859
2860    #[test]
2861    fn test_update_empty_returns_empty() {
2862        let sql = Query::update().table("users").build();
2863        assert_eq!(sql, "");
2864    }
2865
2866    #[test]
2867    fn test_update_with_dialect() {
2868        let sql = Query::update()
2869            .table("users")
2870            .set("name", "'Bob'")
2871            .build_with_dialect(DbType::PostgreSQL);
2872        assert!(sql.contains("\"users\""));
2873        assert!(sql.contains("\"name\""));
2874    }
2875
2876    // ---- Query::delete 测试 ----
2877
2878    #[test]
2879    fn test_delete_basic() {
2880        let sql = Query::delete()
2881            .from_table("users")
2882            .where_clause("id = 1")
2883            .build();
2884        assert!(sql.starts_with("DELETE FROM `users`"));
2885        assert!(sql.contains("WHERE id = 1"));
2886    }
2887
2888    #[test]
2889    fn test_delete_no_where() {
2890        let sql = Query::delete().from_table("users").build();
2891        assert!(sql.contains("DELETE FROM `users`"));
2892        assert!(!sql.contains("WHERE"));
2893    }
2894
2895    #[test]
2896    fn test_delete_multiple_wheres() {
2897        let sql = Query::delete()
2898            .from_table("users")
2899            .where_clause("id > 100")
2900            .where_clause("status = 'inactive'")
2901            .build();
2902        assert!(sql.contains("WHERE id > 100 AND status = 'inactive'"));
2903    }
2904
2905    #[test]
2906    fn test_delete_empty_returns_empty() {
2907        let sql = Query::delete().build();
2908        assert_eq!(sql, "");
2909    }
2910
2911    #[test]
2912    fn test_delete_with_dialect() {
2913        let sql = Query::delete()
2914            .from_table("users")
2915            .where_clause("id = 1")
2916            .build_with_dialect(DbType::PostgreSQL);
2917        assert!(sql.contains("\"users\""));
2918    }
2919
2920    // ---- 完整流程测试 ----
2921
2922    #[test]
2923    fn test_full_crud_flow() {
2924        // CREATE (用 INSERT 模拟)
2925        let insert = Query::insert()
2926            .into_table("users")
2927            .value("name", "'Alice'")
2928            .value("age", "30")
2929            .build();
2930        assert!(insert.contains("INSERT INTO"));
2931
2932        // READ
2933        let select = Query::select()
2934            .column("id")
2935            .column("name")
2936            .from("users")
2937            .where_clause("age > 18")
2938            .order_by("id", true)
2939            .limit(10)
2940            .build(DbType::MySQL);
2941        assert!(select.contains("SELECT"));
2942        assert!(select.contains("FROM"));
2943        assert!(select.contains("WHERE"));
2944        assert!(select.contains("ORDER BY"));
2945        assert!(select.contains("LIMIT"));
2946
2947        // UPDATE
2948        let update = Query::update()
2949            .table("users")
2950            .set("name", "'Bob'")
2951            .where_clause("id = 1")
2952            .build();
2953        assert!(update.contains("UPDATE"));
2954        assert!(update.contains("SET"));
2955        assert!(update.contains("WHERE"));
2956
2957        // DELETE
2958        let delete = Query::delete()
2959            .from_table("users")
2960            .where_clause("id = 1")
2961            .build();
2962        assert!(delete.contains("DELETE FROM"));
2963    }
2964
2965    #[test]
2966    fn test_complex_select_query() {
2967        let sql = Query::select()
2968            .distinct()
2969            .columns(&["u.id", "u.name", "o.total"])
2970            .from("users u")
2971            .inner_join("orders o", "u.id = o.user_id")
2972            .where_clause("u.status = 'active'")
2973            .where_clause("o.total > 100")
2974            .group_by("u.id")
2975            .having("SUM(o.total) > 1000")
2976            .order_by("u.id", true)
2977            .limit(20)
2978            .offset(40)
2979            .build(DbType::MySQL);
2980
2981        assert!(sql.contains("SELECT DISTINCT"));
2982        assert!(sql.contains("INNER JOIN `orders` o"));
2983        assert!(sql.contains("WHERE u.status = 'active' AND o.total > 100"));
2984        assert!(sql.contains("GROUP BY"));
2985        assert!(sql.contains("HAVING SUM(o.total) > 1000"));
2986        assert!(sql.contains("ORDER BY `u`.`id` ASC"));
2987        assert!(sql.contains("LIMIT 20"));
2988        assert!(sql.contains("OFFSET 40"));
2989    }
2990
2991    // ---- v0.2.2 修复 C-6:SQL 注入测试 ----
2992
2993    #[test]
2994    #[should_panic(expected = "SQL injection detected")]
2995    fn test_select_where_rejects_semicolon_drop() {
2996        let _ = Query::select()
2997            .column("id")
2998            .from("users")
2999            .where_clause("1=1; DROP TABLE users")
3000            .build(DbType::MySQL);
3001    }
3002
3003    #[test]
3004    #[should_panic(expected = "SQL injection detected")]
3005    fn test_select_where_rejects_semicolon_space_drop() {
3006        let _ = Query::select()
3007            .column("id")
3008            .from("users")
3009            .where_clause("1=1; DROP TABLE users")
3010            .build(DbType::MySQL);
3011    }
3012
3013    #[test]
3014    #[should_panic(expected = "SQL injection detected")]
3015    fn test_select_where_rejects_line_comment() {
3016        let _ = Query::select()
3017            .column("id")
3018            .from("users")
3019            .where_clause("id = 1 -- DROP TABLE users")
3020            .build(DbType::MySQL);
3021    }
3022
3023    #[test]
3024    #[should_panic(expected = "SQL injection detected")]
3025    fn test_select_where_rejects_block_comment() {
3026        let _ = Query::select()
3027            .column("id")
3028            .from("users")
3029            .where_clause("id = 1 /* comment */ OR 1=1")
3030            .build(DbType::MySQL);
3031    }
3032
3033    #[test]
3034    #[should_panic(expected = "SQL injection detected")]
3035    fn test_update_where_rejects_delete() {
3036        let _ = Query::update()
3037            .table("users")
3038            .set("name", "'x'")
3039            .where_clause("1=1; DELETE FROM users")
3040            .build();
3041    }
3042
3043    #[test]
3044    #[should_panic(expected = "SQL injection detected")]
3045    fn test_update_where_rejects_line_comment() {
3046        let _ = Query::update()
3047            .table("users")
3048            .set("name", "'x'")
3049            .where_clause("id = 1 -- bypass")
3050            .build();
3051    }
3052
3053    #[test]
3054    #[should_panic(expected = "SQL injection detected")]
3055    fn test_delete_where_rejects_drop() {
3056        let _ = Query::delete()
3057            .from_table("users")
3058            .where_clause("1=1; DROP TABLE users")
3059            .build();
3060    }
3061
3062    #[test]
3063    #[should_panic(expected = "SQL injection detected")]
3064    fn test_delete_where_rejects_block_comment() {
3065        let _ = Query::delete()
3066            .from_table("users")
3067            .where_clause("id = 1 /* */ OR 1=1")
3068            .build();
3069    }
3070
3071    #[test]
3072    #[should_panic(expected = "SQL injection detected")]
3073    fn test_delete_where_rejects_line_comment() {
3074        let _ = Query::delete()
3075            .from_table("users")
3076            .where_clause("id = 1--")
3077            .build();
3078    }
3079
3080    #[test]
3081    fn test_safe_where_clauses_pass() {
3082        // 这些是合法的 WHERE 条件,不应触发 panic——同时验证生成 SQL 包含预期的 WHERE 子句
3083        let sql_str = Query::select()
3084            .column("id")
3085            .from("users")
3086            .where_clause("age > 18")
3087            .where_clause("name = 'Alice;Bob'") // 分号在字符串字面量中
3088            .where_clause("id IN (1, 2, 3)")
3089            .where_clause("created_at > '2026-01-01'")
3090            .build(DbType::MySQL);
3091        assert!(!sql_str.is_empty(), "SELECT SQL 不应为空");
3092        assert!(sql_str.contains("age > 18"), "SELECT 应包含 age > 18 条件");
3093        assert!(
3094            sql_str.contains("name = 'Alice;Bob'"),
3095            "SELECT 应包含 name 条件(含分号字面量)"
3096        );
3097        assert!(sql_str.contains("id IN (1, 2, 3)"), "SELECT 应包含 IN 子句");
3098        assert!(
3099            sql_str.contains("created_at > '2026-01-01'"),
3100            "SELECT 应包含日期条件"
3101        );
3102
3103        let sql_str = Query::update()
3104            .table("users")
3105            .set("name", "'x'")
3106            .where_clause("id = 1")
3107            .build();
3108        assert!(!sql_str.is_empty(), "UPDATE SQL 不应为空");
3109        assert!(sql_str.contains("UPDATE"), "应为 UPDATE 语句");
3110        assert!(sql_str.contains("WHERE"), "UPDATE 应包含 WHERE 子句");
3111        assert!(sql_str.contains("id = 1"), "UPDATE WHERE 应包含 id = 1");
3112
3113        let sql_str = Query::delete()
3114            .from_table("users")
3115            .where_clause("id = 1")
3116            .build();
3117        assert!(!sql_str.is_empty(), "DELETE SQL 不应为空");
3118        assert!(sql_str.contains("DELETE"), "应为 DELETE 语句");
3119        assert!(sql_str.contains("WHERE"), "DELETE 应包含 WHERE 子句");
3120        assert!(sql_str.contains("id = 1"), "DELETE WHERE 应包含 id = 1");
3121    }
3122
3123    // ---- 变异测试专项补防测试(杀死存活的变异体) ----
3124    // 目标:覆盖 `||` → `&&` 变异(`check_where_injection` 和 `build_with_dialect` 守卫条件)
3125
3126    #[test]
3127    #[should_panic(expected = "SQL injection detected")]
3128    fn test_mutant_block_comment_open_only() {
3129        // 杀死:`contains("/*") || contains("*/")` → `&&` 变异
3130        // 仅含 `/*` 无 `*/`,`||` 应 panic,`&&` 不会 panic
3131        let _ = Query::select()
3132            .column("id")
3133            .from("users")
3134            .where_clause("id = 1 /* OR 1=1")
3135            .build(DbType::MySQL);
3136    }
3137
3138    #[test]
3139    #[should_panic(expected = "SQL injection detected")]
3140    fn test_mutant_block_comment_close_only() {
3141        // 仅含 `*/` 无 `/*`
3142        let _ = Query::select()
3143            .column("id")
3144            .from("users")
3145            .where_clause("id = 1 */")
3146            .build(DbType::MySQL);
3147    }
3148
3149    #[test]
3150    fn test_mutant_insert_dialect_table_no_columns_returns_empty() {
3151        // 杀死 `||` → `&&` 变异:有 table 无 columns → `||` 返回空,`&&` 不返回空
3152        let sql = Query::insert()
3153            .into_table("users")
3154            .build_with_dialect(DbType::MySQL);
3155        assert_eq!(sql, "", "有表无列时应返回空字符串");
3156    }
3157
3158    #[test]
3159    fn test_mutant_update_dialect_table_no_sets_returns_empty() {
3160        // 杀死 `||` → `&&` 变异
3161        let sql = Query::update()
3162            .table("users")
3163            .build_with_dialect(DbType::MySQL);
3164        assert_eq!(sql, "", "有表无 SET 时应返回空字符串");
3165    }
3166
3167    #[test]
3168    fn test_mutant_update_dialect_no_table_with_sets_returns_empty() {
3169        // 杀死 `delete !` 变异:无表有 SET → 应返回空
3170        let sql = Query::update()
3171            .set("name", "'x'")
3172            .build_with_dialect(DbType::MySQL);
3173        assert_eq!(sql, "", "无表有 SET 时应返回空字符串");
3174    }
3175
3176    #[test]
3177    fn test_mutant_delete_dialect_no_table_returns_empty() {
3178        // 杀死 `delete !` 变异
3179        let sql = Query::delete()
3180            .where_clause("id = 1")
3181            .build_with_dialect(DbType::MySQL);
3182        assert_eq!(sql, "", "无表时应返回空字符串");
3183    }
3184
3185    #[test]
3186    fn test_mutant_select_right_join() {
3187        // 杀死 `right_join -> Self` → `Default::default()` 变异
3188        let sql = Query::select()
3189            .column("u.id")
3190            .from("users u")
3191            .right_join("orders o", "u.id = o.user_id")
3192            .build(DbType::MySQL);
3193        assert!(sql.contains("RIGHT JOIN `orders` o ON u.id = o.user_id"));
3194    }
3195
3196    #[test]
3197    fn test_mutant_all_columns_with_extra() {
3198        // 杀死 `all_columns -> Self` → `Default::default()` 变异
3199        // 原始:`SELECT *, `extra` FROM`;变异体(default):`SELECT `extra` FROM`
3200        let sql = Query::select()
3201            .all_columns()
3202            .column("extra")
3203            .from("users")
3204            .build(DbType::MySQL);
3205        assert!(
3206            sql.contains("SELECT *, `extra` FROM `users`"),
3207            "all_columns + column 应在 SELECT 列表中同时包含 * 和 extra,实际: {sql}"
3208        );
3209    }
3210
3211    #[test]
3212    fn test_mutant_update_dialect_no_where_no_where_clause() {
3213        // 杀死 `delete !` 在 build_with_dialect WHERE 守卫
3214        // 原始:`if !self.wheres.is_empty()`,变异体:`if self.wheres.is_empty()`
3215        let sql = Query::update()
3216            .table("users")
3217            .set("name", "'x'")
3218            .build_with_dialect(DbType::MySQL);
3219        assert!(
3220            !sql.contains("WHERE"),
3221            "无 WHERE 条件时不应包含 WHERE 关键字,实际: {sql}"
3222        );
3223    }
3224
3225    #[test]
3226    fn test_mutant_delete_dialect_no_where_no_where_clause() {
3227        // 杀死 `delete !` 在 DeleteQuery::build_with_dialect WHERE 守卫
3228        let sql = Query::delete()
3229            .from_table("users")
3230            .build_with_dialect(DbType::MySQL);
3231        assert!(
3232            !sql.contains("WHERE"),
3233            "无 WHERE 条件时不应包含 WHERE 关键字,实际: {sql}"
3234        );
3235    }
3236
3237    // ==================== 深度扩展:CTE / 窗口函数 / 集合运算 / FOR UPDATE 测试 ====================
3238
3239    // ---- CTE 测试 ----
3240
3241    #[test]
3242    fn test_cte_single_with_clause() {
3243        let sql = Query::select()
3244            .column("id")
3245            .column("name")
3246            .from("active_users")
3247            .with_cte(
3248                "active_users",
3249                "SELECT * FROM users WHERE status = 'active'",
3250            )
3251            .build(DbType::MySQL);
3252        assert!(sql.starts_with("WITH active_users AS ("));
3253        assert!(sql.contains("SELECT * FROM users WHERE status = 'active'"));
3254        assert!(sql.contains("SELECT `id`, `name` FROM `active_users`"));
3255    }
3256
3257    #[test]
3258    fn test_cte_multiple_with_clauses() {
3259        let sql = Query::select()
3260            .column("id")
3261            .from("combined")
3262            .with_cte("a", "SELECT id FROM table_a")
3263            .with_cte("b", "SELECT id FROM table_b")
3264            .with_cte("combined", "SELECT id FROM a UNION SELECT id FROM b")
3265            .build(DbType::MySQL);
3266        assert!(sql.starts_with(
3267            "WITH a AS (SELECT id FROM table_a), b AS (SELECT id FROM table_b), combined AS ("
3268        ));
3269    }
3270
3271    #[test]
3272    fn test_cte_recursive_with_clause() {
3273        let sql = Query::select()
3274            .column("id")
3275            .column("parent_id")
3276            .from("tree")
3277            .with_recursive_cte("tree", "SELECT id, parent_id FROM nodes WHERE id = 1")
3278            .build(DbType::MySQL);
3279        assert!(sql.starts_with("WITH RECURSIVE tree AS ("));
3280    }
3281
3282    #[test]
3283    fn test_cte_no_cte_no_with_prefix() {
3284        let sql = Query::select()
3285            .column("id")
3286            .from("users")
3287            .build(DbType::MySQL);
3288        assert!(!sql.contains("WITH"));
3289        assert!(sql.starts_with("SELECT"));
3290    }
3291
3292    // ---- 窗口函数测试 ----
3293
3294    #[test]
3295    fn test_window_function_raw_expr() {
3296        let sql = Query::select()
3297            .column("id")
3298            .column("salary")
3299            .from("employees")
3300            .window_function("ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn")
3301            .build(DbType::MySQL);
3302        assert!(sql.contains("ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn"));
3303    }
3304
3305    #[test]
3306    fn test_row_number_helper_with_partition() {
3307        let sql = Query::select()
3308            .column("name")
3309            .column("dept")
3310            .from("employees")
3311            .row_number("dept", "salary DESC", "row_num")
3312            .build(DbType::MySQL);
3313        assert!(
3314            sql.contains("ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS row_num")
3315        );
3316    }
3317
3318    #[test]
3319    fn test_row_number_helper_without_partition() {
3320        let sql = Query::select()
3321            .column("name")
3322            .from("employees")
3323            .row_number("", "salary DESC", "rn")
3324            .build(DbType::MySQL);
3325        assert!(sql.contains("ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn"));
3326        assert!(!sql.contains("PARTITION BY"));
3327    }
3328
3329    #[test]
3330    fn test_rank_helper() {
3331        let sql = Query::select()
3332            .column("name")
3333            .from("scores")
3334            .rank("", "score DESC", "rank_num")
3335            .build(DbType::MySQL);
3336        assert!(sql.contains("RANK() OVER (ORDER BY score DESC) AS rank_num"));
3337    }
3338
3339    #[test]
3340    fn test_dense_rank_helper_with_partition() {
3341        let sql = Query::select()
3342            .column("name")
3343            .from("scores")
3344            .dense_rank("class", "score DESC", "dr")
3345            .build(DbType::MySQL);
3346        assert!(sql.contains("DENSE_RANK() OVER (PARTITION BY class ORDER BY score DESC) AS dr"));
3347    }
3348
3349    #[test]
3350    fn test_multiple_window_functions() {
3351        let sql = Query::select()
3352            .column("name")
3353            .column("salary")
3354            .from("employees")
3355            .row_number("dept", "salary DESC", "rn")
3356            .rank("dept", "salary DESC", "rk")
3357            .dense_rank("dept", "salary DESC", "dr")
3358            .build(DbType::MySQL);
3359        assert!(sql.contains("ROW_NUMBER()"));
3360        assert!(sql.contains("RANK()"));
3361        assert!(sql.contains("DENSE_RANK()"));
3362    }
3363
3364    #[test]
3365    fn test_window_function_with_cte_combined() {
3366        let sql = Query::select()
3367            .column("name")
3368            .from("ranked")
3369            .with_cte(
3370                "ranked",
3371                "SELECT name, ROW_NUMBER() OVER (ORDER BY salary) AS rn FROM employees",
3372            )
3373            .where_clause("rn <= 10")
3374            .build(DbType::MySQL);
3375        assert!(sql.starts_with("WITH ranked AS ("));
3376        assert!(sql.contains("FROM `ranked`"));
3377        assert!(sql.contains("WHERE rn <= 10"));
3378    }
3379
3380    // ---- FOR UPDATE 测试 ----
3381
3382    #[test]
3383    fn test_for_update_basic() {
3384        let sql = Query::select()
3385            .column("id")
3386            .column("balance")
3387            .from("accounts")
3388            .where_clause("id = 1")
3389            .for_update()
3390            .build(DbType::MySQL);
3391        assert!(sql.ends_with(" FOR UPDATE"));
3392        assert!(sql.contains("WHERE id = 1"));
3393    }
3394
3395    #[test]
3396    fn test_for_update_with_nowait() {
3397        let sql = Query::select()
3398            .column("id")
3399            .from("accounts")
3400            .where_clause("id = 1")
3401            .for_update_with_options("NOWAIT")
3402            .build(DbType::MySQL);
3403        assert!(sql.ends_with(" FOR UPDATE NOWAIT"));
3404    }
3405
3406    #[test]
3407    fn test_for_update_with_skip_locked() {
3408        let sql = Query::select()
3409            .column("id")
3410            .from("accounts")
3411            .where_clause("id = 1")
3412            .for_update_with_options("SKIP LOCKED")
3413            .build(DbType::MySQL);
3414        assert!(sql.ends_with(" FOR UPDATE SKIP LOCKED"));
3415    }
3416
3417    #[test]
3418    fn test_for_update_with_limit_and_order() {
3419        let sql = Query::select()
3420            .column("id")
3421            .from("jobs")
3422            .order_by("priority", false)
3423            .limit(1)
3424            .for_update_with_options("SKIP LOCKED")
3425            .build(DbType::MySQL);
3426        assert!(sql.contains("ORDER BY `priority` DESC"));
3427        assert!(sql.contains("LIMIT 1"));
3428        assert!(sql.ends_with(" FOR UPDATE SKIP LOCKED"));
3429    }
3430
3431    #[test]
3432    fn test_no_for_update_by_default() {
3433        let sql = Query::select()
3434            .column("id")
3435            .from("users")
3436            .build(DbType::MySQL);
3437        assert!(!sql.contains("FOR UPDATE"));
3438    }
3439
3440    // ---- 集合运算(UNION / INTERSECT / EXCEPT)测试 ----
3441
3442    #[test]
3443    fn test_set_operator_as_sql() {
3444        assert_eq!(SetOperator::Union.as_sql(), "UNION");
3445        assert_eq!(SetOperator::UnionAll.as_sql(), "UNION ALL");
3446        assert_eq!(SetOperator::Intersect.as_sql(), "INTERSECT");
3447        assert_eq!(SetOperator::Except.as_sql(), "EXCEPT");
3448    }
3449
3450    #[test]
3451    fn test_union_basic() {
3452        let q1 = Query::select().column("id").from("active_users");
3453        let q2 = Query::select().column("id").from("pending_users");
3454        let sql = q1.union(q2).build(DbType::MySQL);
3455        assert!(sql.contains("SELECT `id` FROM `active_users`"));
3456        assert!(sql.contains(" UNION "));
3457        assert!(sql.contains("SELECT `id` FROM `pending_users`"));
3458    }
3459
3460    #[test]
3461    fn test_union_all_basic() {
3462        let q1 = Query::select().column("id").from("table_a");
3463        let q2 = Query::select().column("id").from("table_b");
3464        let sql = q1.union_all(q2).build(DbType::MySQL);
3465        assert!(sql.contains(" UNION ALL "));
3466    }
3467
3468    #[test]
3469    fn test_intersect_basic() {
3470        let q1 = Query::select().column("id").from("table_a");
3471        let q2 = Query::select().column("id").from("table_b");
3472        let sql = q1.intersect(q2).build(DbType::MySQL);
3473        assert!(sql.contains(" INTERSECT "));
3474    }
3475
3476    #[test]
3477    fn test_except_basic() {
3478        let q1 = Query::select().column("id").from("table_a");
3479        let q2 = Query::select().column("id").from("table_b");
3480        let sql = q1.except(q2).build(DbType::MySQL);
3481        assert!(sql.contains(" EXCEPT "));
3482    }
3483
3484    #[test]
3485    fn test_union_chained_multiple() {
3486        let q1 = Query::select().column("id").from("t1");
3487        let q2 = Query::select().column("id").from("t2");
3488        let q3 = Query::select().column("id").from("t3");
3489        let sql = q1.union(q2).union(q3).build(DbType::MySQL);
3490        assert_eq!(sql.matches("UNION").count(), 2);
3491    }
3492
3493    #[test]
3494    fn test_union_mixed_operators() {
3495        let q1 = Query::select().column("id").from("t1");
3496        let q2 = Query::select().column("id").from("t2");
3497        let q3 = Query::select().column("id").from("t3");
3498        let sql = q1.union(q2).intersect(q3).build(DbType::MySQL);
3499        assert!(sql.contains(" UNION "));
3500        assert!(sql.contains(" INTERSECT "));
3501    }
3502
3503    #[test]
3504    fn test_union_with_order_by_limit() {
3505        let q1 = Query::select().column("id").from("t1");
3506        let q2 = Query::select().column("id").from("t2");
3507        let sql = q1
3508            .union(q2)
3509            .order_by("id", true)
3510            .limit(10)
3511            .offset(5)
3512            .build(DbType::MySQL);
3513        assert!(sql.contains("ORDER BY `id` ASC"));
3514        assert!(sql.contains("LIMIT 10"));
3515        assert!(sql.contains("OFFSET 5"));
3516    }
3517
3518    #[test]
3519    fn test_union_postgres_dialect() {
3520        let q1 = Query::select().column("id").from("t1");
3521        let q2 = Query::select().column("id").from("t2");
3522        let sql = q1.union(q2).build(DbType::PostgreSQL);
3523        assert!(sql.contains("\"id\""));
3524        assert!(sql.contains(" UNION "));
3525    }
3526
3527    #[test]
3528    fn test_union_with_where_clauses() {
3529        let q1 = Query::select()
3530            .column("id")
3531            .from("active_users")
3532            .where_clause("age > 18");
3533        let q2 = Query::select()
3534            .column("id")
3535            .from("pending_users")
3536            .where_clause("age > 18");
3537        let sql = q1.union(q2).build(DbType::MySQL);
3538        assert!(sql.contains("WHERE age > 18"));
3539        assert!(sql.contains(" UNION "));
3540    }
3541
3542    // ---- 综合场景测试 ----
3543
3544    #[test]
3545    fn test_cte_window_for_update_combined() {
3546        // 复杂查询:CTE + 窗口函数 + FOR UPDATE
3547        let sql = Query::select()
3548            .column("id")
3549            .column("salary")
3550            .from("ranked_salaries")
3551            .with_cte(
3552                "ranked_salaries",
3553                "SELECT id, salary, ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn FROM employees",
3554            )
3555            .where_clause("rn = 1")
3556            .for_update()
3557            .build(DbType::MySQL);
3558        assert!(sql.starts_with("WITH ranked_salaries AS ("));
3559        assert!(sql.contains("FOR UPDATE"));
3560        assert!(sql.contains("WHERE rn = 1"));
3561    }
3562
3563    #[test]
3564    fn test_complex_window_aggregation() {
3565        // 运行总和 + 排名
3566        let sql = Query::select()
3567            .column("user_id")
3568            .column("amount")
3569            .from("transactions")
3570            .window_function(
3571                "SUM(amount) OVER (PARTITION BY user_id ORDER BY created_at) AS running_total",
3572            )
3573            .rank("user_id", "created_at", "tx_rank")
3574            .build(DbType::MySQL);
3575        assert!(sql.contains(
3576            "SUM(amount) OVER (PARTITION BY user_id ORDER BY created_at) AS running_total"
3577        ));
3578        assert!(sql.contains("RANK() OVER (PARTITION BY user_id ORDER BY created_at) AS tx_rank"));
3579    }
3580
3581    // ---- 参数化 WHERE 测试(P0 修复:SQL 注入防护) ----
3582
3583    #[test]
3584    fn test_select_where_eq_params() {
3585        use sz_orm_core::Value;
3586        let built = Query::select()
3587            .column("id")
3588            .from("users")
3589            .where_eq("age", Value::I32(18))
3590            .build_with_params(DbType::MySQL);
3591        assert!(built.sql.contains("WHERE `age` = ?"));
3592        assert_eq!(built.params.len(), 1);
3593        assert_eq!(built.params[0], Value::I32(18));
3594    }
3595
3596    #[test]
3597    fn test_select_multiple_where_params() {
3598        use sz_orm_core::Value;
3599        let built = Query::select()
3600            .column("id")
3601            .from("users")
3602            .where_eq("age", Value::I32(18))
3603            .where_eq("status", Value::String("active".to_string()))
3604            .build_with_params(DbType::MySQL);
3605        assert!(built.sql.contains("WHERE `age` = ? AND `status` = ?"));
3606        assert_eq!(built.params.len(), 2);
3607    }
3608
3609    #[test]
3610    fn test_select_or_where_eq_params() {
3611        use sz_orm_core::Value;
3612        let built = Query::select()
3613            .column("id")
3614            .from("users")
3615            .where_eq("age", Value::I32(18))
3616            .or_where_eq("role", Value::String("admin".to_string()))
3617            .build_with_params(DbType::MySQL);
3618        assert!(built.sql.contains("WHERE `age` = ? OR `role` = ?"));
3619        assert_eq!(built.params.len(), 2);
3620    }
3621
3622    #[test]
3623    fn test_select_where_in_params() {
3624        use sz_orm_core::Value;
3625        let built = Query::select()
3626            .column("id")
3627            .from("users")
3628            .where_in("id", vec![Value::I32(1), Value::I32(2), Value::I32(3)])
3629            .build_with_params(DbType::MySQL);
3630        assert!(built.sql.contains("WHERE `id` IN (?, ?, ?)"));
3631        assert_eq!(built.params.len(), 3);
3632    }
3633
3634    #[test]
3635    fn test_select_where_in_empty() {
3636        let built = Query::select()
3637            .column("id")
3638            .from("users")
3639            .where_in("id", vec![])
3640            .build_with_params(DbType::MySQL);
3641        assert!(built.sql.contains("WHERE 1 = 0"));
3642        assert_eq!(built.params.len(), 0);
3643    }
3644
3645    #[test]
3646    fn test_select_where_between_params() {
3647        use sz_orm_core::Value;
3648        let built = Query::select()
3649            .column("id")
3650            .from("users")
3651            .where_between("age", Value::I32(18), Value::I32(65))
3652            .build_with_params(DbType::MySQL);
3653        assert!(built.sql.contains("WHERE `age` BETWEEN ? AND ?"));
3654        assert_eq!(built.params.len(), 2);
3655    }
3656
3657    #[test]
3658    fn test_select_where_null_params() {
3659        let built = Query::select()
3660            .column("id")
3661            .from("users")
3662            .where_null("deleted_at")
3663            .build_with_params(DbType::MySQL);
3664        assert!(built.sql.contains("WHERE `deleted_at` IS NULL"));
3665        assert_eq!(built.params.len(), 0);
3666    }
3667
3668    #[test]
3669    fn test_select_where_not_null_params() {
3670        let built = Query::select()
3671            .column("id")
3672            .from("users")
3673            .where_not_null("email")
3674            .build_with_params(DbType::MySQL);
3675        assert!(built.sql.contains("WHERE `email` IS NOT NULL"));
3676    }
3677
3678    #[test]
3679    fn test_select_mixed_raw_and_param_where() {
3680        use sz_orm_core::Value;
3681        let built = Query::select()
3682            .column("id")
3683            .from("users")
3684            .where_clause("age > 18")
3685            .where_eq("status", Value::String("active".to_string()))
3686            .build_with_params(DbType::MySQL);
3687        assert!(built.sql.contains("WHERE age > 18 AND `status` = ?"));
3688        assert_eq!(built.params.len(), 1);
3689    }
3690
3691    #[test]
3692    fn test_select_param_where_with_order_limit() {
3693        use sz_orm_core::Value;
3694        let built = Query::select()
3695            .column("id")
3696            .from("users")
3697            .where_eq("age", Value::I32(18))
3698            .order_by("id", true)
3699            .limit(10)
3700            .build_with_params(DbType::MySQL);
3701        assert!(built.sql.contains("WHERE `age` = ?"));
3702        assert!(built.sql.contains("ORDER BY `id` ASC"));
3703        assert!(built.sql.contains("LIMIT 10"));
3704    }
3705
3706    #[test]
3707    fn test_select_param_where_postgres_dialect() {
3708        use sz_orm_core::Value;
3709        let built = Query::select()
3710            .column("id")
3711            .from("users")
3712            .where_eq("age", Value::I32(18))
3713            .build_with_params(DbType::PostgreSQL);
3714        assert!(built.sql.contains("WHERE \"age\" = ?"));
3715    }
3716
3717    #[test]
3718    fn test_select_param_where_injection_safe() {
3719        // 参数化查询下,恶意输入作为参数绑定,不会改变 SQL 结构
3720        use sz_orm_core::Value;
3721        let malicious = "'; DROP TABLE users; --".to_string();
3722        let built = Query::select()
3723            .column("id")
3724            .from("users")
3725            .where_eq("name", Value::String(malicious.clone()))
3726            .build_with_params(DbType::MySQL);
3727        // SQL 结构不含恶意输入(仅含 ? 占位符)
3728        assert!(!built.sql.contains("DROP TABLE"));
3729        assert!(!built.sql.contains(";"));
3730        // 恶意输入完整保留在参数中(由驱动层转义)
3731        assert_eq!(built.params.len(), 1);
3732        assert_eq!(built.params[0], Value::String(malicious));
3733    }
3734
3735    #[test]
3736    fn test_update_where_eq_params() {
3737        use sz_orm_core::Value;
3738        let built = Query::update()
3739            .table("users")
3740            .set("name", "'Bob'")
3741            .where_eq("id", Value::I64(1))
3742            .build_with_params(DbType::MySQL);
3743        assert!(built.sql.contains("UPDATE `users` SET"));
3744        assert!(built.sql.contains("WHERE `id` = ?"));
3745        assert_eq!(built.params.len(), 1);
3746    }
3747
3748    #[test]
3749    fn test_update_where_in_params() {
3750        use sz_orm_core::Value;
3751        let built = Query::update()
3752            .table("users")
3753            .set("status", "'inactive'")
3754            .where_in("id", vec![Value::I64(1), Value::I64(2)])
3755            .build_with_params(DbType::MySQL);
3756        assert!(built.sql.contains("WHERE `id` IN (?, ?)"));
3757        assert_eq!(built.params.len(), 2);
3758    }
3759
3760    #[test]
3761    fn test_delete_where_eq_params() {
3762        use sz_orm_core::Value;
3763        let built = Query::delete()
3764            .from_table("users")
3765            .where_eq("id", Value::I64(1))
3766            .build_with_params(DbType::MySQL);
3767        assert!(built.sql.contains("DELETE FROM `users`"));
3768        assert!(built.sql.contains("WHERE `id` = ?"));
3769        assert_eq!(built.params.len(), 1);
3770    }
3771
3772    #[test]
3773    fn test_delete_where_between_params() {
3774        use sz_orm_core::Value;
3775        let built = Query::delete()
3776            .from_table("logs")
3777            .where_between(
3778                "created_at",
3779                Value::String("2020-01-01".to_string()),
3780                Value::String("2020-12-31".to_string()),
3781            )
3782            .build_with_params(DbType::MySQL);
3783        assert!(built.sql.contains("WHERE `created_at` BETWEEN ? AND ?"));
3784        assert_eq!(built.params.len(), 2);
3785    }
3786
3787    // ---- 任务4:FROM 子查询支持测试 ----
3788
3789    #[test]
3790    fn test_from_subquery_basic() {
3791        let inner = Query::select()
3792            .column("id")
3793            .column("amount")
3794            .from("orders")
3795            .build(DbType::MySQL);
3796        let sql = Query::select()
3797            .column("id")
3798            .from_subquery(&inner, "t")
3799            .build(DbType::MySQL);
3800        assert!(
3801            sql.contains("FROM (SELECT `id`, `amount` FROM `orders`) AS `t`"),
3802            "FROM 子查询应渲染为 `FROM (subquery) AS alias`,实际: {sql}"
3803        );
3804    }
3805
3806    #[test]
3807    fn test_from_subquery_postgres_dialect() {
3808        let inner = Query::select()
3809            .column("id")
3810            .from("orders")
3811            .build(DbType::PostgreSQL);
3812        let sql = Query::select()
3813            .column("id")
3814            .from_subquery(&inner, "t")
3815            .build(DbType::PostgreSQL);
3816        assert!(
3817            sql.contains("FROM (SELECT \"id\" FROM \"orders\") AS \"t\""),
3818            "PG 方言下别名应使用双引号,实际: {sql}"
3819        );
3820    }
3821
3822    #[test]
3823    fn test_from_subquery_with_where_and_order() {
3824        let inner = Query::select()
3825            .column("id")
3826            .column("amount")
3827            .from("orders")
3828            .where_clause("amount > 100")
3829            .build(DbType::MySQL);
3830        let sql = Query::select()
3831            .column("id")
3832            .column("amount")
3833            .from_subquery(&inner, "t")
3834            .where_clause("t.amount > 200")
3835            .order_by("id", true)
3836            .build(DbType::MySQL);
3837        assert!(
3838            sql.contains("FROM (SELECT `id`, `amount` FROM `orders` WHERE amount > 100) AS `t`")
3839        );
3840        assert!(sql.contains("WHERE t.amount > 200"));
3841        assert!(sql.contains("ORDER BY `id` ASC"));
3842    }
3843
3844    #[test]
3845    fn test_from_subquery_with_params() {
3846        use sz_orm_core::Value;
3847        let inner = Query::select()
3848            .column("id")
3849            .from("orders")
3850            .where_eq("amount", Value::I32(100))
3851            .build_with_params(DbType::MySQL);
3852        let built = Query::select()
3853            .column("id")
3854            .from_subquery(&inner.sql, "t")
3855            .where_eq("t.id", Value::I64(1))
3856            .build_with_params(DbType::MySQL);
3857        // 外层参数 + 内层参数(外层参数在 WHERE 之后,但内层 SQL 已固化为字符串)
3858        assert!(built
3859            .sql
3860            .contains("FROM (SELECT `id` FROM `orders` WHERE `amount` = ?) AS `t`"));
3861        assert!(built.sql.contains("WHERE `t`.`id` = ?"));
3862        // 外层只绑定 1 个参数(内层 SQL 已是字符串)
3863        assert_eq!(built.params.len(), 1);
3864    }
3865
3866    #[test]
3867    fn test_from_subquery_overrides_from_table() {
3868        // 后调用者覆盖前者:from() 后 from_subquery()
3869        let sql = Query::select()
3870            .column("id")
3871            .from("users")
3872            .from_subquery("SELECT id FROM orders", "t")
3873            .build(DbType::MySQL);
3874        assert!(sql.contains("FROM (SELECT id FROM orders) AS `t`"));
3875        assert!(!sql.contains("FROM `users`"));
3876    }
3877
3878    #[test]
3879    fn test_from_table_overrides_from_subquery() {
3880        // 后调用者覆盖前者:from_subquery() 后 from()
3881        let sql = Query::select()
3882            .column("id")
3883            .from_subquery("SELECT id FROM orders", "t")
3884            .from("users")
3885            .build(DbType::MySQL);
3886        assert!(sql.contains("FROM `users`"));
3887        assert!(!sql.contains("FROM ("));
3888    }
3889
3890    #[test]
3891    fn test_from_subquery_no_from_when_neither_set() {
3892        let sql = Query::select().column("id").build(DbType::MySQL);
3893        assert!(!sql.contains("FROM"));
3894    }
3895
3896    // ---- 任务5:RETURNING 子句支持测试 ----
3897
3898    #[test]
3899    fn test_insert_returning_postgres() {
3900        let sql = Query::insert()
3901            .into_table("users")
3902            .value("name", "'Alice'")
3903            .returning(&["id", "created_at"])
3904            .build_with_dialect(DbType::PostgreSQL);
3905        assert!(
3906            sql.contains("RETURNING \"id\", \"created_at\""),
3907            "PG 方言应渲染 RETURNING,实际: {sql}"
3908        );
3909    }
3910
3911    #[test]
3912    fn test_insert_returning_sqlite() {
3913        let sql = Query::insert()
3914            .into_table("users")
3915            .value("name", "'Alice'")
3916            .returning(&["id"])
3917            .build_with_dialect(DbType::Sqlite);
3918        assert!(
3919            sql.contains("RETURNING \"id\""),
3920            "SQLite 方言应渲染 RETURNING,实际: {sql}"
3921        );
3922    }
3923
3924    #[test]
3925    fn test_insert_returning_all() {
3926        let sql = Query::insert()
3927            .into_table("users")
3928            .value("name", "'Alice'")
3929            .returning_all()
3930            .build_with_dialect(DbType::PostgreSQL);
3931        assert!(
3932            sql.contains("RETURNING *"),
3933            "returning_all 应渲染 `RETURNING *`,实际: {sql}"
3934        );
3935    }
3936
3937    #[test]
3938    fn test_insert_returning_mysql_skipped() {
3939        // MySQL 不支持 RETURNING,应跳过
3940        let sql = Query::insert()
3941            .into_table("users")
3942            .value("name", "'Alice'")
3943            .returning(&["id"])
3944            .build_with_dialect(DbType::MySQL);
3945        assert!(
3946            !sql.contains("RETURNING"),
3947            "MySQL 方言应跳过 RETURNING,实际: {sql}"
3948        );
3949    }
3950
3951    #[test]
3952    fn test_insert_returning_with_upsert_postgres() {
3953        // ON CONFLICT + RETURNING 组合(PostgreSQL 经典用法)
3954        let sql = Query::insert()
3955            .into_table("users")
3956            .value("id", "1")
3957            .value("name", "'Alice'")
3958            .on_conflict_do_update(&["id"], &[("name", "EXCLUDED.name")])
3959            .returning(&["id", "name"])
3960            .build_with_dialect(DbType::PostgreSQL);
3961        assert!(sql.contains("ON CONFLICT"));
3962        assert!(sql.contains("RETURNING"));
3963    }
3964
3965    #[test]
3966    fn test_insert_returning_build_mysql_style_skipped() {
3967        // build() 是 MySQL 风格硬编码,不应渲染 RETURNING
3968        let sql = Query::insert()
3969            .into_table("users")
3970            .value("name", "'Alice'")
3971            .returning(&["id"])
3972            .build();
3973        assert!(!sql.contains("RETURNING"));
3974    }
3975
3976    #[test]
3977    fn test_update_returning_postgres() {
3978        let sql = Query::update()
3979            .table("users")
3980            .set("status", "'active'")
3981            .where_clause("id = 1")
3982            .returning(&["id", "status"])
3983            .build_with_dialect(DbType::PostgreSQL);
3984        assert!(sql.contains("RETURNING \"id\", \"status\""));
3985        assert!(sql.contains("WHERE id = 1"));
3986    }
3987
3988    #[test]
3989    fn test_update_returning_sqlite() {
3990        let sql = Query::update()
3991            .table("users")
3992            .set("status", "'active'")
3993            .returning(&["id"])
3994            .build_with_dialect(DbType::Sqlite);
3995        assert!(sql.contains("RETURNING \"id\""));
3996    }
3997
3998    #[test]
3999    fn test_update_returning_mysql_skipped() {
4000        let sql = Query::update()
4001            .table("users")
4002            .set("status", "'active'")
4003            .returning(&["id"])
4004            .build_with_dialect(DbType::MySQL);
4005        assert!(!sql.contains("RETURNING"));
4006    }
4007
4008    #[test]
4009    fn test_update_returning_with_params() {
4010        use sz_orm_core::Value;
4011        let built = Query::update()
4012            .table("users")
4013            .set("status", "'active'")
4014            .where_eq("id", Value::I64(1))
4015            .returning(&["id", "status"])
4016            .build_with_params(DbType::PostgreSQL);
4017        assert!(built.sql.contains("WHERE \"id\" = ?"));
4018        assert!(built.sql.contains("RETURNING \"id\", \"status\""));
4019        assert_eq!(built.params.len(), 1);
4020    }
4021
4022    #[test]
4023    fn test_delete_returning_postgres() {
4024        let sql = Query::delete()
4025            .from_table("users")
4026            .where_clause("id = 1")
4027            .returning(&["id", "name"])
4028            .build_with_dialect(DbType::PostgreSQL);
4029        assert!(sql.contains("RETURNING \"id\", \"name\""));
4030        assert!(sql.contains("WHERE id = 1"));
4031    }
4032
4033    #[test]
4034    fn test_delete_returning_sqlite() {
4035        let sql = Query::delete()
4036            .from_table("users")
4037            .where_clause("id = 1")
4038            .returning(&["id"])
4039            .build_with_dialect(DbType::Sqlite);
4040        assert!(sql.contains("RETURNING \"id\""));
4041    }
4042
4043    #[test]
4044    fn test_delete_returning_mysql_skipped() {
4045        let sql = Query::delete()
4046            .from_table("users")
4047            .where_clause("id = 1")
4048            .returning(&["id"])
4049            .build_with_dialect(DbType::MySQL);
4050        assert!(!sql.contains("RETURNING"));
4051    }
4052
4053    #[test]
4054    fn test_delete_returning_with_params() {
4055        use sz_orm_core::Value;
4056        let built = Query::delete()
4057            .from_table("users")
4058            .where_eq("id", Value::I64(1))
4059            .returning(&["id", "name"])
4060            .build_with_params(DbType::PostgreSQL);
4061        assert!(built.sql.contains("WHERE \"id\" = ?"));
4062        assert!(built.sql.contains("RETURNING \"id\", \"name\""));
4063        assert_eq!(built.params.len(), 1);
4064    }
4065
4066    #[test]
4067    fn test_returning_star_not_quoted() {
4068        // `*` 在 RETURNING 中不应被引号包裹
4069        let sql = Query::insert()
4070            .into_table("users")
4071            .value("name", "'Alice'")
4072            .returning(&["*"])
4073            .build_with_dialect(DbType::PostgreSQL);
4074        assert!(sql.contains("RETURNING *"));
4075        assert!(!sql.contains("RETURNING \"*\""));
4076    }
4077
4078    // ---- 参数化 JOIN 测试(P2 修复 #68:JOIN 注入风险) ----
4079
4080    #[test]
4081    fn test_inner_join_on_column_eq() {
4082        let sql = Query::select()
4083            .column("u.id")
4084            .from("users u")
4085            .inner_join_on("orders o", "u.id", "o.user_id")
4086            .build(DbType::MySQL);
4087        assert!(
4088            sql.contains("INNER JOIN `orders` o ON `u`.`id` = `o`.`user_id`"),
4089            "列对列等值连接应渲染转义标识符,实际: {sql}"
4090        );
4091    }
4092
4093    #[test]
4094    fn test_left_join_on_column_eq() {
4095        let sql = Query::select()
4096            .column("u.id")
4097            .from("users u")
4098            .left_join_on("profiles p", "u.id", "p.user_id")
4099            .build(DbType::MySQL);
4100        assert!(sql.contains("LEFT JOIN `profiles` p ON `u`.`id` = `p`.`user_id`"));
4101    }
4102
4103    #[test]
4104    fn test_right_join_on_column_eq() {
4105        let sql = Query::select()
4106            .column("u.id")
4107            .from("users u")
4108            .right_join_on("orders o", "u.id", "o.user_id")
4109            .build(DbType::MySQL);
4110        assert!(sql.contains("RIGHT JOIN `orders` o ON `u`.`id` = `o`.`user_id`"));
4111    }
4112
4113    #[test]
4114    fn test_inner_join_on_postgres_dialect() {
4115        let sql = Query::select()
4116            .column("u.id")
4117            .from("users u")
4118            .inner_join_on("orders o", "u.id", "o.user_id")
4119            .build(DbType::PostgreSQL);
4120        // 注:quote_join_table 仍使用反引号包裹表名(向后兼容),
4121        // 但 ON 条件的列名按 PG 方言用双引号引用
4122        assert!(
4123            sql.contains("INNER JOIN `orders` o ON \"u\".\"id\" = \"o\".\"user_id\""),
4124            "PG 方言下 ON 条件列名应使用双引号引用,实际: {sql}"
4125        );
4126    }
4127
4128    #[test]
4129    fn test_inner_join_param_binds_value() {
4130        use sz_orm_core::Value;
4131        let built = Query::select()
4132            .column("u.id")
4133            .from("users u")
4134            .inner_join_param("orders o", "o.status", " = ?", Value::String("paid".into()))
4135            .build_with_params(DbType::MySQL);
4136        assert!(
4137            built
4138                .sql
4139                .contains("INNER JOIN `orders` o ON `o`.`status` = ?"),
4140            "参数化 JOIN 应渲染 ? 占位符,实际: {}",
4141            built.sql
4142        );
4143        assert_eq!(built.params.len(), 1);
4144        assert_eq!(built.params[0], Value::String("paid".to_string()));
4145    }
4146
4147    #[test]
4148    fn test_left_join_param_binds_value() {
4149        use sz_orm_core::Value;
4150        let built = Query::select()
4151            .column("u.id")
4152            .from("users u")
4153            .left_join_param("orders o", "o.status", " = ?", Value::String("paid".into()))
4154            .build_with_params(DbType::MySQL);
4155        assert!(built
4156            .sql
4157            .contains("LEFT JOIN `orders` o ON `o`.`status` = ?"));
4158        assert_eq!(built.params.len(), 1);
4159    }
4160
4161    #[test]
4162    fn test_right_join_param_binds_value() {
4163        use sz_orm_core::Value;
4164        let built = Query::select()
4165            .column("u.id")
4166            .from("users u")
4167            .right_join_param("orders o", "o.status", " = ?", Value::String("paid".into()))
4168            .build_with_params(DbType::MySQL);
4169        assert!(built
4170            .sql
4171            .contains("RIGHT JOIN `orders` o ON `o`.`status` = ?"));
4172        assert_eq!(built.params.len(), 1);
4173    }
4174
4175    #[test]
4176    fn test_param_join_injection_safe() {
4177        // 参数化 JOIN 下,恶意输入作为参数绑定,不会改变 SQL 结构
4178        use sz_orm_core::Value;
4179        let malicious = "'; DROP TABLE orders; --".to_string();
4180        let built = Query::select()
4181            .column("u.id")
4182            .from("users u")
4183            .inner_join_param(
4184                "orders o",
4185                "o.status",
4186                " = ?",
4187                Value::String(malicious.clone()),
4188            )
4189            .build_with_params(DbType::MySQL);
4190        // SQL 结构不含恶意输入(仅含 ? 占位符)
4191        assert!(!built.sql.contains("DROP TABLE"));
4192        assert!(!built.sql.contains(";"));
4193        // 恶意输入完整保留在参数中(由驱动层转义)
4194        assert_eq!(built.params.len(), 1);
4195        assert_eq!(built.params[0], Value::String(malicious));
4196    }
4197
4198    #[test]
4199    fn test_mixed_raw_and_param_join() {
4200        // 混合使用原始 inner_join 和参数化 inner_join_param
4201        use sz_orm_core::Value;
4202        let built = Query::select()
4203            .column("u.id")
4204            .from("users u")
4205            .inner_join("orders o", "u.id = o.user_id")
4206            .inner_join_param(
4207                "payments p",
4208                "p.status",
4209                " = ?",
4210                Value::String("paid".into()),
4211            )
4212            .build_with_params(DbType::MySQL);
4213        assert!(built
4214            .sql
4215            .contains("INNER JOIN `orders` o ON u.id = o.user_id"));
4216        assert!(built
4217            .sql
4218            .contains("INNER JOIN `payments` p ON `p`.`status` = ?"));
4219        assert_eq!(built.params.len(), 1);
4220    }
4221
4222    #[test]
4223    fn test_param_join_with_where_params_combined() {
4224        // 参数化 JOIN + 参数化 WHERE 联合使用
4225        use sz_orm_core::Value;
4226        let built = Query::select()
4227            .column("u.id")
4228            .from("users u")
4229            .inner_join_param("orders o", "o.status", " = ?", Value::String("paid".into()))
4230            .where_eq("u.age", Value::I32(18))
4231            .build_with_params(DbType::MySQL);
4232        assert!(built
4233            .sql
4234            .contains("INNER JOIN `orders` o ON `o`.`status` = ?"));
4235        assert!(built.sql.contains("WHERE `u`.`age` = ?"));
4236        // JOIN 参数在前,WHERE 参数在后
4237        assert_eq!(built.params.len(), 2);
4238        assert_eq!(built.params[0], Value::String("paid".to_string()));
4239        assert_eq!(built.params[1], Value::I32(18));
4240    }
4241}