Skip to main content

sz_rust_orm_ext_facade/relation/
with.rs

1//! 关联预加载(with)— 批量 IN 查询 SQL 片段构造器 + 数据处理纯函数
2//!
3//! 本模块对齐 PHP `think\Model::with()` + `eagerlyResultSet()`
4//! 的批量预加载机制,**强制使用 IN 查询避免 N+1 问题**。
5//!
6//! ## PHP 端 with() 机制(think-orm 2.0.x)
7//!
8//! PHP `with()` 仅存储关系名数组到 `options['with']`,真正的预加载在结果集转模型时触发:
9//!
10//! ```php
11//! // ModelRelationQuery.php 第 217-225 行
12//! public function with($with)
13//! {
14//!     if (empty($this->model) || empty($with)) {
15//!         return $this;
16//!     }
17//!     $this->options['with'] = (array) $with;
18//!     return $this;
19//! }
20//! ```
21//!
22//! ### 批量预加载核心(`eagerlyResultSet`)
23//!
24//! PHP `RelationShip::eagerlyResultSet` 第 252 行遍历关系名,调用各关系对象的
25//! `eagerlyResultSet()` 方法。以 `HasMany` 为例(`HasMany.php` 第 77-102 行):
26//!
27//! ```php
28//! public function eagerlyResultSet(array &$resultSet, string $relation, ...): void
29//! {
30//!     $localKey = $this->localKey;
31//!     $range    = [];
32//!     foreach ($resultSet as $result) {
33//!         if (isset($result->$localKey)) {
34//!             $range[] = $result->$localKey;            // 收集所有主键
35//!         }
36//!     }
37//!     if (!empty($range)) {
38//!         $data = $this->eagerlyOneToMany([
39//!             [$this->foreignKey, 'in', $range],        // 真正的 IN 查询
40//!         ], $subRelation, $closure, $cache);
41//!         foreach ($resultSet as $result) {
42//!             $pk = $result->$localKey;
43//!             if (!isset($data[$pk])) {
44//!                 $data[$pk] = [];
45//!             }
46//!             $result->setRelation($relation, $this->resultSetBuild($data[$pk], clone $this->parent));
47//!         }
48//!     }
49//! }
50//! ```
51//!
52//! ### 嵌套关系语法解析
53//!
54//! PHP `RelationShip::eagerlyResultSet` 第 266-270 行解析 `"relation.sub"` 语法:
55//!
56//! ```php
57//! if (strpos($relation, '.')) {
58//!     [$relation, $subRelation] = explode('.', $relation, 2);
59//!     $subRelation = [$subRelation];
60//! }
61//! ```
62//!
63//! ## 本模块提供的函数
64//!
65//! ### 1. IN 查询 SQL 片段构造器(4 种关联类型)
66//!
67//! | 关联类型 | 函数 | SQL 模式 |
68//! |---------|------|---------|
69//! | HasMany | [`has_many_in_sql`] | `SELECT * FROM {child} WHERE {fk} IN (v1, v2, ...)` |
70//! | HasOne | [`has_one_in_sql`] | 同 HasMany |
71//! | BelongsTo | [`belongs_to_in_sql`] | `SELECT * FROM {parent} WHERE {parent_pk} IN (v1, v2, ...)` |
72//! | BelongsToMany | [`belongs_to_many_in_sql`] | `SELECT t.* FROM {target} t INNER JOIN {junction} j ON t.{target_pk} = j.{other_key} WHERE j.{foreign_key} IN (v1, v2, ...)` |
73//!
74//! ### 2. 数据处理纯函数(对齐 PHP `$range` 收集与 `$data[pk]` 分桶)
75//!
76//! - [`collect_pk_values`]:从结果集收集主键值(对齐 PHP `$range`)
77//! - [`group_by_fk`]:按外键分桶(对齐 PHP `$data[$pk]`)
78//!
79//! ### 3. PHP `with()` 语法解析辅助
80//!
81//! - [`parse_with_notation`]:解析 `"relation.sub"` 嵌套关系语法
82//!
83//! ## 架构说明
84//!
85//! sz-orm-core::model 模块私有(`mod model;` 非 `pub mod model;`),sz-rust 端无法
86//! 实现 `Model`/`RelationLoader` trait,因此本模块不直接执行关联加载,而是提供:
87//!
88//! - **IN 查询 SQL 片段构造器**:生成批量 IN 查询 SQL,对齐 PHP `eagerlyOneToMany`
89//! - **数据处理纯函数**:主键收集 + 外键分桶,对齐 PHP `$range` / `$data[$pk]`
90//! - **语法解析辅助**:解析 PHP `with(['relation.sub'])` 嵌套语法
91//!
92//! 端到端批量预加载由调用方协调:收集主键 → 生成 IN SQL → 执行查询 → 分桶回填。
93//! 单条模型加载由 sz-orm-core `WithRelation::load()` 内部覆盖(N+1 模式,本模块不重复)。
94//!
95//! ## SQL 注入防护
96//!
97//! 本模块的 SQL 片段构造器通过 `format!` 拼接参数到 SQL,**仅用于测试验证 SQL 生成模式**,
98//! **不应直接用于业务代码**。业务代码应通过 sz-orm-core 的参数化查询 API 执行 SQL,
99//! 或使用 [`sanitize_pk_value`] 对主键值进行转义后再拼接。
100
101use serde_json::Value;
102use std::collections::HashMap;
103
104// ============================================================================
105// IN 查询 SQL 片段构造器(4 种关联类型)
106// ============================================================================
107
108/// 生成 `HasMany` 关联批量预加载 IN 查询 SQL 片段
109///
110/// 对齐 PHP `HasMany::eagerlyResultSet` 第 87 行 `[$this->foreignKey, 'in', $range]`
111/// 生成的 SQL:`SELECT * FROM {child_table} WHERE {foreign_key} IN (v1, v2, ...)`
112///
113/// ## 参数
114///
115/// - `child_table`:子表名(如 `"orders"`)
116/// - `foreign_key`:外键字段名(如 `"user_id"`)
117/// - `parent_pk_values`:父模型主键值列表(字符串形式)
118///
119/// ## 空列表处理
120///
121/// 当 `parent_pk_values` 为空时,返回 `SELECT * FROM {child} WHERE {fk} IN (NULL)`,
122/// 对齐 PHP `!empty($range)` 检查为 false 时跳过查询的行为,但本函数返回 SQL 字符串
123/// 而非跳过,调用方应自行判断空列表并跳过查询。
124///
125/// ## SQL 注入防护
126///
127/// 本函数仅用于测试验证 SQL 生成模式,**不应直接用于业务代码**。
128/// 主键值通过 `format!` 直接拼接,存在 SQL 注入风险,调用方应通过
129/// [`sanitize_pk_value`] 转义或使用 sz-orm-core 参数化查询 API。
130///
131/// ## 示例
132///
133/// ```ignore
134/// use sz_rust_core::relation::with::has_many_in_sql;
135///
136/// let sql = has_many_in_sql("orders", "user_id", &["1", "2", "3"]);
137/// assert_eq!(sql, "SELECT * FROM orders WHERE user_id IN (1, 2, 3)");
138/// ```
139pub fn has_many_in_sql(child_table: &str, foreign_key: &str, parent_pk_values: &[&str]) -> String {
140    if parent_pk_values.is_empty() {
141        return format!(
142            "SELECT * FROM {} WHERE {} IN (NULL)",
143            child_table, foreign_key
144        );
145    }
146    format!(
147        "SELECT * FROM {} WHERE {} IN ({})",
148        child_table,
149        foreign_key,
150        parent_pk_values.join(", ")
151    )
152}
153
154/// 生成 `HasOne` 关联批量预加载 IN 查询 SQL 片段
155///
156/// 与 [`has_many_in_sql`] SQL 模式**完全相同**(对齐 PHP `HasOne::eagerlyResultSet`
157/// 与 `HasMany::eagerlyResultSet` 使用相同的 `eagerlyOneToMany` 方法)。
158/// 区别仅在返回语义:HasOne 每个主键取第一行,HasMany 取所有行。
159///
160/// ## 示例
161///
162/// ```ignore
163/// use sz_rust_core::relation::with::has_one_in_sql;
164///
165/// let sql = has_one_in_sql("profiles", "user_id", &["1", "2"]);
166/// assert_eq!(sql, "SELECT * FROM profiles WHERE user_id IN (1, 2)");
167/// ```
168pub fn has_one_in_sql(child_table: &str, foreign_key: &str, parent_pk_values: &[&str]) -> String {
169    has_many_in_sql(child_table, foreign_key, parent_pk_values)
170}
171
172/// 生成 `BelongsTo` 关联批量预加载 IN 查询 SQL 片段
173///
174/// 对齐 PHP `BelongsTo::eagerlyResultSet` 使用 `[$this->localKey, 'in', $range]`
175/// 生成的 SQL:`SELECT * FROM {parent_table} WHERE {parent_pk} IN (v1, v2, ...)`
176///
177/// ## 参数
178///
179/// - `parent_table`:父表名(如 `"depts"`)
180/// - `parent_pk`:父表主键字段名(如 `"id"`)
181/// - `fk_values`:当前模型外键值列表(字符串形式)
182///
183/// ## 示例
184///
185/// ```ignore
186/// use sz_rust_core::relation::with::belongs_to_in_sql;
187///
188/// let sql = belongs_to_in_sql("depts", "id", &["1", "2", "3"]);
189/// assert_eq!(sql, "SELECT * FROM depts WHERE id IN (1, 2, 3)");
190/// ```
191pub fn belongs_to_in_sql(parent_table: &str, parent_pk: &str, fk_values: &[&str]) -> String {
192    if fk_values.is_empty() {
193        return format!(
194            "SELECT * FROM {} WHERE {} IN (NULL)",
195            parent_table, parent_pk
196        );
197    }
198    format!(
199        "SELECT * FROM {} WHERE {} IN ({})",
200        parent_table,
201        parent_pk,
202        fk_values.join(", ")
203    )
204}
205
206/// 生成 `BelongsToMany` 关联批量预加载 IN 查询 SQL 片段
207///
208/// 对齐 PHP `BelongsToMany::eagerlyResultSet` 使用
209/// `[$this->localKey, 'in', $range]`(注意:PHP localKey 对应 sz-orm-core foreign_key)
210/// 生成的 SQL:
211///
212/// ```sql
213/// SELECT t.* FROM {target} t
214/// INNER JOIN {junction} j ON t.{target_pk} = j.{other_key}
215/// WHERE j.{foreign_key} IN (v1, v2, ...)
216/// ```
217///
218/// ## 参数
219///
220/// - `target_table`:目标表名(如 `"roles"`)
221/// - `junction_table`:中间表名(如 `"user_role"`)
222/// - `target_pk`:目标表主键字段名(如 `"id"`)
223/// - `other_key`:中间表中指向目标模型的 FK(如 `"role_id"`)
224/// - `foreign_key`:中间表中指向当前模型的 FK(如 `"user_id"`)
225/// - `current_pk_values`:当前模型主键值列表(字符串形式)
226///
227/// ## 示例
228///
229/// ```ignore
230/// use sz_rust_core::relation::with::belongs_to_many_in_sql;
231///
232/// let sql = belongs_to_many_in_sql("roles", "user_role", "id", "role_id", "user_id", &["1", "2"]);
233/// assert_eq!(sql, "SELECT t.* FROM roles t INNER JOIN user_role j ON t.id = j.role_id WHERE j.user_id IN (1, 2)");
234/// ```
235pub fn belongs_to_many_in_sql(
236    target_table: &str,
237    junction_table: &str,
238    target_pk: &str,
239    other_key: &str,
240    foreign_key: &str,
241    current_pk_values: &[&str],
242) -> String {
243    if current_pk_values.is_empty() {
244        return format!(
245            "SELECT t.* FROM {} t INNER JOIN {} j ON t.{} = j.{} WHERE j.{} IN (NULL)",
246            target_table, junction_table, target_pk, other_key, foreign_key
247        );
248    }
249    format!(
250        "SELECT t.* FROM {} t INNER JOIN {} j ON t.{} = j.{} WHERE j.{} IN ({})",
251        target_table,
252        junction_table,
253        target_pk,
254        other_key,
255        foreign_key,
256        current_pk_values.join(", ")
257    )
258}
259
260// ============================================================================
261// 主键值转义辅助(SQL 注入防护)
262// ============================================================================
263
264/// 转义主键值用于 IN 查询拼接
265///
266/// 数值型主键原样返回,字符串型主键包裹单引号并转义内部单引号。
267///
268/// ## 示例
269///
270/// ```ignore
271/// use sz_rust_core::relation::with::sanitize_pk_value;
272///
273/// assert_eq!(sanitize_pk_value("1"), "1");
274/// assert_eq!(sanitize_pk_value("abc"), "'abc'");
275/// assert_eq!(sanitize_pk_value("a'b"), "'a''b'");
276/// ```
277pub fn sanitize_pk_value(value: &str) -> String {
278    // 数值型主键(整数或浮点数)原样返回
279    if value.parse::<i64>().is_ok() || value.parse::<f64>().is_ok() {
280        value.to_string()
281    } else {
282        // 字符串型主键:包裹单引号并转义内部单引号(对齐 SQL 标准 '' 转义)
283        format!("'{}'", value.replace('\'', "''"))
284    }
285}
286
287// ============================================================================
288// 数据处理纯函数(对齐 PHP $range 收集与 $data[pk] 分桶)
289// ============================================================================
290
291/// 从结果集收集主键值(对齐 PHP `$range` 收集逻辑)
292///
293/// 对齐 PHP `HasMany::eagerlyResultSet` 第 81-86 行:
294///
295/// ```php
296/// $range = [];
297/// foreach ($resultSet as $result) {
298///     if (isset($result->$localKey)) {
299///         $range[] = $result->$localKey;
300///     }
301/// }
302/// ```
303///
304/// ## 参数
305///
306/// - `rows`:主表结果集(JSON 对象数组)
307/// - `pk_field`:主键字段名(如 `"id"`)
308///
309/// ## 返回
310///
311/// 主键值列表(字符串形式),跳过 `null` 或不存在的字段。
312///
313/// ## 示例
314///
315/// ```ignore
316/// use sz_rust_core::relation::with::collect_pk_values;
317/// use serde_json::json;
318///
319/// let rows = vec![
320///     json!({"id": 1, "name": "Alice"}),
321///     json!({"id": 2, "name": "Bob"}),
322///     json!({"id": 3, "name": "Charlie"}),
323/// ];
324/// let pks = collect_pk_values(&rows, "id");
325/// assert_eq!(pks, vec!["1".to_string(), "2".to_string(), "3".to_string()]);
326/// ```
327pub fn collect_pk_values(rows: &[Value], pk_field: &str) -> Vec<String> {
328    rows.iter()
329        .filter_map(|row| {
330            row.get(pk_field)
331                .filter(|v| !v.is_null())
332                .map(value_to_pk_string)
333        })
334        .collect()
335}
336
337/// 按外键分桶(对齐 PHP `$data[$pk]` 分桶逻辑)
338///
339/// 对齐 PHP `HasMany::eagerlyResultSet` 第 93-100 行:
340///
341/// ```php
342/// foreach ($resultSet as $result) {
343///     $pk = $result->$localKey;
344///     if (!isset($data[$pk])) {
345///         $data[$pk] = [];
346///     }
347///     $result->setRelation($relation, $this->resultSetBuild($data[$pk], clone $this->parent));
348/// }
349/// ```
350///
351/// 本函数对关联表结果集按外键字段分组,返回 `HashMap<外键值, Vec<行>>`。
352///
353/// ## 参数
354///
355/// - `rows`:关联表结果集(JSON 对象数组)
356/// - `fk_field`:外键字段名(如 `"user_id"`)
357///
358/// ## 返回
359///
360/// `HashMap<String, Vec<Value>>`,键为外键值字符串,值为该外键对应的所有行。
361///
362/// ## 示例
363///
364/// ```ignore
365/// use sz_rust_core::relation::with::group_by_fk;
366/// use serde_json::json;
367///
368/// let rows = vec![
369///     json!({"id": 101, "user_id": 1, "name": "Order A"}),
370///     json!({"id": 102, "user_id": 2, "name": "Order B"}),
371///     json!({"id": 103, "user_id": 1, "name": "Order C"}),
372/// ];
373/// let grouped = group_by_fk(rows, "user_id");
374/// assert_eq!(grouped.get("1").unwrap().len(), 2);
375/// assert_eq!(grouped.get("2").unwrap().len(), 1);
376/// ```
377pub fn group_by_fk(rows: Vec<Value>, fk_field: &str) -> HashMap<String, Vec<Value>> {
378    let mut grouped: HashMap<String, Vec<Value>> = HashMap::new();
379    for row in rows {
380        let fk_value = row
381            .get(fk_field)
382            .filter(|v| !v.is_null())
383            .map(value_to_pk_string)
384            .unwrap_or_default();
385        grouped.entry(fk_value).or_default().push(row);
386    }
387    grouped
388}
389
390// ============================================================================
391// PHP with() 语法解析辅助
392// ============================================================================
393
394/// 解析 PHP `with()` 嵌套关系语法
395///
396/// 对齐 PHP `RelationShip::eagerlyResultSet` 第 266-270 行:
397///
398/// ```php
399/// if (strpos($relation, '.')) {
400///     [$relation, $subRelation] = explode('.', $relation, 2);
401///     $subRelation = [$subRelation];
402/// }
403/// ```
404///
405/// ## 参数
406///
407/// - `with`:关系名字符串(如 `"category"` 或 `"items.product"`)
408///
409/// ## 返回
410///
411/// `(主关系名, Option<子关系名>)`,子关系名为 `Some` 时表示存在嵌套。
412///
413/// ## 示例
414///
415/// ```ignore
416/// use sz_rust_core::relation::with::parse_with_notation;
417///
418/// assert_eq!(parse_with_notation("category"), ("category", None));
419/// assert_eq!(parse_with_notation("items.product"), ("items", Some("product")));
420/// assert_eq!(parse_with_notation("a.b.c"), ("a", Some("b.c"))); // 仅按第一个 . 分割
421/// ```
422pub fn parse_with_notation(with: &str) -> (&str, Option<&str>) {
423    match with.split_once('.') {
424        Some((relation, sub)) => (relation, Some(sub)),
425        None => (with, None),
426    }
427}
428
429// ============================================================================
430// 内部辅助函数
431// ============================================================================
432
433/// 将 `serde_json::Value` 转换为主键字符串
434///
435/// - 数值型:直接转字符串(如 `1` → `"1"`)
436/// - 字符串型:原样返回(如 `"abc"` → `"abc"`)
437/// - 布尔型:转 `"true"` / `"false"`
438/// - null/对象/数组:返回空字符串(不应出现在主键字段)
439fn value_to_pk_string(value: &Value) -> String {
440    match value {
441        Value::Number(n) => n.to_string(),
442        Value::String(s) => s.clone(),
443        Value::Bool(b) => b.to_string(),
444        _ => String::new(),
445    }
446}
447
448// ============================================================================
449// 单元测试
450// ============================================================================
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455    use serde_json::json;
456
457    // ====================================================================
458    // 组 1:has_many_in_sql IN 查询 SQL 片段构造器
459    // ====================================================================
460
461    #[test]
462    fn test_has_many_in_sql_multiple_pks() {
463        // PHP: WHERE user_id IN (1, 2, 3)
464        let sql = has_many_in_sql("orders", "user_id", &["1", "2", "3"]);
465        assert_eq!(sql, "SELECT * FROM orders WHERE user_id IN (1, 2, 3)");
466    }
467
468    #[test]
469    fn test_has_many_in_sql_single_pk() {
470        let sql = has_many_in_sql("orders", "user_id", &["1"]);
471        assert_eq!(sql, "SELECT * FROM orders WHERE user_id IN (1)");
472    }
473
474    #[test]
475    fn test_has_many_in_sql_empty_pks() {
476        // 空列表返回 IN (NULL),调用方应自行判断并跳过查询
477        let sql = has_many_in_sql("orders", "user_id", &[]);
478        assert_eq!(sql, "SELECT * FROM orders WHERE user_id IN (NULL)");
479    }
480
481    #[test]
482    fn test_has_many_in_sql_string_pks() {
483        // 字符串主键(如 UUID)— 调用方负责加引号
484        let sql = has_many_in_sql("orders", "user_id", &["'abc-1'", "'abc-2'"]);
485        assert_eq!(
486            sql,
487            "SELECT * FROM orders WHERE user_id IN ('abc-1', 'abc-2')"
488        );
489    }
490
491    #[test]
492    fn test_has_many_in_sql_custom_foreign_key() {
493        let sql = has_many_in_sql("orders", "uid", &["1", "2"]);
494        assert_eq!(sql, "SELECT * FROM orders WHERE uid IN (1, 2)");
495    }
496
497    #[test]
498    fn test_has_many_in_sql_multi_word_table() {
499        let sql = has_many_in_sql("order_items", "order_id", &["1", "2"]);
500        assert_eq!(sql, "SELECT * FROM order_items WHERE order_id IN (1, 2)");
501    }
502
503    // ====================================================================
504    // 组 2:has_one_in_sql(与 has_many_in_sql 算法相同)
505    // ====================================================================
506
507    #[test]
508    fn test_has_one_in_sql_multiple_pks() {
509        // HasOne 与 HasMany SQL 模式完全相同
510        let sql = has_one_in_sql("profiles", "user_id", &["1", "2", "3"]);
511        assert_eq!(sql, "SELECT * FROM profiles WHERE user_id IN (1, 2, 3)");
512    }
513
514    #[test]
515    fn test_has_one_in_sql_empty_pks() {
516        let sql = has_one_in_sql("profiles", "user_id", &[]);
517        assert_eq!(sql, "SELECT * FROM profiles WHERE user_id IN (NULL)");
518    }
519
520    #[test]
521    fn test_has_one_in_sql_equals_has_many_in_sql() {
522        // 验证 HasOne 与 HasMany IN 查询 SQL 完全相同
523        let has_one = has_one_in_sql("profiles", "user_id", &["1", "2"]);
524        let has_many = has_many_in_sql("profiles", "user_id", &["1", "2"]);
525        assert_eq!(has_one, has_many);
526    }
527
528    // ====================================================================
529    // 组 3:belongs_to_in_sql IN 查询 SQL 片段构造器
530    // ====================================================================
531
532    #[test]
533    fn test_belongs_to_in_sql_multiple_fks() {
534        // PHP: WHERE id IN (1, 2, 3)(父表主键)
535        let sql = belongs_to_in_sql("depts", "id", &["1", "2", "3"]);
536        assert_eq!(sql, "SELECT * FROM depts WHERE id IN (1, 2, 3)");
537    }
538
539    #[test]
540    fn test_belongs_to_in_sql_single_fk() {
541        let sql = belongs_to_in_sql("depts", "id", &["1"]);
542        assert_eq!(sql, "SELECT * FROM depts WHERE id IN (1)");
543    }
544
545    #[test]
546    fn test_belongs_to_in_sql_empty_fks() {
547        let sql = belongs_to_in_sql("depts", "id", &[]);
548        assert_eq!(sql, "SELECT * FROM depts WHERE id IN (NULL)");
549    }
550
551    #[test]
552    fn test_belongs_to_in_sql_custom_parent_pk() {
553        // 自定义父表主键字段名
554        let sql = belongs_to_in_sql("categories", "cid", &["1", "2"]);
555        assert_eq!(sql, "SELECT * FROM categories WHERE cid IN (1, 2)");
556    }
557
558    #[test]
559    fn test_belongs_to_in_sql_multi_word_table() {
560        let sql = belongs_to_in_sql("user_profiles", "id", &["1", "2"]);
561        assert_eq!(sql, "SELECT * FROM user_profiles WHERE id IN (1, 2)");
562    }
563
564    // ====================================================================
565    // 组 4:belongs_to_many_in_sql IN 查询 SQL 片段构造器
566    // ====================================================================
567
568    #[test]
569    fn test_belongs_to_many_in_sql_multiple_pks() {
570        let sql = belongs_to_many_in_sql(
571            "roles",
572            "user_role",
573            "id",
574            "role_id",
575            "user_id",
576            &["1", "2", "3"],
577        );
578        assert_eq!(
579            sql,
580            "SELECT t.* FROM roles t INNER JOIN user_role j ON t.id = j.role_id WHERE j.user_id IN (1, 2, 3)"
581        );
582    }
583
584    #[test]
585    fn test_belongs_to_many_in_sql_single_pk() {
586        let sql = belongs_to_many_in_sql("roles", "user_role", "id", "role_id", "user_id", &["1"]);
587        assert_eq!(
588            sql,
589            "SELECT t.* FROM roles t INNER JOIN user_role j ON t.id = j.role_id WHERE j.user_id IN (1)"
590        );
591    }
592
593    #[test]
594    fn test_belongs_to_many_in_sql_empty_pks() {
595        let sql = belongs_to_many_in_sql("roles", "user_role", "id", "role_id", "user_id", &[]);
596        assert_eq!(
597            sql,
598            "SELECT t.* FROM roles t INNER JOIN user_role j ON t.id = j.role_id WHERE j.user_id IN (NULL)"
599        );
600    }
601
602    #[test]
603    fn test_belongs_to_many_in_sql_custom_keys() {
604        // 自定义外键(foreign_key=uid, other_key=rid, target_pk=pk)
605        let sql = belongs_to_many_in_sql("roles", "user_role", "pk", "rid", "uid", &["1", "2"]);
606        assert_eq!(
607            sql,
608            "SELECT t.* FROM roles t INNER JOIN user_role j ON t.pk = j.rid WHERE j.uid IN (1, 2)"
609        );
610    }
611
612    #[test]
613    fn test_belongs_to_many_in_sql_multi_word_tables() {
614        let sql = belongs_to_many_in_sql(
615            "order_items",
616            "order_item_tag",
617            "id",
618            "tag_id",
619            "order_item_id",
620            &["1", "2"],
621        );
622        assert_eq!(
623            sql,
624            "SELECT t.* FROM order_items t INNER JOIN order_item_tag j ON t.id = j.tag_id WHERE j.order_item_id IN (1, 2)"
625        );
626    }
627
628    #[test]
629    fn test_belongs_to_many_in_sql_aligns_php_pattern() {
630        // 验证 SQL 模式对齐 PHP BelongsToMany::eagerlyResultSet
631        // PHP 使用 [$this->localKey, 'in', $range],对应 sz-orm-core foreign_key
632        let sql = belongs_to_many_in_sql(
633            "roles",
634            "user_role",
635            "id",
636            "role_id",
637            "user_id",
638            &["1", "2"],
639        );
640        assert!(sql.starts_with("SELECT t.* FROM roles t INNER JOIN user_role j ON t.id = j.role_id WHERE j.user_id IN ("));
641    }
642
643    // ====================================================================
644    // 组 5:sanitize_pk_value 主键值转义
645    // ====================================================================
646
647    #[test]
648    fn test_sanitize_pk_value_numeric_integer() {
649        assert_eq!(sanitize_pk_value("1"), "1");
650        assert_eq!(sanitize_pk_value("12345"), "12345");
651        assert_eq!(sanitize_pk_value("-100"), "-100");
652    }
653
654    #[test]
655    fn test_sanitize_pk_value_numeric_float() {
656        assert_eq!(sanitize_pk_value("1.5"), "1.5");
657        assert_eq!(sanitize_pk_value("-0.5"), "-0.5");
658    }
659
660    #[test]
661    fn test_sanitize_pk_value_string() {
662        assert_eq!(sanitize_pk_value("abc"), "'abc'");
663        assert_eq!(sanitize_pk_value("uuid-123"), "'uuid-123'");
664    }
665
666    #[test]
667    fn test_sanitize_pk_value_string_with_quote() {
668        // SQL 标准转义:' → ''
669        assert_eq!(sanitize_pk_value("a'b"), "'a''b'");
670        assert_eq!(sanitize_pk_value("'"), "''''");
671    }
672
673    #[test]
674    fn test_sanitize_pk_value_empty_string() {
675        // 空字符串非数值型,包裹单引号
676        assert_eq!(sanitize_pk_value(""), "''");
677    }
678
679    // ====================================================================
680    // 组 6:collect_pk_values 主键收集
681    // ====================================================================
682
683    #[test]
684    fn test_collect_pk_values_integer_pks() {
685        let rows = vec![
686            json!({"id": 1, "name": "Alice"}),
687            json!({"id": 2, "name": "Bob"}),
688            json!({"id": 3, "name": "Charlie"}),
689        ];
690        let pks = collect_pk_values(&rows, "id");
691        assert_eq!(pks, vec!["1", "2", "3"]);
692    }
693
694    #[test]
695    fn test_collect_pk_values_string_pks() {
696        let rows = vec![
697            json!({"id": "uuid-1", "name": "Alice"}),
698            json!({"id": "uuid-2", "name": "Bob"}),
699        ];
700        let pks = collect_pk_values(&rows, "id");
701        assert_eq!(pks, vec!["uuid-1", "uuid-2"]);
702    }
703
704    #[test]
705    fn test_collect_pk_values_skip_null() {
706        // 对齐 PHP isset($result->$localKey) 检查
707        let rows = vec![
708            json!({"id": 1, "name": "Alice"}),
709            json!({"id": null, "name": "Bob"}), // 跳过 null
710            json!({"id": 3, "name": "Charlie"}),
711        ];
712        let pks = collect_pk_values(&rows, "id");
713        assert_eq!(pks, vec!["1", "3"]);
714    }
715
716    #[test]
717    fn test_collect_pk_values_skip_missing_field() {
718        // 对齐 PHP isset($result->$localKey) 检查缺失字段
719        let rows = vec![
720            json!({"id": 1, "name": "Alice"}),
721            json!({"name": "Bob"}), // 缺失 id 字段
722            json!({"id": 3, "name": "Charlie"}),
723        ];
724        let pks = collect_pk_values(&rows, "id");
725        assert_eq!(pks, vec!["1", "3"]);
726    }
727
728    #[test]
729    fn test_collect_pk_values_empty_rows() {
730        let rows: Vec<Value> = vec![];
731        let pks = collect_pk_values(&rows, "id");
732        assert!(pks.is_empty());
733    }
734
735    #[test]
736    fn test_collect_pk_values_custom_pk_field() {
737        let rows = vec![
738            json!({"uid": 1, "name": "Alice"}),
739            json!({"uid": 2, "name": "Bob"}),
740        ];
741        let pks = collect_pk_values(&rows, "uid");
742        assert_eq!(pks, vec!["1", "2"]);
743    }
744
745    #[test]
746    fn test_collect_pk_values_dedup_not_applied() {
747        // collect_pk_values 不去重,调用方按需处理
748        let rows = vec![json!({"id": 1}), json!({"id": 1}), json!({"id": 2})];
749        let pks = collect_pk_values(&rows, "id");
750        assert_eq!(pks, vec!["1", "1", "2"]);
751    }
752
753    // ====================================================================
754    // 组 7:group_by_fk 外键分桶
755    // ====================================================================
756
757    #[test]
758    fn test_group_by_fk_basic_grouping() {
759        let rows = vec![
760            json!({"id": 101, "user_id": 1, "name": "Order A"}),
761            json!({"id": 102, "user_id": 2, "name": "Order B"}),
762            json!({"id": 103, "user_id": 1, "name": "Order C"}),
763        ];
764        let grouped = group_by_fk(rows, "user_id");
765        assert_eq!(grouped.get("1").unwrap().len(), 2);
766        assert_eq!(grouped.get("2").unwrap().len(), 1);
767    }
768
769    #[test]
770    fn test_group_by_fk_string_fk() {
771        let rows = vec![
772            json!({"id": 101, "user_id": "uuid-1"}),
773            json!({"id": 102, "user_id": "uuid-2"}),
774            json!({"id": 103, "user_id": "uuid-1"}),
775        ];
776        let grouped = group_by_fk(rows, "user_id");
777        assert_eq!(grouped.get("uuid-1").unwrap().len(), 2);
778        assert_eq!(grouped.get("uuid-2").unwrap().len(), 1);
779    }
780
781    #[test]
782    fn test_group_by_fk_skip_null() {
783        // 对齐 PHP isset 检查:null 外键归入空字符串桶
784        let rows = vec![
785            json!({"id": 101, "user_id": 1}),
786            json!({"id": 102, "user_id": null}),
787            json!({"id": 103, "user_id": 1}),
788        ];
789        let grouped = group_by_fk(rows, "user_id");
790        assert_eq!(grouped.get("1").unwrap().len(), 2);
791        // null 外键归入空字符串桶(不应匹配任何主键)
792        assert_eq!(grouped.get("").unwrap().len(), 1);
793    }
794
795    #[test]
796    fn test_group_by_fk_empty_rows() {
797        let rows: Vec<Value> = vec![];
798        let grouped = group_by_fk(rows, "user_id");
799        assert!(grouped.is_empty());
800    }
801
802    #[test]
803    fn test_group_by_fk_custom_fk_field() {
804        let rows = vec![json!({"id": 101, "uid": 1}), json!({"id": 102, "uid": 2})];
805        let grouped = group_by_fk(rows, "uid");
806        assert_eq!(grouped.get("1").unwrap().len(), 1);
807        assert_eq!(grouped.get("2").unwrap().len(), 1);
808    }
809
810    #[test]
811    fn test_group_by_fk_preserves_row_data() {
812        // 验证分桶后行数据完整保留
813        let rows = vec![json!({"id": 101, "user_id": 1, "name": "Order A", "amount": 100.5})];
814        let grouped = group_by_fk(rows, "user_id");
815        let bucket = grouped.get("1").unwrap();
816        assert_eq!(bucket.len(), 1);
817        assert_eq!(bucket[0]["id"], json!(101));
818        assert_eq!(bucket[0]["name"], json!("Order A"));
819        assert_eq!(bucket[0]["amount"], json!(100.5));
820    }
821
822    // ====================================================================
823    // 组 8:parse_with_notation PHP with() 语法解析
824    // ====================================================================
825
826    #[test]
827    fn test_parse_with_notation_simple_relation() {
828        assert_eq!(parse_with_notation("category"), ("category", None));
829        assert_eq!(parse_with_notation("dept"), ("dept", None));
830    }
831
832    #[test]
833    fn test_parse_with_notation_nested_relation() {
834        // 对齐 PHP explode('.', $relation, 2):仅按第一个 . 分割
835        assert_eq!(
836            parse_with_notation("items.product"),
837            ("items", Some("product"))
838        );
839        assert_eq!(
840            parse_with_notation("user.profile"),
841            ("user", Some("profile"))
842        );
843    }
844
845    #[test]
846    fn test_parse_with_notation_deep_nested() {
847        // 深层嵌套:仅按第一个 . 分割,剩余部分作为 sub 字符串
848        assert_eq!(parse_with_notation("a.b.c"), ("a", Some("b.c")));
849        assert_eq!(
850            parse_with_notation("user.orders.items"),
851            ("user", Some("orders.items"))
852        );
853    }
854
855    #[test]
856    fn test_parse_with_notation_empty_string() {
857        assert_eq!(parse_with_notation(""), ("", None));
858    }
859
860    #[test]
861    fn test_parse_with_notation_trailing_dot() {
862        // 末尾点号:sub 为空字符串
863        assert_eq!(parse_with_notation("relation."), ("relation", Some("")));
864    }
865
866    #[test]
867    fn test_parse_with_notation_leading_dot() {
868        // 前导点号:relation 为空字符串
869        assert_eq!(parse_with_notation(".sub"), ("", Some("sub")));
870    }
871
872    // ====================================================================
873    // 组 9:value_to_pk_string 内部辅助函数
874    // ====================================================================
875
876    #[test]
877    fn test_value_to_pk_string_integer() {
878        assert_eq!(value_to_pk_string(&json!(1)), "1");
879        assert_eq!(value_to_pk_string(&json!(-100)), "-100");
880    }
881
882    #[test]
883    fn test_value_to_pk_string_float() {
884        assert_eq!(value_to_pk_string(&json!(1.5)), "1.5");
885    }
886
887    #[test]
888    fn test_value_to_pk_string_string() {
889        assert_eq!(value_to_pk_string(&json!("uuid-123")), "uuid-123");
890    }
891
892    #[test]
893    fn test_value_to_pk_string_bool() {
894        assert_eq!(value_to_pk_string(&json!(true)), "true");
895        assert_eq!(value_to_pk_string(&json!(false)), "false");
896    }
897
898    #[test]
899    fn test_value_to_pk_string_null_returns_empty() {
900        // null 返回空字符串(不应出现在主键字段)
901        assert_eq!(value_to_pk_string(&Value::Null), "");
902    }
903
904    #[test]
905    fn test_value_to_pk_string_object_returns_empty() {
906        // 对象返回空字符串(不应出现在主键字段)
907        assert_eq!(value_to_pk_string(&json!({"a": 1})), "");
908    }
909
910    #[test]
911    fn test_value_to_pk_string_array_returns_empty() {
912        // 数组返回空字符串(不应出现在主键字段)
913        assert_eq!(value_to_pk_string(&json!([1, 2, 3])), "");
914    }
915
916    // ====================================================================
917    // 组 10:R5 PHP 行为对齐验证(硬约束)
918    // ====================================================================
919
920    #[test]
921    fn test_r5_php_with_in_query_pattern_has_many() {
922        // R5-1:PHP HasMany::eagerlyResultSet 使用 IN 查询
923        // PHP 源码:[$this->foreignKey, 'in', $range]
924        let sql = has_many_in_sql("orders", "user_id", &["1", "2", "3"]);
925        assert_eq!(sql, "SELECT * FROM orders WHERE user_id IN (1, 2, 3)");
926    }
927
928    #[test]
929    fn test_r5_php_with_in_query_pattern_belongs_to() {
930        // R5-2:PHP BelongsTo::eagerlyResultSet 使用 IN 查询
931        // PHP 源码:[$this->localKey, 'in', $range]
932        let sql = belongs_to_in_sql("depts", "id", &["1", "2", "3"]);
933        assert_eq!(sql, "SELECT * FROM depts WHERE id IN (1, 2, 3)");
934    }
935
936    #[test]
937    fn test_r5_php_with_in_query_pattern_belongs_to_many() {
938        // R5-3:PHP BelongsToMany::eagerlyResultSet 使用 IN 查询
939        // PHP localKey → sz-orm-core foreign_key(命名反转)
940        let sql = belongs_to_many_in_sql(
941            "roles",
942            "user_role",
943            "id",
944            "role_id",
945            "user_id",
946            &["1", "2"],
947        );
948        assert!(sql.contains("INNER JOIN user_role j"));
949        assert!(sql.contains("WHERE j.user_id IN (1, 2)"));
950    }
951
952    #[test]
953    fn test_r5_php_collect_range_skips_null() {
954        // R5-4:PHP $range 收集跳过 isset 检查失败的行
955        let rows = vec![
956            json!({"id": 1}),
957            json!({"id": null}), // isset 返回 false
958            json!({"id": 3}),
959        ];
960        let pks = collect_pk_values(&rows, "id");
961        assert_eq!(pks, vec!["1", "3"]);
962    }
963
964    #[test]
965    fn test_r5_php_group_by_fk_matches_php_data_bucket() {
966        // R5-5:PHP $data[$pk] 分桶逻辑
967        let rows = vec![
968            json!({"id": 101, "user_id": 1, "name": "A"}),
969            json!({"id": 102, "user_id": 2, "name": "B"}),
970            json!({"id": 103, "user_id": 1, "name": "C"}),
971        ];
972        let grouped = group_by_fk(rows, "user_id");
973        // user_id=1 桶包含 2 行(A 和 C)
974        assert_eq!(grouped.get("1").unwrap().len(), 2);
975        // user_id=2 桶包含 1 行(B)
976        assert_eq!(grouped.get("2").unwrap().len(), 1);
977    }
978
979    #[test]
980    fn test_r5_php_parse_with_notation_explode_dot() {
981        // R5-6:PHP explode('.', $relation, 2) 语法解析
982        assert_eq!(parse_with_notation("category"), ("category", None));
983        assert_eq!(
984            parse_with_notation("items.product"),
985            ("items", Some("product"))
986        );
987        // PHP explode limit=2:仅按第一个 . 分割
988        assert_eq!(parse_with_notation("a.b.c"), ("a", Some("b.c")));
989    }
990
991    #[test]
992    fn test_r5_php_has_one_in_sql_same_as_has_many() {
993        // R5-7:PHP HasOne 与 HasMany IN 查询 SQL 模式相同
994        let has_one = has_one_in_sql("profiles", "user_id", &["1", "2"]);
995        let has_many = has_many_in_sql("profiles", "user_id", &["1", "2"]);
996        assert_eq!(has_one, has_many);
997    }
998
999    #[test]
1000    fn test_r5_php_empty_range_returns_in_null() {
1001        // R5-8:PHP !empty($range) 为 false 时跳过查询
1002        // 本函数返回 IN (NULL) 而非跳过,调用方应自行判断空列表
1003        let sql_has_many = has_many_in_sql("orders", "user_id", &[]);
1004        let sql_belongs_to = belongs_to_in_sql("depts", "id", &[]);
1005        let sql_belongs_to_many =
1006            belongs_to_many_in_sql("roles", "user_role", "id", "role_id", "user_id", &[]);
1007        assert!(sql_has_many.contains("IN (NULL)"));
1008        assert!(sql_belongs_to.contains("IN (NULL)"));
1009        assert!(sql_belongs_to_many.contains("IN (NULL)"));
1010    }
1011
1012    #[test]
1013    fn test_r5_php_sanitize_pk_value_sql_escaping() {
1014        // R5-9:SQL 标准单引号转义(' → '')
1015        assert_eq!(sanitize_pk_value("1"), "1");
1016        assert_eq!(sanitize_pk_value("abc"), "'abc'");
1017        assert_eq!(sanitize_pk_value("a'b"), "'a''b'");
1018    }
1019
1020    // ====================================================================
1021    // 组 11:集成测试(PHP with() 业务场景)
1022    // ====================================================================
1023
1024    #[test]
1025    fn test_integration_with_has_many_user_orders() {
1026        // PHP 业务场景:User::with('orders')->select([1, 2, 3])
1027        // 1. 收集主键
1028        let users = vec![
1029            json!({"id": 1, "name": "Alice"}),
1030            json!({"id": 2, "name": "Bob"}),
1031            json!({"id": 3, "name": "Charlie"}),
1032        ];
1033        let user_pks = collect_pk_values(&users, "id");
1034        assert_eq!(user_pks, vec!["1", "2", "3"]);
1035
1036        // 2. 生成 IN 查询 SQL
1037        let pk_refs: Vec<&str> = user_pks.iter().map(|s| s.as_str()).collect();
1038        let sql = has_many_in_sql("orders", "user_id", &pk_refs);
1039        assert_eq!(sql, "SELECT * FROM orders WHERE user_id IN (1, 2, 3)");
1040
1041        // 3. 模拟关联表结果集分桶
1042        let orders = vec![
1043            json!({"id": 101, "user_id": 1, "amount": 100}),
1044            json!({"id": 102, "user_id": 2, "amount": 200}),
1045            json!({"id": 103, "user_id": 1, "amount": 150}),
1046        ];
1047        let grouped = group_by_fk(orders, "user_id");
1048        assert_eq!(grouped.get("1").unwrap().len(), 2);
1049        assert_eq!(grouped.get("2").unwrap().len(), 1);
1050        assert!(!grouped.contains_key("3")); // user_id=3 无订单
1051    }
1052
1053    #[test]
1054    fn test_integration_with_belongs_to_order_user() {
1055        // PHP 业务场景:Order::with('user')->select()
1056        // 1. 收集外键值
1057        let orders = vec![
1058            json!({"id": 101, "user_id": 1, "amount": 100}),
1059            json!({"id": 102, "user_id": 2, "amount": 200}),
1060            json!({"id": 103, "user_id": 1, "amount": 150}),
1061        ];
1062        let user_fks = collect_pk_values(&orders, "user_id");
1063        assert_eq!(user_fks, vec!["1", "2", "1"]);
1064
1065        // 2. 去重后生成 IN 查询 SQL(调用方负责去重)
1066        let unique_fks: Vec<&str> = vec!["1", "2"];
1067        let sql = belongs_to_in_sql("users", "id", &unique_fks);
1068        assert_eq!(sql, "SELECT * FROM users WHERE id IN (1, 2)");
1069
1070        // 3. 模拟父表结果集分桶(BelongsTo 按父表主键分桶)
1071        let users = vec![
1072            json!({"id": 1, "name": "Alice"}),
1073            json!({"id": 2, "name": "Bob"}),
1074        ];
1075        let grouped = group_by_fk(users, "id");
1076        assert_eq!(grouped.get("1").unwrap().len(), 1);
1077        assert_eq!(grouped.get("2").unwrap().len(), 1);
1078    }
1079
1080    #[test]
1081    fn test_integration_with_belongs_to_many_user_roles() {
1082        // PHP 业务场景:User::with('roles')->select([1, 2])
1083        let users = vec![
1084            json!({"id": 1, "name": "Alice"}),
1085            json!({"id": 2, "name": "Bob"}),
1086        ];
1087        let user_pks = collect_pk_values(&users, "id");
1088        let pk_refs: Vec<&str> = user_pks.iter().map(|s| s.as_str()).collect();
1089
1090        // 生成 BelongsToMany IN 查询
1091        let sql =
1092            belongs_to_many_in_sql("roles", "user_role", "id", "role_id", "user_id", &pk_refs);
1093        assert_eq!(
1094            sql,
1095            "SELECT t.* FROM roles t INNER JOIN user_role j ON t.id = j.role_id WHERE j.user_id IN (1, 2)"
1096        );
1097    }
1098
1099    #[test]
1100    fn test_integration_with_nested_notation_user_orders_items() {
1101        // PHP 业务场景:User::with(['orders.items'])->select()
1102        // 解析嵌套语法
1103        let (relation, sub) = parse_with_notation("orders.items");
1104        assert_eq!(relation, "orders");
1105        assert_eq!(sub, Some("items"));
1106
1107        // 第一层:User hasMany Orders
1108        let users = vec![json!({"id": 1, "name": "Alice"})];
1109        let user_pks = collect_pk_values(&users, "id");
1110        let pk_refs: Vec<&str> = user_pks.iter().map(|s| s.as_str()).collect();
1111        let sql = has_many_in_sql("orders", "user_id", &pk_refs);
1112        assert_eq!(sql, "SELECT * FROM orders WHERE user_id IN (1)");
1113
1114        // 第二层:Order hasMany Items(递归处理 sub)
1115        let orders = vec![
1116            json!({"id": 101, "user_id": 1}),
1117            json!({"id": 102, "user_id": 1}),
1118        ];
1119        let order_pks = collect_pk_values(&orders, "id");
1120        let pk_refs2: Vec<&str> = order_pks.iter().map(|s| s.as_str()).collect();
1121        let sql2 = has_many_in_sql("order_items", "order_id", &pk_refs2);
1122        assert_eq!(
1123            sql2,
1124            "SELECT * FROM order_items WHERE order_id IN (101, 102)"
1125        );
1126    }
1127
1128    #[test]
1129    fn test_integration_with_has_one_user_profile() {
1130        // PHP 业务场景:User::with('profile')->select([1, 2])
1131        let users = vec![
1132            json!({"id": 1, "name": "Alice"}),
1133            json!({"id": 2, "name": "Bob"}),
1134        ];
1135        let user_pks = collect_pk_values(&users, "id");
1136        let pk_refs: Vec<&str> = user_pks.iter().map(|s| s.as_str()).collect();
1137
1138        // HasOne IN 查询与 HasMany 相同,区别在调用方取第一行
1139        let sql = has_one_in_sql("profiles", "user_id", &pk_refs);
1140        assert_eq!(sql, "SELECT * FROM profiles WHERE user_id IN (1, 2)");
1141
1142        // 模拟 profile 结果集
1143        let profiles = vec![
1144            json!({"id": 11, "user_id": 1, "bio": "Alice's bio"}),
1145            json!({"id": 22, "user_id": 2, "bio": "Bob's bio"}),
1146        ];
1147        let grouped = group_by_fk(profiles, "user_id");
1148        // HasOne 场景:每个 user_id 桶取第一行
1149        let alice_profile = &grouped.get("1").unwrap()[0];
1150        assert_eq!(alice_profile["bio"], json!("Alice's bio"));
1151    }
1152
1153    #[test]
1154    fn test_integration_sanitize_pk_values_for_in_query() {
1155        // 混合主键类型:数值 + 字符串
1156        let raw_pks = ["1", "abc", "2", "x'y"];
1157        let sanitized: Vec<String> = raw_pks.iter().map(|s| sanitize_pk_value(s)).collect();
1158        let joined = sanitized.join(", ");
1159        assert_eq!(joined, "1, 'abc', 2, 'x''y'");
1160
1161        // 生成安全的 IN 查询 SQL
1162        let sanitized_refs: Vec<&str> = sanitized.iter().map(|s| s.as_str()).collect();
1163        let sql = has_many_in_sql("orders", "user_id", &sanitized_refs);
1164        assert_eq!(
1165            sql,
1166            "SELECT * FROM orders WHERE user_id IN (1, 'abc', 2, 'x''y')"
1167        );
1168    }
1169}