Skip to main content

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}