Skip to main content

dynamodb_facade/operations/
update.rs

1use std::future::{Future, IntoFuture};
2use std::pin::Pin;
3
4use super::*;
5
6use aws_sdk_dynamodb::operation::update_item::builders::UpdateItemFluentBuilder;
7
8/// Builder for a DynamoDB `UpdateItem` request.
9///
10/// Constructed via [`DynamoDBItemOp::update`] / [`DynamoDBItemOp::update_by_id`]
11/// (typed, with a concrete `T`) or [`UpdateItemRequest::new`] (stand-alone,
12/// raw output). The builder provides:
13///
14/// - **Output format** — the result can be deserialized into `T`.
15///   Call [`.raw()`][UpdateItemRequest::raw] to receive an untyped [`Item<TD>`]
16///   instead (one-way).
17/// - **Return value** — by default nothing is returned. Call
18///   [`.return_old()`][UpdateItemRequest::return_old],
19///   [`.return_new()`][UpdateItemRequest::return_new], or
20///   [`.return_none()`][UpdateItemRequest::return_none] to choose whether
21///   DynamoDB returns the pre- or post-update item.
22///   [`.return_new()`][UpdateItemRequest::return_new] returns `T` /
23///   `Item<TD>` directly (DynamoDB's `ALL_NEW` always provides the updated
24///   item). [`.return_old()`][UpdateItemRequest::return_old] returns
25///   `Option<T>` / `Option<Item<TD>>` because the item may not have existed
26///   before the update (DynamoDB's `UpdateItem` is an upsert). Note:
27///   [`DynamoDBItemOp::update_by_id`] starts with return-new by default.
28/// - **Condition** — optionally add a guard expression via
29///   [`.condition()`][UpdateItemRequest::condition],
30///   [`.exists()`][UpdateItemRequest::exists], or
31///   [`.not_exists()`][UpdateItemRequest::not_exists]. DynamoDB accepts a
32///   single condition expression per request, so this can only be called once.
33///
34/// The builder implements [`IntoFuture`], so it can
35/// be `.await`ed directly.
36///
37/// # Errors
38///
39/// Returns [`Err`] if the DynamoDB request fails, if a condition check
40/// fails, or if deserialization of the returned attributes fails.
41///
42/// # Examples
43///
44/// ```no_run
45/// # use dynamodb_facade::test_fixtures::*;
46/// use dynamodb_facade::{DynamoDBItemOp, Condition, KeyId, Update};
47///
48/// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
49/// # let client = cclient.clone();
50/// // Simple update by ID (returns the updated item by default)
51/// let updated /* : User */ = User::update_by_id(
52///     client,
53///     KeyId::pk("user-1"),
54///     Update::set("role", "instructor"),
55/// )
56/// .await?;
57///
58/// # let client = cclient.clone();
59/// // Update guarded by existence
60/// let updated /* : User */ = User::update_by_id(
61///     client,
62///     KeyId::pk("user-1"),
63///     Update::set("role", "instructor"),
64/// )
65/// .exists()
66/// .await?;
67///
68/// # let client = cclient.clone();
69/// // Update with a custom condition
70/// let updated /* : User */ = User::update_by_id(
71///     client,
72///     KeyId::pk("user-1"),
73///     Update::set("role", "instructor"),
74/// )
75/// .condition(Condition::eq("role", "student"))
76/// .await?;
77///
78/// # let client = cclient.clone();
79/// // Update without returning the item
80/// User::update_by_id(
81///     client,
82///     KeyId::pk("user-1"),
83///     Update::set("name", "Bob"),
84/// )
85/// .exists()
86/// .return_none()
87/// .await?;
88/// # Ok(())
89/// # }
90/// ```
91#[must_use = "builder does nothing until awaited or executed"]
92pub struct UpdateItemRequest<
93    TD: TableDefinition,
94    T = (),
95    O: OutputFormat = Raw,
96    R: ReturnValue = ReturnNothing,
97    C: ConditionState = NoCondition,
98> {
99    builder: UpdateItemFluentBuilder,
100    _marker: PhantomData<(TD, T, O, R, C)>,
101}
102
103// -- Common methods (all states) --------------------------------------------
104
105impl<TD: TableDefinition, T, O: OutputFormat, R: ReturnValue, C: ConditionState>
106    UpdateItemRequest<TD, T, O, R, C>
107{
108    /// Consumes the builder and returns the underlying SDK
109    /// [`UpdateItemFluentBuilder`].
110    ///
111    /// Use this escape hatch when you need to set options not exposed by this
112    /// facade, or when integrating with code that expects the raw SDK builder.
113    ///
114    /// # Examples
115    ///
116    /// ```no_run
117    /// # use dynamodb_facade::test_fixtures::*;
118    /// use dynamodb_facade::{DynamoDBItemOp, Update};
119    ///
120    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
121    /// let sdk_builder = sample_user()
122    ///     .update(client, Update::set("role", "instructor"))
123    ///     .into_inner();
124    /// // configure sdk_builder further, then call .send().await
125    /// # Ok(())
126    /// # }
127    /// ```
128    pub fn into_inner(self) -> UpdateItemFluentBuilder {
129        self.builder
130    }
131}
132
133// -- Stand-alone constructor (ReturnNothing, any C, T = (), O = Raw)
134
135impl<TD: TableDefinition> UpdateItemRequest<TD> {
136    /// Creates a stand-alone `UpdateItemRequest` with raw output (`T = ()`, `O = Raw`).
137    ///
138    /// Use this when you already have a [`Key<TD>`] and an [`Update`] expression
139    /// and do not need typed deserialization of the returned item. For typed
140    /// access, prefer [`DynamoDBItemOp::update`] or
141    /// [`DynamoDBItemOp::update_by_id`] instead.
142    ///
143    /// # Examples
144    ///
145    /// ```no_run
146    /// # use dynamodb_facade::test_fixtures::*;
147    /// use dynamodb_facade::{UpdateItemRequest, Update};
148    ///
149    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
150    /// let key = sample_user_item().into_key_only();
151    /// UpdateItemRequest::<PlatformTable>::new(client, key, Update::set("role", "instructor"))
152    ///     .await?;
153    /// # Ok(())
154    /// # }
155    /// ```
156    pub fn new(client: aws_sdk_dynamodb::Client, key: Key<TD>, update: Update<'_>) -> Self {
157        Self::_new(client, key, update)
158    }
159}
160
161// -- Constructor (any R, any O, any C) ------------------------------
162
163impl<TD: TableDefinition, T, O: OutputFormat, R: ReturnValue, C: ConditionState>
164    UpdateItemRequest<TD, T, O, R, C>
165{
166    /// Creates a new `UpdateItemRequest` targeting the given key and applying `update`.
167    pub(super) fn _new(client: aws_sdk_dynamodb::Client, key: Key<TD>, update: Update<'_>) -> Self {
168        let table_name = TD::table_name();
169        tracing::debug!(table_name, ?key, %update, "UpdateItem");
170        Self {
171            builder: update.apply(
172                client
173                    .update_item()
174                    .table_name(table_name)
175                    .set_key(Some(key.into_inner())),
176            ),
177            _marker: PhantomData,
178        }
179    }
180}
181
182// -- Return-value transitions (preserve O, C) -------------------------------
183
184// From ReturnNothing
185impl<TD: TableDefinition, T, O: OutputFormat, C: ConditionState>
186    UpdateItemRequest<TD, T, O, ReturnNothing, C>
187{
188    /// Requests that DynamoDB return the item's attributes before the update.
189    ///
190    /// When executed, [`execute`][UpdateItemRequest::execute] returns
191    /// `Option<T>` (typed) or `Option<Item<TD>>` (raw) containing the
192    /// pre-update state, or `None` if no item existed at the target key
193    /// prior to the update.
194    ///
195    /// # Examples
196    ///
197    /// ```no_run
198    /// # use dynamodb_facade::test_fixtures::*;
199    /// use dynamodb_facade::{DynamoDBItemOp, Update};
200    ///
201    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
202    /// let before /* : Option<User> */ = sample_user()
203    ///     .update(client, Update::set("role", "instructor"))
204    ///     .exists()
205    ///     .return_old()
206    ///     .await?;
207    /// # Ok(())
208    /// # }
209    /// ```
210    pub fn return_old(self) -> UpdateItemRequest<TD, T, O, Return<Old>, C> {
211        tracing::debug!("UpdateItem return_old");
212        UpdateItemRequest {
213            builder: self.builder,
214            _marker: PhantomData,
215        }
216    }
217
218    /// Requests that DynamoDB return the item's attributes after the update.
219    ///
220    /// When executed, [`execute`][UpdateItemRequest::execute] returns `T`
221    /// (typed) or [`Item<TD>`] (raw) containing the post-update state.
222    ///
223    /// # Examples
224    ///
225    /// ```no_run
226    /// # use dynamodb_facade::test_fixtures::*;
227    /// use dynamodb_facade::{DynamoDBItemOp, Update};
228    ///
229    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
230    /// let after /* : User */ = sample_user()
231    ///     .update(client, Update::set("role", "instructor"))
232    ///     .exists()
233    ///     .return_new()
234    ///     .await?;
235    /// # Ok(())
236    /// # }
237    /// ```
238    pub fn return_new(self) -> UpdateItemRequest<TD, T, O, Return<New>, C> {
239        tracing::debug!("UpdateItem return_new");
240        UpdateItemRequest {
241            builder: self.builder,
242            _marker: PhantomData,
243        }
244    }
245}
246
247// From ReturnItem<New>
248impl<TD: TableDefinition, T, O: OutputFormat, C: ConditionState>
249    UpdateItemRequest<TD, T, O, Return<New>, C>
250{
251    /// Switches from returning the post-update item to returning the
252    /// pre-update item.
253    ///
254    /// The [`execute`][UpdateItemRequest::execute] return type changes
255    /// from `T` / `Item<TD>` to `Option<T>` / `Option<Item<TD>>`,
256    /// because the old item may not exist if the update created it
257    /// (DynamoDB's `UpdateItem` is an upsert).
258    ///
259    /// # Examples
260    ///
261    /// ```no_run
262    /// # use dynamodb_facade::test_fixtures::*;
263    /// use dynamodb_facade::{DynamoDBItemOp, KeyId, Update};
264    ///
265    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
266    /// // update_by_id defaults to Return<New>; switch to Return<Old>
267    /// let before /* : Option<User> */ = User::update_by_id(
268    ///     client,
269    ///     KeyId::pk("user-1"),
270    ///     Update::set("role", "instructor"),
271    /// )
272    /// .exists()
273    /// .return_old()
274    /// .await?;
275    /// # Ok(())
276    /// # }
277    /// ```
278    pub fn return_old(self) -> UpdateItemRequest<TD, T, O, Return<Old>, C> {
279        tracing::debug!("UpdateItem return_old");
280        UpdateItemRequest {
281            builder: self.builder,
282            _marker: PhantomData,
283        }
284    }
285
286    /// Reverts the return-value setting so that nothing is returned.
287    ///
288    /// After this call, [`execute`][UpdateItemRequest::execute] returns `()`.
289    ///
290    /// # Examples
291    ///
292    /// ```no_run
293    /// # use dynamodb_facade::test_fixtures::*;
294    /// use dynamodb_facade::{DynamoDBItemOp, KeyId, Update};
295    ///
296    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
297    /// // update_by_id defaults to Return<New>; opt out
298    /// User::update_by_id(
299    ///     client,
300    ///     KeyId::pk("user-1"),
301    ///     Update::set("role", "instructor"),
302    /// )
303    /// .exists()
304    /// .return_none()
305    /// .await?;
306    /// # Ok(())
307    /// # }
308    /// ```
309    pub fn return_none(self) -> UpdateItemRequest<TD, T, O, ReturnNothing, C> {
310        tracing::debug!("UpdateItem return_none");
311        UpdateItemRequest {
312            builder: self.builder,
313            _marker: PhantomData,
314        }
315    }
316}
317
318// From ReturnItem<Old>
319impl<TD: TableDefinition, T, O: OutputFormat, C: ConditionState>
320    UpdateItemRequest<TD, T, O, Return<Old>, C>
321{
322    /// Switches from returning the pre-update item to returning the
323    /// post-update item.
324    ///
325    /// The [`execute`][UpdateItemRequest::execute] return type changes
326    /// from `Option<T>` / `Option<Item<TD>>` to `T` / `Item<TD>`,
327    /// because DynamoDB's `ALL_NEW` return mode always includes the full
328    /// item after the update.
329    ///
330    /// # Examples
331    ///
332    /// ```no_run
333    /// # use dynamodb_facade::test_fixtures::*;
334    /// use dynamodb_facade::{DynamoDBItemOp, Update};
335    ///
336    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
337    /// let after /* : User */ = sample_user()
338    ///     .update(client, Update::set("role", "instructor"))
339    ///     .exists()
340    ///     .return_old()
341    ///     .return_new()
342    ///     .await?;
343    /// # Ok(())
344    /// # }
345    /// ```
346    pub fn return_new(self) -> UpdateItemRequest<TD, T, O, Return<New>, C> {
347        tracing::debug!("UpdateItem return_new");
348        UpdateItemRequest {
349            builder: self.builder,
350            _marker: PhantomData,
351        }
352    }
353
354    /// Reverts the return-value setting so that nothing is returned.
355    ///
356    /// After this call, [`execute`][UpdateItemRequest::execute] returns `()`.
357    ///
358    /// # Examples
359    ///
360    /// ```no_run
361    /// # use dynamodb_facade::test_fixtures::*;
362    /// use dynamodb_facade::{DynamoDBItemOp, Update};
363    ///
364    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
365    /// sample_user()
366    ///     .update(client, Update::set("role", "instructor"))
367    ///     .exists()
368    ///     .return_old()
369    ///     .return_none()
370    ///     .await?;
371    /// # Ok(())
372    /// # }
373    /// ```
374    pub fn return_none(self) -> UpdateItemRequest<TD, T, O, ReturnNothing, C> {
375        tracing::debug!("UpdateItem return_none");
376        UpdateItemRequest {
377            builder: self.builder,
378            _marker: PhantomData,
379        }
380    }
381}
382
383// -- Condition (NoCondition only) -------------------------------------------
384
385impl<TD: TableDefinition, T, O: OutputFormat, R: ReturnValue>
386    UpdateItemRequest<TD, T, O, R, NoCondition>
387{
388    /// Adds a condition expression that must be satisfied for the update to succeed.
389    ///
390    /// DynamoDB accepts a single condition expression per request, so this
391    /// method can only be called once. If the condition fails at runtime,
392    /// DynamoDB returns a `ConditionalCheckFailedException`.
393    ///
394    /// For the common item exists/not_exists cases, prefer
395    /// the [`.exists()`][UpdateItemRequest::exists] and
396    /// [`.not_exists()`][UpdateItemRequest::not_exists] shorthands.
397    ///
398    /// # Examples
399    ///
400    /// ```no_run
401    /// # use dynamodb_facade::test_fixtures::*;
402    /// use dynamodb_facade::{DynamoDBItemOp, KeyId, Update, Condition};
403    ///
404    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
405    /// // Update role only if the current role is not "student"
406    /// User::update_by_id(
407    ///     client,
408    ///     KeyId::pk("user-1"),
409    ///     Update::set("role", "instructor"),
410    /// )
411    /// .condition(Condition::ne("role", "student"))
412    /// .await?;
413    /// # Ok(())
414    /// # }
415    /// ```
416    pub fn condition(
417        mut self,
418        condition: Condition<'_>,
419    ) -> UpdateItemRequest<TD, T, O, R, AlreadyHasCondition> {
420        tracing::debug!(%condition, "UpdateItem condition");
421        self.builder = condition.apply(self.builder);
422        UpdateItemRequest {
423            builder: self.builder,
424            _marker: PhantomData,
425        }
426    }
427}
428
429impl<TD: TableDefinition, T: DynamoDBItem<TD>, O: OutputFormat, R: ReturnValue>
430    UpdateItemRequest<TD, T, O, R, NoCondition>
431{
432    /// Adds an `attribute_exists(<PK>)` condition, requiring the item to exist before updating.
433    ///
434    /// The update fails with `ConditionalCheckFailedException` if the item does not exist.
435    ///
436    /// # Examples
437    ///
438    /// ```no_run
439    /// # use dynamodb_facade::test_fixtures::*;
440    /// use dynamodb_facade::{DynamoDBItemOp, KeyId, Update};
441    ///
442    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
443    /// User::update_by_id(
444    ///     client,
445    ///     KeyId::pk("user-1"),
446    ///     Update::set("name", "Bob"),
447    /// )
448    /// .exists()
449    /// .await?;
450    /// # Ok(())
451    /// # }
452    /// ```
453    pub fn exists(self) -> UpdateItemRequest<TD, T, O, R, AlreadyHasCondition> {
454        self.condition(T::exists())
455    }
456
457    /// Adds an `attribute_not_exists(<PK>)` condition, requiring the item to not yet exist.
458    ///
459    /// Useful for upsert-style operations where you want to initialize an item only
460    /// if it does not already exist.
461    ///
462    /// # Examples
463    ///
464    /// ```no_run
465    /// # use dynamodb_facade::test_fixtures::*;
466    /// use dynamodb_facade::{DynamoDBItemOp, KeyId, Update};
467    ///
468    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
469    /// User::update_by_id(
470    ///     client,
471    ///     KeyId::pk("user-1"),
472    ///     Update::set("role", "student"),
473    /// )
474    /// .not_exists()
475    /// .await?;
476    /// # Ok(())
477    /// # }
478    /// ```
479    pub fn not_exists(self) -> UpdateItemRequest<TD, T, O, R, AlreadyHasCondition> {
480        self.condition(T::not_exists())
481    }
482}
483
484// -- Output format transition (preserve R, C) -------------------------------
485
486impl<TD: TableDefinition, T, R: ReturnValue, C: ConditionState>
487    UpdateItemRequest<TD, T, Typed, R, C>
488{
489    /// Switches the output format from `Typed` to `Raw`.
490    ///
491    /// After calling `.raw()`, [`execute`][UpdateItemRequest::execute] returns
492    /// [`Item<TD>`] instead of `T` when a return value is requested.
493    /// This transition is one-way.
494    ///
495    /// # Examples
496    ///
497    /// ```no_run
498    /// # use dynamodb_facade::test_fixtures::*;
499    /// use dynamodb_facade::{DynamoDBItemOp, KeyId, Update};
500    ///
501    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
502    /// let raw_new = User::update_by_id(
503    ///     client,
504    ///     KeyId::pk("user-1"),
505    ///     Update::set("role", "instructor"),
506    /// )
507    /// .exists()
508    /// .raw()
509    /// .await?;
510    /// // raw_new: Item<PlatformTable>
511    /// # Ok(())
512    /// # }
513    /// ```
514    pub fn raw(self) -> UpdateItemRequest<TD, T, Raw, R, C> {
515        UpdateItemRequest {
516            builder: self.builder,
517            _marker: PhantomData,
518        }
519    }
520}
521
522// -- Terminal: ReturnNothing (any O, any C) ---------------------------------
523
524impl<TD: TableDefinition, T, O: OutputFormat, C: ConditionState>
525    UpdateItemRequest<TD, T, O, ReturnNothing, C>
526{
527    /// Sends the `UpdateItem` request, returning nothing on success.
528    ///
529    /// This method is also available implicitly via `.await`.
530    ///
531    /// # Errors
532    ///
533    /// Returns [`Err`] if the DynamoDB request fails or if a condition
534    /// expression is set and the check fails
535    /// (`ConditionalCheckFailedException`).
536    ///
537    /// # Examples
538    ///
539    /// ```no_run
540    /// # use dynamodb_facade::test_fixtures::*;
541    /// use dynamodb_facade::{DynamoDBItemOp, KeyId, Update};
542    ///
543    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
544    /// User::update_by_id(
545    ///     client,
546    ///     KeyId::pk("user-1"),
547    ///     Update::set("name", "Bob"),
548    /// )
549    /// .exists()
550    /// .return_none()
551    /// .execute()
552    /// .await?;
553    /// # Ok(())
554    /// # }
555    /// ```
556    #[tracing::instrument(level = "debug", skip(self), name = "update_execute")]
557    pub fn execute(self) -> impl Future<Output = Result<()>> + Send + 'static {
558        let builder = self.builder;
559        async move {
560            builder.return_values(SDKReturnValue::None).send().await?;
561            Ok(())
562        }
563    }
564}
565
566impl<TD: TableDefinition, T, O: OutputFormat, C: ConditionState> IntoFuture
567    for UpdateItemRequest<TD, T, O, ReturnNothing, C>
568{
569    type Output = Result<()>;
570    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
571
572    fn into_future(self) -> Self::IntoFuture {
573        Box::pin(self.execute())
574    }
575}
576
577// -- Terminal: ReturnItem<Old> + Typed (any C) -------------------------------
578
579impl<TD: TableDefinition, T: DynamoDBItem<TD> + DeserializeOwned, C: ConditionState>
580    UpdateItemRequest<TD, T, Typed, Return<Old>, C>
581{
582    /// Sends the `UpdateItem` request and returns the pre-update item
583    /// deserialized as `Option<T>`.
584    ///
585    /// Returns `Some(T)` containing the item's state **before** the update
586    /// was applied, or `None` if no item existed at the target key prior to
587    /// the update (DynamoDB's `UpdateItem` is an upsert — it creates the
588    /// item if absent).
589    ///
590    /// This method is also available implicitly via `.await`.
591    ///
592    /// # Errors
593    ///
594    /// Returns [`Err`] if the DynamoDB request fails, if a condition check
595    /// fails, or if deserialization of the returned attributes fails.
596    ///
597    /// # Examples
598    ///
599    /// ```no_run
600    /// # use dynamodb_facade::test_fixtures::*;
601    /// use dynamodb_facade::{DynamoDBItemOp, KeyId, Update};
602    ///
603    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
604    /// let before /* : Option<User> */ = User::update_by_id(
605    ///     client,
606    ///     KeyId::pk("user-1"),
607    ///     Update::set("role", "instructor"),
608    /// )
609    /// .exists()
610    /// .return_old()
611    /// .execute()
612    /// .await?;
613    /// # Ok(())
614    /// # }
615    /// ```
616    #[tracing::instrument(level = "debug", skip(self), name = "update_execute_old")]
617    pub fn execute(self) -> impl Future<Output = Result<Option<T>>> + Send + 'static {
618        let builder = self.builder;
619        async move {
620            let out = builder.return_values(Old::return_value()).send().await?;
621
622            out.attributes
623                .map(Item::from_dynamodb_response)
624                .map(T::try_from_item)
625                .transpose()
626        }
627    }
628}
629
630impl<TD: TableDefinition, T: DynamoDBItem<TD> + DeserializeOwned, C: ConditionState> IntoFuture
631    for UpdateItemRequest<TD, T, Typed, Return<Old>, C>
632{
633    type Output = Result<Option<T>>;
634    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
635
636    fn into_future(self) -> Self::IntoFuture {
637        Box::pin(self.execute())
638    }
639}
640
641// -- Terminal: ReturnItem<New> + Typed (any C) -------------------------------
642
643impl<TD: TableDefinition, T: DynamoDBItem<TD> + DeserializeOwned, C: ConditionState>
644    UpdateItemRequest<TD, T, Typed, Return<New>, C>
645{
646    /// Sends the `UpdateItem` request and returns the post-update item
647    /// deserialized as `T`.
648    ///
649    /// Because DynamoDB's `ALL_NEW` return mode always includes the full
650    /// item after the update, this method returns `T` directly (not
651    /// `Option<T>`).
652    ///
653    /// This method is also available implicitly via `.await`.
654    ///
655    /// # Panics
656    ///
657    /// Panics if DynamoDB does not return attributes in the response. This
658    /// should not happen when `ALL_NEW` is requested, but could indicate a
659    /// bug in the SDK or an unexpected API change.
660    ///
661    /// # Errors
662    ///
663    /// Returns [`Err`] if the DynamoDB request fails, if a condition check
664    /// fails, or if deserialization of the returned attributes fails.
665    ///
666    /// # Examples
667    ///
668    /// ```no_run
669    /// # use dynamodb_facade::test_fixtures::*;
670    /// use dynamodb_facade::{DynamoDBItemOp, KeyId, Update};
671    ///
672    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
673    /// let updated /* : User */ = User::update_by_id(
674    ///     client,
675    ///     KeyId::pk("user-1"),
676    ///     Update::set("role", "instructor"),
677    /// )
678    /// .exists()
679    /// .execute()
680    /// .await?;
681    /// # Ok(())
682    /// # }
683    /// ```
684    #[tracing::instrument(level = "debug", skip(self), name = "update_execute_new")]
685    pub fn execute(self) -> impl Future<Output = Result<T>> + Send + 'static {
686        let builder = self.builder;
687        async move {
688            let out = builder.return_values(New::return_value()).send().await?;
689
690            out.attributes
691                .map(Item::from_dynamodb_response)
692                .map(T::try_from_item)
693                .expect("asked to return something")
694        }
695    }
696}
697
698impl<TD: TableDefinition, T: DynamoDBItem<TD> + DeserializeOwned, C: ConditionState> IntoFuture
699    for UpdateItemRequest<TD, T, Typed, Return<New>, C>
700{
701    type Output = Result<T>;
702    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
703
704    fn into_future(self) -> Self::IntoFuture {
705        Box::pin(self.execute())
706    }
707}
708
709// -- Terminal: ReturnItem<Old> + Raw (any C) ---------------------------------
710
711impl<TD: TableDefinition, T, C: ConditionState> UpdateItemRequest<TD, T, Raw, Return<Old>, C> {
712    /// Sends the `UpdateItem` request and returns the pre-update raw item
713    /// map as `Option<Item<TD>>`.
714    ///
715    /// Returns `Some(Item<TD>)` containing the item's state **before** the
716    /// update was applied, or `None` if no item existed at the target key
717    /// prior to the update (DynamoDB's `UpdateItem` is an upsert — it
718    /// creates the item if absent).
719    ///
720    /// This method is also available implicitly via `.await`.
721    ///
722    /// # Errors
723    ///
724    /// Returns [`Err`] if the DynamoDB request fails or if a condition check
725    /// fails.
726    ///
727    /// # Examples
728    ///
729    /// ```no_run
730    /// # use dynamodb_facade::test_fixtures::*;
731    /// use dynamodb_facade::{DynamoDBItemOp, KeyId, Update};
732    ///
733    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
734    /// let raw /* : Option<dynamodb_facade::Item<PlatformTable>> */ = User::update_by_id(
735    ///     client,
736    ///     KeyId::pk("user-1"),
737    ///     Update::set("role", "instructor"),
738    /// )
739    /// .exists()
740    /// .return_old()
741    /// .raw()
742    /// .execute()
743    /// .await?;
744    /// # Ok(())
745    /// # }
746    /// ```
747    #[tracing::instrument(level = "debug", skip(self), name = "update_execute_old_raw")]
748    pub fn execute(self) -> impl Future<Output = Result<Option<Item<TD>>>> + Send + 'static {
749        let builder = self.builder;
750        async move {
751            let out = builder.return_values(Old::return_value()).send().await?;
752
753            Ok(out.attributes.map(Item::from_dynamodb_response))
754        }
755    }
756}
757
758impl<TD: TableDefinition, T, C: ConditionState> IntoFuture
759    for UpdateItemRequest<TD, T, Raw, Return<Old>, C>
760{
761    type Output = Result<Option<Item<TD>>>;
762    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
763
764    fn into_future(self) -> Self::IntoFuture {
765        Box::pin(self.execute())
766    }
767}
768
769// -- Terminal: ReturnItem<New> + Raw (any C) ---------------------------------
770
771impl<TD: TableDefinition, T, C: ConditionState> UpdateItemRequest<TD, T, Raw, Return<New>, C> {
772    /// Sends the `UpdateItem` request and returns the post-update raw item
773    /// map as `Item<TD>`.
774    ///
775    /// Because DynamoDB's `ALL_NEW` return mode always includes the full
776    /// item after the update, this method returns `Item<TD>` directly (not
777    /// `Option<Item<TD>>`).
778    ///
779    /// This method is also available implicitly via `.await`.
780    ///
781    /// # Panics
782    ///
783    /// Panics if DynamoDB does not return attributes in the response. This
784    /// should not happen when `ALL_NEW` is requested, but could indicate a
785    /// bug in the SDK or an unexpected API change.
786    ///
787    /// # Errors
788    ///
789    /// Returns [`Err`] if the DynamoDB request fails or if a condition check
790    /// fails.
791    ///
792    /// # Examples
793    ///
794    /// ```no_run
795    /// # use dynamodb_facade::test_fixtures::*;
796    /// use dynamodb_facade::{DynamoDBItemOp, KeyId, Update};
797    ///
798    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
799    /// let raw /* : dynamodb_facade::Item<PlatformTable> */ = User::update_by_id(
800    ///     client,
801    ///     KeyId::pk("user-1"),
802    ///     Update::set("role", "instructor"),
803    /// )
804    /// .exists()
805    /// .raw()
806    /// .execute()
807    /// .await?;
808    /// assert!(raw.get("role").is_some());
809    /// # Ok(())
810    /// # }
811    /// ```
812    #[tracing::instrument(level = "debug", skip(self), name = "update_execute_new_raw")]
813    pub fn execute(self) -> impl Future<Output = Result<Item<TD>>> + Send + 'static {
814        let builder = self.builder;
815        async move {
816            let out = builder.return_values(New::return_value()).send().await?;
817
818            Ok(out
819                .attributes
820                .map(Item::from_dynamodb_response)
821                .expect("asked to return something"))
822        }
823    }
824}
825
826impl<TD: TableDefinition, T, C: ConditionState> IntoFuture
827    for UpdateItemRequest<TD, T, Raw, Return<New>, C>
828{
829    type Output = Result<Item<TD>>;
830    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
831
832    fn into_future(self) -> Self::IntoFuture {
833        Box::pin(self.execute())
834    }
835}