1use crate::entity::{IEntitySnapshot, IEntityType, IGetKeyValues};
4use crate::entity_snapshot::EntitySnapshot;
5use crate::error::{EFError, EFResult};
6use crate::metadata::{EntityTypeMeta, PropertyMeta};
7use crate::provider::{DbValue, IAsyncConnection, IDatabaseProvider};
8use crate::query::{collect_bool_expr_values, compile_bool_expr, BoolExpr};
9
10use super::executor::ChangeExecutor;
11
12impl ChangeExecutor {
13 #[allow(clippy::type_complexity)]
30 pub async fn execute_updates<E>(
31 conn: &mut dyn IAsyncConnection,
32 provider: &dyn IDatabaseProvider,
33 entities: &[(&E, &EntityTypeMeta, Option<&EntitySnapshot>, &[String])],
34 query_filter: Option<&BoolExpr>,
35 ) -> EFResult<usize>
36 where
37 E: IEntityType + IEntitySnapshot + IGetKeyValues,
38 {
39 if entities.is_empty() {
40 return Ok(0);
41 }
42 let gen = provider.sql_generator();
43 let meta = entities[0].1;
44 let scalar_props: Vec<_> = meta.mapped_scalar_properties().collect();
45 let pk_props: Vec<_> = scalar_props.iter().filter(|p| p.is_primary_key).collect();
46 let has_concurrency_tokens = scalar_props.iter().any(|p| p.is_concurrency_token);
47
48 let modified_fields: std::collections::HashSet<&str> = entities
52 .iter()
53 .flat_map(|(_, _, _, mods)| mods.iter().map(|s| s.as_str()))
54 .collect();
55 let set_props: Vec<&PropertyMeta> = if modified_fields.is_empty() {
56 scalar_props
57 .iter()
58 .copied()
59 .filter(|p| !p.is_primary_key)
60 .collect()
61 } else {
62 scalar_props
63 .iter()
64 .copied()
65 .filter(|p| !p.is_primary_key && modified_fields.contains(p.field_name.as_ref()))
66 .collect()
67 };
68 let set_cols: Vec<&str> = set_props.iter().map(|p| p.column_name.as_ref()).collect();
69
70 if has_concurrency_tokens || pk_props.len() != 1 || set_cols.is_empty() {
74 return Self::execute_updates_per_row(conn, gen, entities, query_filter).await;
75 }
76
77 let pk_col = pk_props[0].column_name.as_ref();
78 let pk_field = pk_props[0].field_name.as_ref();
79
80 let entity_data: Vec<(EntitySnapshot, EntitySnapshot)> = entities
82 .iter()
83 .map(|(e, _, _, _)| (e.snapshot(), e.key_values()))
84 .collect();
85
86 let filter_params: Vec<DbValue> = match query_filter {
88 Some(filter) => collect_bool_expr_values(filter),
89 None => Vec::new(),
90 };
91
92 let cached_filter_sql: Option<String> = if !gen.uses_numbered_placeholders() {
96 query_filter.map(|f| {
97 let mut idx = 1;
98 compile_bool_expr(f, gen, &mut idx)
99 })
100 } else {
101 None
102 };
103
104 const MAX_PARAMS: usize = 900;
107 let params_per_row = 2 * set_cols.len() + 1;
108 let batch_size =
109 ((MAX_PARAMS.saturating_sub(filter_params.len())) / params_per_row.max(1)).max(1);
110
111 let mut updated = 0usize;
112 let mut start = 0usize;
113 while start < entity_data.len() {
114 let end = (start + batch_size).min(entity_data.len());
115 let row_count = end - start;
116
117 let set_param_count = 2 * set_cols.len() * row_count;
119 let mut idx = set_param_count + 1;
120
121 let pk_placeholders: Vec<String> = (0..row_count)
123 .map(|_| {
124 let ph = gen.parameter_placeholder(idx);
125 idx += 1;
126 ph
127 })
128 .collect();
129 let mut where_clause = format!(
130 "{} IN ({})",
131 gen.quote_identifier(pk_col),
132 pk_placeholders.join(", ")
133 );
134
135 let mut params: Vec<DbValue> = Vec::with_capacity(set_param_count + row_count);
137 for col_prop in &set_props {
138 for (snap, keys) in entity_data[start..end].iter() {
139 let pk_val = keys.get(pk_field).cloned().unwrap_or(DbValue::Null);
140 let col_val = snap
141 .get(col_prop.field_name.as_ref())
142 .cloned()
143 .unwrap_or(DbValue::Null);
144 params.push(pk_val);
145 params.push(col_val);
146 }
147 }
148 for (_, keys) in entity_data[start..end].iter() {
150 let pk_val = keys.get(pk_field).cloned().unwrap_or(DbValue::Null);
151 params.push(pk_val);
152 }
153
154 if let Some(filter) = query_filter {
156 let filter_sql = match &cached_filter_sql {
157 Some(cached) => cached.clone(),
158 None => compile_bool_expr(filter, gen, &mut idx),
159 };
160 params.extend(filter_params.iter().cloned());
161 where_clause = format!("({}) AND ({})", where_clause, filter_sql);
162 }
163
164 let sql = gen.update_batch(
165 meta.table_name.as_ref(),
166 &set_cols,
167 pk_col,
168 row_count,
169 &where_clause,
170 );
171 let rows = conn.execute(&sql, ¶ms).await?;
172 if rows == 0 && row_count > 0 {
173 return Err(EFError::concurrency_conflict(format!(
174 "batch update affected 0 rows on {} (rows may have been modified or deleted)",
175 meta.table_name
176 )));
177 }
178 updated += (rows as usize).min(row_count);
179 start = end;
180 }
181
182 Ok(updated)
183 }
184
185 #[allow(clippy::type_complexity)]
191 async fn execute_updates_per_row<E>(
192 conn: &mut dyn IAsyncConnection,
193 gen: &'static dyn crate::provider::ISqlGenerator,
194 entities: &[(&E, &EntityTypeMeta, Option<&EntitySnapshot>, &[String])],
195 query_filter: Option<&BoolExpr>,
196 ) -> EFResult<usize>
197 where
198 E: IEntityType + IEntitySnapshot + IGetKeyValues,
199 {
200 let mut updated = 0;
201
202 let meta0 = entities[0].1;
206 let scalar_props: Vec<&PropertyMeta> = meta0.mapped_scalar_properties().collect();
207 let concurrency_tokens: Vec<&PropertyMeta> = scalar_props
208 .iter()
209 .copied()
210 .filter(|p| p.is_concurrency_token)
211 .collect();
212 let table_name = meta0.table_name.as_ref();
213
214 for (entity, _meta, original, modified_props) in entities {
215 let snap = entity.snapshot();
216 let keys = entity.key_values();
217
218 let set_props: Vec<&PropertyMeta> = if modified_props.is_empty() {
221 scalar_props
222 .iter()
223 .copied()
224 .filter(|p| !p.is_primary_key)
225 .collect()
226 } else {
227 let modified_set: std::collections::HashSet<&str> =
228 modified_props.iter().map(|s| s.as_str()).collect();
229 scalar_props
230 .iter()
231 .copied()
232 .filter(|p| !p.is_primary_key && modified_set.contains(p.field_name.as_ref()))
233 .collect()
234 };
235 let set_cols: Vec<&str> = set_props.iter().map(|p| p.column_name.as_ref()).collect();
236
237 if set_cols.is_empty() || keys.is_empty() {
238 continue;
239 }
240
241 let (mut where_clause, mut where_params) = build_where_with_concurrency(
242 gen,
243 &keys,
244 &concurrency_tokens,
245 *original,
246 set_cols.len() + 1,
247 )?;
248
249 if let Some(filter) = query_filter {
250 let mut idx = set_cols.len() + where_params.len() + 1;
251 let filter_sql = compile_bool_expr(filter, gen, &mut idx);
252 where_params.extend(collect_bool_expr_values(filter));
253 where_clause = format!("({}) AND ({})", where_clause, filter_sql);
254 }
255
256 let sql = gen.update(table_name, &set_cols, &where_clause);
257
258 let mut params: Vec<DbValue> = set_props
259 .iter()
260 .map(|p| {
261 snap.get(p.field_name.as_ref())
262 .cloned()
263 .unwrap_or(DbValue::Null)
264 })
265 .collect();
266 params.extend(where_params);
267
268 let rows = conn.execute(&sql, ¶ms).await?;
269 if rows == 0 {
270 return Err(EFError::concurrency_conflict(format!(
271 "update affected 0 rows on {} (row may have been modified or deleted)",
272 table_name
273 )));
274 }
275 updated += 1;
276 }
277
278 Ok(updated)
279 }
280
281 #[allow(clippy::type_complexity)]
292 pub async fn execute_deletes<E>(
293 conn: &mut dyn IAsyncConnection,
294 provider: &dyn IDatabaseProvider,
295 entities: &[(&E, &EntityTypeMeta, Option<&EntitySnapshot>)],
296 query_filter: Option<&BoolExpr>,
297 ) -> EFResult<usize>
298 where
299 E: IEntityType + IGetKeyValues,
300 {
301 if entities.is_empty() {
302 return Ok(0);
303 }
304 let gen = provider.sql_generator();
305 let meta = entities[0].1;
306 let scalar_props: Vec<_> = meta.mapped_scalar_properties().collect();
307 let has_concurrency_tokens = scalar_props.iter().any(|p| p.is_concurrency_token);
308 let pk_props: Vec<_> = scalar_props.iter().filter(|p| p.is_primary_key).collect();
309
310 if has_concurrency_tokens || pk_props.len() != 1 {
314 return Self::execute_deletes_per_row(conn, gen, entities, query_filter).await;
315 }
316
317 let pk_col = pk_props[0].column_name.as_ref();
318 let pk_field = pk_props[0].field_name.as_ref();
319
320 let pk_values: Vec<DbValue> = entities
322 .iter()
323 .filter_map(|(e, _, _)| e.key_values().get(pk_field).cloned())
324 .collect();
325 if pk_values.is_empty() {
326 return Ok(0);
327 }
328
329 let filter_params: Vec<DbValue> = match query_filter {
334 Some(filter) => collect_bool_expr_values(filter),
335 None => Vec::new(),
336 };
337 let cached_filter_sql: Option<String> = if !gen.uses_numbered_placeholders() {
338 query_filter.map(|f| {
339 let mut idx = 1;
340 compile_bool_expr(f, gen, &mut idx)
341 })
342 } else {
343 None
344 };
345 const MAX_PARAMS: usize = 900;
346 let batch_size = MAX_PARAMS.saturating_sub(filter_params.len()).max(1);
347
348 let mut deleted = 0usize;
349 let mut start = 0usize;
350 while start < pk_values.len() {
351 let end = (start + batch_size).min(pk_values.len());
352 let batch = &pk_values[start..end];
353 let pk_count = batch.len();
354
355 let pk_placeholders: Vec<String> = (1..=pk_count)
356 .map(|i| gen.parameter_placeholder(i))
357 .collect();
358 let mut where_clause = format!(
359 "{} IN ({})",
360 gen.quote_identifier(pk_col),
361 pk_placeholders.join(", "),
362 );
363 let mut params: Vec<DbValue> = batch.to_vec();
364 if let Some(filter) = query_filter {
365 let filter_sql = match &cached_filter_sql {
366 Some(cached) => cached.clone(),
367 None => {
368 let mut idx = pk_count + 1;
369 compile_bool_expr(filter, gen, &mut idx)
370 }
371 };
372 params.extend(filter_params.iter().cloned());
373 where_clause = format!("({}) AND ({})", where_clause, filter_sql);
374 }
375
376 let sql = gen.delete(meta.table_name.as_ref(), &where_clause);
377 let rows = conn.execute(&sql, ¶ms).await?;
378 if rows == 0 && pk_count > 0 {
379 return Err(EFError::concurrency_conflict(format!(
380 "batch delete affected 0 rows on {} (rows may have been modified or deleted)",
381 meta.table_name
382 )));
383 }
384 deleted += (rows as usize).min(pk_count);
385 start = end;
386 }
387
388 Ok(deleted)
389 }
390
391 #[allow(clippy::type_complexity)]
394 async fn execute_deletes_per_row<E>(
395 conn: &mut dyn IAsyncConnection,
396 gen: &'static dyn crate::provider::ISqlGenerator,
397 entities: &[(&E, &EntityTypeMeta, Option<&EntitySnapshot>)],
398 query_filter: Option<&BoolExpr>,
399 ) -> EFResult<usize>
400 where
401 E: IEntityType + IGetKeyValues,
402 {
403 let mut deleted = 0;
404
405 let meta0 = entities[0].1;
408 let concurrency_tokens: Vec<&PropertyMeta> = meta0
409 .mapped_scalar_properties()
410 .filter(|p| p.is_concurrency_token)
411 .collect();
412 let table_name = meta0.table_name.as_ref();
413
414 for (entity, _meta, original) in entities {
415 let keys = entity.key_values();
416 if keys.is_empty() {
417 continue;
418 }
419
420 let (mut where_clause, mut where_params) =
421 build_where_with_concurrency(gen, &keys, &concurrency_tokens, *original, 1)?;
422
423 if let Some(filter) = query_filter {
424 let mut idx = where_params.len() + 1;
425 let filter_sql = compile_bool_expr(filter, gen, &mut idx);
426 where_params.extend(collect_bool_expr_values(filter));
427 where_clause = format!("({}) AND ({})", where_clause, filter_sql);
428 }
429
430 let sql = gen.delete(table_name, &where_clause);
431 let rows = conn.execute(&sql, &where_params).await?;
432 if rows == 0 {
433 return Err(EFError::concurrency_conflict(format!(
434 "delete affected 0 rows on {} (row may have been modified or deleted)",
435 table_name
436 )));
437 }
438 deleted += 1;
439 }
440 Ok(deleted)
441 }
442}
443
444fn build_where_with_concurrency(
445 gen: &dyn crate::provider::ISqlGenerator,
446 keys: &EntitySnapshot,
447 concurrency_tokens: &[&PropertyMeta],
448 original: Option<&EntitySnapshot>,
449 start_param_idx: usize,
450) -> EFResult<(String, Vec<DbValue>)> {
451 let mut where_parts: Vec<String> = keys
452 .iter()
453 .enumerate()
454 .map(|(i, (k, _))| {
455 format!(
456 "{} = {}",
457 gen.quote_identifier(k),
458 gen.parameter_placeholder(start_param_idx + i)
459 )
460 })
461 .collect();
462
463 let mut params: Vec<DbValue> = keys.iter().map(|(_, v)| v.clone()).collect();
464
465 for (next_idx, token) in (start_param_idx + keys.len()..).zip(concurrency_tokens.iter()) {
466 where_parts.push(format!(
467 "{} = {}",
468 gen.quote_identifier(token.column_name.as_ref()),
469 gen.parameter_placeholder(next_idx)
470 ));
471
472 let original_val = original
473 .and_then(|o| o.get(token.field_name.as_ref()))
474 .ok_or_else(|| {
475 EFError::change_tracking(format!(
476 "missing original concurrency token for '{}'",
477 token.field_name
478 ))
479 })?;
480 params.push(original_val.clone());
481 }
482
483 Ok((where_parts.join(" AND "), params))
484}