Skip to main content

sz_orm_ai/
nl2sql.rs

1//! NL→SQL(自然语言转 SQL)模块
2//!
3//! 提供将自然语言查询转换为 SQL 语句的能力,支持:
4//! - 内存模拟引擎 [`SimpleNl2SqlEngine`](基于规则匹配,无需外部 API)
5//! - 真实 LLM 引擎 [`OpenAINl2SqlEngine`](调用 OpenAI 兼容 API,需 `real` feature)
6//!
7//! 所有生成的 SQL 均经过安全验证:只允许 SELECT 查询,并检测注入风险。
8
9use async_trait::async_trait;
10use thiserror::Error;
11
12use crate::safety;
13
14#[cfg(feature = "real")]
15use serde::{Deserialize, Serialize};
16
17// ==================== 数据结构 ====================
18
19/// NL→SQL 查询结果
20#[derive(Debug, Clone)]
21pub struct SqlQuery {
22    /// 生成的 SQL(使用 $1, $2 等参数化占位符)
23    pub sql: String,
24    /// 生成过程的自然语言解释
25    pub explanation: String,
26    /// 置信度(0.0 ~ 1.0)
27    pub confidence: f32,
28}
29
30/// Schema 上下文,描述数据库中的表和列信息
31#[derive(Debug, Clone, Default)]
32pub struct SchemaContext {
33    pub tables: Vec<TableInfo>,
34}
35
36/// 表信息
37#[derive(Debug, Clone)]
38pub struct TableInfo {
39    pub name: String,
40    pub columns: Vec<ColumnInfo>,
41}
42
43/// 列信息
44#[derive(Debug, Clone)]
45pub struct ColumnInfo {
46    pub name: String,
47    pub data_type: String,
48    pub nullable: bool,
49    pub is_primary_key: bool,
50}
51
52// ==================== 错误类型 ====================
53
54/// NL→SQL 相关错误
55#[derive(Debug, Error)]
56pub enum Nl2SqlError {
57    /// 无法解析自然语言查询
58    #[error("Invalid query: {0}")]
59    InvalidQuery(String),
60    /// Schema 信息不足(表/列不存在)
61    #[error("Schema error: {0}")]
62    SchemaError(String),
63    /// SQL 安全验证失败
64    #[error("Safety error: {0}")]
65    SafetyError(String),
66    /// SQL 生成失败
67    #[error("Generation error: {0}")]
68    GenerationError(String),
69    /// API 调用错误(仅 real feature)
70    #[error("API error (status {0}): {1}")]
71    ApiError(u16, String),
72    /// 网络错误(仅 real feature)
73    #[error("Network error: {0}")]
74    NetworkError(String),
75    /// 配置错误
76    #[error("Config error: {0}")]
77    ConfigError(String),
78}
79
80// ==================== Trait 定义 ====================
81
82/// NL→SQL 引擎 trait
83///
84/// 所有 NL→SQL 实现必须实现此 trait,以保证一致的接口。
85#[async_trait]
86pub trait Nl2SqlEngine: Send + Sync {
87    /// 将自然语言查询转换为 SQL
88    ///
89    /// # 参数
90    /// - `nl_query`: 自然语言查询(如 "show all users where age > 25")
91    /// - `schema`: 数据库 schema 上下文
92    ///
93    /// # 返回值
94    /// - `Ok(SqlQuery)`: 生成的 SQL 查询
95    /// - `Err(Nl2SqlError)`: 转换失败
96    async fn generate(
97        &self,
98        nl_query: &str,
99        schema: &SchemaContext,
100    ) -> Result<SqlQuery, Nl2SqlError>;
101
102    /// 验证生成的 SQL 是否安全可用
103    ///
104    /// 执行以下检查:
105    /// - 只允许 SELECT 语句
106    /// - 无 SQL 注入风险
107    async fn validate(&self, query: &SqlQuery) -> Result<bool, Nl2SqlError>;
108}
109
110// ==================== SimpleNl2SqlEngine ====================
111
112/// 基于规则匹配的 NL→SQL 引擎(内存模拟,无需 LLM API)
113///
114/// 通过关键词匹配和模式识别,将常见自然语言查询转换为 SQL。
115/// 支持以下查询模式:
116/// - 简单 SELECT(`SELECT *` / `SELECT col1, col2`)
117/// - COUNT / COUNT DISTINCT
118/// - 聚合函数(SUM, AVG, MIN, MAX)
119/// - WHERE 条件(=, >, <, >=, <=, !=, LIKE)
120/// - ORDER BY(ASC / DESC)
121/// - GROUP BY
122/// - JOIN(通过外键名称推断关联)
123/// - LIMIT
124///
125/// 所有生成的 SQL 使用 `$1`, `$2` 等参数化占位符防止注入。
126///
127/// # 示例
128///
129/// ```ignore
130/// use sz_orm_ai::nl2sql::{SimpleNl2SqlEngine, Nl2SqlEngine, SchemaContext, TableInfo, ColumnInfo};
131///
132/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
133/// let engine = SimpleNl2SqlEngine::new();
134/// let schema = SchemaContext {
135///     tables: vec![TableInfo {
136///         name: "users".into(),
137///         columns: vec![
138///             ColumnInfo { name: "id".into(), data_type: "INTEGER".into(), nullable: false, is_primary_key: true },
139///             ColumnInfo { name: "name".into(), data_type: "TEXT".into(), nullable: true, is_primary_key: false },
140///         ],
141///     }],
142/// };
143/// let result = engine.generate("show all users", &schema).await?;
144/// assert_eq!(result.sql, "SELECT * FROM users");
145/// # Ok(())
146/// # }
147/// ```
148pub struct SimpleNl2SqlEngine {
149    /// 表别名映射(如 "user" → "users")
150    aliases: std::collections::HashMap<String, String>,
151}
152
153impl Default for SimpleNl2SqlEngine {
154    fn default() -> Self {
155        Self::new()
156    }
157}
158
159impl SimpleNl2SqlEngine {
160    pub fn new() -> Self {
161        Self {
162            aliases: std::collections::HashMap::new(),
163        }
164    }
165
166    /// 注册表别名(例如 `with_alias("user", "users")`)
167    pub fn with_alias(mut self, alias: &str, table: &str) -> Self {
168        self.aliases
169            .insert(alias.to_lowercase(), table.to_lowercase());
170        self
171    }
172
173    /// 在 schema 中查找表名(匹配原名或别名)
174    fn find_table<'a>(&self, query: &str, schema: &'a SchemaContext) -> Option<&'a TableInfo> {
175        let lower = query.to_lowercase();
176
177        // 按名称在 query 中出现的顺序打分
178        let mut best: Option<&TableInfo> = None;
179        let mut best_pos: Option<usize> = None;
180
181        for table in &schema.tables {
182            let name = table.name.to_lowercase();
183            if let Some(pos) = lower.find(&name) {
184                let is_better = match best_pos {
185                    None => true,
186                    Some(best) => pos < best,
187                };
188                if is_better {
189                    best = Some(table);
190                    best_pos = Some(pos);
191                }
192            }
193        }
194
195        // 检查别名:遍历所有别名,看查询中是否包含别名
196        for (alias, target_table_name) in &self.aliases {
197            if let Some(pos) = lower.find(alias) {
198                // 找到对应表
199                if let Some(table) = schema
200                    .tables
201                    .iter()
202                    .find(|t| t.name.to_lowercase() == *target_table_name)
203                {
204                    let is_better = match best_pos {
205                        None => true,
206                        Some(best) => pos < best,
207                    };
208                    if is_better {
209                        best = Some(table);
210                        best_pos = Some(pos);
211                    }
212                }
213            }
214        }
215
216        best
217    }
218
219    /// 只在 SELECT/NL 部分提取列名(排除 WHERE/ORDER BY/GROUP BY 后的部分)
220    fn extract_columns_from_nl(query: &str, table: &TableInfo) -> Vec<String> {
221        let lower = query.to_lowercase();
222        // 截取到第一个子句关键字之前
223        let select_part = if let Some(pos) = lower.find(" where ") {
224            &query[..pos]
225        } else if let Some(pos) = lower.find(" having ") {
226            &query[..pos]
227        } else if let Some(pos) = lower.find(" order by") {
228            &query[..pos]
229        } else if let Some(pos) = lower.find(" sorted by") {
230            &query[..pos]
231        } else if let Some(pos) = lower.find(" sort by") {
232            &query[..pos]
233        } else if let Some(pos) = lower.find(" ordered by") {
234            &query[..pos]
235        } else if let Some(pos) = lower.find(" group by") {
236            &query[..pos]
237        } else {
238            query
239        };
240
241        Self::extract_columns(select_part, table)
242    }
243
244    /// 确认 query 中提到的表名(不含 FROM 子句的上下文检测)
245    fn find_mentioned_table<'a>(
246        &self,
247        query: &str,
248        schema: &'a SchemaContext,
249    ) -> Option<&'a TableInfo> {
250        self.find_table(query, schema)
251    }
252
253    /// 在 query 中查找提及的所有表名(用于 JOIN 检测)
254    fn find_all_tables<'a>(&self, query: &str, schema: &'a SchemaContext) -> Vec<&'a TableInfo> {
255        let lower = query.to_lowercase();
256        let mut found = Vec::new();
257
258        for table in &schema.tables {
259            let name = table.name.to_lowercase();
260            if lower.contains(&name) {
261                found.push(table);
262            }
263        }
264
265        found
266    }
267
268    /// 检查列名是否存在于 schema 的指定表中
269    fn column_exists(col: &str, table: &TableInfo) -> bool {
270        let col_lower = col.to_lowercase();
271        table
272            .columns
273            .iter()
274            .any(|c| c.name.to_lowercase() == col_lower)
275    }
276
277    /// 从 query 中提取列名列表(匹配 schema 中的列)
278    fn extract_columns(query: &str, table: &TableInfo) -> Vec<String> {
279        let lower = query.to_lowercase();
280        let mut columns = Vec::new();
281
282        for col in &table.columns {
283            let col_lower = col.name.to_lowercase();
284            // 避免匹配短列名导致误匹配(如 "id")
285            if col_lower.len() >= 2
286                && (lower.contains(&col_lower) || lower.contains(&format!(" {} ", col_lower)))
287            {
288                // 确认不是 from 子句后的表名列
289                if !columns.contains(&col.name) {
290                    columns.push(col.name.clone());
291                }
292            }
293        }
294
295        columns
296    }
297
298    /// 尝试寻找两个表之间的外键关联列
299    fn find_join_columns(t1: &TableInfo, t2: &TableInfo) -> Option<(String, String)> {
300        // 模式 1: t2 中有 t1_name + _id → t2.t1_name_id = t1.id
301        for col1 in &t1.columns {
302            if col1.is_primary_key {
303                let expected_fk = format!("{}_{}", t2.name, col1.name).to_lowercase();
304                for col2 in &t2.columns {
305                    if col2.name.to_lowercase() == expected_fk {
306                        return Some((
307                            format!("{}.{}", t1.name, col1.name),
308                            format!("{}.{}", t2.name, col2.name),
309                        ));
310                    }
311                }
312            }
313        }
314        // 模式 2: t1 中有 t2_name + _id → t1.t2_name_id = t2.id
315        for col2 in &t2.columns {
316            if col2.is_primary_key {
317                let expected_fk = format!("{}_{}", t1.name, col2.name).to_lowercase();
318                for col1 in &t1.columns {
319                    if col1.name.to_lowercase() == expected_fk {
320                        return Some((
321                            format!("{}.{}", t1.name, col1.name),
322                            format!("{}.{}", t2.name, col2.name),
323                        ));
324                    }
325                }
326            }
327        }
328        None
329    }
330
331    /// 从 WHERE 条件文本中提取参数化条件
332    fn parse_conditions(where_text: &str, table: &TableInfo) -> (Vec<String>, Vec<String>) {
333        let text = where_text.trim();
334        let mut conditions = Vec::new();
335        let mut params = Vec::new();
336        let param_idx = 1;
337
338        // 处理单个条件表达式
339        let expr = text;
340        if expr.is_empty() {
341            return (conditions, params);
342        }
343
344        let operators = [
345            (">=", ">="),
346            ("<=", "<="),
347            ("!=", "!="),
348            ("=", "="),
349            (">", ">"),
350            ("<", "<"),
351        ];
352
353        let mut resolved = false;
354        for (op_str, sql_op) in &operators {
355            if let Some(pos) = expr.find(op_str) {
356                let field = expr[..pos].trim();
357                let raw_val = expr[pos + op_str.len()..].trim();
358
359                if !field.is_empty() && !raw_val.is_empty() {
360                    // 检查 raw_val 是否为列名
361                    if Self::column_exists(raw_val, table) {
362                        conditions.push(format!("{} {} {}", field, sql_op, raw_val));
363                    } else {
364                        let param = format!("${}", param_idx);
365                        conditions.push(format!("{} {} {}", field, sql_op, param));
366                        params.push(raw_val.to_string());
367                    }
368                    resolved = true;
369                }
370                break;
371            }
372        }
373
374        // 处理 LIKE / contains
375        if !resolved {
376            let lower_expr = expr.to_lowercase();
377            if let Some(pos) = lower_expr.find(" like ") {
378                let field = expr[..pos].trim();
379                let raw_val = expr[pos + 6..].trim();
380                if !field.is_empty() && !raw_val.is_empty() {
381                    let param = "$1".to_string();
382                    conditions.push(format!("{} LIKE {}", field, param));
383                    params.push(raw_val.to_string());
384                    resolved = true;
385                }
386            } else if let Some(pos) = lower_expr.find(" contains ") {
387                let field = expr[..pos].trim();
388                let raw_val = expr[pos + 10..].trim();
389                if !field.is_empty() && !raw_val.is_empty() {
390                    let param = "$1".to_string();
391                    conditions.push(format!("{} LIKE '%' || {} || '%'", field, param));
392                    params.push(raw_val.to_string());
393                    resolved = true;
394                }
395            }
396        }
397
398        // 未解析的条件原样保留
399        if !resolved && !expr.is_empty() {
400            conditions.push(expr.to_string());
401        }
402
403        (conditions, params)
404    }
405}
406
407#[async_trait]
408impl Nl2SqlEngine for SimpleNl2SqlEngine {
409    async fn generate(
410        &self,
411        nl_query: &str,
412        schema: &SchemaContext,
413    ) -> Result<SqlQuery, Nl2SqlError> {
414        if nl_query.trim().is_empty() {
415            return Err(Nl2SqlError::InvalidQuery("自然语言查询不能为空".into()));
416        }
417
418        if schema.tables.is_empty() {
419            return Err(Nl2SqlError::SchemaError("Schema 中未定义任何表".into()));
420        }
421
422        let lower = nl_query.to_lowercase();
423        let table = self.find_mentioned_table(nl_query, schema).ok_or_else(|| {
424            Nl2SqlError::SchemaError(format!(
425                "无法在查询中识别表名,schema 包含: {}",
426                schema
427                    .tables
428                    .iter()
429                    .map(|t| t.name.as_str())
430                    .collect::<Vec<_>>()
431                    .join(", ")
432            ))
433        })?;
434
435        // ============ 识别查询类型 ============
436
437        // 是否是 COUNT 查询
438        let is_count =
439            lower.contains("count ") || lower.contains("how many") || lower.contains("number of");
440        let is_distinct = lower.contains("distinct");
441        let has_aggregation = lower.contains("sum ")
442            || lower.contains("total ")
443            || lower.contains("avg ")
444            || lower.contains("average ")
445            || lower.contains("mean ")
446            || lower.contains("min ")
447            || lower.contains("minimum ")
448            || lower.contains("max ")
449            || lower.contains("maximum ")
450            || lower.contains("highest");
451
452        // 提取 ORDER BY、GROUP BY、LIMIT、JOIN
453        let has_order = lower.contains("order by")
454            || lower.contains("sort by")
455            || lower.contains("ordered by")
456            || lower.contains("sorted by");
457        let has_group = lower.contains("group by");
458        let has_limit =
459            lower.contains("limit") || lower.contains("top ") || lower.contains("first ");
460        let has_join =
461            lower.contains(" join ") || lower.contains("combine with") || lower.contains("with ");
462
463        // ============ 提取列(仅从 SELECT/NL 部分提取,排除子句关键字后的部分) ============
464        let columns = Self::extract_columns_from_nl(nl_query, table);
465
466        // ============ 提取 WHERE 条件 ============
467        let where_text = if let Some(pos) = lower.find(" where ") {
468            Some(nl_query[pos + 7..].trim())
469        } else if let Some(pos) = lower.find(" having ") {
470            Some(nl_query[pos + 8..].trim())
471        } else if let Some(pos) = lower.find(" with ") {
472            // "with" 可能表示条件或 JOIN
473            let after = nl_query[pos + 5..].trim();
474            // 如果 with 后跟列名和操作符,视为条件
475            if table
476                .columns
477                .iter()
478                .any(|c| after.to_lowercase().starts_with(&c.name.to_lowercase()))
479            {
480                Some(after)
481            } else {
482                None
483            }
484        } else {
485            None
486        };
487
488        let (conditions, params) = match where_text {
489            Some(text) => {
490                // 去掉 order by / group by / limit 部分
491                let clean_text = if let Some(pos) = text.to_lowercase().find(" order by") {
492                    &text[..pos]
493                } else if let Some(pos) = text.to_lowercase().find(" group by") {
494                    &text[..pos]
495                } else if let Some(pos) = text.to_lowercase().find(" limit") {
496                    &text[..pos]
497                } else {
498                    text
499                };
500                Self::parse_conditions(clean_text, table)
501            }
502            None => (Vec::new(), Vec::new()),
503        };
504
505        // ============ 提取 ORDER BY ============
506        let order_clause: Option<String> = if has_order {
507            let (after_text, is_desc) = if let Some(pos) = lower.find("order by") {
508                let txt = nl_query[pos + 8..].trim().to_string();
509                let d = txt.to_lowercase().contains("desc")
510                    || txt.to_lowercase().contains("descending");
511                (txt, d)
512            } else if let Some(pos) = lower.find("sort by") {
513                let txt = nl_query[pos + 7..].trim().to_string();
514                let d = txt.to_lowercase().contains("desc")
515                    || txt.to_lowercase().contains("descending");
516                (txt, d)
517            } else if let Some(pos) = lower.find("sorted by") {
518                let txt = nl_query[pos + 9..].trim().to_string();
519                let d = txt.to_lowercase().contains("desc")
520                    || txt.to_lowercase().contains("descending");
521                (txt, d)
522            } else if let Some(pos) = lower.find("ordered by") {
523                let txt = nl_query[pos + 10..].trim().to_string();
524                let d = txt.to_lowercase().contains("desc")
525                    || txt.to_lowercase().contains("descending");
526                (txt, d)
527            } else {
528                (String::new(), false)
529            };
530
531            let field = after_text
532                .split([' ', ','])
533                .next()
534                .unwrap_or("")
535                .trim()
536                .to_string();
537            if field.is_empty() {
538                None
539            } else if is_desc {
540                Some(format!(" ORDER BY {} DESC", field))
541            } else {
542                Some(format!(" ORDER BY {} ASC", field))
543            }
544        } else {
545            None
546        };
547
548        // ============ 提取 GROUP BY ============
549        let group_clause = if has_group {
550            let after = if let Some(pos) = lower.find("group by") {
551                &nl_query[pos + 8..]
552            } else {
553                ""
554            };
555            let group_cols: Vec<String> = after
556                .split(',')
557                .filter_map(|s| {
558                    let first = s.split_whitespace().next()?;
559                    if first.is_empty() {
560                        None
561                    } else {
562                        Some(first.to_string())
563                    }
564                })
565                .collect();
566            if group_cols.is_empty() {
567                None
568            } else {
569                Some(format!(" GROUP BY {}", group_cols.join(", ")))
570            }
571        } else {
572            None
573        };
574
575        // ============ 提取 LIMIT ============
576        let limit_val: Option<usize> = if has_limit {
577            let limit_text = if let Some(pos) = lower.find("limit ") {
578                let after = &nl_query[pos + 6..].trim();
579                after.split_whitespace().next()
580            } else if let Some(pos) = lower.find("top ") {
581                let after = &nl_query[pos + 4..].trim();
582                after.split_whitespace().next()
583            } else if let Some(pos) = lower.find("first ") {
584                let after = &nl_query[pos + 6..].trim();
585                after.split_whitespace().next()
586            } else {
587                None
588            };
589            limit_text.and_then(|s| s.parse::<usize>().ok())
590        } else {
591            None
592        };
593
594        // ============ 提取 JOIN ============
595        let join_tables = if has_join {
596            let all_tables = self.find_all_tables(nl_query, schema);
597            let others: Vec<&&TableInfo> =
598                all_tables.iter().filter(|t| t.name != table.name).collect();
599            others.into_iter().copied().collect()
600        } else {
601            Vec::new()
602        };
603
604        // ============ 构建 SQL ============
605
606        // 构建 SELECT 子句
607        let select_clause = if is_count {
608            if is_distinct && !columns.is_empty() {
609                format!("SELECT COUNT(DISTINCT {})", columns[0])
610            } else {
611                "SELECT COUNT(*)".to_string()
612            }
613        } else if has_aggregation && !columns.is_empty() {
614            // 检测具体聚合函数
615            let agg_func = if lower.contains("sum ") || lower.contains("total ") {
616                "SUM"
617            } else if lower.contains("avg ")
618                || lower.contains("average ")
619                || lower.contains("mean ")
620            {
621                "AVG"
622            } else if lower.contains("min ") || lower.contains("minimum ") {
623                "MIN"
624            } else if lower.contains("max ")
625                || lower.contains("maximum ")
626                || lower.contains("highest")
627            {
628                "MAX"
629            } else {
630                "SUM"
631            };
632            if group_clause.is_some() {
633                // 聚合 + group by
634                format!("SELECT {}, {}({})", columns.join(", "), agg_func, agg_func)
635            } else if !columns.is_empty() {
636                format!("SELECT {}({})", agg_func, columns[0])
637            } else {
638                format!("SELECT {}(*)", agg_func)
639            }
640        } else if !columns.is_empty() {
641            format!("SELECT {}", columns.join(", "))
642        } else {
643            "SELECT *".to_string()
644        };
645
646        // 修正 group by 场景的 select
647        let select_clause = if group_clause.is_some() && has_aggregation {
648            let agg_func = if lower.contains("sum ") || lower.contains("total ") {
649                "SUM"
650            } else if lower.contains("avg ")
651                || lower.contains("average ")
652                || lower.contains("mean ")
653            {
654                "AVG"
655            } else if lower.contains("min ") || lower.contains("minimum ") {
656                "MIN"
657            } else if lower.contains("max ")
658                || lower.contains("maximum ")
659                || lower.contains("highest")
660            {
661                "MAX"
662            } else {
663                "COUNT"
664            };
665
666            if !columns.is_empty() {
667                if columns.len() >= 2 {
668                    format!("SELECT {}, {}({})", columns[1], agg_func, columns[1])
669                } else {
670                    format!("SELECT {}, {}({})", columns[0], agg_func, columns[0])
671                }
672            } else {
673                select_clause
674            }
675        } else {
676            select_clause
677        };
678
679        // 构建 FROM 子句 + JOIN
680        let mut from_clause = format!(" FROM {}", table.name);
681        let mut join_descriptions = Vec::new();
682        for join_table in &join_tables {
683            if let Some((left, right)) = Self::find_join_columns(table, join_table) {
684                from_clause.push_str(&format!(
685                    " JOIN {} ON {} = {}",
686                    join_table.name, left, right
687                ));
688                join_descriptions.push(format!("{} ON {} = {}", join_table.name, left, right));
689            }
690        }
691
692        // 构建 WHERE 子句(参数化)
693        let where_clause = if conditions.is_empty() {
694            String::new()
695        } else {
696            // SAFETY: conditions 来自 NL2SQL 内部解析(field/op/param 由 schema 和 NLP 解析生成),非直接用户输入;AI 生成 SQL 需在执行前经 sql-validator 校验
697            format!(" WHERE {}", conditions.join(" AND "))
698        };
699
700        // 组合 SQL
701        let sql = format!(
702            "{}{}{}{}{}{}",
703            select_clause,
704            from_clause,
705            where_clause,
706            order_clause.as_deref().unwrap_or(""),
707            group_clause.as_deref().unwrap_or(""),
708            limit_val
709                .map(|v| format!(" LIMIT {}", v))
710                .unwrap_or_default(),
711        );
712
713        // 安全验证
714        if !safety::validate_select_only(&sql) {
715            return Err(Nl2SqlError::SafetyError(
716                "生成的 SQL 不是 SELECT 查询".into(),
717            ));
718        }
719        if !safety::validate_no_injection(&sql) {
720            return Err(Nl2SqlError::SafetyError("生成的 SQL 包含注入风险".into()));
721        }
722        let sql = safety::sanitize_sql(&sql);
723
724        // 构建解释
725        let mut explanation_parts = Vec::new();
726        explanation_parts.push(format!("查询 {} 表", table.name));
727        if !columns.is_empty() {
728            explanation_parts.push(format!("列: {}", columns.join(", ")));
729        }
730        if is_count {
731            explanation_parts.push("统计数量".to_string());
732        }
733        if !conditions.is_empty() {
734            let cond_desc: Vec<String> = conditions
735                .iter()
736                .enumerate()
737                .map(|(i, cond)| {
738                    if i < params.len() {
739                        cond.replace(&format!("${}", i + 1), &format!("'{}'", params[i]))
740                    } else {
741                        cond.clone()
742                    }
743                })
744                .collect();
745            explanation_parts.push(format!("条件: {}", cond_desc.join(", ")));
746        }
747        if !params.is_empty() {
748            explanation_parts.push(format!(
749                "参数: [{}]",
750                params
751                    .iter()
752                    .map(|p| format!("'{}'", p))
753                    .collect::<Vec<_>>()
754                    .join(", ")
755            ));
756        }
757        let explanation = explanation_parts.join(";");
758
759        // 计算置信度
760        // 简单的规则:明确的模式匹配给高置信度
761        let mut confidence = 0.7;
762        if !conditions.is_empty() || is_count {
763            confidence = 0.8;
764        }
765        if !columns.is_empty() && !conditions.is_empty() {
766            confidence = 0.9;
767        }
768
769        Ok(SqlQuery {
770            sql,
771            explanation,
772            confidence,
773        })
774    }
775
776    async fn validate(&self, query: &SqlQuery) -> Result<bool, Nl2SqlError> {
777        if !safety::validate_select_only(&query.sql) {
778            return Ok(false);
779        }
780        if !safety::validate_no_injection(&query.sql) {
781            return Ok(false);
782        }
783        if query.confidence < 0.0 || query.confidence > 1.0 {
784            return Err(Nl2SqlError::GenerationError(format!(
785                "confidence 必须在 0.0~1.0 范围内,实际 {}",
786                query.confidence
787            )));
788        }
789        Ok(true)
790    }
791}
792
793// ==================== OpenAINl2SqlEngine ====================
794
795/// OpenAI 兼容的 NL→SQL 引擎(调用 LLM API)
796///
797/// 仅在启用 `real` feature 时编译。
798/// 调用 OpenAI 兼容的 `/v1/chat/completions` 接口生成 SQL。
799///
800/// 生成的 SQL 经过安全验证:只允许 SELECT,并检测注入风险。
801///
802/// # 用法
803///
804/// ```ignore
805/// use sz_orm_ai::nl2sql::{Nl2SqlEngine, OpenAINl2SqlEngine, SchemaContext};
806///
807/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
808/// let engine = OpenAINl2SqlEngine::new("sk-xxxx")
809///     .with_model("gpt-4o-mini");
810/// let schema = SchemaContext::default();
811/// let result = engine.generate("show all users", &schema).await?;
812/// println!("SQL: {}", result.sql);
813/// # Ok(())
814/// # }
815/// ```
816#[cfg(feature = "real")]
817pub struct OpenAINl2SqlEngine {
818    /// API 基础地址(默认 `https://api.openai.com/v1`)
819    api_base: String,
820    /// API Key(Bearer token)
821    api_key: String,
822    /// 模型名称(默认 `gpt-4o-mini`)
823    model: String,
824    /// HTTP 客户端
825    http_client: reqwest::Client,
826}
827
828#[cfg(feature = "real")]
829impl OpenAINl2SqlEngine {
830    /// 默认 API 基础地址
831    const DEFAULT_API_BASE: &'static str = "https://api.openai.com/v1";
832    /// 默认模型
833    const DEFAULT_MODEL: &'static str = "gpt-4o-mini";
834
835    /// 创建客户端实例
836    pub fn new(api_key: impl Into<String>) -> Self {
837        Self {
838            api_base: Self::DEFAULT_API_BASE.to_string(),
839            api_key: api_key.into(),
840            model: Self::DEFAULT_MODEL.to_string(),
841            http_client: reqwest::Client::new(),
842        }
843    }
844
845    /// 设置 API base URL
846    pub fn with_api_base(mut self, api_base: impl Into<String>) -> Self {
847        self.api_base = api_base.into();
848        self
849    }
850
851    /// 设置模型名称
852    pub fn with_model(mut self, model: impl Into<String>) -> Self {
853        self.model = model.into();
854        self
855    }
856
857    /// 校验 API key 非空
858    fn ensure_api_key(&self) -> Result<(), Nl2SqlError> {
859        if self.api_key.is_empty() {
860            return Err(Nl2SqlError::ConfigError(
861                "API key 为空,无法调用 OpenAI API".into(),
862            ));
863        }
864        Ok(())
865    }
866
867    /// 构建 system prompt(包含 schema 信息和 SQL 生成规范)
868    fn build_system_prompt(schema: &SchemaContext) -> String {
869        let mut prompt = String::from(
870            "You are a SQL generator. Given a database schema and a natural language query, ",
871        );
872        prompt.push_str("generate a valid SQL SELECT statement.\n\n");
873        prompt.push_str("Rules:\n");
874        prompt.push_str("- Only generate SELECT statements (no INSERT, UPDATE, DELETE, DROP, ALTER, TRUNCATE)\n");
875        prompt.push_str("- Use parameterized placeholders ($1, $2, ...) for all values to prevent SQL injection\n");
876        prompt.push_str("- If the query is ambiguous, choose the most likely interpretation\n");
877        prompt
878            .push_str("- Return ONLY the SQL statement, no explanation or markdown formatting\n\n");
879
880        prompt.push_str("Database Schema:\n");
881        for table in &schema.tables {
882            prompt.push_str(&format!("CREATE TABLE {} (\n", table.name));
883            for col in &table.columns {
884                prompt.push_str(&format!(
885                    "  {} {} {} {},\n",
886                    col.name,
887                    col.data_type,
888                    if col.is_primary_key {
889                        "PRIMARY KEY"
890                    } else {
891                        ""
892                    },
893                    if col.nullable { "NULL" } else { "NOT NULL" },
894                ));
895            }
896            prompt.push_str(");\n\n");
897        }
898
899        prompt
900    }
901}
902
903#[cfg(feature = "real")]
904#[derive(Serialize)]
905struct ChatCompletionRequest<'a> {
906    model: &'a str,
907    messages: Vec<Message<'a>>,
908    temperature: f32,
909    max_tokens: u32,
910}
911
912#[cfg(feature = "real")]
913#[derive(Serialize)]
914struct Message<'a> {
915    role: &'a str,
916    content: String,
917}
918
919#[cfg(feature = "real")]
920#[derive(Deserialize)]
921struct ChatCompletionResponse {
922    choices: Vec<Choice>,
923}
924
925#[cfg(feature = "real")]
926#[derive(Deserialize)]
927struct Choice {
928    message: ChoiceMessage,
929    #[allow(dead_code)]
930    finish_reason: Option<String>,
931}
932
933#[cfg(feature = "real")]
934#[derive(Deserialize)]
935struct ChoiceMessage {
936    content: Option<String>,
937}
938
939#[cfg(feature = "real")]
940#[async_trait]
941impl Nl2SqlEngine for OpenAINl2SqlEngine {
942    async fn generate(
943        &self,
944        nl_query: &str,
945        schema: &SchemaContext,
946    ) -> Result<SqlQuery, Nl2SqlError> {
947        self.ensure_api_key()?;
948
949        if nl_query.trim().is_empty() {
950            return Err(Nl2SqlError::InvalidQuery("自然语言查询不能为空".into()));
951        }
952
953        let system_prompt = Self::build_system_prompt(schema);
954        let user_message = format!(
955            "Given the schema above, generate a SQL query for: {}",
956            nl_query
957        );
958
959        let body = ChatCompletionRequest {
960            model: &self.model,
961            messages: vec![
962                Message {
963                    role: "system",
964                    content: system_prompt,
965                },
966                Message {
967                    role: "user",
968                    content: user_message,
969                },
970            ],
971            temperature: 0.1,
972            max_tokens: 500,
973        };
974
975        let url = format!("{}/chat/completions", self.api_base);
976        let resp = self
977            .http_client
978            .post(&url)
979            .bearer_auth(&self.api_key)
980            .json(&body)
981            .send()
982            .await
983            .map_err(|e| Nl2SqlError::NetworkError(e.to_string()))?;
984
985        let status = resp.status().as_u16();
986        if !resp.status().is_success() {
987            let message = resp.text().await.unwrap_or_default();
988            return Err(Nl2SqlError::ApiError(status, message));
989        }
990
991        let parsed: ChatCompletionResponse = resp
992            .json()
993            .await
994            .map_err(|e| Nl2SqlError::NetworkError(e.to_string()))?;
995
996        let raw_sql = parsed
997            .choices
998            .into_iter()
999            .next()
1000            .and_then(|c| c.message.content)
1001            .ok_or_else(|| Nl2SqlError::ApiError(status, "API 返回空响应".into()))?;
1002
1003        // 清理 LLM 返回的 SQL(去掉 markdown 代码块标记)
1004        let cleaned_sql = clean_llm_sql_output(&raw_sql);
1005
1006        let query = SqlQuery {
1007            sql: cleaned_sql.clone(),
1008            explanation: format!("由 {} 模型根据自然语言查询生成", self.model),
1009            confidence: 0.8,
1010        };
1011
1012        // 安全验证
1013        if !safety::validate_select_only(&query.sql) {
1014            return Err(Nl2SqlError::SafetyError(
1015                "生成的 SQL 不是 SELECT 查询,已被拦截".into(),
1016            ));
1017        }
1018        if !safety::validate_no_injection(&query.sql) {
1019            return Err(Nl2SqlError::SafetyError(
1020                "生成的 SQL 包含注入风险,已被拦截".into(),
1021            ));
1022        }
1023
1024        Ok(query)
1025    }
1026
1027    async fn validate(&self, query: &SqlQuery) -> Result<bool, Nl2SqlError> {
1028        if !safety::validate_select_only(&query.sql) {
1029            return Ok(false);
1030        }
1031        if !safety::validate_no_injection(&query.sql) {
1032            return Ok(false);
1033        }
1034        if query.confidence < 0.0 || query.confidence > 1.0 {
1035            return Err(Nl2SqlError::GenerationError(format!(
1036                "confidence 必须在 0.0~1.0 范围内,实际 {}",
1037                query.confidence
1038            )));
1039        }
1040        Ok(true)
1041    }
1042}
1043
1044/// 清理 LLM 输出的 SQL(移除 markdown 代码块标记和前后空白)
1045#[cfg(feature = "real")]
1046fn clean_llm_sql_output(raw: &str) -> String {
1047    let trimmed = raw.trim();
1048    // 移除 ```sql ... ``` 或 ``` ... ``` 包装
1049    if trimmed.starts_with("```") {
1050        let content = trimmed.trim_start_matches('`');
1051        if let Some(end) = content.rfind("```") {
1052            let inner = content[..end].trim();
1053            // 移除 "sql" 语言标记(如 ```sql\n...)
1054            return inner
1055                .strip_prefix("sql")
1056                .unwrap_or(inner)
1057                .trim()
1058                .to_string();
1059        }
1060        return content.trim().to_string();
1061    }
1062    trimmed.to_string()
1063}
1064
1065// ==================== 查询优化提示 ====================
1066
1067/// 优化建议的严重级别
1068#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1069pub enum HintSeverity {
1070    /// 信息级:可选的优化建议
1071    Info,
1072    /// 警告级:可能影响性能
1073    Warning,
1074    /// 严重级:强烈建议修改
1075    Critical,
1076}
1077
1078impl HintSeverity {
1079    /// 转换为字符串
1080    pub fn as_str(&self) -> &'static str {
1081        match self {
1082            HintSeverity::Info => "INFO",
1083            HintSeverity::Warning => "WARNING",
1084            HintSeverity::Critical => "CRITICAL",
1085        }
1086    }
1087}
1088
1089/// 单条查询优化建议
1090#[derive(Debug, Clone)]
1091pub struct QueryOptimizationHint {
1092    /// 建议标题
1093    pub title: String,
1094    /// 详细描述
1095    pub description: String,
1096    /// 严重级别
1097    pub severity: HintSeverity,
1098    /// 优化后的 SQL 建议(可选)
1099    pub suggested_sql: Option<String>,
1100}
1101
1102impl QueryOptimizationHint {
1103    /// 创建一条信息级建议
1104    pub fn info(title: impl Into<String>, description: impl Into<String>) -> Self {
1105        Self {
1106            title: title.into(),
1107            description: description.into(),
1108            severity: HintSeverity::Info,
1109            suggested_sql: None,
1110        }
1111    }
1112
1113    /// 创建一条警告级建议
1114    pub fn warning(title: impl Into<String>, description: impl Into<String>) -> Self {
1115        Self {
1116            title: title.into(),
1117            description: description.into(),
1118            severity: HintSeverity::Warning,
1119            suggested_sql: None,
1120        }
1121    }
1122
1123    /// 创建一条严重级建议
1124    pub fn critical(title: impl Into<String>, description: impl Into<String>) -> Self {
1125        Self {
1126            title: title.into(),
1127            description: description.into(),
1128            severity: HintSeverity::Critical,
1129            suggested_sql: None,
1130        }
1131    }
1132
1133    /// 附加优化后的 SQL 建议
1134    pub fn with_suggested_sql(mut self, sql: impl Into<String>) -> Self {
1135        self.suggested_sql = Some(sql.into());
1136        self
1137    }
1138}
1139
1140/// 查询分析结果
1141#[derive(Debug, Clone)]
1142pub struct QueryAnalysis {
1143    /// 原始 SQL
1144    pub original_sql: String,
1145    /// 所有优化建议
1146    pub hints: Vec<QueryOptimizationHint>,
1147    /// 预估的 SQL 复杂度评分(0-100,越高越复杂)
1148    pub complexity_score: u32,
1149    /// 检测到的表名列表
1150    pub detected_tables: Vec<String>,
1151    /// 是否包含 WHERE 子句
1152    pub has_where: bool,
1153    /// 是否包含 LIMIT 子句
1154    pub has_limit: bool,
1155    /// 是否包含 JOIN
1156    pub has_join: bool,
1157    /// 是否包含子查询
1158    pub has_subquery: bool,
1159    /// 是否使用了 SELECT *
1160    pub uses_select_star: bool,
1161}
1162
1163impl QueryAnalysis {
1164    /// 返回严重级别为 Critical 的建议数量
1165    pub fn critical_count(&self) -> usize {
1166        self.hints
1167            .iter()
1168            .filter(|h| h.severity == HintSeverity::Critical)
1169            .count()
1170    }
1171
1172    /// 返回严重级别为 Warning 的建议数量
1173    pub fn warning_count(&self) -> usize {
1174        self.hints
1175            .iter()
1176            .filter(|h| h.severity == HintSeverity::Warning)
1177            .count()
1178    }
1179
1180    /// 是否存在任何建议
1181    pub fn has_hints(&self) -> bool {
1182        !self.hints.is_empty()
1183    }
1184}
1185
1186/// SQL 查询优化分析器
1187///
1188/// 基于规则分析 SQL 查询,生成优化建议。
1189/// 不依赖外部 LLM API,纯规则匹配,适用于离线场景。
1190pub struct QueryOptimizer {
1191    /// 是否检测 SELECT *
1192    pub check_select_star: bool,
1193    /// 是否检测缺失的 LIMIT
1194    pub check_missing_limit: bool,
1195    /// 是否检测缺失的 WHERE
1196    pub check_missing_where: bool,
1197    /// LIMIT 建议的默认行数
1198    pub default_limit: usize,
1199    /// 复杂度评分中 JOIN 的权重
1200    pub join_weight: u32,
1201    /// 复杂度评分中子查询的权重
1202    pub subquery_weight: u32,
1203    /// 复杂度评分中 WHERE 条件的权重
1204    pub where_weight: u32,
1205}
1206
1207impl Default for QueryOptimizer {
1208    fn default() -> Self {
1209        Self {
1210            check_select_star: true,
1211            check_missing_limit: true,
1212            check_missing_where: true,
1213            default_limit: 100,
1214            join_weight: 15,
1215            subquery_weight: 20,
1216            where_weight: 5,
1217        }
1218    }
1219}
1220
1221impl QueryOptimizer {
1222    /// 创建新的查询优化分析器
1223    pub fn new() -> Self {
1224        Self::default()
1225    }
1226
1227    /// 设置默认 LIMIT 行数
1228    pub fn with_default_limit(mut self, limit: usize) -> Self {
1229        self.default_limit = limit;
1230        self
1231    }
1232
1233    /// 禁用 SELECT * 检测
1234    pub fn disable_select_star_check(mut self) -> Self {
1235        self.check_select_star = false;
1236        self
1237    }
1238
1239    /// 禁用缺失 LIMIT 检测
1240    pub fn disable_missing_limit_check(mut self) -> Self {
1241        self.check_missing_limit = false;
1242        self
1243    }
1244
1245    /// 禁用缺失 WHERE 检测
1246    pub fn disable_missing_where_check(mut self) -> Self {
1247        self.check_missing_where = false;
1248        self
1249    }
1250
1251    /// 分析 SQL 查询并生成优化建议
1252    ///
1253    /// # 参数
1254    /// - `sql`: 要分析的 SQL 查询语句
1255    /// - `schema`: 数据库 schema 上下文(用于检测表名和索引)
1256    pub fn analyze(&self, sql: &str, schema: &SchemaContext) -> QueryAnalysis {
1257        let normalized = Self::normalize_sql(sql);
1258        let lower = normalized.to_lowercase();
1259
1260        let uses_select_star = self.detect_select_star(&lower);
1261        let has_where = self.detect_where(&lower);
1262        let has_limit = self.detect_limit(&lower);
1263        let has_join = self.detect_join(&lower);
1264        let has_subquery = self.detect_subquery(&lower);
1265        let detected_tables = self.extract_tables(&lower, schema);
1266
1267        let mut hints = Vec::new();
1268
1269        // 检测 SELECT *
1270        if self.check_select_star && uses_select_star {
1271            let columns_hint = self.suggest_columns(&detected_tables, schema);
1272            let mut hint = QueryOptimizationHint::warning(
1273                "避免使用 SELECT *",
1274                "SELECT * 会返回所有列,可能导致不必要的数据传输和内存消耗。建议显式指定所需列。",
1275            );
1276            if !columns_hint.is_empty() {
1277                hint = hint.with_suggested_sql(columns_hint);
1278            }
1279            hints.push(hint);
1280        }
1281
1282        // 检测缺失的 WHERE
1283        if self.check_missing_where && !has_where {
1284            hints.push(QueryOptimizationHint::critical(
1285                "缺少 WHERE 子句",
1286                "查询没有 WHERE 条件,将扫描全表。对于大表这会导致严重的性能问题。",
1287            ));
1288        }
1289
1290        // 检测缺失的 LIMIT
1291        if self.check_missing_limit && !has_limit {
1292            let suggested = self.add_limit_suggestion(&normalized, self.default_limit);
1293            hints.push(
1294                QueryOptimizationHint::warning(
1295                    "缺少 LIMIT 子句",
1296                    format!(
1297                        "查询没有 LIMIT 限制,可能返回大量数据。建议添加 LIMIT {}。",
1298                        self.default_limit
1299                    ),
1300                )
1301                .with_suggested_sql(suggested),
1302            );
1303        }
1304
1305        // 检测多表 JOIN 无索引建议
1306        if has_join {
1307            let join_count = Self::count_joins(&lower);
1308            if join_count >= 3 {
1309                hints.push(QueryOptimizationHint::critical(
1310                    "JOIN 数量过多",
1311                    format!(
1312                        "查询包含 {} 个 JOIN,可能导致性能下降。建议拆分为多个查询或使用临时表。",
1313                        join_count
1314                    ),
1315                ));
1316            } else if join_count >= 1 {
1317                hints.push(QueryOptimizationHint::info(
1318                    "JOIN 查询建议",
1319                    "确保 JOIN 条件涉及的列已建立索引,避免嵌套循环扫描。",
1320                ));
1321            }
1322        }
1323
1324        // 检测子查询
1325        if has_subquery {
1326            let subquery_count = Self::count_subqueries(&lower);
1327            if subquery_count >= 2 {
1328                hints.push(QueryOptimizationHint::warning(
1329                    "子查询嵌套过深",
1330                    format!(
1331                        "查询包含 {} 个子查询,建议考虑使用 JOIN 重写以提高性能。",
1332                        subquery_count
1333                    ),
1334                ));
1335            }
1336        }
1337
1338        // 检测 LIKE '%...' 前缀通配符
1339        if Self::has_leading_wildcard_like(&lower) {
1340            hints.push(QueryOptimizationHint::warning(
1341                "LIKE 使用前缀通配符",
1342                "LIKE '%keyword' 无法使用索引,会导致全表扫描。如可能,使用 LIKE 'keyword%' 或全文索引。",
1343            ));
1344        }
1345
1346        // 检测 OR 条件(可能导致无法使用索引)
1347        if Self::count_or_conditions(&lower) >= 3 {
1348            hints.push(QueryOptimizationHint::info(
1349                "多个 OR 条件",
1350                "多个 OR 条件可能导致无法有效使用索引。考虑使用 UNION ALL 重写。",
1351            ));
1352        }
1353
1354        // 检测 ORDER BY 无 LIMIT
1355        if Self::has_order_by_without_limit(&lower) {
1356            hints.push(QueryOptimizationHint::warning(
1357                "ORDER BY 无 LIMIT",
1358                "ORDER BY 无 LIMIT 时需要排序全部数据,可能消耗大量内存。建议添加 LIMIT。",
1359            ));
1360        }
1361
1362        // 检测 COUNT(*) 建议使用 COUNT(1)
1363        if lower.contains("count(*)") {
1364            hints.push(QueryOptimizationHint::info(
1365                "考虑使用 COUNT(1)",
1366                "某些数据库中 COUNT(1) 比 COUNT(*) 略快(虽然现代优化器通常已优化)。",
1367            ));
1368        }
1369
1370        // 检测缺失索引建议(基于 WHERE 条件)
1371        if has_where {
1372            let where_columns = self.extract_where_columns(&lower, schema);
1373            for col in &where_columns {
1374                if !self.column_has_index(col, schema) {
1375                    hints.push(QueryOptimizationHint::info(
1376                        format!("建议为列 {} 添加索引", col),
1377                        format!(
1378                            "WHERE 条件中使用了列 {},但该列似乎没有索引。添加索引可提高查询速度。",
1379                            col
1380                        ),
1381                    ));
1382                }
1383            }
1384        }
1385
1386        // 计算复杂度评分
1387        let complexity_score =
1388            self.calculate_complexity(has_join, has_subquery, has_where, &detected_tables, &lower);
1389
1390        QueryAnalysis {
1391            original_sql: sql.to_string(),
1392            hints,
1393            complexity_score,
1394            detected_tables,
1395            has_where,
1396            has_limit,
1397            has_join,
1398            has_subquery,
1399            uses_select_star,
1400        }
1401    }
1402
1403    /// 生成优化报告文本
1404    pub fn format_report(analysis: &QueryAnalysis) -> String {
1405        let mut report = String::new();
1406        report.push_str("=== SQL 查询优化分析报告 ===\n\n");
1407        report.push_str(&format!("原始 SQL: {}\n", analysis.original_sql));
1408        report.push_str(&format!("复杂度评分: {}/100\n", analysis.complexity_score));
1409        report.push_str(&format!(
1410            "检测到的表: {}\n",
1411            if analysis.detected_tables.is_empty() {
1412                "无".to_string()
1413            } else {
1414                analysis.detected_tables.join(", ")
1415            }
1416        ));
1417        report.push_str(&format!("包含 WHERE: {}\n", analysis.has_where)); // SAFETY: 报告文本拼接,非 SQL 执行
1418        report.push_str(&format!("包含 LIMIT: {}\n", analysis.has_limit));
1419        report.push_str(&format!("包含 JOIN: {}\n", analysis.has_join));
1420        report.push_str(&format!("包含子查询: {}\n", analysis.has_subquery));
1421        report.push_str(&format!("使用 SELECT *: {}\n\n", analysis.uses_select_star));
1422
1423        if analysis.hints.is_empty() {
1424            report.push_str("✓ 未发现优化建议,查询看起来良好。\n");
1425        } else {
1426            report.push_str(&format!(
1427                "共 {} 条优化建议({} 严重,{} 警告):\n\n",
1428                analysis.hints.len(),
1429                analysis.critical_count(),
1430                analysis.warning_count()
1431            ));
1432            for (i, hint) in analysis.hints.iter().enumerate() {
1433                report.push_str(&format!(
1434                    "{}. [{}] {}\n   {}\n",
1435                    i + 1,
1436                    hint.severity.as_str(),
1437                    hint.title,
1438                    hint.description
1439                ));
1440                if let Some(ref sql) = hint.suggested_sql {
1441                    report.push_str(&format!("   建议SQL: {}\n", sql));
1442                }
1443                report.push('\n');
1444            }
1445        }
1446
1447        report
1448    }
1449
1450    // ---- 内部辅助方法 ----
1451
1452    /// 规范化 SQL(去除多余空白、换行)
1453    fn normalize_sql(sql: &str) -> String {
1454        sql.split_whitespace().collect::<Vec<_>>().join(" ")
1455    }
1456
1457    /// 检测 SELECT *
1458    fn detect_select_star(&self, lower: &str) -> bool {
1459        lower.contains("select *") || lower.contains("select  *")
1460    }
1461
1462    /// 检测 WHERE 子句
1463    fn detect_where(&self, lower: &str) -> bool {
1464        lower.contains(" where ")
1465    }
1466
1467    /// 检测 LIMIT 子句
1468    fn detect_limit(&self, lower: &str) -> bool {
1469        lower.contains(" limit ")
1470    }
1471
1472    /// 检测 JOIN
1473    fn detect_join(&self, lower: &str) -> bool {
1474        lower.contains(" join ")
1475            || lower.contains(" inner join ")
1476            || lower.contains(" left join ")
1477            || lower.contains(" right join ")
1478            || lower.contains(" full join ")
1479            || lower.contains(" cross join ")
1480    }
1481
1482    /// 检测子查询(括号内的 SELECT)
1483    fn detect_subquery(&self, lower: &str) -> bool {
1484        if let Some(paren_pos) = lower.find('(') {
1485            let after = &lower[paren_pos..];
1486            after.contains("select")
1487        } else {
1488            false
1489        }
1490    }
1491
1492    /// 统计 JOIN 数量
1493    fn count_joins(lower: &str) -> usize {
1494        lower.matches(" join ").count()
1495    }
1496
1497    /// 统计子查询数量
1498    fn count_subqueries(lower: &str) -> usize {
1499        lower.matches("(select").count() + lower.matches("( select").count()
1500    }
1501
1502    /// 检测前缀通配符 LIKE
1503    fn has_leading_wildcard_like(lower: &str) -> bool {
1504        lower.contains("like '%") || lower.contains("like \"%")
1505    }
1506
1507    /// 统计 OR 条件数量
1508    fn count_or_conditions(lower: &str) -> usize {
1509        lower.matches(" or ").count()
1510    }
1511
1512    /// 检测 ORDER BY 无 LIMIT
1513    fn has_order_by_without_limit(lower: &str) -> bool {
1514        lower.contains(" order by ") && !lower.contains(" limit ")
1515    }
1516
1517    /// 从 SQL 中提取表名(基于 schema)
1518    fn extract_tables(&self, lower: &str, schema: &SchemaContext) -> Vec<String> {
1519        let mut tables = Vec::new();
1520        for table in &schema.tables {
1521            let name_lower = table.name.to_lowercase();
1522            if lower.contains(&name_lower) {
1523                tables.push(table.name.clone());
1524            }
1525        }
1526        tables
1527    }
1528
1529    /// 生成列建议 SQL
1530    fn suggest_columns(&self, tables: &[String], schema: &SchemaContext) -> String {
1531        if tables.is_empty() {
1532            return String::new();
1533        }
1534        let mut cols = Vec::new();
1535        for table_name in tables {
1536            if let Some(table) = schema.tables.iter().find(|t| t.name == *table_name) {
1537                for col in &table.columns {
1538                    cols.push(format!("{}.{}", table_name, col.name));
1539                }
1540            }
1541        }
1542        if cols.is_empty() {
1543            String::new()
1544        } else {
1545            format!("SELECT {} FROM {}", cols.join(", "), tables.join(", "))
1546        }
1547    }
1548
1549    /// 为 SQL 添加 LIMIT 建议
1550    fn add_limit_suggestion(&self, sql: &str, limit: usize) -> String {
1551        let trimmed = sql.trim_end_matches(';');
1552        format!("{} LIMIT {}", trimmed, limit)
1553    }
1554
1555    /// 从 WHERE 子句中提取列名
1556    fn extract_where_columns(&self, lower: &str, schema: &SchemaContext) -> Vec<String> {
1557        let mut columns = Vec::new();
1558        if let Some(where_pos) = lower.find(" where ") {
1559            let after_where = &lower[where_pos + 7..];
1560            // 截取到 GROUP BY / ORDER BY / LIMIT 之前
1561            let where_clause = after_where
1562                .split(" group by ")
1563                .next()
1564                .unwrap_or(after_where)
1565                .split(" order by ")
1566                .next()
1567                .unwrap_or(after_where)
1568                .split(" limit ")
1569                .next()
1570                .unwrap_or(after_where);
1571
1572            for table in &schema.tables {
1573                for col in &table.columns {
1574                    let col_lower = col.name.to_lowercase();
1575                    if col_lower.len() >= 2
1576                        && where_clause.contains(&col_lower)
1577                        && !columns.contains(&col.name)
1578                    {
1579                        columns.push(col.name.clone());
1580                    }
1581                }
1582            }
1583        }
1584        columns
1585    }
1586
1587    /// 检查列是否有索引(简化版:主键视为有索引)
1588    fn column_has_index(&self, col: &str, schema: &SchemaContext) -> bool {
1589        for table in &schema.tables {
1590            for c in &table.columns {
1591                if c.name.eq_ignore_ascii_case(col) && c.is_primary_key {
1592                    return true;
1593                }
1594            }
1595        }
1596        false
1597    }
1598
1599    /// 计算 SQL 复杂度评分
1600    fn calculate_complexity(
1601        &self,
1602        has_join: bool,
1603        has_subquery: bool,
1604        has_where: bool,
1605        tables: &[String],
1606        lower: &str,
1607    ) -> u32 {
1608        let mut score: u32 = 10; // 基础分
1609
1610        if has_join {
1611            let join_count = Self::count_joins(lower) as u32;
1612            score += join_count * self.join_weight;
1613        }
1614
1615        if has_subquery {
1616            let sub_count = Self::count_subqueries(lower) as u32;
1617            score += sub_count * self.subquery_weight;
1618        }
1619
1620        if has_where {
1621            let or_count = Self::count_or_conditions(lower) as u32;
1622            score += self.where_weight + or_count * 3;
1623        }
1624
1625        // 表数量影响
1626        if tables.len() > 1 {
1627            score += (tables.len() as u32 - 1) * 10;
1628        }
1629
1630        // ORDER BY 影响
1631        if lower.contains(" order by ") {
1632            score += 5;
1633        }
1634
1635        // GROUP BY 影响
1636        if lower.contains(" group by ") {
1637            score += 10;
1638        }
1639
1640        // DISTINCT 影响
1641        if lower.contains(" distinct ") {
1642            score += 5;
1643        }
1644
1645        score.min(100)
1646    }
1647}
1648
1649// ==================== 单元测试 ====================
1650
1651#[cfg(test)]
1652mod tests {
1653    use super::*;
1654
1655    fn test_schema() -> SchemaContext {
1656        SchemaContext {
1657            tables: vec![
1658                TableInfo {
1659                    name: "users".into(),
1660                    columns: vec![
1661                        ColumnInfo {
1662                            name: "id".into(),
1663                            data_type: "INTEGER".into(),
1664                            nullable: false,
1665                            is_primary_key: true,
1666                        },
1667                        ColumnInfo {
1668                            name: "name".into(),
1669                            data_type: "TEXT".into(),
1670                            nullable: true,
1671                            is_primary_key: false,
1672                        },
1673                        ColumnInfo {
1674                            name: "email".into(),
1675                            data_type: "TEXT".into(),
1676                            nullable: true,
1677                            is_primary_key: false,
1678                        },
1679                        ColumnInfo {
1680                            name: "age".into(),
1681                            data_type: "INTEGER".into(),
1682                            nullable: true,
1683                            is_primary_key: false,
1684                        },
1685                        ColumnInfo {
1686                            name: "city".into(),
1687                            data_type: "TEXT".into(),
1688                            nullable: true,
1689                            is_primary_key: false,
1690                        },
1691                        ColumnInfo {
1692                            name: "score".into(),
1693                            data_type: "REAL".into(),
1694                            nullable: true,
1695                            is_primary_key: false,
1696                        },
1697                    ],
1698                },
1699                TableInfo {
1700                    name: "orders".into(),
1701                    columns: vec![
1702                        ColumnInfo {
1703                            name: "id".into(),
1704                            data_type: "INTEGER".into(),
1705                            nullable: false,
1706                            is_primary_key: true,
1707                        },
1708                        ColumnInfo {
1709                            name: "user_id".into(),
1710                            data_type: "INTEGER".into(),
1711                            nullable: false,
1712                            is_primary_key: false,
1713                        },
1714                        ColumnInfo {
1715                            name: "product".into(),
1716                            data_type: "TEXT".into(),
1717                            nullable: true,
1718                            is_primary_key: false,
1719                        },
1720                        ColumnInfo {
1721                            name: "price".into(),
1722                            data_type: "REAL".into(),
1723                            nullable: true,
1724                            is_primary_key: false,
1725                        },
1726                        ColumnInfo {
1727                            name: "quantity".into(),
1728                            data_type: "INTEGER".into(),
1729                            nullable: true,
1730                            is_primary_key: false,
1731                        },
1732                    ],
1733                },
1734                TableInfo {
1735                    name: "products".into(),
1736                    columns: vec![
1737                        ColumnInfo {
1738                            name: "id".into(),
1739                            data_type: "INTEGER".into(),
1740                            nullable: false,
1741                            is_primary_key: true,
1742                        },
1743                        ColumnInfo {
1744                            name: "name".into(),
1745                            data_type: "TEXT".into(),
1746                            nullable: false,
1747                            is_primary_key: false,
1748                        },
1749                        ColumnInfo {
1750                            name: "price".into(),
1751                            data_type: "REAL".into(),
1752                            nullable: false,
1753                            is_primary_key: false,
1754                        },
1755                        ColumnInfo {
1756                            name: "category".into(),
1757                            data_type: "TEXT".into(),
1758                            nullable: true,
1759                            is_primary_key: false,
1760                        },
1761                    ],
1762                },
1763            ],
1764        }
1765    }
1766
1767    // ============ SimpleNl2SqlEngine ============
1768
1769    #[tokio::test]
1770    async fn test_simple_select_all() {
1771        let engine = SimpleNl2SqlEngine::new();
1772        let schema = test_schema();
1773        let result = engine.generate("show all users", &schema).await.unwrap();
1774        assert_eq!(result.sql, "SELECT * FROM users");
1775        assert!(result.confidence > 0.0);
1776    }
1777
1778    #[tokio::test]
1779    async fn test_simple_select_columns() {
1780        let engine = SimpleNl2SqlEngine::new();
1781        let schema = test_schema();
1782        // "name" and "email" both appear in the query; schema matching picks them up
1783        let result = engine
1784            .generate("show name and email of users", &schema)
1785            .await
1786            .unwrap();
1787        assert_eq!(result.sql, "SELECT name, email FROM users");
1788    }
1789
1790    #[tokio::test]
1791    async fn test_simple_count() {
1792        let engine = SimpleNl2SqlEngine::new();
1793        let schema = test_schema();
1794        let result = engine.generate("how many users", &schema).await.unwrap();
1795        assert_eq!(result.sql, "SELECT COUNT(*) FROM users");
1796    }
1797
1798    #[tokio::test]
1799    async fn test_simple_where_equality() {
1800        let engine = SimpleNl2SqlEngine::new();
1801        let schema = test_schema();
1802        let result = engine
1803            .generate("find users where name = John", &schema)
1804            .await
1805            .unwrap();
1806        assert_eq!(result.sql, "SELECT * FROM users WHERE name = $1");
1807        assert!(result.explanation.contains("John"));
1808    }
1809
1810    #[tokio::test]
1811    async fn test_simple_where_comparison() {
1812        let engine = SimpleNl2SqlEngine::new();
1813        let schema = test_schema();
1814        let result = engine
1815            .generate("find users where age > 25", &schema)
1816            .await
1817            .unwrap();
1818        assert_eq!(result.sql, "SELECT * FROM users WHERE age > $1");
1819    }
1820
1821    #[tokio::test]
1822    async fn test_simple_order_by() {
1823        let engine = SimpleNl2SqlEngine::new();
1824        let schema = test_schema();
1825        let result = engine
1826            .generate("list users ordered by name", &schema)
1827            .await
1828            .unwrap();
1829        assert_eq!(result.sql, "SELECT * FROM users ORDER BY name ASC");
1830    }
1831
1832    #[tokio::test]
1833    async fn test_simple_order_by_desc() {
1834        let engine = SimpleNl2SqlEngine::new();
1835        let schema = test_schema();
1836        let result = engine
1837            .generate("list users sorted by name descending", &schema)
1838            .await
1839            .unwrap();
1840        assert_eq!(result.sql, "SELECT * FROM users ORDER BY name DESC");
1841    }
1842
1843    #[tokio::test]
1844    async fn test_simple_limit() {
1845        let engine = SimpleNl2SqlEngine::new();
1846        let schema = test_schema();
1847        let result = engine.generate("first 10 users", &schema).await.unwrap();
1848        assert_eq!(result.sql, "SELECT * FROM users LIMIT 10");
1849    }
1850
1851    #[tokio::test]
1852    async fn test_simple_order_by_with_limit() {
1853        let engine = SimpleNl2SqlEngine::new();
1854        let schema = test_schema();
1855        let result = engine
1856            .generate("top 5 users by score", &schema)
1857            .await
1858            .unwrap();
1859        // "top" triggers LIMIT; "score" is a known column, and "by score" matched as sort
1860        // This generates: SELECT * FROM users ORDER BY score ASC LIMIT 5
1861        assert!(result.sql.contains("LIMIT 5"));
1862    }
1863
1864    #[tokio::test]
1865    async fn test_simple_aggregation() {
1866        let engine = SimpleNl2SqlEngine::new();
1867        let schema = test_schema();
1868        let result = engine
1869            .generate("total price from orders", &schema)
1870            .await
1871            .unwrap();
1872        assert_eq!(result.sql, "SELECT SUM(price) FROM orders");
1873    }
1874
1875    #[tokio::test]
1876    async fn test_simple_avg() {
1877        let engine = SimpleNl2SqlEngine::new();
1878        let schema = test_schema();
1879        let result = engine
1880            .generate("average age of users", &schema)
1881            .await
1882            .unwrap();
1883        assert_eq!(result.sql, "SELECT AVG(age) FROM users");
1884    }
1885
1886    #[tokio::test]
1887    async fn test_simple_empty_query() {
1888        let engine = SimpleNl2SqlEngine::new();
1889        let schema = test_schema();
1890        let result = engine.generate("", &schema).await;
1891        assert!(result.is_err());
1892        match result {
1893            Err(Nl2SqlError::InvalidQuery(_)) => {}
1894            Err(e) => panic!("期望 InvalidQuery,实际: {:?}", e),
1895            Ok(_) => panic!("期望错误"),
1896        }
1897    }
1898
1899    #[tokio::test]
1900    async fn test_simple_empty_schema() {
1901        let engine = SimpleNl2SqlEngine::new();
1902        let schema = SchemaContext { tables: vec![] };
1903        let result = engine.generate("show users", &schema).await;
1904        assert!(result.is_err());
1905        match result {
1906            Err(Nl2SqlError::SchemaError(_)) => {}
1907            _ => panic!("期望 SchemaError"),
1908        }
1909    }
1910
1911    #[tokio::test]
1912    async fn test_simple_validate_valid() {
1913        let engine = SimpleNl2SqlEngine::new();
1914        let query = SqlQuery {
1915            sql: "SELECT * FROM users".into(),
1916            explanation: "test".into(),
1917            confidence: 0.9,
1918        };
1919        assert!(engine.validate(&query).await.unwrap());
1920    }
1921
1922    #[tokio::test]
1923    async fn test_simple_validate_invalid_confidence() {
1924        let engine = SimpleNl2SqlEngine::new();
1925        let query = SqlQuery {
1926            sql: "SELECT * FROM users".into(),
1927            explanation: "test".into(),
1928            confidence: 1.5,
1929        };
1930        let result = engine.validate(&query).await;
1931        assert!(result.is_err());
1932    }
1933
1934    #[tokio::test]
1935    async fn test_simple_validate_rejects_drop() {
1936        let engine = SimpleNl2SqlEngine::new();
1937        let query = SqlQuery {
1938            sql: "DROP TABLE users".into(),
1939            explanation: "test".into(),
1940            confidence: 0.9,
1941        };
1942        assert!(!engine.validate(&query).await.unwrap());
1943    }
1944
1945    #[tokio::test]
1946    async fn test_simple_alias() {
1947        let engine = SimpleNl2SqlEngine::new().with_alias("person", "users");
1948        let schema = test_schema();
1949        let result = engine.generate("show all persons", &schema).await.unwrap();
1950        assert_eq!(result.sql, "SELECT * FROM users");
1951    }
1952
1953    #[tokio::test]
1954    async fn test_simple_table_not_found() {
1955        let engine = SimpleNl2SqlEngine::new();
1956        let schema = test_schema();
1957        let result = engine
1958            .generate("show something from nonexistent_table", &schema)
1959            .await;
1960        assert!(result.is_err());
1961    }
1962
1963    #[tokio::test]
1964    async fn test_simple_sql_in_schema_passthrough() {
1965        let engine = SimpleNl2SqlEngine::new();
1966        let schema = test_schema();
1967        // "name" and "email" are both schema columns
1968        let result = engine
1969            .generate("select name and email from users", &schema)
1970            .await
1971            .unwrap();
1972        assert!(result.sql.contains("name"));
1973        assert!(result.sql.contains("email"));
1974        assert!(result.sql.contains("users"));
1975    }
1976
1977    // ============ 仅在 real feature 下测试 OpenAINl2SqlEngine ============
1978
1979    #[cfg(feature = "real")]
1980    #[test]
1981    fn test_openai_engine_new_with_defaults() {
1982        let engine = OpenAINl2SqlEngine::new("sk-test-key");
1983        assert_eq!(engine.api_base, "https://api.openai.com/v1");
1984        assert_eq!(engine.api_key, "sk-test-key");
1985        assert_eq!(engine.model, "gpt-4o-mini");
1986    }
1987
1988    #[cfg(feature = "real")]
1989    #[test]
1990    fn test_openai_engine_with_options() {
1991        let engine = OpenAINl2SqlEngine::new("sk-test")
1992            .with_api_base("https://api.deepseek.com/v1")
1993            .with_model("deepseek-chat");
1994        assert_eq!(engine.api_base, "https://api.deepseek.com/v1");
1995        assert_eq!(engine.model, "deepseek-chat");
1996    }
1997
1998    #[cfg(feature = "real")]
1999    #[tokio::test]
2000    async fn test_openai_engine_missing_api_key() {
2001        let engine = OpenAINl2SqlEngine::new("");
2002        let schema = test_schema();
2003        let result = engine.generate("show users", &schema).await;
2004        match result {
2005            Err(Nl2SqlError::ConfigError(_)) => {}
2006            other => panic!("期望 ConfigError,实际: {:?}", other),
2007        }
2008    }
2009
2010    #[cfg(feature = "real")]
2011    #[test]
2012    fn test_clean_llm_sql_output() {
2013        assert_eq!(
2014            clean_llm_sql_output("SELECT * FROM users"),
2015            "SELECT * FROM users"
2016        );
2017        assert_eq!(
2018            clean_llm_sql_output("```sql\nSELECT * FROM users\n```"),
2019            "SELECT * FROM users"
2020        );
2021        assert_eq!(
2022            clean_llm_sql_output("```\nSELECT * FROM users\n```"),
2023            "SELECT * FROM users"
2024        );
2025        assert_eq!(
2026            clean_llm_sql_output("\n  SELECT * FROM users  \n"),
2027            "SELECT * FROM users"
2028        );
2029    }
2030
2031    // ============ 查询优化提示测试 ============
2032
2033    fn optimizer_test_schema() -> SchemaContext {
2034        SchemaContext {
2035            tables: vec![
2036                TableInfo {
2037                    name: "users".into(),
2038                    columns: vec![
2039                        ColumnInfo {
2040                            name: "id".into(),
2041                            data_type: "INTEGER".into(),
2042                            nullable: false,
2043                            is_primary_key: true,
2044                        },
2045                        ColumnInfo {
2046                            name: "name".into(),
2047                            data_type: "TEXT".into(),
2048                            nullable: true,
2049                            is_primary_key: false,
2050                        },
2051                        ColumnInfo {
2052                            name: "email".into(),
2053                            data_type: "TEXT".into(),
2054                            nullable: true,
2055                            is_primary_key: false,
2056                        },
2057                        ColumnInfo {
2058                            name: "age".into(),
2059                            data_type: "INTEGER".into(),
2060                            nullable: true,
2061                            is_primary_key: false,
2062                        },
2063                    ],
2064                },
2065                TableInfo {
2066                    name: "orders".into(),
2067                    columns: vec![
2068                        ColumnInfo {
2069                            name: "id".into(),
2070                            data_type: "INTEGER".into(),
2071                            nullable: false,
2072                            is_primary_key: true,
2073                        },
2074                        ColumnInfo {
2075                            name: "user_id".into(),
2076                            data_type: "INTEGER".into(),
2077                            nullable: false,
2078                            is_primary_key: false,
2079                        },
2080                        ColumnInfo {
2081                            name: "amount".into(),
2082                            data_type: "DECIMAL".into(),
2083                            nullable: true,
2084                            is_primary_key: false,
2085                        },
2086                    ],
2087                },
2088            ],
2089        }
2090    }
2091
2092    #[test]
2093    fn test_hint_severity_as_str() {
2094        assert_eq!(HintSeverity::Info.as_str(), "INFO");
2095        assert_eq!(HintSeverity::Warning.as_str(), "WARNING");
2096        assert_eq!(HintSeverity::Critical.as_str(), "CRITICAL");
2097    }
2098
2099    #[test]
2100    fn test_query_optimization_hint_info() {
2101        let hint = QueryOptimizationHint::info("标题", "描述");
2102        assert_eq!(hint.title, "标题");
2103        assert_eq!(hint.description, "描述");
2104        assert_eq!(hint.severity, HintSeverity::Info);
2105        assert!(hint.suggested_sql.is_none());
2106    }
2107
2108    #[test]
2109    fn test_query_optimization_hint_warning() {
2110        let hint = QueryOptimizationHint::warning("警告", "警告描述");
2111        assert_eq!(hint.severity, HintSeverity::Warning);
2112    }
2113
2114    #[test]
2115    fn test_query_optimization_hint_critical() {
2116        let hint = QueryOptimizationHint::critical("严重", "严重描述");
2117        assert_eq!(hint.severity, HintSeverity::Critical);
2118    }
2119
2120    #[test]
2121    fn test_query_optimization_hint_with_suggested_sql() {
2122        let hint =
2123            QueryOptimizationHint::info("建议", "描述").with_suggested_sql("SELECT id FROM users");
2124        assert_eq!(hint.suggested_sql.as_deref(), Some("SELECT id FROM users"));
2125    }
2126
2127    #[test]
2128    fn test_query_optimizer_default() {
2129        let opt = QueryOptimizer::default();
2130        assert!(opt.check_select_star);
2131        assert!(opt.check_missing_limit);
2132        assert!(opt.check_missing_where);
2133        assert_eq!(opt.default_limit, 100);
2134    }
2135
2136    #[test]
2137    fn test_query_optimizer_new() {
2138        let opt = QueryOptimizer::new();
2139        assert!(opt.check_select_star);
2140    }
2141
2142    #[test]
2143    fn test_query_optimizer_with_default_limit() {
2144        let opt = QueryOptimizer::new().with_default_limit(50);
2145        assert_eq!(opt.default_limit, 50);
2146    }
2147
2148    #[test]
2149    fn test_query_optimizer_disable_checks() {
2150        let opt = QueryOptimizer::new()
2151            .disable_select_star_check()
2152            .disable_missing_limit_check()
2153            .disable_missing_where_check();
2154        assert!(!opt.check_select_star);
2155        assert!(!opt.check_missing_limit);
2156        assert!(!opt.check_missing_where);
2157    }
2158
2159    #[test]
2160    fn test_analyze_select_star() {
2161        let opt = QueryOptimizer::new();
2162        let schema = optimizer_test_schema();
2163        let analysis = opt.analyze("SELECT * FROM users", &schema);
2164
2165        assert!(analysis.uses_select_star);
2166        assert!(!analysis.has_where);
2167        assert!(!analysis.has_limit);
2168        // 应该有 SELECT * 建议、缺失 WHERE 建议、缺失 LIMIT 建议
2169        assert!(analysis.has_hints());
2170        assert!(analysis.critical_count() >= 1); // 缺失 WHERE 是 critical
2171    }
2172
2173    #[test]
2174    fn test_analyze_select_star_with_suggested_columns() {
2175        let opt = QueryOptimizer::new();
2176        let schema = optimizer_test_schema();
2177        let analysis = opt.analyze("SELECT * FROM users", &schema);
2178
2179        // 应包含 SELECT * 警告,且附带建议 SQL
2180        let select_star_hint = analysis
2181            .hints
2182            .iter()
2183            .find(|h| h.title == "避免使用 SELECT *");
2184        assert!(select_star_hint.is_some());
2185        let hint = select_star_hint.unwrap();
2186        assert!(hint.suggested_sql.is_some());
2187        let suggested = hint.suggested_sql.as_ref().unwrap();
2188        assert!(suggested.contains("users.id"));
2189        assert!(suggested.contains("users.name"));
2190    }
2191
2192    #[test]
2193    fn test_analyze_missing_where_critical() {
2194        let opt = QueryOptimizer::new();
2195        let schema = optimizer_test_schema();
2196        let analysis = opt.analyze("SELECT id FROM users LIMIT 10", &schema);
2197
2198        // 没有 WHERE 应产生 critical 建议
2199        assert!(analysis.critical_count() >= 1);
2200        let where_hint = analysis.hints.iter().find(|h| h.title == "缺少 WHERE 子句");
2201        assert!(where_hint.is_some());
2202        assert_eq!(where_hint.unwrap().severity, HintSeverity::Critical);
2203    }
2204
2205    #[test]
2206    fn test_analyze_missing_limit_warning() {
2207        let opt = QueryOptimizer::new();
2208        let schema = optimizer_test_schema();
2209        let analysis = opt.analyze("SELECT id FROM users WHERE age > 18", &schema);
2210
2211        // 没有 LIMIT 应产生 warning 建议
2212        assert!(analysis.warning_count() >= 1);
2213        let limit_hint = analysis.hints.iter().find(|h| h.title == "缺少 LIMIT 子句");
2214        assert!(limit_hint.is_some());
2215        let hint = limit_hint.unwrap();
2216        assert!(hint.suggested_sql.is_some());
2217        assert!(hint.suggested_sql.as_ref().unwrap().contains("LIMIT 100"));
2218    }
2219
2220    #[test]
2221    fn test_analyze_with_limit_no_limit_hint() {
2222        let opt = QueryOptimizer::new();
2223        let schema = optimizer_test_schema();
2224        let analysis = opt.analyze("SELECT id FROM users WHERE age > 18 LIMIT 10", &schema);
2225
2226        let limit_hint = analysis.hints.iter().find(|h| h.title == "缺少 LIMIT 子句");
2227        assert!(limit_hint.is_none());
2228    }
2229
2230    #[test]
2231    fn test_analyze_join_detection() {
2232        let opt = QueryOptimizer::new();
2233        let schema = optimizer_test_schema();
2234        let analysis = opt.analyze(
2235            "SELECT u.id FROM users u JOIN orders o ON u.id = o.user_id WHERE u.age > 18 LIMIT 10",
2236            &schema,
2237        );
2238
2239        assert!(analysis.has_join);
2240        assert!(analysis.detected_tables.contains(&"users".to_string()));
2241        assert!(analysis.detected_tables.contains(&"orders".to_string()));
2242    }
2243
2244    #[test]
2245    fn test_analyze_multiple_joins_critical() {
2246        let opt = QueryOptimizer::new();
2247        let schema = SchemaContext {
2248            tables: vec![
2249                TableInfo {
2250                    name: "t1".into(),
2251                    columns: vec![ColumnInfo {
2252                        name: "id".into(),
2253                        data_type: "INT".into(),
2254                        nullable: false,
2255                        is_primary_key: true,
2256                    }],
2257                },
2258                TableInfo {
2259                    name: "t2".into(),
2260                    columns: vec![ColumnInfo {
2261                        name: "id".into(),
2262                        data_type: "INT".into(),
2263                        nullable: false,
2264                        is_primary_key: true,
2265                    }],
2266                },
2267                TableInfo {
2268                    name: "t3".into(),
2269                    columns: vec![ColumnInfo {
2270                        name: "id".into(),
2271                        data_type: "INT".into(),
2272                        nullable: false,
2273                        is_primary_key: true,
2274                    }],
2275                },
2276                TableInfo {
2277                    name: "t4".into(),
2278                    columns: vec![ColumnInfo {
2279                        name: "id".into(),
2280                        data_type: "INT".into(),
2281                        nullable: false,
2282                        is_primary_key: true,
2283                    }],
2284                },
2285            ],
2286        };
2287        let analysis = opt.analyze(
2288            "SELECT * FROM t1 JOIN t2 ON t1.id = t2.id JOIN t3 ON t2.id = t3.id JOIN t4 ON t3.id = t4.id LIMIT 10",
2289            &schema,
2290        );
2291        // 4 个 JOIN 应触发 critical
2292        let join_hint = analysis.hints.iter().find(|h| h.title == "JOIN 数量过多");
2293        assert!(join_hint.is_some());
2294        assert_eq!(join_hint.unwrap().severity, HintSeverity::Critical);
2295    }
2296
2297    #[test]
2298    fn test_analyze_subquery_detection() {
2299        let opt = QueryOptimizer::new();
2300        let schema = optimizer_test_schema();
2301        let analysis = opt.analyze(
2302            "SELECT id FROM users WHERE id IN (SELECT user_id FROM orders) LIMIT 10",
2303            &schema,
2304        );
2305
2306        assert!(analysis.has_subquery);
2307    }
2308
2309    #[test]
2310    fn test_analyze_leading_wildcard_like() {
2311        let opt = QueryOptimizer::new();
2312        let schema = optimizer_test_schema();
2313        let analysis = opt.analyze(
2314            "SELECT id FROM users WHERE name LIKE '%john' LIMIT 10",
2315            &schema,
2316        );
2317
2318        let like_hint = analysis
2319            .hints
2320            .iter()
2321            .find(|h| h.title == "LIKE 使用前缀通配符");
2322        assert!(like_hint.is_some());
2323    }
2324
2325    #[test]
2326    fn test_analyze_order_by_without_limit() {
2327        let opt = QueryOptimizer::new();
2328        let schema = optimizer_test_schema();
2329        let analysis = opt.analyze("SELECT id FROM users WHERE age > 18 ORDER BY id", &schema);
2330
2331        let order_hint = analysis
2332            .hints
2333            .iter()
2334            .find(|h| h.title == "ORDER BY 无 LIMIT");
2335        assert!(order_hint.is_some());
2336    }
2337
2338    #[test]
2339    fn test_analyze_count_star_hint() {
2340        let opt = QueryOptimizer::new();
2341        let schema = optimizer_test_schema();
2342        let analysis = opt.analyze("SELECT COUNT(*) FROM users LIMIT 1", &schema);
2343
2344        let count_hint = analysis
2345            .hints
2346            .iter()
2347            .find(|h| h.title == "考虑使用 COUNT(1)");
2348        assert!(count_hint.is_some());
2349    }
2350
2351    #[test]
2352    fn test_analyze_missing_index_hint() {
2353        let opt = QueryOptimizer::new();
2354        let schema = optimizer_test_schema();
2355        // age 列不是主键,应建议添加索引
2356        let analysis = opt.analyze("SELECT id FROM users WHERE age > 18 LIMIT 10", &schema);
2357
2358        let index_hint = analysis.hints.iter().find(|h| h.title.contains("添加索引"));
2359        assert!(index_hint.is_some());
2360        assert!(index_hint.unwrap().title.contains("age"));
2361    }
2362
2363    #[test]
2364    fn test_analyze_primary_key_no_index_hint() {
2365        let opt = QueryOptimizer::new();
2366        let schema = optimizer_test_schema();
2367        // id 列是主键,不应建议添加索引
2368        let analysis = opt.analyze("SELECT name FROM users WHERE id = 1 LIMIT 10", &schema);
2369
2370        let index_hint = analysis
2371            .hints
2372            .iter()
2373            .find(|h| h.title.contains("添加索引") && h.title.contains("id"));
2374        // id 是主键,不应有索引建议
2375        assert!(index_hint.is_none());
2376    }
2377
2378    #[test]
2379    fn test_analyze_complexity_score_simple() {
2380        let opt = QueryOptimizer::new();
2381        let schema = optimizer_test_schema();
2382        let analysis = opt.analyze("SELECT id FROM users WHERE id = 1 LIMIT 10", &schema);
2383        // 简单查询应该低分
2384        assert!(analysis.complexity_score < 30);
2385    }
2386
2387    #[test]
2388    fn test_analyze_complexity_score_complex() {
2389        let opt = QueryOptimizer::new();
2390        let schema = optimizer_test_schema();
2391        let analysis = opt.analyze(
2392            "SELECT u.id, o.amount FROM users u JOIN orders o ON u.id = o.user_id WHERE u.age > 18 OR u.name LIKE '%a%' GROUP BY u.id ORDER BY o.amount DESC",
2393            &schema,
2394        );
2395        // 复杂查询应该高分
2396        assert!(analysis.complexity_score > 30);
2397    }
2398
2399    #[test]
2400    fn test_analyze_well_optimized_query() {
2401        let opt = QueryOptimizer::new();
2402        let schema = optimizer_test_schema();
2403        let analysis = opt.analyze("SELECT id, name FROM users WHERE id = 1 LIMIT 10", &schema);
2404
2405        // 这个查询写得很好,不应该有 critical 或 warning 建议
2406        assert_eq!(analysis.critical_count(), 0);
2407        // id 是主键,不应有索引建议
2408        // 有 LIMIT,不应有 LIMIT 建议
2409        // 有 WHERE,不应有 WHERE 建议
2410        // 没有 SELECT *,不应有 SELECT * 建议
2411    }
2412
2413    #[test]
2414    fn test_query_analysis_has_hints() {
2415        let opt = QueryOptimizer::new();
2416        let schema = optimizer_test_schema();
2417        let analysis = opt.analyze("SELECT * FROM users", &schema);
2418        assert!(analysis.has_hints());
2419
2420        let good_analysis = opt.analyze("SELECT id FROM users WHERE id = 1 LIMIT 1", &schema);
2421        // 可能仍有 info 级建议,但不应有 critical
2422        assert_eq!(good_analysis.critical_count(), 0);
2423    }
2424
2425    #[test]
2426    fn test_format_report_contains_key_info() {
2427        let opt = QueryOptimizer::new();
2428        let schema = optimizer_test_schema();
2429        let analysis = opt.analyze("SELECT * FROM users", &schema);
2430        let report = QueryOptimizer::format_report(&analysis);
2431
2432        assert!(report.contains("SQL 查询优化分析报告"));
2433        assert!(report.contains("原始 SQL"));
2434        assert!(report.contains("复杂度评分"));
2435        assert!(report.contains("SELECT *"));
2436    }
2437
2438    #[test]
2439    fn test_format_report_no_hints() {
2440        let opt = QueryOptimizer::new();
2441        let schema = optimizer_test_schema();
2442        let analysis = opt.analyze("SELECT id FROM users WHERE id = 1 LIMIT 1", &schema);
2443        let report = QueryOptimizer::format_report(&analysis);
2444        // 即使没有建议,报告也应包含基本字段
2445        assert!(report.contains("复杂度评分"));
2446    }
2447
2448    #[test]
2449    fn test_analyze_disable_select_star() {
2450        let opt = QueryOptimizer::new().disable_select_star_check();
2451        let schema = optimizer_test_schema();
2452        let analysis = opt.analyze("SELECT * FROM users WHERE id = 1 LIMIT 10", &schema);
2453
2454        let star_hint = analysis
2455            .hints
2456            .iter()
2457            .find(|h| h.title == "避免使用 SELECT *");
2458        assert!(star_hint.is_none());
2459    }
2460
2461    #[test]
2462    fn test_analyze_disable_missing_where() {
2463        let opt = QueryOptimizer::new().disable_missing_where_check();
2464        let schema = optimizer_test_schema();
2465        let analysis = opt.analyze("SELECT id FROM users LIMIT 10", &schema);
2466
2467        let where_hint = analysis.hints.iter().find(|h| h.title == "缺少 WHERE 子句");
2468        assert!(where_hint.is_none());
2469    }
2470
2471    #[test]
2472    fn test_analyze_disable_missing_limit() {
2473        let opt = QueryOptimizer::new().disable_missing_limit_check();
2474        let schema = optimizer_test_schema();
2475        let analysis = opt.analyze("SELECT id FROM users WHERE id = 1", &schema);
2476
2477        let limit_hint = analysis.hints.iter().find(|h| h.title == "缺少 LIMIT 子句");
2478        assert!(limit_hint.is_none());
2479    }
2480
2481    #[test]
2482    fn test_analyze_multiple_or_conditions() {
2483        let opt = QueryOptimizer::new();
2484        let schema = optimizer_test_schema();
2485        let analysis = opt.analyze(
2486            "SELECT id FROM users WHERE age = 1 OR age = 2 OR age = 3 OR age = 4 LIMIT 10",
2487            &schema,
2488        );
2489
2490        let or_hint = analysis.hints.iter().find(|h| h.title == "多个 OR 条件");
2491        assert!(or_hint.is_some());
2492    }
2493
2494    #[test]
2495    fn test_analyze_detected_tables() {
2496        let opt = QueryOptimizer::new();
2497        let schema = optimizer_test_schema();
2498        let analysis = opt.analyze("SELECT id FROM users LIMIT 10", &schema);
2499
2500        assert_eq!(analysis.detected_tables, vec!["users".to_string()]);
2501    }
2502
2503    #[test]
2504    fn test_analyze_no_detected_tables() {
2505        let opt = QueryOptimizer::new();
2506        let schema = optimizer_test_schema();
2507        let analysis = opt.analyze("SELECT 1 LIMIT 10", &schema);
2508
2509        assert!(analysis.detected_tables.is_empty());
2510    }
2511
2512    #[test]
2513    fn test_normalize_sql() {
2514        let normalized = QueryOptimizer::normalize_sql("SELECT  id\nFROM   users");
2515        assert_eq!(normalized, "SELECT id FROM users");
2516    }
2517}