1use std::marker::PhantomData;
4
5use crate::error::QueryResult;
6use crate::filter::{Filter, FilterValue};
7use crate::inputs::WriteOp;
8use crate::nested::NestedWriteOp;
9use crate::traits::{Model, QueryEngine};
10use crate::types::Select;
11
12pub(crate) fn extract_pk_from_filter(filter: &Filter, pk_col: &str) -> Option<FilterValue> {
19 match filter {
20 Filter::Equals(name, value) if name.as_ref() == pk_col => Some(value.clone()),
21 _ => None,
22 }
23}
24
25pub struct UpdateOperation<E: QueryEngine, M: Model> {
39 engine: E,
40 filter: Filter,
41 updates: Vec<(String, WriteOp)>,
42 select: Select,
43 nested: Vec<NestedWriteOp>,
47 _model: PhantomData<M>,
48}
49
50impl<E: QueryEngine, M: Model + crate::row::FromRow> UpdateOperation<E, M> {
51 pub fn new(engine: E) -> Self {
53 Self {
54 engine,
55 filter: Filter::None,
56 updates: Vec::new(),
57 select: Select::All,
58 nested: Vec::new(),
59 _model: PhantomData,
60 }
61 }
62
63 pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
65 let new_filter = filter.into();
66 self.filter = self.filter.and_then(new_filter);
67 self
68 }
69
70 pub fn set(mut self, column: impl Into<String>, value: impl Into<FilterValue>) -> Self {
72 self.updates
73 .push((column.into(), WriteOp::Set(value.into())));
74 self
75 }
76
77 pub fn set_many(
79 mut self,
80 values: impl IntoIterator<Item = (impl Into<String>, impl Into<FilterValue>)>,
81 ) -> Self {
82 for (col, val) in values {
83 self.updates.push((col.into(), WriteOp::Set(val.into())));
84 }
85 self
86 }
87
88 pub fn increment(mut self, column: impl Into<String>, amount: i64) -> Self {
90 self.updates
91 .push((column.into(), WriteOp::Increment(FilterValue::Int(amount))));
92 self
93 }
94
95 pub fn set_op(mut self, column: impl Into<String>, op: WriteOp) -> Self {
102 self.updates.push((column.into(), op));
103 self
104 }
105
106 pub fn select(mut self, select: impl Into<Select>) -> Self {
108 self.select = select.into();
109 self
110 }
111
112 pub fn build_sql(
114 &self,
115 dialect: &dyn crate::dialect::SqlDialect,
116 ) -> (String, Vec<FilterValue>) {
117 let mut sql = String::new();
118 let mut params = Vec::new();
119 let mut param_idx = 1;
120
121 sql.push_str("UPDATE ");
123 sql.push_str(M::TABLE_NAME);
124
125 sql.push_str(" SET ");
127 let set_parts: Vec<String> = self
128 .updates
129 .iter()
130 .map(|(col, op)| {
131 let placeholder = dialect.placeholder(param_idx);
132 let (fragment, value) = op.to_set_fragment(col, &placeholder);
133 if let Some(v) = value {
134 params.push(v);
135 param_idx += 1;
136 }
137 fragment
138 })
139 .collect();
140 sql.push_str(&set_parts.join(", "));
141
142 if !self.filter.is_none() {
144 let (where_sql, where_params) = self.filter.to_sql(param_idx - 1, dialect);
145 sql.push_str(" WHERE ");
146 sql.push_str(&where_sql);
147 params.extend(where_params);
148 }
149
150 sql.push_str(&dialect.returning_clause(&self.select.to_sql()));
152
153 (sql, params)
154 }
155
156 pub fn with(mut self, nw: NestedWriteOp) -> Self
168 where
169 E: crate::capabilities::SupportsNestedWrites,
170 {
171 self.nested.push(nw);
172 self
173 }
174
175 fn empty_updates_error() -> crate::error::QueryError {
179 crate::error::QueryError::invalid_input(
180 "updates",
181 "update requires at least one SET value — an empty SET list would \
182 produce invalid SQL (`UPDATE ... SET WHERE ...`)",
183 )
184 .with_help(format!(
185 "add a field update via `.set(...)`, `.set_many(...)`, `.increment(...)`, \
186 `.set_op(...)`, or a typed `UpdateInput` before executing the update on `{table}`.",
187 table = M::TABLE_NAME,
188 ))
189 }
190
191 pub async fn exec(self) -> QueryResult<Vec<M>>
193 where
194 M: Send + 'static,
195 {
196 if self.updates.is_empty() {
197 return Err(Self::empty_updates_error());
198 }
199
200 if self.nested.is_empty() {
202 let dialect = self.engine.dialect();
203 let (sql, params) = self.build_sql(dialect);
204 return self.engine.execute_update::<M>(&sql, params).await;
205 }
206
207 let parent_pk =
210 extract_pk_from_filter(&self.filter, M::PRIMARY_KEY[0]).ok_or_else(|| {
211 crate::error::QueryError::invalid_input(
212 "where",
213 "nested writes inside `update!` require the `where:` clause to equal-match \
214 the primary-key column",
215 )
216 .with_help(format!(
217 "expected `where: {{ {pk}: <value> }}` on `{table}` — non-PK unique \
218 columns are not yet supported for nested writes inside update!. \
219 Lift this restriction by running the nested ops in a separate operation \
220 after looking up the row's PK.",
221 pk = M::PRIMARY_KEY[0],
222 table = M::TABLE_NAME,
223 ))
224 })?;
225
226 let UpdateOperation {
227 engine,
228 filter,
229 updates,
230 select,
231 nested,
232 _model,
233 } = self;
234
235 engine
236 .transaction(move |tx| async move {
237 let dialect = tx.dialect();
238 let (sql, params) = Self::build_sql_parts(&filter, &updates, &select, dialect);
239 let parent: Vec<M> = tx.execute_update::<M>(&sql, params).await?;
240
241 let mut idx = 0;
243 while idx < nested.len() {
244 if let NestedWriteOp::Connect {
245 target_table: run_table,
246 foreign_key: run_fk,
247 target_pk: run_target_pk,
248 ..
249 } = &nested[idx]
250 {
251 let run_table = *run_table;
252 let run_fk = *run_fk;
253 let run_target_pk = *run_target_pk;
254 let mut end = idx + 1;
255 while end < nested.len() {
256 match &nested[end] {
257 NestedWriteOp::Connect {
258 target_table,
259 foreign_key,
260 target_pk,
261 ..
262 } if *target_table == run_table
263 && *foreign_key == run_fk
264 && *target_pk == run_target_pk =>
265 {
266 end += 1;
267 }
268 _ => break,
269 }
270 }
271
272 if end - idx == 1 {
273 let op = nested[idx].clone();
274 op.execute(&tx, &parent_pk).await?;
275 } else {
276 let expected = (end - idx) as u64;
277 let mut pks: Vec<FilterValue> = Vec::with_capacity(end - idx + 1);
278 pks.push(parent_pk.clone());
279 for op in &nested[idx..end] {
280 if let NestedWriteOp::Connect { pk, .. } = op {
281 pks.push(pk.clone());
282 }
283 }
284 let placeholders: Vec<String> =
285 (2..=pks.len()).map(|i| dialect.placeholder(i)).collect();
286 let sql = format!(
287 "UPDATE {} SET {} = {} WHERE {} IN ({})",
288 dialect.quote_ident(run_table),
289 dialect.quote_ident(run_fk),
290 dialect.placeholder(1),
291 dialect.quote_ident(run_target_pk),
292 placeholders.join(", "),
293 );
294 let affected = tx.execute_raw(&sql, pks).await?;
295 if affected != expected {
296 return Err(crate::error::QueryError::not_found(run_table)
297 .with_context("Nested Connect batch")
298 .with_help(format!(
299 "Expected {} matching rows but UPDATE affected {}",
300 expected, affected
301 )));
302 }
303 }
304 idx = end;
305 } else {
306 let op = nested[idx].clone();
307 op.execute(&tx, &parent_pk).await?;
308 idx += 1;
309 }
310 }
311 Ok(parent)
312 })
313 .await
314 }
315
316 fn build_sql_parts(
320 filter: &Filter,
321 updates: &[(String, WriteOp)],
322 select: &Select,
323 dialect: &dyn crate::dialect::SqlDialect,
324 ) -> (String, Vec<FilterValue>) {
325 let mut sql = String::new();
326 let mut params = Vec::new();
327 let mut param_idx = 1;
328
329 sql.push_str("UPDATE ");
330 sql.push_str(M::TABLE_NAME);
331
332 sql.push_str(" SET ");
333 let set_parts: Vec<String> = updates
334 .iter()
335 .map(|(col, op)| {
336 let placeholder = dialect.placeholder(param_idx);
337 let (fragment, value) = op.to_set_fragment(col, &placeholder);
338 if let Some(v) = value {
339 params.push(v);
340 param_idx += 1;
341 }
342 fragment
343 })
344 .collect();
345 sql.push_str(&set_parts.join(", "));
346
347 if !filter.is_none() {
348 let (where_sql, where_params) = filter.to_sql(param_idx - 1, dialect);
349 sql.push_str(" WHERE ");
350 sql.push_str(&where_sql);
351 params.extend(where_params);
352 }
353
354 sql.push_str(&dialect.returning_clause(&select.to_sql()));
355
356 (sql, params)
357 }
358
359 pub async fn exec_one(self) -> QueryResult<M>
361 where
362 M: Send + 'static,
363 {
364 if self.updates.is_empty() {
365 return Err(Self::empty_updates_error());
366 }
367
368 let dialect = self.engine.dialect();
369 let (sql, params) = self.build_sql(dialect);
370 self.engine.query_one::<M>(&sql, params).await
371 }
372
373 pub fn with_where_input<W: crate::inputs::WhereUniqueInput<Model = M>>(mut self, w: W) -> Self {
377 let f = w.into_ir();
378 self.filter = self.filter.and_then(f);
379 self
380 }
381
382 pub fn with_select_input<S: crate::inputs::SelectInput<Model = M>>(mut self, s: S) -> Self {
384 self.select = s.into_ir();
385 self
386 }
387
388 pub fn with_update_input<I>(mut self, input: I) -> Self
396 where
397 I: crate::inputs::UpdateInput<Model = M, Data = crate::inputs::UpdatePayload>,
398 {
399 let data: crate::inputs::UpdatePayload = input.into_ir();
400 for (col, op) in data {
401 self.updates.push((col, op));
402 }
403 self
404 }
405
406 #[doc(hidden)]
408 pub fn filter_for_test(&self) -> &Filter {
409 &self.filter
410 }
411}
412
413pub struct UpdateManyOperation<E: QueryEngine, M: Model> {
415 engine: E,
416 filter: Filter,
417 updates: Vec<(String, WriteOp)>,
418 _model: PhantomData<M>,
419}
420
421impl<E: QueryEngine, M: Model> UpdateManyOperation<E, M> {
422 pub fn new(engine: E) -> Self {
424 Self {
425 engine,
426 filter: Filter::None,
427 updates: Vec::new(),
428 _model: PhantomData,
429 }
430 }
431
432 pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
434 let new_filter = filter.into();
435 self.filter = self.filter.and_then(new_filter);
436 self
437 }
438
439 pub fn set(mut self, column: impl Into<String>, value: impl Into<FilterValue>) -> Self {
441 self.updates
442 .push((column.into(), WriteOp::Set(value.into())));
443 self
444 }
445
446 pub fn set_op(mut self, column: impl Into<String>, op: WriteOp) -> Self {
448 self.updates.push((column.into(), op));
449 self
450 }
451
452 pub fn with_where_input<W: crate::inputs::WhereInput<Model = M>>(mut self, w: W) -> Self {
454 let f = w.into_ir();
455 self.filter = self.filter.and_then(f);
456 self
457 }
458
459 pub fn with_update_input<I>(mut self, input: I) -> Self
465 where
466 I: crate::inputs::UpdateInput<Model = M, Data = crate::inputs::UpdatePayload>,
467 {
468 let data: crate::inputs::UpdatePayload = input.into_ir();
469 for (col, op) in data {
470 self.updates.push((col, op));
471 }
472 self
473 }
474
475 pub fn build_sql(
477 &self,
478 dialect: &dyn crate::dialect::SqlDialect,
479 ) -> (String, Vec<FilterValue>) {
480 let mut sql = String::new();
481 let mut params = Vec::new();
482 let mut param_idx = 1;
483
484 sql.push_str("UPDATE ");
486 sql.push_str(M::TABLE_NAME);
487
488 sql.push_str(" SET ");
490 let set_parts: Vec<String> = self
491 .updates
492 .iter()
493 .map(|(col, op)| {
494 let placeholder = dialect.placeholder(param_idx);
495 let (fragment, value) = op.to_set_fragment(col, &placeholder);
496 if let Some(v) = value {
497 params.push(v);
498 param_idx += 1;
499 }
500 fragment
501 })
502 .collect();
503 sql.push_str(&set_parts.join(", "));
504
505 if !self.filter.is_none() {
507 let (where_sql, where_params) = self.filter.to_sql(param_idx - 1, dialect);
508 sql.push_str(" WHERE ");
509 sql.push_str(&where_sql);
510 params.extend(where_params);
511 }
512
513 (sql, params)
514 }
515
516 pub async fn exec(self) -> QueryResult<u64> {
518 let dialect = self.engine.dialect();
519 let (sql, params) = self.build_sql(dialect);
520 self.engine.execute_raw(&sql, params).await
521 }
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527 use crate::error::QueryError;
528 use crate::types::Select;
529
530 struct TestModel;
531
532 impl Model for TestModel {
533 const MODEL_NAME: &'static str = "TestModel";
534 const TABLE_NAME: &'static str = "test_models";
535 const PRIMARY_KEY: &'static [&'static str] = &["id"];
536 const COLUMNS: &'static [&'static str] = &["id", "name", "email"];
537 }
538
539 impl crate::row::FromRow for TestModel {
540 fn from_row(_row: &impl crate::row::RowRef) -> Result<Self, crate::row::RowError> {
541 Ok(TestModel)
542 }
543 }
544
545 #[derive(Clone)]
546 struct MockEngine {
547 return_count: u64,
548 }
549
550 impl MockEngine {
551 fn new() -> Self {
552 Self { return_count: 0 }
553 }
554
555 fn with_count(count: u64) -> Self {
556 Self {
557 return_count: count,
558 }
559 }
560 }
561
562 impl QueryEngine for MockEngine {
563 fn dialect(&self) -> &dyn crate::dialect::SqlDialect {
564 &crate::dialect::Postgres
565 }
566
567 fn query_many<T: Model + crate::row::FromRow + Send + 'static>(
568 &self,
569 _sql: &str,
570 _params: Vec<FilterValue>,
571 ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
572 Box::pin(async { Ok(Vec::new()) })
573 }
574
575 fn query_one<T: Model + crate::row::FromRow + Send + 'static>(
576 &self,
577 _sql: &str,
578 _params: Vec<FilterValue>,
579 ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
580 Box::pin(async { Err(QueryError::not_found("test")) })
581 }
582
583 fn query_optional<T: Model + crate::row::FromRow + Send + 'static>(
584 &self,
585 _sql: &str,
586 _params: Vec<FilterValue>,
587 ) -> crate::traits::BoxFuture<'_, QueryResult<Option<T>>> {
588 Box::pin(async { Ok(None) })
589 }
590
591 fn execute_insert<T: Model + crate::row::FromRow + Send + 'static>(
592 &self,
593 _sql: &str,
594 _params: Vec<FilterValue>,
595 ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
596 Box::pin(async { Err(QueryError::not_found("test")) })
597 }
598
599 fn execute_update<T: Model + crate::row::FromRow + Send + 'static>(
600 &self,
601 _sql: &str,
602 _params: Vec<FilterValue>,
603 ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
604 Box::pin(async { Ok(Vec::new()) })
605 }
606
607 fn execute_delete(
608 &self,
609 _sql: &str,
610 _params: Vec<FilterValue>,
611 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
612 Box::pin(async { Ok(0) })
613 }
614
615 fn execute_raw(
616 &self,
617 _sql: &str,
618 _params: Vec<FilterValue>,
619 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
620 let count = self.return_count;
621 Box::pin(async move { Ok(count) })
622 }
623
624 fn count(
625 &self,
626 _sql: &str,
627 _params: Vec<FilterValue>,
628 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
629 Box::pin(async { Ok(0) })
630 }
631 }
632
633 #[test]
636 fn test_update_new() {
637 let op = UpdateOperation::<MockEngine, TestModel>::new(MockEngine::new());
638 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
639
640 assert!(sql.contains("UPDATE test_models SET"));
641 assert!(sql.contains("RETURNING *"));
642 assert!(params.is_empty());
643 }
644
645 #[test]
646 fn test_update_basic() {
647 let op = UpdateOperation::<MockEngine, TestModel>::new(MockEngine::new())
648 .r#where(Filter::Equals("id".into(), FilterValue::Int(1)))
649 .set("name", "Updated");
650
651 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
652
653 assert!(sql.contains("UPDATE test_models SET"));
654 assert!(sql.contains("name = $1"));
655 assert!(sql.contains("WHERE"));
656 assert!(sql.contains("RETURNING *"));
657 assert_eq!(params.len(), 2);
658 }
659
660 #[test]
661 fn test_update_many_fields() {
662 let op = UpdateOperation::<MockEngine, TestModel>::new(MockEngine::new())
663 .set("name", "Updated")
664 .set("email", "updated@example.com");
665
666 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
667
668 assert!(sql.contains("name = $1"));
669 assert!(sql.contains("email = $2"));
670 assert_eq!(params.len(), 2);
671 }
672
673 #[test]
674 fn test_update_with_set_many() {
675 let updates = vec![
676 ("name", FilterValue::String("Alice".to_string())),
677 ("email", FilterValue::String("alice@test.com".to_string())),
678 ("age", FilterValue::Int(30)),
679 ];
680 let op = UpdateOperation::<MockEngine, TestModel>::new(MockEngine::new()).set_many(updates);
681
682 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
683
684 assert!(sql.contains("name = $1"));
685 assert!(sql.contains("email = $2"));
686 assert!(sql.contains("age = $3"));
687 assert_eq!(params.len(), 3);
688 }
689
690 #[test]
691 fn test_update_increment() {
692 let op = UpdateOperation::<MockEngine, TestModel>::new(MockEngine::new())
693 .increment("counter", 5);
694
695 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
696
697 assert!(
701 sql.contains("counter = counter + $1"),
702 "expected `counter = counter + $1`, got: {sql}"
703 );
704 assert_eq!(params.len(), 1);
705 assert_eq!(params[0], FilterValue::Int(5));
706 }
707
708 #[test]
709 fn test_update_with_select() {
710 let op = UpdateOperation::<MockEngine, TestModel>::new(MockEngine::new())
711 .set("name", "Updated")
712 .select(Select::fields(["id", "name"]));
713
714 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
715
716 assert!(sql.contains("RETURNING id, name"));
717 }
718
719 #[test]
720 fn test_update_with_complex_filter() {
721 let op = UpdateOperation::<MockEngine, TestModel>::new(MockEngine::new())
722 .r#where(Filter::Equals(
723 "status".into(),
724 FilterValue::String("active".to_string()),
725 ))
726 .r#where(Filter::Gt("age".into(), FilterValue::Int(18)))
727 .set("verified", FilterValue::Bool(true));
728
729 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
730
731 assert!(sql.contains("WHERE"));
732 assert!(sql.contains("AND"));
733 assert_eq!(params.len(), 3); }
735
736 #[test]
737 fn test_update_without_filter() {
738 let op = UpdateOperation::<MockEngine, TestModel>::new(MockEngine::new())
739 .set("status", "updated");
740
741 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
742
743 assert!(!sql.contains("WHERE"));
745 assert!(sql.contains("UPDATE test_models SET"));
746 }
747
748 #[test]
749 fn test_update_with_null_value() {
750 let op = UpdateOperation::<MockEngine, TestModel>::new(MockEngine::new())
751 .set("deleted_at", FilterValue::Null);
752
753 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
754
755 assert!(sql.contains("deleted_at = $1"));
756 assert_eq!(params.len(), 1);
757 assert_eq!(params[0], FilterValue::Null);
758 }
759
760 #[test]
761 fn test_update_with_boolean() {
762 let op = UpdateOperation::<MockEngine, TestModel>::new(MockEngine::new())
763 .set("active", FilterValue::Bool(true))
764 .set("verified", FilterValue::Bool(false));
765
766 let (_sql, params) = op.build_sql(&crate::dialect::Postgres);
767
768 assert_eq!(params.len(), 2);
769 assert_eq!(params[0], FilterValue::Bool(true));
770 assert_eq!(params[1], FilterValue::Bool(false));
771 }
772
773 #[tokio::test]
774 async fn test_update_exec() {
775 let op =
776 UpdateOperation::<MockEngine, TestModel>::new(MockEngine::new()).set("name", "Updated");
777
778 let result = op.exec().await;
779 assert!(result.is_ok());
780 assert!(result.unwrap().is_empty());
781 }
782
783 #[tokio::test]
784 async fn test_update_exec_one() {
785 let op = UpdateOperation::<MockEngine, TestModel>::new(MockEngine::new())
786 .r#where(Filter::Equals("id".into(), FilterValue::Int(1)))
787 .set("name", "Updated");
788
789 let result = op.exec_one().await;
790 assert!(result.is_err()); }
792
793 #[tokio::test]
794 async fn test_update_exec_rejects_empty_updates() {
795 let op = UpdateOperation::<MockEngine, TestModel>::new(MockEngine::new())
796 .r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
797
798 let err = op
799 .exec()
800 .await
801 .err()
802 .expect("empty SET list must be rejected before building SQL");
803 let msg = err.to_string();
804 assert!(
805 msg.contains("at least one SET value"),
806 "expected empty-update diagnostic, got: {msg}"
807 );
808 }
809
810 #[tokio::test]
811 async fn test_update_exec_one_rejects_empty_updates() {
812 let op = UpdateOperation::<MockEngine, TestModel>::new(MockEngine::new());
813
814 let err = op
815 .exec_one()
816 .await
817 .err()
818 .expect("empty SET list must be rejected before building SQL");
819 let msg = err.to_string();
820 assert!(
823 msg.contains("at least one SET value"),
824 "expected empty-update diagnostic, got: {msg}"
825 );
826 }
827
828 #[test]
831 fn test_update_many_new() {
832 let op = UpdateManyOperation::<MockEngine, TestModel>::new(MockEngine::new());
833 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
834
835 assert!(sql.contains("UPDATE test_models SET"));
836 assert!(!sql.contains("RETURNING")); assert!(params.is_empty());
838 }
839
840 #[test]
841 fn test_update_many_basic() {
842 let op = UpdateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
843 .r#where(Filter::In(
844 "id".into(),
845 vec![
846 FilterValue::Int(1),
847 FilterValue::Int(2),
848 FilterValue::Int(3),
849 ],
850 ))
851 .set("status", "processed");
852
853 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
854
855 assert!(sql.contains("UPDATE test_models SET"));
856 assert!(sql.contains("status = $1"));
857 assert!(sql.contains("WHERE"));
858 assert!(sql.contains("IN"));
859 assert_eq!(params.len(), 4); }
861
862 #[test]
863 fn test_update_many_with_multiple_conditions() {
864 let op = UpdateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
865 .r#where(Filter::Equals(
866 "department".into(),
867 FilterValue::String("engineering".to_string()),
868 ))
869 .r#where(Filter::Equals("active".into(), FilterValue::Bool(true)))
870 .set("reviewed", FilterValue::Bool(true));
871
872 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
873
874 assert!(sql.contains("AND"));
875 assert_eq!(params.len(), 3);
876 }
877
878 #[test]
879 fn test_update_many_without_where() {
880 let op = UpdateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
881 .set("reset_password", FilterValue::Bool(true));
882
883 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
884
885 assert!(!sql.contains("WHERE"));
886 }
887
888 #[tokio::test]
889 async fn test_update_many_exec() {
890 let op = UpdateManyOperation::<MockEngine, TestModel>::new(MockEngine::with_count(5))
891 .set("status", "updated");
892
893 let result = op.exec().await;
894 assert!(result.is_ok());
895 assert_eq!(result.unwrap(), 5);
896 }
897
898 #[test]
901 fn test_update_param_ordering() {
902 let op = UpdateOperation::<MockEngine, TestModel>::new(MockEngine::new())
903 .set("field1", "value1")
904 .set("field2", "value2")
905 .r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
906
907 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
908
909 assert!(sql.contains("field1 = $1"));
911 assert!(sql.contains("field2 = $2"));
912 assert!(sql.contains(r#""id" = $3"#));
913 assert_eq!(params.len(), 3);
914 }
915
916 #[test]
917 fn test_update_many_param_ordering() {
918 let op = UpdateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
919 .set("field1", "value1")
920 .r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
921
922 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
923
924 assert!(sql.contains("field1 = $1"));
925 assert!(sql.contains(r#""id" = $2"#));
926 assert_eq!(params.len(), 2);
927 }
928
929 #[test]
930 fn test_update_with_float_value() {
931 let op = UpdateOperation::<MockEngine, TestModel>::new(MockEngine::new())
932 .set("price", FilterValue::Float(99.99));
933
934 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
935
936 assert!(sql.contains("price = $1"));
937 assert_eq!(params.len(), 1);
938 }
939
940 #[test]
941 fn test_update_with_json_value() {
942 let json_value = serde_json::json!({"key": "value"});
943 let op = UpdateOperation::<MockEngine, TestModel>::new(MockEngine::new())
944 .set("metadata", FilterValue::Json(json_value.clone()));
945
946 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
947
948 assert!(sql.contains("metadata = $1"));
949 assert_eq!(params[0], FilterValue::Json(json_value));
950 }
951
952 struct MockUpdateInput(Vec<(String, WriteOp)>);
958
959 impl crate::inputs::UpdateInput for MockUpdateInput {
960 type Model = TestModel;
961 type Data = crate::inputs::UpdatePayload;
962 fn into_ir(self) -> Self::Data {
963 self.0
964 }
965 }
966
967 #[test]
968 fn with_update_input_appends_set_ops() {
969 let input = MockUpdateInput(vec![
970 (
971 "name".into(),
972 WriteOp::Set(FilterValue::String("Bob".into())),
973 ),
974 ("age".into(), WriteOp::Increment(FilterValue::Int(1))),
975 ]);
976
977 let op = UpdateOperation::<MockEngine, TestModel>::new(MockEngine::new())
978 .r#where(Filter::Equals("id".into(), FilterValue::Int(1)))
979 .with_update_input(input);
980
981 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
982
983 assert!(sql.contains("name = $1"), "got: {sql}");
986 assert!(sql.contains("age = age + $2"), "got: {sql}");
987 assert_eq!(params.len(), 3);
989 assert_eq!(params[0], FilterValue::String("Bob".into()));
990 assert_eq!(params[1], FilterValue::Int(1));
991 }
992
993 #[test]
994 fn with_update_input_unset_emits_null_no_param() {
995 let input = MockUpdateInput(vec![("nickname".into(), WriteOp::Unset)]);
996
997 let op = UpdateOperation::<MockEngine, TestModel>::new(MockEngine::new())
998 .with_update_input(input);
999
1000 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
1001
1002 assert!(sql.contains("nickname = NULL"), "got: {sql}");
1003 assert!(params.is_empty(), "expected no params, got: {params:?}");
1005 }
1006
1007 #[test]
1008 fn update_many_with_update_input_appends() {
1009 let input = MockUpdateInput(vec![(
1010 "name".into(),
1011 WriteOp::Set(FilterValue::String("Bob".into())),
1012 )]);
1013
1014 let op = UpdateManyOperation::<MockEngine, TestModel>::new(MockEngine::new())
1015 .r#where(Filter::Equals("active".into(), FilterValue::Bool(true)))
1016 .with_update_input(input);
1017
1018 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
1019
1020 assert!(sql.contains("UPDATE test_models SET"));
1021 assert!(sql.contains("name = $1"), "got: {sql}");
1022 assert!(sql.contains("WHERE"));
1023 assert_eq!(params.len(), 2);
1024 }
1025
1026 use std::sync::{Arc, Mutex};
1029
1030 type StatementLog = Arc<Mutex<Vec<(String, Vec<FilterValue>)>>>;
1031
1032 #[derive(Clone)]
1037 struct RecordingEngine {
1038 recorded: StatementLog,
1039 affected: Arc<Mutex<Vec<u64>>>,
1040 }
1041
1042 impl RecordingEngine {
1043 fn new() -> Self {
1044 Self {
1045 recorded: Arc::new(Mutex::new(Vec::new())),
1046 affected: Arc::new(Mutex::new(Vec::new())),
1047 }
1048 }
1049
1050 fn statements(&self) -> Vec<(String, Vec<FilterValue>)> {
1051 self.recorded.lock().unwrap().clone()
1052 }
1053 }
1054
1055 impl crate::capabilities::SupportsNestedWrites for RecordingEngine {}
1056
1057 impl QueryEngine for RecordingEngine {
1058 fn dialect(&self) -> &dyn crate::dialect::SqlDialect {
1059 &crate::dialect::Postgres
1060 }
1061
1062 fn query_many<T: Model + crate::row::FromRow + Send + 'static>(
1063 &self,
1064 _sql: &str,
1065 _params: Vec<FilterValue>,
1066 ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
1067 Box::pin(async { Ok(Vec::new()) })
1068 }
1069
1070 fn query_one<T: Model + crate::row::FromRow + Send + 'static>(
1071 &self,
1072 _sql: &str,
1073 _params: Vec<FilterValue>,
1074 ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
1075 Box::pin(async { Err(QueryError::not_found("test")) })
1076 }
1077
1078 fn query_optional<T: Model + crate::row::FromRow + Send + 'static>(
1079 &self,
1080 _sql: &str,
1081 _params: Vec<FilterValue>,
1082 ) -> crate::traits::BoxFuture<'_, QueryResult<Option<T>>> {
1083 Box::pin(async { Ok(None) })
1084 }
1085
1086 fn execute_insert<T: Model + crate::row::FromRow + Send + 'static>(
1087 &self,
1088 _sql: &str,
1089 _params: Vec<FilterValue>,
1090 ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
1091 Box::pin(async { Err(QueryError::not_found("test")) })
1092 }
1093
1094 fn execute_update<T: Model + crate::row::FromRow + Send + 'static>(
1095 &self,
1096 sql: &str,
1097 params: Vec<FilterValue>,
1098 ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
1099 let recorded = self.recorded.clone();
1100 let sql = sql.to_string();
1101 Box::pin(async move {
1102 recorded.lock().unwrap().push((sql, params));
1103 Ok(Vec::new())
1104 })
1105 }
1106
1107 fn execute_delete(
1108 &self,
1109 _sql: &str,
1110 _params: Vec<FilterValue>,
1111 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
1112 Box::pin(async { Ok(0) })
1113 }
1114
1115 fn execute_raw(
1116 &self,
1117 sql: &str,
1118 params: Vec<FilterValue>,
1119 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
1120 let recorded = self.recorded.clone();
1121 let affected = self.affected.clone();
1122 let sql_string = sql.to_string();
1123 let default = if sql.contains(" IN (") {
1124 (params.len() as u64).saturating_sub(1)
1125 } else {
1126 1
1127 };
1128 Box::pin(async move {
1129 recorded.lock().unwrap().push((sql_string, params));
1130 Ok(affected.lock().unwrap().pop().unwrap_or(default))
1131 })
1132 }
1133
1134 fn count(
1135 &self,
1136 _sql: &str,
1137 _params: Vec<FilterValue>,
1138 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
1139 Box::pin(async { Ok(0) })
1140 }
1141 }
1142
1143 #[tokio::test]
1144 async fn update_with_nested_create_runs_parent_then_child_insert() {
1145 let engine = RecordingEngine::new();
1146 let op = UpdateOperation::<RecordingEngine, TestModel>::new(engine.clone())
1147 .r#where(Filter::Equals("id".into(), FilterValue::Int(7)))
1148 .set("name", "Renamed")
1149 .with(NestedWriteOp::Create {
1150 relation: "posts",
1151 target_table: "posts",
1152 foreign_key: "author_id",
1153 payload: vec![vec![("title".into(), FilterValue::String("p1".into()))]],
1154 });
1155
1156 let _ = op.exec().await.expect("update + nested create");
1157
1158 let stmts = engine.statements();
1159 assert_eq!(
1160 stmts.len(),
1161 2,
1162 "parent UPDATE + nested child INSERT; got {stmts:#?}"
1163 );
1164 assert!(
1165 stmts[0].0.contains("UPDATE test_models"),
1166 "got: {}",
1167 stmts[0].0
1168 );
1169 assert!(stmts[1].0.contains("INSERT INTO"), "got: {}", stmts[1].0);
1170 assert!(stmts[1].0.contains("posts"), "got: {}", stmts[1].0);
1171 assert!(stmts[1].0.contains("author_id"), "got: {}", stmts[1].0);
1172 }
1173
1174 #[tokio::test]
1175 async fn update_with_nested_disconnect_emits_set_null_update() {
1176 let engine = RecordingEngine::new();
1177 let op = UpdateOperation::<RecordingEngine, TestModel>::new(engine.clone())
1178 .r#where(Filter::Equals("id".into(), FilterValue::Int(7)))
1179 .set("name", "Renamed")
1180 .with(NestedWriteOp::Disconnect {
1181 relation: "posts",
1182 target_table: "posts",
1183 foreign_key: "author_id",
1184 target_pk: "id",
1185 pk: FilterValue::Int(42),
1186 });
1187
1188 let _ = op.exec().await.expect("update + nested disconnect");
1189
1190 let stmts = engine.statements();
1191 assert_eq!(stmts.len(), 2, "got {stmts:#?}");
1192 assert!(
1193 stmts[0].0.contains("UPDATE test_models"),
1194 "got: {}",
1195 stmts[0].0
1196 );
1197 let (sql, params) = &stmts[1];
1198 assert!(sql.contains("UPDATE"), "got: {sql}");
1199 assert!(sql.contains("posts"), "got: {sql}");
1200 assert!(sql.contains("author_id"), "got: {sql}");
1201 assert!(sql.contains("NULL"), "got: {sql}");
1202 assert_eq!(params, &vec![FilterValue::Int(42)]);
1203 }
1204
1205 #[tokio::test]
1206 async fn update_nested_requires_pk_in_where_filter() {
1207 let engine = RecordingEngine::new();
1208 let op = UpdateOperation::<RecordingEngine, TestModel>::new(engine.clone())
1211 .r#where(Filter::Equals(
1212 "email".into(),
1213 FilterValue::String("a@x.com".into()),
1214 ))
1215 .set("name", "Renamed")
1216 .with(NestedWriteOp::Disconnect {
1217 relation: "posts",
1218 target_table: "posts",
1219 foreign_key: "author_id",
1220 target_pk: "id",
1221 pk: FilterValue::Int(42),
1222 });
1223
1224 let result = op.exec().await;
1225 let err = result.err().expect("non-PK where must error");
1226 let msg = err.to_string();
1227 assert!(
1228 msg.contains("primary-key column") || msg.contains("primary key"),
1229 "expected PK-required diagnostic, got: {msg}"
1230 );
1231 assert!(
1233 engine.statements().is_empty(),
1234 "no SQL should run: {:#?}",
1235 engine.statements()
1236 );
1237 }
1238}