1use async_trait::async_trait;
10use thiserror::Error;
11
12use crate::safety;
13
14#[cfg(feature = "real")]
15use serde::{Deserialize, Serialize};
16
17#[derive(Debug, Clone)]
21pub struct SqlQuery {
22 pub sql: String,
24 pub explanation: String,
26 pub confidence: f32,
28}
29
30#[derive(Debug, Clone, Default)]
32pub struct SchemaContext {
33 pub tables: Vec<TableInfo>,
34}
35
36#[derive(Debug, Clone)]
38pub struct TableInfo {
39 pub name: String,
40 pub columns: Vec<ColumnInfo>,
41}
42
43#[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#[derive(Debug, Error)]
56pub enum Nl2SqlError {
57 #[error("Invalid query: {0}")]
59 InvalidQuery(String),
60 #[error("Schema error: {0}")]
62 SchemaError(String),
63 #[error("Safety error: {0}")]
65 SafetyError(String),
66 #[error("Generation error: {0}")]
68 GenerationError(String),
69 #[error("API error (status {0}): {1}")]
71 ApiError(u16, String),
72 #[error("Network error: {0}")]
74 NetworkError(String),
75 #[error("Config error: {0}")]
77 ConfigError(String),
78}
79
80#[async_trait]
86pub trait Nl2SqlEngine: Send + Sync {
87 async fn generate(
97 &self,
98 nl_query: &str,
99 schema: &SchemaContext,
100 ) -> Result<SqlQuery, Nl2SqlError>;
101
102 async fn validate(&self, query: &SqlQuery) -> Result<bool, Nl2SqlError>;
108}
109
110pub struct SimpleNl2SqlEngine {
149 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 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 fn find_table<'a>(&self, query: &str, schema: &'a SchemaContext) -> Option<&'a TableInfo> {
175 let lower = query.to_lowercase();
176
177 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 for (alias, target_table_name) in &self.aliases {
197 if let Some(pos) = lower.find(alias) {
198 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 fn extract_columns_from_nl(query: &str, table: &TableInfo) -> Vec<String> {
221 let lower = query.to_lowercase();
222 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 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 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 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 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 if col_lower.len() >= 2
286 && (lower.contains(&col_lower) || lower.contains(&format!(" {} ", col_lower)))
287 {
288 if !columns.contains(&col.name) {
290 columns.push(col.name.clone());
291 }
292 }
293 }
294
295 columns
296 }
297
298 fn find_join_columns(t1: &TableInfo, t2: &TableInfo) -> Option<(String, String)> {
300 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 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 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 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 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 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 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 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 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 let columns = Self::extract_columns_from_nl(nl_query, table);
465
466 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 let after = nl_query[pos + 5..].trim();
474 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 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 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 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 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 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 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 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 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 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 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 let where_clause = if conditions.is_empty() {
694 String::new()
695 } else {
696 format!(" WHERE {}", conditions.join(" AND "))
698 };
699
700 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 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 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 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#[cfg(feature = "real")]
817pub struct OpenAINl2SqlEngine {
818 api_base: String,
820 api_key: String,
822 model: String,
824 http_client: reqwest::Client,
826}
827
828#[cfg(feature = "real")]
829impl OpenAINl2SqlEngine {
830 const DEFAULT_API_BASE: &'static str = "https://api.openai.com/v1";
832 const DEFAULT_MODEL: &'static str = "gpt-4o-mini";
834
835 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 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 pub fn with_model(mut self, model: impl Into<String>) -> Self {
853 self.model = model.into();
854 self
855 }
856
857 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 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 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 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#[cfg(feature = "real")]
1046fn clean_llm_sql_output(raw: &str) -> String {
1047 let trimmed = raw.trim();
1048 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1069pub enum HintSeverity {
1070 Info,
1072 Warning,
1074 Critical,
1076}
1077
1078impl HintSeverity {
1079 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#[derive(Debug, Clone)]
1091pub struct QueryOptimizationHint {
1092 pub title: String,
1094 pub description: String,
1096 pub severity: HintSeverity,
1098 pub suggested_sql: Option<String>,
1100}
1101
1102impl QueryOptimizationHint {
1103 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 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 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 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#[derive(Debug, Clone)]
1142pub struct QueryAnalysis {
1143 pub original_sql: String,
1145 pub hints: Vec<QueryOptimizationHint>,
1147 pub complexity_score: u32,
1149 pub detected_tables: Vec<String>,
1151 pub has_where: bool,
1153 pub has_limit: bool,
1155 pub has_join: bool,
1157 pub has_subquery: bool,
1159 pub uses_select_star: bool,
1161}
1162
1163impl QueryAnalysis {
1164 pub fn critical_count(&self) -> usize {
1166 self.hints
1167 .iter()
1168 .filter(|h| h.severity == HintSeverity::Critical)
1169 .count()
1170 }
1171
1172 pub fn warning_count(&self) -> usize {
1174 self.hints
1175 .iter()
1176 .filter(|h| h.severity == HintSeverity::Warning)
1177 .count()
1178 }
1179
1180 pub fn has_hints(&self) -> bool {
1182 !self.hints.is_empty()
1183 }
1184}
1185
1186pub struct QueryOptimizer {
1191 pub check_select_star: bool,
1193 pub check_missing_limit: bool,
1195 pub check_missing_where: bool,
1197 pub default_limit: usize,
1199 pub join_weight: u32,
1201 pub subquery_weight: u32,
1203 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 pub fn new() -> Self {
1224 Self::default()
1225 }
1226
1227 pub fn with_default_limit(mut self, limit: usize) -> Self {
1229 self.default_limit = limit;
1230 self
1231 }
1232
1233 pub fn disable_select_star_check(mut self) -> Self {
1235 self.check_select_star = false;
1236 self
1237 }
1238
1239 pub fn disable_missing_limit_check(mut self) -> Self {
1241 self.check_missing_limit = false;
1242 self
1243 }
1244
1245 pub fn disable_missing_where_check(mut self) -> Self {
1247 self.check_missing_where = false;
1248 self
1249 }
1250
1251 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 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 if self.check_missing_where && !has_where {
1284 hints.push(QueryOptimizationHint::critical(
1285 "缺少 WHERE 子句",
1286 "查询没有 WHERE 条件,将扫描全表。对于大表这会导致严重的性能问题。",
1287 ));
1288 }
1289
1290 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 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 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 if Self::has_leading_wildcard_like(&lower) {
1340 hints.push(QueryOptimizationHint::warning(
1341 "LIKE 使用前缀通配符",
1342 "LIKE '%keyword' 无法使用索引,会导致全表扫描。如可能,使用 LIKE 'keyword%' 或全文索引。",
1343 ));
1344 }
1345
1346 if Self::count_or_conditions(&lower) >= 3 {
1348 hints.push(QueryOptimizationHint::info(
1349 "多个 OR 条件",
1350 "多个 OR 条件可能导致无法有效使用索引。考虑使用 UNION ALL 重写。",
1351 ));
1352 }
1353
1354 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 if lower.contains("count(*)") {
1364 hints.push(QueryOptimizationHint::info(
1365 "考虑使用 COUNT(1)",
1366 "某些数据库中 COUNT(1) 比 COUNT(*) 略快(虽然现代优化器通常已优化)。",
1367 ));
1368 }
1369
1370 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 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 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)); 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 fn normalize_sql(sql: &str) -> String {
1454 sql.split_whitespace().collect::<Vec<_>>().join(" ")
1455 }
1456
1457 fn detect_select_star(&self, lower: &str) -> bool {
1459 lower.contains("select *") || lower.contains("select *")
1460 }
1461
1462 fn detect_where(&self, lower: &str) -> bool {
1464 lower.contains(" where ")
1465 }
1466
1467 fn detect_limit(&self, lower: &str) -> bool {
1469 lower.contains(" limit ")
1470 }
1471
1472 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 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 fn count_joins(lower: &str) -> usize {
1494 lower.matches(" join ").count()
1495 }
1496
1497 fn count_subqueries(lower: &str) -> usize {
1499 lower.matches("(select").count() + lower.matches("( select").count()
1500 }
1501
1502 fn has_leading_wildcard_like(lower: &str) -> bool {
1504 lower.contains("like '%") || lower.contains("like \"%")
1505 }
1506
1507 fn count_or_conditions(lower: &str) -> usize {
1509 lower.matches(" or ").count()
1510 }
1511
1512 fn has_order_by_without_limit(lower: &str) -> bool {
1514 lower.contains(" order by ") && !lower.contains(" limit ")
1515 }
1516
1517 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 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 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 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 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 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 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; 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 if tables.len() > 1 {
1627 score += (tables.len() as u32 - 1) * 10;
1628 }
1629
1630 if lower.contains(" order by ") {
1632 score += 5;
1633 }
1634
1635 if lower.contains(" group by ") {
1637 score += 10;
1638 }
1639
1640 if lower.contains(" distinct ") {
1642 score += 5;
1643 }
1644
1645 score.min(100)
1646 }
1647}
1648
1649#[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 #[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 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 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 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 #[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 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 assert!(analysis.has_hints());
2170 assert!(analysis.critical_count() >= 1); }
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 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 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 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 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 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 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 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 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 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 assert_eq!(analysis.critical_count(), 0);
2407 }
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 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 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}