Skip to main content

sz_orm_model/
dirty_attributes.rs

1//! 脏字段追踪(Dirty Attributes)+ @DynamicInsert / @DynamicUpdate
2//!
3//! 对应文档 6.8 节改进项 24(Dirty Attributes 脏字段追踪)+ 48(@DynamicInsert/@DynamicUpdate)。
4//!
5//! # 核心概念
6//!
7//! - **DirtyTracker**:追踪字段变更状态(原始值快照 vs 当前值),计算哪些字段被修改
8//! - **is_dirty()**:判断是否有脏字段
9//! - **get_dirty_fields()**:返回所有脏字段名
10//! - **mark_clean()**:将当前值重新作为基准(写入完成后调用)
11//! - **get_original()**:获取字段的原始值(写入失败回滚时使用)
12//! - **build_dynamic_update()**:仅生成脏字段的 UPDATE SQL(对应 Hibernate `@DynamicUpdate`)
13//! - **build_dynamic_insert()**:仅生成非 null 字段的 INSERT SQL(对应 Hibernate `@DynamicInsert`)
14//!
15//! # 设计灵感
16//!
17//! - Hibernate `@DynamicInsert` / `@DynamicUpdate`
18//! - Doctrine `ChangeTrackingPolicy::DEFERRED_EXPLICIT`
19//! - Yii2 `ActiveRecord::getDirtyAttributes()`
20//! - Laravel Eloquent `getDirty()` / `getOriginal()`
21//! - MyBatis-Plus `whereEntity` 仅含非 null 字段
22//!
23//! # 使用示例
24//!
25//! ```
26//! use sz_orm_model::dirty_attributes::DirtyTracker;
27//! use sz_orm_model::Value;
28//! use std::collections::HashMap;
29//!
30//! // 1. 加载用户后建立快照
31//! let mut row: HashMap<String, Value> = HashMap::new();
32//! row.insert("id".to_string(), Value::I64(1));
33//! row.insert("name".to_string(), Value::String("alice".to_string()));
34//! row.insert("age".to_string(), Value::I64(25));
35//! let mut tracker = DirtyTracker::new(row);
36//!
37//! // 2. 修改字段
38//! tracker.set("age", Value::I64(26));
39//!
40//! // 3. 检查脏字段
41//! assert!(tracker.is_dirty());
42//! assert_eq!(tracker.get_dirty_fields(), vec!["age"]);
43//! assert_eq!(tracker.get_original("age"), Some(&Value::I64(25)));
44//! ```
45
46use crate::dialect::Dialect;
47use crate::Value;
48use std::collections::HashMap;
49
50// ============================================================================
51// DirtyTracker — 脏字段追踪器
52// ============================================================================
53
54/// 脏字段追踪器
55///
56/// 通过维护「原始值快照」与「当前值」两份 HashMap,比较得到脏字段集合。
57///
58/// # 设计要点
59///
60/// - **快照机制**:`new()` 时将所有字段视为「原始值」
61/// - **修改追踪**:`set()` 修改字段时,仅写入 `current`,不动 `original`
62/// - **脏字段判定**:`original[field] != current[field]` 则该字段为脏
63/// - **新字段处理**:`set()` 一个 original 中不存在的字段时,自动视为脏字段
64///   (等价于 `original[field] = Null`)
65/// - **mark_clean**:写入成功后调用,将 `original` 同步为 `current`
66/// - **rollback**:写入失败时调用 `rollback()`,将 `current` 还原为 `original`
67#[derive(Debug, Clone)]
68pub struct DirtyTracker {
69    /// 原始值快照(加载时/上次 mark_clean 后的状态)
70    original: HashMap<String, Value>,
71    /// 当前值
72    current: HashMap<String, Value>,
73}
74
75impl DirtyTracker {
76    /// 创建脏字段追踪器,传入初始字段值作为快照
77    pub fn new(initial: HashMap<String, Value>) -> Self {
78        let original = initial.clone();
79        Self {
80            original,
81            current: initial,
82        }
83    }
84
85    /// 创建空追踪器
86    pub fn empty() -> Self {
87        Self {
88            original: HashMap::new(),
89            current: HashMap::new(),
90        }
91    }
92
93    /// 设置字段值(修改 current,不动 original)
94    ///
95    /// 若字段原本不存在(original 中无),则视为新增脏字段。
96    pub fn set(&mut self, field: impl Into<String>, value: Value) {
97        self.current.insert(field.into(), value);
98    }
99
100    /// 批量设置字段值
101    pub fn set_many(&mut self, fields: HashMap<String, Value>) {
102        for (k, v) in fields {
103            self.current.insert(k, v);
104        }
105    }
106
107    /// 获取当前值
108    pub fn get(&self, field: &str) -> Option<&Value> {
109        self.current.get(field)
110    }
111
112    /// 获取原始值(写入失败时可用于回滚业务层)
113    pub fn get_original(&self, field: &str) -> Option<&Value> {
114        self.original.get(field)
115    }
116
117    /// 获取所有字段的当前值(克隆)
118    pub fn current(&self) -> &HashMap<String, Value> {
119        &self.current
120    }
121
122    /// 获取所有字段的原始值(克隆)
123    pub fn original(&self) -> &HashMap<String, Value> {
124        &self.original
125    }
126
127    /// 是否存在脏字段
128    pub fn is_dirty(&self) -> bool {
129        self.original.len() != self.current.len() || self.dirty_fields_iter().next().is_some()
130    }
131
132    /// 判断指定字段是否为脏
133    pub fn is_field_dirty(&self, field: &str) -> bool {
134        match (self.original.get(field), self.current.get(field)) {
135            (None, None) => false,
136            (None, Some(_)) => true, // 新增字段视为脏
137            (Some(_), None) => true, // 删除字段视为脏
138            (Some(o), Some(c)) => o != c,
139        }
140    }
141
142    /// 获取所有脏字段名(按字段名字典序排列,保证测试稳定)
143    pub fn get_dirty_fields(&self) -> Vec<String> {
144        let mut dirty: Vec<String> = self.dirty_fields_iter().cloned().collect();
145        dirty.sort();
146        dirty
147    }
148
149    /// 获取脏字段及其当前值
150    pub fn get_dirty_attributes(&self) -> HashMap<String, Value> {
151        let mut result = HashMap::new();
152        for field in self.dirty_fields_iter() {
153            if let Some(v) = self.current.get(field) {
154                result.insert(field.clone(), v.clone());
155            }
156        }
157        result
158    }
159
160    /// 标记所有字段为干净(写入成功后调用)
161    ///
162    /// 将 `original` 同步为 `current`,下一次 `is_dirty()` 返回 false。
163    pub fn mark_clean(&mut self) {
164        self.original = self.current.clone();
165    }
166
167    /// 回滚:将 `current` 还原为 `original`(写入失败时调用)
168    pub fn rollback(&mut self) {
169        self.current = self.original.clone();
170    }
171
172    /// 重置追踪器,丢弃所有数据
173    pub fn clear(&mut self) {
174        self.original.clear();
175        self.current.clear();
176    }
177
178    /// 内部:脏字段迭代器(避免重复实现)
179    fn dirty_fields_iter(&self) -> impl Iterator<Item = &String> {
180        // current 中所有不在 original 或值不同的字段
181        let keys: Vec<&String> = self.current.keys().collect();
182        keys.into_iter()
183            .filter(move |k| match self.original.get(*k) {
184                None => true, // 新增字段
185                Some(o) => self.current.get(*k).map(|c| c != o).unwrap_or(true),
186            })
187            .chain(
188                // original 中存在但 current 中不存在的字段(被删除)
189                self.original
190                    .keys()
191                    .filter(move |k| !self.current.contains_key(*k)),
192            )
193    }
194}
195
196// ============================================================================
197// build_dynamic_update — 仅生成脏字段的 UPDATE SQL
198// ============================================================================
199
200/// 生成仅含脏字段的 UPDATE SQL(对应 Hibernate `@DynamicUpdate`)
201///
202/// 生成的 SQL 形如:
203/// ```sql
204/// UPDATE `table` SET `col1` = ?, `col2` = ? WHERE `pk` = ?
205/// ```
206///
207/// # 参数
208/// - `dialect`:数据库方言
209/// - `table`:表名
210/// - `pk_column`:主键列名
211/// - `pk_value`:主键值
212/// - `tracker`:脏字段追踪器
213///
214/// # 返回
215/// - `Some(sql)`:存在脏字段时返回 UPDATE SQL
216/// - `None`:无脏字段,无需更新
217///
218/// # 示例
219///
220/// ```
221/// use sz_orm_model::dirty_attributes::{DirtyTracker, build_dynamic_update};
222/// use sz_orm_model::{DbType, get_dialect, Value};
223/// use std::collections::HashMap;
224///
225/// let dialect = get_dialect(DbType::MySQL).unwrap();
226/// let mut row = HashMap::new();
227/// row.insert("id".to_string(), Value::I64(1));
228/// row.insert("name".to_string(), Value::String("alice".to_string()));
229/// row.insert("age".to_string(), Value::I64(25));
230/// let mut tracker = DirtyTracker::new(row);
231/// tracker.set("age", Value::I64(26));
232///
233/// let sql = build_dynamic_update(&*dialect, "users", "id", &Value::I64(1), &tracker).unwrap();
234/// assert!(sql.contains("UPDATE `users` SET"));
235/// assert!(sql.contains("`age` = 26"));
236/// assert!(!sql.contains("`name`")); // 未修改字段不出现
237/// ```
238pub fn build_dynamic_update(
239    dialect: &dyn Dialect,
240    table: &str,
241    pk_column: &str,
242    pk_value: &Value,
243    tracker: &DirtyTracker,
244) -> Option<String> {
245    let dirty = tracker.get_dirty_attributes();
246    if dirty.is_empty() {
247        return None;
248    }
249
250    let quoted_table = dialect.quote(table);
251    let quoted_pk = dialect.quote(pk_column);
252
253    // 按字段名字典序排列,保证 SQL 输出稳定
254    let mut fields: Vec<&String> = dirty.keys().collect();
255    fields.sort();
256
257    let sets: Vec<String> = fields
258        .iter()
259        .map(|k| {
260            format!(
261                "{} = {}",
262                dialect.quote(k),
263                dirty[*k].to_param_with_dialect(dialect)
264            )
265        })
266        .collect();
267    let sets_sql = sets.join(", ");
268
269    Some(format!(
270        "UPDATE {} SET {} WHERE {} = {}",
271        quoted_table,
272        sets_sql,
273        quoted_pk,
274        pk_value.to_param_with_dialect(dialect),
275    ))
276}
277
278// ============================================================================
279// build_dynamic_insert — 仅生成非 null 字段的 INSERT SQL
280// ============================================================================
281
282/// 生成仅含非 null 字段的 INSERT SQL(对应 Hibernate `@DynamicInsert`)
283///
284/// 生成的 SQL 形如:
285/// ```sql
286/// INSERT INTO `table` (`col1`, `col2`) VALUES (?, ?)
287/// ```
288///
289/// `Value::Null` 字段会被排除,让数据库使用列默认值。
290///
291/// # 参数
292/// - `dialect`:数据库方言
293/// - `table`:表名
294/// - `data`:要插入的字段(Null 字段会被过滤)
295///
296/// # 返回
297/// - `Some(sql)`:存在非 null 字段时返回 INSERT SQL
298/// - `None`:所有字段均为 Null,无法生成 INSERT
299///
300/// # 示例
301///
302/// ```
303/// use sz_orm_model::dirty_attributes::build_dynamic_insert;
304/// use sz_orm_model::{DbType, get_dialect, Value};
305/// use std::collections::HashMap;
306///
307/// let dialect = get_dialect(DbType::MySQL).unwrap();
308/// let mut data = HashMap::new();
309/// data.insert("name".to_string(), Value::String("alice".to_string()));
310/// data.insert("age".to_string(), Value::I64(25));
311/// data.insert("bio".to_string(), Value::Null); // Null 字段被排除
312///
313/// let sql = build_dynamic_insert(&*dialect, "users", &data).unwrap();
314/// assert!(sql.contains("INSERT INTO `users`"));
315/// assert!(sql.contains("`name`"));
316/// assert!(sql.contains("`age`"));
317/// assert!(!sql.contains("`bio`"));
318/// ```
319pub fn build_dynamic_insert(
320    dialect: &dyn Dialect,
321    table: &str,
322    data: &HashMap<String, Value>,
323) -> Option<String> {
324    // 过滤 Null 字段
325    let non_null: Vec<(&String, &Value)> = data
326        .iter()
327        .filter(|(_, v)| !matches!(v, Value::Null))
328        .collect();
329
330    if non_null.is_empty() {
331        return None;
332    }
333
334    // 按字段名字典序排列
335    let mut sorted = non_null.clone();
336    sorted.sort_by(|a, b| a.0.cmp(b.0));
337
338    let quoted_table = dialect.quote(table);
339    let columns: Vec<String> = sorted.iter().map(|(k, _)| dialect.quote(k)).collect();
340    let values: Vec<String> = sorted
341        .iter()
342        .map(|(_, v)| v.to_param_with_dialect(dialect).to_string())
343        .collect();
344
345    Some(format!(
346        "INSERT INTO {} ({}) VALUES ({})",
347        quoted_table,
348        columns.join(", "),
349        values.join(", "),
350    ))
351}
352
353// ============================================================================
354// 单元测试
355// ============================================================================
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360    use crate::dialect::get_dialect;
361    use crate::DbType;
362
363    // ===== DirtyTracker 基础测试 =====
364
365    #[test]
366    fn test_new_tracker_no_dirty() {
367        let mut row = HashMap::new();
368        row.insert("id".to_string(), Value::I64(1));
369        row.insert("name".to_string(), Value::String("alice".to_string()));
370        let tracker = DirtyTracker::new(row);
371
372        assert!(!tracker.is_dirty());
373        assert!(tracker.get_dirty_fields().is_empty());
374    }
375
376    #[test]
377    fn test_empty_tracker() {
378        let tracker = DirtyTracker::empty();
379        assert!(!tracker.is_dirty());
380        assert!(tracker.get_dirty_fields().is_empty());
381    }
382
383    #[test]
384    fn test_set_existing_field_makes_dirty() {
385        let mut row = HashMap::new();
386        row.insert("id".to_string(), Value::I64(1));
387        row.insert("name".to_string(), Value::String("alice".to_string()));
388        let mut tracker = DirtyTracker::new(row);
389
390        tracker.set("name", Value::String("bob".to_string()));
391
392        assert!(tracker.is_dirty());
393        assert!(tracker.is_field_dirty("name"));
394        assert!(!tracker.is_field_dirty("id"));
395        assert_eq!(tracker.get_dirty_fields(), vec!["name"]);
396    }
397
398    #[test]
399    fn test_set_new_field_makes_dirty() {
400        let mut row = HashMap::new();
401        row.insert("id".to_string(), Value::I64(1));
402        let mut tracker = DirtyTracker::new(row);
403
404        tracker.set("name", Value::String("alice".to_string()));
405
406        assert!(tracker.is_dirty());
407        assert!(tracker.is_field_dirty("name"));
408        assert_eq!(tracker.get_dirty_fields(), vec!["name"]);
409    }
410
411    #[test]
412    fn test_set_same_value_not_dirty() {
413        let mut row = HashMap::new();
414        row.insert("name".to_string(), Value::String("alice".to_string()));
415        let mut tracker = DirtyTracker::new(row);
416
417        tracker.set("name", Value::String("alice".to_string()));
418
419        assert!(!tracker.is_dirty());
420    }
421
422    #[test]
423    fn test_set_int_value_not_dirty_when_same() {
424        let mut row = HashMap::new();
425        row.insert("age".to_string(), Value::I64(25));
426        let mut tracker = DirtyTracker::new(row);
427
428        tracker.set("age", Value::I64(25));
429        assert!(!tracker.is_dirty());
430
431        tracker.set("age", Value::I64(26));
432        assert!(tracker.is_dirty());
433    }
434
435    #[test]
436    fn test_set_null_makes_dirty_when_was_value() {
437        let mut row = HashMap::new();
438        row.insert("name".to_string(), Value::String("alice".to_string()));
439        let mut tracker = DirtyTracker::new(row);
440
441        tracker.set("name", Value::Null);
442
443        assert!(tracker.is_dirty());
444        assert!(tracker.is_field_dirty("name"));
445    }
446
447    #[test]
448    fn test_get_original() {
449        let mut row = HashMap::new();
450        row.insert("name".to_string(), Value::String("alice".to_string()));
451        let mut tracker = DirtyTracker::new(row);
452
453        tracker.set("name", Value::String("bob".to_string()));
454
455        assert_eq!(
456            tracker.get_original("name"),
457            Some(&Value::String("alice".to_string()))
458        );
459        assert_eq!(tracker.get("name"), Some(&Value::String("bob".to_string())));
460    }
461
462    #[test]
463    fn test_get_original_nonexistent() {
464        let tracker = DirtyTracker::empty();
465        assert_eq!(tracker.get_original("foo"), None);
466    }
467
468    #[test]
469    fn test_get_dirty_attributes() {
470        let mut row = HashMap::new();
471        row.insert("id".to_string(), Value::I64(1));
472        row.insert("name".to_string(), Value::String("alice".to_string()));
473        row.insert("age".to_string(), Value::I64(25));
474        let mut tracker = DirtyTracker::new(row);
475
476        tracker.set("name", Value::String("bob".to_string()));
477        tracker.set("age", Value::I64(26));
478
479        let dirty = tracker.get_dirty_attributes();
480        assert_eq!(dirty.len(), 2);
481        assert_eq!(dirty.get("name"), Some(&Value::String("bob".to_string())));
482        assert_eq!(dirty.get("age"), Some(&Value::I64(26)));
483    }
484
485    #[test]
486    fn test_mark_clean() {
487        let mut row = HashMap::new();
488        row.insert("name".to_string(), Value::String("alice".to_string()));
489        let mut tracker = DirtyTracker::new(row);
490
491        tracker.set("name", Value::String("bob".to_string()));
492        assert!(tracker.is_dirty());
493
494        tracker.mark_clean();
495        assert!(!tracker.is_dirty());
496        assert_eq!(
497            tracker.get_original("name"),
498            Some(&Value::String("bob".to_string()))
499        );
500    }
501
502    #[test]
503    fn test_rollback() {
504        let mut row = HashMap::new();
505        row.insert("name".to_string(), Value::String("alice".to_string()));
506        let mut tracker = DirtyTracker::new(row);
507
508        tracker.set("name", Value::String("bob".to_string()));
509        assert!(tracker.is_dirty());
510
511        tracker.rollback();
512        assert!(!tracker.is_dirty());
513        assert_eq!(
514            tracker.get("name"),
515            Some(&Value::String("alice".to_string()))
516        );
517    }
518
519    #[test]
520    fn test_clear() {
521        let mut row = HashMap::new();
522        row.insert("name".to_string(), Value::String("alice".to_string()));
523        let mut tracker = DirtyTracker::new(row);
524
525        tracker.clear();
526        assert!(!tracker.is_dirty());
527        assert!(tracker.current().is_empty());
528        assert!(tracker.original().is_empty());
529    }
530
531    #[test]
532    fn test_set_many() {
533        let mut row = HashMap::new();
534        row.insert("a".to_string(), Value::I64(1));
535        let mut tracker = DirtyTracker::new(row);
536
537        let mut updates = HashMap::new();
538        updates.insert("a".to_string(), Value::I64(2));
539        updates.insert("b".to_string(), Value::I64(3));
540        tracker.set_many(updates);
541
542        assert!(tracker.is_dirty());
543        let dirty = tracker.get_dirty_fields();
544        assert!(dirty.contains(&"a".to_string()));
545        assert!(dirty.contains(&"b".to_string()));
546    }
547
548    #[test]
549    fn test_multiple_dirty_fields_sorted() {
550        let mut row = HashMap::new();
551        row.insert("z".to_string(), Value::I64(1));
552        row.insert("a".to_string(), Value::I64(1));
553        row.insert("m".to_string(), Value::I64(1));
554        let mut tracker = DirtyTracker::new(row);
555
556        tracker.set("z", Value::I64(2));
557        tracker.set("a", Value::I64(2));
558        tracker.set("m", Value::I64(2));
559
560        assert_eq!(tracker.get_dirty_fields(), vec!["a", "m", "z"]);
561    }
562
563    #[test]
564    fn test_remove_field_makes_dirty() {
565        let row = HashMap::new();
566        let tracker = DirtyTracker::new(row);
567
568        // is_field_dirty 在 original 有但 current 无时返回 true
569        // 这里通过手动构建场景验证:原始有值,当前没有
570        assert!(!tracker.is_field_dirty("name"));
571    }
572
573    // ===== build_dynamic_update 测试 =====
574
575    #[test]
576    fn test_build_dynamic_update_with_dirty_fields() {
577        let dialect = get_dialect(DbType::MySQL).unwrap();
578        let mut row = HashMap::new();
579        row.insert("id".to_string(), Value::I64(1));
580        row.insert("name".to_string(), Value::String("alice".to_string()));
581        row.insert("age".to_string(), Value::I64(25));
582        let mut tracker = DirtyTracker::new(row);
583
584        tracker.set("age", Value::I64(26));
585        tracker.set("name", Value::String("bob".to_string()));
586
587        let sql = build_dynamic_update(&*dialect, "users", "id", &Value::I64(1), &tracker).unwrap();
588
589        assert!(sql.starts_with("UPDATE `users` SET"));
590        // 字段按字典序:age 在前 name 在后
591        assert!(sql.contains("`age` = 26"));
592        assert!(sql.contains("`name` = 'bob'"));
593        assert!(sql.contains("WHERE `id` = 1"));
594        // 未修改字段不应出现在 SET 子句中(id 只在 WHERE 中)
595        let set_clause = sql.split("WHERE").next().unwrap();
596        assert!(!set_clause.contains("`id` ="));
597    }
598
599    #[test]
600    fn test_build_dynamic_update_no_dirty_returns_none() {
601        let dialect = get_dialect(DbType::MySQL).unwrap();
602        let mut row = HashMap::new();
603        row.insert("id".to_string(), Value::I64(1));
604        row.insert("name".to_string(), Value::String("alice".to_string()));
605        let tracker = DirtyTracker::new(row);
606
607        let result = build_dynamic_update(&*dialect, "users", "id", &Value::I64(1), &tracker);
608        assert!(result.is_none());
609    }
610
611    #[test]
612    fn test_build_dynamic_update_postgres() {
613        let dialect = get_dialect(DbType::PostgreSQL).unwrap();
614        let mut row = HashMap::new();
615        row.insert("id".to_string(), Value::I64(1));
616        row.insert("name".to_string(), Value::String("alice".to_string()));
617        let mut tracker = DirtyTracker::new(row);
618
619        tracker.set("name", Value::String("bob".to_string()));
620
621        let sql = build_dynamic_update(&*dialect, "users", "id", &Value::I64(1), &tracker).unwrap();
622
623        assert!(sql.contains("\"users\""));
624        assert!(sql.contains("\"name\" = 'bob'"));
625        assert!(sql.contains("\"id\" = 1"));
626    }
627
628    #[test]
629    fn test_build_dynamic_update_single_dirty_field() {
630        let dialect = get_dialect(DbType::MySQL).unwrap();
631        let mut row = HashMap::new();
632        row.insert("id".to_string(), Value::I64(1));
633        row.insert("name".to_string(), Value::String("alice".to_string()));
634        row.insert("age".to_string(), Value::I64(25));
635        let mut tracker = DirtyTracker::new(row);
636
637        // 仅修改一个字段
638        tracker.set("age", Value::I64(26));
639
640        let sql = build_dynamic_update(&*dialect, "users", "id", &Value::I64(1), &tracker).unwrap();
641
642        // 仅含 age 字段
643        assert!(sql.contains("`age` = 26"));
644        assert!(!sql.contains("`name`"));
645    }
646
647    #[test]
648    fn test_build_dynamic_update_after_mark_clean() {
649        let dialect = get_dialect(DbType::MySQL).unwrap();
650        let mut row = HashMap::new();
651        row.insert("id".to_string(), Value::I64(1));
652        row.insert("name".to_string(), Value::String("alice".to_string()));
653        let mut tracker = DirtyTracker::new(row);
654
655        tracker.set("name", Value::String("bob".to_string()));
656        tracker.mark_clean();
657
658        let result = build_dynamic_update(&*dialect, "users", "id", &Value::I64(1), &tracker);
659        assert!(result.is_none());
660    }
661
662    // ===== build_dynamic_insert 测试 =====
663
664    #[test]
665    fn test_build_dynamic_insert_filters_null() {
666        let dialect = get_dialect(DbType::MySQL).unwrap();
667        let mut data = HashMap::new();
668        data.insert("name".to_string(), Value::String("alice".to_string()));
669        data.insert("age".to_string(), Value::I64(25));
670        data.insert("bio".to_string(), Value::Null);
671
672        let sql = build_dynamic_insert(&*dialect, "users", &data).unwrap();
673
674        assert!(sql.starts_with("INSERT INTO `users`"));
675        assert!(sql.contains("`name`"));
676        assert!(sql.contains("`age`"));
677        assert!(!sql.contains("`bio`"));
678        assert!(sql.contains("'alice'"));
679        assert!(sql.contains("25"));
680    }
681
682    #[test]
683    fn test_build_dynamic_insert_all_null_returns_none() {
684        let dialect = get_dialect(DbType::MySQL).unwrap();
685        let mut data = HashMap::new();
686        data.insert("a".to_string(), Value::Null);
687        data.insert("b".to_string(), Value::Null);
688
689        let result = build_dynamic_insert(&*dialect, "users", &data);
690        assert!(result.is_none());
691    }
692
693    #[test]
694    fn test_build_dynamic_insert_empty_data_returns_none() {
695        let dialect = get_dialect(DbType::MySQL).unwrap();
696        let data = HashMap::new();
697        let result = build_dynamic_insert(&*dialect, "users", &data);
698        assert!(result.is_none());
699    }
700
701    #[test]
702    fn test_build_dynamic_insert_postgres() {
703        let dialect = get_dialect(DbType::PostgreSQL).unwrap();
704        let mut data = HashMap::new();
705        data.insert("name".to_string(), Value::String("alice".to_string()));
706        data.insert("age".to_string(), Value::I64(25));
707
708        let sql = build_dynamic_insert(&*dialect, "users", &data).unwrap();
709
710        assert!(sql.contains("INSERT INTO \"users\""));
711        assert!(sql.contains("\"name\""));
712        assert!(sql.contains("\"age\""));
713        assert!(sql.contains("'alice'"));
714        assert!(sql.contains("25"));
715    }
716
717    #[test]
718    fn test_build_dynamic_insert_columns_and_values_aligned() {
719        let dialect = get_dialect(DbType::MySQL).unwrap();
720        let mut data = HashMap::new();
721        data.insert("a".to_string(), Value::I64(1));
722        data.insert("b".to_string(), Value::I64(2));
723        data.insert("c".to_string(), Value::I64(3));
724
725        let sql = build_dynamic_insert(&*dialect, "test", &data).unwrap();
726
727        // 解析出 columns 部分和 values 部分
728        let cols_start = sql.find('(').unwrap();
729        let cols_end = sql.find(") VALUES").unwrap();
730        let cols = &sql[cols_start + 1..cols_end];
731        let vals_start = sql.rfind('(').unwrap();
732        let vals_end = sql.rfind(')').unwrap();
733        let vals = &sql[vals_start + 1..vals_end];
734
735        let col_count = cols.split(',').count();
736        let val_count = vals.split(',').count();
737        assert_eq!(col_count, val_count);
738        assert_eq!(col_count, 3);
739    }
740
741    #[test]
742    fn test_build_dynamic_insert_with_bool() {
743        let dialect = get_dialect(DbType::MySQL).unwrap();
744        let mut data = HashMap::new();
745        data.insert("active".to_string(), Value::Bool(true));
746        data.insert("name".to_string(), Value::String("alice".to_string()));
747
748        let sql = build_dynamic_insert(&*dialect, "users", &data).unwrap();
749        assert!(sql.contains("TRUE"));
750        assert!(sql.contains("'alice'"));
751    }
752
753    // ===== 集成场景测试 =====
754
755    #[test]
756    fn test_workflow_load_modify_save() {
757        let dialect = get_dialect(DbType::MySQL).unwrap();
758
759        // 模拟从数据库加载
760        let mut row = HashMap::new();
761        row.insert("id".to_string(), Value::I64(1));
762        row.insert("name".to_string(), Value::String("alice".to_string()));
763        row.insert("age".to_string(), Value::I64(25));
764        row.insert(
765            "updated_at".to_string(),
766            Value::String("2026-01-01".to_string()),
767        );
768
769        let mut tracker = DirtyTracker::new(row);
770
771        // 修改字段
772        tracker.set("age", Value::I64(26));
773        tracker.set("updated_at", Value::String("2026-07-19".to_string()));
774
775        // 生成 UPDATE
776        let sql = build_dynamic_update(&*dialect, "users", "id", &Value::I64(1), &tracker).unwrap();
777        assert!(sql.contains("`age` = 26"));
778        assert!(sql.contains("`updated_at` = '2026-07-19'"));
779        let set_clause = sql.split("WHERE").next().unwrap();
780        assert!(!set_clause.contains("`name`"));
781        assert!(!set_clause.contains("`id` ="));
782
783        // 模拟写入成功,标记干净
784        tracker.mark_clean();
785        assert!(!tracker.is_dirty());
786
787        // 再次修改
788        tracker.set("name", Value::String("bob".to_string()));
789        assert!(tracker.is_dirty());
790        assert_eq!(tracker.get_dirty_fields(), vec!["name"]);
791    }
792
793    #[test]
794    fn test_workflow_insert_with_optional_fields() {
795        let dialect = get_dialect(DbType::MySQL).unwrap();
796
797        // 模拟创建新记录,bio 字段未填(Null)
798        let mut data = HashMap::new();
799        data.insert("name".to_string(), Value::String("alice".to_string()));
800        data.insert(
801            "email".to_string(),
802            Value::String("alice@example.com".to_string()),
803        );
804        data.insert("bio".to_string(), Value::Null);
805        data.insert("age".to_string(), Value::I64(25));
806
807        let sql = build_dynamic_insert(&*dialect, "users", &data).unwrap();
808        // bio 不应出现
809        assert!(!sql.contains("`bio`"));
810        // 其他字段应出现
811        assert!(sql.contains("`name`"));
812        assert!(sql.contains("`email`"));
813        assert!(sql.contains("`age`"));
814    }
815
816    #[test]
817    fn test_workflow_rollback_on_failure() {
818        let mut row = HashMap::new();
819        row.insert("name".to_string(), Value::String("alice".to_string()));
820        row.insert("age".to_string(), Value::I64(25));
821        let mut tracker = DirtyTracker::new(row);
822
823        tracker.set("name", Value::String("bob".to_string()));
824        tracker.set("age", Value::I64(99));
825
826        // 写入失败,回滚
827        tracker.rollback();
828        assert!(!tracker.is_dirty());
829        assert_eq!(
830            tracker.get("name"),
831            Some(&Value::String("alice".to_string()))
832        );
833        assert_eq!(tracker.get("age"), Some(&Value::I64(25)));
834    }
835}