1use crate::dialect::Dialect;
47use crate::Value;
48use std::collections::HashMap;
49
50#[derive(Debug, Clone)]
68pub struct DirtyTracker {
69 original: HashMap<String, Value>,
71 current: HashMap<String, Value>,
73}
74
75impl DirtyTracker {
76 pub fn new(initial: HashMap<String, Value>) -> Self {
78 let original = initial.clone();
79 Self {
80 original,
81 current: initial,
82 }
83 }
84
85 pub fn empty() -> Self {
87 Self {
88 original: HashMap::new(),
89 current: HashMap::new(),
90 }
91 }
92
93 pub fn set(&mut self, field: impl Into<String>, value: Value) {
97 self.current.insert(field.into(), value);
98 }
99
100 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 pub fn get(&self, field: &str) -> Option<&Value> {
109 self.current.get(field)
110 }
111
112 pub fn get_original(&self, field: &str) -> Option<&Value> {
114 self.original.get(field)
115 }
116
117 pub fn current(&self) -> &HashMap<String, Value> {
119 &self.current
120 }
121
122 pub fn original(&self) -> &HashMap<String, Value> {
124 &self.original
125 }
126
127 pub fn is_dirty(&self) -> bool {
129 self.original.len() != self.current.len() || self.dirty_fields_iter().next().is_some()
130 }
131
132 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, (Some(_), None) => true, (Some(o), Some(c)) => o != c,
139 }
140 }
141
142 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 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 pub fn mark_clean(&mut self) {
164 self.original = self.current.clone();
165 }
166
167 pub fn rollback(&mut self) {
169 self.current = self.original.clone();
170 }
171
172 pub fn clear(&mut self) {
174 self.original.clear();
175 self.current.clear();
176 }
177
178 fn dirty_fields_iter(&self) -> impl Iterator<Item = &String> {
180 let keys: Vec<&String> = self.current.keys().collect();
182 keys.into_iter()
183 .filter(move |k| match self.original.get(*k) {
184 None => true, Some(o) => self.current.get(*k).map(|c| c != o).unwrap_or(true),
186 })
187 .chain(
188 self.original
190 .keys()
191 .filter(move |k| !self.current.contains_key(*k)),
192 )
193 }
194}
195
196pub 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 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
278pub fn build_dynamic_insert(
320 dialect: &dyn Dialect,
321 table: &str,
322 data: &HashMap<String, Value>,
323) -> Option<String> {
324 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 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#[cfg(test)]
358mod tests {
359 use super::*;
360 use crate::dialect::get_dialect;
361 use crate::DbType;
362
363 #[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 assert!(!tracker.is_field_dirty("name"));
571 }
572
573 #[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 assert!(sql.contains("`age` = 26"));
592 assert!(sql.contains("`name` = 'bob'"));
593 assert!(sql.contains("WHERE `id` = 1"));
594 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 tracker.set("age", Value::I64(26));
639
640 let sql = build_dynamic_update(&*dialect, "users", "id", &Value::I64(1), &tracker).unwrap();
641
642 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 #[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 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 #[test]
756 fn test_workflow_load_modify_save() {
757 let dialect = get_dialect(DbType::MySQL).unwrap();
758
759 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 tracker.set("age", Value::I64(26));
773 tracker.set("updated_at", Value::String("2026-07-19".to_string()));
774
775 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 tracker.mark_clean();
785 assert!(!tracker.is_dirty());
786
787 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 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 assert!(!sql.contains("`bio`"));
810 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 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}