1use crate::entity::{IEntitySnapshot, IEntityType, IGetKeyValues};
8use crate::error::{EFError, EFResult};
9use crate::metadata::{EntityTypeMeta, PropertyMeta};
10use crate::provider::{DbValue, IAsyncConnection, IDatabaseProvider};
11use crate::query::{collect_bool_expr_values, compile_bool_expr, BoolExpr};
12use std::collections::HashMap;
13
14pub struct ChangeExecutor;
16
17impl ChangeExecutor {
18 pub async fn execute_inserts<E, F>(
33 conn: &mut dyn IAsyncConnection,
34 provider: &dyn IDatabaseProvider,
35 entities: &[(&E, &EntityTypeMeta)],
36 mut on_key_backfill: F,
37 ) -> EFResult<usize>
38 where
39 E: IEntityType + IEntitySnapshot + IGetKeyValues,
40 F: FnMut(usize, i64),
41 {
42 if entities.is_empty() {
43 return Ok(0);
44 }
45 let gen = provider.sql_generator();
46 let meta = entities[0].1;
47 let scalar_props: Vec<_> = meta.mapped_scalar_properties().collect();
48 if scalar_props.is_empty() {
49 return Ok(0);
50 }
51 let insert_cols: Vec<&str> = scalar_props
52 .iter()
53 .filter(|p| !p.is_primary_key || (!p.is_auto_increment && !p.is_sequence))
54 .map(|p| p.column_name.as_ref())
55 .collect();
56 if insert_cols.is_empty() {
57 return Ok(0);
58 }
59
60 let auto_inc_pk = scalar_props
62 .iter()
63 .find(|p| (p.is_auto_increment || p.is_sequence) && p.is_primary_key);
64
65 const MAX_PARAMS: usize = 900;
67 let batch_size = (MAX_PARAMS / insert_cols.len()).max(1);
68
69 let mut inserted = 0usize;
70 let mut start = 0usize;
71 while start < entities.len() {
72 let end = (start + batch_size).min(entities.len());
73 let batch = &entities[start..end];
74 let row_count = batch.len();
75
76 let sql = gen.insert_batch(meta.table_name.as_ref(), &insert_cols, row_count);
77 let mut params: Vec<DbValue> = Vec::with_capacity(row_count * insert_cols.len());
78 for (entity, _) in batch {
79 let snap = entity.snapshot();
80 for p in &scalar_props {
81 if !p.is_primary_key || (!p.is_auto_increment && !p.is_sequence) {
82 params.push(
83 snap.get(p.field_name.as_ref())
84 .cloned()
85 .unwrap_or(DbValue::Null),
86 );
87 }
88 }
89 }
90
91 if let Some(_pk_prop) = auto_inc_pk {
92 if gen.supports_returning() {
93 let rows = conn.query(&sql, ¶ms).await?;
96 let affected = rows.len().min(row_count);
97 for (i, row) in rows.iter().enumerate().take(affected) {
98 if let Some(pk_val) = row.first() {
99 if let Ok(key) = pk_val.clone().try_into() {
100 on_key_backfill(start + i, key);
101 }
102 }
103 }
104 inserted += affected;
105 } else if let Some(last_id_sql) = gen.last_insert_id_sql() {
106 conn.execute(&sql, ¶ms).await?;
109 let id_rows = conn.query(last_id_sql, &[]).await?;
110 if let Some(id_row) = id_rows.first() {
111 if let Some(id_val) = id_row.first() {
112 let raw_id: i64 = id_val.clone().try_into().unwrap_or(0);
113 let first_id = if gen.last_insert_id_returns_first() {
114 raw_id
115 } else {
116 raw_id - row_count as i64 + 1
117 };
118 for i in 0..row_count {
119 on_key_backfill(start + i, first_id + i as i64);
120 }
121 }
122 }
123 inserted += row_count;
124 } else {
125 let rows = conn.execute(&sql, ¶ms).await?;
127 let affected = (rows as usize).min(row_count);
128 for i in 0..affected {
129 on_key_backfill(start + i, 0);
130 }
131 inserted += affected;
132 }
133 } else {
134 let rows = conn.execute(&sql, ¶ms).await?;
136 let affected = (rows as usize).min(row_count);
137 for i in 0..affected {
138 on_key_backfill(start + i, 0);
139 }
140 inserted += affected;
141 }
142
143 start = end;
144 }
145
146 Ok(inserted)
147 }
148
149 pub async fn execute_upserts<E>(
158 conn: &mut dyn IAsyncConnection,
159 provider: &dyn IDatabaseProvider,
160 entities: &[(&E, &EntityTypeMeta)],
161 ) -> EFResult<usize>
162 where
163 E: IEntityType + IEntitySnapshot + IGetKeyValues,
164 {
165 if entities.is_empty() {
166 return Ok(0);
167 }
168 let gen = provider.sql_generator();
169 let meta = entities[0].1;
170 let scalar_props: Vec<_> = meta.mapped_scalar_properties().collect();
171 if scalar_props.is_empty() {
172 return Ok(0);
173 }
174 let insert_cols: Vec<&str> = scalar_props
178 .iter()
179 .map(|p| p.column_name.as_ref())
180 .collect();
181 if insert_cols.is_empty() {
182 return Ok(0);
183 }
184
185 let conflict_cols: Vec<&str> = scalar_props
187 .iter()
188 .filter(|p| p.is_primary_key)
189 .map(|p| p.column_name.as_ref())
190 .collect();
191 if conflict_cols.is_empty() {
192 return Err(EFError::configuration(
193 "Upsert requires at least one primary key column as conflict target.",
194 ));
195 }
196
197 const MAX_PARAMS: usize = 900;
198 let batch_size = (MAX_PARAMS / insert_cols.len()).max(1);
199
200 let mut upserted = 0usize;
201 let mut start = 0usize;
202 while start < entities.len() {
203 let end = (start + batch_size).min(entities.len());
204 let batch = &entities[start..end];
205 let row_count = batch.len();
206
207 let sql = gen.upsert_batch(
208 meta.table_name.as_ref(),
209 &insert_cols,
210 &conflict_cols,
211 row_count,
212 );
213 let mut params: Vec<DbValue> = Vec::with_capacity(row_count * insert_cols.len());
214 for (entity, _) in batch {
215 let snap = entity.snapshot();
216 for p in &scalar_props {
217 params.push(
218 snap.get(p.field_name.as_ref())
219 .cloned()
220 .unwrap_or(DbValue::Null),
221 );
222 }
223 }
224
225 let affected = conn.execute(&sql, ¶ms).await?;
226 upserted += (affected as usize).min(row_count);
227
228 start = end;
229 }
230
231 Ok(upserted)
232 }
233
234 #[allow(clippy::type_complexity)]
251 pub async fn execute_updates<E>(
252 conn: &mut dyn IAsyncConnection,
253 provider: &dyn IDatabaseProvider,
254 entities: &[(
255 &E,
256 &EntityTypeMeta,
257 Option<&HashMap<String, DbValue>>,
258 &[String],
259 )],
260 query_filter: Option<&BoolExpr>,
261 ) -> EFResult<usize>
262 where
263 E: IEntityType + IEntitySnapshot + IGetKeyValues,
264 {
265 if entities.is_empty() {
266 return Ok(0);
267 }
268 let gen = provider.sql_generator();
269 let meta = entities[0].1;
270 let scalar_props: Vec<_> = meta.mapped_scalar_properties().collect();
271 let pk_props: Vec<_> = scalar_props.iter().filter(|p| p.is_primary_key).collect();
272 let has_concurrency_tokens = scalar_props.iter().any(|p| p.is_concurrency_token);
273
274 let modified_fields: std::collections::HashSet<&str> = entities
278 .iter()
279 .flat_map(|(_, _, _, mods)| mods.iter().map(|s| s.as_str()))
280 .collect();
281 let set_props: Vec<&PropertyMeta> = if modified_fields.is_empty() {
282 scalar_props
283 .iter()
284 .copied()
285 .filter(|p| !p.is_primary_key)
286 .collect()
287 } else {
288 scalar_props
289 .iter()
290 .copied()
291 .filter(|p| !p.is_primary_key && modified_fields.contains(p.field_name.as_ref()))
292 .collect()
293 };
294 let set_cols: Vec<&str> = set_props.iter().map(|p| p.column_name.as_ref()).collect();
295
296 if has_concurrency_tokens || pk_props.len() != 1 || set_cols.is_empty() {
300 return Self::execute_updates_per_row(conn, gen, entities, query_filter).await;
301 }
302
303 let pk_col = pk_props[0].column_name.as_ref();
304 let pk_field = pk_props[0].field_name.as_ref();
305
306 let entity_data: Vec<(HashMap<String, DbValue>, HashMap<String, DbValue>)> = entities
308 .iter()
309 .map(|(e, _, _, _)| (e.snapshot(), e.key_values()))
310 .collect();
311
312 let filter_params: Vec<DbValue> = match query_filter {
314 Some(filter) => collect_bool_expr_values(filter),
315 None => Vec::new(),
316 };
317
318 const MAX_PARAMS: usize = 900;
321 let params_per_row = 2 * set_cols.len() + 1;
322 let batch_size =
323 ((MAX_PARAMS.saturating_sub(filter_params.len())) / params_per_row.max(1)).max(1);
324
325 let mut updated = 0usize;
326 let mut start = 0usize;
327 while start < entity_data.len() {
328 let end = (start + batch_size).min(entity_data.len());
329 let row_count = end - start;
330
331 let set_param_count = 2 * set_cols.len() * row_count;
333 let mut idx = set_param_count + 1;
334
335 let pk_placeholders: Vec<String> = (0..row_count)
337 .map(|_| {
338 let ph = gen.parameter_placeholder(idx);
339 idx += 1;
340 ph
341 })
342 .collect();
343 let mut where_clause = format!(
344 "{} IN ({})",
345 gen.quote_identifier(pk_col),
346 pk_placeholders.join(", ")
347 );
348
349 let mut params: Vec<DbValue> = Vec::with_capacity(set_param_count + row_count);
351 for col_prop in &set_props {
352 for (snap, keys) in entity_data[start..end].iter() {
353 let pk_val = keys.get(pk_field).cloned().unwrap_or(DbValue::Null);
354 let col_val = snap
355 .get(col_prop.field_name.as_ref())
356 .cloned()
357 .unwrap_or(DbValue::Null);
358 params.push(pk_val);
359 params.push(col_val);
360 }
361 }
362 for (_, keys) in entity_data[start..end].iter() {
364 let pk_val = keys.get(pk_field).cloned().unwrap_or(DbValue::Null);
365 params.push(pk_val);
366 }
367
368 if let Some(filter) = query_filter {
370 let filter_sql = compile_bool_expr(filter, gen, &mut idx);
371 params.extend(filter_params.iter().cloned());
372 where_clause = format!("({}) AND ({})", where_clause, filter_sql);
373 }
374
375 let sql = gen.update_batch(
376 meta.table_name.as_ref(),
377 &set_cols,
378 pk_col,
379 row_count,
380 &where_clause,
381 );
382 let rows = conn.execute(&sql, ¶ms).await?;
383 if rows == 0 && row_count > 0 {
384 return Err(EFError::concurrency_conflict(format!(
385 "batch update affected 0 rows on {} (rows may have been modified or deleted)",
386 meta.table_name
387 )));
388 }
389 updated += (rows as usize).min(row_count);
390 start = end;
391 }
392
393 Ok(updated)
394 }
395
396 #[allow(clippy::type_complexity)]
402 async fn execute_updates_per_row<E>(
403 conn: &mut dyn IAsyncConnection,
404 gen: &'static dyn crate::provider::ISqlGenerator,
405 entities: &[(
406 &E,
407 &EntityTypeMeta,
408 Option<&HashMap<String, DbValue>>,
409 &[String],
410 )],
411 query_filter: Option<&BoolExpr>,
412 ) -> EFResult<usize>
413 where
414 E: IEntityType + IEntitySnapshot + IGetKeyValues,
415 {
416 let mut updated = 0;
417 let mut sql_cache: HashMap<(String, Vec<String>, String), String> = HashMap::new();
418
419 for (entity, meta, original, modified_props) in entities {
420 let snap = entity.snapshot();
421 let keys = entity.key_values();
422 let scalar_props: Vec<_> = meta.mapped_scalar_properties().collect();
423
424 let set_props: Vec<&PropertyMeta> = if modified_props.is_empty() {
427 scalar_props
428 .iter()
429 .copied()
430 .filter(|p| !p.is_primary_key)
431 .collect()
432 } else {
433 let modified_set: std::collections::HashSet<&str> =
434 modified_props.iter().map(|s| s.as_str()).collect();
435 scalar_props
436 .iter()
437 .copied()
438 .filter(|p| !p.is_primary_key && modified_set.contains(p.field_name.as_ref()))
439 .collect()
440 };
441 let set_cols: Vec<&str> = set_props.iter().map(|p| p.column_name.as_ref()).collect();
442
443 if set_cols.is_empty() || keys.is_empty() {
444 continue;
445 }
446
447 let concurrency_tokens: Vec<&PropertyMeta> = scalar_props
448 .iter()
449 .copied()
450 .filter(|p| p.is_concurrency_token)
451 .collect();
452
453 let (mut where_clause, mut where_params) = build_where_with_concurrency(
454 gen,
455 &keys,
456 &concurrency_tokens,
457 *original,
458 set_cols.len() + 1,
459 )?;
460
461 if let Some(filter) = query_filter {
462 let mut idx = set_cols.len() + where_params.len() + 1;
463 let filter_sql = compile_bool_expr(filter, gen, &mut idx);
464 where_params.extend(collect_bool_expr_values(filter));
465 where_clause = format!("({}) AND ({})", where_clause, filter_sql);
466 }
467
468 let sql = sql_cache
469 .entry((
470 meta.table_name.to_string(),
471 set_cols.iter().map(|s| (*s).to_string()).collect(),
472 where_clause.clone(),
473 ))
474 .or_insert_with(|| gen.update(meta.table_name.as_ref(), &set_cols, &where_clause))
475 .clone();
476
477 let mut params: Vec<DbValue> = set_props
478 .iter()
479 .map(|p| {
480 snap.get(p.field_name.as_ref())
481 .cloned()
482 .unwrap_or(DbValue::Null)
483 })
484 .collect();
485 params.extend(where_params);
486
487 let rows = conn.execute(&sql, ¶ms).await?;
488 if rows == 0 {
489 return Err(EFError::concurrency_conflict(format!(
490 "update affected 0 rows on {} (row may have been modified or deleted)",
491 meta.table_name
492 )));
493 }
494 updated += 1;
495 }
496
497 Ok(updated)
498 }
499
500 #[allow(clippy::type_complexity)]
511 pub async fn execute_deletes<E>(
512 conn: &mut dyn IAsyncConnection,
513 provider: &dyn IDatabaseProvider,
514 entities: &[(&E, &EntityTypeMeta, Option<&HashMap<String, DbValue>>)],
515 query_filter: Option<&BoolExpr>,
516 ) -> EFResult<usize>
517 where
518 E: IEntityType + IGetKeyValues,
519 {
520 if entities.is_empty() {
521 return Ok(0);
522 }
523 let gen = provider.sql_generator();
524 let meta = entities[0].1;
525 let scalar_props: Vec<_> = meta.mapped_scalar_properties().collect();
526 let has_concurrency_tokens = scalar_props.iter().any(|p| p.is_concurrency_token);
527 let pk_props: Vec<_> = scalar_props.iter().filter(|p| p.is_primary_key).collect();
528
529 if has_concurrency_tokens || pk_props.len() != 1 {
533 return Self::execute_deletes_per_row(conn, gen, entities, query_filter).await;
534 }
535
536 let pk_col = pk_props[0].column_name.as_ref();
537 let pk_field = pk_props[0].field_name.as_ref();
538
539 let pk_values: Vec<DbValue> = entities
541 .iter()
542 .filter_map(|(e, _, _)| e.key_values().get(pk_field).cloned())
543 .collect();
544 if pk_values.is_empty() {
545 return Ok(0);
546 }
547
548 let filter_params: Vec<DbValue> = match query_filter {
551 Some(filter) => collect_bool_expr_values(filter),
552 None => Vec::new(),
553 };
554 const MAX_PARAMS: usize = 900;
555 let batch_size = MAX_PARAMS.saturating_sub(filter_params.len()).max(1);
556
557 let mut deleted = 0usize;
558 let mut start = 0usize;
559 while start < pk_values.len() {
560 let end = (start + batch_size).min(pk_values.len());
561 let batch = &pk_values[start..end];
562 let pk_count = batch.len();
563
564 let pk_placeholders: Vec<String> = (1..=pk_count)
565 .map(|i| gen.parameter_placeholder(i))
566 .collect();
567 let mut where_clause = format!(
568 "{} IN ({})",
569 gen.quote_identifier(pk_col),
570 pk_placeholders.join(", "),
571 );
572 let mut params: Vec<DbValue> = batch.to_vec();
573 if let Some(filter) = query_filter {
574 let mut idx = pk_count + 1;
576 let filter_sql = compile_bool_expr(filter, gen, &mut idx);
577 params.extend(filter_params.iter().cloned());
578 where_clause = format!("({}) AND ({})", where_clause, filter_sql);
579 }
580
581 let sql = gen.delete(meta.table_name.as_ref(), &where_clause);
582 let rows = conn.execute(&sql, ¶ms).await?;
583 if rows == 0 && pk_count > 0 {
584 return Err(EFError::concurrency_conflict(format!(
585 "batch delete affected 0 rows on {} (rows may have been modified or deleted)",
586 meta.table_name
587 )));
588 }
589 deleted += (rows as usize).min(pk_count);
590 start = end;
591 }
592
593 Ok(deleted)
594 }
595
596 #[allow(clippy::type_complexity)]
599 async fn execute_deletes_per_row<E>(
600 conn: &mut dyn IAsyncConnection,
601 gen: &'static dyn crate::provider::ISqlGenerator,
602 entities: &[(&E, &EntityTypeMeta, Option<&HashMap<String, DbValue>>)],
603 query_filter: Option<&BoolExpr>,
604 ) -> EFResult<usize>
605 where
606 E: IEntityType + IGetKeyValues,
607 {
608 let mut deleted = 0;
609 for (entity, meta, original) in entities {
610 let keys = entity.key_values();
611 if keys.is_empty() {
612 continue;
613 }
614 let scalar_props: Vec<_> = meta.mapped_scalar_properties().collect();
615 let concurrency_tokens: Vec<&PropertyMeta> = scalar_props
616 .iter()
617 .copied()
618 .filter(|p| p.is_concurrency_token)
619 .collect();
620
621 let (mut where_clause, mut where_params) =
622 build_where_with_concurrency(gen, &keys, &concurrency_tokens, *original, 1)?;
623
624 if let Some(filter) = query_filter {
625 let mut idx = where_params.len() + 1;
626 let filter_sql = compile_bool_expr(filter, gen, &mut idx);
627 where_params.extend(collect_bool_expr_values(filter));
628 where_clause = format!("({}) AND ({})", where_clause, filter_sql);
629 }
630
631 let sql = gen.delete(meta.table_name.as_ref(), &where_clause);
632 let rows = conn.execute(&sql, &where_params).await?;
633 if rows == 0 {
634 return Err(EFError::concurrency_conflict(format!(
635 "delete affected 0 rows on {} (row may have been modified or deleted)",
636 meta.table_name
637 )));
638 }
639 deleted += 1;
640 }
641 Ok(deleted)
642 }
643}
644
645fn build_where_with_concurrency(
646 gen: &dyn crate::provider::ISqlGenerator,
647 keys: &HashMap<String, DbValue>,
648 concurrency_tokens: &[&PropertyMeta],
649 original: Option<&HashMap<String, DbValue>>,
650 start_param_idx: usize,
651) -> EFResult<(String, Vec<DbValue>)> {
652 let mut where_parts: Vec<String> = keys
653 .keys()
654 .enumerate()
655 .map(|(i, k)| {
656 format!(
657 "{} = {}",
658 gen.quote_identifier(k),
659 gen.parameter_placeholder(start_param_idx + i)
660 )
661 })
662 .collect();
663
664 let mut params: Vec<DbValue> = keys.values().cloned().collect();
665
666 for (next_idx, token) in (start_param_idx + keys.len()..).zip(concurrency_tokens.iter()) {
667 where_parts.push(format!(
668 "{} = {}",
669 gen.quote_identifier(token.column_name.as_ref()),
670 gen.parameter_placeholder(next_idx)
671 ));
672
673 let original_val = original
674 .and_then(|o| o.get(token.field_name.as_ref()))
675 .ok_or_else(|| {
676 EFError::change_tracking(format!(
677 "missing original concurrency token for '{}'",
678 token.field_name
679 ))
680 })?;
681 params.push(original_val.clone());
682 }
683
684 Ok((where_parts.join(" AND "), params))
685}
686
687pub fn generate_insert_sql(
692 provider: &dyn IDatabaseProvider,
693 meta: &EntityTypeMeta,
694 _property_values: &HashMap<String, DbValue>,
695) -> String {
696 let gen = provider.sql_generator();
697 let scalar_props: Vec<_> = meta.mapped_scalar_properties().collect();
698 let columns: Vec<&str> = scalar_props
699 .iter()
700 .map(|p| p.column_name.as_ref())
701 .collect();
702 if columns.is_empty() {
703 return String::new();
704 }
705 gen.insert(meta.table_name.as_ref(), &columns, true)
706}
707
708pub fn generate_update_sql(
709 provider: &dyn IDatabaseProvider,
710 meta: &EntityTypeMeta,
711 property_values: &HashMap<String, DbValue>,
712 primary_key_values: &HashMap<String, DbValue>,
713) -> String {
714 let gen = provider.sql_generator();
715 let set_columns: Vec<&str> = property_values
716 .keys()
717 .filter(|k| !primary_key_values.contains_key(*k))
718 .map(|k| k.as_str())
719 .collect();
720 if set_columns.is_empty() || primary_key_values.is_empty() {
721 return String::new();
722 }
723 let where_parts: Vec<String> = primary_key_values
724 .keys()
725 .enumerate()
726 .map(|(i, k)| {
727 format!(
728 "{} = {}",
729 gen.quote_identifier(k),
730 gen.parameter_placeholder(i + 1)
731 )
732 })
733 .collect();
734 gen.update(
735 meta.table_name.as_ref(),
736 &set_columns,
737 &where_parts.join(" AND "),
738 )
739}
740
741pub fn generate_delete_sql(
742 provider: &dyn IDatabaseProvider,
743 meta: &EntityTypeMeta,
744 primary_key_values: &HashMap<String, DbValue>,
745) -> String {
746 let gen = provider.sql_generator();
747 if primary_key_values.is_empty() {
748 return String::new();
749 }
750 let where_parts: Vec<String> = primary_key_values
751 .keys()
752 .enumerate()
753 .map(|(i, k)| {
754 format!(
755 "{} = {}",
756 gen.quote_identifier(k),
757 gen.parameter_placeholder(i + 1)
758 )
759 })
760 .collect();
761 gen.delete(meta.table_name.as_ref(), &where_parts.join(" AND "))
762}
763
764pub fn collect_insert_params(
765 meta: &EntityTypeMeta,
766 property_values: &HashMap<String, DbValue>,
767) -> Vec<DbValue> {
768 meta.mapped_scalar_properties()
769 .map(|p| {
770 property_values
771 .get(p.field_name.as_ref())
772 .cloned()
773 .unwrap_or(DbValue::Null)
774 })
775 .collect()
776}
777
778pub fn collect_update_params(
779 property_values: &HashMap<String, DbValue>,
780 primary_key_values: &HashMap<String, DbValue>,
781 set_keys: &[String],
782) -> Vec<DbValue> {
783 let mut params: Vec<DbValue> = set_keys
784 .iter()
785 .filter(|k| !primary_key_values.contains_key(*k))
786 .map(|k| property_values.get(k).cloned().unwrap_or(DbValue::Null))
787 .collect();
788 for v in primary_key_values.values() {
789 params.push(v.clone());
790 }
791 params
792}
793
794pub fn collect_delete_params(primary_key_values: &HashMap<String, DbValue>) -> Vec<DbValue> {
795 primary_key_values.values().cloned().collect()
796}