Skip to main content

sz_rust_orm_ext_facade/relation/
n_plus_one.rs

1//! N+1 问题检测 — SQL 计数 + 模板分组 + 告警生成
2//!
3//! 本模块提供 N+1 问题检测能力,通过 SQL 查询计数
4//! 与模板分组识别 N+1 模式,对齐 PHP `with()` 批量预加载机制避免 N+1 问题。
5//!
6//! ## PHP 端 N+1 问题背景
7//!
8//! PHP think-orm 2.0.x 通过 `with()` + `eagerlyResultSet()` 提供批量预加载能力,
9//! **避免 N+1 问题**(一次查询加载 N 条父模型,再 N 次查询加载每条父模型的关联数据)。
10//! 但 PHP 端**不主动检测 N+1 问题**,开发者需要自行识别并使用 `with()` 规避。
11//!
12//! ### PHP `with()` 批量预加载机制
13//!
14//! ```php
15//! // ❌ N+1 模式(N 次查询)
16//! $users = User::select();
17//! foreach ($users as $user) {
18//!     $orders = $user->orders;  // 每次循环触发一次 SQL 查询
19//! }
20//!
21//! // ✅ 批量预加载(2 次查询)
22//! $users = User::with('orders')->select();
23//! // 内部通过 eagerlyResultSet() 批量 IN 查询
24//! ```
25//!
26//! ## 本模块提供的检测能力
27//!
28//! sz-rust 端作为 PHP 端的扩展,提供**主动 N+1 问题检测**能力:
29//!
30//! 1. [`SqlQueryRecord`]:SQL 查询记录(原始 SQL + 模板 + 表名 + 时间戳 + 序号)
31//! 2. [`DetectionConfig`]:检测配置(阈值 + 时间窗口)
32//! 3. [`NPlusOneAlert`]:N+1 告警(模板 + 表名 + 次数 + 时间跨度 + 建议)
33//! 4. [`NPlusOneDetector`]:检测器(累积记录 + 批量分析)
34//! 5. [`extract_template`]:SQL 模板提取(参数替换为 `?`)
35//! 6. [`detect_n_plus_one`]:核心检测函数(按模板分组 + 时间窗口分析)
36//! 7. [`suggest_with_usage`]:生成 `with()` 使用建议
37//!
38//! ## N+1 检测算法
39//!
40//! 1. 收集 SQL 查询记录(`SqlQueryRecord`)
41//! 2. 按 SQL 模板分组(`extract_template` 去除具体参数)
42//! 3. 每组按时间戳排序
43//! 4. 检查每组在时间窗口内是否有超过阈值的查询
44//! 5. 如果有,生成告警(`NPlusOneAlert`)
45//!
46//! ## 架构说明
47//!
48//! sz-orm-core::model 模块私有(`mod model;` 非 `pub mod model;`),sz-rust 端无法
49//! 实现 `Model`/`RelationLoader` trait,因此本模块不直接执行 SQL 查询,而是提供:
50//!
51//! - **SQL 查询记录类型**:`SqlQueryRecord` 供调用方(如中间件 / Repository)记录
52//! - **检测器**:`NPlusOneDetector` 累积记录并批量分析
53//! - **核心检测函数**:`detect_n_plus_one` 纯函数,可独立调用
54//! - **建议生成**:`suggest_with_usage` 生成 `with()` 使用建议
55//!
56//! 端到端 SQL 执行计数由调用方集成(如 `tracing` 中间件 / `Repository` 包装器)。
57
58use std::collections::HashMap;
59
60// ============================================================================
61// SQL 查询记录
62// ============================================================================
63
64/// SQL 查询记录
65///
66/// 记录单条 SQL 查询的原始 SQL、模板、表名、时间戳与序号。
67///
68/// ## 字段
69///
70/// - `sql`:原始 SQL 字符串(含具体参数)
71/// - `template`:SQL 模板(参数替换为 `?`,用于分组)
72/// - `table`:主表名(如 `"users"`)
73/// - `timestamp_ms`:查询时间戳(毫秒)
74/// - `query_index`:查询序号(从 0 开始递增)
75///
76/// ## 示例
77///
78/// ```ignore
79/// use sz_rust_core::relation::n_plus_one::SqlQueryRecord;
80///
81/// let record = SqlQueryRecord::new(
82///     "SELECT * FROM orders WHERE user_id = 1",
83///     "orders",
84///     1000,
85///     0,
86/// );
87/// assert_eq!(record.template(), "SELECT * FROM orders WHERE user_id = ?");
88/// ```
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct SqlQueryRecord {
91    /// 原始 SQL 字符串(含具体参数)
92    pub sql: String,
93    /// SQL 模板(参数替换为 `?`,用于分组)
94    pub template: String,
95    /// 主表名
96    pub table: String,
97    /// 查询时间戳(毫秒)
98    pub timestamp_ms: u64,
99    /// 查询序号(从 0 开始递增)
100    pub query_index: u64,
101}
102
103impl SqlQueryRecord {
104    /// 创建新的 SQL 查询记录
105    ///
106    /// 自动调用 [`extract_template`] 提取 SQL 模板。
107    ///
108    /// ## 参数
109    ///
110    /// - `sql`:原始 SQL 字符串
111    /// - `table`:主表名
112    /// - `timestamp_ms`:查询时间戳(毫秒)
113    /// - `query_index`:查询序号
114    pub fn new(sql: &str, table: &str, timestamp_ms: u64, query_index: u64) -> Self {
115        Self {
116            sql: sql.to_string(),
117            template: extract_template(sql),
118            table: table.to_string(),
119            timestamp_ms,
120            query_index,
121        }
122    }
123
124    /// 获取原始 SQL 字符串
125    pub fn sql(&self) -> &str {
126        &self.sql
127    }
128
129    /// 获取 SQL 模板
130    pub fn template(&self) -> &str {
131        &self.template
132    }
133
134    /// 获取主表名
135    pub fn table(&self) -> &str {
136        &self.table
137    }
138
139    /// 获取查询时间戳(毫秒)
140    pub fn timestamp_ms(&self) -> u64 {
141        self.timestamp_ms
142    }
143
144    /// 获取查询序号
145    pub fn query_index(&self) -> u64 {
146        self.query_index
147    }
148}
149
150// ============================================================================
151// SQL 模板提取
152// ============================================================================
153
154/// 提取 SQL 模板(将参数替换为 `?`)
155///
156/// 将 SQL 中的数字字面量、字符串字面量替换为 `?`,用于 N+1 检测的模板分组。
157///
158/// ## 替换规则
159///
160/// - 数字字面量(如 `1` / `123` / `3.14`)→ `?`
161/// - 字符串字面量(如 `'abc'` / `'user@example.com'`)→ `?`
162/// - 其他字符原样保留
163///
164/// ## 示例
165///
166/// ```ignore
167/// use sz_rust_core::relation::n_plus_one::extract_template;
168///
169/// assert_eq!(
170///     extract_template("SELECT * FROM orders WHERE user_id = 1"),
171///     "SELECT * FROM orders WHERE user_id = ?"
172/// );
173/// assert_eq!(
174///     extract_template("SELECT * FROM users WHERE email = 'abc@x.com' AND id = 5"),
175///     "SELECT * FROM users WHERE email = ? AND id = ?"
176/// );
177/// ```
178pub fn extract_template(sql: &str) -> String {
179    let chars: Vec<char> = sql.chars().collect();
180    let mut result = String::with_capacity(sql.len());
181    let mut i = 0;
182    while i < chars.len() {
183        let c = chars[i];
184        if c == '\'' {
185            // 字符串字面量:从 ' 到下一个 '
186            result.push('?');
187            i += 1;
188            while i < chars.len() && chars[i] != '\'' {
189                i += 1;
190            }
191            // 跳过结束的 '
192            if i < chars.len() {
193                i += 1;
194            }
195        } else if c == '"' {
196            // 双引号字符串字面量
197            result.push('?');
198            i += 1;
199            while i < chars.len() && chars[i] != '"' {
200                i += 1;
201            }
202            if i < chars.len() {
203                i += 1;
204            }
205        } else if c.is_ascii_digit() {
206            // 数字字面量:连续数字(含小数点)
207            result.push('?');
208            i += 1;
209            // 跳过连续数字和小数点
210            while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
211                i += 1;
212            }
213        } else {
214            result.push(c);
215            i += 1;
216        }
217    }
218    result
219}
220
221// ============================================================================
222// 检测配置
223// ============================================================================
224
225/// N+1 检测配置
226///
227/// 配置检测阈值与时间窗口。
228///
229/// ## 默认值
230///
231/// - `threshold`:5(同一模板在时间窗口内查询超过 5 次判定为 N+1)
232/// - `time_window_ms`:1000(时间窗口 1000 毫秒)
233///
234/// ## 示例
235///
236/// ```ignore
237/// use sz_rust_core::relation::n_plus_one::DetectionConfig;
238///
239/// let config = DetectionConfig::default();
240/// assert_eq!(config.threshold, 5);
241/// assert_eq!(config.time_window_ms, 1000);
242/// ```
243#[derive(Debug, Clone, PartialEq, Eq)]
244pub struct DetectionConfig {
245    /// 阈值(同一模板在时间窗口内查询超过此值判定为 N+1)
246    pub threshold: usize,
247    /// 时间窗口(毫秒)
248    pub time_window_ms: u64,
249}
250
251impl Default for DetectionConfig {
252    fn default() -> Self {
253        Self {
254            threshold: 5,
255            time_window_ms: 1000,
256        }
257    }
258}
259
260impl DetectionConfig {
261    /// 创建新的检测配置
262    pub fn new(threshold: usize, time_window_ms: u64) -> Self {
263        Self {
264            threshold,
265            time_window_ms,
266        }
267    }
268
269    /// 获取阈值
270    pub fn threshold(&self) -> usize {
271        self.threshold
272    }
273
274    /// 获取时间窗口(毫秒)
275    pub fn time_window_ms(&self) -> u64 {
276        self.time_window_ms
277    }
278}
279
280// ============================================================================
281// N+1 告警
282// ============================================================================
283
284/// N+1 告警
285///
286/// 当检测到 N+1 问题时生成,包含模板、表名、查询次数、时间跨度与建议。
287///
288/// ## 字段
289///
290/// - `template`:SQL 模板(参数替换为 `?`)
291/// - `table`:涉及的表名
292/// - `query_count`:查询次数
293/// - `time_span_ms`:时间跨度(毫秒)
294/// - `suggestion`:`with()` 使用建议
295///
296/// ## 示例
297///
298/// ```ignore
299/// use sz_rust_core::relation::n_plus_one::{NPlusOneAlert, suggest_with_usage};
300///
301/// let alert = NPlusOneAlert::new(
302///     "SELECT * FROM orders WHERE user_id = ?",
303///     "orders",
304///     10,
305///     500,
306/// );
307/// assert_eq!(alert.query_count, 10);
308/// assert!(alert.suggestion.contains("with"));
309/// ```
310#[derive(Debug, Clone, PartialEq, Eq)]
311pub struct NPlusOneAlert {
312    /// SQL 模板(参数替换为 `?`)
313    pub template: String,
314    /// 涉及的表名
315    pub table: String,
316    /// 查询次数
317    pub query_count: usize,
318    /// 时间跨度(毫秒)
319    pub time_span_ms: u64,
320    /// `with()` 使用建议
321    pub suggestion: String,
322}
323
324impl NPlusOneAlert {
325    /// 创建新的 N+1 告警
326    pub fn new(template: &str, table: &str, query_count: usize, time_span_ms: u64) -> Self {
327        Self {
328            template: template.to_string(),
329            table: table.to_string(),
330            query_count,
331            time_span_ms,
332            suggestion: suggest_with_usage(table, query_count),
333        }
334    }
335
336    /// 获取 SQL 模板
337    pub fn template(&self) -> &str {
338        &self.template
339    }
340
341    /// 获取表名
342    pub fn table(&self) -> &str {
343        &self.table
344    }
345
346    /// 获取查询次数
347    pub fn query_count(&self) -> usize {
348        self.query_count
349    }
350
351    /// 获取时间跨度(毫秒)
352    pub fn time_span_ms(&self) -> u64 {
353        self.time_span_ms
354    }
355
356    /// 获取建议
357    pub fn suggestion(&self) -> &str {
358        &self.suggestion
359    }
360}
361
362// ============================================================================
363// 建议生成
364// ============================================================================
365
366/// 生成 `with()` 使用建议
367///
368/// 根据表名与查询次数生成 `with()` 使用建议字符串。
369///
370/// ## 参数
371///
372/// - `table`:表名(如 `"orders"`)
373/// - `count`:查询次数
374///
375/// ## 示例
376///
377/// ```ignore
378/// use sz_rust_core::relation::n_plus_one::suggest_with_usage;
379///
380/// let suggestion = suggest_with_usage("orders", 10);
381/// assert!(suggestion.contains("with"));
382/// assert!(suggestion.contains("orders"));
383/// assert!(suggestion.contains("10"));
384/// ```
385pub fn suggest_with_usage(table: &str, count: usize) -> String {
386    format!(
387        "Detected N+1 problem: {} queries on table '{}' with same template. \
388         Consider using `with('{}')` for batch preloading to reduce {} queries to 1.",
389        count, table, table, count
390    )
391}
392
393// ============================================================================
394// 核心检测函数
395// ============================================================================
396
397/// 检测 N+1 问题
398///
399/// 按 SQL 模板分组,检查每组在时间窗口内是否有超过阈值的查询。
400///
401/// ## 算法
402///
403/// 1. 按 SQL 模板分组查询记录
404/// 2. 每组按时间戳排序
405/// 3. 检查每组在时间窗口内是否有超过阈值的查询
406/// 4. 如果有,生成告警
407///
408/// ## 参数
409///
410/// - `records`:SQL 查询记录列表
411/// - `config`:检测配置
412///
413/// ## 返回
414///
415/// N+1 告警列表(按查询次数降序排序)
416///
417/// ## 示例
418///
419/// ```ignore
420/// use sz_rust_core::relation::n_plus_one::*;
421///
422/// let records = vec![
423///     SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 1", "orders", 100, 0),
424///     SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 2", "orders", 200, 1),
425///     SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 3", "orders", 300, 2),
426///     SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 4", "orders", 400, 3),
427///     SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 5", "orders", 500, 4),
428///     SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 6", "orders", 600, 5),
429/// ];
430/// let config = DetectionConfig::new(5, 1000);
431/// let alerts = detect_n_plus_one(&records, &config);
432/// assert_eq!(alerts.len(), 1);
433/// assert_eq!(alerts[0].query_count, 6);
434/// ```
435pub fn detect_n_plus_one(
436    records: &[SqlQueryRecord],
437    config: &DetectionConfig,
438) -> Vec<NPlusOneAlert> {
439    // 1. 按 SQL 模板分组
440    let mut groups: HashMap<String, Vec<&SqlQueryRecord>> = HashMap::new();
441    for record in records {
442        groups
443            .entry(record.template.clone())
444            .or_default()
445            .push(record);
446    }
447
448    // 2. 每组按时间戳排序并检查
449    let mut alerts: Vec<NPlusOneAlert> = Vec::new();
450    for group_records in groups.values() {
451        // 按时间戳排序
452        let mut sorted_records: Vec<&&SqlQueryRecord> = group_records.iter().collect();
453        sorted_records.sort_by_key(|r| r.timestamp_ms);
454
455        if sorted_records.len() < config.threshold {
456            continue;
457        }
458
459        // 滑动窗口检查:在时间窗口内是否有超过阈值的查询
460        let window = config.time_window_ms;
461        let threshold = config.threshold;
462        let mut start = 0;
463        while start < sorted_records.len() {
464            let start_time = sorted_records[start].timestamp_ms;
465            let mut end = start;
466            while end < sorted_records.len()
467                && sorted_records[end].timestamp_ms <= start_time + window
468            {
469                end += 1;
470            }
471            // [start, end) 范围内的查询都在时间窗口内
472            let count = end - start;
473            if count >= threshold {
474                // 找到 N+1 模式
475                let template = sorted_records[start].template.clone();
476                let table = sorted_records[start].table.clone();
477                let time_span = if end > 0 {
478                    sorted_records[end - 1]
479                        .timestamp_ms
480                        .saturating_sub(start_time)
481                } else {
482                    0
483                };
484                // 使用整个组的查询次数(而非窗口内的次数),便于反映问题严重程度
485                let total_count = group_records.len();
486                alerts.push(NPlusOneAlert::new(
487                    &template,
488                    &table,
489                    total_count,
490                    time_span,
491                ));
492                break; // 该组已检测到 N+1,不再检查
493            }
494            start += 1;
495        }
496    }
497
498    // 3. 按查询次数降序排序
499    alerts.sort_by_key(|a| std::cmp::Reverse(a.query_count));
500    alerts
501}
502
503// ============================================================================
504// N+1 检测器
505// ============================================================================
506
507/// N+1 检测器
508///
509/// 累积 SQL 查询记录并提供批量分析能力。
510///
511/// ## 示例
512///
513/// ```ignore
514/// use sz_rust_core::relation::n_plus_one::*;
515///
516/// let mut detector = NPlusOneDetector::default();
517/// detector.record("SELECT * FROM orders WHERE user_id = 1", "orders", 100);
518/// detector.record("SELECT * FROM orders WHERE user_id = 2", "orders", 200);
519/// detector.record("SELECT * FROM orders WHERE user_id = 3", "orders", 300);
520/// detector.record("SELECT * FROM orders WHERE user_id = 4", "orders", 400);
521/// detector.record("SELECT * FROM orders WHERE user_id = 5", "orders", 500);
522/// detector.record("SELECT * FROM orders WHERE user_id = 6", "orders", 600);
523///
524/// let alerts = detector.detect();
525/// assert_eq!(alerts.len(), 1);
526/// ```
527#[derive(Debug, Clone, Default)]
528pub struct NPlusOneDetector {
529    records: Vec<SqlQueryRecord>,
530    config: DetectionConfig,
531    next_query_index: u64,
532}
533
534impl NPlusOneDetector {
535    /// 创建新的检测器
536    pub fn new(config: DetectionConfig) -> Self {
537        Self {
538            records: Vec::new(),
539            config,
540            next_query_index: 0,
541        }
542    }
543
544    /// 记录一条 SQL 查询
545    ///
546    /// 自动分配查询序号。
547    ///
548    /// ## 参数
549    ///
550    /// - `sql`:原始 SQL 字符串
551    /// - `table`:主表名
552    /// - `timestamp_ms`:查询时间戳(毫秒)
553    pub fn record(&mut self, sql: &str, table: &str, timestamp_ms: u64) {
554        let record = SqlQueryRecord::new(sql, table, timestamp_ms, self.next_query_index);
555        self.next_query_index += 1;
556        self.records.push(record);
557    }
558
559    /// 显式记录一条 SQL 查询(带查询序号)
560    pub fn record_with_index(
561        &mut self,
562        sql: &str,
563        table: &str,
564        timestamp_ms: u64,
565        query_index: u64,
566    ) {
567        let record = SqlQueryRecord::new(sql, table, timestamp_ms, query_index);
568        self.records.push(record);
569        if query_index >= self.next_query_index {
570            self.next_query_index = query_index + 1;
571        }
572    }
573
574    /// 批量检测 N+1 问题
575    pub fn detect(&self) -> Vec<NPlusOneAlert> {
576        detect_n_plus_one(&self.records, &self.config)
577    }
578
579    /// 清空累积的查询记录
580    pub fn clear(&mut self) {
581        self.records.clear();
582        self.next_query_index = 0;
583    }
584
585    /// 获取累积的查询记录数
586    pub fn record_count(&self) -> usize {
587        self.records.len()
588    }
589
590    /// 获取检测配置
591    pub fn config(&self) -> &DetectionConfig {
592        &self.config
593    }
594
595    /// 更新检测配置
596    pub fn set_config(&mut self, config: DetectionConfig) {
597        self.config = config;
598    }
599
600    /// 获取累积的查询记录(只读)
601    pub fn records(&self) -> &[SqlQueryRecord] {
602        &self.records
603    }
604}
605
606// ============================================================================
607// 单元测试
608// ============================================================================
609
610#[cfg(test)]
611mod tests {
612    use super::*;
613
614    // ====================================================================
615    // 组 1:SqlQueryRecord 结构体
616    // ====================================================================
617
618    #[test]
619    fn test_sql_query_record_new() {
620        let record =
621            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 1", "orders", 1000, 0);
622        assert_eq!(record.sql, "SELECT * FROM orders WHERE user_id = 1");
623        assert_eq!(record.template, "SELECT * FROM orders WHERE user_id = ?");
624        assert_eq!(record.table, "orders");
625        assert_eq!(record.timestamp_ms, 1000);
626        assert_eq!(record.query_index, 0);
627    }
628
629    #[test]
630    fn test_sql_query_record_accessors() {
631        let record = SqlQueryRecord::new("SELECT * FROM users WHERE id = 5", "users", 2000, 3);
632        assert_eq!(record.sql(), "SELECT * FROM users WHERE id = 5");
633        assert_eq!(record.template(), "SELECT * FROM users WHERE id = ?");
634        assert_eq!(record.table(), "users");
635        assert_eq!(record.timestamp_ms(), 2000);
636        assert_eq!(record.query_index(), 3);
637    }
638
639    #[test]
640    fn test_sql_query_record_string_param() {
641        let record = SqlQueryRecord::new(
642            "SELECT * FROM users WHERE email = 'abc@x.com'",
643            "users",
644            1000,
645            0,
646        );
647        assert_eq!(record.template, "SELECT * FROM users WHERE email = ?");
648    }
649
650    #[test]
651    fn test_sql_query_record_multiple_params() {
652        let record = SqlQueryRecord::new(
653            "SELECT * FROM users WHERE id = 5 AND email = 'abc' AND age > 18",
654            "users",
655            1000,
656            0,
657        );
658        assert_eq!(
659            record.template,
660            "SELECT * FROM users WHERE id = ? AND email = ? AND age > ?"
661        );
662    }
663
664    #[test]
665    fn test_sql_query_record_in_clause() {
666        let record = SqlQueryRecord::new(
667            "SELECT * FROM orders WHERE user_id IN (1, 2, 3)",
668            "orders",
669            1000,
670            0,
671        );
672        assert_eq!(
673            record.template,
674            "SELECT * FROM orders WHERE user_id IN (?, ?, ?)"
675        );
676    }
677
678    #[test]
679    fn test_sql_query_record_clone_eq() {
680        let record1 = SqlQueryRecord::new("SELECT * FROM users WHERE id = 1", "users", 1000, 0);
681        let record2 = record1.clone();
682        assert_eq!(record1, record2);
683    }
684
685    // ====================================================================
686    // 组 2:extract_template 函数
687    // ====================================================================
688
689    #[test]
690    fn test_extract_template_numeric_param() {
691        assert_eq!(
692            extract_template("SELECT * FROM orders WHERE user_id = 1"),
693            "SELECT * FROM orders WHERE user_id = ?"
694        );
695        assert_eq!(
696            extract_template("SELECT * FROM orders WHERE user_id = 123"),
697            "SELECT * FROM orders WHERE user_id = ?"
698        );
699    }
700
701    #[test]
702    fn test_extract_template_string_param() {
703        assert_eq!(
704            extract_template("SELECT * FROM users WHERE email = 'abc'"),
705            "SELECT * FROM users WHERE email = ?"
706        );
707        assert_eq!(
708            extract_template("SELECT * FROM users WHERE name = 'John Doe'"),
709            "SELECT * FROM users WHERE name = ?"
710        );
711    }
712
713    #[test]
714    fn test_extract_template_multiple_params() {
715        assert_eq!(
716            extract_template("SELECT * FROM users WHERE id = 1 AND name = 'abc'"),
717            "SELECT * FROM users WHERE id = ? AND name = ?"
718        );
719    }
720
721    #[test]
722    fn test_extract_template_in_clause() {
723        assert_eq!(
724            extract_template("SELECT * FROM orders WHERE user_id IN (1, 2, 3)"),
725            "SELECT * FROM orders WHERE user_id IN (?, ?, ?)"
726        );
727    }
728
729    #[test]
730    fn test_extract_template_no_params() {
731        assert_eq!(
732            extract_template("SELECT * FROM users"),
733            "SELECT * FROM users"
734        );
735    }
736
737    #[test]
738    fn test_extract_template_float_param() {
739        assert_eq!(
740            extract_template("SELECT * FROM products WHERE price = 9.99"),
741            "SELECT * FROM products WHERE price = ?"
742        );
743    }
744
745    #[test]
746    fn test_extract_template_double_quoted_string() {
747        assert_eq!(
748            extract_template("SELECT * FROM users WHERE name = \"abc\""),
749            "SELECT * FROM users WHERE name = ?"
750        );
751    }
752
753    #[test]
754    fn test_extract_template_empty_string() {
755        assert_eq!(extract_template(""), "");
756    }
757
758    // ====================================================================
759    // 组 3:DetectionConfig 结构体
760    // ====================================================================
761
762    #[test]
763    fn test_detection_config_default() {
764        let config = DetectionConfig::default();
765        assert_eq!(config.threshold, 5);
766        assert_eq!(config.time_window_ms, 1000);
767    }
768
769    #[test]
770    fn test_detection_config_new() {
771        let config = DetectionConfig::new(10, 5000);
772        assert_eq!(config.threshold, 10);
773        assert_eq!(config.time_window_ms, 5000);
774    }
775
776    #[test]
777    fn test_detection_config_accessors() {
778        let config = DetectionConfig::new(8, 2000);
779        assert_eq!(config.threshold(), 8);
780        assert_eq!(config.time_window_ms(), 2000);
781    }
782
783    #[test]
784    fn test_detection_config_clone_eq() {
785        let config1 = DetectionConfig::new(5, 1000);
786        let config2 = config1.clone();
787        assert_eq!(config1, config2);
788    }
789
790    // ====================================================================
791    // 组 4:NPlusOneAlert 结构体
792    // ====================================================================
793
794    #[test]
795    fn test_n_plus_one_alert_new() {
796        let alert = NPlusOneAlert::new("SELECT * FROM orders WHERE user_id = ?", "orders", 10, 500);
797        assert_eq!(alert.template, "SELECT * FROM orders WHERE user_id = ?");
798        assert_eq!(alert.table, "orders");
799        assert_eq!(alert.query_count, 10);
800        assert_eq!(alert.time_span_ms, 500);
801        assert!(alert.suggestion.contains("with"));
802        assert!(alert.suggestion.contains("orders"));
803        assert!(alert.suggestion.contains("10"));
804    }
805
806    #[test]
807    fn test_n_plus_one_alert_accessors() {
808        let alert = NPlusOneAlert::new("SELECT * FROM users WHERE id = ?", "users", 8, 300);
809        assert_eq!(alert.template(), "SELECT * FROM users WHERE id = ?");
810        assert_eq!(alert.table(), "users");
811        assert_eq!(alert.query_count(), 8);
812        assert_eq!(alert.time_span_ms(), 300);
813        assert!(alert.suggestion().contains("with"));
814    }
815
816    #[test]
817    fn test_n_plus_one_alert_clone_eq() {
818        let alert1 = NPlusOneAlert::new("SELECT * FROM users WHERE id = ?", "users", 5, 100);
819        let alert2 = alert1.clone();
820        assert_eq!(alert1, alert2);
821    }
822
823    // ====================================================================
824    // 组 5:suggest_with_usage 函数
825    // ====================================================================
826
827    #[test]
828    fn test_suggest_with_usage_basic() {
829        let suggestion = suggest_with_usage("orders", 10);
830        assert!(suggestion.contains("with"));
831        assert!(suggestion.contains("orders"));
832        assert!(suggestion.contains("10"));
833    }
834
835    #[test]
836    fn test_suggest_with_usage_different_table() {
837        let suggestion = suggest_with_usage("users", 5);
838        assert!(suggestion.contains("users"));
839        assert!(suggestion.contains("5"));
840    }
841
842    #[test]
843    fn test_suggest_with_usage_count_zero() {
844        let suggestion = suggest_with_usage("orders", 0);
845        assert!(suggestion.contains("0"));
846    }
847
848    #[test]
849    fn test_suggest_with_usage_large_count() {
850        let suggestion = suggest_with_usage("orders", 1000);
851        assert!(suggestion.contains("1000"));
852    }
853
854    // ====================================================================
855    // 组 6:detect_n_plus_one 核心检测函数
856    // ====================================================================
857
858    #[test]
859    fn test_detect_n_plus_one_no_alerts_under_threshold() {
860        // 4 次查询,未达阈值 5
861        let records = vec![
862            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 1", "orders", 100, 0),
863            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 2", "orders", 200, 1),
864            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 3", "orders", 300, 2),
865            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 4", "orders", 400, 3),
866        ];
867        let config = DetectionConfig::new(5, 1000);
868        let alerts = detect_n_plus_one(&records, &config);
869        assert!(alerts.is_empty());
870    }
871
872    #[test]
873    fn test_detect_n_plus_one_alert_at_threshold() {
874        // 5 次查询,达到阈值 5
875        let records = vec![
876            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 1", "orders", 100, 0),
877            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 2", "orders", 200, 1),
878            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 3", "orders", 300, 2),
879            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 4", "orders", 400, 3),
880            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 5", "orders", 500, 4),
881        ];
882        let config = DetectionConfig::new(5, 1000);
883        let alerts = detect_n_plus_one(&records, &config);
884        assert_eq!(alerts.len(), 1);
885        assert_eq!(alerts[0].query_count, 5);
886        assert_eq!(alerts[0].table, "orders");
887    }
888
889    #[test]
890    fn test_detect_n_plus_one_alert_over_threshold() {
891        // 6 次查询,超过阈值 5
892        let records = vec![
893            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 1", "orders", 100, 0),
894            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 2", "orders", 200, 1),
895            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 3", "orders", 300, 2),
896            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 4", "orders", 400, 3),
897            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 5", "orders", 500, 4),
898            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 6", "orders", 600, 5),
899        ];
900        let config = DetectionConfig::new(5, 1000);
901        let alerts = detect_n_plus_one(&records, &config);
902        assert_eq!(alerts.len(), 1);
903        assert_eq!(alerts[0].query_count, 6);
904    }
905
906    #[test]
907    fn test_detect_n_plus_one_multiple_templates() {
908        // 两种模板,各达到阈值
909        let records = vec![
910            // 模板 1:orders WHERE user_id = ?
911            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 1", "orders", 100, 0),
912            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 2", "orders", 200, 1),
913            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 3", "orders", 300, 2),
914            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 4", "orders", 400, 3),
915            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 5", "orders", 500, 4),
916            // 模板 2:profiles WHERE user_id = ?
917            SqlQueryRecord::new(
918                "SELECT * FROM profiles WHERE user_id = 1",
919                "profiles",
920                600,
921                5,
922            ),
923            SqlQueryRecord::new(
924                "SELECT * FROM profiles WHERE user_id = 2",
925                "profiles",
926                700,
927                6,
928            ),
929            SqlQueryRecord::new(
930                "SELECT * FROM profiles WHERE user_id = 3",
931                "profiles",
932                800,
933                7,
934            ),
935            SqlQueryRecord::new(
936                "SELECT * FROM profiles WHERE user_id = 4",
937                "profiles",
938                900,
939                8,
940            ),
941            SqlQueryRecord::new(
942                "SELECT * FROM profiles WHERE user_id = 5",
943                "profiles",
944                1000,
945                9,
946            ),
947        ];
948        let config = DetectionConfig::new(5, 2000);
949        let alerts = detect_n_plus_one(&records, &config);
950        assert_eq!(alerts.len(), 2);
951        // 按查询次数降序排序(都是 5 次,顺序可能因 HashMap 而异)
952        let tables: Vec<&str> = alerts.iter().map(|a| a.table.as_str()).collect();
953        assert!(tables.contains(&"orders"));
954        assert!(tables.contains(&"profiles"));
955    }
956
957    #[test]
958    fn test_detect_n_plus_one_outside_time_window() {
959        // 6 次查询,但时间跨度超过时间窗口
960        let records = vec![
961            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 1", "orders", 0, 0),
962            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 2", "orders", 500, 1),
963            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 3", "orders", 1000, 2),
964            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 4", "orders", 1500, 3),
965            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 5", "orders", 2000, 4),
966            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 6", "orders", 2500, 5),
967        ];
968        // 时间窗口 100ms,所有查询都不在同一窗口内
969        let config = DetectionConfig::new(5, 100);
970        let alerts = detect_n_plus_one(&records, &config);
971        assert!(alerts.is_empty());
972    }
973
974    #[test]
975    fn test_detect_n_plus_one_empty_records() {
976        let records: Vec<SqlQueryRecord> = vec![];
977        let config = DetectionConfig::default();
978        let alerts = detect_n_plus_one(&records, &config);
979        assert!(alerts.is_empty());
980    }
981
982    #[test]
983    fn test_detect_n_plus_one_different_tables_same_template() {
984        // 不同表名但相同模板(按模板分组,不应混合)
985        let records = vec![
986            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 1", "orders", 100, 0),
987            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 2", "orders", 200, 1),
988            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 3", "orders", 300, 2),
989            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 4", "orders", 400, 3),
990            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 5", "orders", 500, 4),
991        ];
992        let config = DetectionConfig::new(5, 1000);
993        let alerts = detect_n_plus_one(&records, &config);
994        assert_eq!(alerts.len(), 1);
995        assert_eq!(alerts[0].table, "orders");
996    }
997
998    #[test]
999    fn test_detect_n_plus_one_sorted_by_count_desc() {
1000        // orders 6 次,profiles 5 次,应按次数降序排序
1001        let records = vec![
1002            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 1", "orders", 100, 0),
1003            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 2", "orders", 200, 1),
1004            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 3", "orders", 300, 2),
1005            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 4", "orders", 400, 3),
1006            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 5", "orders", 500, 4),
1007            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 6", "orders", 600, 5),
1008            SqlQueryRecord::new(
1009                "SELECT * FROM profiles WHERE user_id = 1",
1010                "profiles",
1011                700,
1012                6,
1013            ),
1014            SqlQueryRecord::new(
1015                "SELECT * FROM profiles WHERE user_id = 2",
1016                "profiles",
1017                800,
1018                7,
1019            ),
1020            SqlQueryRecord::new(
1021                "SELECT * FROM profiles WHERE user_id = 3",
1022                "profiles",
1023                900,
1024                8,
1025            ),
1026            SqlQueryRecord::new(
1027                "SELECT * FROM profiles WHERE user_id = 4",
1028                "profiles",
1029                1000,
1030                9,
1031            ),
1032            SqlQueryRecord::new(
1033                "SELECT * FROM profiles WHERE user_id = 5",
1034                "profiles",
1035                1100,
1036                10,
1037            ),
1038        ];
1039        let config = DetectionConfig::new(5, 2000);
1040        let alerts = detect_n_plus_one(&records, &config);
1041        assert_eq!(alerts.len(), 2);
1042        assert_eq!(alerts[0].query_count, 6); // orders
1043        assert_eq!(alerts[1].query_count, 5); // profiles
1044    }
1045
1046    // ====================================================================
1047    // 组 7:NPlusOneDetector 检测器
1048    // ====================================================================
1049
1050    #[test]
1051    fn test_detector_default() {
1052        let detector = NPlusOneDetector::default();
1053        assert_eq!(detector.record_count(), 0);
1054        assert_eq!(detector.config().threshold, 5);
1055        assert_eq!(detector.config().time_window_ms, 1000);
1056    }
1057
1058    #[test]
1059    fn test_detector_new_with_config() {
1060        let config = DetectionConfig::new(10, 5000);
1061        let detector = NPlusOneDetector::new(config);
1062        assert_eq!(detector.config().threshold, 10);
1063        assert_eq!(detector.config().time_window_ms, 5000);
1064    }
1065
1066    #[test]
1067    fn test_detector_record_auto_index() {
1068        let mut detector = NPlusOneDetector::default();
1069        detector.record("SELECT * FROM users WHERE id = 1", "users", 100);
1070        detector.record("SELECT * FROM users WHERE id = 2", "users", 200);
1071        assert_eq!(detector.record_count(), 2);
1072        assert_eq!(detector.records()[0].query_index, 0);
1073        assert_eq!(detector.records()[1].query_index, 1);
1074    }
1075
1076    #[test]
1077    fn test_detector_record_with_explicit_index() {
1078        let mut detector = NPlusOneDetector::default();
1079        detector.record_with_index("SELECT * FROM users WHERE id = 1", "users", 100, 5);
1080        assert_eq!(detector.records()[0].query_index, 5);
1081        // 后续自动分配应从 6 开始
1082        detector.record("SELECT * FROM users WHERE id = 2", "users", 200);
1083        assert_eq!(detector.records()[1].query_index, 6);
1084    }
1085
1086    #[test]
1087    fn test_detector_detect_no_alerts() {
1088        let mut detector = NPlusOneDetector::default();
1089        detector.record("SELECT * FROM orders WHERE user_id = 1", "orders", 100);
1090        detector.record("SELECT * FROM orders WHERE user_id = 2", "orders", 200);
1091        let alerts = detector.detect();
1092        assert!(alerts.is_empty());
1093    }
1094
1095    #[test]
1096    fn test_detector_detect_with_alerts() {
1097        let mut detector = NPlusOneDetector::default();
1098        for i in 1..=6 {
1099            detector.record(
1100                &format!("SELECT * FROM orders WHERE user_id = {}", i),
1101                "orders",
1102                i * 100,
1103            );
1104        }
1105        let alerts = detector.detect();
1106        assert_eq!(alerts.len(), 1);
1107        assert_eq!(alerts[0].query_count, 6);
1108        assert_eq!(alerts[0].table, "orders");
1109    }
1110
1111    #[test]
1112    fn test_detector_clear() {
1113        let mut detector = NPlusOneDetector::default();
1114        detector.record("SELECT * FROM users WHERE id = 1", "users", 100);
1115        assert_eq!(detector.record_count(), 1);
1116        detector.clear();
1117        assert_eq!(detector.record_count(), 0);
1118        // 清空后 next_query_index 应重置为 0
1119        detector.record("SELECT * FROM users WHERE id = 2", "users", 200);
1120        assert_eq!(detector.records()[0].query_index, 0);
1121    }
1122
1123    #[test]
1124    fn test_detector_set_config() {
1125        let mut detector = NPlusOneDetector::default();
1126        assert_eq!(detector.config().threshold, 5);
1127        detector.set_config(DetectionConfig::new(20, 10000));
1128        assert_eq!(detector.config().threshold, 20);
1129        assert_eq!(detector.config().time_window_ms, 10000);
1130    }
1131
1132    #[test]
1133    fn test_detector_records_accessor() {
1134        let mut detector = NPlusOneDetector::default();
1135        detector.record("SELECT * FROM users WHERE id = 1", "users", 100);
1136        detector.record("SELECT * FROM users WHERE id = 2", "users", 200);
1137        let records = detector.records();
1138        assert_eq!(records.len(), 2);
1139        assert_eq!(records[0].table, "users");
1140        assert_eq!(records[1].table, "users");
1141    }
1142
1143    // ====================================================================
1144    // 组 8:R5 PHP 行为对齐验证(硬约束)
1145    // ====================================================================
1146
1147    #[test]
1148    fn test_r5_php_n_plus_one_pattern_detection() {
1149        // R5-1:检测 PHP N+1 模式(循环内访问关联触发 N 次查询)
1150        // PHP 模式:
1151        //   $users = User::select();  // 1 次
1152        //   foreach ($users as $user) {
1153        //       $orders = $user->orders;  // N 次
1154        //   }
1155        let mut records = vec![SqlQueryRecord::new("SELECT * FROM users", "users", 0, 0)];
1156        for i in 1..=6 {
1157            records.push(SqlQueryRecord::new(
1158                &format!("SELECT * FROM orders WHERE user_id = {}", i),
1159                "orders",
1160                i * 100,
1161                i,
1162            ));
1163        }
1164        let config = DetectionConfig::new(5, 1000);
1165        let alerts = detect_n_plus_one(&records, &config);
1166        // 应检测到 orders 表的 N+1 问题(6 次相同模板查询)
1167        assert_eq!(alerts.len(), 1);
1168        assert_eq!(alerts[0].table, "orders");
1169        assert_eq!(alerts[0].query_count, 6);
1170    }
1171
1172    #[test]
1173    fn test_r5_php_with_avoids_n_plus_one() {
1174        // R5-2:PHP `with()` 批量预加载避免 N+1 问题
1175        // PHP 模式:
1176        //   $users = User::with('orders')->select();
1177        //   // 内部通过 eagerlyResultSet() 批量 IN 查询(2 次查询)
1178        let records = vec![
1179            SqlQueryRecord::new("SELECT * FROM users", "users", 0, 0),
1180            SqlQueryRecord::new(
1181                "SELECT * FROM orders WHERE user_id IN (1, 2, 3, 4, 5, 6)",
1182                "orders",
1183                100,
1184                1,
1185            ),
1186        ];
1187        let config = DetectionConfig::new(5, 1000);
1188        let alerts = detect_n_plus_one(&records, &config);
1189        // 使用 `with()` 后无 N+1 问题
1190        assert!(alerts.is_empty());
1191    }
1192
1193    #[test]
1194    fn test_r5_php_eagerly_result_set_in_query_template() {
1195        // R5-3:PHP `eagerlyResultSet()` 批量 IN 查询 SQL 模板提取对齐
1196        // PHP `HasMany::eagerlyResultSet` 第 87 行 `[$this->foreignKey, 'in', $range]`
1197        // 生成 SQL:`SELECT * FROM {child} WHERE {fk} IN (v1, v2, ...)`
1198        let sql = "SELECT * FROM orders WHERE user_id IN (1, 2, 3, 4, 5)";
1199        let template = extract_template(sql);
1200        assert_eq!(
1201            template,
1202            "SELECT * FROM orders WHERE user_id IN (?, ?, ?, ?, ?)"
1203        );
1204    }
1205
1206    #[test]
1207    fn test_r5_php_single_query_no_n_plus_one() {
1208        // R5-4:单次查询不构成 N+1 问题
1209        let records = vec![SqlQueryRecord::new(
1210            "SELECT * FROM orders WHERE user_id = 1",
1211            "orders",
1212            100,
1213            0,
1214        )];
1215        let config = DetectionConfig::default();
1216        let alerts = detect_n_plus_one(&records, &config);
1217        assert!(alerts.is_empty());
1218    }
1219
1220    #[test]
1221    fn test_r5_php_belongs_to_n_plus_one_detection() {
1222        // R5-5:BelongsTo N+1 模式检测(PHP `belongsTo` 关联)
1223        // PHP 模式:
1224        //   $orders = Order::select();  // 1 次
1225        //   foreach ($orders as $order) {
1226        //       $user = $order->user;  // N 次(每个 order 查询 user)
1227        //   }
1228        let mut records = vec![SqlQueryRecord::new("SELECT * FROM orders", "orders", 0, 0)];
1229        for i in 1..=6 {
1230            records.push(SqlQueryRecord::new(
1231                &format!("SELECT * FROM users WHERE id = {}", i),
1232                "users",
1233                i * 100,
1234                i,
1235            ));
1236        }
1237        let config = DetectionConfig::new(5, 1000);
1238        let alerts = detect_n_plus_one(&records, &config);
1239        assert_eq!(alerts.len(), 1);
1240        assert_eq!(alerts[0].table, "users");
1241        assert_eq!(alerts[0].query_count, 6);
1242    }
1243
1244    #[test]
1245    fn test_r5_php_morph_to_n_plus_one_detection() {
1246        // R5-6:MorphTo N+1 模式检测(PHP `morphTo` 多态反向关联)
1247        // PHP 模式:
1248        //   $comments = Comment::select();  // 1 次
1249        //   foreach ($comments as $comment) {
1250        //       $commentable = $comment->commentable;  // N 次(每个 comment 查询不同父表)
1251        //   }
1252        let mut records = vec![SqlQueryRecord::new(
1253            "SELECT * FROM comments",
1254            "comments",
1255            0,
1256            0,
1257        )];
1258        // 注:MorphTo 的 N+1 检测较复杂,因为每个 comment 可能查询不同父表
1259        // 但模板相同(SELECT * FROM {table} WHERE id = ?),表名不同
1260        // 本测试验证模板分组与表名分组的关系
1261        for i in 1..=3 {
1262            records.push(SqlQueryRecord::new(
1263                &format!("SELECT * FROM posts WHERE id = {}", i),
1264                "posts",
1265                i * 100,
1266                i,
1267            ));
1268        }
1269        for i in 1..=3 {
1270            records.push(SqlQueryRecord::new(
1271                &format!("SELECT * FROM videos WHERE id = {}", i),
1272                "videos",
1273                (i + 3) * 100,
1274                i + 3,
1275            ));
1276        }
1277        let config = DetectionConfig::new(3, 1000);
1278        let alerts = detect_n_plus_one(&records, &config);
1279        // posts 和 videos 各 3 次,应分别告警
1280        assert_eq!(alerts.len(), 2);
1281        let tables: Vec<&str> = alerts.iter().map(|a| a.table.as_str()).collect();
1282        assert!(tables.contains(&"posts"));
1283        assert!(tables.contains(&"videos"));
1284    }
1285
1286    #[test]
1287    fn test_r5_php_suggest_with_usage_format() {
1288        // R5-7:`with()` 使用建议格式对齐 PHP think-orm 2.0.x
1289        let suggestion = suggest_with_usage("orders", 10);
1290        assert!(suggestion.contains("with("));
1291        assert!(suggestion.contains("orders"));
1292        assert!(suggestion.contains("10"));
1293        assert!(suggestion.contains("batch preloading"));
1294    }
1295
1296    #[test]
1297    fn test_r5_php_threshold_default_5() {
1298        // R5-8:默认阈值 5 对齐常见 N+1 检测最佳实践
1299        // PHP think-orm 2.0.x 未提供主动检测,sz-rust 端默认阈值 5
1300        // 参考行业实践:Laravel Telescope 默认阈值 5+,Django Debug Toolbar 阈值 5+
1301        let config = DetectionConfig::default();
1302        assert_eq!(config.threshold, 5);
1303    }
1304
1305    #[test]
1306    fn test_r5_php_time_window_default_1000ms() {
1307        // R5-9:默认时间窗口 1000ms 对齐 Web 请求典型时长
1308        let config = DetectionConfig::default();
1309        assert_eq!(config.time_window_ms, 1000);
1310    }
1311
1312    #[test]
1313    fn test_r5_php_different_query_no_n_plus_one() {
1314        // R5-10:不同查询模板不构成 N+1 问题
1315        let records = vec![
1316            SqlQueryRecord::new("SELECT * FROM orders WHERE user_id = 1", "orders", 100, 0),
1317            SqlQueryRecord::new(
1318                "SELECT * FROM orders WHERE user_id = 2 AND status = 1",
1319                "orders",
1320                200,
1321                1,
1322            ),
1323            SqlQueryRecord::new(
1324                "SELECT * FROM orders WHERE user_id = 3 AND status = 2",
1325                "orders",
1326                300,
1327                2,
1328            ),
1329        ];
1330        let config = DetectionConfig::default();
1331        let alerts = detect_n_plus_one(&records, &config);
1332        // 不同模板(WHERE 条件不同),不应判定为 N+1
1333        assert!(alerts.is_empty());
1334    }
1335
1336    #[test]
1337    fn test_r5_php_detector_integration() {
1338        // R5-11:检测器集成测试(模拟 PHP Web 请求生命周期)
1339        let mut detector = NPlusOneDetector::default();
1340        // 模拟 PHP Web 请求:1 次主查询 + 6 次关联查询
1341        detector.record("SELECT * FROM users", "users", 0);
1342        for i in 1..=6 {
1343            detector.record(
1344                &format!("SELECT * FROM orders WHERE user_id = {}", i),
1345                "orders",
1346                i * 50,
1347            );
1348        }
1349        let alerts = detector.detect();
1350        assert_eq!(alerts.len(), 1);
1351        assert_eq!(alerts[0].table, "orders");
1352        assert_eq!(alerts[0].query_count, 6);
1353        assert!(alerts[0].suggestion.contains("with"));
1354    }
1355
1356    // ====================================================================
1357    // 组 9:集成测试
1358    // ====================================================================
1359
1360    #[test]
1361    fn test_integration_detector_with_config_change() {
1362        // 集成测试:检测器配置变更后重新检测
1363        let mut detector = NPlusOneDetector::new(DetectionConfig::new(10, 1000));
1364        for i in 1..=6 {
1365            detector.record(
1366                &format!("SELECT * FROM orders WHERE user_id = {}", i),
1367                "orders",
1368                i * 100,
1369            );
1370        }
1371        // 阈值 10 时无告警
1372        assert!(detector.detect().is_empty());
1373        // 降低阈值到 5 后有告警
1374        detector.set_config(DetectionConfig::new(5, 1000));
1375        let alerts = detector.detect();
1376        assert_eq!(alerts.len(), 1);
1377    }
1378
1379    #[test]
1380    fn test_integration_multiple_rounds() {
1381        // 集成测试:多轮检测(清空后重新累积)
1382        let mut detector = NPlusOneDetector::default();
1383        // 第 1 轮:N+1 问题
1384        for i in 1..=6 {
1385            detector.record(
1386                &format!("SELECT * FROM orders WHERE user_id = {}", i),
1387                "orders",
1388                i * 100,
1389            );
1390        }
1391        assert_eq!(detector.detect().len(), 1);
1392        // 清空
1393        detector.clear();
1394        assert_eq!(detector.record_count(), 0);
1395        // 第 2 轮:无 N+1 问题(使用 with 批量预加载)
1396        detector.record("SELECT * FROM users", "users", 0);
1397        detector.record(
1398            "SELECT * FROM orders WHERE user_id IN (1, 2, 3, 4, 5, 6)",
1399            "orders",
1400            100,
1401        );
1402        assert!(detector.detect().is_empty());
1403    }
1404
1405    #[test]
1406    fn test_integration_complex_scenario() {
1407        // 集成测试:复杂场景(混合多种查询模式)
1408        let mut detector = NPlusOneDetector::default();
1409        // 主查询
1410        detector.record("SELECT * FROM users WHERE status = 1", "users", 0);
1411        // N+1 模式:orders 表 5 次查询
1412        for i in 1..=5 {
1413            detector.record(
1414                &format!("SELECT * FROM orders WHERE user_id = {}", i),
1415                "orders",
1416                i * 100,
1417            );
1418        }
1419        // 单次查询:profiles 表 1 次
1420        detector.record("SELECT * FROM profiles WHERE user_id = 1", "profiles", 600);
1421        // N+1 模式:comments 表 7 次查询
1422        for i in 1..=7 {
1423            detector.record(
1424                &format!("SELECT * FROM comments WHERE post_id = {}", i),
1425                "comments",
1426                700 + i * 50,
1427            );
1428        }
1429        let alerts = detector.detect();
1430        assert_eq!(alerts.len(), 2);
1431        // 按查询次数降序排序:comments (7) > orders (5)
1432        assert_eq!(alerts[0].table, "comments");
1433        assert_eq!(alerts[0].query_count, 7);
1434        assert_eq!(alerts[1].table, "orders");
1435        assert_eq!(alerts[1].query_count, 5);
1436    }
1437}