dynamodb_facade/expressions/updates.rs
1use core::fmt;
2use std::borrow::Cow;
3
4use super::{
5 super::IntoAttributeValue, ApplyExpressionAttributes, ApplyUpdate, AttrNames, AttrValues,
6 AttributeValue, Expression, UpdatableBuilder, fmt_attr_maps, resolve_expression,
7 utils::resolve_attr_path,
8};
9
10// ---------------------------------------------------------------------------
11// Composable Update type
12// ---------------------------------------------------------------------------
13
14#[derive(Debug, Clone)]
15enum UpdateSetRhsInner<'a> {
16 Value {
17 value: AttributeValue,
18 },
19 Attribute {
20 attr: Cow<'a, str>,
21 },
22 IfNotExists {
23 attr: Cow<'a, str>,
24 value: AttributeValue,
25 },
26 Add {
27 lhs: Box<UpdateSetRhs<'a>>,
28 rhs: Box<UpdateSetRhs<'a>>,
29 },
30 Sub {
31 lhs: Box<UpdateSetRhs<'a>>,
32 rhs: Box<UpdateSetRhs<'a>>,
33 },
34}
35
36/// Advanced right-hand-side expression builder for DynamoDB SET actions.
37///
38/// `UpdateSetRhs<'a>` represents the right-hand side of a `SET attr = <rhs>`
39/// expression. It is used with [`Update::set_custom`] when the simple
40/// [`Update::set`] shorthand is insufficient — for example, when you need to
41/// reference another attribute, use `if_not_exists`, or build complex
42/// arithmetic expressions.
43///
44/// `UpdateSetRhs` values can be combined with `+` and `-` to ease building
45/// of arithmetic expressions:
46///
47/// ```
48/// use dynamodb_facade::{Update, UpdateSetRhs};
49///
50/// // SET score = other_score + 10
51/// let rhs = UpdateSetRhs::attr("other_score") + UpdateSetRhs::value(10);
52/// let update = Update::set_custom("score", rhs);
53/// assert_eq!(format!("{update}"), r#"SET score = other_score + N("10")"#);
54///
55/// // SET balance = base_balance - penalty
56/// let rhs = UpdateSetRhs::attr("base_balance") - UpdateSetRhs::attr("penalty");
57/// let update = Update::set_custom("balance", rhs);
58/// assert_eq!(format!("{update}"), "SET balance = base_balance - penalty");
59/// ```
60#[derive(Debug, Clone)]
61#[must_use = "expression does nothing until applied to a request"]
62pub struct UpdateSetRhs<'a>(UpdateSetRhsInner<'a>);
63
64impl<'a> UpdateSetRhs<'a> {
65 /// Creates an RHS that reference a literal value.
66 ///
67 /// This is the simplest form. For most cases, [`Update::set`] is more
68 /// ergonomic and equivalent.
69 ///
70 /// # Examples
71 ///
72 /// ```
73 /// use dynamodb_facade::{Update, UpdateSetRhs};
74 ///
75 /// let update = Update::set_custom("score", UpdateSetRhs::value(100));
76 /// assert_eq!(format!("{update}"), r#"SET score = N("100")"#);
77 ///
78 /// // Same as Update::set
79 /// let update = Update::set("score", 100);
80 /// assert_eq!(format!("{update}"), r#"SET score = N("100")"#);
81 /// ```
82 pub fn value(value: impl IntoAttributeValue) -> Self {
83 Self(UpdateSetRhsInner::Value {
84 value: value.into_attribute_value(),
85 })
86 }
87
88 /// Creates an RHS that references another attribute by name.
89 ///
90 /// Use this to copy one attribute's value to another, or as part of an
91 /// arithmetic expression.
92 ///
93 /// # Examples
94 ///
95 /// ```
96 /// use dynamodb_facade::{Update, UpdateSetRhs};
97 ///
98 /// // SET old_score = current_score
99 /// let update = Update::set_custom("old_score", UpdateSetRhs::attr("current_score"));
100 /// assert_eq!(format!("{update}"), "SET old_score = current_score");
101 /// ```
102 pub fn attr(attr: impl Into<Cow<'a, str>>) -> Self {
103 Self(UpdateSetRhsInner::Attribute { attr: attr.into() })
104 }
105
106 /// Creates an RHS using `if_not_exists(attr, default)`.
107 ///
108 /// Evaluates to the current value of `attr` if it exists, or `default`
109 /// otherwise. Useful for initializing an attribute on first write without
110 /// overwriting an existing value.
111 ///
112 /// # Examples
113 ///
114 /// ```
115 /// use dynamodb_facade::{Update, UpdateSetRhs};
116 ///
117 /// // SET old_score = if_not_exists(score, 0)
118 /// let update = Update::set_custom(
119 /// "old_score",
120 /// UpdateSetRhs::if_not_exists("score", 0),
121 /// );
122 /// assert_eq!(
123 /// format!("{update}"),
124 /// r#"SET old_score = if_not_exists(score, N("0"))"#
125 /// );
126 /// ```
127 pub fn if_not_exists(attr: impl Into<Cow<'a, str>>, value: impl IntoAttributeValue) -> Self {
128 Self(UpdateSetRhsInner::IfNotExists {
129 attr: attr.into(),
130 value: value.into_attribute_value(),
131 })
132 }
133
134 fn rhs_expr(self, counter: &mut usize) -> (Expression, AttrNames, AttrValues) {
135 match self.0 {
136 UpdateSetRhsInner::IfNotExists { attr, value } => {
137 let (attr_expr, names) = resolve_attr_path(&attr, "u", counter);
138 let val_id = *counter;
139 *counter += 1;
140 let value_ph = format!(":u{val_id}");
141 (
142 format!("if_not_exists({attr_expr}, {value_ph})"),
143 names,
144 vec![(value_ph, value)],
145 )
146 }
147 UpdateSetRhsInner::Value { value } => {
148 let val_id = *counter;
149 *counter += 1;
150 let value_ph = format!(":u{val_id}");
151 (value_ph.clone(), vec![], vec![(value_ph, value)])
152 }
153 UpdateSetRhsInner::Attribute { attr } => {
154 let (attr_expr, names) = resolve_attr_path(&attr, "u", counter);
155 (attr_expr.into_owned(), names, vec![])
156 }
157 UpdateSetRhsInner::Add { lhs, rhs } => {
158 let mut lhs = lhs.rhs_expr(counter);
159 let rhs = rhs.rhs_expr(counter);
160 lhs.1.extend(rhs.1);
161 lhs.2.extend(rhs.2);
162 (format!("{} + {}", lhs.0, rhs.0), lhs.1, lhs.2)
163 }
164 UpdateSetRhsInner::Sub { lhs, rhs } => {
165 let mut lhs = lhs.rhs_expr(counter);
166 let rhs = rhs.rhs_expr(counter);
167 lhs.1.extend(rhs.1);
168 lhs.2.extend(rhs.2);
169 (format!("{} - {}", lhs.0, rhs.0), lhs.1, lhs.2)
170 }
171 }
172 }
173}
174
175/// Combines two RHS expressions with addition: `expr1 + expr2`.
176///
177/// Produces a `expr1 + expr2` arithmetic expression fragment for use in a SET
178/// action.
179///
180/// # Examples
181///
182/// ```
183/// use dynamodb_facade::{Update, UpdateSetRhs};
184///
185/// // SET score = base_score + 10
186/// let update = Update::set_custom(
187/// "score",
188/// UpdateSetRhs::attr("base_score") + UpdateSetRhs::value(10),
189/// );
190/// assert_eq!(format!("{update}"), r#"SET score = base_score + N("10")"#);
191///
192/// // SET balance = base_balance + last_payment
193/// let rhs = UpdateSetRhs::attr("base_balance") + UpdateSetRhs::attr("last_payment");
194/// let update = Update::set_custom("balance", rhs);
195/// assert_eq!(format!("{update}"), "SET balance = base_balance + last_payment");
196/// ```
197impl<'a> core::ops::Add<UpdateSetRhs<'a>> for UpdateSetRhs<'a> {
198 type Output = UpdateSetRhs<'a>;
199
200 fn add(self, rhs: UpdateSetRhs<'a>) -> Self::Output {
201 Self(UpdateSetRhsInner::Add {
202 lhs: Box::new(self),
203 rhs: Box::new(rhs),
204 })
205 }
206}
207
208/// Combines two RHS expressions with subtraction: `expr1 - expr2`.
209///
210/// Produces a `expr1 - expr2` arithmetic expression fragment for use in a SET
211/// action.
212///
213/// # Examples
214///
215/// ```
216/// use dynamodb_facade::{Update, UpdateSetRhs};
217///
218/// // SET balance = base_balance - 5
219/// let update = Update::set_custom(
220/// "balance",
221/// UpdateSetRhs::attr("base_balance") - UpdateSetRhs::value(5),
222/// );
223/// assert_eq!(format!("{update}"), r#"SET balance = base_balance - N("5")"#);
224///
225/// // SET balance = base_balance - penalty
226/// let rhs = UpdateSetRhs::attr("base_balance") - UpdateSetRhs::attr("penalty");
227/// let update = Update::set_custom("balance", rhs);
228/// assert_eq!(format!("{update}"), "SET balance = base_balance - penalty");
229/// ```
230impl<'a> core::ops::Sub<UpdateSetRhs<'a>> for UpdateSetRhs<'a> {
231 type Output = UpdateSetRhs<'a>;
232
233 fn sub(self, rhs: UpdateSetRhs<'a>) -> Self::Output {
234 Self(UpdateSetRhsInner::Sub {
235 lhs: Box::new(self),
236 rhs: Box::new(rhs),
237 })
238 }
239}
240
241#[derive(Debug, Clone)]
242enum UpdateInner<'a> {
243 Combine(Vec<Update<'a>>),
244
245 // -- SET actions ----------------------------------------------------------
246 Set {
247 attr: Cow<'a, str>,
248 value: AttributeValue,
249 },
250
251 SetIfNotExists {
252 attr: Cow<'a, str>,
253 value: AttributeValue,
254 },
255
256 Increment {
257 attr: Cow<'a, str>,
258 by: AttributeValue,
259 },
260
261 Decrement {
262 attr: Cow<'a, str>,
263 by: AttributeValue,
264 },
265
266 InitIncrement {
267 attr: Cow<'a, str>,
268 initial: AttributeValue,
269 by: AttributeValue,
270 },
271
272 InitDecrement {
273 attr: Cow<'a, str>,
274 initial: AttributeValue,
275 by: AttributeValue,
276 },
277
278 ListAppend {
279 attr: Cow<'a, str>,
280 value: AttributeValue,
281 },
282
283 ListPrepend {
284 attr: Cow<'a, str>,
285 value: AttributeValue,
286 },
287
288 SetCustom {
289 attr: Cow<'a, str>,
290 rhs: UpdateSetRhs<'a>,
291 },
292
293 // -- REMOVE actions -------------------------------------------------------
294 Remove {
295 attr: Cow<'a, str>,
296 },
297
298 ListRemove {
299 attr: Cow<'a, str>,
300 index: usize,
301 },
302
303 // -- ADD actions ----------------------------------------------------------
304 Add {
305 attr: Cow<'a, str>,
306 value: AttributeValue,
307 },
308
309 // -- DELETE actions -------------------------------------------------------
310 Delete {
311 attr: Cow<'a, str>,
312 set: AttributeValue,
313 },
314}
315
316/// Composable DynamoDB update expression builder.
317///
318/// `Update<'a>` represents a single DynamoDB update expression that can be
319/// used as an `UpdateExpression`. It supports all four DynamoDB update clauses
320/// — `SET`, `REMOVE`, `ADD`, and `DELETE` — and multiple actions can be
321/// combined into a single expression with [`and`](Update::and),
322/// [`combine`](Update::combine), or [`try_combine`](Update::try_combine).
323///
324/// Attribute names that are DynamoDB reserved words are automatically escaped
325/// with `#` expression attribute name placeholders.
326///
327/// # Display
328///
329/// `Update` implements [`Display`](core::fmt::Display) in two modes:
330///
331/// - **Default (`{}`)** — resolves all placeholders inline for human-readable
332/// debugging: `SET balance = N("100")`
333/// - **Alternate (`{:#}`)** — shows the raw expression with `#name` / `:value`
334/// placeholders and separate maps: `SET balance = :u0\n values: { :u0 = N("100") }`
335///
336/// # Examples
337///
338/// Simple SET:
339///
340/// ```
341/// use dynamodb_facade::Update;
342///
343/// let update = Update::set("role", "instructor");
344/// assert_eq!(format!("{update}"), r#"SET role = S("instructor")"#);
345/// ```
346///
347/// Combining multiple actions:
348///
349/// ```
350/// use dynamodb_facade::Update;
351///
352/// let update = Update::set("name", "Alice")
353/// .and(Update::remove("legacy_field"))
354/// .and(Update::increment("login_count", 1));
355/// let rendered = format!("{update}");
356/// assert_eq!(
357/// format!("{update}"),
358/// r#"SET name = S("Alice"), login_count = login_count + N("1") REMOVE legacy_field"#,
359/// );
360/// ```
361#[derive(Debug, Clone)]
362#[must_use = "expression does nothing until applied to a request"]
363pub struct Update<'a>(UpdateInner<'a>);
364
365// -- Constructor methods ------------------------------------------------------
366
367impl<'a> Update<'a> {
368 // SET
369
370 /// Creates a SET action with a custom right-hand-side expression.
371 ///
372 /// Use this when [`set`](Update::set) is insufficient — for example, to
373 /// reference another attribute, use `if_not_exists`, or build complex
374 /// arithmetic expressions. See [`UpdateSetRhs`] for the available RHS forms.
375 ///
376 /// # Examples
377 ///
378 /// ```
379 /// use dynamodb_facade::{Update, UpdateSetRhs};
380 ///
381 /// // SET old_score = if_not_exists(score, 0)
382 /// let update = Update::set_custom(
383 /// "old_score",
384 /// UpdateSetRhs::if_not_exists("score", 0),
385 /// );
386 /// assert_eq!(
387 /// format!("{update}"),
388 /// r#"SET old_score = if_not_exists(score, N("0"))"#
389 /// );
390 /// ```
391 pub fn set_custom(attr: impl Into<Cow<'a, str>>, rhs: UpdateSetRhs<'a>) -> Self {
392 Self(UpdateInner::SetCustom {
393 attr: attr.into(),
394 rhs,
395 })
396 }
397
398 /// Creates a SET action that assigns a literal value: `SET attr = value`.
399 ///
400 /// # Examples
401 ///
402 /// ```
403 /// use dynamodb_facade::Update;
404 ///
405 /// let update = Update::set("role", "instructor");
406 /// assert_eq!(format!("{update}"), r#"SET role = S("instructor")"#);
407 /// ```
408 pub fn set(attr: impl Into<Cow<'a, str>>, value: impl IntoAttributeValue) -> Self {
409 Self(UpdateInner::Set {
410 attr: attr.into(),
411 value: value.into_attribute_value(),
412 })
413 }
414
415 /// Creates a SET action that only writes if the attribute does not already exist.
416 ///
417 /// Generates `SET attr = if_not_exists(attr, value)`. This is an atomic
418 /// "initialize if absent" operation — it will not overwrite an existing value.
419 ///
420 /// # Examples
421 ///
422 /// ```
423 /// use dynamodb_facade::Update;
424 ///
425 /// let update = Update::set_if_not_exists("created_at", "2024-01-01");
426 /// assert_eq!(
427 /// format!("{update}"),
428 /// r#"SET created_at = if_not_exists(created_at, S("2024-01-01"))"#
429 /// );
430 /// ```
431 pub fn set_if_not_exists(
432 attr: impl Into<Cow<'a, str>>,
433 value: impl IntoAttributeValue,
434 ) -> Self {
435 Self(UpdateInner::SetIfNotExists {
436 attr: attr.into(),
437 value: value.into_attribute_value(),
438 })
439 }
440
441 // Arithmetic
442
443 /// Creates a SET action that increments a numeric attribute: `SET attr = attr + by`.
444 ///
445 /// The attribute must already exist and be a number. To safely initialize
446 /// and increment in one operation, use [`init_increment`](Update::init_increment).
447 ///
448 /// # Examples
449 ///
450 /// ```
451 /// use dynamodb_facade::Update;
452 ///
453 /// let update = Update::increment("login_count", 1);
454 /// assert_eq!(
455 /// format!("{update}"),
456 /// r#"SET login_count = login_count + N("1")"#
457 /// );
458 /// ```
459 pub fn increment(attr: impl Into<Cow<'a, str>>, by: impl IntoAttributeValue) -> Self {
460 Self(UpdateInner::Increment {
461 attr: attr.into(),
462 by: by.into_attribute_value(),
463 })
464 }
465
466 /// Creates a SET action that decrements a numeric attribute: `SET attr = attr - by`.
467 ///
468 /// The attribute must already exist and be a number. To safely initialize
469 /// and decrement in one operation, use [`init_decrement`](Update::init_decrement).
470 ///
471 /// # Examples
472 ///
473 /// ```
474 /// use dynamodb_facade::Update;
475 ///
476 /// let update = Update::decrement("credits", 10);
477 /// assert_eq!(
478 /// format!("{update}"),
479 /// r#"SET credits = credits - N("10")"#
480 /// );
481 /// ```
482 pub fn decrement(attr: impl Into<Cow<'a, str>>, by: impl IntoAttributeValue) -> Self {
483 Self(UpdateInner::Decrement {
484 attr: attr.into(),
485 by: by.into_attribute_value(),
486 })
487 }
488
489 /// Creates a SET action that initializes and increments atomically.
490 ///
491 /// Generates `SET attr = if_not_exists(attr, initial) + by`. If the
492 /// attribute does not exist, it is initialized to `initial` before the
493 /// increment is applied. This is safe to call even on a new item.
494 ///
495 /// # Examples
496 ///
497 /// ```
498 /// use dynamodb_facade::Update;
499 ///
500 /// // SET enrollment_count = if_not_exists(enrollment_count, 0) + 1
501 /// let update = Update::init_increment("enrollment_count", 0, 1);
502 /// assert_eq!(
503 /// format!("{update}"),
504 /// r#"SET enrollment_count = if_not_exists(enrollment_count, N("0")) + N("1")"#
505 /// );
506 /// ```
507 pub fn init_increment(
508 attr: impl Into<Cow<'a, str>>,
509 initial: impl IntoAttributeValue,
510 by: impl IntoAttributeValue,
511 ) -> Self {
512 Self(UpdateInner::InitIncrement {
513 attr: attr.into(),
514 initial: initial.into_attribute_value(),
515 by: by.into_attribute_value(),
516 })
517 }
518
519 /// Creates a SET action that initializes and decrements atomically.
520 ///
521 /// Generates `SET attr = if_not_exists(attr, initial) - by`. If the
522 /// attribute does not exist, it is initialized to `initial` before the
523 /// decrement is applied. This is safe to call even on a new item.
524 ///
525 /// # Examples
526 ///
527 /// ```
528 /// use dynamodb_facade::Update;
529 ///
530 /// // SET balance = if_not_exists(balance, 1000) - 50
531 /// let update = Update::init_decrement("balance", 1000, 50);
532 /// assert_eq!(
533 /// format!("{update}"),
534 /// r#"SET balance = if_not_exists(balance, N("1000")) - N("50")"#
535 /// );
536 /// ```
537 pub fn init_decrement(
538 attr: impl Into<Cow<'a, str>>,
539 initial: impl IntoAttributeValue,
540 by: impl IntoAttributeValue,
541 ) -> Self {
542 Self(UpdateInner::InitDecrement {
543 attr: attr.into(),
544 initial: initial.into_attribute_value(),
545 by: by.into_attribute_value(),
546 })
547 }
548
549 // Lists
550
551 /// Creates a SET action that appends elements to a list attribute.
552 ///
553 /// Generates `SET attr = list_append(attr, value)`. The `value` must be a
554 /// DynamoDB List (`L`) attribute value.
555 ///
556 /// # Examples
557 ///
558 /// ```
559 /// use dynamodb_facade::Update;
560 ///
561 /// let update = Update::list_append("tags", vec!["rust"]);
562 /// assert_eq!(
563 /// format!("{update}"),
564 /// r#"SET tags = list_append(tags, L([S("rust")]))"#
565 /// );
566 /// ```
567 pub fn list_append(attr: impl Into<Cow<'a, str>>, value: impl IntoAttributeValue) -> Self {
568 Self(UpdateInner::ListAppend {
569 attr: attr.into(),
570 value: value.into_attribute_value(),
571 })
572 }
573
574 /// Creates a SET action that prepends elements to a list attribute.
575 ///
576 /// Generates `SET attr = list_append(value, attr)`. The `value` must be a
577 /// DynamoDB List (`L`) attribute value.
578 ///
579 /// # Examples
580 ///
581 /// ```
582 /// use dynamodb_facade::Update;
583 ///
584 /// let update = Update::list_prepend("notifications", vec!["new_event"]);
585 /// assert_eq!(
586 /// format!("{update}"),
587 /// r#"SET notifications = list_append(L([S("new_event")]), notifications)"#
588 /// );
589 /// ```
590 pub fn list_prepend(attr: impl Into<Cow<'a, str>>, value: impl IntoAttributeValue) -> Self {
591 Self(UpdateInner::ListPrepend {
592 attr: attr.into(),
593 value: value.into_attribute_value(),
594 })
595 }
596
597 // REMOVE
598
599 /// Creates a REMOVE action that deletes an attribute from the item.
600 ///
601 /// Generates `REMOVE attr`. The attribute path may include a list index
602 /// (e.g. `"tags[2]"`) to remove a specific list element.
603 ///
604 /// # Examples
605 ///
606 /// ```
607 /// use dynamodb_facade::Update;
608 ///
609 /// let update = Update::remove("legacy_field");
610 /// assert_eq!(format!("{update}"), "REMOVE legacy_field");
611 ///
612 /// let update = Update::remove("tags[2]");
613 /// assert_eq!(format!("{update}"), "REMOVE tags[2]");
614 /// ```
615 pub fn remove(attr: impl Into<Cow<'a, str>>) -> Self {
616 Self(UpdateInner::Remove { attr: attr.into() })
617 }
618
619 /// Creates a REMOVE action that deletes a specific element from a list attribute.
620 ///
621 /// Generates `REMOVE attr[index]`. The `index` is zero-based.
622 ///
623 /// # Examples
624 ///
625 /// ```
626 /// use dynamodb_facade::Update;
627 ///
628 /// let update = Update::list_remove("tags", 2);
629 /// assert_eq!(format!("{update}"), "REMOVE tags[2]");
630 /// ```
631 pub fn list_remove(attr: impl Into<Cow<'a, str>>, index: usize) -> Self {
632 Self(UpdateInner::ListRemove {
633 attr: attr.into(),
634 index,
635 })
636 }
637
638 // ADD
639
640 /// Creates an ADD action for numeric attributes or set attributes.
641 ///
642 /// For numeric attributes, generates `ADD attr value` which atomically
643 /// adds `value` to the current attribute value (initializing to zero if
644 /// absent). For DynamoDB Set types (`SS`, `NS`, `BS`), adds elements to
645 /// the set.
646 ///
647 /// # Examples
648 ///
649 /// ```
650 /// use dynamodb_facade::Update;
651 ///
652 /// let update = Update::add("visitor_count", 5);
653 /// assert_eq!(format!("{update}"), r#"ADD visitor_count N("5")"#);
654 /// ```
655 pub fn add(attr: impl Into<Cow<'a, str>>, value: impl IntoAttributeValue) -> Self {
656 Self(UpdateInner::Add {
657 attr: attr.into(),
658 value: value.into_attribute_value(),
659 })
660 }
661
662 // DELETE
663
664 /// Creates a DELETE action that removes elements from a DynamoDB Set attribute.
665 ///
666 /// Generates `DELETE attr set`. The `value` must be a DynamoDB Set type
667 /// (`SS`, `NS`, or `BS`). Elements present in `value` are removed from the
668 /// set attribute.
669 ///
670 /// # Examples
671 ///
672 /// ```
673 /// use dynamodb_facade::{AsSet, Update};
674 ///
675 /// // Remove "rust" from the tag_set (SS attribute).
676 /// let update = Update::delete("tag_set", AsSet(vec!["rust"]));
677 /// assert_eq!(format!("{update}"), r#"DELETE tag_set Ss(["rust"])"#);
678 /// ```
679 pub fn delete(attr: impl Into<Cow<'a, str>>, value: impl IntoAttributeValue) -> Self {
680 Self(UpdateInner::Delete {
681 attr: attr.into(),
682 set: value.into_attribute_value(),
683 })
684 }
685
686 // Combinators
687
688 /// Chains another update action onto this one.
689 ///
690 /// The resulting expression merges all SET, REMOVE, ADD, and DELETE actions
691 /// from both updates into a single `UpdateExpression`. Existing `Combine`
692 /// wrappers are flattened.
693 ///
694 /// # Examples
695 ///
696 /// ```
697 /// use dynamodb_facade::Update;
698 ///
699 /// let update = Update::set("name", "Alice")
700 /// .and(Update::set("role", "instructor"))
701 /// .and(Update::remove("legacy_field"));
702 ///
703 /// assert_eq!(
704 /// format!("{update}"),
705 /// r#"SET name = S("Alice"), role = S("instructor") REMOVE legacy_field"#,
706 /// );
707 /// ```
708 pub fn and(self, other: Update<'a>) -> Self {
709 Self(UpdateInner::Combine(match self.0 {
710 UpdateInner::Combine(mut updates) => {
711 updates.push(other);
712 updates
713 }
714 _ => vec![self, other],
715 }))
716 }
717
718 /// Combines an iterator of updates into a single update expression.
719 ///
720 /// All SET, REMOVE, ADD, and DELETE actions from the iterator are merged
721 /// into one `UpdateExpression`.
722 ///
723 /// # Panics
724 ///
725 /// Panics if the iterator is empty. Use [`try_combine`](Update::try_combine)
726 /// for a non-panicking alternative.
727 ///
728 /// # Examples
729 ///
730 /// ```
731 /// use dynamodb_facade::Update;
732 ///
733 /// let update = Update::combine([
734 /// Update::set("name", "Alice"),
735 /// Update::set("role", "instructor"),
736 /// Update::remove("legacy_field"),
737 /// ]);
738 ///
739 /// assert_eq!(
740 /// format!("{update}"),
741 /// r#"SET name = S("Alice"), role = S("instructor") REMOVE legacy_field"#,
742 /// );
743 /// ```
744 ///
745 /// Combining optional updates from an iterator:
746 ///
747 /// ```
748 /// use dynamodb_facade::Update;
749 ///
750 /// let new_name: Option<&str> = Some("Alice");
751 /// let new_email: Option<&str> = None;
752 ///
753 /// let update = Update::combine(
754 /// [
755 /// new_name.map(|n| Update::set("name", n)),
756 /// new_email.map(|e| Update::set("email", e)),
757 /// ]
758 /// .into_iter()
759 /// .flatten(),
760 /// );
761 /// assert_eq!(format!("{update}"), r#"SET name = S("Alice")"#);
762 /// ```
763 pub fn combine(updates: impl IntoIterator<Item = Update<'a>>) -> Self {
764 let updates: Vec<_> = updates.into_iter().collect();
765 assert!(
766 !updates.is_empty(),
767 "Update::combine requires at least one update"
768 );
769 Self(UpdateInner::Combine(updates))
770 }
771
772 /// Combines an iterator of updates into a single update expression, returning `None` if empty.
773 ///
774 /// This is the non-panicking version of [`combine`](Update::combine). Returns
775 /// `None` when the iterator yields no items, which is useful when all updates
776 /// are conditional and none may apply.
777 ///
778 /// # Examples
779 ///
780 /// ```
781 /// use dynamodb_facade::Update;
782 ///
783 /// let new_name: Option<&str> = None;
784 /// let new_address: Option<&str> = None;
785 ///
786 /// // No updates — returns None.
787 /// let update = Update::try_combine(
788 /// [
789 /// new_name.map(|n| Update::set("name", n)),
790 /// new_address.map(|a| Update::set("address", a))
791 /// ]
792 /// .into_iter()
793 /// .flatten(),
794 /// );
795 /// assert!(update.is_none());
796 ///
797 /// // At least one update — returns Some.
798 /// let update = Update::try_combine([Update::set("role", "admin")]);
799 /// assert!(update.is_some());
800 /// ```
801 pub fn try_combine(updates: impl IntoIterator<Item = Update<'a>>) -> Option<Self> {
802 let updates: Vec<_> = updates.into_iter().collect();
803 if updates.is_empty() {
804 None
805 } else {
806 Some(Self(UpdateInner::Combine(updates)))
807 }
808 }
809}
810
811// -- Internal build machinery -------------------------------------------------
812
813/// Compiled output of an [`Update`], with actions grouped by clause (SET, REMOVE, ADD, DELETE).
814#[derive(Debug, Default)]
815struct BuiltUpdate {
816 set_actions: Vec<String>,
817 remove_actions: Vec<String>,
818 add_actions: Vec<String>,
819 delete_actions: Vec<String>,
820 names: AttrNames,
821 values: AttrValues,
822}
823
824impl BuiltUpdate {
825 /// Merges another `BuiltUpdate` into `self`, extending all action lists and placeholder maps.
826 fn merge(&mut self, other: BuiltUpdate) {
827 self.set_actions.extend(other.set_actions);
828 self.remove_actions.extend(other.remove_actions);
829 self.add_actions.extend(other.add_actions);
830 self.delete_actions.extend(other.delete_actions);
831 self.names.extend(other.names);
832 self.values.extend(other.values);
833 }
834
835 /// Assembles the final update expression string from the grouped action lists.
836 fn into_expression(self) -> (String, AttrNames, AttrValues) {
837 use core::fmt::Write;
838 let mut expression = String::new();
839
840 if !self.set_actions.is_empty() {
841 let _ = write!(expression, "SET {}", self.set_actions.join(", "));
842 }
843 if !self.remove_actions.is_empty() {
844 if !expression.is_empty() {
845 let _ = write!(expression, " ");
846 }
847 let _ = write!(expression, "REMOVE {}", self.remove_actions.join(", "));
848 }
849 if !self.add_actions.is_empty() {
850 if !expression.is_empty() {
851 let _ = write!(expression, " ");
852 }
853 let _ = write!(expression, "ADD {}", self.add_actions.join(", "));
854 }
855 if !self.delete_actions.is_empty() {
856 if !expression.is_empty() {
857 let _ = write!(expression, " ");
858 }
859 let _ = write!(expression, "DELETE {}", self.delete_actions.join(", "));
860 }
861
862 (expression, self.names, self.values)
863 }
864}
865
866impl Update<'_> {
867 /// Builds the update into grouped action lists using a shared placeholder counter.
868 fn build(self, counter: &mut usize) -> BuiltUpdate {
869 match self.0 {
870 UpdateInner::Combine(updates) => {
871 let mut result = BuiltUpdate::default();
872 for update in updates {
873 result.merge(update.build(counter));
874 }
875 result
876 }
877
878 UpdateInner::SetCustom { attr, rhs } => {
879 let (attr_expr, mut names) = resolve_attr_path(&attr, "u", counter);
880 let (rhs_expr, rhs_names, rhs_values) = rhs.rhs_expr(counter);
881 names.extend(rhs_names);
882 BuiltUpdate {
883 set_actions: vec![format!("{attr_expr} = {rhs_expr}")],
884 names,
885 values: rhs_values,
886 ..Default::default()
887 }
888 }
889
890 UpdateInner::Set { attr, value } => {
891 let (attr_expr, names) = resolve_attr_path(&attr, "u", counter);
892 let val_id = *counter;
893 *counter += 1;
894 let value_ph = format!(":u{val_id}");
895 BuiltUpdate {
896 set_actions: vec![format!("{attr_expr} = {value_ph}")],
897 names,
898 values: vec![(value_ph, value)],
899 ..Default::default()
900 }
901 }
902
903 UpdateInner::SetIfNotExists { attr, value } => {
904 let (attr_expr, names) = resolve_attr_path(&attr, "u", counter);
905 let val_id = *counter;
906 *counter += 1;
907 let value_ph = format!(":u{val_id}");
908 BuiltUpdate {
909 set_actions: vec![format!(
910 "{attr_expr} = if_not_exists({attr_expr}, {value_ph})"
911 )],
912 names,
913 values: vec![(value_ph, value)],
914 ..Default::default()
915 }
916 }
917
918 UpdateInner::Increment { attr, by } => {
919 let (attr_expr, names) = resolve_attr_path(&attr, "u", counter);
920 let val_id = *counter;
921 *counter += 1;
922 let by_ph = format!(":u{val_id}");
923 BuiltUpdate {
924 set_actions: vec![format!("{attr_expr} = {attr_expr} + {by_ph}")],
925 names,
926 values: vec![(by_ph, by)],
927 ..Default::default()
928 }
929 }
930
931 UpdateInner::Decrement { attr, by } => {
932 let (attr_expr, names) = resolve_attr_path(&attr, "u", counter);
933 let val_id = *counter;
934 *counter += 1;
935 let by_ph = format!(":u{val_id}");
936 BuiltUpdate {
937 set_actions: vec![format!("{attr_expr} = {attr_expr} - {by_ph}")],
938 names,
939 values: vec![(by_ph, by)],
940 ..Default::default()
941 }
942 }
943
944 UpdateInner::InitIncrement { attr, initial, by } => {
945 let (attr_expr, names) = resolve_attr_path(&attr, "u", counter);
946 let val_id = *counter;
947 *counter += 1;
948 let by_ph = format!(":u{val_id}");
949 let init_ph = format!(":u{val_id}init");
950 BuiltUpdate {
951 set_actions: vec![format!(
952 "{attr_expr} = if_not_exists({attr_expr}, {init_ph}) + {by_ph}"
953 )],
954 names,
955 values: vec![(init_ph, initial), (by_ph, by)],
956 ..Default::default()
957 }
958 }
959
960 UpdateInner::InitDecrement { attr, initial, by } => {
961 let (attr_expr, names) = resolve_attr_path(&attr, "u", counter);
962 let val_id = *counter;
963 *counter += 1;
964 let by_ph = format!(":u{val_id}");
965 let init_ph = format!(":u{val_id}init");
966 BuiltUpdate {
967 set_actions: vec![format!(
968 "{attr_expr} = if_not_exists({attr_expr}, {init_ph}) - {by_ph}"
969 )],
970 names,
971 values: vec![(init_ph, initial), (by_ph, by)],
972 ..Default::default()
973 }
974 }
975
976 UpdateInner::ListAppend { attr, value } => {
977 let (attr_expr, names) = resolve_attr_path(&attr, "u", counter);
978 let val_id = *counter;
979 *counter += 1;
980 let val_ph = format!(":u{val_id}");
981 BuiltUpdate {
982 set_actions: vec![format!("{attr_expr} = list_append({attr_expr}, {val_ph})")],
983 names,
984 values: vec![(val_ph, value)],
985 ..Default::default()
986 }
987 }
988
989 UpdateInner::ListPrepend { attr, value } => {
990 let (attr_expr, names) = resolve_attr_path(&attr, "u", counter);
991 let val_id = *counter;
992 *counter += 1;
993 let val_ph = format!(":u{val_id}");
994 BuiltUpdate {
995 set_actions: vec![format!("{attr_expr} = list_append({val_ph}, {attr_expr})")],
996 names,
997 values: vec![(val_ph, value)],
998 ..Default::default()
999 }
1000 }
1001
1002 UpdateInner::Remove { attr } => {
1003 let (attr_expr, names) = resolve_attr_path(&attr, "u", counter);
1004 BuiltUpdate {
1005 remove_actions: vec![attr_expr.into_owned()],
1006 names,
1007 ..Default::default()
1008 }
1009 }
1010
1011 UpdateInner::ListRemove { attr, index } => {
1012 let (attr_expr, names) = resolve_attr_path(&attr, "u", counter);
1013 BuiltUpdate {
1014 remove_actions: vec![format!("{attr_expr}[{index}]")],
1015 names,
1016 ..Default::default()
1017 }
1018 }
1019
1020 UpdateInner::Add { attr, value } => {
1021 let (attr_expr, names) = resolve_attr_path(&attr, "u", counter);
1022 let val_id = *counter;
1023 *counter += 1;
1024 let val_ph = format!(":u{val_id}");
1025 BuiltUpdate {
1026 add_actions: vec![format!("{attr_expr} {val_ph}")],
1027 names,
1028 values: vec![(val_ph, value)],
1029 ..Default::default()
1030 }
1031 }
1032
1033 UpdateInner::Delete { attr, set } => {
1034 let (attr_expr, names) = resolve_attr_path(&attr, "u", counter);
1035 let val_id = *counter;
1036 *counter += 1;
1037 let set_ph = format!(":u{val_id}");
1038 BuiltUpdate {
1039 delete_actions: vec![format!("{attr_expr} {set_ph}")],
1040 names,
1041 values: vec![(set_ph, set)],
1042 ..Default::default()
1043 }
1044 }
1045 }
1046 }
1047}
1048
1049// -- Display ------------------------------------------------------------------
1050
1051impl fmt::Display for Update<'_> {
1052 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1053 let mut counter = 0;
1054 let (expression, names, values) = self.clone().build(&mut counter).into_expression();
1055 if f.alternate() {
1056 f.write_str(&expression)?;
1057 fmt_attr_maps(f, &names, &values)
1058 } else {
1059 f.write_str(&resolve_expression(&expression, &names, &values))
1060 }
1061 }
1062}
1063
1064// -- ApplyUpdate impl ---------------------------------------------------------
1065
1066impl<B: UpdatableBuilder> ApplyUpdate<B> for Update<'_> {
1067 fn apply(self, builder: B) -> B {
1068 let mut counter = 0;
1069 let (expression, names, values) = self.build(&mut counter).into_expression();
1070 builder
1071 .update_expression(expression)
1072 .apply_names_and_values(names, values)
1073 }
1074}
1075
1076impl<B: UpdatableBuilder> ApplyUpdate<B> for Option<Update<'_>> {
1077 fn apply(self, builder: B) -> B {
1078 match self {
1079 Some(u) => u.apply(builder),
1080 None => builder,
1081 }
1082 }
1083}
1084
1085#[cfg(test)]
1086mod tests {
1087 use super::super::super::values::AsSet;
1088 use super::*;
1089
1090 #[test]
1091 fn test_update_display_default_set() {
1092 let u = Update::set("balance", 100u32);
1093 let display = format!("{u}");
1094 assert_eq!(display, r#"SET balance = N("100")"#);
1095 }
1096
1097 #[test]
1098 fn test_update_display_default_set_reserved_word() {
1099 let u = Update::set("Status", "active");
1100 let display = format!("{u}");
1101 assert_eq!(display, r#"SET Status = S("active")"#);
1102 }
1103
1104 #[test]
1105 fn test_update_display_default_increment() {
1106 let u = Update::increment("login_count", 1u32);
1107 let display = format!("{u}");
1108 assert_eq!(display, r#"SET login_count = login_count + N("1")"#);
1109 }
1110
1111 #[test]
1112 fn test_update_display_default_remove() {
1113 let u = Update::remove("legacy_field");
1114 let display = format!("{u}");
1115 assert_eq!(display, "REMOVE legacy_field");
1116 }
1117
1118 #[test]
1119 fn test_update_display_default_combined() {
1120 let u = Update::combine([
1121 Update::set("balance", 100u32),
1122 Update::increment("login_count", 1u32),
1123 Update::remove("legacy_field"),
1124 ]);
1125 let display = format!("{u}");
1126 assert_eq!(
1127 display,
1128 r#"SET balance = N("100"), login_count = login_count + N("1") REMOVE legacy_field"#
1129 );
1130 }
1131
1132 #[test]
1133 fn test_update_display_default_add_action() {
1134 let u = Update::add("visitor_count", 5u32);
1135 let display = format!("{u}");
1136 assert_eq!(display, r#"ADD visitor_count N("5")"#);
1137 }
1138
1139 #[test]
1140 fn test_update_display_default_set_if_not_exists() {
1141 let u = Update::set_if_not_exists("created_at", "2024-01-01");
1142 let display = format!("{u}");
1143 assert_eq!(
1144 display,
1145 r#"SET created_at = if_not_exists(created_at, S("2024-01-01"))"#
1146 );
1147 }
1148
1149 #[test]
1150 fn test_update_display_default_init_increment() {
1151 let u = Update::init_increment("counter", 0u32, 1u32);
1152 let display = format!("{u}");
1153 assert_eq!(
1154 display,
1155 r#"SET counter = if_not_exists(counter, N("0")) + N("1")"#
1156 );
1157 }
1158
1159 #[test]
1160 fn test_update_display_alternate_set() {
1161 let u = Update::set("balance", 100u32);
1162 let display = format!("{u:#}");
1163 assert_eq!(display, "SET balance = :u0\n values: { :u0 = N(\"100\") }");
1164 }
1165
1166 #[test]
1167 fn test_update_display_alternate_reserved_word() {
1168 let u = Update::set("Status", "active");
1169 let display = format!("{u:#}");
1170 assert_eq!(
1171 display,
1172 "SET #u0 = :u1\n names: { #u0 = Status }\n values: { :u1 = S(\"active\") }"
1173 );
1174 }
1175
1176 #[test]
1177 fn test_update_display_alternate_remove() {
1178 let u = Update::remove("legacy_field");
1179 let display = format!("{u:#}");
1180 // No names (not reserved) and no values
1181 assert_eq!(display, "REMOVE legacy_field");
1182 }
1183
1184 #[test]
1185 fn test_update_display_alternate_combined() {
1186 let u = Update::combine([Update::set("balance", 100u32), Update::remove("Name")]);
1187 let display = format!("{u:#}");
1188 assert_eq!(
1189 display,
1190 "SET balance = :u0 REMOVE #u1\n names: { #u1 = Name }\n values: { :u0 = N(\"100\") }"
1191 );
1192 }
1193
1194 // -- Decrement ------------------------------------------------------------
1195
1196 #[test]
1197 fn test_decrement_display_default() {
1198 // Non-reserved attribute: no name placeholder
1199 let u = Update::decrement("credits", 1u32);
1200 assert_eq!(format!("{u}"), r#"SET credits = credits - N("1")"#);
1201 }
1202
1203 #[test]
1204 fn test_decrement_display_alternate() {
1205 // Non-reserved attribute: only a value placeholder
1206 let u = Update::decrement("credits", 1u32);
1207 assert_eq!(
1208 format!("{u:#}"),
1209 "SET credits = credits - :u0\n values: { :u0 = N(\"1\") }"
1210 );
1211 }
1212
1213 #[test]
1214 fn test_decrement_display_default_reserved_word() {
1215 // Reserved word "Count" must be aliased with a name placeholder
1216 let u = Update::decrement("Count", 1u32);
1217 assert_eq!(format!("{u}"), r#"SET Count = Count - N("1")"#);
1218 }
1219
1220 #[test]
1221 fn test_decrement_display_alternate_reserved_word() {
1222 let u = Update::decrement("Count", 1u32);
1223 assert_eq!(
1224 format!("{u:#}"),
1225 "SET #u0 = #u0 - :u1\n names: { #u0 = Count }\n values: { :u1 = N(\"1\") }"
1226 );
1227 }
1228
1229 // -- InitDecrement --------------------------------------------------------
1230
1231 #[test]
1232 fn test_init_decrement_display_default() {
1233 let u = Update::init_decrement("credits", 100u32, 1u32);
1234 assert_eq!(
1235 format!("{u}"),
1236 r#"SET credits = if_not_exists(credits, N("100")) - N("1")"#
1237 );
1238 }
1239
1240 #[test]
1241 fn test_init_decrement_display_alternate() {
1242 let u = Update::init_decrement("credits", 100u32, 1u32);
1243 assert_eq!(
1244 format!("{u:#}"),
1245 "SET credits = if_not_exists(credits, :u0init) - :u0\n values: { :u0init = N(\"100\"), :u0 = N(\"1\") }"
1246 );
1247 }
1248
1249 // -- ListAppend -----------------------------------------------------------
1250
1251 #[test]
1252 fn test_list_append_display_default() {
1253 let u = Update::list_append("tags", vec!["a", "b"]);
1254 assert_eq!(
1255 format!("{u}"),
1256 r#"SET tags = list_append(tags, L([S("a"), S("b")]))"#
1257 );
1258 }
1259
1260 #[test]
1261 fn test_list_append_display_alternate() {
1262 let u = Update::list_append("tags", vec!["a", "b"]);
1263 assert_eq!(
1264 format!("{u:#}"),
1265 "SET tags = list_append(tags, :u0)\n values: { :u0 = L([S(\"a\"), S(\"b\")]) }"
1266 );
1267 }
1268
1269 // -- ListPrepend ----------------------------------------------------------
1270
1271 #[test]
1272 fn test_list_prepend_display_default() {
1273 // list_prepend reverses argument order: list_append(:u0, attr)
1274 let u = Update::list_prepend("tags", vec!["a", "b"]);
1275 assert_eq!(
1276 format!("{u}"),
1277 r#"SET tags = list_append(L([S("a"), S("b")]), tags)"#
1278 );
1279 }
1280
1281 #[test]
1282 fn test_list_prepend_display_alternate() {
1283 let u = Update::list_prepend("tags", vec!["a", "b"]);
1284 assert_eq!(
1285 format!("{u:#}"),
1286 "SET tags = list_append(:u0, tags)\n values: { :u0 = L([S(\"a\"), S(\"b\")]) }"
1287 );
1288 }
1289
1290 // -- ListRemove -----------------------------------------------------------
1291
1292 #[test]
1293 fn test_list_remove_display_default() {
1294 let u = Update::list_remove("tags", 0);
1295 assert_eq!(format!("{u}"), "REMOVE tags[0]");
1296 }
1297
1298 #[test]
1299 fn test_list_remove_display_alternate() {
1300 // No names or values — alternate mode is identical to default
1301 let u = Update::list_remove("tags", 0);
1302 assert_eq!(format!("{u:#}"), "REMOVE tags[0]");
1303 }
1304
1305 // -- Delete ---------------------------------------------------------------
1306
1307 #[test]
1308 fn test_delete_display_default() {
1309 let u = Update::delete("tag_set", AsSet(vec!["old".to_owned()]));
1310 assert_eq!(format!("{u}"), r#"DELETE tag_set Ss(["old"])"#);
1311 }
1312
1313 #[test]
1314 fn test_delete_display_alternate() {
1315 let u = Update::delete("tag_set", AsSet(vec!["old".to_owned()]));
1316 assert_eq!(
1317 format!("{u:#}"),
1318 "DELETE tag_set :u0\n values: { :u0 = Ss([\"old\"]) }"
1319 );
1320 }
1321
1322 // -- SetCustom / UpdateSetRhs variants ------------------------------------
1323
1324 #[test]
1325 fn test_set_custom_value_display_default() {
1326 let u = Update::set_custom("score", UpdateSetRhs::value("Alice"));
1327 assert_eq!(format!("{u}"), r#"SET score = S("Alice")"#);
1328 }
1329
1330 #[test]
1331 fn test_set_custom_value_display_alternate() {
1332 let u = Update::set_custom("score", UpdateSetRhs::value("Alice"));
1333 assert_eq!(
1334 format!("{u:#}"),
1335 "SET score = :u0\n values: { :u0 = S(\"Alice\") }"
1336 );
1337 }
1338
1339 #[test]
1340 fn test_set_custom_attr_display_default() {
1341 // RHS is another attribute reference — no value placeholder, name placeholder for "name"
1342 let u = Update::set_custom("display_name", UpdateSetRhs::attr("name"));
1343 assert_eq!(format!("{u}"), "SET display_name = name");
1344 }
1345
1346 #[test]
1347 fn test_set_custom_attr_display_alternate() {
1348 // "name" is a reserved word → name placeholder in RHS
1349 let u = Update::set_custom("display_name", UpdateSetRhs::attr("name"));
1350 assert_eq!(
1351 format!("{u:#}"),
1352 "SET display_name = #u0\n names: { #u0 = name }"
1353 );
1354 }
1355
1356 #[test]
1357 fn test_set_custom_if_not_exists_display_default() {
1358 let u = Update::set_custom("score", UpdateSetRhs::if_not_exists("score", 0u32));
1359 assert_eq!(
1360 format!("{u}"),
1361 r#"SET score = if_not_exists(score, N("0"))"#
1362 );
1363 }
1364
1365 #[test]
1366 fn test_set_custom_if_not_exists_display_alternate() {
1367 let u = Update::set_custom("score", UpdateSetRhs::if_not_exists("score", 0u32));
1368 assert_eq!(
1369 format!("{u:#}"),
1370 "SET score = if_not_exists(score, :u0)\n values: { :u0 = N(\"0\") }"
1371 );
1372 }
1373
1374 #[test]
1375 fn test_set_custom_composite_rhs_display_default() {
1376 // (attr("a") + value(5u32)) - attr("b") → "total = a + N("5") - b"
1377 let rhs = UpdateSetRhs::attr("a") + UpdateSetRhs::value(5u32) - UpdateSetRhs::attr("b");
1378 let u = Update::set_custom("total", rhs);
1379 assert_eq!(format!("{u}"), r#"SET total = a + N("5") - b"#);
1380 }
1381
1382 // -- combine / try_combine / and ------------------------------------------
1383
1384 #[test]
1385 #[should_panic(expected = "Update::combine requires at least one update")]
1386 fn test_update_combine_empty_panics() {
1387 let _ = Update::combine(std::iter::empty::<Update>());
1388 }
1389
1390 #[test]
1391 fn test_update_try_combine_empty_none() {
1392 let result = Update::try_combine(std::iter::empty::<Update>());
1393 assert!(result.is_none());
1394 }
1395
1396 #[test]
1397 fn test_update_try_combine_non_empty_some() {
1398 let result = Update::try_combine([Update::set("score", 1u32)]);
1399 assert!(result.is_some());
1400 assert_eq!(format!("{}", result.unwrap()), r#"SET score = N("1")"#);
1401 }
1402
1403 #[test]
1404 fn test_update_and_flattens_nested_combine() {
1405 // Chaining .and() should flatten into a single Combine, not nest them
1406 let u = Update::set("score", 1u32)
1407 .and(Update::set("balance", 2u32))
1408 .and(Update::remove("legacy_field"));
1409 assert_eq!(
1410 format!("{u}"),
1411 r#"SET score = N("1"), balance = N("2") REMOVE legacy_field"#
1412 );
1413 }
1414}